#include "luamanagerimp.hpp" #include #include #include #include #include #include #include #include #include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/ptr.hpp" #include "luabindings.hpp" #include "userdataserializer.hpp" namespace MWLua { LuaManager::LuaManager(const VFS::Manager* vfs, const std::string& libsDir) : mLua(vfs, &mConfiguration) , mUiResourceManager(vfs) , mI18n(vfs, &mLua) { Log(Debug::Info) << "Lua version: " << LuaUtil::getLuaVersion(); mLua.addInternalLibSearchPath(libsDir); mGlobalSerializer = createUserdataSerializer(false, mWorldView.getObjectRegistry()); mLocalSerializer = createUserdataSerializer(true, mWorldView.getObjectRegistry()); mGlobalLoader = createUserdataSerializer(false, mWorldView.getObjectRegistry(), &mContentFileMapping); mLocalLoader = createUserdataSerializer(true, mWorldView.getObjectRegistry(), &mContentFileMapping); mGlobalScripts.setSerializer(mGlobalSerializer.get()); } void LuaManager::initConfiguration() { mConfiguration.init(MWBase::Environment::get().getWorld()->getStore().getLuaScriptsCfg()); Log(Debug::Verbose) << "Lua scripts configuration (" << mConfiguration.size() << " scripts):"; for (size_t i = 0; i < mConfiguration.size(); ++i) Log(Debug::Verbose) << "#" << i << " " << LuaUtil::scriptCfgToString(mConfiguration[i]); } void LuaManager::init() { Context context; context.mIsGlobal = true; context.mLuaManager = this; context.mLua = &mLua; context.mI18n = &mI18n; context.mWorldView = &mWorldView; context.mLocalEventQueue = &mLocalEvents; context.mGlobalEventQueue = &mGlobalEvents; context.mSerializer = mGlobalSerializer.get(); Context localContext = context; localContext.mIsGlobal = false; localContext.mSerializer = mLocalSerializer.get(); mI18n.init(); std::vector preferredLanguages; Misc::StringUtils::split(Settings::Manager::getString("i18n preferred languages", "Lua"), preferredLanguages, ", "); mI18n.setPreferredLanguages(preferredLanguages); initObjectBindingsForGlobalScripts(context); initCellBindingsForGlobalScripts(context); initObjectBindingsForLocalScripts(localContext); initCellBindingsForLocalScripts(localContext); LocalScripts::initializeSelfPackage(localContext); LuaUtil::LuaStorage::initLuaBindings(mLua.sol()); mLua.addCommonPackage("openmw.async", getAsyncPackageInitializer(context)); mLua.addCommonPackage("openmw.util", LuaUtil::initUtilPackage(mLua.sol())); mLua.addCommonPackage("openmw.core", initCorePackage(context)); mLua.addCommonPackage("openmw.query", initQueryPackage(context)); mGlobalScripts.addPackage("openmw.world", initWorldPackage(context)); mGlobalScripts.addPackage("openmw.settings", initGlobalSettingsPackage(context)); mGlobalScripts.addPackage("openmw.storage", initGlobalStoragePackage(context, &mGlobalStorage)); mCameraPackage = initCameraPackage(localContext); mUserInterfacePackage = initUserInterfacePackage(localContext); mInputPackage = initInputPackage(localContext); mNearbyPackage = initNearbyPackage(localContext); mLocalSettingsPackage = initGlobalSettingsPackage(localContext); mPlayerSettingsPackage = initPlayerSettingsPackage(localContext); mLocalStoragePackage = initLocalStoragePackage(localContext, &mGlobalStorage); mPlayerStoragePackage = initPlayerStoragePackage(localContext, &mGlobalStorage, &mPlayerStorage); initConfiguration(); mInitialized = true; } void LuaManager::loadPermanentStorage(const std::string& userConfigPath) { auto globalPath = std::filesystem::path(userConfigPath) / "global_storage.bin"; auto playerPath = std::filesystem::path(userConfigPath) / "player_storage.bin"; if (std::filesystem::exists(globalPath)) mGlobalStorage.load(globalPath.string()); if (std::filesystem::exists(playerPath)) mPlayerStorage.load(playerPath.string()); } void LuaManager::savePermanentStorage(const std::string& userConfigPath) { std::filesystem::path confDir(userConfigPath); mGlobalStorage.save((confDir / "global_storage.bin").string()); mPlayerStorage.save((confDir / "player_storage.bin").string()); } void LuaManager::update() { static const bool luaDebug = Settings::Manager::getBool("lua debug", "Lua"); if (mPlayer.isEmpty()) return; // The game is not started yet. float frameDuration = MWBase::Environment::get().getFrameDuration(); ObjectRegistry* objectRegistry = mWorldView.getObjectRegistry(); MWWorld::Ptr newPlayerPtr = MWBase::Environment::get().getWorld()->getPlayerPtr(); if (!(getId(mPlayer) == getId(newPlayerPtr))) throw std::logic_error("Player Refnum was changed unexpectedly"); if (!mPlayer.isInCell() || !newPlayerPtr.isInCell() || mPlayer.getCell() != newPlayerPtr.getCell()) { mPlayer = newPlayerPtr; // player was moved to another cell, update ptr in registry objectRegistry->registerPtr(mPlayer); } mWorldView.update(); std::vector globalEvents = std::move(mGlobalEvents); std::vector localEvents = std::move(mLocalEvents); mGlobalEvents = std::vector(); mLocalEvents = std::vector(); if (!mWorldView.isPaused()) { // Update time and process timers double simulationTime = mWorldView.getSimulationTime() + frameDuration; mWorldView.setSimulationTime(simulationTime); double gameTime = mWorldView.getGameTime(); mGlobalScripts.processTimers(simulationTime, gameTime); for (LocalScripts* scripts : mActiveLocalScripts) scripts->processTimers(simulationTime, gameTime); } // Receive events for (GlobalEvent& e : globalEvents) mGlobalScripts.receiveEvent(e.mEventName, e.mEventData); for (LocalEvent& e : localEvents) { LObject obj(e.mDest, objectRegistry); LocalScripts* scripts = obj.isValid() ? obj.ptr().getRefData().getLuaScripts() : nullptr; if (scripts) scripts->receiveEvent(e.mEventName, e.mEventData); else Log(Debug::Debug) << "Ignored event " << e.mEventName << " to L" << idToString(e.mDest) << ". Object not found or has no attached scripts"; } // Run queued callbacks for (CallbackWithData& c : mQueuedCallbacks) c.mCallback(c.mArg); mQueuedCallbacks.clear(); // Engine handlers in local scripts for (const LocalEngineEvent& e : mLocalEngineEvents) { LObject obj(e.mDest, objectRegistry); if (!obj.isValid()) { if (luaDebug) Log(Debug::Verbose) << "Can not call engine handlers: object" << idToString(e.mDest) << " is not found"; continue; } LocalScripts* scripts = obj.ptr().getRefData().getLuaScripts(); if (scripts) scripts->receiveEngineEvent(e.mEvent); } mLocalEngineEvents.clear(); if (!mWorldView.isPaused()) { for (LocalScripts* scripts : mActiveLocalScripts) scripts->update(frameDuration); } // Engine handlers in global scripts if (mPlayerChanged) { mPlayerChanged = false; mGlobalScripts.playerAdded(GObject(getId(mPlayer), objectRegistry)); } if (mNewGameStarted) { mNewGameStarted = false; mGlobalScripts.newGameStarted(); } for (ObjectId id : mActorAddedEvents) { GObject obj(id, objectRegistry); if (obj.isValid()) mGlobalScripts.actorActive(obj); else if (luaDebug) Log(Debug::Verbose) << "Can not call onActorActive engine handler: object" << idToString(id) << " is already removed"; } mActorAddedEvents.clear(); if (!mWorldView.isPaused()) mGlobalScripts.update(frameDuration); } void LuaManager::synchronizedUpdate() { if (mPlayer.isEmpty()) return; // The game is not started yet. // We apply input events in `synchronizedUpdate` rather than in `update` in order to reduce input latency. PlayerScripts* playerScripts = dynamic_cast(mPlayer.getRefData().getLuaScripts()); if (playerScripts && !MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_MainMenu)) { for (const auto& event : mInputEvents) playerScripts->processInputEvent(event); } mInputEvents.clear(); if (playerScripts && !mWorldView.isPaused()) playerScripts->inputUpdate(MWBase::Environment::get().getFrameDuration()); MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); for (const std::string& message : mUIMessages) windowManager->messageBox(message); mUIMessages.clear(); for (std::unique_ptr& action : mActionQueue) action->safeApply(mWorldView); mActionQueue.clear(); if (mTeleportPlayerAction) mTeleportPlayerAction->safeApply(mWorldView); mTeleportPlayerAction.reset(); } void LuaManager::clear() { LuaUi::clearUserInterface(); mUiResourceManager.clear(); mActiveLocalScripts.clear(); mLocalEvents.clear(); mGlobalEvents.clear(); mInputEvents.clear(); mActorAddedEvents.clear(); mLocalEngineEvents.clear(); mNewGameStarted = false; mPlayerChanged = false; mWorldView.clear(); mGlobalScripts.removeAllScripts(); mGlobalScriptsStarted = false; if (!mPlayer.isEmpty()) { mPlayer.getCellRef().unsetRefNum(); mPlayer.getRefData().setLuaScripts(nullptr); mPlayer = MWWorld::Ptr(); } mGlobalStorage.clearTemporary(); mPlayerStorage.clearTemporary(); } void LuaManager::setupPlayer(const MWWorld::Ptr& ptr) { if (!mInitialized) return; if (!mPlayer.isEmpty()) throw std::logic_error("Player is initialized twice"); mWorldView.objectAddedToScene(ptr); mPlayer = ptr; LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); if (!localScripts) { localScripts = createLocalScripts(ptr, ESM::LuaScriptCfg::sPlayer); localScripts->addAutoStartedScripts(); } mActiveLocalScripts.insert(localScripts); mLocalEngineEvents.push_back({getId(ptr), LocalScripts::OnActive{}}); mPlayerChanged = true; } void LuaManager::newGameStarted() { mNewGameStarted = true; mInputEvents.clear(); mGlobalScripts.addAutoStartedScripts(); mGlobalScriptsStarted = true; } void LuaManager::gameLoaded() { if (!mGlobalScriptsStarted) mGlobalScripts.addAutoStartedScripts(); mGlobalScriptsStarted = true; } void LuaManager::objectAddedToScene(const MWWorld::Ptr& ptr) { mWorldView.objectAddedToScene(ptr); // assigns generated RefNum if it is not set yet. LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); if (!localScripts) { ESM::LuaScriptCfg::Flags flag = getLuaScriptFlag(ptr); if (!mConfiguration.getListByFlag(flag).empty()) { localScripts = createLocalScripts(ptr, flag); localScripts->addAutoStartedScripts(); // TODO: put to a queue and apply on next `update()` } } if (localScripts) { mActiveLocalScripts.insert(localScripts); mLocalEngineEvents.push_back({getId(ptr), LocalScripts::OnActive{}}); } if (ptr.getClass().isActor() && ptr != mPlayer) mActorAddedEvents.push_back(getId(ptr)); } void LuaManager::objectRemovedFromScene(const MWWorld::Ptr& ptr) { mWorldView.objectRemovedFromScene(ptr); LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); if (localScripts) { mActiveLocalScripts.erase(localScripts); if (!mWorldView.getObjectRegistry()->getPtr(getId(ptr), true).isEmpty()) mLocalEngineEvents.push_back({getId(ptr), LocalScripts::OnInactive{}}); } } void LuaManager::registerObject(const MWWorld::Ptr& ptr) { mWorldView.getObjectRegistry()->registerPtr(ptr); } void LuaManager::deregisterObject(const MWWorld::Ptr& ptr) { mWorldView.getObjectRegistry()->deregisterPtr(ptr); } void LuaManager::appliedToObject(const MWWorld::Ptr& toPtr, std::string_view recordId, const MWWorld::Ptr& fromPtr) { mLocalEngineEvents.push_back({getId(toPtr), LocalScripts::OnConsume{std::string(recordId)}}); } void LuaManager::objectActivated(const MWWorld::Ptr& object, const MWWorld::Ptr& actor) { mLocalEngineEvents.push_back({getId(object), LocalScripts::OnActivated{LObject(getId(actor), mWorldView.getObjectRegistry())}}); } MWBase::LuaManager::ActorControls* LuaManager::getActorControls(const MWWorld::Ptr& ptr) const { LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); if (!localScripts) return nullptr; return localScripts->getActorControls(); } void LuaManager::addCustomLocalScript(const MWWorld::Ptr& ptr, int scriptId) { LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); if (!localScripts) { localScripts = createLocalScripts(ptr, getLuaScriptFlag(ptr)); localScripts->addAutoStartedScripts(); if (ptr.isInCell() && MWBase::Environment::get().getWorld()->isCellActive(ptr.getCell())) mActiveLocalScripts.insert(localScripts); } localScripts->addCustomScript(scriptId); } LocalScripts* LuaManager::createLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScriptCfg::Flags flag) { assert(mInitialized); assert(flag != ESM::LuaScriptCfg::sGlobal); std::shared_ptr scripts; if (flag == ESM::LuaScriptCfg::sPlayer) { assert(ptr.getCellRef().getRefId() == "player"); scripts = std::make_shared(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry())); scripts->addPackage("openmw.ui", mUserInterfacePackage); scripts->addPackage("openmw.camera", mCameraPackage); scripts->addPackage("openmw.input", mInputPackage); scripts->addPackage("openmw.settings", mPlayerSettingsPackage); scripts->addPackage("openmw.storage", mPlayerStoragePackage); } else { scripts = std::make_shared(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry()), flag); scripts->addPackage("openmw.settings", mLocalSettingsPackage); scripts->addPackage("openmw.storage", mLocalStoragePackage); } scripts->addPackage("openmw.nearby", mNearbyPackage); scripts->setSerializer(mLocalSerializer.get()); MWWorld::RefData& refData = ptr.getRefData(); refData.setLuaScripts(std::move(scripts)); return refData.getLuaScripts(); } void LuaManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) { writer.startRecord(ESM::REC_LUAM); mWorldView.save(writer); ESM::LuaScripts globalScripts; mGlobalScripts.save(globalScripts); globalScripts.save(writer); saveEvents(writer, mGlobalEvents, mLocalEvents); writer.endRecord(ESM::REC_LUAM); } void LuaManager::readRecord(ESM::ESMReader& reader, uint32_t type) { if (type != ESM::REC_LUAM) throw std::runtime_error("ESM::REC_LUAM is expected"); mWorldView.load(reader); ESM::LuaScripts globalScripts; globalScripts.load(reader); loadEvents(mLua.sol(), reader, mGlobalEvents, mLocalEvents, mContentFileMapping, mGlobalLoader.get()); mGlobalScripts.setSerializer(mGlobalLoader.get()); mGlobalScripts.load(globalScripts); mGlobalScripts.setSerializer(mGlobalSerializer.get()); mGlobalScriptsStarted = true; } void LuaManager::saveLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScripts& data) { if (ptr.getRefData().getLuaScripts()) ptr.getRefData().getLuaScripts()->save(data); else data.mScripts.clear(); } void LuaManager::loadLocalScripts(const MWWorld::Ptr& ptr, const ESM::LuaScripts& data) { if (data.mScripts.empty()) { if (ptr.getRefData().getLuaScripts()) ptr.getRefData().setLuaScripts(nullptr); return; } mWorldView.getObjectRegistry()->registerPtr(ptr); LocalScripts* scripts = createLocalScripts(ptr, getLuaScriptFlag(ptr)); scripts->setSerializer(mLocalLoader.get()); scripts->load(data); scripts->setSerializer(mLocalSerializer.get()); // LiveCellRef is usually copied after loading, so this Ptr will become invalid and should be deregistered. mWorldView.getObjectRegistry()->deregisterPtr(ptr); } void LuaManager::reloadAllScripts() { Log(Debug::Info) << "Reload Lua"; LuaUi::clearUserInterface(); mUiResourceManager.clear(); mLua.dropScriptCache(); initConfiguration(); { // Reload global scripts ESM::LuaScripts data; mGlobalScripts.save(data); mGlobalScripts.load(data); } for (const auto& [id, ptr] : mWorldView.getObjectRegistry()->mObjectMapping) { // Reload local scripts LocalScripts* scripts = ptr.getRefData().getLuaScripts(); if (scripts == nullptr) continue; ESM::LuaScripts data; scripts->save(data); scripts->load(data); } for (LocalScripts* scripts : mActiveLocalScripts) scripts->receiveEngineEvent(LocalScripts::OnActive()); } }