From 9adc1902092f32b8dc79c193f3063f72c207cb09 Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Sat, 25 Sep 2021 10:46:47 +0200 Subject: [PATCH] Redesign LuaUtil::ScriptsContainer to work with ScriptsConfiguration --- apps/openmw/mwbase/luamanager.hpp | 1 + apps/openmw/mwlua/asyncbindings.cpp | 12 +- apps/openmw/mwlua/globalscripts.hpp | 3 +- apps/openmw/mwlua/localscripts.cpp | 6 +- apps/openmw/mwlua/localscripts.hpp | 4 +- apps/openmw/mwlua/luabindings.cpp | 2 +- apps/openmw/mwlua/luabindings.hpp | 2 +- apps/openmw/mwlua/luamanagerimp.cpp | 100 +++- apps/openmw/mwlua/luamanagerimp.hpp | 21 +- apps/openmw/mwlua/objectbindings.cpp | 38 +- apps/openmw/mwlua/playerscripts.hpp | 2 +- apps/openmw/mwstate/statemanagerimp.cpp | 2 + apps/openmw_test_suite/lua/test_lua.cpp | 5 +- .../lua/test_omwscriptsparser.cpp | 59 --- .../lua/test_scriptscontainer.cpp | 197 +++++--- components/CMakeLists.txt | 2 +- components/lua/luastate.cpp | 13 +- components/lua/luastate.hpp | 21 +- components/lua/omwscriptsparser.cpp | 44 -- components/lua/omwscriptsparser.hpp | 14 - components/lua/scriptscontainer.cpp | 451 +++++++++++------- components/lua/scriptscontainer.hpp | 105 ++-- 22 files changed, 638 insertions(+), 466 deletions(-) delete mode 100644 apps/openmw_test_suite/lua/test_omwscriptsparser.cpp delete mode 100644 components/lua/omwscriptsparser.cpp delete mode 100644 components/lua/omwscriptsparser.hpp diff --git a/apps/openmw/mwbase/luamanager.hpp b/apps/openmw/mwbase/luamanager.hpp index ebcd8f50b3..cc479a2937 100644 --- a/apps/openmw/mwbase/luamanager.hpp +++ b/apps/openmw/mwbase/luamanager.hpp @@ -30,6 +30,7 @@ namespace MWBase virtual ~LuaManager() = default; virtual void newGameStarted() = 0; + virtual void gameLoaded() = 0; virtual void registerObject(const MWWorld::Ptr& ptr) = 0; virtual void deregisterObject(const MWWorld::Ptr& ptr) = 0; virtual void objectAddedToScene(const MWWorld::Ptr& ptr) = 0; diff --git a/apps/openmw/mwlua/asyncbindings.cpp b/apps/openmw/mwlua/asyncbindings.cpp index 9fdda53d9d..d438518452 100644 --- a/apps/openmw/mwlua/asyncbindings.cpp +++ b/apps/openmw/mwlua/asyncbindings.cpp @@ -23,7 +23,7 @@ namespace MWLua sol::usertype api = context.mLua->sol().new_usertype("AsyncPackage"); api["registerTimerCallback"] = [](const AsyncPackageId& asyncId, std::string_view name, sol::function callback) { - asyncId.mContainer->registerTimerCallback(asyncId.mScript, name, std::move(callback)); + asyncId.mContainer->registerTimerCallback(asyncId.mScriptId, name, std::move(callback)); return TimerCallback{asyncId, std::string(name)}; }; api["newTimerInSeconds"] = [world=context.mWorldView](const AsyncPackageId&, double delay, @@ -31,24 +31,24 @@ namespace MWLua { callback.mAsyncId.mContainer->setupSerializableTimer( TimeUnit::SECONDS, world->getGameTimeInSeconds() + delay, - callback.mAsyncId.mScript, callback.mName, std::move(callbackArg)); + callback.mAsyncId.mScriptId, callback.mName, std::move(callbackArg)); }; api["newTimerInHours"] = [world=context.mWorldView](const AsyncPackageId&, double delay, const TimerCallback& callback, sol::object callbackArg) { callback.mAsyncId.mContainer->setupSerializableTimer( TimeUnit::HOURS, world->getGameTimeInHours() + delay, - callback.mAsyncId.mScript, callback.mName, std::move(callbackArg)); + callback.mAsyncId.mScriptId, callback.mName, std::move(callbackArg)); }; api["newUnsavableTimerInSeconds"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback) { asyncId.mContainer->setupUnsavableTimer( - TimeUnit::SECONDS, world->getGameTimeInSeconds() + delay, asyncId.mScript, std::move(callback)); + TimeUnit::SECONDS, world->getGameTimeInSeconds() + delay, asyncId.mScriptId, std::move(callback)); }; api["newUnsavableTimerInHours"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback) { asyncId.mContainer->setupUnsavableTimer( - TimeUnit::HOURS, world->getGameTimeInHours() + delay, asyncId.mScript, std::move(callback)); + TimeUnit::HOURS, world->getGameTimeInHours() + delay, asyncId.mScriptId, std::move(callback)); }; api["callback"] = [](const AsyncPackageId& asyncId, sol::function fn) { @@ -59,7 +59,7 @@ namespace MWLua { LuaUtil::ScriptsContainer::ScriptId id = hiddenData[LuaUtil::ScriptsContainer::ScriptId::KEY]; hiddenData[Callback::SCRIPT_NAME_KEY] = id.toString(); - return AsyncPackageId{id.mContainer, id.mPath, hiddenData}; + return AsyncPackageId{id.mContainer, id.mIndex, hiddenData}; }; return sol::make_object(context.mLua->sol(), initializer); } diff --git a/apps/openmw/mwlua/globalscripts.hpp b/apps/openmw/mwlua/globalscripts.hpp index 9a371809ac..2737dabaca 100644 --- a/apps/openmw/mwlua/globalscripts.hpp +++ b/apps/openmw/mwlua/globalscripts.hpp @@ -16,7 +16,8 @@ namespace MWLua class GlobalScripts : public LuaUtil::ScriptsContainer { public: - GlobalScripts(LuaUtil::LuaState* lua) : LuaUtil::ScriptsContainer(lua, "Global") + GlobalScripts(LuaUtil::LuaState* lua) : + LuaUtil::ScriptsContainer(lua, "Global", ESM::LuaScriptCfg::sGlobal) { registerEngineHandlers({&mActorActiveHandlers, &mNewGameHandlers, &mPlayerAddedHandlers}); } diff --git a/apps/openmw/mwlua/localscripts.cpp b/apps/openmw/mwlua/localscripts.cpp index 8a1b76a8ce..ee23b4b90c 100644 --- a/apps/openmw/mwlua/localscripts.cpp +++ b/apps/openmw/mwlua/localscripts.cpp @@ -82,14 +82,14 @@ namespace MWLua }; } - LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj) - : LuaUtil::ScriptsContainer(lua, "L" + idToString(obj.id())), mData(obj) + LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj, ESM::LuaScriptCfg::Flags autoStartMode) + : LuaUtil::ScriptsContainer(lua, "L" + idToString(obj.id()), autoStartMode), mData(obj) { this->addPackage("openmw.self", sol::make_object(lua->sol(), &mData)); registerEngineHandlers({&mOnActiveHandlers, &mOnInactiveHandlers, &mOnConsumeHandlers}); } - void LocalScripts::receiveEngineEvent(const EngineEvent& event, ObjectRegistry*) + void LocalScripts::receiveEngineEvent(const EngineEvent& event) { std::visit([this](auto&& arg) { diff --git a/apps/openmw/mwlua/localscripts.hpp b/apps/openmw/mwlua/localscripts.hpp index 80d04b7a40..68da0b8b03 100644 --- a/apps/openmw/mwlua/localscripts.hpp +++ b/apps/openmw/mwlua/localscripts.hpp @@ -20,7 +20,7 @@ namespace MWLua { public: static void initializeSelfPackage(const Context&); - LocalScripts(LuaUtil::LuaState* lua, const LObject& obj); + LocalScripts(LuaUtil::LuaState* lua, const LObject& obj, ESM::LuaScriptCfg::Flags autoStartMode); MWBase::LuaManager::ActorControls* getActorControls() { return &mData.mControls; } @@ -39,7 +39,7 @@ namespace MWLua }; using EngineEvent = std::variant; - void receiveEngineEvent(const EngineEvent&, ObjectRegistry*); + void receiveEngineEvent(const EngineEvent&); protected: SelfObject mData; diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp index aceffc24db..1c05debc73 100644 --- a/apps/openmw/mwlua/luabindings.cpp +++ b/apps/openmw/mwlua/luabindings.cpp @@ -25,7 +25,7 @@ namespace MWLua { auto* lua = context.mLua; sol::table api(lua->sol(), sol::create); - api["API_REVISION"] = 7; + api["API_REVISION"] = 8; api["quit"] = [lua]() { std::string traceback = lua->sol()["debug"]["traceback"]().get(); diff --git a/apps/openmw/mwlua/luabindings.hpp b/apps/openmw/mwlua/luabindings.hpp index d1c62e43e3..aad3183734 100644 --- a/apps/openmw/mwlua/luabindings.hpp +++ b/apps/openmw/mwlua/luabindings.hpp @@ -48,7 +48,7 @@ namespace MWLua struct AsyncPackageId { LuaUtil::ScriptsContainer* mContainer; - std::string mScript; + int mScriptId; sol::table mHiddenData; }; sol::function getAsyncPackageInitializer(const Context&); diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 38055c99b7..7a4b319f27 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -7,7 +7,6 @@ #include #include -#include #include "../mwbase/windowmanager.hpp" @@ -20,10 +19,10 @@ namespace MWLua { - LuaManager::LuaManager(const VFS::Manager* vfs, const std::vector& scriptLists) : mLua(vfs) + LuaManager::LuaManager(const VFS::Manager* vfs, std::vector OMWScriptsFiles) : + mVFS(vfs), mOMWScriptsFiles(std::move(OMWScriptsFiles)), mLua(vfs, &mConfiguration) { Log(Debug::Info) << "Lua version: " << LuaUtil::getLuaVersion(); - mGlobalScriptList = LuaUtil::parseOMWScriptsFiles(vfs, scriptLists); mGlobalSerializer = createUserdataSerializer(false, mWorldView.getObjectRegistry()); mLocalSerializer = createUserdataSerializer(true, mWorldView.getObjectRegistry()); @@ -33,6 +32,30 @@ namespace MWLua mGlobalScripts.setSerializer(mGlobalSerializer.get()); } + void LuaManager::initConfiguration() + { + ESM::LuaScriptsCfg cfg; + for (const std::string& file : mOMWScriptsFiles) + { + if (!Misc::StringUtils::endsWith(file, ".omwscripts")) + { + Log(Debug::Error) << "Script list should have suffix '.omwscripts', got: '" << file << "'"; + continue; + } + try + { + std::string content(std::istreambuf_iterator(*mVFS->get(file)), {}); + LuaUtil::parseOMWScripts(cfg, content); + } + catch (std::exception& e) { Log(Debug::Error) << e.what(); } + } + // TODO: Add data from content files + mConfiguration.init(cfg); + 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; @@ -67,10 +90,7 @@ namespace MWLua mLocalSettingsPackage = initLocalSettingsPackage(localContext); mPlayerSettingsPackage = initPlayerSettingsPackage(localContext); - mInputEvents.clear(); - for (const std::string& path : mGlobalScriptList) - if (mGlobalScripts.addNewScript(path)) - Log(Debug::Info) << "Global script started: " << path; + initConfiguration(); mInitialized = true; } @@ -160,7 +180,7 @@ namespace MWLua } LocalScripts* scripts = obj.ptr().getRefData().getLuaScripts(); if (scripts) - scripts->receiveEngineEvent(e.mEvent, objectRegistry); + scripts->receiveEngineEvent(e.mEvent); } mLocalEngineEvents.clear(); @@ -173,6 +193,11 @@ namespace MWLua mPlayerChanged = false; mGlobalScripts.playerAdded(GObject(getId(mPlayer), objectRegistry)); } + if (mNewGameStarted) + { + mNewGameStarted = false; + mGlobalScripts.newGameStarted(); + } for (ObjectId id : mActorAddedEvents) mGlobalScripts.actorActive(GObject(id, objectRegistry)); @@ -205,8 +230,11 @@ namespace MWLua mInputEvents.clear(); mActorAddedEvents.clear(); mLocalEngineEvents.clear(); + mNewGameStarted = false; mPlayerChanged = false; mWorldView.clear(); + mGlobalScripts.removeAllScripts(); + mGlobalScriptsStarted = false; if (!mPlayer.isEmpty()) { mPlayer.getCellRef().unsetRefNum(); @@ -225,17 +253,38 @@ namespace MWLua mPlayer = ptr; LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); if (!localScripts) - localScripts = createLocalScripts(ptr); + localScripts = createLocalScripts(ptr, ESM::LuaScriptCfg::sPlayer); 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); // TODO: put to a queue and apply on next `update()` + } if (localScripts) { mActiveLocalScripts.insert(localScripts); @@ -281,26 +330,26 @@ namespace MWLua return localScripts->getActorControls(); } - void LuaManager::addLocalScript(const MWWorld::Ptr& ptr, const std::string& scriptPath) + void LuaManager::addCustomLocalScript(const MWWorld::Ptr& ptr, int scriptId) { LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); if (!localScripts) { - localScripts = createLocalScripts(ptr); + localScripts = createLocalScripts(ptr, getLuaScriptFlag(ptr)); if (ptr.isInCell() && MWBase::Environment::get().getWorld()->isCellActive(ptr.getCell())) mActiveLocalScripts.insert(localScripts); } - localScripts->addNewScript(scriptPath); + localScripts->addCustomScript(scriptId); } - LocalScripts* LuaManager::createLocalScripts(const MWWorld::Ptr& ptr) + LocalScripts* LuaManager::createLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScriptCfg::Flags flag) { assert(mInitialized); + assert(flag != ESM::LuaScriptCfg::sGlobal); std::shared_ptr scripts; - // When loading a game, it can be called before LuaManager::setPlayer, - // so we can't just check ptr == mPlayer here. - if (ptr.getCellRef().getRefIdRef() == "player") + if (flag == ESM::LuaScriptCfg::sPlayer) { + assert(ptr.getCellRef().getRefIdRef() == "player"); scripts = std::make_shared(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry())); scripts->addPackage("openmw.ui", mUserInterfacePackage); scripts->addPackage("openmw.camera", mCameraPackage); @@ -309,11 +358,12 @@ namespace MWLua } else { - scripts = std::make_shared(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry())); + scripts = std::make_shared(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry()), flag); scripts->addPackage("openmw.settings", mLocalSettingsPackage); } scripts->addPackage("openmw.nearby", mNearbyPackage); scripts->setSerializer(mLocalSerializer.get()); + scripts->addAutoStartedScripts(); MWWorld::RefData& refData = ptr.getRefData(); refData.setLuaScripts(std::move(scripts)); @@ -344,8 +394,9 @@ namespace MWLua loadEvents(mLua.sol(), reader, mGlobalEvents, mLocalEvents, mContentFileMapping, mGlobalLoader.get()); mGlobalScripts.setSerializer(mGlobalLoader.get()); - mGlobalScripts.load(globalScripts, false); + mGlobalScripts.load(globalScripts); mGlobalScripts.setSerializer(mGlobalSerializer.get()); + mGlobalScriptsStarted = true; } void LuaManager::saveLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScripts& data) @@ -366,10 +417,10 @@ namespace MWLua } mWorldView.getObjectRegistry()->registerPtr(ptr); - LocalScripts* scripts = createLocalScripts(ptr); + LocalScripts* scripts = createLocalScripts(ptr, getLuaScriptFlag(ptr)); scripts->setSerializer(mLocalLoader.get()); - scripts->load(data, true); + scripts->load(data); scripts->setSerializer(mLocalSerializer.get()); // LiveCellRef is usually copied after loading, so this Ptr will become invalid and should be deregistered. @@ -380,15 +431,12 @@ namespace MWLua { Log(Debug::Info) << "Reload Lua"; mLua.dropScriptCache(); + initConfiguration(); { // Reload global scripts ESM::LuaScripts data; mGlobalScripts.save(data); - mGlobalScripts.removeAllScripts(); - for (const std::string& path : mGlobalScriptList) - if (mGlobalScripts.addNewScript(path)) - Log(Debug::Info) << "Global script restarted: " << path; - mGlobalScripts.load(data, false); + mGlobalScripts.load(data); } for (const auto& [id, ptr] : mWorldView.getObjectRegistry()->mObjectMapping) @@ -398,7 +446,7 @@ namespace MWLua continue; ESM::LuaScripts data; scripts->save(data); - scripts->load(data, true); + scripts->load(data); } } diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index 91f48171f3..be80cfe2e7 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -35,9 +35,9 @@ namespace MWLua class LuaManager : public MWBase::LuaManager { public: - LuaManager(const VFS::Manager* vfs, const std::vector& globalScriptLists); + LuaManager(const VFS::Manager* vfs, std::vector OMWScriptsFiles); - // Called by engine.cpp when environment is fully initialized. + // Called by engine.cpp when the environment is fully initialized. void init(); // Called by engine.cpp every frame. For performance reasons it works in a separate @@ -49,7 +49,8 @@ namespace MWLua // Available everywhere through the MWBase::LuaManager interface. // LuaManager queues these events and propagates to scripts on the next `update` call. - void newGameStarted() override { mGlobalScripts.newGameStarted(); } + void newGameStarted() override; + void gameLoaded() override; void objectAddedToScene(const MWWorld::Ptr& ptr) override; void objectRemovedFromScene(const MWWorld::Ptr& ptr) override; void registerObject(const MWWorld::Ptr& ptr) override; @@ -62,8 +63,8 @@ namespace MWLua void clear() override; // should be called before loading game or starting a new game to reset internal state. void setupPlayer(const MWWorld::Ptr& ptr) override; // Should be called once after each "clear". - // Used only in luabindings - void addLocalScript(const MWWorld::Ptr&, const std::string& scriptPath); + // Used only in Lua bindings + void addCustomLocalScript(const MWWorld::Ptr&, int scriptId); void addAction(std::unique_ptr&& action) { mActionQueue.push_back(std::move(action)); } void addTeleportPlayerAction(std::unique_ptr&& action) { mTeleportPlayerAction = std::move(action); } void addUIMessage(std::string_view message) { mUIMessages.emplace_back(message); } @@ -93,9 +94,15 @@ namespace MWLua } private: - LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr); + void initConfiguration(); + LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScriptCfg::Flags); + + const VFS::Manager* mVFS; + const std::vector mOMWScriptsFiles; bool mInitialized = false; + bool mGlobalScriptsStarted = false; + LuaUtil::ScriptsConfiguration mConfiguration; LuaUtil::LuaState mLua; sol::table mNearbyPackage; sol::table mUserInterfacePackage; @@ -104,12 +111,12 @@ namespace MWLua sol::table mLocalSettingsPackage; sol::table mPlayerSettingsPackage; - std::vector mGlobalScriptList; GlobalScripts mGlobalScripts{&mLua}; std::set mActiveLocalScripts; WorldView mWorldView; bool mPlayerChanged = false; + bool mNewGameStarted = false; MWWorld::Ptr mPlayer; GlobalEventQueue mGlobalEvents; diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp index 07b7921a09..2eceecc061 100644 --- a/apps/openmw/mwlua/objectbindings.cpp +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -140,9 +140,43 @@ namespace MWLua if constexpr (std::is_same_v) { // Only for global scripts - objectT["addScript"] = [luaManager=context.mLuaManager](const GObject& object, const std::string& path) + objectT["addScript"] = [lua=context.mLua, luaManager=context.mLuaManager](const GObject& object, std::string_view path) { - luaManager->addLocalScript(object.ptr(), path); + const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration(); + std::optional scriptId = cfg.findId(path); + if (!scriptId) + throw std::runtime_error("Unknown script: " + std::string(path)); + if (!(cfg[*scriptId].mFlags & ESM::LuaScriptCfg::sCustom)) + throw std::runtime_error("Script without CUSTOM tag can not be added dynamically: " + std::string(path)); + luaManager->addCustomLocalScript(object.ptr(), *scriptId); + }; + objectT["hasScript"] = [lua=context.mLua](const GObject& object, std::string_view path) + { + const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration(); + std::optional scriptId = cfg.findId(path); + if (!scriptId) + return false; + MWWorld::Ptr ptr = object.ptr(); + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (localScripts) + return localScripts->hasScript(*scriptId); + else + return false; + }; + objectT["removeScript"] = [lua=context.mLua](const GObject& object, std::string_view path) + { + const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration(); + std::optional scriptId = cfg.findId(path); + if (!scriptId) + throw std::runtime_error("Unknown script: " + std::string(path)); + MWWorld::Ptr ptr = object.ptr(); + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (!localScripts || !localScripts->hasScript(*scriptId)) + throw std::runtime_error("There is no script " + std::string(path) + " on " + ptrToString(ptr)); + ESM::LuaScriptCfg::Flags flags = cfg[*scriptId].mFlags; + if ((flags & (localScripts->getAutoStartMode() | ESM::LuaScriptCfg::sCustom)) != ESM::LuaScriptCfg::sCustom) + throw std::runtime_error("Autostarted script can not be removed: " + std::string(path)); + localScripts->removeScript(*scriptId); }; objectT["teleport"] = [luaManager=context.mLuaManager](const GObject& object, std::string_view cell, diff --git a/apps/openmw/mwlua/playerscripts.hpp b/apps/openmw/mwlua/playerscripts.hpp index ff0349b3c6..0393a1375d 100644 --- a/apps/openmw/mwlua/playerscripts.hpp +++ b/apps/openmw/mwlua/playerscripts.hpp @@ -13,7 +13,7 @@ namespace MWLua class PlayerScripts : public LocalScripts { public: - PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) : LocalScripts(lua, obj) + PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) : LocalScripts(lua, obj, ESM::LuaScriptCfg::sPlayer) { registerEngineHandlers({&mKeyPressHandlers, &mKeyReleaseHandlers, &mControllerButtonPressHandlers, &mControllerButtonReleaseHandlers, diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index 4387d7faa7..5e51e2f621 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -558,6 +558,8 @@ void MWState::StateManager::loadGame (const Character *character, const std::str // Since we passed "changeEvent=false" to changeCell, we shouldn't have triggered the cell change flag. // But make sure the flag is cleared anyway in case it was set from an earlier game. MWBase::Environment::get().getWorld()->markCellAsUnchanged(); + + MWBase::Environment::get().getLuaManager()->gameLoaded(); } catch (const std::exception& e) { diff --git a/apps/openmw_test_suite/lua/test_lua.cpp b/apps/openmw_test_suite/lua/test_lua.cpp index 32d4ea49b8..4b3ecdcb2b 100644 --- a/apps/openmw_test_suite/lua/test_lua.cpp +++ b/apps/openmw_test_suite/lua/test_lua.cpp @@ -58,7 +58,8 @@ return { {"invalid.lua", &invalidScriptFile} }); - LuaUtil::LuaState mLua{mVFS.get()}; + LuaUtil::ScriptsConfiguration mCfg; + LuaUtil::LuaState mLua{mVFS.get(), &mCfg}; }; TEST_F(LuaStateTest, Sandbox) @@ -148,7 +149,7 @@ return { TEST_F(LuaStateTest, ProvideAPI) { - LuaUtil::LuaState lua(mVFS.get()); + LuaUtil::LuaState lua(mVFS.get(), &mCfg); sol::table api1 = LuaUtil::makeReadOnly(lua.sol().create_table_with("name", "api1")); sol::table api2 = LuaUtil::makeReadOnly(lua.sol().create_table_with("name", "api2")); diff --git a/apps/openmw_test_suite/lua/test_omwscriptsparser.cpp b/apps/openmw_test_suite/lua/test_omwscriptsparser.cpp deleted file mode 100644 index b1526ef9b6..0000000000 --- a/apps/openmw_test_suite/lua/test_omwscriptsparser.cpp +++ /dev/null @@ -1,59 +0,0 @@ -#include "gmock/gmock.h" -#include - -#include - -#include "testing_util.hpp" - -namespace -{ - using namespace testing; - - TestFile file1( - "#comment.lua\n" - "\n" - "script1.lua\n" - "some mod/Some Script.lua" - ); - TestFile file2( - "#comment.lua\r\n" - "\r\n" - "script2.lua\r\n" - "some other mod/Some Script.lua\r" - ); - TestFile emptyFile(""); - TestFile invalidFile("Invalid file"); - - struct OMWScriptsParserTest : Test - { - std::unique_ptr mVFS = createTestVFS({ - {"file1.omwscripts", &file1}, - {"file2.omwscripts", &file2}, - {"empty.omwscripts", &emptyFile}, - {"invalid.lua", &file1}, - {"invalid.omwscripts", &invalidFile}, - }); - }; - - TEST_F(OMWScriptsParserTest, Basic) - { - internal::CaptureStdout(); - std::vector res = LuaUtil::parseOMWScriptsFiles( - mVFS.get(), {"file2.omwscripts", "empty.omwscripts", "file1.omwscripts"}); - EXPECT_EQ(internal::GetCapturedStdout(), ""); - EXPECT_THAT(res, ElementsAre("script2.lua", "some other mod/Some Script.lua", - "script1.lua", "some mod/Some Script.lua")); - } - - TEST_F(OMWScriptsParserTest, InvalidFiles) - { - internal::CaptureStdout(); - std::vector res = LuaUtil::parseOMWScriptsFiles( - mVFS.get(), {"invalid.lua", "invalid.omwscripts"}); - EXPECT_EQ(internal::GetCapturedStdout(), - "Script list should have suffix '.omwscripts', got: 'invalid.lua'\n" - "Lua script should have suffix '.lua', got: 'Invalid file'\n"); - EXPECT_THAT(res, ElementsAre()); - } - -} diff --git a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp index 8f05138782..344fbb3c78 100644 --- a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp +++ b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp @@ -18,7 +18,10 @@ namespace TestFile testScript(R"X( return { - engineHandlers = { onUpdate = function(dt) print(' update ' .. tostring(dt)) end }, + engineHandlers = { + onUpdate = function(dt) print(' update ' .. tostring(dt)) end, + onLoad = function() print('load') end, + }, eventHandlers = { Event1 = function(eventData) print(' event1 ' .. tostring(eventData.x)) end, Event2 = function(eventData) print(' event2 ' .. tostring(eventData.x)) end, @@ -75,15 +78,25 @@ return { )X"); TestFile overrideInterfaceScript(R"X( -local old = require('openmw.interfaces').TestInterface +local old = nil +local interface = { + fn = function(x) + print('NEW FN', x) + old.fn(x) + end, + value, +} return { interfaceName = "TestInterface", - interface = { - fn = function(x) - print('NEW FN', x) - old.fn(x) - end, - value = old.value + 1 + interface = interface, + engineHandlers = { + onInit = function() print('init') end, + onLoad = function() print('load') end, + onInterfaceOverride = function(oldInterface) + print('override') + old = oldInterface + interface.value = oldInterface.value + 1 + end }, } )X"); @@ -115,7 +128,25 @@ return { {"useInterface.lua", &useInterfaceScript}, }); - LuaUtil::LuaState mLua{mVFS.get()}; + LuaUtil::ScriptsConfiguration mCfg; + LuaUtil::LuaState mLua{mVFS.get(), &mCfg}; + + LuaScriptsContainerTest() + { + ESM::LuaScriptsCfg cfg; + cfg.mScripts.push_back({"invalid.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"incorrect.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"empty.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"test1.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"stopEvent.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"test2.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"loadSave1.lua", "", ESM::LuaScriptCfg::sNPC}); + cfg.mScripts.push_back({"loadSave2.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sNPC}); + cfg.mScripts.push_back({"testInterface.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sPlayer}); + cfg.mScripts.push_back({"overrideInterface.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sPlayer}); + cfg.mScripts.push_back({"useInterface.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sPlayer}); + mCfg.init(std::move(cfg)); + } }; TEST_F(LuaScriptsContainerTest, VerifyStructure) @@ -123,21 +154,21 @@ return { LuaUtil::ScriptsContainer scripts(&mLua, "Test"); { testing::internal::CaptureStdout(); - EXPECT_FALSE(scripts.addNewScript("invalid.lua")); + EXPECT_FALSE(scripts.addCustomScript(*mCfg.findId("invalid.lua"))); std::string output = testing::internal::GetCapturedStdout(); EXPECT_THAT(output, HasSubstr("Can't start Test[invalid.lua]")); } { testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.addNewScript("incorrect.lua")); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("incorrect.lua"))); std::string output = testing::internal::GetCapturedStdout(); EXPECT_THAT(output, HasSubstr("Not supported handler 'incorrectHandler' in Test[incorrect.lua]")); EXPECT_THAT(output, HasSubstr("Not supported section 'incorrectSection' in Test[incorrect.lua]")); } { testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.addNewScript("empty.lua")); - EXPECT_FALSE(scripts.addNewScript("empty.lua")); // already present + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("empty.lua"))); + EXPECT_FALSE(scripts.addCustomScript(*mCfg.findId("empty.lua"))); // already present EXPECT_EQ(internal::GetCapturedStdout(), ""); } } @@ -146,9 +177,9 @@ return { { LuaUtil::ScriptsContainer scripts(&mLua, "Test"); testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.addNewScript("test1.lua")); - EXPECT_TRUE(scripts.addNewScript("stopEvent.lua")); - EXPECT_TRUE(scripts.addNewScript("test2.lua")); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test1.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("stopEvent.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test2.lua"))); scripts.update(1.5f); EXPECT_EQ(internal::GetCapturedStdout(), "Test[test1.lua]:\t update 1.5\n" "Test[test2.lua]:\t update 1.5\n"); @@ -157,9 +188,9 @@ return { TEST_F(LuaScriptsContainerTest, CallEvent) { LuaUtil::ScriptsContainer scripts(&mLua, "Test"); - EXPECT_TRUE(scripts.addNewScript("test1.lua")); - EXPECT_TRUE(scripts.addNewScript("stopEvent.lua")); - EXPECT_TRUE(scripts.addNewScript("test2.lua")); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test1.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("stopEvent.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test2.lua"))); std::string X0 = LuaUtil::serialize(mLua.sol().create_table_with("x", 0.5)); std::string X1 = LuaUtil::serialize(mLua.sol().create_table_with("x", 1.5)); @@ -204,9 +235,9 @@ return { TEST_F(LuaScriptsContainerTest, RemoveScript) { LuaUtil::ScriptsContainer scripts(&mLua, "Test"); - EXPECT_TRUE(scripts.addNewScript("test1.lua")); - EXPECT_TRUE(scripts.addNewScript("stopEvent.lua")); - EXPECT_TRUE(scripts.addNewScript("test2.lua")); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test1.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("stopEvent.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test2.lua"))); std::string X = LuaUtil::serialize(mLua.sol().create_table_with("x", 0.5)); { @@ -221,8 +252,10 @@ return { } { testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.removeScript("stopEvent.lua")); - EXPECT_FALSE(scripts.removeScript("stopEvent.lua")); // already removed + int stopEventScriptId = *mCfg.findId("stopEvent.lua"); + EXPECT_TRUE(scripts.hasScript(stopEventScriptId)); + scripts.removeScript(stopEventScriptId); + EXPECT_FALSE(scripts.hasScript(stopEventScriptId)); scripts.update(1.5f); scripts.receiveEvent("Event1", X); EXPECT_EQ(internal::GetCapturedStdout(), @@ -233,7 +266,7 @@ return { } { testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.removeScript("test1.lua")); + scripts.removeScript(*mCfg.findId("test1.lua")); scripts.update(1.5f); scripts.receiveEvent("Event1", X); EXPECT_EQ(internal::GetCapturedStdout(), @@ -242,17 +275,41 @@ return { } } - TEST_F(LuaScriptsContainerTest, Interface) + TEST_F(LuaScriptsContainerTest, AutoStart) { - LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + LuaUtil::ScriptsContainer scripts(&mLua, "Test", ESM::LuaScriptCfg::sPlayer); testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.addNewScript("testInterface.lua")); - EXPECT_TRUE(scripts.addNewScript("overrideInterface.lua")); - EXPECT_TRUE(scripts.addNewScript("useInterface.lua")); - scripts.update(1.5f); - EXPECT_TRUE(scripts.removeScript("overrideInterface.lua")); + scripts.addAutoStartedScripts(); scripts.update(1.5f); EXPECT_EQ(internal::GetCapturedStdout(), + "Test[overrideInterface.lua]:\toverride\n" + "Test[overrideInterface.lua]:\tinit\n" + "Test[overrideInterface.lua]:\tNEW FN\t4.5\n" + "Test[testInterface.lua]:\tFN\t4.5\n"); + } + + TEST_F(LuaScriptsContainerTest, Interface) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test", ESM::LuaScriptCfg::sCreature); + int addIfaceId = *mCfg.findId("testInterface.lua"); + int overrideIfaceId = *mCfg.findId("overrideInterface.lua"); + int useIfaceId = *mCfg.findId("useInterface.lua"); + + testing::internal::CaptureStdout(); + scripts.addAutoStartedScripts(); + scripts.update(1.5f); + EXPECT_EQ(internal::GetCapturedStdout(), ""); + + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.addCustomScript(addIfaceId)); + EXPECT_TRUE(scripts.addCustomScript(overrideIfaceId)); + EXPECT_TRUE(scripts.addCustomScript(useIfaceId)); + scripts.update(1.5f); + scripts.removeScript(overrideIfaceId); + scripts.update(1.5f); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[overrideInterface.lua]:\toverride\n" + "Test[overrideInterface.lua]:\tinit\n" "Test[overrideInterface.lua]:\tNEW FN\t4.5\n" "Test[testInterface.lua]:\tFN\t4.5\n" "Test[testInterface.lua]:\tFN\t3.5\n"); @@ -260,16 +317,12 @@ return { TEST_F(LuaScriptsContainerTest, LoadSave) { - LuaUtil::ScriptsContainer scripts1(&mLua, "Test"); - LuaUtil::ScriptsContainer scripts2(&mLua, "Test"); - LuaUtil::ScriptsContainer scripts3(&mLua, "Test"); + LuaUtil::ScriptsContainer scripts1(&mLua, "Test", ESM::LuaScriptCfg::sNPC); + LuaUtil::ScriptsContainer scripts2(&mLua, "Test", ESM::LuaScriptCfg::sNPC); + LuaUtil::ScriptsContainer scripts3(&mLua, "Test", ESM::LuaScriptCfg::sPlayer); - EXPECT_TRUE(scripts1.addNewScript("loadSave1.lua")); - EXPECT_TRUE(scripts1.addNewScript("test1.lua")); - EXPECT_TRUE(scripts1.addNewScript("loadSave2.lua")); - - EXPECT_TRUE(scripts3.addNewScript("test2.lua")); - EXPECT_TRUE(scripts3.addNewScript("loadSave2.lua")); + scripts1.addAutoStartedScripts(); + EXPECT_TRUE(scripts1.addCustomScript(*mCfg.findId("test1.lua"))); scripts1.receiveEvent("Set", LuaUtil::serialize(mLua.sol().create_table_with( "n", 1, @@ -282,23 +335,30 @@ return { ESM::LuaScripts data; scripts1.save(data); - scripts2.load(data, true); - scripts3.load(data, false); { testing::internal::CaptureStdout(); + scripts2.load(data); scripts2.receiveEvent("Print", ""); EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test1.lua]:\tload\n" "Test[loadSave2.lua]:\t0.5\t3.5\n" - "Test[test1.lua]:\tprint\n" - "Test[loadSave1.lua]:\t2.5\t1.5\n"); + "Test[loadSave1.lua]:\t2.5\t1.5\n" + "Test[test1.lua]:\tprint\n"); + EXPECT_FALSE(scripts2.hasScript(*mCfg.findId("testInterface.lua"))); } { testing::internal::CaptureStdout(); + scripts3.load(data); scripts3.receiveEvent("Print", ""); EXPECT_EQ(internal::GetCapturedStdout(), + "Ignoring Test[loadSave1.lua]; this script is not allowed here\n" + "Test[test1.lua]:\tload\n" + "Test[overrideInterface.lua]:\toverride\n" + "Test[overrideInterface.lua]:\tinit\n" "Test[loadSave2.lua]:\t0.5\t3.5\n" - "Test[test2.lua]:\tprint\n"); + "Test[test1.lua]:\tprint\n"); + EXPECT_TRUE(scripts3.hasScript(*mCfg.findId("testInterface.lua"))); } } @@ -306,8 +366,13 @@ return { { using TimeUnit = LuaUtil::ScriptsContainer::TimeUnit; LuaUtil::ScriptsContainer scripts(&mLua, "Test"); - EXPECT_TRUE(scripts.addNewScript("test1.lua")); - EXPECT_TRUE(scripts.addNewScript("test2.lua")); + int test1Id = *mCfg.findId("test1.lua"); + int test2Id = *mCfg.findId("test2.lua"); + + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.addCustomScript(test1Id)); + EXPECT_TRUE(scripts.addCustomScript(test2Id)); + EXPECT_EQ(internal::GetCapturedStdout(), ""); int counter1 = 0, counter2 = 0, counter3 = 0, counter4 = 0; sol::function fn1 = sol::make_object(mLua.sol(), [&]() { counter1++; }); @@ -315,25 +380,25 @@ return { sol::function fn3 = sol::make_object(mLua.sol(), [&](int d) { counter3 += d; }); sol::function fn4 = sol::make_object(mLua.sol(), [&](int d) { counter4 += d; }); - scripts.registerTimerCallback("test1.lua", "A", fn3); - scripts.registerTimerCallback("test1.lua", "B", fn4); - scripts.registerTimerCallback("test2.lua", "B", fn3); - scripts.registerTimerCallback("test2.lua", "A", fn4); + scripts.registerTimerCallback(test1Id, "A", fn3); + scripts.registerTimerCallback(test1Id, "B", fn4); + scripts.registerTimerCallback(test2Id, "B", fn3); + scripts.registerTimerCallback(test2Id, "A", fn4); scripts.processTimers(1, 2); - scripts.setupSerializableTimer(TimeUnit::SECONDS, 10, "test1.lua", "B", sol::make_object(mLua.sol(), 3)); - scripts.setupSerializableTimer(TimeUnit::HOURS, 10, "test2.lua", "B", sol::make_object(mLua.sol(), 4)); - scripts.setupSerializableTimer(TimeUnit::SECONDS, 5, "test1.lua", "A", sol::make_object(mLua.sol(), 1)); - scripts.setupSerializableTimer(TimeUnit::HOURS, 5, "test2.lua", "A", sol::make_object(mLua.sol(), 2)); - scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, "test1.lua", "A", sol::make_object(mLua.sol(), 10)); - scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, "test1.lua", "B", sol::make_object(mLua.sol(), 20)); + scripts.setupSerializableTimer(TimeUnit::SECONDS, 10, test1Id, "B", sol::make_object(mLua.sol(), 3)); + scripts.setupSerializableTimer(TimeUnit::HOURS, 10, test2Id, "B", sol::make_object(mLua.sol(), 4)); + scripts.setupSerializableTimer(TimeUnit::SECONDS, 5, test1Id, "A", sol::make_object(mLua.sol(), 1)); + scripts.setupSerializableTimer(TimeUnit::HOURS, 5, test2Id, "A", sol::make_object(mLua.sol(), 2)); + scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, test1Id, "A", sol::make_object(mLua.sol(), 10)); + scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, test1Id, "B", sol::make_object(mLua.sol(), 20)); - scripts.setupUnsavableTimer(TimeUnit::SECONDS, 10, "test2.lua", fn2); - scripts.setupUnsavableTimer(TimeUnit::HOURS, 10, "test1.lua", fn2); - scripts.setupUnsavableTimer(TimeUnit::SECONDS, 5, "test2.lua", fn1); - scripts.setupUnsavableTimer(TimeUnit::HOURS, 5, "test1.lua", fn1); - scripts.setupUnsavableTimer(TimeUnit::SECONDS, 15, "test2.lua", fn1); + scripts.setupUnsavableTimer(TimeUnit::SECONDS, 10, test2Id, fn2); + scripts.setupUnsavableTimer(TimeUnit::HOURS, 10, test1Id, fn2); + scripts.setupUnsavableTimer(TimeUnit::SECONDS, 5, test2Id, fn1); + scripts.setupUnsavableTimer(TimeUnit::HOURS, 5, test1Id, fn1); + scripts.setupUnsavableTimer(TimeUnit::SECONDS, 15, test2Id, fn1); EXPECT_EQ(counter1, 0); EXPECT_EQ(counter3, 0); @@ -358,10 +423,12 @@ return { EXPECT_EQ(counter3, 5); EXPECT_EQ(counter4, 5); + testing::internal::CaptureStdout(); ESM::LuaScripts data; scripts.save(data); - scripts.load(data, true); - scripts.registerTimerCallback("test1.lua", "B", fn4); + scripts.load(data); + scripts.registerTimerCallback(test1Id, "B", fn4); + EXPECT_EQ(internal::GetCapturedStdout(), "Test[test1.lua]:\tload\nTest[test2.lua]:\tload\n"); testing::internal::CaptureStdout(); scripts.processTimers(20, 20); diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 7e1abe0a4e..a3f77d86bf 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -29,7 +29,7 @@ endif (GIT_CHECKOUT) # source files add_component_dir (lua - luastate scriptscontainer utilpackage serialization omwscriptsparser + luastate scriptscontainer utilpackage serialization configuration ) add_component_dir (settings diff --git a/components/lua/luastate.cpp b/components/lua/luastate.cpp index 8e4719dba4..e78f7bed06 100644 --- a/components/lua/luastate.cpp +++ b/components/lua/luastate.cpp @@ -22,7 +22,7 @@ namespace LuaUtil "type", "unpack", "xpcall", "rawequal", "rawget", "rawset", "getmetatable", "setmetatable"}; static const std::string safePackages[] = {"coroutine", "math", "string", "table"}; - LuaState::LuaState(const VFS::Manager* vfs) : mVFS(vfs) + LuaState::LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf) : mConf(conf), mVFS(vfs) { mLua.open_libraries(sol::lib::base, sol::lib::coroutine, sol::lib::math, sol::lib::string, sol::lib::table, sol::lib::debug); @@ -95,12 +95,11 @@ namespace LuaUtil return res; } - void LuaState::addCommonPackage(const std::string& packageName, const sol::object& package) + void LuaState::addCommonPackage(std::string packageName, sol::object package) { - if (package.is()) - mCommonPackages[packageName] = package; - else - mCommonPackages[packageName] = makeReadOnly(package); + if (!package.is()) + package = makeReadOnly(std::move(package)); + mCommonPackages.emplace(std::move(packageName), std::move(package)); } sol::protected_function_result LuaState::runInNewSandbox( @@ -148,7 +147,7 @@ namespace LuaUtil return std::move(res); } - sol::protected_function LuaState::loadScript(const std::string& path) + sol::function LuaState::loadScript(const std::string& path) { auto iter = mCompiledScripts.find(path); if (iter != mCompiledScripts.end()) diff --git a/components/lua/luastate.hpp b/components/lua/luastate.hpp index 8982b49b36..7ac5af0b1b 100644 --- a/components/lua/luastate.hpp +++ b/components/lua/luastate.hpp @@ -7,6 +7,8 @@ #include +#include "configuration.hpp" + namespace LuaUtil { @@ -22,12 +24,12 @@ namespace LuaUtil // - Access to common read-only resources from different sandboxes; // - Replace standard `require` with a safe version that allows to search // Lua libraries (only source, no dll's) in the virtual filesystem; - // - Make `print` to add the script name to the every message and - // write to Log rather than directly to stdout; + // - Make `print` to add the script name to every message and + // write to the Log rather than directly to stdout; class LuaState { public: - explicit LuaState(const VFS::Manager* vfs); + explicit LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf); ~LuaState(); // Returns underlying sol::state. @@ -40,7 +42,7 @@ namespace LuaUtil // The package can be either a sol::table with an API or a sol::function. If it is a function, // it will be evaluated (once per sandbox) the first time when requested. If the package // is a table, then `makeReadOnly` is applied to it automatically (but not to other tables it contains). - void addCommonPackage(const std::string& packageName, const sol::object& package); + void addCommonPackage(std::string packageName, sol::object package); // Creates a new sandbox, runs a script, and returns the result // (the result is expected to be an interface of the script). @@ -58,14 +60,17 @@ namespace LuaUtil void dropScriptCache() { mCompiledScripts.clear(); } + const ScriptsConfiguration& getConfiguration() const { return *mConf; } + private: static sol::protected_function_result throwIfError(sol::protected_function_result&&); template - friend sol::protected_function_result call(sol::protected_function fn, Args&&... args); + friend sol::protected_function_result call(const sol::protected_function& fn, Args&&... args); - sol::protected_function loadScript(const std::string& path); + sol::function loadScript(const std::string& path); sol::state mLua; + const ScriptsConfiguration* mConf; sol::table mSandboxEnv; std::map mCompiledScripts; std::map mCommonPackages; @@ -75,7 +80,7 @@ namespace LuaUtil // Should be used for every call of every Lua function. // It is a workaround for a bug in `sol`. See https://github.com/ThePhD/sol2/issues/1078 template - sol::protected_function_result call(sol::protected_function fn, Args&&... args) + sol::protected_function_result call(const sol::protected_function& fn, Args&&... args) { try { @@ -101,7 +106,7 @@ namespace LuaUtil std::string toString(const sol::object&); // Makes a table read only (when accessed from Lua) by wrapping it with an empty userdata. - // Needed to forbid any changes in common resources that can accessed from different sandboxes. + // Needed to forbid any changes in common resources that can be accessed from different sandboxes. sol::table makeReadOnly(sol::table); sol::table getMutableFromReadOnly(const sol::userdata&); diff --git a/components/lua/omwscriptsparser.cpp b/components/lua/omwscriptsparser.cpp deleted file mode 100644 index bc73e013db..0000000000 --- a/components/lua/omwscriptsparser.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "omwscriptsparser.hpp" - -#include - -#include - -std::vector LuaUtil::parseOMWScriptsFiles(const VFS::Manager* vfs, const std::vector& scriptLists) -{ - auto endsWith = [](std::string_view s, std::string_view suffix) - { - return s.size() >= suffix.size() && std::equal(suffix.rbegin(), suffix.rend(), s.rbegin()); - }; - std::vector res; - for (const std::string& scriptListFile : scriptLists) - { - if (!endsWith(scriptListFile, ".omwscripts")) - { - Log(Debug::Error) << "Script list should have suffix '.omwscripts', got: '" << scriptListFile << "'"; - continue; - } - std::string content(std::istreambuf_iterator(*vfs->get(scriptListFile)), {}); - std::string_view view(content); - while (!view.empty()) - { - size_t pos = 0; - while (pos < view.size() && view[pos] != '\n') - pos++; - std::string_view line = view.substr(0, pos); - view = view.substr(std::min(pos + 1, view.size())); - if (!line.empty() && line.back() == '\r') - line = line.substr(0, pos - 1); - // Lines starting with '#' are comments. - // TODO: Maybe make the parser more robust. It is a bit inconsistent that 'path/#to/file.lua' - // is a valid path, but '#path/to/file.lua' is considered as a comment and ignored. - if (line.empty() || line[0] == '#') - continue; - if (endsWith(line, ".lua")) - res.push_back(std::string(line)); - else - Log(Debug::Error) << "Lua script should have suffix '.lua', got: '" << line.substr(0, 300) << "'"; - } - } - return res; -} diff --git a/components/lua/omwscriptsparser.hpp b/components/lua/omwscriptsparser.hpp deleted file mode 100644 index 1da9f123b2..0000000000 --- a/components/lua/omwscriptsparser.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef COMPONENTS_LUA_OMWSCRIPTSPARSER_H -#define COMPONENTS_LUA_OMWSCRIPTSPARSER_H - -#include - -namespace LuaUtil -{ - - // Parses list of `*.omwscripts` files. - std::vector parseOMWScriptsFiles(const VFS::Manager* vfs, const std::vector& scriptLists); - -} - -#endif // COMPONENTS_LUA_OMWSCRIPTSPARSER_H diff --git a/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp index 703381a453..f4922a8b6b 100644 --- a/components/lua/scriptscontainer.cpp +++ b/components/lua/scriptscontainer.cpp @@ -10,8 +10,10 @@ namespace LuaUtil static constexpr std::string_view INTERFACE_NAME = "interfaceName"; static constexpr std::string_view INTERFACE = "interface"; + static constexpr std::string_view HANDLER_INIT = "onInit"; static constexpr std::string_view HANDLER_SAVE = "onSave"; static constexpr std::string_view HANDLER_LOAD = "onLoad"; + static constexpr std::string_view HANDLER_INTERFACE_OVERRIDE = "onInterfaceOverride"; static constexpr std::string_view REGISTERED_TIMER_CALLBACKS = "_timers"; static constexpr std::string_view TEMPORARY_TIMER_CALLBACKS = "_temp_timers"; @@ -25,147 +27,238 @@ namespace LuaUtil return res; } - ScriptsContainer::ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix) : mNamePrefix(namePrefix), mLua(*lua) + ScriptsContainer::ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix, ESM::LuaScriptCfg::Flags autoStartMode) + : mNamePrefix(namePrefix), mLua(*lua), mAutoStartMode(autoStartMode) { registerEngineHandlers({&mUpdateHandlers}); mPublicInterfaces = sol::table(lua->sol(), sol::create); addPackage("openmw.interfaces", mPublicInterfaces); } - void ScriptsContainer::addPackage(const std::string& packageName, sol::object package) + void ScriptsContainer::printError(int scriptId, std::string_view msg, const std::exception& e) { - API[packageName] = makeReadOnly(std::move(package)); + Log(Debug::Error) << mNamePrefix << "[" << scriptPath(scriptId) << "] " << msg << ": " << e.what(); } - bool ScriptsContainer::addNewScript(const std::string& path) + void ScriptsContainer::addPackage(std::string packageName, sol::object package) { - if (mScripts.count(path) != 0) + mAPI.emplace(std::move(packageName), makeReadOnly(std::move(package))); + } + + bool ScriptsContainer::addCustomScript(int scriptId) + { + assert(mLua.getConfiguration()[scriptId].mFlags & ESM::LuaScriptCfg::sCustom); + std::optional onInit, onLoad; + bool ok = addScript(scriptId, onInit, onLoad); + if (ok && onInit) + callOnInit(scriptId, *onInit); + return ok; + } + + void ScriptsContainer::addAutoStartedScripts() + { + for (int scriptId : mLua.getConfiguration().getListByFlag(mAutoStartMode)) + { + std::optional onInit, onLoad; + bool ok = addScript(scriptId, onInit, onLoad); + if (ok && onInit) + callOnInit(scriptId, *onInit); + } + } + + bool ScriptsContainer::addScript(int scriptId, std::optional& onInit, std::optional& onLoad) + { + assert(scriptId >= 0 && scriptId < static_cast(mLua.getConfiguration().size())); + if (mScripts.count(scriptId) != 0) return false; // already present + const std::string& path = scriptPath(scriptId); try { - sol::table hiddenData(mLua.sol(), sol::create); - hiddenData[ScriptId::KEY] = ScriptId{this, path}; - hiddenData[REGISTERED_TIMER_CALLBACKS] = mLua.newTable(); - hiddenData[TEMPORARY_TIMER_CALLBACKS] = mLua.newTable(); - mScripts[path].mHiddenData = hiddenData; - sol::object script = mLua.runInNewSandbox(path, mNamePrefix, API, hiddenData); - std::string interfaceName = ""; - sol::object publicInterface = sol::nil; - if (script != sol::nil) + Script& script = mScripts[scriptId]; + script.mHiddenData = mLua.newTable(); + script.mHiddenData[ScriptId::KEY] = ScriptId{this, scriptId, path}; + script.mHiddenData[REGISTERED_TIMER_CALLBACKS] = mLua.newTable(); + script.mHiddenData[TEMPORARY_TIMER_CALLBACKS] = mLua.newTable(); + sol::object scriptOutput = mLua.runInNewSandbox(path, mNamePrefix, mAPI, script.mHiddenData); + if (scriptOutput == sol::nil) + return true; + sol::object engineHandlers = sol::nil, eventHandlers = sol::nil; + for (const auto& [key, value] : sol::table(scriptOutput)) { - for (auto& [key, value] : sol::table(script)) + std::string_view sectionName = key.as(); + if (sectionName == ENGINE_HANDLERS) + engineHandlers = value; + else if (sectionName == EVENT_HANDLERS) + eventHandlers = value; + else if (sectionName == INTERFACE_NAME) + script.mInterfaceName = value.as(); + else if (sectionName == INTERFACE) + script.mInterface = value.as(); + else + Log(Debug::Error) << "Not supported section '" << sectionName << "' in " << mNamePrefix << "[" << path << "]"; + } + if (engineHandlers != sol::nil) + { + for (const auto& [key, fn] : sol::table(engineHandlers)) { - std::string_view sectionName = key.as(); - if (sectionName == ENGINE_HANDLERS) - parseEngineHandlers(value, path); - else if (sectionName == EVENT_HANDLERS) - parseEventHandlers(value, path); - else if (sectionName == INTERFACE_NAME) - interfaceName = value.as(); - else if (sectionName == INTERFACE) - publicInterface = value.as(); + std::string_view handlerName = key.as(); + if (handlerName == HANDLER_INIT) + onInit = sol::function(fn); + else if (handlerName == HANDLER_LOAD) + onLoad = sol::function(fn); + else if (handlerName == HANDLER_SAVE) + script.mOnSave = sol::function(fn); + else if (handlerName == HANDLER_INTERFACE_OVERRIDE) + script.mOnOverride = sol::function(fn); else - Log(Debug::Error) << "Not supported section '" << sectionName << "' in " << mNamePrefix << "[" << path << "]"; + { + auto it = mEngineHandlers.find(handlerName); + if (it == mEngineHandlers.end()) + Log(Debug::Error) << "Not supported handler '" << handlerName + << "' in " << mNamePrefix << "[" << path << "]"; + else + insertHandler(it->second->mList, scriptId, fn); + } } } - if (interfaceName.empty() != (publicInterface == sol::nil)) + if (eventHandlers != sol::nil) + { + for (const auto& [key, fn] : sol::table(eventHandlers)) + { + std::string_view eventName = key.as(); + auto it = mEventHandlers.find(eventName); + if (it == mEventHandlers.end()) + it = mEventHandlers.emplace(std::string(eventName), EventHandlerList()).first; + insertHandler(it->second, scriptId, fn); + } + } + + if (script.mInterfaceName.empty() == script.mInterface.has_value()) + { Log(Debug::Error) << mNamePrefix << "[" << path << "]: 'interfaceName' should always be used together with 'interface'"; - else if (!interfaceName.empty()) - script.as()[INTERFACE] = mPublicInterfaces[interfaceName] = makeReadOnly(publicInterface); - mScriptOrder.push_back(path); - mScripts[path].mInterface = std::move(script); + script.mInterfaceName.clear(); + script.mInterface = sol::nil; + } + else if (script.mInterface) + { + script.mInterface = makeReadOnly(*script.mInterface); + insertInterface(scriptId, script); + } + return true; } catch (std::exception& e) { - mScripts.erase(path); + mScripts.erase(scriptId); Log(Debug::Error) << "Can't start " << mNamePrefix << "[" << path << "]; " << e.what(); return false; } } - bool ScriptsContainer::removeScript(const std::string& path) + void ScriptsContainer::removeScript(int scriptId) { - auto scriptIter = mScripts.find(path); + auto scriptIter = mScripts.find(scriptId); if (scriptIter == mScripts.end()) - return false; // no such script - scriptIter->second.mHiddenData[ScriptId::KEY] = sol::nil; - sol::object& script = scriptIter->second.mInterface; - if (getFieldOrNil(script, INTERFACE_NAME) != sol::nil) - { - std::string_view interfaceName = getFieldOrNil(script, INTERFACE_NAME).as(); - if (mPublicInterfaces[interfaceName] == getFieldOrNil(script, INTERFACE)) - { - mPublicInterfaces[interfaceName] = sol::nil; - auto prevIt = mScriptOrder.rbegin(); - while (*prevIt != path) - prevIt++; - prevIt++; - while (prevIt != mScriptOrder.rend()) - { - sol::object& prevScript = mScripts[*(prevIt++)].mInterface; - sol::object prevInterfaceName = getFieldOrNil(prevScript, INTERFACE_NAME); - if (prevInterfaceName != sol::nil && prevInterfaceName.as() == interfaceName) - { - mPublicInterfaces[interfaceName] = getFieldOrNil(prevScript, INTERFACE); - break; - } - } - } - } - sol::object engineHandlers = getFieldOrNil(script, ENGINE_HANDLERS); - if (engineHandlers != sol::nil) - { - for (auto& [key, value] : sol::table(engineHandlers)) - { - std::string_view handlerName = key.as(); - auto handlerIter = mEngineHandlers.find(handlerName); - if (handlerIter == mEngineHandlers.end()) - continue; - std::vector& list = handlerIter->second->mList; - list.erase(std::find(list.begin(), list.end(), value.as())); - } - } - sol::object eventHandlers = getFieldOrNil(script, EVENT_HANDLERS); - if (eventHandlers != sol::nil) - { - for (auto& [key, value] : sol::table(eventHandlers)) - { - EventHandlerList& list = mEventHandlers.find(key.as())->second; - list.erase(std::find(list.begin(), list.end(), value.as())); - } - } + return; // no such script + Script& script = scriptIter->second; + if (script.mInterface) + removeInterface(scriptId, script); + script.mHiddenData[ScriptId::KEY] = sol::nil; mScripts.erase(scriptIter); - mScriptOrder.erase(std::find(mScriptOrder.begin(), mScriptOrder.end(), path)); - return true; + for (auto& [_, handlers] : mEngineHandlers) + removeHandler(handlers->mList, scriptId); + for (auto& [_, handlers] : mEventHandlers) + removeHandler(handlers, scriptId); } - void ScriptsContainer::parseEventHandlers(sol::table handlers, std::string_view scriptPath) + void ScriptsContainer::insertInterface(int scriptId, const Script& script) { - for (auto& [key, value] : handlers) + assert(script.mInterface); + const Script* prev = nullptr; + const Script* next = nullptr; + int nextId = 0; + for (const auto& [otherId, otherScript] : mScripts) { - std::string_view eventName = key.as(); - auto it = mEventHandlers.find(eventName); - if (it == mEventHandlers.end()) - it = mEventHandlers.insert({std::string(eventName), EventHandlerList()}).first; - it->second.push_back(value); - } - } - - void ScriptsContainer::parseEngineHandlers(sol::table handlers, std::string_view scriptPath) - { - for (auto& [key, value] : handlers) - { - std::string_view handlerName = key.as(); - if (handlerName == HANDLER_LOAD || handlerName == HANDLER_SAVE) - continue; // save and load are handled separately - auto it = mEngineHandlers.find(handlerName); - if (it == mEngineHandlers.end()) - Log(Debug::Error) << "Not supported handler '" << handlerName << "' in " << mNamePrefix << "[" << scriptPath << "]"; + if (scriptId == otherId || script.mInterfaceName != otherScript.mInterfaceName) + continue; + if (otherId < scriptId) + prev = &otherScript; else - it->second->mList.push_back(value); + { + next = &otherScript; + nextId = otherId; + break; + } } + if (prev && script.mOnOverride) + { + try { LuaUtil::call(*script.mOnOverride, *prev->mInterface); } + catch (std::exception& e) { printError(scriptId, "onInterfaceOverride failed", e); } + } + if (next && next->mOnOverride) + { + try { LuaUtil::call(*next->mOnOverride, *script.mInterface); } + catch (std::exception& e) { printError(nextId, "onInterfaceOverride failed", e); } + } + if (next == nullptr) + mPublicInterfaces[script.mInterfaceName] = *script.mInterface; + } + + void ScriptsContainer::removeInterface(int scriptId, const Script& script) + { + assert(script.mInterface); + const Script* prev = nullptr; + const Script* next = nullptr; + int nextId = 0; + for (const auto& [otherId, otherScript] : mScripts) + { + if (scriptId == otherId || script.mInterfaceName != otherScript.mInterfaceName) + continue; + if (otherId < scriptId) + prev = &otherScript; + else + { + next = &otherScript; + nextId = otherId; + break; + } + } + if (next) + { + if (next->mOnOverride) + { + sol::object prevInterface = sol::nil; + if (prev) + prevInterface = *prev->mInterface; + try { LuaUtil::call(*next->mOnOverride, prevInterface); } + catch (std::exception& e) { printError(nextId, "onInterfaceOverride failed", e); } + } + } + else if (prev) + mPublicInterfaces[script.mInterfaceName] = *prev->mInterface; + else + mPublicInterfaces[script.mInterfaceName] = sol::nil; + } + + void ScriptsContainer::insertHandler(std::vector& list, int scriptId, sol::function fn) + { + list.emplace_back(); + int pos = list.size() - 1; + while (pos > 0 && list[pos - 1].mScriptId > scriptId) + { + list[pos] = std::move(list[pos - 1]); + pos--; + } + list[pos].mScriptId = scriptId; + list[pos].mFn = std::move(fn); + } + + void ScriptsContainer::removeHandler(std::vector& list, int scriptId) + { + list.erase(std::remove_if(list.begin(), list.end(), + [scriptId](const Handler& h){ return h.mScriptId == scriptId; }), + list.end()); } void ScriptsContainer::receiveEvent(std::string_view eventName, std::string_view eventData) @@ -191,13 +284,14 @@ namespace LuaUtil { try { - sol::object res = LuaUtil::call(list[i], data); + sol::object res = LuaUtil::call(list[i].mFn, data); if (res != sol::nil && !res.as()) break; // Skip other handlers if 'false' was returned. } catch (std::exception& e) { - Log(Debug::Error) << mNamePrefix << " eventHandler[" << eventName << "] failed. " << e.what(); + Log(Debug::Error) << mNamePrefix << "[" << scriptPath(list[i].mScriptId) + << "] eventHandler[" << eventName << "] failed. " << e.what(); } } } @@ -208,9 +302,19 @@ namespace LuaUtil mEngineHandlers[h->mName] = h; } + void ScriptsContainer::callOnInit(int scriptId, const sol::function& onInit) + { + try + { + const std::string& data = mLua.getConfiguration()[scriptId].mInitializationData; + LuaUtil::call(onInit, deserialize(mLua.sol(), data, mSerializer)); + } + catch (std::exception& e) { printError(scriptId, "onInit failed", e); } + } + void ScriptsContainer::save(ESM::LuaScripts& data) { - std::map> timers; + std::map> timers; auto saveTimerFn = [&](const Timer& timer, TimeUnit timeUnit) { if (!timer.mSerializable) @@ -220,78 +324,85 @@ namespace LuaUtil savedTimer.mUnit = timeUnit; savedTimer.mCallbackName = std::get(timer.mCallback); savedTimer.mCallbackArgument = timer.mSerializedArg; - if (timers.count(timer.mScript) == 0) - timers[timer.mScript] = {}; - timers[timer.mScript].push_back(std::move(savedTimer)); + timers[timer.mScriptId].push_back(std::move(savedTimer)); }; for (const Timer& timer : mSecondsTimersQueue) saveTimerFn(timer, TimeUnit::SECONDS); for (const Timer& timer : mHoursTimersQueue) saveTimerFn(timer, TimeUnit::HOURS); data.mScripts.clear(); - for (const std::string& path : mScriptOrder) + for (auto& [scriptId, script] : mScripts) { ESM::LuaScript savedScript; - savedScript.mScriptPath = path; - sol::object handler = getFieldOrNil(mScripts[path].mInterface, ENGINE_HANDLERS, HANDLER_SAVE); - if (handler != sol::nil) + savedScript.mScriptPath = script.mHiddenData.get(ScriptId::KEY).mPath; + if (script.mOnSave) { try { - sol::object state = LuaUtil::call(handler); + sol::object state = LuaUtil::call(*script.mOnSave); savedScript.mData = serialize(state, mSerializer); } - catch (std::exception& e) - { - Log(Debug::Error) << mNamePrefix << "[" << path << "] onSave failed: " << e.what(); - } + catch (std::exception& e) { printError(scriptId, "onSave failed", e); } } - auto timersIt = timers.find(path); + auto timersIt = timers.find(scriptId); if (timersIt != timers.end()) savedScript.mTimers = std::move(timersIt->second); data.mScripts.push_back(std::move(savedScript)); } } - void ScriptsContainer::load(const ESM::LuaScripts& data, bool resetScriptList) + void ScriptsContainer::load(const ESM::LuaScripts& data) { - std::map scriptsWithoutSavedData; - if (resetScriptList) + removeAllScripts(); + const ScriptsConfiguration& cfg = mLua.getConfiguration(); + + std::map scripts; + for (int scriptId : mLua.getConfiguration().getListByFlag(mAutoStartMode)) + scripts[scriptId] = nullptr; + for (const ESM::LuaScript& s : data.mScripts) { - removeAllScripts(); - for (const ESM::LuaScript& script : data.mScripts) - addNewScript(script.mScriptPath); - } - else - scriptsWithoutSavedData = mScripts; - mSecondsTimersQueue.clear(); - mHoursTimersQueue.clear(); - for (const ESM::LuaScript& script : data.mScripts) - { - auto iter = mScripts.find(script.mScriptPath); - if (iter == mScripts.end()) + std::optional scriptId = cfg.findId(s.mScriptPath); + if (!scriptId) + { + Log(Debug::Verbose) << "Ignoring " << mNamePrefix << "[" << s.mScriptPath << "]; script not registered"; continue; - scriptsWithoutSavedData.erase(iter->first); - iter->second.mHiddenData.get(TEMPORARY_TIMER_CALLBACKS).clear(); - try + } + if (!(cfg[*scriptId].mFlags & (ESM::LuaScriptCfg::sCustom | mAutoStartMode))) { - sol::object handler = getFieldOrNil(iter->second.mInterface, ENGINE_HANDLERS, HANDLER_LOAD); - if (handler != sol::nil) + Log(Debug::Verbose) << "Ignoring " << mNamePrefix << "[" << s.mScriptPath << "]; this script is not allowed here"; + continue; + } + scripts[*scriptId] = &s; + } + + for (const auto& [scriptId, savedScript] : scripts) + { + std::optional onInit, onLoad; + if (!addScript(scriptId, onInit, onLoad)) + continue; + if (savedScript == nullptr) + { + if (onInit) + callOnInit(scriptId, *onInit); + continue; + } + if (onLoad) + { + try { - sol::object state = deserialize(mLua.sol(), script.mData, mSerializer); - LuaUtil::call(handler, state); + sol::object state = deserialize(mLua.sol(), savedScript->mData, mSerializer); + sol::object initializationData = + deserialize(mLua.sol(), mLua.getConfiguration()[scriptId].mInitializationData, mSerializer); + LuaUtil::call(*onLoad, state, initializationData); } + catch (std::exception& e) { printError(scriptId, "onLoad failed", e); } } - catch (std::exception& e) - { - Log(Debug::Error) << mNamePrefix << "[" << script.mScriptPath << "] onLoad failed: " << e.what(); - } - for (const ESM::LuaTimer& savedTimer : script.mTimers) + for (const ESM::LuaTimer& savedTimer : savedScript->mTimers) { Timer timer; timer.mCallback = savedTimer.mCallbackName; timer.mSerializable = true; - timer.mScript = script.mScriptPath; + timer.mScriptId = scriptId; timer.mTime = savedTimer.mTime; try @@ -306,24 +417,10 @@ namespace LuaUtil else mSecondsTimersQueue.push_back(std::move(timer)); } - catch (std::exception& e) - { - Log(Debug::Error) << mNamePrefix << "[" << script.mScriptPath << "] can not load timer: " << e.what(); - } - } - } - for (auto& [path, script] : scriptsWithoutSavedData) - { - script.mHiddenData.get(TEMPORARY_TIMER_CALLBACKS).clear(); - sol::object handler = getFieldOrNil(script.mInterface, ENGINE_HANDLERS, HANDLER_LOAD); - if (handler == sol::nil) - continue; - try { LuaUtil::call(handler); } - catch (std::exception& e) - { - Log(Debug::Error) << mNamePrefix << "[" << path << "] onLoad failed: " << e.what(); + catch (std::exception& e) { printError(scriptId, "can not load timer", e); } } } + std::make_heap(mSecondsTimersQueue.begin(), mSecondsTimersQueue.end()); std::make_heap(mHoursTimersQueue.begin(), mHoursTimersQueue.end()); } @@ -334,12 +431,13 @@ namespace LuaUtil script.mHiddenData[ScriptId::KEY] = sol::nil; } + // Note: shouldn't be called from destructor because mEngineHandlers has pointers on + // external objects that are already removed during child class destruction. void ScriptsContainer::removeAllScripts() { for (auto& [_, script] : mScripts) script.mHiddenData[ScriptId::KEY] = sol::nil; mScripts.clear(); - mScriptOrder.clear(); for (auto& [_, handlers] : mEngineHandlers) handlers->mList.clear(); mEventHandlers.clear(); @@ -351,17 +449,17 @@ namespace LuaUtil mPublicInterfaces[sol::meta_function::index] = mPublicInterfaces; } - sol::table ScriptsContainer::getHiddenData(const std::string& scriptPath) + sol::table ScriptsContainer::getHiddenData(int scriptId) { - auto it = mScripts.find(scriptPath); + auto it = mScripts.find(scriptId); if (it == mScripts.end()) throw std::logic_error("ScriptsContainer::getHiddenData: script doesn't exist"); return it->second.mHiddenData; } - void ScriptsContainer::registerTimerCallback(const std::string& scriptPath, std::string_view callbackName, sol::function callback) + void ScriptsContainer::registerTimerCallback(int scriptId, std::string_view callbackName, sol::function callback) { - getHiddenData(scriptPath)[REGISTERED_TIMER_CALLBACKS][callbackName] = std::move(callback); + getHiddenData(scriptId)[REGISTERED_TIMER_CALLBACKS][callbackName] = std::move(callback); } void ScriptsContainer::insertTimer(std::vector& timerQueue, Timer&& t) @@ -370,12 +468,12 @@ namespace LuaUtil std::push_heap(timerQueue.begin(), timerQueue.end()); } - void ScriptsContainer::setupSerializableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath, + void ScriptsContainer::setupSerializableTimer(TimeUnit timeUnit, double time, int scriptId, std::string_view callbackName, sol::object callbackArg) { Timer t; t.mCallback = std::string(callbackName); - t.mScript = scriptPath; + t.mScriptId = scriptId; t.mSerializable = true; t.mTime = time; t.mArg = callbackArg; @@ -383,15 +481,15 @@ namespace LuaUtil insertTimer(timeUnit == TimeUnit::HOURS ? mHoursTimersQueue : mSecondsTimersQueue, std::move(t)); } - void ScriptsContainer::setupUnsavableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath, sol::function callback) + void ScriptsContainer::setupUnsavableTimer(TimeUnit timeUnit, double time, int scriptId, sol::function callback) { Timer t; - t.mScript = scriptPath; + t.mScriptId = scriptId; t.mSerializable = false; t.mTime = time; t.mCallback = mTemporaryCallbackCounter; - getHiddenData(scriptPath)[TEMPORARY_TIMER_CALLBACKS][mTemporaryCallbackCounter] = std::move(callback); + getHiddenData(scriptId)[TEMPORARY_TIMER_CALLBACKS][mTemporaryCallbackCounter] = std::move(callback); mTemporaryCallbackCounter++; insertTimer(timeUnit == TimeUnit::HOURS ? mHoursTimersQueue : mSecondsTimersQueue, std::move(t)); @@ -401,7 +499,7 @@ namespace LuaUtil { try { - sol::table data = getHiddenData(t.mScript); + sol::table data = getHiddenData(t.mScriptId); if (t.mSerializable) { const std::string& callbackName = std::get(t.mCallback); @@ -421,10 +519,7 @@ namespace LuaUtil callbacks[id] = sol::nil; } } - catch (std::exception& e) - { - Log(Debug::Error) << mNamePrefix << "[" << t.mScript << "] callTimer failed: " << e.what(); - } + catch (std::exception& e) { printError(t.mScriptId, "callTimer failed", e); } } void ScriptsContainer::updateTimerQueue(std::vector& timerQueue, double time) diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp index 69aa18e940..184072eef1 100644 --- a/components/lua/scriptscontainer.hpp +++ b/components/lua/scriptscontainer.hpp @@ -17,7 +17,7 @@ namespace LuaUtil // ScriptsContainer is a base class for all scripts containers (LocalScripts, // GlobalScripts, PlayerScripts, etc). Each script runs in a separate sandbox. // Scripts from different containers can interact to each other only via events. -// Scripts within one container can interact via interfaces (not implemented yet). +// Scripts within one container can interact via interfaces. // All scripts from one container have the same set of API packages available. // // Each script should return a table in a specific format that describes its @@ -42,11 +42,12 @@ namespace LuaUtil // -- An error is printed if unknown handler is specified. // engineHandlers = { // onUpdate = update, +// onInit = function(initData) ... end, -- used when the script is just created (not loaded) // onSave = function() return ... end, -// onLoad = function(state) ... end, -- "state" is the data that was earlier returned by onSave +// onLoad = function(state, initData) ... end, -- "state" is the data that was earlier returned by onSave // -// -- Works only if ScriptsContainer::registerEngineHandler is overloaded in a child class -// -- and explicitly supports 'onSomethingElse' +// -- Works only if a child class has passed a EngineHandlerList +// -- for 'onSomethingElse' to ScriptsContainer::registerEngineHandlers. // onSomethingElse = function() print("something else") end // }, // @@ -65,30 +66,36 @@ namespace LuaUtil constexpr static std::string_view KEY = "_id"; ScriptsContainer* mContainer; - std::string mPath; + int mIndex; // index in LuaUtil::ScriptsConfiguration + std::string mPath; // path to the script source in VFS std::string toString() const; }; using TimeUnit = ESM::LuaTimer::TimeUnit; // `namePrefix` is a common prefix for all scripts in the container. Used in logs for error messages and `print` output. - ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix); + // `autoStartMode` specifies the list of scripts that should be autostarted in this container; the list itself is + // stored in ScriptsConfiguration: lua->getConfiguration().getListByFlag(autoStartMode). + ScriptsContainer(LuaState* lua, std::string_view namePrefix, ESM::LuaScriptCfg::Flags autoStartMode = 0); + ScriptsContainer(const ScriptsContainer&) = delete; ScriptsContainer(ScriptsContainer&&) = delete; virtual ~ScriptsContainer(); + ESM::LuaScriptCfg::Flags getAutoStartMode() const { return mAutoStartMode; } + // Adds package that will be available (via `require`) for all scripts in the container. // Automatically applies LuaUtil::makeReadOnly to the package. - void addPackage(const std::string& packageName, sol::object package); + void addPackage(std::string packageName, sol::object package); - // Finds a file with given path in the virtual file system, starts as a new script, and adds it to the container. - // Returns `true` if the script was successfully added. Otherwise prints an error message and returns `false`. - // `false` can be returned if either file not found or has syntax errors or such script already exists in the container. - bool addNewScript(const std::string& path); + // Gets script with given id from ScriptsConfiguration, finds the source in the virtual file system, starts as a new script, + // adds it to the container, and calls onInit for this script. Returns `true` if the script was successfully added. + // The script should have CUSTOM flag. If the flag is not set, or file not found, or has syntax errors, returns false. + // If such script already exists in the container, then also returns false. + bool addCustomScript(int scriptId); - // Removes script. Returns `true` if it was successfully removed. - bool removeScript(const std::string& path); - void removeAllScripts(); + bool hasScript(int scriptId) const { return mScripts.count(scriptId) != 0; } + void removeScript(int scriptId); // Processes timers. gameSeconds and gameHours are time (in seconds and in game hours) passed from the game start. void processTimers(double gameSeconds, double gameHours); @@ -107,22 +114,22 @@ namespace LuaUtil // only built-in types and types from util package can be serialized. void setSerializer(const UserdataSerializer* serializer) { mSerializer = serializer; } + // Starts scripts according to `autoStartMode` and calls `onInit` for them. Not needed if `load` is used. + void addAutoStartedScripts(); + + // Removes all scripts including the auto started. + void removeAllScripts(); + // Calls engineHandler "onSave" for every script and saves the list of the scripts with serialized data to ESM::LuaScripts. void save(ESM::LuaScripts&); - // Calls engineHandler "onLoad" for every script with given data. - // If resetScriptList=true, then removes all currently active scripts and runs the scripts that were saved in ESM::LuaScripts. - // If resetScriptList=false, then list of running scripts is not changed, only engineHandlers "onLoad" are called. - void load(const ESM::LuaScripts&, bool resetScriptList); - - // Returns the hidden data of a script. - // Each script has a corresponding "hidden data" - a lua table that is not accessible from the script itself, - // but can be used by built-in packages. It contains ScriptId and can contain any arbitrary data. - sol::table getHiddenData(const std::string& scriptPath); + // Removes all scripts; starts scripts according to `autoStartMode` and + // loads the savedScripts. Runs "onLoad" for each script. + void load(const ESM::LuaScripts& savedScripts); // Callbacks for serializable timers should be registered in advance. // The script with the given path should already present in the container. - void registerTimerCallback(const std::string& scriptPath, std::string_view callbackName, sol::function callback); + void registerTimerCallback(int scriptId, std::string_view callbackName, sol::function callback); // Sets up a timer, that can be automatically saved and loaded. // timeUnit - game seconds (TimeUnit::Seconds) or game hours (TimeUnit::Hours). @@ -130,18 +137,24 @@ namespace LuaUtil // scriptPath - script path in VFS is used as script id. The script with the given path should already present in the container. // callbackName - callback (should be registered in advance) for this timer. // callbackArg - parameter for the callback (should be serializable). - void setupSerializableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath, + void setupSerializableTimer(TimeUnit timeUnit, double time, int scriptId, std::string_view callbackName, sol::object callbackArg); // Creates a timer. `callback` is an arbitrary Lua function. This type of timers is called "unsavable" // because it can not be stored in saves. I.e. loading a saved game will not fully restore the state. - void setupUnsavableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath, sol::function callback); + void setupUnsavableTimer(TimeUnit timeUnit, double time, int scriptId, sol::function callback); protected: + struct Handler + { + int mScriptId; + sol::function mFn; + }; + struct EngineHandlerList { std::string_view mName; - std::vector mList; + std::vector mList; // "name" must be string literal explicit EngineHandlerList(std::string_view name) : mName(name) {} @@ -151,12 +164,13 @@ namespace LuaUtil template void callEngineHandlers(EngineHandlerList& handlers, const Args&... args) { - for (sol::protected_function& handler : handlers.mList) + for (Handler& handler : handlers.mList) { - try { LuaUtil::call(handler, args...); } + try { LuaUtil::call(handler.mFn, args...); } catch (std::exception& e) { - Log(Debug::Error) << mNamePrefix << " " << handlers.mName << " failed. " << e.what(); + Log(Debug::Error) << mNamePrefix << "[" << scriptPath(handler.mScriptId) << "] " + << handlers.mName << " failed. " << e.what(); } } } @@ -171,34 +185,49 @@ namespace LuaUtil private: struct Script { - sol::object mInterface; // returned value of the script (sol::table or nil) + std::optional mOnSave; + std::optional mOnOverride; + std::optional mInterface; + std::string mInterfaceName; sol::table mHiddenData; }; struct Timer { double mTime; bool mSerializable; - std::string mScript; + int mScriptId; std::variant mCallback; // string if serializable, integer otherwise sol::object mArg; std::string mSerializedArg; bool operator<(const Timer& t) const { return mTime > t.mTime; } }; - using EventHandlerList = std::vector; + using EventHandlerList = std::vector; - void parseEngineHandlers(sol::table handlers, std::string_view scriptPath); - void parseEventHandlers(sol::table handlers, std::string_view scriptPath); + // Add to container without calling onInit/onLoad. + bool addScript(int scriptId, std::optional& onInit, std::optional& onLoad); + // Returns the hidden data of a script. + // Each script has a corresponding "hidden data" - a lua table that is not accessible from the script itself, + // but can be used by built-in packages. It contains ScriptId and can contain any arbitrary data. + sol::table getHiddenData(int scriptId); + + void printError(int scriptId, std::string_view msg, const std::exception& e); + const std::string& scriptPath(int scriptId) const { return mLua.getConfiguration()[scriptId].mScriptPath; } + void callOnInit(int scriptId, const sol::function& onInit); void callTimer(const Timer& t); void updateTimerQueue(std::vector& timerQueue, double time); static void insertTimer(std::vector& timerQueue, Timer&& t); + static void insertHandler(std::vector& list, int scriptId, sol::function fn); + static void removeHandler(std::vector& list, int scriptId); + void insertInterface(int scriptId, const Script& script); + void removeInterface(int scriptId, const Script& script); + ESM::LuaScriptCfg::Flags mAutoStartMode; const UserdataSerializer* mSerializer = nullptr; - std::map API; + std::map mAPI; - std::vector mScriptOrder; - std::map mScripts; + std::map mScripts; sol::table mPublicInterfaces; EngineHandlerList mUpdateHandlers{"onUpdate"};