From 1dd7a1525558b733fccc10ba1cca1a46b8e30d4a Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Tue, 5 Sep 2023 22:45:38 +0200 Subject: [PATCH 01/39] Draft: add new type of Lua scripts - menu scripts --- apps/openmw/CMakeLists.txt | 11 +- apps/openmw/mwbase/statemanager.hpp | 3 + apps/openmw/mwbase/windowmanager.hpp | 3 +- apps/openmw/mwgui/windowmanagerimp.cpp | 7 +- apps/openmw/mwgui/windowmanagerimp.hpp | 3 +- apps/openmw/mwlua/corebindings.cpp | 136 +++++++++ apps/openmw/mwlua/corebindings.hpp | 19 ++ apps/openmw/mwlua/globalscripts.hpp | 4 - apps/openmw/mwlua/luabindings.cpp | 328 ++-------------------- apps/openmw/mwlua/luabindings.hpp | 10 +- apps/openmw/mwlua/luamanagerimp.cpp | 38 ++- apps/openmw/mwlua/luamanagerimp.hpp | 2 + apps/openmw/mwlua/menuscripts.cpp | 114 ++++++++ apps/openmw/mwlua/menuscripts.hpp | 46 +++ apps/openmw/mwlua/soundbindings.cpp | 9 +- apps/openmw/mwlua/types/types.cpp | 7 +- apps/openmw/mwlua/uibindings.cpp | 11 +- apps/openmw/mwlua/worldbindings.cpp | 215 ++++++++++++++ apps/openmw/mwlua/worldbindings.hpp | 13 + apps/openmw/mwstate/statemanagerimp.cpp | 12 + apps/openmw/mwstate/statemanagerimp.hpp | 5 + components/esm/luascripts.hpp | 6 +- components/lua/configuration.cpp | 1 + components/lua/configuration.hpp | 1 + components/lua/storage.cpp | 26 +- components/lua/storage.hpp | 10 +- files/data/CMakeLists.txt | 3 +- files/data/builtin.omwscripts | 1 + files/data/scripts/omw/console/menu.lua | 114 ++++++++ files/data/scripts/omw/console/player.lua | 7 +- 30 files changed, 818 insertions(+), 347 deletions(-) create mode 100644 apps/openmw/mwlua/corebindings.cpp create mode 100644 apps/openmw/mwlua/corebindings.hpp create mode 100644 apps/openmw/mwlua/menuscripts.cpp create mode 100644 apps/openmw/mwlua/menuscripts.hpp create mode 100644 apps/openmw/mwlua/worldbindings.cpp create mode 100644 apps/openmw/mwlua/worldbindings.hpp create mode 100644 files/data/scripts/omw/console/menu.lua diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index a05d20af73..b987b1d124 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -61,10 +61,13 @@ add_openmw_dir (mwscript add_openmw_dir (mwlua luamanagerimp object objectlists userdataserializer luaevents engineevents objectvariant - context globalscripts localscripts playerscripts luabindings objectbindings cellbindings mwscriptbindings - camerabindings vfsbindings uibindings soundbindings inputbindings nearbybindings postprocessingbindings stats debugbindings - types/types types/door types/item types/actor types/container types/lockable types/weapon types/npc types/creature types/player types/activator types/book types/lockpick types/probe types/apparatus types/potion types/ingredient types/misc types/repair types/armor types/light types/static types/clothing types/levelledlist types/terminal - worker magicbindings factionbindings + context menuscripts globalscripts localscripts playerscripts luabindings objectbindings cellbindings + mwscriptbindings camerabindings vfsbindings uibindings soundbindings inputbindings nearbybindings + postprocessingbindings stats debugbindings corebindings worldbindings worker magicbindings factionbindings + types/types types/door types/item types/actor types/container types/lockable types/weapon types/npc + types/creature types/player types/activator types/book types/lockpick types/probe types/apparatus + types/potion types/ingredient types/misc types/repair types/armor types/light types/static + types/clothing types/levelledlist types/terminal ) add_openmw_dir (mwsound diff --git a/apps/openmw/mwbase/statemanager.hpp b/apps/openmw/mwbase/statemanager.hpp index 1a25da32b0..35435e1430 100644 --- a/apps/openmw/mwbase/statemanager.hpp +++ b/apps/openmw/mwbase/statemanager.hpp @@ -44,6 +44,9 @@ namespace MWBase virtual void askLoadRecent() = 0; + virtual void requestNewGame() = 0; + virtual void requestLoad(const std::filesystem::path& filepath) = 0; + virtual State getState() const = 0; virtual void newGame(bool bypass = false) = 0; diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index f225ebf24e..92ad28647b 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -164,7 +164,8 @@ namespace MWBase virtual void setConsoleSelectedObject(const MWWorld::Ptr& object) = 0; virtual MWWorld::Ptr getConsoleSelectedObject() const = 0; - virtual void setConsoleMode(const std::string& mode) = 0; + virtual void setConsoleMode(std::string_view mode) = 0; + virtual const std::string& getConsoleMode() = 0; static constexpr std::string_view sConsoleColor_Default = "#FFFFFF"; static constexpr std::string_view sConsoleColor_Error = "#FF2222"; diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index 43b37623d5..d832a25023 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -2173,11 +2173,16 @@ namespace MWGui mConsole->print(msg, color); } - void WindowManager::setConsoleMode(const std::string& mode) + void WindowManager::setConsoleMode(std::string_view mode) { mConsole->setConsoleMode(mode); } + const std::string& WindowManager::getConsoleMode() + { + return mConsole->getConsoleMode(); + } + void WindowManager::createCursors() { MyGUI::ResourceManager::EnumeratorPtr enumerator = MyGUI::ResourceManager::getInstance().getEnumerator(); diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index 5f6b12b7e5..c9eced4e94 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -191,7 +191,8 @@ namespace MWGui void setConsoleSelectedObject(const MWWorld::Ptr& object) override; MWWorld::Ptr getConsoleSelectedObject() const override; void printToConsole(const std::string& msg, std::string_view color) override; - void setConsoleMode(const std::string& mode) override; + void setConsoleMode(std::string_view mode) override; + const std::string& getConsoleMode() override; /// Set time left for the player to start drowning (update the drowning bar) /// @param time time left to start drowning diff --git a/apps/openmw/mwlua/corebindings.cpp b/apps/openmw/mwlua/corebindings.cpp new file mode 100644 index 0000000000..b9ff991406 --- /dev/null +++ b/apps/openmw/mwlua/corebindings.cpp @@ -0,0 +1,136 @@ +#include "corebindings.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/statemanager.hpp" +#include "../mwbase/world.hpp" +#include "../mwworld/datetimemanager.hpp" +#include "../mwworld/esmstore.hpp" + +#include "factionbindings.hpp" +#include "luaevents.hpp" +#include "magicbindings.hpp" +#include "soundbindings.hpp" +#include "stats.hpp" + +namespace MWLua +{ + static sol::table initContentFilesBindings(sol::state_view& lua) + { + const std::vector& contentList = MWBase::Environment::get().getWorld()->getContentFiles(); + sol::table list(lua, sol::create); + for (size_t i = 0; i < contentList.size(); ++i) + list[i + 1] = Misc::StringUtils::lowerCase(contentList[i]); + sol::table res(lua, sol::create); + res["list"] = LuaUtil::makeReadOnly(list); + res["indexOf"] = [&contentList](std::string_view contentFile) -> sol::optional { + for (size_t i = 0; i < contentList.size(); ++i) + if (Misc::StringUtils::ciEqual(contentList[i], contentFile)) + return i + 1; + return sol::nullopt; + }; + res["has"] = [&contentList](std::string_view contentFile) -> bool { + for (size_t i = 0; i < contentList.size(); ++i) + if (Misc::StringUtils::ciEqual(contentList[i], contentFile)) + return true; + return false; + }; + return LuaUtil::makeReadOnly(res); + } + + void addCoreTimeBindings(sol::table& api, const Context& context) + { + MWWorld::DateTimeManager* timeManager = MWBase::Environment::get().getWorld()->getTimeManager(); + + api["getSimulationTime"] = [timeManager]() { return timeManager->getSimulationTime(); }; + api["getSimulationTimeScale"] = [timeManager]() { return timeManager->getSimulationTimeScale(); }; + api["getGameTime"] = [timeManager]() { return timeManager->getGameTime(); }; + api["getGameTimeScale"] = [timeManager]() { return timeManager->getGameTimeScale(); }; + api["isWorldPaused"] = [timeManager]() { return timeManager->isPaused(); }; + api["getRealTime"] = []() { + return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); + }; + } + + sol::table initCorePackage(const Context& context) + { + auto* lua = context.mLua; + + if (lua->sol()["openmw_core"] != sol::nil) + return lua->sol()["openmw_core"]; + + sol::table api(lua->sol(), sol::create); + api["API_REVISION"] = Version::getLuaApiRevision(); // specified in CMakeLists.txt + api["quit"] = [lua]() { + Log(Debug::Warning) << "Quit requested by a Lua script.\n" << lua->debugTraceback(); + MWBase::Environment::get().getStateManager()->requestQuit(); + }; + api["sendGlobalEvent"] = [context](std::string eventName, const sol::object& eventData) { + context.mLuaEvents->addGlobalEvent( + { std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer) }); + }; + api["contentFiles"] = initContentFilesBindings(lua->sol()); + api["sound"] = initCoreSoundBindings(context); + api["getFormId"] = [](std::string_view contentFile, unsigned int index) -> std::string { + const std::vector& contentList = MWBase::Environment::get().getWorld()->getContentFiles(); + for (size_t i = 0; i < contentList.size(); ++i) + if (Misc::StringUtils::ciEqual(contentList[i], contentFile)) + return ESM::RefId(ESM::FormId{ index, int(i) }).serializeText(); + throw std::runtime_error("Content file not found: " + std::string(contentFile)); + }; + addCoreTimeBindings(api, context); + api["magic"] = initCoreMagicBindings(context); + api["stats"] = initCoreStatsBindings(context); + + initCoreFactionBindings(context); + api["factions"] = &MWBase::Environment::get().getESMStore()->get(); + + api["l10n"] = LuaUtil::initL10nLoader(lua->sol(), MWBase::Environment::get().getL10nManager()); + const MWWorld::Store* gmstStore + = &MWBase::Environment::get().getESMStore()->get(); + api["getGMST"] = [lua = context.mLua, gmstStore](const std::string& setting) -> sol::object { + const ESM::GameSetting* gmst = gmstStore->search(setting); + if (gmst == nullptr) + return sol::nil; + const ESM::Variant& value = gmst->mValue; + switch (value.getType()) + { + case ESM::VT_Float: + return sol::make_object(lua->sol(), value.getFloat()); + case ESM::VT_Short: + case ESM::VT_Long: + case ESM::VT_Int: + return sol::make_object(lua->sol(), value.getInteger()); + case ESM::VT_String: + return sol::make_object(lua->sol(), value.getString()); + case ESM::VT_Unknown: + case ESM::VT_None: + break; + } + return sol::nil; + }; + + lua->sol()["openmw_core"] = LuaUtil::makeReadOnly(api); + return lua->sol()["openmw_core"]; + } + + sol::table initCorePackageForMenuScripts(const Context& context) + { + sol::table api(context.mLua->sol(), sol::create); + for (auto& [k, v] : LuaUtil::getMutableFromReadOnly(initCorePackage(context))) + api[k] = v; + api["sendGlobalEvent"] = sol::nil; + api["sound"] = sol::nil; + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/corebindings.hpp b/apps/openmw/mwlua/corebindings.hpp new file mode 100644 index 0000000000..d086d3884c --- /dev/null +++ b/apps/openmw/mwlua/corebindings.hpp @@ -0,0 +1,19 @@ +#ifndef MWLUA_COREBINDINGS_H +#define MWLUA_COREBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + void addCoreTimeBindings(sol::table& api, const Context& context); + + sol::table initCorePackage(const Context&); + + // Returns `openmw.core`, but disables the functionality that shouldn't + // be availabe in menu scripts (to prevent cheating in mutiplayer via menu console). + sol::table initCorePackageForMenuScripts(const Context&); +} + +#endif // MWLUA_COREBINDINGS_H diff --git a/apps/openmw/mwlua/globalscripts.hpp b/apps/openmw/mwlua/globalscripts.hpp index afaadb9d0a..37fff22f99 100644 --- a/apps/openmw/mwlua/globalscripts.hpp +++ b/apps/openmw/mwlua/globalscripts.hpp @@ -1,10 +1,6 @@ #ifndef MWLUA_GLOBALSCRIPTS_H #define MWLUA_GLOBALSCRIPTS_H -#include -#include -#include - #include #include diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp index a50459502b..931cd43296 100644 --- a/apps/openmw/mwlua/luabindings.cpp +++ b/apps/openmw/mwlua/luabindings.cpp @@ -1,328 +1,30 @@ #include "luabindings.hpp" -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include #include -#include -#include #include "../mwbase/environment.hpp" -#include "../mwbase/statemanager.hpp" -#include "../mwbase/windowmanager.hpp" -#include "../mwworld/action.hpp" -#include "../mwworld/class.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/datetimemanager.hpp" -#include "../mwworld/esmstore.hpp" -#include "../mwworld/manualref.hpp" -#include "../mwworld/store.hpp" -#include "../mwworld/worldmodel.hpp" - -#include "luaevents.hpp" -#include "luamanagerimp.hpp" -#include "mwscriptbindings.hpp" -#include "objectlists.hpp" #include "camerabindings.hpp" #include "cellbindings.hpp" +#include "corebindings.hpp" #include "debugbindings.hpp" -#include "factionbindings.hpp" #include "inputbindings.hpp" -#include "magicbindings.hpp" +#include "localscripts.hpp" +#include "menuscripts.hpp" #include "nearbybindings.hpp" #include "objectbindings.hpp" #include "postprocessingbindings.hpp" #include "soundbindings.hpp" -#include "stats.hpp" #include "types/types.hpp" #include "uibindings.hpp" #include "vfsbindings.hpp" +#include "worldbindings.hpp" namespace MWLua { - struct CellsStore - { - }; -} - -namespace sol -{ - template <> - struct is_automagical : std::false_type - { - }; -} - -namespace MWLua -{ - - static void checkGameInitialized(LuaUtil::LuaState* lua) - { - if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame) - throw std::runtime_error( - "This function cannot be used until the game is fully initialized.\n" + lua->debugTraceback()); - } - - static void addTimeBindings(sol::table& api, const Context& context, bool global) - { - MWWorld::DateTimeManager* timeManager = MWBase::Environment::get().getWorld()->getTimeManager(); - - api["getSimulationTime"] = [timeManager]() { return timeManager->getSimulationTime(); }; - api["getSimulationTimeScale"] = [timeManager]() { return timeManager->getSimulationTimeScale(); }; - api["getGameTime"] = [timeManager]() { return timeManager->getGameTime(); }; - api["getGameTimeScale"] = [timeManager]() { return timeManager->getGameTimeScale(); }; - api["isWorldPaused"] = [timeManager]() { return timeManager->isPaused(); }; - api["getRealTime"] = []() { - return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); - }; - - if (!global) - return; - - api["setGameTimeScale"] = [timeManager](double scale) { timeManager->setGameTimeScale(scale); }; - api["setSimulationTimeScale"] = [context, timeManager](float scale) { - context.mLuaManager->addAction([scale, timeManager] { timeManager->setSimulationTimeScale(scale); }); - }; - - api["pause"] - = [timeManager](sol::optional tag) { timeManager->pause(tag.value_or("paused")); }; - api["unpause"] - = [timeManager](sol::optional tag) { timeManager->unpause(tag.value_or("paused")); }; - api["getPausedTags"] = [timeManager](sol::this_state lua) { - sol::table res(lua, sol::create); - for (const std::string& tag : timeManager->getPausedTags()) - res[tag] = tag; - return res; - }; - } - - static sol::table initContentFilesBindings(sol::state_view& lua) - { - const std::vector& contentList = MWBase::Environment::get().getWorld()->getContentFiles(); - sol::table list(lua, sol::create); - for (size_t i = 0; i < contentList.size(); ++i) - list[i + 1] = Misc::StringUtils::lowerCase(contentList[i]); - sol::table res(lua, sol::create); - res["list"] = LuaUtil::makeReadOnly(list); - res["indexOf"] = [&contentList](std::string_view contentFile) -> sol::optional { - for (size_t i = 0; i < contentList.size(); ++i) - if (Misc::StringUtils::ciEqual(contentList[i], contentFile)) - return i + 1; - return sol::nullopt; - }; - res["has"] = [&contentList](std::string_view contentFile) -> bool { - for (size_t i = 0; i < contentList.size(); ++i) - if (Misc::StringUtils::ciEqual(contentList[i], contentFile)) - return true; - return false; - }; - return LuaUtil::makeReadOnly(res); - } - - static sol::table initCorePackage(const Context& context) - { - auto* lua = context.mLua; - sol::table api(lua->sol(), sol::create); - api["API_REVISION"] = Version::getLuaApiRevision(); // specified in CMakeLists.txt - api["quit"] = [lua]() { - Log(Debug::Warning) << "Quit requested by a Lua script.\n" << lua->debugTraceback(); - MWBase::Environment::get().getStateManager()->requestQuit(); - }; - api["sendGlobalEvent"] = [context](std::string eventName, const sol::object& eventData) { - context.mLuaEvents->addGlobalEvent( - { std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer) }); - }; - api["contentFiles"] = initContentFilesBindings(lua->sol()); - api["sound"] = initCoreSoundBindings(context); - api["getFormId"] = [](std::string_view contentFile, unsigned int index) -> std::string { - const std::vector& contentList = MWBase::Environment::get().getWorld()->getContentFiles(); - for (size_t i = 0; i < contentList.size(); ++i) - if (Misc::StringUtils::ciEqual(contentList[i], contentFile)) - return ESM::RefId(ESM::FormId{ index, int(i) }).serializeText(); - throw std::runtime_error("Content file not found: " + std::string(contentFile)); - }; - addTimeBindings(api, context, false); - api["magic"] = initCoreMagicBindings(context); - api["stats"] = initCoreStatsBindings(context); - - initCoreFactionBindings(context); - api["factions"] = &MWBase::Environment::get().getWorld()->getStore().get(); - - api["l10n"] = LuaUtil::initL10nLoader(lua->sol(), MWBase::Environment::get().getL10nManager()); - const MWWorld::Store* gmstStore - = &MWBase::Environment::get().getESMStore()->get(); - api["getGMST"] = [lua = context.mLua, gmstStore](const std::string& setting) -> sol::object { - const ESM::GameSetting* gmst = gmstStore->search(setting); - if (gmst == nullptr) - return sol::nil; - const ESM::Variant& value = gmst->mValue; - switch (value.getType()) - { - case ESM::VT_Float: - return sol::make_object(lua->sol(), value.getFloat()); - case ESM::VT_Short: - case ESM::VT_Long: - case ESM::VT_Int: - return sol::make_object(lua->sol(), value.getInteger()); - case ESM::VT_String: - return sol::make_object(lua->sol(), value.getString()); - case ESM::VT_Unknown: - case ESM::VT_None: - break; - } - return sol::nil; - }; - - return LuaUtil::makeReadOnly(api); - } - - static void addCellGetters(sol::table& api, const Context& context) - { - api["getCellByName"] = [](std::string_view name) { - return GCell{ &MWBase::Environment::get().getWorldModel()->getCell(name, /*forceLoad=*/false) }; - }; - api["getExteriorCell"] = [](int x, int y, sol::object cellOrName) { - ESM::RefId worldspace; - if (cellOrName.is()) - worldspace = cellOrName.as().mStore->getCell()->getWorldSpace(); - else if (cellOrName.is() && !cellOrName.as().empty()) - worldspace = MWBase::Environment::get() - .getWorldModel() - ->getCell(cellOrName.as()) - .getCell() - ->getWorldSpace(); - else - worldspace = ESM::Cell::sDefaultWorldspaceId; - return GCell{ &MWBase::Environment::get().getWorldModel()->getExterior( - ESM::ExteriorCellLocation(x, y, worldspace), /*forceLoad=*/false) }; - }; - - const MWWorld::Store* cells3Store = &MWBase::Environment::get().getESMStore()->get(); - const MWWorld::Store* cells4Store = &MWBase::Environment::get().getESMStore()->get(); - sol::usertype cells = context.mLua->sol().new_usertype("Cells"); - cells[sol::meta_function::length] - = [cells3Store, cells4Store](const CellsStore&) { return cells3Store->getSize() + cells4Store->getSize(); }; - cells[sol::meta_function::index] - = [cells3Store, cells4Store](const CellsStore&, size_t index) -> sol::optional { - if (index > cells3Store->getSize() + cells3Store->getSize() || index == 0) - return sol::nullopt; - - index--; // Translate from Lua's 1-based indexing. - if (index < cells3Store->getSize()) - { - const ESM::Cell* cellRecord = cells3Store->at(index); - return GCell{ &MWBase::Environment::get().getWorldModel()->getCell( - cellRecord->mId, /*forceLoad=*/false) }; - } - else - { - const ESM4::Cell* cellRecord = cells4Store->at(index - cells3Store->getSize()); - return GCell{ &MWBase::Environment::get().getWorldModel()->getCell( - cellRecord->mId, /*forceLoad=*/false) }; - } - }; - cells[sol::meta_function::pairs] = context.mLua->sol()["ipairsForArray"].template get(); - cells[sol::meta_function::ipairs] = context.mLua->sol()["ipairsForArray"].template get(); - api["cells"] = CellsStore{}; - } - - static sol::table initWorldPackage(const Context& context) - { - sol::table api(context.mLua->sol(), sol::create); - ObjectLists* objectLists = context.mObjectLists; - addTimeBindings(api, context, true); - addCellGetters(api, context); - api["mwscript"] = initMWScriptBindings(context); - api["activeActors"] = GObjectList{ objectLists->getActorsInScene() }; - api["players"] = GObjectList{ objectLists->getPlayers() }; - api["createObject"] = [lua = context.mLua](std::string_view recordId, sol::optional count) -> GObject { - checkGameInitialized(lua); - MWWorld::ManualRef mref(*MWBase::Environment::get().getESMStore(), ESM::RefId::deserializeText(recordId)); - const MWWorld::Ptr& ptr = mref.getPtr(); - ptr.getRefData().disable(); - MWWorld::CellStore& cell = MWBase::Environment::get().getWorldModel()->getDraftCell(); - MWWorld::Ptr newPtr = ptr.getClass().copyToCell(ptr, cell, count.value_or(1)); - return GObject(newPtr); - }; - api["getObjectByFormId"] = [](std::string_view formIdStr) -> GObject { - ESM::RefId refId = ESM::RefId::deserializeText(formIdStr); - if (!refId.is()) - throw std::runtime_error("FormId expected, got " + std::string(formIdStr) + "; use core.getFormId"); - return GObject(*refId.getIf()); - }; - - // Creates a new record in the world database. - api["createRecord"] = sol::overload( - [lua = context.mLua](const ESM::Activator& activator) -> const ESM::Activator* { - checkGameInitialized(lua); - return MWBase::Environment::get().getESMStore()->insert(activator); - }, - [lua = context.mLua](const ESM::Armor& armor) -> const ESM::Armor* { - checkGameInitialized(lua); - return MWBase::Environment::get().getESMStore()->insert(armor); - }, - [lua = context.mLua](const ESM::Clothing& clothing) -> const ESM::Clothing* { - checkGameInitialized(lua); - return MWBase::Environment::get().getESMStore()->insert(clothing); - }, - [lua = context.mLua](const ESM::Book& book) -> const ESM::Book* { - checkGameInitialized(lua); - return MWBase::Environment::get().getESMStore()->insert(book); - }, - [lua = context.mLua](const ESM::Miscellaneous& misc) -> const ESM::Miscellaneous* { - checkGameInitialized(lua); - return MWBase::Environment::get().getESMStore()->insert(misc); - }, - [lua = context.mLua](const ESM::Potion& potion) -> const ESM::Potion* { - checkGameInitialized(lua); - return MWBase::Environment::get().getESMStore()->insert(potion); - }, - [lua = context.mLua](const ESM::Weapon& weapon) -> const ESM::Weapon* { - checkGameInitialized(lua); - return MWBase::Environment::get().getESMStore()->insert(weapon); - }); - - api["_runStandardActivationAction"] = [context](const GObject& object, const GObject& actor) { - if (!object.ptr().getRefData().activate()) - return; - context.mLuaManager->addAction( - [object, actor] { - const MWWorld::Ptr& objPtr = object.ptr(); - const MWWorld::Ptr& actorPtr = actor.ptr(); - objPtr.getClass().activate(objPtr, actorPtr)->execute(actorPtr); - }, - "_runStandardActivationAction"); - }; - api["_runStandardUseAction"] = [context](const GObject& object, const GObject& actor, bool force) { - context.mLuaManager->addAction( - [object, actor, force] { - const MWWorld::Ptr& actorPtr = actor.ptr(); - const MWWorld::Ptr& objectPtr = object.ptr(); - if (actorPtr == MWBase::Environment::get().getWorld()->getPlayerPtr()) - MWBase::Environment::get().getWindowManager()->useItem(objectPtr, force); - else - { - std::unique_ptr action = objectPtr.getClass().use(objectPtr, force); - action->execute(actorPtr, true); - } - }, - "_runStandardUseAction"); - }; - - return LuaUtil::makeReadOnly(api); - } - std::map initCommonPackages(const Context& context) { sol::state_view lua = context.mLua->sol(); @@ -331,8 +33,6 @@ namespace MWLua { "openmw.async", LuaUtil::getAsyncPackageInitializer( lua, [tm] { return tm->getSimulationTime(); }, [tm] { return tm->getGameTime(); }) }, - { "openmw.core", initCorePackage(context) }, - { "openmw.types", initTypesPackage(context) }, { "openmw.util", LuaUtil::initUtilPackage(lua) }, { "openmw.vfs", initVFSPackage(context) }, }; @@ -343,6 +43,8 @@ namespace MWLua initObjectBindingsForGlobalScripts(context); initCellBindingsForGlobalScripts(context); return { + { "openmw.core", initCorePackage(context) }, + { "openmw.types", initTypesPackage(context) }, { "openmw.world", initWorldPackage(context) }, }; } @@ -353,6 +55,8 @@ namespace MWLua initCellBindingsForLocalScripts(context); LocalScripts::initializeSelfPackage(context); return { + { "openmw.core", initCorePackage(context) }, + { "openmw.types", initTypesPackage(context) }, { "openmw.nearby", initNearbyPackage(context) }, }; } @@ -369,4 +73,16 @@ namespace MWLua }; } + std::map initMenuPackages(const Context& context) + { + return { + { "openmw.core", initCorePackageForMenuScripts(context) }, // + { "openmw.ambient", initAmbientPackage(context) }, // + { "openmw.ui", initUserInterfacePackage(context) }, // + { "openmw.menu", initMenuPackage(context) }, + // TODO: Maybe add: + // { "openmw.input", initInputPackage(context) }, + // { "openmw.postprocessing", initPostprocessingPackage(context) }, + }; + } } diff --git a/apps/openmw/mwlua/luabindings.hpp b/apps/openmw/mwlua/luabindings.hpp index e5d481d1eb..749987e5b2 100644 --- a/apps/openmw/mwlua/luabindings.hpp +++ b/apps/openmw/mwlua/luabindings.hpp @@ -12,14 +12,18 @@ namespace MWLua // Initialize Lua packages that are available for all scripts. std::map initCommonPackages(const Context&); - // Initialize Lua packages that are available only for global scripts. + // Initialize Lua packages that are available for global scripts (additionally to common packages). std::map initGlobalPackages(const Context&); - // Initialize Lua packages that are available only for local scripts (including player scripts). + // Initialize Lua packages that are available for local scripts (additionally to common packages). std::map initLocalPackages(const Context&); - // Initialize Lua packages that are available only for local scripts on the player. + // Initialize Lua packages that are available only for local scripts on the player (additionally to common and local + // packages). std::map initPlayerPackages(const Context&); + + // Initialize Lua packages that are available only for menu scripts (additionally to common packages). + std::map initMenuPackages(const Context&); } #endif // MWLUA_LUABINDINGS_H diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 63a2838250..77ddfcc4a7 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -68,6 +68,7 @@ namespace MWLua 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]); + mMenuScripts.setAutoStartConf(mConfiguration.getMenuConf()); mGlobalScripts.setAutoStartConf(mConfiguration.getGlobalConf()); } @@ -89,20 +90,25 @@ namespace MWLua mLua.addCommonPackage(name, package); for (const auto& [name, package] : initGlobalPackages(context)) mGlobalScripts.addPackage(name, package); + for (const auto& [name, package] : initMenuPackages(context)) + mMenuScripts.addPackage(name, package); mLocalPackages = initLocalPackages(localContext); + mPlayerPackages = initPlayerPackages(localContext); mPlayerPackages.insert(mLocalPackages.begin(), mLocalPackages.end()); LuaUtil::LuaStorage::initLuaBindings(mLua.sol()); mGlobalScripts.addPackage( "openmw.storage", LuaUtil::LuaStorage::initGlobalPackage(mLua.sol(), &mGlobalStorage)); + mMenuScripts.addPackage("openmw.storage", LuaUtil::LuaStorage::initMenuPackage(mLua.sol(), &mPlayerStorage)); mLocalPackages["openmw.storage"] = LuaUtil::LuaStorage::initLocalPackage(mLua.sol(), &mGlobalStorage); mPlayerPackages["openmw.storage"] = LuaUtil::LuaStorage::initPlayerPackage(mLua.sol(), &mGlobalStorage, &mPlayerStorage); initConfiguration(); mInitialized = true; + mMenuScripts.addAutoStartedScripts(); } void LuaManager::loadPermanentStorage(const std::filesystem::path& userConfigPath) @@ -204,9 +210,6 @@ namespace MWLua void LuaManager::synchronizedUpdate() { - if (mPlayer.isEmpty()) - return; // The game is not started yet. - if (mNewGameStarted) { mNewGameStarted = false; @@ -217,7 +220,8 @@ namespace MWLua // We apply input events in `synchronizedUpdate` rather than in `update` in order to reduce input latency. mProcessingInputEvents = true; - PlayerScripts* playerScripts = dynamic_cast(mPlayer.getRefData().getLuaScripts()); + PlayerScripts* playerScripts + = mPlayer.isEmpty() ? nullptr : dynamic_cast(mPlayer.getRefData().getLuaScripts()); MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); if (playerScripts && !windowManager->containsMode(MWGui::GM_MainMenu)) { @@ -225,6 +229,7 @@ namespace MWLua playerScripts->processInputEvent(event); } mInputEvents.clear(); + mMenuScripts.update(0); if (playerScripts) playerScripts->onFrame(MWBase::Environment::get().getWorld()->getTimeManager()->isPaused() ? 0.0 @@ -272,7 +277,6 @@ namespace MWLua { LuaUi::clearUserInterface(); mUiResourceManager.clear(); - MWBase::Environment::get().getWindowManager()->setConsoleMode(""); MWBase::Environment::get().getWorld()->getPostProcessor()->disableDynamicShaders(); mActiveLocalScripts.clear(); mLuaEvents.clear(); @@ -320,6 +324,7 @@ namespace MWLua mGlobalScripts.addAutoStartedScripts(); mGlobalScriptsStarted = true; mNewGameStarted = true; + mMenuScripts.stateChanged(); } void LuaManager::gameLoaded() @@ -327,6 +332,7 @@ namespace MWLua if (!mGlobalScriptsStarted) mGlobalScripts.addAutoStartedScripts(); mGlobalScriptsStarted = true; + mMenuScripts.stateChanged(); } void LuaManager::uiModeChanged(const MWWorld::Ptr& arg) @@ -529,6 +535,9 @@ namespace MWLua } for (LocalScripts* scripts : mActiveLocalScripts) scripts->setActive(true); + + mMenuScripts.removeAllScripts(); + mMenuScripts.addAutoStartedScripts(); } void LuaManager::handleConsoleCommand( @@ -537,16 +546,16 @@ namespace MWLua PlayerScripts* playerScripts = nullptr; if (!mPlayer.isEmpty()) playerScripts = dynamic_cast(mPlayer.getRefData().getLuaScripts()); - if (!playerScripts) + bool processed = mMenuScripts.consoleCommand(consoleMode, command); + if (playerScripts) { - MWBase::Environment::get().getWindowManager()->printToConsole( - "You must enter a game session to run Lua commands\n", MWBase::WindowManager::sConsoleColor_Error); - return; + sol::object selected = sol::nil; + if (!selectedPtr.isEmpty()) + selected = sol::make_object(mLua.sol(), LObject(getId(selectedPtr))); + if (playerScripts->consoleCommand(consoleMode, command, selected)) + processed = true; } - sol::object selected = sol::nil; - if (!selectedPtr.isEmpty()) - selected = sol::make_object(mLua.sol(), LObject(getId(selectedPtr))); - if (!playerScripts->consoleCommand(consoleMode, command, selected)) + if (!processed) MWBase::Environment::get().getWindowManager()->printToConsole( "No Lua handlers for console\n", MWBase::WindowManager::sConsoleColor_Error); } @@ -680,6 +689,7 @@ namespace MWLua for (size_t i = 0; i < mConfiguration.size(); ++i) { bool isGlobal = mConfiguration[i].mFlags & ESM::LuaScriptCfg::sGlobal; + bool isMenu = mConfiguration[i].mFlags & ESM::LuaScriptCfg::sMenu; out << std::left; out << " " << std::setw(nameW) << mConfiguration[i].mScriptPath; @@ -692,6 +702,8 @@ namespace MWLua if (isGlobal) out << std::setw(valueW * 2) << "NA (global script)"; + else if (isMenu && (!selectedScripts || !selectedScripts->hasScript(i))) + out << std::setw(valueW * 2) << "NA (menu script)"; else if (selectedPtr.isEmpty()) out << std::setw(valueW * 2) << "NA (not selected) "; else if (!selectedScripts || !selectedScripts->hasScript(i)) diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index a725761dbd..b16e81082b 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -17,6 +17,7 @@ #include "globalscripts.hpp" #include "localscripts.hpp" #include "luaevents.hpp" +#include "menuscripts.hpp" #include "object.hpp" #include "objectlists.hpp" @@ -164,6 +165,7 @@ namespace MWLua std::map mLocalPackages; std::map mPlayerPackages; + MenuScripts mMenuScripts{ &mLua }; GlobalScripts mGlobalScripts{ &mLua }; std::set mActiveLocalScripts; ObjectLists mObjectLists; diff --git a/apps/openmw/mwlua/menuscripts.cpp b/apps/openmw/mwlua/menuscripts.cpp new file mode 100644 index 0000000000..300f5ad489 --- /dev/null +++ b/apps/openmw/mwlua/menuscripts.cpp @@ -0,0 +1,114 @@ +#include "menuscripts.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/statemanager.hpp" +#include "../mwstate/character.hpp" + +namespace MWLua +{ + static const MWState::Character* findCharacter(std::string_view characterDir) + { + MWBase::StateManager* manager = MWBase::Environment::get().getStateManager(); + for (auto it = manager->characterBegin(); it != manager->characterEnd(); ++it) + if (it->getPath().filename() == characterDir) + return &*it; + return nullptr; + } + + static const MWState::Slot* findSlot(const MWState::Character* character, std::string_view slotName) + { + if (!character) + return nullptr; + for (const MWState::Slot& slot : *character) + if (slot.mPath.filename() == slotName) + return &slot; + return nullptr; + } + + sol::table initMenuPackage(const Context& context) + { + sol::state_view lua = context.mLua->sol(); + sol::table api(lua, sol::create); + + api["STATE"] + = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ + { "NoGame", MWBase::StateManager::State_NoGame }, + { "Running", MWBase::StateManager::State_Running }, + { "Ended", MWBase::StateManager::State_Ended }, + })); + + api["getState"] = []() -> int { return MWBase::Environment::get().getStateManager()->getState(); }; + + api["newGame"] = []() { MWBase::Environment::get().getStateManager()->requestNewGame(); }; + + api["loadGame"] = [](std::string_view dir, std::string_view slotName) { + const MWState::Character* character = findCharacter(dir); + const MWState::Slot* slot = findSlot(character, slotName); + if (!slot) + throw std::runtime_error("Save game slot not found: " + std::string(dir) + "/" + std::string(slotName)); + MWBase::Environment::get().getStateManager()->requestLoad(slot->mPath); + }; + + api["deleteGame"] = [](std::string_view dir, std::string_view slotName) { + const MWState::Character* character = findCharacter(dir); + const MWState::Slot* slot = findSlot(character, slotName); + if (!slot) + throw std::runtime_error("Save game slot not found: " + std::string(dir) + "/" + std::string(slotName)); + MWBase::Environment::get().getStateManager()->deleteGame(character, slot); + }; + + api["getCurrentSaveDir"] = []() -> sol::optional { + MWBase::StateManager* manager = MWBase::Environment::get().getStateManager(); + const MWState::Character* character = manager->getCurrentCharacter(); + if (character) + return character->getPath().filename().string(); + else + return sol::nullopt; + }; + + api["saveGame"] = [](std::string_view description, sol::optional slotName) { + MWBase::StateManager* manager = MWBase::Environment::get().getStateManager(); + const MWState::Character* character = manager->getCurrentCharacter(); + const MWState::Slot* slot = nullptr; + if (slotName) + slot = findSlot(character, *slotName); + manager->saveGame(description, slot); + }; + + auto getSaves = [](sol::state_view lua, const MWState::Character& character) { + sol::table saves(lua, sol::create); + for (const MWState::Slot& slot : character) + { + sol::table slotInfo(lua, sol::create); + slotInfo["description"] = slot.mProfile.mDescription; + slotInfo["playerName"] = slot.mProfile.mPlayerName; + slotInfo["playerLevel"] = slot.mProfile.mPlayerLevel; + sol::table contentFiles(lua, sol::create); + for (size_t i = 0; i < slot.mProfile.mContentFiles.size(); ++i) + contentFiles[i + 1] = slot.mProfile.mContentFiles[i]; + slotInfo["contentFiles"] = contentFiles; + saves[slot.mPath.filename().string()] = slotInfo; + } + return saves; + }; + + api["getSaves"] = [getSaves](sol::this_state lua, std::string_view dir) -> sol::table { + const MWState::Character* character = findCharacter(dir); + if (!character) + throw std::runtime_error("Saves not found: " + std::string(dir)); + return getSaves(lua, *character); + }; + + api["getAllSaves"] = [getSaves](sol::this_state lua) -> sol::table { + sol::table saves(lua, sol::create); + MWBase::StateManager* manager = MWBase::Environment::get().getStateManager(); + for (auto it = manager->characterBegin(); it != manager->characterEnd(); ++it) + saves[it->getPath().filename().string()] = getSaves(lua, *it); + return saves; + }; + + api["quit"] = []() { MWBase::Environment::get().getStateManager()->requestQuit(); }; + + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/menuscripts.hpp b/apps/openmw/mwlua/menuscripts.hpp new file mode 100644 index 0000000000..3fd1bce186 --- /dev/null +++ b/apps/openmw/mwlua/menuscripts.hpp @@ -0,0 +1,46 @@ +#ifndef MWLUA_MENUSCRIPTS_H +#define MWLUA_MENUSCRIPTS_H + +#include + +#include +#include +#include + +#include "../mwbase/luamanager.hpp" + +#include "context.hpp" + +namespace MWLua +{ + + sol::table initMenuPackage(const Context& context); + + class MenuScripts : public LuaUtil::ScriptsContainer + { + public: + MenuScripts(LuaUtil::LuaState* lua) + : LuaUtil::ScriptsContainer(lua, "Menu") + { + registerEngineHandlers({ &mStateChanged, &mConsoleCommandHandlers, &mUiModeChanged }); + } + + void stateChanged() { callEngineHandlers(mStateChanged); } + + bool consoleCommand(const std::string& consoleMode, const std::string& command) + { + callEngineHandlers(mConsoleCommandHandlers, consoleMode, command); + return !mConsoleCommandHandlers.mList.empty(); + } + + void uiModeChanged() { callEngineHandlers(mUiModeChanged); } + + private: + EngineHandlerList mStateChanged{ "onStateChanged" }; + EngineHandlerList mConsoleCommandHandlers{ "onConsoleCommand" }; + EngineHandlerList mUiModeChanged{ "_onUiModeChanged" }; + }; + +} + +#endif // MWLUA_GLOBALSCRIPTS_H diff --git a/apps/openmw/mwlua/soundbindings.cpp b/apps/openmw/mwlua/soundbindings.cpp index dc45a672b4..55071ea374 100644 --- a/apps/openmw/mwlua/soundbindings.cpp +++ b/apps/openmw/mwlua/soundbindings.cpp @@ -61,7 +61,11 @@ namespace MWLua { sol::table initAmbientPackage(const Context& context) { - sol::table api(context.mLua->sol(), sol::create); + sol::state_view& lua = context.mLua->sol(); + if (lua["openmw_ambient"] != sol::nil) + return lua["openmw_ambient"]; + + sol::table api(lua, sol::create); api["playSound"] = [](std::string_view soundId, const sol::optional& options) { auto args = getPlaySoundArgs(options); @@ -104,7 +108,8 @@ namespace MWLua api["stopMusic"] = []() { MWBase::Environment::get().getSoundManager()->stopMusic(); }; - return LuaUtil::makeReadOnly(api); + lua["openmw_ambient"] = LuaUtil::makeReadOnly(api); + return lua["openmw_ambient"]; } sol::table initCoreSoundBindings(const Context& context) diff --git a/apps/openmw/mwlua/types/types.cpp b/apps/openmw/mwlua/types/types.cpp index bd8b592f7a..cf03d1baef 100644 --- a/apps/openmw/mwlua/types/types.cpp +++ b/apps/openmw/mwlua/types/types.cpp @@ -162,6 +162,10 @@ namespace MWLua sol::table initTypesPackage(const Context& context) { auto* lua = context.mLua; + + if (lua->sol()["openmw_types"] != sol::nil) + return lua->sol()["openmw_types"]; + sol::table types(lua->sol(), sol::create); auto addType = [&](std::string_view name, std::vector recTypes, std::optional base = std::nullopt) -> sol::table { @@ -250,6 +254,7 @@ namespace MWLua packageToType[t] = type; } - return LuaUtil::makeReadOnly(types); + lua->sol()["openmw_types"] = LuaUtil::makeReadOnly(types); + return lua->sol()["openmw_types"]; } } diff --git a/apps/openmw/mwlua/uibindings.cpp b/apps/openmw/mwlua/uibindings.cpp index 79f5fac9a1..96f3246ebe 100644 --- a/apps/openmw/mwlua/uibindings.cpp +++ b/apps/openmw/mwlua/uibindings.cpp @@ -92,6 +92,12 @@ namespace MWLua sol::table initUserInterfacePackage(const Context& context) { + { + sol::state_view& lua = context.mLua->sol(); + if (lua["openmw_ui"] != sol::nil) + return lua["openmw_ui"]; + } + MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); auto element = context.mLua->sol().new_usertype("Element"); @@ -130,6 +136,7 @@ namespace MWLua api["setConsoleMode"] = [luaManager = context.mLuaManager, windowManager](std::string_view mode) { luaManager->addAction([mode = std::string(mode), windowManager] { windowManager->setConsoleMode(mode); }); }; + api["getConsoleMode"] = [windowManager]() -> std::string_view { return windowManager->getConsoleMode(); }; api["setConsoleSelectedObject"] = [luaManager = context.mLuaManager, windowManager](const sol::object& obj) { if (obj == sol::nil) luaManager->addAction([windowManager] { windowManager->setConsoleSelectedObject(MWWorld::Ptr()); }); @@ -302,6 +309,8 @@ namespace MWLua // TODO // api["_showMouseCursor"] = [](bool) {}; - return LuaUtil::makeReadOnly(api); + sol::state_view& lua = context.mLua->sol(); + lua["openmw_ui"] = LuaUtil::makeReadOnly(api); + return lua["openmw_ui"]; } } diff --git a/apps/openmw/mwlua/worldbindings.cpp b/apps/openmw/mwlua/worldbindings.cpp new file mode 100644 index 0000000000..c5ff7c89ca --- /dev/null +++ b/apps/openmw/mwlua/worldbindings.cpp @@ -0,0 +1,215 @@ +#include "worldbindings.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/statemanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" +#include "../mwworld/action.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/datetimemanager.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/manualref.hpp" +#include "../mwworld/store.hpp" +#include "../mwworld/worldmodel.hpp" + +#include "luamanagerimp.hpp" + +#include "corebindings.hpp" +#include "mwscriptbindings.hpp" + +namespace MWLua +{ + struct CellsStore + { + }; +} + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + + static void checkGameInitialized(LuaUtil::LuaState* lua) + { + if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame) + throw std::runtime_error( + "This function cannot be used until the game is fully initialized.\n" + lua->debugTraceback()); + } + + static void addWorldTimeBindings(sol::table& api, const Context& context) + { + MWWorld::DateTimeManager* timeManager = MWBase::Environment::get().getWorld()->getTimeManager(); + + api["setGameTimeScale"] = [timeManager](double scale) { timeManager->setGameTimeScale(scale); }; + api["setSimulationTimeScale"] = [context, timeManager](float scale) { + context.mLuaManager->addAction([scale, timeManager] { timeManager->setSimulationTimeScale(scale); }); + }; + + api["pause"] + = [timeManager](sol::optional tag) { timeManager->pause(tag.value_or("paused")); }; + api["unpause"] + = [timeManager](sol::optional tag) { timeManager->unpause(tag.value_or("paused")); }; + api["getPausedTags"] = [timeManager](sol::this_state lua) { + sol::table res(lua, sol::create); + for (const std::string& tag : timeManager->getPausedTags()) + res[tag] = tag; + return res; + }; + } + + static void addCellGetters(sol::table& api, const Context& context) + { + api["getCellByName"] = [](std::string_view name) { + return GCell{ &MWBase::Environment::get().getWorldModel()->getCell(name, /*forceLoad=*/false) }; + }; + api["getExteriorCell"] = [](int x, int y, sol::object cellOrName) { + ESM::RefId worldspace; + if (cellOrName.is()) + worldspace = cellOrName.as().mStore->getCell()->getWorldSpace(); + else if (cellOrName.is() && !cellOrName.as().empty()) + worldspace = MWBase::Environment::get() + .getWorldModel() + ->getCell(cellOrName.as()) + .getCell() + ->getWorldSpace(); + else + worldspace = ESM::Cell::sDefaultWorldspaceId; + return GCell{ &MWBase::Environment::get().getWorldModel()->getExterior( + ESM::ExteriorCellLocation(x, y, worldspace), /*forceLoad=*/false) }; + }; + + const MWWorld::Store* cells3Store = &MWBase::Environment::get().getESMStore()->get(); + const MWWorld::Store* cells4Store = &MWBase::Environment::get().getESMStore()->get(); + sol::usertype cells = context.mLua->sol().new_usertype("Cells"); + cells[sol::meta_function::length] + = [cells3Store, cells4Store](const CellsStore&) { return cells3Store->getSize() + cells4Store->getSize(); }; + cells[sol::meta_function::index] + = [cells3Store, cells4Store](const CellsStore&, size_t index) -> sol::optional { + if (index > cells3Store->getSize() + cells3Store->getSize() || index == 0) + return sol::nullopt; + + index--; // Translate from Lua's 1-based indexing. + if (index < cells3Store->getSize()) + { + const ESM::Cell* cellRecord = cells3Store->at(index); + return GCell{ &MWBase::Environment::get().getWorldModel()->getCell( + cellRecord->mId, /*forceLoad=*/false) }; + } + else + { + const ESM4::Cell* cellRecord = cells4Store->at(index - cells3Store->getSize()); + return GCell{ &MWBase::Environment::get().getWorldModel()->getCell( + cellRecord->mId, /*forceLoad=*/false) }; + } + }; + cells[sol::meta_function::pairs] = context.mLua->sol()["ipairsForArray"].template get(); + cells[sol::meta_function::ipairs] = context.mLua->sol()["ipairsForArray"].template get(); + api["cells"] = CellsStore{}; + } + + sol::table initWorldPackage(const Context& context) + { + sol::table api(context.mLua->sol(), sol::create); + + addCoreTimeBindings(api, context); + addWorldTimeBindings(api, context); + addCellGetters(api, context); + api["mwscript"] = initMWScriptBindings(context); + + ObjectLists* objectLists = context.mObjectLists; + api["activeActors"] = GObjectList{ objectLists->getActorsInScene() }; + api["players"] = GObjectList{ objectLists->getPlayers() }; + + api["createObject"] = [lua = context.mLua](std::string_view recordId, sol::optional count) -> GObject { + checkGameInitialized(lua); + MWWorld::ManualRef mref(*MWBase::Environment::get().getESMStore(), ESM::RefId::deserializeText(recordId)); + const MWWorld::Ptr& ptr = mref.getPtr(); + ptr.getRefData().disable(); + MWWorld::CellStore& cell = MWBase::Environment::get().getWorldModel()->getDraftCell(); + MWWorld::Ptr newPtr = ptr.getClass().copyToCell(ptr, cell, count.value_or(1)); + return GObject(newPtr); + }; + api["getObjectByFormId"] = [](std::string_view formIdStr) -> GObject { + ESM::RefId refId = ESM::RefId::deserializeText(formIdStr); + if (!refId.is()) + throw std::runtime_error("FormId expected, got " + std::string(formIdStr) + "; use core.getFormId"); + return GObject(*refId.getIf()); + }; + + // Creates a new record in the world database. + api["createRecord"] = sol::overload( + [lua = context.mLua](const ESM::Activator& activator) -> const ESM::Activator* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(activator); + }, + [lua = context.mLua](const ESM::Armor& armor) -> const ESM::Armor* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(armor); + }, + [lua = context.mLua](const ESM::Clothing& clothing) -> const ESM::Clothing* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(clothing); + }, + [lua = context.mLua](const ESM::Book& book) -> const ESM::Book* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(book); + }, + [lua = context.mLua](const ESM::Miscellaneous& misc) -> const ESM::Miscellaneous* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(misc); + }, + [lua = context.mLua](const ESM::Potion& potion) -> const ESM::Potion* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(potion); + }, + [lua = context.mLua](const ESM::Weapon& weapon) -> const ESM::Weapon* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(weapon); + }); + + api["_runStandardActivationAction"] = [context](const GObject& object, const GObject& actor) { + if (!object.ptr().getRefData().activate()) + return; + context.mLuaManager->addAction( + [object, actor] { + const MWWorld::Ptr& objPtr = object.ptr(); + const MWWorld::Ptr& actorPtr = actor.ptr(); + objPtr.getClass().activate(objPtr, actorPtr)->execute(actorPtr); + }, + "_runStandardActivationAction"); + }; + api["_runStandardUseAction"] = [context](const GObject& object, const GObject& actor, bool force) { + context.mLuaManager->addAction( + [object, actor, force] { + const MWWorld::Ptr& actorPtr = actor.ptr(); + const MWWorld::Ptr& objectPtr = object.ptr(); + if (actorPtr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + MWBase::Environment::get().getWindowManager()->useItem(objectPtr, force); + else + { + std::unique_ptr action = objectPtr.getClass().use(objectPtr, force); + action->execute(actorPtr, true); + } + }, + "_runStandardUseAction"); + }; + + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/worldbindings.hpp b/apps/openmw/mwlua/worldbindings.hpp new file mode 100644 index 0000000000..4bd2318b68 --- /dev/null +++ b/apps/openmw/mwlua/worldbindings.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_WORLDBINDINGS_H +#define MWLUA_WORLDBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + sol::table initWorldPackage(const Context&); +} + +#endif // MWLUA_WORLDBINDINGS_H diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index fb3590a3f0..8819aaa29c 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -666,6 +666,18 @@ void MWState::StateManager::update(float duration) MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_MainMenu); } } + + if (mNewGameRequest) + { + newGame(); + mNewGameRequest = false; + } + + if (mLoadRequest) + { + loadGame(*mLoadRequest); + mLoadRequest = std::nullopt; + } } bool MWState::StateManager::verifyProfile(const ESM::SavedGame& profile) const diff --git a/apps/openmw/mwstate/statemanagerimp.hpp b/apps/openmw/mwstate/statemanagerimp.hpp index df62ca7ebf..dfd4dd12f0 100644 --- a/apps/openmw/mwstate/statemanagerimp.hpp +++ b/apps/openmw/mwstate/statemanagerimp.hpp @@ -14,6 +14,8 @@ namespace MWState { bool mQuitRequest; bool mAskLoadRecent; + bool mNewGameRequest = false; + std::optional mLoadRequest; State mState; CharacterManager mCharacterManager; double mTimePlayed; @@ -36,6 +38,9 @@ namespace MWState void askLoadRecent() override; + void requestNewGame() override { mNewGameRequest = true; } + void requestLoad(const std::filesystem::path& filepath) override { mLoadRequest = filepath; } + State getState() const override; void newGame(bool bypass = false) override; diff --git a/components/esm/luascripts.hpp b/components/esm/luascripts.hpp index 2a6bf0dbb1..01d322285f 100644 --- a/components/esm/luascripts.hpp +++ b/components/esm/luascripts.hpp @@ -20,8 +20,10 @@ namespace ESM static constexpr Flags sCustom = 1ull << 1; // local; can be attached/detached by a global script static constexpr Flags sPlayer = 1ull << 2; // auto attach to players - static constexpr Flags sMerge = 1ull - << 3; // merge with configuration for this script from previous content files. + // merge with configuration for this script from previous content files. + static constexpr Flags sMerge = 1ull << 3; + + static constexpr Flags sMenu = 1ull << 4; // start as a menu script std::string mScriptPath; // VFS path to the script. std::string mInitializationData; // Serialized Lua table. It is a binary data. Can contain '\0'. diff --git a/components/lua/configuration.cpp b/components/lua/configuration.cpp index 85c0cb6724..c6c296f8d5 100644 --- a/components/lua/configuration.cpp +++ b/components/lua/configuration.cpp @@ -18,6 +18,7 @@ namespace LuaUtil { "GLOBAL", ESM::LuaScriptCfg::sGlobal }, { "CUSTOM", ESM::LuaScriptCfg::sCustom }, { "PLAYER", ESM::LuaScriptCfg::sPlayer }, + { "MENU", ESM::LuaScriptCfg::sMenu }, }; const std::map> typeTagsByName{ diff --git a/components/lua/configuration.hpp b/components/lua/configuration.hpp index 3a2df8e43d..eb2a4cd9a5 100644 --- a/components/lua/configuration.hpp +++ b/components/lua/configuration.hpp @@ -22,6 +22,7 @@ namespace LuaUtil std::optional findId(std::string_view path) const; bool isCustomScript(int id) const { return mScripts[id].mFlags & ESM::LuaScriptCfg::sCustom; } + ScriptIdsWithInitializationData getMenuConf() const { return getConfByFlag(ESM::LuaScriptCfg::sMenu); } ScriptIdsWithInitializationData getGlobalConf() const { return getConfByFlag(ESM::LuaScriptCfg::sGlobal); } ScriptIdsWithInitializationData getPlayerConf() const { return getConfByFlag(ESM::LuaScriptCfg::sPlayer); } ScriptIdsWithInitializationData getLocalConf( diff --git a/components/lua/storage.cpp b/components/lua/storage.cpp index b96da916be..5594b31c6b 100644 --- a/components/lua/storage.cpp +++ b/components/lua/storage.cpp @@ -49,6 +49,14 @@ namespace LuaUtil return !valid; }), mCallbacks.end()); + mPermanentCallbacks.erase(std::remove_if(mPermanentCallbacks.begin(), mPermanentCallbacks.end(), + [&](const Callback& callback) { + bool valid = callback.isValid(); + if (valid) + callback.tryCall(mSectionName, changedKey); + return !valid; + }), + mPermanentCallbacks.end()); mStorage->mRunningCallbacks.erase(this); } @@ -112,7 +120,8 @@ namespace LuaUtil }; sview["asTable"] = [](const SectionView& section) { return section.mSection->asTable(); }; sview["subscribe"] = [](const SectionView& section, const sol::table& callback) { - std::vector& callbacks = section.mSection->mCallbacks; + std::vector& callbacks + = section.mForMenuScripts ? section.mSection->mPermanentCallbacks : section.mSection->mCallbacks; if (!callbacks.empty() && callbacks.size() == callbacks.capacity()) { callbacks.erase( @@ -166,6 +175,16 @@ namespace LuaUtil return LuaUtil::makeReadOnly(res); } + sol::table LuaStorage::initMenuPackage(lua_State* lua, LuaStorage* playerStorage) + { + sol::table res(lua, sol::create); + res["playerSection"] = [playerStorage](std::string_view section) { + return playerStorage->getMutableSection(section, /*forMenuScripts=*/true); + }; + res["allPlayerSections"] = [playerStorage]() { return playerStorage->getAllSections(); }; + return LuaUtil::makeReadOnly(res); + } + void LuaStorage::clearTemporaryAndRemoveCallbacks() { auto it = mData.begin(); @@ -174,6 +193,7 @@ namespace LuaUtil it->second->mCallbacks.clear(); if (!it->second->mPermanent) { + it->second->mPermanentCallbacks.clear(); it->second->mValues.clear(); it = mData.erase(it); } @@ -231,10 +251,10 @@ namespace LuaUtil return newIt->second; } - sol::object LuaStorage::getSection(std::string_view sectionName, bool readOnly) + sol::object LuaStorage::getSection(std::string_view sectionName, bool readOnly, bool forMenuScripts) { const std::shared_ptr
& section = getSection(sectionName); - return sol::make_object(mLua, SectionView{ section, readOnly }); + return sol::make_object(mLua, SectionView{ section, readOnly, forMenuScripts }); } sol::table LuaStorage::getAllSections(bool readOnly) diff --git a/components/lua/storage.hpp b/components/lua/storage.hpp index 9998af9430..a785755f10 100644 --- a/components/lua/storage.hpp +++ b/components/lua/storage.hpp @@ -17,6 +17,7 @@ namespace LuaUtil static sol::table initGlobalPackage(lua_State* lua, LuaStorage* globalStorage); static sol::table initLocalPackage(lua_State* lua, LuaStorage* globalStorage); static sol::table initPlayerPackage(lua_State* lua, LuaStorage* globalStorage, LuaStorage* playerStorage); + static sol::table initMenuPackage(lua_State* lua, LuaStorage* playerStorage); explicit LuaStorage(lua_State* lua) : mLua(lua) @@ -27,8 +28,11 @@ namespace LuaUtil void load(const std::filesystem::path& path); void save(const std::filesystem::path& path) const; - sol::object getSection(std::string_view sectionName, bool readOnly); - sol::object getMutableSection(std::string_view sectionName) { return getSection(sectionName, false); } + sol::object getSection(std::string_view sectionName, bool readOnly, bool forMenuScripts = false); + sol::object getMutableSection(std::string_view sectionName, bool forMenuScripts = false) + { + return getSection(sectionName, false, forMenuScripts); + } sol::object getReadOnlySection(std::string_view sectionName) { return getSection(sectionName, true); } sol::table getAllSections(bool readOnly = false); @@ -87,6 +91,7 @@ namespace LuaUtil std::string mSectionName; std::map> mValues; std::vector mCallbacks; + std::vector mPermanentCallbacks; bool mPermanent = true; static Value sEmpty; }; @@ -94,6 +99,7 @@ namespace LuaUtil { std::shared_ptr
mSection; bool mReadOnly; + bool mForMenuScripts = false; }; const std::shared_ptr
& getSection(std::string_view sectionName); diff --git a/files/data/CMakeLists.txt b/files/data/CMakeLists.txt index dbf86cc44d..14728be732 100644 --- a/files/data/CMakeLists.txt +++ b/files/data/CMakeLists.txt @@ -72,9 +72,10 @@ set(BUILTIN_DATA_FILES scripts/omw/camera/settings.lua scripts/omw/camera/move360.lua scripts/omw/camera/first_person_auto_switch.lua - scripts/omw/console/player.lua scripts/omw/console/global.lua scripts/omw/console/local.lua + scripts/omw/console/player.lua + scripts/omw/console/menu.lua scripts/omw/mechanics/playercontroller.lua scripts/omw/playercontrols.lua scripts/omw/settings/player.lua diff --git a/files/data/builtin.omwscripts b/files/data/builtin.omwscripts index ec08c5299d..c717a13f02 100644 --- a/files/data/builtin.omwscripts +++ b/files/data/builtin.omwscripts @@ -19,6 +19,7 @@ NPC,CREATURE: scripts/omw/ai.lua PLAYER: scripts/omw/ui.lua # Lua console +MENU: scripts/omw/console/menu.lua PLAYER: scripts/omw/console/player.lua GLOBAL: scripts/omw/console/global.lua CUSTOM: scripts/omw/console/local.lua diff --git a/files/data/scripts/omw/console/menu.lua b/files/data/scripts/omw/console/menu.lua new file mode 100644 index 0000000000..1aa3e7b166 --- /dev/null +++ b/files/data/scripts/omw/console/menu.lua @@ -0,0 +1,114 @@ +local menu = require('openmw.menu') +local ui = require('openmw.ui') +local util = require('openmw.util') + +local menuModeName = 'Lua[Menu]' + +local function printHelp() + local msg = [[ +This is the built-in Lua interpreter. +help() - print this message +exit() - exit Lua mode +view(_G) - print content of the table `_G` (current environment) + standard libraries (math, string, etc.) are loaded by default but not visible in `_G` +view(menu, 2) - print table `menu` (i.e. `openmw.menu`) and its subtables (2 - traversal depth)]] + ui.printToConsole(msg, ui.CONSOLE_COLOR.Info) +end + +local function printToConsole(...) + local strs = {} + for i = 1, select('#', ...) do + strs[i] = tostring(select(i, ...)) + end + return ui.printToConsole(table.concat(strs, '\t'), ui.CONSOLE_COLOR.Info) +end + +local function printRes(...) + if select('#', ...) >= 0 then + printToConsole(...) + end +end + +local function exitLuaMenuMode() + ui.setConsoleMode('') + ui.printToConsole('Lua mode OFF', ui.CONSOLE_COLOR.Success) +end + +local function enterLuaMenuMode() + ui.printToConsole('Lua mode ON, use exit() to return, help() for more info', ui.CONSOLE_COLOR.Success) + ui.printToConsole('Context: Menu', ui.CONSOLE_COLOR.Success) + ui.setConsoleMode(menuModeName) +end + +local env = { + I = require('openmw.interfaces'), + menu = require('openmw.menu'), + util = require('openmw.util'), + core = require('openmw.core'), + storage = require('openmw.storage'), + vfs = require('openmw.vfs'), + ambient = require('openmw.ambient'), + async = require('openmw.async'), + ui = require('openmw.ui'), + aux_util = require('openmw_aux.util'), + view = require('openmw_aux.util').deepToString, + print = printToConsole, + exit = exitLuaMenuMode, + help = printHelp, +} +env._G = env +setmetatable(env, {__index = _G, __metatable = false}) +_G = nil + +local function executeLuaCode(code) + local fn + local ok, err = pcall(function() fn = util.loadCode('return ' .. code, env) end) + if ok then + ok, err = pcall(function() printRes(fn()) end) + else + ok, err = pcall(function() util.loadCode(code, env)() end) + end + if not ok then + ui.printToConsole(err, ui.CONSOLE_COLOR.Error) + end +end + +local usageInfo = [[ +Usage: 'lua menu' or 'luam' - enter menu context +Other contexts are available only when the game is started: + 'lua player' or 'luap' - enter player context + 'lua global' or 'luag' - enter global context + 'lua selected' or 'luas' - enter local context on the selected object]] + +local function onConsoleCommand(mode, cmd) + if mode == '' then + cmd, arg = cmd:lower():match('(%w+) *(%w*)') + if (cmd == 'lua' and arg == 'menu') or cmd == 'luam' then + enterLuaMenuMode() + elseif menu.getState() == menu.STATE.NoGame and (cmd == 'lua' or cmd == 'luap' or cmd == 'luas' or cmd == 'luag') then + ui.printToConsole(usageInfo, ui.CONSOLE_COLOR.Info) + end + elseif mode == menuModeName then + if cmd == 'exit()' then + exitLuaMenuMode() + else + executeLuaCode(cmd) + end + end +end + +local function onStateChanged() + local mode = ui.getConsoleMode() + if menu.getState() ~= menu.STATE.Ended and mode ~= menuModeName then + -- When a new game started or loaded reset console mode (except of `luam`) because + -- other modes become invalid after restarting Lua scripts. + ui.setConsoleMode('') + end +end + +return { + engineHandlers = { + onConsoleCommand = onConsoleCommand, + onStateChanged = onStateChanged, + }, +} diff --git a/files/data/scripts/omw/console/player.lua b/files/data/scripts/omw/console/player.lua index c614d2d962..6d0ee790a9 100644 --- a/files/data/scripts/omw/console/player.lua +++ b/files/data/scripts/omw/console/player.lua @@ -77,6 +77,7 @@ local env = { nearby = require('openmw.nearby'), self = require('openmw.self'), input = require('openmw.input'), + postprocessing = require('openmw.postprocessing'), ui = require('openmw.ui'), camera = require('openmw.camera'), aux_util = require('openmw_aux.util'), @@ -114,9 +115,12 @@ local function onConsoleCommand(mode, cmd, selectedObject) cmd = 'luag' elseif arg == 'selected' then cmd = 'luas' + elseif arg == 'menu' then + -- handled in menu.lua else local msg = [[ -Usage: 'lua player' or 'luap' - enter player context +Usage: 'lua menu' or 'luam' - enter menu context + 'lua player' or 'luap' - enter player context 'lua global' or 'luag' - enter global context 'lua selected' or 'luas' - enter local context on the selected object]] ui.printToConsole(msg, ui.CONSOLE_COLOR.Info) @@ -158,4 +162,3 @@ return { OMWConsoleHelp = printHelp, } } - From 889ddc10d6b51827239532efa083590b5e5bae09 Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Tue, 31 Oct 2023 10:22:58 +0100 Subject: [PATCH 02/39] Enable `openmw.input` in menu scripts --- apps/openmw/mwlua/inputbindings.cpp | 10 +++++++++- apps/openmw/mwlua/luabindings.cpp | 10 ++++------ files/data/scripts/omw/console/menu.lua | 1 + 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/apps/openmw/mwlua/inputbindings.cpp b/apps/openmw/mwlua/inputbindings.cpp index 02babf0399..41b2b5c94f 100644 --- a/apps/openmw/mwlua/inputbindings.cpp +++ b/apps/openmw/mwlua/inputbindings.cpp @@ -24,6 +24,12 @@ namespace MWLua sol::table initInputPackage(const Context& context) { + { + sol::state_view& lua = context.mLua->sol(); + if (lua["openmw_input"] != sol::nil) + return lua["openmw_input"]; + } + sol::usertype keyEvent = context.mLua->sol().new_usertype("KeyEvent"); keyEvent["symbol"] = sol::readonly_property([](const SDL_Keysym& e) { if (e.sym > 0 && e.sym <= 255) @@ -291,7 +297,9 @@ namespace MWLua { "Tab", SDL_SCANCODE_TAB }, })); - return LuaUtil::makeReadOnly(api); + sol::state_view& lua = context.mLua->sol(); + lua["openmw_input"] = LuaUtil::makeReadOnly(api); + return lua["openmw_input"]; } } diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp index 931cd43296..a7269d6e52 100644 --- a/apps/openmw/mwlua/luabindings.cpp +++ b/apps/openmw/mwlua/luabindings.cpp @@ -76,13 +76,11 @@ namespace MWLua std::map initMenuPackages(const Context& context) { return { - { "openmw.core", initCorePackageForMenuScripts(context) }, // - { "openmw.ambient", initAmbientPackage(context) }, // - { "openmw.ui", initUserInterfacePackage(context) }, // + { "openmw.core", initCorePackageForMenuScripts(context) }, + { "openmw.ambient", initAmbientPackage(context) }, + { "openmw.ui", initUserInterfacePackage(context) }, { "openmw.menu", initMenuPackage(context) }, - // TODO: Maybe add: - // { "openmw.input", initInputPackage(context) }, - // { "openmw.postprocessing", initPostprocessingPackage(context) }, + { "openmw.input", initInputPackage(context) }, }; } } diff --git a/files/data/scripts/omw/console/menu.lua b/files/data/scripts/omw/console/menu.lua index 1aa3e7b166..9d6dbaf1d7 100644 --- a/files/data/scripts/omw/console/menu.lua +++ b/files/data/scripts/omw/console/menu.lua @@ -50,6 +50,7 @@ local env = { ambient = require('openmw.ambient'), async = require('openmw.async'), ui = require('openmw.ui'), + input = require('openmw.input'), aux_util = require('openmw_aux.util'), view = require('openmw_aux.util').deepToString, print = printToConsole, From 1490f6f082562f4943e4f1fbaabb877249def658 Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Tue, 31 Oct 2023 10:24:33 +0100 Subject: [PATCH 03/39] Fix: lower content file names in `menu.getSaves` --- apps/openmw/mwlua/menuscripts.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwlua/menuscripts.cpp b/apps/openmw/mwlua/menuscripts.cpp index 300f5ad489..16e57961b1 100644 --- a/apps/openmw/mwlua/menuscripts.cpp +++ b/apps/openmw/mwlua/menuscripts.cpp @@ -1,5 +1,7 @@ #include "menuscripts.hpp" +#include + #include "../mwbase/environment.hpp" #include "../mwbase/statemanager.hpp" #include "../mwstate/character.hpp" @@ -85,7 +87,7 @@ namespace MWLua slotInfo["playerLevel"] = slot.mProfile.mPlayerLevel; sol::table contentFiles(lua, sol::create); for (size_t i = 0; i < slot.mProfile.mContentFiles.size(); ++i) - contentFiles[i + 1] = slot.mProfile.mContentFiles[i]; + contentFiles[i + 1] = Misc::StringUtils::lowerCase(slot.mProfile.mContentFiles[i]); slotInfo["contentFiles"] = contentFiles; saves[slot.mPath.filename().string()] = slotInfo; } From dafe858cb4e115f8acaa83147284793d275bf1e0 Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Tue, 31 Oct 2023 10:28:52 +0100 Subject: [PATCH 04/39] Add types.Player.sendMenuEvent --- apps/openmw/mwlua/luaevents.cpp | 9 ++++++++ apps/openmw/mwlua/luaevents.hpp | 8 +++++++- apps/openmw/mwlua/luamanagerimp.cpp | 1 + apps/openmw/mwlua/luamanagerimp.hpp | 2 +- apps/openmw/mwlua/types/player.cpp | 32 ++++++++++++++++------------- 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/apps/openmw/mwlua/luaevents.cpp b/apps/openmw/mwlua/luaevents.cpp index 02ea3415d2..4ffb4fc1cc 100644 --- a/apps/openmw/mwlua/luaevents.cpp +++ b/apps/openmw/mwlua/luaevents.cpp @@ -13,6 +13,7 @@ #include "globalscripts.hpp" #include "localscripts.hpp" +#include "menuscripts.hpp" namespace MWLua { @@ -23,6 +24,7 @@ namespace MWLua mLocalEventBatch.clear(); mNewGlobalEventBatch.clear(); mNewLocalEventBatch.clear(); + mMenuEvents.clear(); } void LuaEvents::finalizeEventBatch() @@ -51,6 +53,13 @@ namespace MWLua mLocalEventBatch.clear(); } + void LuaEvents::callMenuEventHandlers() + { + for (const Global& e : mMenuEvents) + mMenuScripts.receiveEvent(e.mEventName, e.mEventData); + mMenuEvents.clear(); + } + template static void saveEvent(ESM::ESMWriter& esm, ESM::RefNum dest, const Event& event) { diff --git a/apps/openmw/mwlua/luaevents.hpp b/apps/openmw/mwlua/luaevents.hpp index 5eeae46538..3890b45b6d 100644 --- a/apps/openmw/mwlua/luaevents.hpp +++ b/apps/openmw/mwlua/luaevents.hpp @@ -23,12 +23,14 @@ namespace MWLua { class GlobalScripts; + class MenuScripts; class LuaEvents { public: - explicit LuaEvents(GlobalScripts& globalScripts) + explicit LuaEvents(GlobalScripts& globalScripts, MenuScripts& menuScripts) : mGlobalScripts(globalScripts) + , mMenuScripts(menuScripts) { } @@ -45,11 +47,13 @@ namespace MWLua }; void addGlobalEvent(Global event) { mNewGlobalEventBatch.push_back(std::move(event)); } + void addMenuEvent(Global event) { mMenuEvents.push_back(std::move(event)); } void addLocalEvent(Local event) { mNewLocalEventBatch.push_back(std::move(event)); } void clear(); void finalizeEventBatch(); void callEventHandlers(); + void callMenuEventHandlers(); void load(lua_State* lua, ESM::ESMReader& esm, const std::map& contentFileMapping, const LuaUtil::UserdataSerializer* serializer); @@ -57,10 +61,12 @@ namespace MWLua private: GlobalScripts& mGlobalScripts; + MenuScripts& mMenuScripts; std::vector mNewGlobalEventBatch; std::vector mNewLocalEventBatch; std::vector mGlobalEventBatch; std::vector mLocalEventBatch; + std::vector mMenuEvents; }; } diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 77ddfcc4a7..52b5dace3b 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -229,6 +229,7 @@ namespace MWLua playerScripts->processInputEvent(event); } mInputEvents.clear(); + mLuaEvents.callMenuEventHandlers(); mMenuScripts.update(0); if (playerScripts) playerScripts->onFrame(MWBase::Environment::get().getWorld()->getTimeManager()->isPaused() diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index b16e81082b..53031516a8 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -172,7 +172,7 @@ namespace MWLua MWWorld::Ptr mPlayer; - LuaEvents mLuaEvents{ mGlobalScripts }; + LuaEvents mLuaEvents{ mGlobalScripts, mMenuScripts }; EngineEvents mEngineEvents{ mGlobalScripts }; std::vector mInputEvents; diff --git a/apps/openmw/mwlua/types/player.cpp b/apps/openmw/mwlua/types/player.cpp index cef0753817..3acfec12b7 100644 --- a/apps/openmw/mwlua/types/player.cpp +++ b/apps/openmw/mwlua/types/player.cpp @@ -35,14 +35,18 @@ namespace sol namespace MWLua { + static void verifyPlayer(const Object& player) + { + if (player.ptr() != MWBase::Environment::get().getWorld()->getPlayerPtr()) + throw std::runtime_error("The argument must be a player!"); + } void addPlayerQuestBindings(sol::table& player, const Context& context) { MWBase::Journal* const journal = MWBase::Environment::get().getJournal(); player["quests"] = [](const Object& player) { - if (player.ptr() != MWBase::Environment::get().getWorld()->getPlayerPtr()) - throw std::runtime_error("The argument must be a player!"); + verifyPlayer(player); bool allowChanges = dynamic_cast(&player) != nullptr || dynamic_cast(&player) != nullptr; return Quests{ .mMutable = allowChanges }; @@ -134,28 +138,28 @@ namespace MWLua MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); player["getControlSwitch"] = [input](const Object& player, std::string_view key) { - if (player.ptr() != MWBase::Environment::get().getWorld()->getPlayerPtr()) - throw std::runtime_error("The argument must be a player."); + verifyPlayer(player); return input->getControlSwitch(key); }; + player["setControlSwitch"] = [input](const Object& player, std::string_view key, bool v) { + verifyPlayer(player); + if (dynamic_cast(&player) && !dynamic_cast(&player)) + throw std::runtime_error("Only player and global scripts can toggle control switches."); + input->toggleControlSwitch(key, v); + }; player["isTeleportingEnabled"] = [](const Object& player) -> bool { - if (player.ptr() != MWBase::Environment::get().getWorld()->getPlayerPtr()) - throw std::runtime_error("The argument must be a player."); + verifyPlayer(player); return MWBase::Environment::get().getWorld()->isTeleportingEnabled(); }; player["setTeleportingEnabled"] = [](const Object& player, bool state) { - if (player.ptr() != MWBase::Environment::get().getWorld()->getPlayerPtr()) - throw std::runtime_error("The argument must be a player."); + verifyPlayer(player); if (dynamic_cast(&player) && !dynamic_cast(&player)) throw std::runtime_error("Only player and global scripts can toggle teleportation."); MWBase::Environment::get().getWorld()->enableTeleporting(state); }; - player["setControlSwitch"] = [input](const Object& player, std::string_view key, bool v) { - if (player.ptr() != MWBase::Environment::get().getWorld()->getPlayerPtr()) - throw std::runtime_error("The argument must be a player."); - if (dynamic_cast(&player) && !dynamic_cast(&player)) - throw std::runtime_error("Only player and global scripts can toggle control switches."); - input->toggleControlSwitch(key, v); + player["sendMenuEvent"] = [context](const Object& player, std::string eventName, const sol::object& eventData) { + verifyPlayer(player); + context.mLuaEvents->addMenuEvent({ std::move(eventName), LuaUtil::serialize(eventData) }); }; } From f5325e11e343274b40d12fa0dad05f590e44ffb7 Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Tue, 31 Oct 2023 11:01:17 +0100 Subject: [PATCH 05/39] Rename mPermanentCallbacks -> mMenuScriptsCallbacks in LuaUtil::Storage --- components/lua/storage.cpp | 22 ++++++++++++---------- components/lua/storage.hpp | 3 ++- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/components/lua/storage.cpp b/components/lua/storage.cpp index 5594b31c6b..b2f972e853 100644 --- a/components/lua/storage.cpp +++ b/components/lua/storage.cpp @@ -49,14 +49,14 @@ namespace LuaUtil return !valid; }), mCallbacks.end()); - mPermanentCallbacks.erase(std::remove_if(mPermanentCallbacks.begin(), mPermanentCallbacks.end(), - [&](const Callback& callback) { - bool valid = callback.isValid(); - if (valid) - callback.tryCall(mSectionName, changedKey); - return !valid; - }), - mPermanentCallbacks.end()); + mMenuScriptsCallbacks.erase(std::remove_if(mMenuScriptsCallbacks.begin(), mMenuScriptsCallbacks.end(), + [&](const Callback& callback) { + bool valid = callback.isValid(); + if (valid) + callback.tryCall(mSectionName, changedKey); + return !valid; + }), + mMenuScriptsCallbacks.end()); mStorage->mRunningCallbacks.erase(this); } @@ -121,7 +121,7 @@ namespace LuaUtil sview["asTable"] = [](const SectionView& section) { return section.mSection->asTable(); }; sview["subscribe"] = [](const SectionView& section, const sol::table& callback) { std::vector& callbacks - = section.mForMenuScripts ? section.mSection->mPermanentCallbacks : section.mSection->mCallbacks; + = section.mForMenuScripts ? section.mSection->mMenuScriptsCallbacks : section.mSection->mCallbacks; if (!callbacks.empty() && callbacks.size() == callbacks.capacity()) { callbacks.erase( @@ -191,9 +191,11 @@ namespace LuaUtil while (it != mData.end()) { it->second->mCallbacks.clear(); + // Note that we don't clear menu callbacks for permanent sections + // because starting/loading a game doesn't reset menu scripts. if (!it->second->mPermanent) { - it->second->mPermanentCallbacks.clear(); + it->second->mMenuScriptsCallbacks.clear(); it->second->mValues.clear(); it = mData.erase(it); } diff --git a/components/lua/storage.hpp b/components/lua/storage.hpp index a785755f10..3376e7e50c 100644 --- a/components/lua/storage.hpp +++ b/components/lua/storage.hpp @@ -91,7 +91,8 @@ namespace LuaUtil std::string mSectionName; std::map> mValues; std::vector mCallbacks; - std::vector mPermanentCallbacks; + std::vector mMenuScriptsCallbacks; // menu callbacks are in a separate vector because we don't + // remove them in clear() bool mPermanent = true; static Value sEmpty; }; From a6e2ceebb832ddb43e0cb606f571a99b8477741b Mon Sep 17 00:00:00 2001 From: uramer Date: Sun, 7 Jan 2024 23:29:20 +0100 Subject: [PATCH 06/39] Don't clear menu UI on game load --- apps/openmw/mwgui/windowmanagerimp.cpp | 3 ++- apps/openmw/mwlua/context.hpp | 1 + apps/openmw/mwlua/luamanagerimp.cpp | 11 ++++++++--- apps/openmw/mwlua/uibindings.cpp | 24 ++++++++++++++++++------ components/lua_ui/element.cpp | 17 +++++++++++++---- components/lua_ui/element.hpp | 20 +++++++++++++++----- components/lua_ui/util.cpp | 13 ++++++++++--- components/lua_ui/util.hpp | 3 ++- 8 files changed, 69 insertions(+), 23 deletions(-) diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index 09b4bd5d5f..c6b729332a 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -546,7 +546,8 @@ namespace MWGui { try { - LuaUi::clearUserInterface(); + LuaUi::clearGameInterface(); + LuaUi::clearMenuInterface(); mStatsWatcher.reset(); diff --git a/apps/openmw/mwlua/context.hpp b/apps/openmw/mwlua/context.hpp index 68b46164d6..def38a5309 100644 --- a/apps/openmw/mwlua/context.hpp +++ b/apps/openmw/mwlua/context.hpp @@ -15,6 +15,7 @@ namespace MWLua struct Context { + bool mIsMenu; bool mIsGlobal; LuaManager* mLuaManager; LuaUtil::LuaState* mLua; diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 2e60d4ea2d..97e1dfae39 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -75,6 +75,7 @@ namespace MWLua void LuaManager::init() { Context context; + context.mIsMenu = false; context.mIsGlobal = true; context.mLuaManager = this; context.mLua = &mLua; @@ -86,11 +87,14 @@ namespace MWLua localContext.mIsGlobal = false; localContext.mSerializer = mLocalSerializer.get(); + Context menuContext = context; + menuContext.mIsMenu = true; + for (const auto& [name, package] : initCommonPackages(context)) mLua.addCommonPackage(name, package); for (const auto& [name, package] : initGlobalPackages(context)) mGlobalScripts.addPackage(name, package); - for (const auto& [name, package] : initMenuPackages(context)) + for (const auto& [name, package] : initMenuPackages(menuContext)) mMenuScripts.addPackage(name, package); mLocalPackages = initLocalPackages(localContext); @@ -278,7 +282,7 @@ namespace MWLua void LuaManager::clear() { - LuaUi::clearUserInterface(); + LuaUi::clearGameInterface(); mUiResourceManager.clear(); MWBase::Environment::get().getWorld()->getPostProcessor()->disableDynamicShaders(); mActiveLocalScripts.clear(); @@ -526,7 +530,8 @@ namespace MWLua { Log(Debug::Info) << "Reload Lua"; - LuaUi::clearUserInterface(); + LuaUi::clearGameInterface(); + LuaUi::clearMenuInterface(); MWBase::Environment::get().getWindowManager()->setConsoleMode(""); MWBase::Environment::get().getL10nManager()->dropCache(); mUiResourceManager.clear(); diff --git a/apps/openmw/mwlua/uibindings.cpp b/apps/openmw/mwlua/uibindings.cpp index 3a838f4544..54d8523b4d 100644 --- a/apps/openmw/mwlua/uibindings.cpp +++ b/apps/openmw/mwlua/uibindings.cpp @@ -155,15 +155,27 @@ namespace MWLua } }; api["content"] = LuaUi::loadContentConstructor(context.mLua); - api["create"] = [luaManager = context.mLuaManager](const sol::table& layout) { - auto element = LuaUi::Element::make(layout); - luaManager->addAction([element] { wrapAction(element, [&] { element->create(); }); }, "Create UI"); + api["create"] = [context](const sol::table& layout) { + auto element + = context.mIsMenu ? LuaUi::Element::makeMenuElement(layout) : LuaUi::Element::makeGameElement(layout); + context.mLuaManager->addAction([element] { wrapAction(element, [&] { element->create(); }); }, "Create UI"); return element; }; api["updateAll"] = [context]() { - LuaUi::Element::forEach([](LuaUi::Element* e) { e->mUpdate = true; }); - context.mLuaManager->addAction( - []() { LuaUi::Element::forEach([](LuaUi::Element* e) { e->update(); }); }, "Update all UI elements"); + if (context.mIsMenu) + { + LuaUi::Element::forEachMenuElement([](LuaUi::Element* e) { e->mUpdate = true; }); + context.mLuaManager->addAction( + []() { LuaUi::Element::forEachMenuElement([](LuaUi::Element* e) { e->update(); }); }, + "Update all menu UI elements"); + } + else + { + LuaUi::Element::forEachGameElement([](LuaUi::Element* e) { e->mUpdate = true; }); + context.mLuaManager->addAction( + []() { LuaUi::Element::forEachGameElement([](LuaUi::Element* e) { e->update(); }); }, + "Update all game UI elements"); + } }; api["_getMenuTransparency"] = []() -> float { return Settings::gui().mMenuTransparency; }; diff --git a/components/lua_ui/element.cpp b/components/lua_ui/element.cpp index baa3438982..b74938b044 100644 --- a/components/lua_ui/element.cpp +++ b/components/lua_ui/element.cpp @@ -240,7 +240,8 @@ namespace LuaUi } } - std::map> Element::sAllElements; + std::map> Element::sGameElements; + std::map> Element::sMenuElements; Element::Element(sol::table layout) : mRoot(nullptr) @@ -251,10 +252,17 @@ namespace LuaUi { } - std::shared_ptr Element::make(sol::table layout) + std::shared_ptr Element::makeGameElement(sol::table layout) { std::shared_ptr ptr(new Element(std::move(layout))); - sAllElements[ptr.get()] = ptr; + sGameElements[ptr.get()] = ptr; + return ptr; + } + + std::shared_ptr Element::makeMenuElement(sol::table layout) + { + std::shared_ptr ptr(new Element(std::move(layout))); + sMenuElements[ptr.get()] = ptr; return ptr; } @@ -302,6 +310,7 @@ namespace LuaUi mRoot = nullptr; mLayout = sol::make_object(mLayout.lua_state(), sol::nil); } - sAllElements.erase(this); + sGameElements.erase(this); + sMenuElements.erase(this); } } diff --git a/components/lua_ui/element.hpp b/components/lua_ui/element.hpp index 5aadb1beab..0446f448b6 100644 --- a/components/lua_ui/element.hpp +++ b/components/lua_ui/element.hpp @@ -7,12 +7,20 @@ namespace LuaUi { struct Element { - static std::shared_ptr make(sol::table layout); + static std::shared_ptr makeGameElement(sol::table layout); + static std::shared_ptr makeMenuElement(sol::table layout); template - static void forEach(Callback callback) + static void forEachGameElement(Callback callback) { - for (auto& [e, _] : sAllElements) + for (auto& [e, _] : sGameElements) + callback(e); + } + + template + static void forEachMenuElement(Callback callback) + { + for (auto& [e, _] : sMenuElements) callback(e); } @@ -28,12 +36,14 @@ namespace LuaUi void destroy(); - friend void clearUserInterface(); + friend void clearGameInterface(); + friend void clearMenuInterface(); private: Element(sol::table layout); sol::table layout() { return LuaUtil::cast(mLayout); } - static std::map> sAllElements; + static std::map> sGameElements; + static std::map> sMenuElements; }; } diff --git a/components/lua_ui/util.cpp b/components/lua_ui/util.cpp index 78237c54ea..ac5e63e405 100644 --- a/components/lua_ui/util.cpp +++ b/components/lua_ui/util.cpp @@ -44,10 +44,17 @@ namespace LuaUi return types; } - void clearUserInterface() + void clearGameInterface() { + // TODO: move settings clearing logic to Lua? clearSettings(); - while (!Element::sAllElements.empty()) - Element::sAllElements.begin()->second->destroy(); + while (!Element::sGameElements.empty()) + Element::sGameElements.begin()->second->destroy(); + } + + void clearMenuInterface() + { + while (!Element::sMenuElements.empty()) + Element::sMenuElements.begin()->second->destroy(); } } diff --git a/components/lua_ui/util.hpp b/components/lua_ui/util.hpp index 78daf6669c..2b5c4ff13c 100644 --- a/components/lua_ui/util.hpp +++ b/components/lua_ui/util.hpp @@ -10,7 +10,8 @@ namespace LuaUi const std::unordered_map& widgetTypeToName(); - void clearUserInterface(); + void clearGameInterface(); + void clearMenuInterface(); } #endif // OPENMW_LUAUI_WIDGETLIST From 539dc1ee43de7de947b68b5722afd6a10b3c9d43 Mon Sep 17 00:00:00 2001 From: uramer Date: Mon, 8 Jan 2024 21:57:59 +0100 Subject: [PATCH 07/39] Remove confusing addPlayerQuestBindings method --- apps/openmw/mwlua/types/player.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/openmw/mwlua/types/player.cpp b/apps/openmw/mwlua/types/player.cpp index 3acfec12b7..1be84b04fa 100644 --- a/apps/openmw/mwlua/types/player.cpp +++ b/apps/openmw/mwlua/types/player.cpp @@ -41,7 +41,7 @@ namespace MWLua throw std::runtime_error("The argument must be a player!"); } - void addPlayerQuestBindings(sol::table& player, const Context& context) + void addPlayerBindings(sol::table player, const Context& context) { MWBase::Journal* const journal = MWBase::Environment::get().getJournal(); @@ -161,10 +161,7 @@ namespace MWLua verifyPlayer(player); context.mLuaEvents->addMenuEvent({ std::move(eventName), LuaUtil::serialize(eventData) }); }; - } - void addPlayerBindings(sol::table player, const Context& context) - { player["getCrimeLevel"] = [](const Object& o) -> int { const MWWorld::Class& cls = o.ptr().getClass(); return cls.getNpcStats(o.ptr()).getBounty(); @@ -172,6 +169,5 @@ namespace MWLua player["isCharGenFinished"] = [](const Object&) -> bool { return MWBase::Environment::get().getWorld()->getGlobalFloat(MWWorld::Globals::sCharGenState) == -1; }; - addPlayerQuestBindings(player, context); } } From a3fd1b3d6f944cca94873c8be47cdf02fc1480e8 Mon Sep 17 00:00:00 2001 From: uramer Date: Mon, 8 Jan 2024 21:58:07 +0100 Subject: [PATCH 08/39] Document menu scripts --- .../lua-scripting/engine_handlers.rst | 11 ++++ .../reference/lua-scripting/openmw_menu.rst | 7 ++ .../reference/lua-scripting/overview.rst | 10 +++ files/lua_api/CMakeLists.txt | 1 + files/lua_api/openmw/menu.lua | 65 +++++++++++++++++++ files/lua_api/openmw/types.lua | 6 ++ 6 files changed, 100 insertions(+) create mode 100644 docs/source/reference/lua-scripting/openmw_menu.rst create mode 100644 files/lua_api/openmw/menu.lua diff --git a/docs/source/reference/lua-scripting/engine_handlers.rst b/docs/source/reference/lua-scripting/engine_handlers.rst index 10ed3ee555..6ef9846d2e 100644 --- a/docs/source/reference/lua-scripting/engine_handlers.rst +++ b/docs/source/reference/lua-scripting/engine_handlers.rst @@ -127,3 +127,14 @@ Engine handler is a function defined by a script, that can be called by the engi - | User entered `command` in in-game console. Called if either | `mode` is not default or `command` starts with prefix `lua`. +**Only for menu scripts** + +.. list-table:: + :widths: 20 80 + * - onStateChanged() + - | Called whenever the current game changes + | (i. e. the result of `getState `_ changes) + * - | onConsoleCommand( + | mode, command, selectedObject) + - | User entered `command` in in-game console. Called if either + | `mode` is not default or `command` starts with prefix `lua`. diff --git a/docs/source/reference/lua-scripting/openmw_menu.rst b/docs/source/reference/lua-scripting/openmw_menu.rst new file mode 100644 index 0000000000..587e4337e0 --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_menu.rst @@ -0,0 +1,7 @@ +Package openmw.menu +====================== + +.. include:: version.rst + +.. raw:: html + :file: generated_html/openmw_ambient.html diff --git a/docs/source/reference/lua-scripting/overview.rst b/docs/source/reference/lua-scripting/overview.rst index 5515351e20..ec5ab7338c 100644 --- a/docs/source/reference/lua-scripting/overview.rst +++ b/docs/source/reference/lua-scripting/overview.rst @@ -70,6 +70,9 @@ Cell Global scripts Lua scripts that are not attached to any game object and are always active. Global scripts can not be started or stopped during a game session. Lists of global scripts are defined by `omwscripts` files, which should be :ref:`registered ` in `openmw.cfg`. +Menu scripts + Lua scripts that are ran regardless of a game being loaded. They can be used to add features to the main menu and manage save files. + Local scripts Lua scripts that are attached to some game object. A local script is active only if the object it is attached to is in an active cell. There are no limitations to the number of local scripts on one object. Local scripts can be attached to (or detached from) any object at any moment by a global script. In some cases inactive local scripts still can run code (for example during saving and loading), but while inactive they can not see nearby objects. @@ -173,6 +176,7 @@ The order of lines determines the script load order (i.e. script priorities). Possible flags are: - ``GLOBAL`` - a global script; always active, can not be stopped; +- ``MENU`` - a menu script; always active, even before a game is loaded - ``CUSTOM`` - dynamic local script that can be started or stopped by a global script; - ``PLAYER`` - an auto started player script; - ``ACTIVATOR`` - a local script that will be automatically attached to any activator; @@ -474,6 +478,12 @@ This is another kind of script-to-script interactions. The differences: - Event handlers can not return any data to the sender. - Event handlers have a single argument `eventData` (must be :ref:`serializable `) +There are a few methods for sending events: + +- `core.sendGlovalEvent `_ to send events to global scripts +- `GameObject:sendEvent `_ to send events to local scripts attached to a game object +- `types.Player.sendMenuEvent `_ to send events to menu scripts of the given player + Events are the main way of interacting between local and global scripts. They are not recommended for interactions between two global scripts, because in this case interfaces are more convenient. diff --git a/files/lua_api/CMakeLists.txt b/files/lua_api/CMakeLists.txt index 96409e803e..06c90e4633 100644 --- a/files/lua_api/CMakeLists.txt +++ b/files/lua_api/CMakeLists.txt @@ -21,6 +21,7 @@ set(LUA_API_FILES openmw/util.lua openmw/vfs.lua openmw/world.lua + openmw/menu.lua ) foreach (f ${LUA_API_FILES}) diff --git a/files/lua_api/openmw/menu.lua b/files/lua_api/openmw/menu.lua new file mode 100644 index 0000000000..c1a1a65a62 --- /dev/null +++ b/files/lua_api/openmw/menu.lua @@ -0,0 +1,65 @@ +--- +-- `openmw.menu` can be used only in menu scripts. +-- @module menu +-- @usage local menu = require('openmw.menu') + +--- +-- @type STATE +-- @field [parent=#STATE] NoGame +-- @field [parent=#STATE] Running +-- @field [parent=#STATE] Ended + + +--- +-- Current game state +-- @function [parent=#menu] getState +-- @return #STATE + +--- +-- Start a new game +-- @function [parent=#menu] newGame + +--- +-- Load the game from a save slot +-- @function [parent=#menu] loadGame +-- @param #string directory name of the save directory (e. g. character) +-- @param #string slotName name of the save slot + +--- +-- Delete a saved game +-- @function [parent=#menu] deleteGame +-- @param #string directory name of the save directory (e. g. character) +-- @param #string slotName name of the save slot + +--- +-- Current save directory +-- @function [parent=#menu] getCurrentSaveDir +-- @return #string + +--- +-- Save the game +-- @function [parent=#menu] saveGame +-- @param #string description human readable description of the save +-- @param #string slotName name of the save slot + +--- +-- @type SaveInfo +-- @field #string description +-- @field #string playerName +-- @field #string playerLevel +-- @field #list<#string> contentFiles + +--- +-- List of all saves for the given directory +-- @function [parent=#menu] getSaves +-- @param #string directory name of the save directory (e. g. character) +-- @return #list<#SaveInfo> + +--- +-- List of all available saves +-- @function [parent=#menu] getAllSaves +-- @return #list<#SaveInfo> + +--- +-- Exit the game +-- @function [parent=#menu] quit diff --git a/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index a350d4dbea..6abb209b2c 100644 --- a/files/lua_api/openmw/types.lua +++ b/files/lua_api/openmw/types.lua @@ -1035,6 +1035,12 @@ -- Values that can be used with getControlSwitch/setControlSwitch. -- @field [parent=#Player] #CONTROL_SWITCH CONTROL_SWITCH +--- +-- Send an event to menu scripts. +-- @function [parent=#core] sendMenuEvent +-- @param openmw.core#GameObject player +-- @param #string eventName +-- @param eventData -------------------------------------------------------------------------------- -- @{#Armor} functions From 88049ffac65f5e2308f57a5b4d57e70d8d0daefd Mon Sep 17 00:00:00 2001 From: uramer Date: Mon, 8 Jan 2024 21:14:44 +0100 Subject: [PATCH 09/39] Document packages available in menu scripts --- files/lua_api/openmw/ambient.lua | 2 +- files/lua_api/openmw/core.lua | 4 ++-- files/lua_api/openmw/input.lua | 2 +- files/lua_api/openmw/ui.lua | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/files/lua_api/openmw/ambient.lua b/files/lua_api/openmw/ambient.lua index 917ec86c85..9601ecc076 100644 --- a/files/lua_api/openmw/ambient.lua +++ b/files/lua_api/openmw/ambient.lua @@ -1,6 +1,6 @@ --- -- `openmw.ambient` controls background sounds, specific to given player (2D-sounds). --- Can be used only by local scripts, that are attached to a player. +-- Can be used only by menu scripts and local scripts, that are attached to a player. -- @module ambient -- @usage local ambient = require('openmw.ambient') diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index 890532fc13..a4d8ecee45 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -1,6 +1,6 @@ --- --- `openmw.core` defines functions and types that are available in both local --- and global scripts. +-- `openmw.core` defines functions and types that are available in local, +-- global and menu scripts. -- @module core -- @usage local core = require('openmw.core') diff --git a/files/lua_api/openmw/input.lua b/files/lua_api/openmw/input.lua index 0a85602bcc..a34bc040da 100644 --- a/files/lua_api/openmw/input.lua +++ b/files/lua_api/openmw/input.lua @@ -1,5 +1,5 @@ --- --- `openmw.input` can be used only in scripts attached to a player. +-- `openmw.input` can be used only in menu scripts and scripts attached to a player. -- @module input -- @usage local input = require('openmw.input') diff --git a/files/lua_api/openmw/ui.lua b/files/lua_api/openmw/ui.lua index 451f919077..8582996c4f 100644 --- a/files/lua_api/openmw/ui.lua +++ b/files/lua_api/openmw/ui.lua @@ -1,6 +1,6 @@ --- -- `openmw.ui` controls user interface. --- Can be used only by local scripts, that are attached to a player. +-- Can be used only by menu scripts and local scripts, that are attached to a player. -- @module ui -- @usage -- local ui = require('openmw.ui') From 9b54f479e8fadd24bf45fe58579e86cd8e47f8cb Mon Sep 17 00:00:00 2001 From: uramer Date: Mon, 8 Jan 2024 21:55:09 +0100 Subject: [PATCH 10/39] Move settings rendering to Menu scripts --- files/data/builtin.omwscripts | 2 ++ files/data/scripts/omw/settings/menu.lua | 19 +++++++++++++++++++ files/data/scripts/omw/settings/player.lua | 15 ++++++++------- 3 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 files/data/scripts/omw/settings/menu.lua diff --git a/files/data/builtin.omwscripts b/files/data/builtin.omwscripts index 30f6941059..e35e86aaaf 100644 --- a/files/data/builtin.omwscripts +++ b/files/data/builtin.omwscripts @@ -1,9 +1,11 @@ # UI framework PLAYER: scripts/omw/mwui/init.lua +MENU: scripts/omw/mwui/init.lua # Settings framework GLOBAL: scripts/omw/settings/global.lua PLAYER: scripts/omw/settings/player.lua +MENU: scripts/omw/settings/menu.lua # Mechanics GLOBAL: scripts/omw/activationhandlers.lua diff --git a/files/data/scripts/omw/settings/menu.lua b/files/data/scripts/omw/settings/menu.lua new file mode 100644 index 0000000000..6fd26d40d2 --- /dev/null +++ b/files/data/scripts/omw/settings/menu.lua @@ -0,0 +1,19 @@ +local common = require('scripts.omw.settings.common') +local render = require('scripts.omw.settings.render') + +require('scripts.omw.settings.renderers')(render.registerRenderer) + +return { + interfaceName = 'Settings', + interface = { + version = 0, + registerPage = render.registerPage, + registerRenderer = render.registerRenderer, + registerGroup = common.registerGroup, + updateRendererArgument = common.updateRendererArgument, + }, + engineHandlers = { + onLoad = common.onLoad, + onSave = common.onSave, + }, +} diff --git a/files/data/scripts/omw/settings/player.lua b/files/data/scripts/omw/settings/player.lua index ae483935d5..3a71f11456 100644 --- a/files/data/scripts/omw/settings/player.lua +++ b/files/data/scripts/omw/settings/player.lua @@ -1,8 +1,6 @@ local common = require('scripts.omw.settings.common') local render = require('scripts.omw.settings.render') -require('scripts.omw.settings.renderers')(render.registerRenderer) - --- -- @type PageOptions -- @field #string key A unique key @@ -71,11 +69,11 @@ return { -- local globalSettings = storage.globalSection('SettingsGlobalMyMod') interface = { --- - -- @field [parent=#Settings] #string version - version = 0, + -- @field [parent=#Settings] #number version + version = 1, --- -- @function [parent=#Settings] registerPage Register a page to be displayed in the settings menu, - -- only available in player scripts + -- available in player and menu scripts -- @param #PageOptions options -- @usage -- I.Settings.registerPage({ @@ -87,7 +85,7 @@ return { registerPage = render.registerPage, --- -- @function [parent=#Settings] registerRenderer Register a renderer, - -- only avaialable in player scripts + -- only avaialable in menu scripts (DEPRECATED in player scripts) -- @param #string key -- @param #function renderer A renderer function, receives setting's value, -- a function to change it and an argument from the setting options @@ -107,7 +105,10 @@ return { -- }, -- } -- end) - registerRenderer = render.registerRenderer, + registerRenderer = function() + print( + 'Register setting renderers in player scripts has been deprecated and moved to menu Settings interface') + end, --- -- @function [parent=#Settings] registerGroup Register a group to be attached to a page, -- available both in player and global scripts From 962ecc43298749cf08110b1613fd3d14b29ab6a3 Mon Sep 17 00:00:00 2001 From: uramer Date: Tue, 9 Jan 2024 21:25:26 +0100 Subject: [PATCH 11/39] Allow menu scripts to read global sections while a game is loaded --- apps/openmw/mwbase/luamanager.hpp | 1 + apps/openmw/mwlua/luamanagerimp.cpp | 55 +++++++++++++++++---- apps/openmw/mwlua/luamanagerimp.hpp | 1 + apps/openmw/mwstate/statemanagerimp.cpp | 4 +- apps/openmw_test_suite/lua/test_storage.cpp | 4 ++ components/lua/storage.cpp | 11 ++++- components/lua/storage.hpp | 13 ++++- files/lua_api/openmw/storage.lua | 5 +- 8 files changed, 80 insertions(+), 14 deletions(-) diff --git a/apps/openmw/mwbase/luamanager.hpp b/apps/openmw/mwbase/luamanager.hpp index f3cea83224..10d6476653 100644 --- a/apps/openmw/mwbase/luamanager.hpp +++ b/apps/openmw/mwbase/luamanager.hpp @@ -54,6 +54,7 @@ namespace MWBase virtual void newGameStarted() = 0; virtual void gameLoaded() = 0; + virtual void gameEnded() = 0; virtual void objectAddedToScene(const MWWorld::Ptr& ptr) = 0; virtual void objectRemovedFromScene(const MWWorld::Ptr& ptr) = 0; virtual void objectTeleported(const MWWorld::Ptr& ptr) = 0; diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 97e1dfae39..2da2afac5d 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -105,11 +105,14 @@ namespace MWLua LuaUtil::LuaStorage::initLuaBindings(mLua.sol()); mGlobalScripts.addPackage( "openmw.storage", LuaUtil::LuaStorage::initGlobalPackage(mLua.sol(), &mGlobalStorage)); - mMenuScripts.addPackage("openmw.storage", LuaUtil::LuaStorage::initMenuPackage(mLua.sol(), &mPlayerStorage)); + mMenuScripts.addPackage( + "openmw.storage", LuaUtil::LuaStorage::initMenuPackage(mLua.sol(), &mGlobalStorage, &mPlayerStorage)); mLocalPackages["openmw.storage"] = LuaUtil::LuaStorage::initLocalPackage(mLua.sol(), &mGlobalStorage); mPlayerPackages["openmw.storage"] = LuaUtil::LuaStorage::initPlayerPackage(mLua.sol(), &mGlobalStorage, &mPlayerStorage); + mPlayerStorage.setActive(true); + initConfiguration(); mInitialized = true; mMenuScripts.addAutoStartedScripts(); @@ -301,6 +304,7 @@ namespace MWLua mPlayer = MWWorld::Ptr(); } mGlobalStorage.clearTemporaryAndRemoveCallbacks(); + mGlobalStorage.setActive(false); mPlayerStorage.clearTemporaryAndRemoveCallbacks(); mInputActions.clear(); mInputTriggers.clear(); @@ -329,6 +333,7 @@ namespace MWLua void LuaManager::newGameStarted() { + mGlobalStorage.setActive(true); mInputEvents.clear(); mGlobalScripts.addAutoStartedScripts(); mGlobalScriptsStarted = true; @@ -338,12 +343,20 @@ namespace MWLua void LuaManager::gameLoaded() { + mGlobalStorage.setActive(true); if (!mGlobalScriptsStarted) mGlobalScripts.addAutoStartedScripts(); mGlobalScriptsStarted = true; mMenuScripts.stateChanged(); } + void LuaManager::gameEnded() + { + // TODO: disable scripts and global storage when the game is actually unloaded + // mGlobalStorage.setActive(false); + mMenuScripts.stateChanged(); + } + void LuaManager::uiModeChanged(const MWWorld::Ptr& arg) { if (mPlayer.isEmpty()) @@ -492,6 +505,10 @@ namespace MWLua throw std::runtime_error("Last generated RefNum is invalid"); MWBase::Environment::get().getWorldModel()->setLastGeneratedRefNum(lastGenerated); + // TODO: don't execute scripts right away, it will be necessary in multiplayer where global storage requires + // initialization. For now just set global storage as active slightly before it would be set by gameLoaded() + mGlobalStorage.setActive(true); + ESM::LuaScripts globalScripts; globalScripts.load(reader); mLuaEvents.load(mLua.sol(), reader, mContentFileMapping, mGlobalLoader.get()); @@ -540,29 +557,49 @@ namespace MWLua mInputTriggers.clear(); initConfiguration(); - { // Reload global scripts + ESM::LuaScripts globalData; + + if (mGlobalScriptsStarted) + { mGlobalScripts.setSavedDataDeserializer(mGlobalSerializer.get()); - ESM::LuaScripts data; - mGlobalScripts.save(data); + mGlobalScripts.save(globalData); mGlobalStorage.clearTemporaryAndRemoveCallbacks(); - mGlobalScripts.load(data); } + std::unordered_map localData; + for (const auto& [id, ptr] : MWBase::Environment::get().getWorldModel()->getPtrRegistryView()) - { // Reload local scripts + { LocalScripts* scripts = ptr.getRefData().getLuaScripts(); if (scripts == nullptr) continue; scripts->setSavedDataDeserializer(mLocalSerializer.get()); ESM::LuaScripts data; scripts->save(data); - scripts->load(data); + localData[id] = data; } + + mMenuScripts.removeAllScripts(); + + mPlayerStorage.clearTemporaryAndRemoveCallbacks(); + + mMenuScripts.addAutoStartedScripts(); + + for (const auto& [id, ptr] : MWBase::Environment::get().getWorldModel()->getPtrRegistryView()) + { + LocalScripts* scripts = ptr.getRefData().getLuaScripts(); + if (scripts == nullptr) + continue; + scripts->load(localData[id]); + } + for (LocalScripts* scripts : mActiveLocalScripts) scripts->setActive(true); - mMenuScripts.removeAllScripts(); - mMenuScripts.addAutoStartedScripts(); + if (mGlobalScriptsStarted) + { + mGlobalScripts.load(globalData); + } } void LuaManager::handleConsoleCommand( diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index ccd5386c4a..965aa67fab 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -68,6 +68,7 @@ namespace MWLua // LuaManager queues these events and propagates to scripts on the next `update` call. void newGameStarted() override; void gameLoaded() override; + void gameEnded() override; void objectAddedToScene(const MWWorld::Ptr& ptr) override; void objectRemovedFromScene(const MWWorld::Ptr& ptr) override; void inputEvent(const InputEvent& event) override; diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index f29d34c72a..a8df64e3e3 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -157,10 +157,10 @@ void MWState::StateManager::newGame(bool bypass) { Log(Debug::Info) << "Starting a new game"; MWBase::Environment::get().getScriptManager()->getGlobalScripts().addStartup(); - MWBase::Environment::get().getLuaManager()->newGameStarted(); MWBase::Environment::get().getWorld()->startNewGame(bypass); mState = State_Running; + MWBase::Environment::get().getLuaManager()->newGameStarted(); MWBase::Environment::get().getWindowManager()->fadeScreenOut(0); MWBase::Environment::get().getWindowManager()->fadeScreenIn(1); @@ -184,11 +184,13 @@ void MWState::StateManager::newGame(bool bypass) void MWState::StateManager::endGame() { mState = State_Ended; + MWBase::Environment::get().getLuaManager()->gameEnded(); } void MWState::StateManager::resumeGame() { mState = State_Running; + MWBase::Environment::get().getLuaManager()->gameLoaded(); } void MWState::StateManager::saveGame(std::string_view description, const Slot* slot) diff --git a/apps/openmw_test_suite/lua/test_storage.cpp b/apps/openmw_test_suite/lua/test_storage.cpp index 6bba813529..a36a527e0c 100644 --- a/apps/openmw_test_suite/lua/test_storage.cpp +++ b/apps/openmw_test_suite/lua/test_storage.cpp @@ -22,6 +22,7 @@ namespace sol::state_view& mLua = luaState.sol(); LuaUtil::LuaStorage::initLuaBindings(mLua); LuaUtil::LuaStorage storage(mLua); + storage.setActive(true); std::vector callbackCalls; sol::table callbackHiddenData(mLua, sol::create); @@ -65,6 +66,7 @@ namespace sol::state mLua; LuaUtil::LuaStorage::initLuaBindings(mLua); LuaUtil::LuaStorage storage(mLua); + storage.setActive(true); mLua["mutable"] = storage.getMutableSection("test"); mLua["ro"] = storage.getReadOnlySection("test"); @@ -82,6 +84,7 @@ namespace sol::state mLua; LuaUtil::LuaStorage::initLuaBindings(mLua); LuaUtil::LuaStorage storage(mLua); + storage.setActive(true); mLua["permanent"] = storage.getMutableSection("permanent"); mLua["temporary"] = storage.getMutableSection("temporary"); @@ -104,6 +107,7 @@ namespace mLua.safe_script("permanent:set('z', 4)"); LuaUtil::LuaStorage storage2(mLua); + storage2.setActive(true); storage2.load(tmpFile); mLua["permanent"] = storage2.getMutableSection("permanent"); mLua["temporary"] = storage2.getMutableSection("temporary"); diff --git a/components/lua/storage.cpp b/components/lua/storage.cpp index b2f972e853..dd53fdffcb 100644 --- a/components/lua/storage.cpp +++ b/components/lua/storage.cpp @@ -31,6 +31,7 @@ namespace LuaUtil const LuaStorage::Value& LuaStorage::Section::get(std::string_view key) const { + checkIfActive(); auto it = mValues.find(key); if (it != mValues.end()) return it->second; @@ -72,6 +73,7 @@ namespace LuaUtil void LuaStorage::Section::set(std::string_view key, const sol::object& value) { + checkIfActive(); throwIfCallbackRecursionIsTooDeep(); if (value != sol::nil) mValues[std::string(key)] = Value(value); @@ -88,6 +90,7 @@ namespace LuaUtil void LuaStorage::Section::setAll(const sol::optional& values) { + checkIfActive(); throwIfCallbackRecursionIsTooDeep(); mValues.clear(); if (values) @@ -102,6 +105,7 @@ namespace LuaUtil sol::table LuaStorage::Section::asTable() { + checkIfActive(); sol::table res(mStorage->mLua, sol::create); for (const auto& [k, v] : mValues) res[k] = v.getCopy(mStorage->mLua); @@ -175,12 +179,14 @@ namespace LuaUtil return LuaUtil::makeReadOnly(res); } - sol::table LuaStorage::initMenuPackage(lua_State* lua, LuaStorage* playerStorage) + sol::table LuaStorage::initMenuPackage(lua_State* lua, LuaStorage* globalStorage, LuaStorage* playerStorage) { sol::table res(lua, sol::create); res["playerSection"] = [playerStorage](std::string_view section) { return playerStorage->getMutableSection(section, /*forMenuScripts=*/true); }; + res["globalSection"] + = [globalStorage](std::string_view section) { return globalStorage->getReadOnlySection(section); }; res["allPlayerSections"] = [playerStorage]() { return playerStorage->getAllSections(); }; return LuaUtil::makeReadOnly(res); } @@ -244,6 +250,7 @@ namespace LuaUtil const std::shared_ptr& LuaStorage::getSection(std::string_view sectionName) { + checkIfActive(); auto it = mData.find(sectionName); if (it != mData.end()) return it->second; @@ -255,12 +262,14 @@ namespace LuaUtil sol::object LuaStorage::getSection(std::string_view sectionName, bool readOnly, bool forMenuScripts) { + checkIfActive(); const std::shared_ptr
& section = getSection(sectionName); return sol::make_object(mLua, SectionView{ section, readOnly, forMenuScripts }); } sol::table LuaStorage::getAllSections(bool readOnly) { + checkIfActive(); sol::table res(mLua, sol::create); for (const auto& [sectionName, _] : mData) res[sectionName] = getSection(sectionName, readOnly); diff --git a/components/lua/storage.hpp b/components/lua/storage.hpp index 3376e7e50c..75e0e14a16 100644 --- a/components/lua/storage.hpp +++ b/components/lua/storage.hpp @@ -3,6 +3,7 @@ #include #include +#include #include "asyncpackage.hpp" #include "serialization.hpp" @@ -17,10 +18,11 @@ namespace LuaUtil static sol::table initGlobalPackage(lua_State* lua, LuaStorage* globalStorage); static sol::table initLocalPackage(lua_State* lua, LuaStorage* globalStorage); static sol::table initPlayerPackage(lua_State* lua, LuaStorage* globalStorage, LuaStorage* playerStorage); - static sol::table initMenuPackage(lua_State* lua, LuaStorage* playerStorage); + static sol::table initMenuPackage(lua_State* lua, LuaStorage* globalStorage, LuaStorage* playerStorage); explicit LuaStorage(lua_State* lua) : mLua(lua) + , mActive(false) { } @@ -55,6 +57,7 @@ namespace LuaUtil virtual void sectionReplaced(std::string_view section, const sol::optional& values) const = 0; }; void setListener(const Listener* listener) { mListener = listener; } + void setActive(bool active) { mActive = active; } private: class Value @@ -95,6 +98,8 @@ namespace LuaUtil // remove them in clear() bool mPermanent = true; static Value sEmpty; + + void checkIfActive() const { mStorage->checkIfActive(); } }; struct SectionView { @@ -109,6 +114,12 @@ namespace LuaUtil std::map> mData; const Listener* mListener = nullptr; std::set mRunningCallbacks; + bool mActive; + void checkIfActive() const + { + if (!mActive) + throw std::logic_error("Trying to access inactive storage"); + } }; } diff --git a/files/lua_api/openmw/storage.lua b/files/lua_api/openmw/storage.lua index 303c674319..2335719be8 100644 --- a/files/lua_api/openmw/storage.lua +++ b/files/lua_api/openmw/storage.lua @@ -17,13 +17,14 @@ --- -- Get a section of the global storage; can be used by any script, but only global scripts can change values. +-- Menu scripts can only access it when a game is running. -- Creates the section if it doesn't exist. -- @function [parent=#storage] globalSection -- @param #string sectionName -- @return #StorageSection --- --- Get a section of the player storage; can be used by player scripts only. +-- Get a section of the player storage; can only be used by player and menu scripts. -- Creates the section if it doesn't exist. -- @function [parent=#storage] playerSection -- @param #string sectionName @@ -36,7 +37,7 @@ -- @return #table --- --- Get all global sections as a table; can be used by player scripts only. +-- Get all player sections as a table; can only be used by player and menu scripts. -- Note that adding/removing items to the returned table doesn't create or remove sections. -- @function [parent=#storage] allPlayerSections -- @return #table From d1d430b431631d2c3d8ab953610e973a19084ec6 Mon Sep 17 00:00:00 2001 From: uramer Date: Wed, 10 Jan 2024 00:44:52 +0100 Subject: [PATCH 12/39] Initial Menu context Settings implementation --- files/data/CMakeLists.txt | 2 +- files/data/builtin.omwscripts | 7 +- files/data/scripts/omw/settings/common.lua | 4 +- files/data/scripts/omw/settings/global.lua | 3 +- files/data/scripts/omw/settings/menu.lua | 491 ++++++++++++++++++++- files/data/scripts/omw/settings/player.lua | 17 +- files/data/scripts/omw/settings/render.lua | 423 ------------------ 7 files changed, 506 insertions(+), 441 deletions(-) delete mode 100644 files/data/scripts/omw/settings/render.lua diff --git a/files/data/CMakeLists.txt b/files/data/CMakeLists.txt index 4b36254183..e038e9f573 100644 --- a/files/data/CMakeLists.txt +++ b/files/data/CMakeLists.txt @@ -78,10 +78,10 @@ set(BUILTIN_DATA_FILES scripts/omw/console/menu.lua scripts/omw/mechanics/playercontroller.lua scripts/omw/playercontrols.lua + scripts/omw/settings/menu.lua scripts/omw/settings/player.lua scripts/omw/settings/global.lua scripts/omw/settings/common.lua - scripts/omw/settings/render.lua scripts/omw/settings/renderers.lua scripts/omw/mwui/constants.lua scripts/omw/mwui/borders.lua diff --git a/files/data/builtin.omwscripts b/files/data/builtin.omwscripts index e35e86aaaf..3e902f6639 100644 --- a/files/data/builtin.omwscripts +++ b/files/data/builtin.omwscripts @@ -1,11 +1,10 @@ # UI framework -PLAYER: scripts/omw/mwui/init.lua -MENU: scripts/omw/mwui/init.lua +MENU,PLAYER: scripts/omw/mwui/init.lua # Settings framework -GLOBAL: scripts/omw/settings/global.lua -PLAYER: scripts/omw/settings/player.lua MENU: scripts/omw/settings/menu.lua +PLAYER: scripts/omw/settings/player.lua +GLOBAL: scripts/omw/settings/global.lua # Mechanics GLOBAL: scripts/omw/activationhandlers.lua diff --git a/files/data/scripts/omw/settings/common.lua b/files/data/scripts/omw/settings/common.lua index cb83b4223b..9155c64ba7 100644 --- a/files/data/scripts/omw/settings/common.lua +++ b/files/data/scripts/omw/settings/common.lua @@ -6,7 +6,6 @@ local argumentSectionPostfix = 'Arguments' local contextSection = storage.playerSection or storage.globalSection local groupSection = contextSection(groupSectionKey) -groupSection:reset() groupSection:removeOnExit() local function validateSettingOptions(options) @@ -110,9 +109,11 @@ end return { getSection = function(global, key) + if global then error('Getting global section') end return (global and storage.globalSection or storage.playerSection)(key) end, getArgumentSection = function(global, key) + if global then error('Getting global section') end return (global and storage.globalSection or storage.playerSection)(key .. argumentSectionPostfix) end, updateRendererArgument = function(groupKey, settingKey, argument) @@ -120,6 +121,7 @@ return { argumentSection:set(settingKey, argument) end, setGlobalEvent = 'OMWSettingsGlobalSet', + registerPageEvent = 'OmWSettingsRegisterPage', groupSectionKey = groupSectionKey, onLoad = function(saved) if not saved then return end diff --git a/files/data/scripts/omw/settings/global.lua b/files/data/scripts/omw/settings/global.lua index d84794f61d..423c38680b 100644 --- a/files/data/scripts/omw/settings/global.lua +++ b/files/data/scripts/omw/settings/global.lua @@ -5,6 +5,7 @@ local common = require('scripts.omw.settings.common') return { interfaceName = 'Settings', interface = { + version = 1, registerGroup = common.registerGroup, updateRendererArgument = common.updateRendererArgument, }, @@ -17,4 +18,4 @@ return { storage.globalSection(e.groupKey):set(e.settingKey, e.value) end, }, -} \ No newline at end of file +} diff --git a/files/data/scripts/omw/settings/menu.lua b/files/data/scripts/omw/settings/menu.lua index 6fd26d40d2..ff554df768 100644 --- a/files/data/scripts/omw/settings/menu.lua +++ b/files/data/scripts/omw/settings/menu.lua @@ -1,19 +1,498 @@ -local common = require('scripts.omw.settings.common') -local render = require('scripts.omw.settings.render') +local menu = require('openmw.menu') +local ui = require('openmw.ui') +local util = require('openmw.util') +local async = require('openmw.async') +local core = require('openmw.core') +local storage = require('openmw.storage') +local I = require('openmw.interfaces') -require('scripts.omw.settings.renderers')(render.registerRenderer) +local common = require('scripts.omw.settings.common') + +local renderers = {} +local function registerRenderer(name, renderFunction) + renderers[name] = renderFunction +end +require('scripts.omw.settings.renderers')(registerRenderer) + +local interfaceL10n = core.l10n('Interface') + +local pages = {} +local groups = {} +local pageOptions = {} + +local interval = { template = I.MWUI.templates.interval } +local growingIntreval = { + template = I.MWUI.templates.interval, + external = { + grow = 1, + }, +} +local spacer = { + props = { + size = util.vector2(0, 10), + }, +} +local bigSpacer = { + props = { + size = util.vector2(0, 50), + }, +} +local stretchingLine = { + template = I.MWUI.templates.horizontalLine, + external = { + stretch = 1, + }, +} +local spacedLines = function(count) + local content = {} + table.insert(content, spacer) + table.insert(content, stretchingLine) + for i = 2, count do + table.insert(content, interval) + table.insert(content, stretchingLine) + end + table.insert(content, spacer) + return { + type = ui.TYPE.Flex, + external = { + stretch = 1, + }, + content = ui.content(content), + } +end + +local function interlaceSeparator(layouts, separator) + local result = {} + result[1] = layouts[1] + for i = 2, #layouts do + table.insert(result, separator) + table.insert(result, layouts[i]) + end + return result +end + +local function setSettingValue(global, groupKey, settingKey, value) + if global then + core.sendGlobalEvent(common.setGlobalEvent, { + groupKey = groupKey, + settingKey = settingKey, + value = value, + }) + else + storage.playerSection(groupKey):set(settingKey, value) + end +end + +local function renderSetting(group, setting, value, global) + local renderFunction = renderers[setting.renderer] + if not renderFunction then + error(('Setting %s of %s has unknown renderer %s'):format(setting.key, group.key, setting.renderer)) + end + local set = function(value) + setSettingValue(global, group.key, setting.key, value) + end + local l10n = core.l10n(group.l10n) + local titleLayout = { + type = ui.TYPE.Flex, + content = ui.content { + { + template = I.MWUI.templates.textHeader, + props = { + text = l10n(setting.name), + }, + }, + }, + } + if setting.description then + titleLayout.content:add(interval) + titleLayout.content:add { + template = I.MWUI.templates.textParagraph, + props = { + text = l10n(setting.description), + size = util.vector2(300, 0), + }, + } + end + local argument = common.getArgumentSection(global, group.key):get(setting.key) + return { + name = setting.key, + type = ui.TYPE.Flex, + props = { + horizontal = true, + arrange = ui.ALIGNMENT.Center, + }, + external = { + stretch = 1, + }, + content = ui.content { + titleLayout, + growingIntreval, + renderFunction(value, set, argument), + }, + } +end + +local groupLayoutName = function(key, global) + return ('%s%s'):format(global and 'global_' or 'player_', key) +end + +local function renderGroup(group, global) + local l10n = core.l10n(group.l10n) + + local valueSection = common.getSection(global, group.key) + local settingLayouts = {} + local sortedSettings = {} + for _, setting in pairs(group.settings) do + sortedSettings[setting.order] = setting + end + for _, setting in ipairs(sortedSettings) do + table.insert(settingLayouts, renderSetting(group, setting, valueSection:get(setting.key), global)) + end + local settingsContent = ui.content(interlaceSeparator(settingLayouts, spacedLines(1))) + + local resetButtonLayout = { + template = I.MWUI.templates.box, + events = { + mouseClick = async:callback(function() + for _, setting in pairs(group.settings) do + setSettingValue(global, group.key, setting.key, setting.default) + end + end), + }, + content = ui.content { + { + template = I.MWUI.templates.padding, + content = ui.content { + { + template = I.MWUI.templates.textNormal, + props = { + text = interfaceL10n('Reset') + }, + }, + }, + }, + }, + } + + local titleLayout = { + type = ui.TYPE.Flex, + external = { + stretch = 1, + }, + content = ui.content { + { + template = I.MWUI.templates.textHeader, + props = { + text = l10n(group.name), + textSize = 20, + }, + } + }, + } + if group.description then + titleLayout.content:add(interval) + titleLayout.content:add { + template = I.MWUI.templates.textParagraph, + props = { + text = l10n(group.description), + size = util.vector2(300, 0), + }, + } + end + + return { + name = groupLayoutName(group.key, global), + type = ui.TYPE.Flex, + external = { + stretch = 1, + }, + content = ui.content { + { + type = ui.TYPE.Flex, + props = { + horizontal = true, + arrange = ui.ALIGNMENT.Center, + }, + external = { + stretch = 1, + }, + content = ui.content { + titleLayout, + growingIntreval, + resetButtonLayout, + }, + }, + spacedLines(2), + { + name = 'settings', + type = ui.TYPE.Flex, + content = settingsContent, + external = { + stretch = 1, + }, + }, + }, + } +end + +local function pageGroupComparator(a, b) + return a.order < b.order or ( + a.order == b.order and a.key < b.key + ) +end + +local function generateSearchHints(page) + local hints = {} + local l10n = core.l10n(page.l10n) + table.insert(hints, l10n(page.name)) + if page.description then + table.insert(hints, l10n(page.description)) + end + local pageGroups = groups[page.key] + for _, pageGroup in pairs(pageGroups) do + local group = common.getSection(pageGroup.global, common.groupSectionKey):get(pageGroup.key) + local l10n = core.l10n(group.l10n) + table.insert(hints, l10n(group.name)) + if group.description then + table.insert(hints, l10n(group.description)) + end + for _, setting in pairs(group.settings) do + table.insert(hints, l10n(setting.name)) + if setting.description then + table.insert(hints, l10n(setting.description)) + end + end + end + return table.concat(hints, ' ') +end + +local function renderPage(page) + local l10n = core.l10n(page.l10n) + local sortedGroups = {} + for _, group in pairs(groups[page.key]) do + table.insert(sortedGroups, group) + end + table.sort(sortedGroups, pageGroupComparator) + local groupLayouts = {} + for _, pageGroup in ipairs(sortedGroups) do + local group = common.getSection(pageGroup.global, common.groupSectionKey):get(pageGroup.key) + table.insert(groupLayouts, renderGroup(group, pageGroup.global)) + end + local groupsLayout = { + name = 'groups', + type = ui.TYPE.Flex, + external = { + stretch = 1, + }, + content = ui.content(interlaceSeparator(groupLayouts, bigSpacer)), + } + local titleLayout = { + type = ui.TYPE.Flex, + external = { + stretch = 1, + }, + content = ui.content { + { + template = I.MWUI.templates.textHeader, + props = { + text = l10n(page.name), + textSize = 22, + }, + }, + spacedLines(3), + }, + } + if page.description then + titleLayout.content:add { + template = I.MWUI.templates.textParagraph, + props = { + text = l10n(page.description), + size = util.vector2(300, 0), + }, + } + end + local layout = { + name = page.key, + type = ui.TYPE.Flex, + props = { + position = util.vector2(10, 10), + }, + content = ui.content { + titleLayout, + bigSpacer, + groupsLayout, + bigSpacer, + }, + } + return { + name = l10n(page.name), + element = ui.create(layout), + searchHints = generateSearchHints(page), + } +end + +local function onSettingChanged(global) + return async:callback(function(groupKey, settingKey) + local group = common.getSection(global, common.groupSectionKey):get(groupKey) + if not group or not pageOptions[group.page] then return end + + local value = common.getSection(global, group.key):get(settingKey) + + local element = pageOptions[group.page].element + local groupsLayout = element.layout.content.groups + local groupLayout = groupsLayout.content[groupLayoutName(group.key, global)] + local settingsContent = groupLayout.content.settings.content + settingsContent[settingKey] = renderSetting(group, group.settings[settingKey], value, global) + element:update() + end) +end + +local function onGroupRegistered(global, key) + local group = common.getSection(global, common.groupSectionKey):get(key) + groups[group.page] = groups[group.page] or {} + local pageGroup = { + key = group.key, + global = global, + order = group.order, + } + + if not groups[group.page][pageGroup.key] then + common.getSection(global, group.key):subscribe(onSettingChanged(global)) + common.getArgumentSection(global, group.key):subscribe(async:callback(function(_, settingKey) + local group = common.getSection(global, common.groupSectionKey):get(group.key) + if not group or not pageOptions[group.page] then return end + + local value = common.getSection(global, group.key):get(settingKey) + + local element = pageOptions[group.page].element + local groupsLayout = element.layout.content.groups + local groupLayout = groupsLayout.content[groupLayoutName(group.key, global)] + local settingsContent = groupLayout.content.settings.content + settingsContent[settingKey] = renderSetting(group, group.settings[settingKey], value, global) + element:update() + end)) + end + + groups[group.page][pageGroup.key] = pageGroup + + if not pages[group.page] then return end + if pageOptions[group.page] then + pageOptions[group.page].element:destroy() + else + pageOptions[group.page] = {} + end + local renderedOptions = renderPage(pages[group.page]) + for k, v in pairs(renderedOptions) do + pageOptions[group.page][k] = v + end +end + + +local function updatePlayerGroups() + local playerGroups = storage.playerSection(common.groupSectionKey) + for groupKey in pairs(playerGroups:asTable()) do + onGroupRegistered(false, groupKey) + end + playerGroups:subscribe(async:callback(function(_, key) + if key then + onGroupRegistered(false, key) + else + for groupKey in pairs(playerGroups:asTable()) do + onGroupRegistered(false, groupKey) + end + end + end)) +end + +updatePlayerGroups() + +local function updateGlobalGroups() + local globalGroups = storage.globalSection(common.groupSectionKey) + for groupKey in pairs(globalGroups:asTable()) do + onGroupRegistered(true, groupKey) + end + globalGroups:subscribe(async:callback(function(_, key) + if key then + onGroupRegistered(true, key) + else + for groupKey in pairs(globalGroups:asTable()) do + onGroupRegistered(true, groupKey) + end + end + end)) +end + +local function resetGroups() + for pageKey, page in pairs(groups) do + for groupKey in pairs(page) do + page[groupKey] = nil + end + local renderedOptions = renderPage(pages[pageKey]) + for k, v in pairs(renderedOptions) do + pageOptions[pageKey][k] = v + end + end +end + +local function registerPage(options) + if type(options) ~= 'table' then + error('Page options must be a table') + end + if type(options.key) ~= 'string' then + error('Page must have a key') + end + if type(options.l10n) ~= 'string' then + error('Page must have a localization context') + end + if type(options.name) ~= 'string' then + error('Page must have a name') + end + if options.description ~= nil and type(options.description) ~= 'string' then + error('Page description key must be a string') + end + local page = { + key = options.key, + l10n = options.l10n, + name = options.name, + description = options.description, + } + pages[page.key] = page + groups[page.key] = groups[page.key] or {} + if pageOptions[page.key] then + pageOptions[page.key].element:destroy() + end + pageOptions[page.key] = pageOptions[page.key] or {} + local renderedOptions = renderPage(page) + for k, v in pairs(renderedOptions) do + pageOptions[page.key][k] = v + end + ui.registerSettingsPage(pageOptions[page.key]) +end return { interfaceName = 'Settings', interface = { - version = 0, - registerPage = render.registerPage, - registerRenderer = render.registerRenderer, + version = 1, + registerPage = registerPage, + registerRenderer = registerRenderer, registerGroup = common.registerGroup, updateRendererArgument = common.updateRendererArgument, }, engineHandlers = { onLoad = common.onLoad, onSave = common.onSave, + onStateChanged = function() + if menu.getState() == menu.STATE.Running then + updateGlobalGroups() + else + resetGroups() + end + updatePlayerGroups() + end, }, + eventHandlers = { + [common.registerPageEvent] = function(options) + registerPage(options) + end, + } } diff --git a/files/data/scripts/omw/settings/player.lua b/files/data/scripts/omw/settings/player.lua index 3a71f11456..3898e733e1 100644 --- a/files/data/scripts/omw/settings/player.lua +++ b/files/data/scripts/omw/settings/player.lua @@ -1,5 +1,12 @@ +local storage = require('openmw.storage') +local types = require('openmw.types') +local self = require('openmw.self') + local common = require('scripts.omw.settings.common') -local render = require('scripts.omw.settings.render') + +local function registerPage(options) + types.Player.sendMenuEvent(self, common.registerPageEvent, options) +end --- -- @type PageOptions @@ -82,7 +89,7 @@ return { -- name = 'MyModName', -- description = 'MyModDescription', -- })--- - registerPage = render.registerPage, + registerPage = registerPage, --- -- @function [parent=#Settings] registerRenderer Register a renderer, -- only avaialable in menu scripts (DEPRECATED in player scripts) @@ -105,9 +112,9 @@ return { -- }, -- } -- end) - registerRenderer = function() - print( - 'Register setting renderers in player scripts has been deprecated and moved to menu Settings interface') + registerRenderer = function(name) + print(([[Can't register setting renderer "%s". registerRenderer and moved to Menu context Settings interface]]) + :format(name)) end, --- -- @function [parent=#Settings] registerGroup Register a group to be attached to a page, diff --git a/files/data/scripts/omw/settings/render.lua b/files/data/scripts/omw/settings/render.lua deleted file mode 100644 index d4de143b02..0000000000 --- a/files/data/scripts/omw/settings/render.lua +++ /dev/null @@ -1,423 +0,0 @@ -local ui = require('openmw.ui') -local util = require('openmw.util') -local async = require('openmw.async') -local core = require('openmw.core') -local storage = require('openmw.storage') -local I = require('openmw.interfaces') - -local common = require('scripts.omw.settings.common') - -local renderers = {} -local function registerRenderer(name, renderFunction) - renderers[name] = renderFunction -end - -local interfaceL10n = core.l10n('Interface') - -local pages = {} -local groups = {} -local pageOptions = {} - -local interval = { template = I.MWUI.templates.interval } -local growingIntreval = { - template = I.MWUI.templates.interval, - external = { - grow = 1, - }, -} -local spacer = { - props = { - size = util.vector2(0, 10), - }, -} -local bigSpacer = { - props = { - size = util.vector2(0, 50), - }, -} -local stretchingLine = { - template = I.MWUI.templates.horizontalLine, - external = { - stretch = 1, - }, -} -local spacedLines = function(count) - local content = {} - table.insert(content, spacer) - table.insert(content, stretchingLine) - for i = 2, count do - table.insert(content, interval) - table.insert(content, stretchingLine) - end - table.insert(content, spacer) - return { - type = ui.TYPE.Flex, - external = { - stretch = 1, - }, - content = ui.content(content), - } -end - -local function interlaceSeparator(layouts, separator) - local result = {} - result[1] = layouts[1] - for i = 2, #layouts do - table.insert(result, separator) - table.insert(result, layouts[i]) - end - return result -end - -local function setSettingValue(global, groupKey, settingKey, value) - if global then - core.sendGlobalEvent(common.setGlobalEvent, { - groupKey = groupKey, - settingKey = settingKey, - value = value, - }) - else - storage.playerSection(groupKey):set(settingKey, value) - end -end - -local function renderSetting(group, setting, value, global) - local renderFunction = renderers[setting.renderer] - if not renderFunction then - error(('Setting %s of %s has unknown renderer %s'):format(setting.key, group.key, setting.renderer)) - end - local set = function(value) - setSettingValue(global, group.key, setting.key, value) - end - local l10n = core.l10n(group.l10n) - local titleLayout = { - type = ui.TYPE.Flex, - content = ui.content { - { - template = I.MWUI.templates.textHeader, - props = { - text = l10n(setting.name), - }, - }, - }, - } - if setting.description then - titleLayout.content:add(interval) - titleLayout.content:add { - template = I.MWUI.templates.textParagraph, - props = { - text = l10n(setting.description), - size = util.vector2(300, 0), - }, - } - end - local argument = common.getArgumentSection(global, group.key):get(setting.key) - return { - name = setting.key, - type = ui.TYPE.Flex, - props = { - horizontal = true, - arrange = ui.ALIGNMENT.Center, - }, - external = { - stretch = 1, - }, - content = ui.content { - titleLayout, - growingIntreval, - renderFunction(value, set, argument), - }, - } -end - -local groupLayoutName = function(key, global) - return ('%s%s'):format(global and 'global_' or 'player_', key) -end - -local function renderGroup(group, global) - local l10n = core.l10n(group.l10n) - - local valueSection = common.getSection(global, group.key) - local settingLayouts = {} - local sortedSettings = {} - for _, setting in pairs(group.settings) do - sortedSettings[setting.order] = setting - end - for _, setting in ipairs(sortedSettings) do - table.insert(settingLayouts, renderSetting(group, setting, valueSection:get(setting.key), global)) - end - local settingsContent = ui.content(interlaceSeparator(settingLayouts, spacedLines(1))) - - local resetButtonLayout = { - template = I.MWUI.templates.box, - events = { - mouseClick = async:callback(function() - for _, setting in pairs(group.settings) do - setSettingValue(global, group.key, setting.key, setting.default) - end - end), - }, - content = ui.content { - { - template = I.MWUI.templates.padding, - content = ui.content { - { - template = I.MWUI.templates.textNormal, - props = { - text = interfaceL10n('Reset') - }, - }, - }, - }, - }, - } - - local titleLayout = { - type = ui.TYPE.Flex, - external = { - stretch = 1, - }, - content = ui.content { - { - template = I.MWUI.templates.textHeader, - props = { - text = l10n(group.name), - textSize = 20, - }, - } - }, - } - if group.description then - titleLayout.content:add(interval) - titleLayout.content:add { - template = I.MWUI.templates.textParagraph, - props = { - text = l10n(group.description), - size = util.vector2(300, 0), - }, - } - end - - return { - name = groupLayoutName(group.key, global), - type = ui.TYPE.Flex, - external = { - stretch = 1, - }, - content = ui.content { - { - type = ui.TYPE.Flex, - props = { - horizontal = true, - arrange = ui.ALIGNMENT.Center, - }, - external = { - stretch = 1, - }, - content = ui.content { - titleLayout, - growingIntreval, - resetButtonLayout, - }, - }, - spacedLines(2), - { - name = 'settings', - type = ui.TYPE.Flex, - content = settingsContent, - external = { - stretch = 1, - }, - }, - }, - } -end - -local function pageGroupComparator(a, b) - return a.order < b.order or ( - a.order == b.order and a.key < b.key - ) -end - -local function generateSearchHints(page) - local hints = {} - local l10n = core.l10n(page.l10n) - table.insert(hints, l10n(page.name)) - if page.description then - table.insert(hints, l10n(page.description)) - end - local pageGroups = groups[page.key] - for _, pageGroup in pairs(pageGroups) do - local group = common.getSection(pageGroup.global, common.groupSectionKey):get(pageGroup.key) - local l10n = core.l10n(group.l10n) - table.insert(hints, l10n(group.name)) - if group.description then - table.insert(hints, l10n(group.description)) - end - for _, setting in pairs(group.settings) do - table.insert(hints, l10n(setting.name)) - if setting.description then - table.insert(hints, l10n(setting.description)) - end - end - end - return table.concat(hints, ' ') -end - -local function renderPage(page) - local l10n = core.l10n(page.l10n) - local sortedGroups = {} - for i, v in ipairs(groups[page.key]) do sortedGroups[i] = v end - table.sort(sortedGroups, pageGroupComparator) - local groupLayouts = {} - for _, pageGroup in ipairs(sortedGroups) do - local group = common.getSection(pageGroup.global, common.groupSectionKey):get(pageGroup.key) - table.insert(groupLayouts, renderGroup(group, pageGroup.global)) - end - local groupsLayout = { - name = 'groups', - type = ui.TYPE.Flex, - external = { - stretch = 1, - }, - content = ui.content(interlaceSeparator(groupLayouts, bigSpacer)), - } - local titleLayout = { - type = ui.TYPE.Flex, - external = { - stretch = 1, - }, - content = ui.content { - { - template = I.MWUI.templates.textHeader, - props = { - text = l10n(page.name), - textSize = 22, - }, - }, - spacedLines(3), - }, - } - if page.description then - titleLayout.content:add { - template = I.MWUI.templates.textParagraph, - props = { - text = l10n(page.description), - size = util.vector2(300, 0), - }, - } - end - local layout = { - name = page.key, - type = ui.TYPE.Flex, - props = { - position = util.vector2(10, 10), - }, - content = ui.content { - titleLayout, - bigSpacer, - groupsLayout, - bigSpacer, - }, - } - return { - name = l10n(page.name), - element = ui.create(layout), - searchHints = generateSearchHints(page), - } -end - -local function onSettingChanged(global) - return async:callback(function(groupKey, settingKey) - local group = common.getSection(global, common.groupSectionKey):get(groupKey) - if not group or not pageOptions[group.page] then return end - - local value = common.getSection(global, group.key):get(settingKey) - - local element = pageOptions[group.page].element - local groupsLayout = element.layout.content.groups - local groupLayout = groupsLayout.content[groupLayoutName(group.key, global)] - local settingsContent = groupLayout.content.settings.content - settingsContent[settingKey] = renderSetting(group, group.settings[settingKey], value, global) - element:update() - end) -end -local function onGroupRegistered(global, key) - local group = common.getSection(global, common.groupSectionKey):get(key) - groups[group.page] = groups[group.page] or {} - local pageGroup = { - key = group.key, - global = global, - order = group.order, - } - table.insert(groups[group.page], pageGroup) - common.getSection(global, group.key):subscribe(onSettingChanged(global)) - common.getArgumentSection(global, group.key):subscribe(async:callback(function(_, settingKey) - local groupKey = group.key - local group = common.getSection(global, common.groupSectionKey):get(groupKey) - if not group or not pageOptions[group.page] then return end - - local value = common.getSection(global, group.key):get(settingKey) - - local element = pageOptions[group.page].element - local groupsLayout = element.layout.content.groups - local groupLayout = groupsLayout.content[groupLayoutName(group.key, global)] - local settingsContent = groupLayout.content.settings.content - settingsContent[settingKey] = renderSetting(group, group.settings[settingKey], value, global) - element:update() - end)) - - if not pages[group.page] then return end - local options = renderPage(pages[group.page]) - if pageOptions[group.page] then - pageOptions[group.page].element:destroy() - else - pageOptions[group.page] = {} - end - for k, v in pairs(options) do - pageOptions[group.page][k] = v - end -end -local globalGroups = storage.globalSection(common.groupSectionKey) -for groupKey in pairs(globalGroups:asTable()) do - onGroupRegistered(true, groupKey) -end -globalGroups:subscribe(async:callback(function(_, key) - if key then onGroupRegistered(true, key) end -end)) -storage.playerSection(common.groupSectionKey):subscribe(async:callback(function(_, key) - if key then onGroupRegistered(false, key) end -end)) - -local function registerPage(options) - if type(options) ~= 'table' then - error('Page options must be a table') - end - if type(options.key) ~= 'string' then - error('Page must have a key') - end - if type(options.l10n) ~= 'string' then - error('Page must have a localization context') - end - if type(options.name) ~= 'string' then - error('Page must have a name') - end - if options.description ~= nil and type(options.description) ~= 'string' then - error('Page description key must be a string') - end - local page = { - key = options.key, - l10n = options.l10n, - name = options.name, - description = options.description, - } - pages[page.key] = page - groups[page.key] = groups[page.key] or {} - pageOptions[page.key] = renderPage(page) - ui.registerSettingsPage(pageOptions[page.key]) -end - -return { - registerPage = registerPage, - registerRenderer = registerRenderer, -} \ No newline at end of file From 2107bbc01db56c376b878718b2c011d4b6523c1a Mon Sep 17 00:00:00 2001 From: uramer Date: Wed, 10 Jan 2024 19:27:39 +0100 Subject: [PATCH 13/39] Reuse input engine handlers in menu scripts --- apps/openmw/CMakeLists.txt | 2 +- apps/openmw/mwlua/inputprocessor.hpp | 73 +++++++++++++++++++ apps/openmw/mwlua/luamanagerimp.cpp | 4 +- apps/openmw/mwlua/menuscripts.hpp | 8 ++ apps/openmw/mwlua/playerscripts.hpp | 44 ++--------- components/lua/scriptscontainer.hpp | 4 +- .../lua-scripting/engine_handlers.rst | 35 +++++---- 7 files changed, 114 insertions(+), 56 deletions(-) create mode 100644 apps/openmw/mwlua/inputprocessor.hpp diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 645e1ba8f3..184c461cb3 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -64,7 +64,7 @@ add_openmw_dir (mwlua context menuscripts globalscripts localscripts playerscripts luabindings objectbindings cellbindings mwscriptbindings camerabindings vfsbindings uibindings soundbindings inputbindings nearbybindings postprocessingbindings stats debugbindings corebindings worldbindings worker magicbindings factionbindings - classbindings itemdata + classbindings itemdata inputprocessor types/types types/door types/item types/actor types/container types/lockable types/weapon types/npc types/creature types/player types/activator types/book types/lockpick types/probe types/apparatus types/potion types/ingredient types/misc types/repair types/armor types/light types/static diff --git a/apps/openmw/mwlua/inputprocessor.hpp b/apps/openmw/mwlua/inputprocessor.hpp new file mode 100644 index 0000000000..112d10c750 --- /dev/null +++ b/apps/openmw/mwlua/inputprocessor.hpp @@ -0,0 +1,73 @@ +#ifndef MWLUA_INPUTPROCESSOR_H +#define MWLUA_INPUTPROCESSOR_H + +#include + +#include +#include +#include + +#include "../mwbase/luamanager.hpp" + +namespace MWLua +{ + class InputProcessor + { + public: + InputProcessor(LuaUtil::ScriptsContainer* scriptsContainer) + : mScriptsContainer(scriptsContainer) + { + mScriptsContainer->registerEngineHandlers({ &mKeyPressHandlers, &mKeyReleaseHandlers, + &mControllerButtonPressHandlers, &mControllerButtonReleaseHandlers, &mActionHandlers, &mTouchpadPressed, + &mTouchpadReleased, &mTouchpadMoved }); + } + + void processInputEvent(const MWBase::LuaManager::InputEvent& event) + { + using InputEvent = MWBase::LuaManager::InputEvent; + switch (event.mType) + { + case InputEvent::KeyPressed: + mScriptsContainer->callEngineHandlers(mKeyPressHandlers, std::get(event.mValue)); + break; + case InputEvent::KeyReleased: + mScriptsContainer->callEngineHandlers(mKeyReleaseHandlers, std::get(event.mValue)); + break; + case InputEvent::ControllerPressed: + mScriptsContainer->callEngineHandlers(mControllerButtonPressHandlers, std::get(event.mValue)); + break; + case InputEvent::ControllerReleased: + mScriptsContainer->callEngineHandlers( + mControllerButtonReleaseHandlers, std::get(event.mValue)); + break; + case InputEvent::Action: + mScriptsContainer->callEngineHandlers(mActionHandlers, std::get(event.mValue)); + break; + case InputEvent::TouchPressed: + mScriptsContainer->callEngineHandlers( + mTouchpadPressed, std::get(event.mValue)); + break; + case InputEvent::TouchReleased: + mScriptsContainer->callEngineHandlers( + mTouchpadReleased, std::get(event.mValue)); + break; + case InputEvent::TouchMoved: + mScriptsContainer->callEngineHandlers(mTouchpadMoved, std::get(event.mValue)); + break; + } + } + + private: + LuaUtil::ScriptsContainer* mScriptsContainer; + LuaUtil::ScriptsContainer::EngineHandlerList mKeyPressHandlers{ "onKeyPress" }; + LuaUtil::ScriptsContainer::EngineHandlerList mKeyReleaseHandlers{ "onKeyRelease" }; + LuaUtil::ScriptsContainer::EngineHandlerList mControllerButtonPressHandlers{ "onControllerButtonPress" }; + LuaUtil::ScriptsContainer::EngineHandlerList mControllerButtonReleaseHandlers{ "onControllerButtonRelease" }; + LuaUtil::ScriptsContainer::EngineHandlerList mActionHandlers{ "onInputAction" }; + LuaUtil::ScriptsContainer::EngineHandlerList mTouchpadPressed{ "onTouchPress" }; + LuaUtil::ScriptsContainer::EngineHandlerList mTouchpadReleased{ "onTouchRelease" }; + LuaUtil::ScriptsContainer::EngineHandlerList mTouchpadMoved{ "onTouchMove" }; + }; +} + +#endif // MWLUA_INPUTPROCESSOR_H diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 2da2afac5d..5b5a1b7d0d 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -229,7 +229,9 @@ namespace MWLua PlayerScripts* playerScripts = mPlayer.isEmpty() ? nullptr : dynamic_cast(mPlayer.getRefData().getLuaScripts()); MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); - // TODO: handle main menu input events + + for (const auto& event : mInputEvents) + mMenuScripts.processInputEvent(event); if (playerScripts && !windowManager->containsMode(MWGui::GM_MainMenu)) { for (const auto& event : mInputEvents) diff --git a/apps/openmw/mwlua/menuscripts.hpp b/apps/openmw/mwlua/menuscripts.hpp index 3fd1bce186..3bd55952ad 100644 --- a/apps/openmw/mwlua/menuscripts.hpp +++ b/apps/openmw/mwlua/menuscripts.hpp @@ -10,6 +10,7 @@ #include "../mwbase/luamanager.hpp" #include "context.hpp" +#include "inputprocessor.hpp" namespace MWLua { @@ -21,10 +22,16 @@ namespace MWLua public: MenuScripts(LuaUtil::LuaState* lua) : LuaUtil::ScriptsContainer(lua, "Menu") + , mInputProcessor(this) { registerEngineHandlers({ &mStateChanged, &mConsoleCommandHandlers, &mUiModeChanged }); } + void processInputEvent(const MWBase::LuaManager::InputEvent& event) + { + mInputProcessor.processInputEvent(event); + } + void stateChanged() { callEngineHandlers(mStateChanged); } bool consoleCommand(const std::string& consoleMode, const std::string& command) @@ -36,6 +43,7 @@ namespace MWLua void uiModeChanged() { callEngineHandlers(mUiModeChanged); } private: + MWLua::InputProcessor mInputProcessor; EngineHandlerList mStateChanged{ "onStateChanged" }; EngineHandlerList mConsoleCommandHandlers{ "onConsoleCommand" }; EngineHandlerList mUiModeChanged{ "_onUiModeChanged" }; diff --git a/apps/openmw/mwlua/playerscripts.hpp b/apps/openmw/mwlua/playerscripts.hpp index 2d3aa9bc78..bc3bee15ca 100644 --- a/apps/openmw/mwlua/playerscripts.hpp +++ b/apps/openmw/mwlua/playerscripts.hpp @@ -7,6 +7,7 @@ #include "../mwbase/luamanager.hpp" +#include "inputprocessor.hpp" #include "localscripts.hpp" namespace MWLua @@ -17,42 +18,14 @@ namespace MWLua public: PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) : LocalScripts(lua, obj) + , mInputProcessor(this) { - registerEngineHandlers({ &mConsoleCommandHandlers, &mKeyPressHandlers, &mKeyReleaseHandlers, - &mControllerButtonPressHandlers, &mControllerButtonReleaseHandlers, &mActionHandlers, &mOnFrameHandlers, - &mTouchpadPressed, &mTouchpadReleased, &mTouchpadMoved, &mQuestUpdate, &mUiModeChanged }); + registerEngineHandlers({ &mConsoleCommandHandlers, &mOnFrameHandlers, &mQuestUpdate, &mUiModeChanged }); } void processInputEvent(const MWBase::LuaManager::InputEvent& event) { - using InputEvent = MWBase::LuaManager::InputEvent; - switch (event.mType) - { - case InputEvent::KeyPressed: - callEngineHandlers(mKeyPressHandlers, std::get(event.mValue)); - break; - case InputEvent::KeyReleased: - callEngineHandlers(mKeyReleaseHandlers, std::get(event.mValue)); - break; - case InputEvent::ControllerPressed: - callEngineHandlers(mControllerButtonPressHandlers, std::get(event.mValue)); - break; - case InputEvent::ControllerReleased: - callEngineHandlers(mControllerButtonReleaseHandlers, std::get(event.mValue)); - break; - case InputEvent::Action: - callEngineHandlers(mActionHandlers, std::get(event.mValue)); - break; - case InputEvent::TouchPressed: - callEngineHandlers(mTouchpadPressed, std::get(event.mValue)); - break; - case InputEvent::TouchReleased: - callEngineHandlers(mTouchpadReleased, std::get(event.mValue)); - break; - case InputEvent::TouchMoved: - callEngineHandlers(mTouchpadMoved, std::get(event.mValue)); - break; - } + mInputProcessor.processInputEvent(event); } void onFrame(float dt) { callEngineHandlers(mOnFrameHandlers, dt); } @@ -75,16 +48,9 @@ namespace MWLua } private: + InputProcessor mInputProcessor; EngineHandlerList mConsoleCommandHandlers{ "onConsoleCommand" }; - EngineHandlerList mKeyPressHandlers{ "onKeyPress" }; - EngineHandlerList mKeyReleaseHandlers{ "onKeyRelease" }; - EngineHandlerList mControllerButtonPressHandlers{ "onControllerButtonPress" }; - EngineHandlerList mControllerButtonReleaseHandlers{ "onControllerButtonRelease" }; - EngineHandlerList mActionHandlers{ "onInputAction" }; EngineHandlerList mOnFrameHandlers{ "onFrame" }; - EngineHandlerList mTouchpadPressed{ "onTouchPress" }; - EngineHandlerList mTouchpadReleased{ "onTouchRelease" }; - EngineHandlerList mTouchpadMoved{ "onTouchMove" }; EngineHandlerList mQuestUpdate{ "onQuestUpdate" }; EngineHandlerList mUiModeChanged{ "_onUiModeChanged" }; }; diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp index 631b1e58a8..b3fb0bd376 100644 --- a/components/lua/scriptscontainer.hpp +++ b/components/lua/scriptscontainer.hpp @@ -157,7 +157,8 @@ namespace LuaUtil void collectStats(std::vector& stats) const; static int64_t getInstanceCount() { return sInstanceCount; } - protected: + public: // TODO: public to be available to MWLua::InputProcessor. Consider other ways of reusing engine handlers + // between containers struct Handler { int mScriptId; @@ -198,6 +199,7 @@ namespace LuaUtil // a public function (see how ScriptsContainer::update is implemented) that calls `callEngineHandlers`. void registerEngineHandlers(std::initializer_list handlers); + protected: const std::string mNamePrefix; LuaUtil::LuaState& mLua; diff --git a/docs/source/reference/lua-scripting/engine_handlers.rst b/docs/source/reference/lua-scripting/engine_handlers.rst index 6ef9846d2e..e4ef9b20e4 100644 --- a/docs/source/reference/lua-scripting/engine_handlers.rst +++ b/docs/source/reference/lua-scripting/engine_handlers.rst @@ -80,23 +80,16 @@ Engine handler is a function defined by a script, that can be called by the engi | Similarly to onActivated, the item has already been removed | from the actor's inventory, and the count was set to zero. -**Only for local scripts attached to a player** +**Only menu scripts and local scripts attached to a player** .. list-table:: :widths: 20 80 - * - onFrame(dt) - - | Called every frame (even if the game is paused) right after - | processing user input. Use it only for latency-critical stuff - | and for UI that should work on pause. - | `dt` is simulation time delta (0 when on pause). - * - onKeyPress(key) +* - onKeyPress(key) - | `Key `_ is pressed. | Usage example: | ``if key.symbol == 'z' and key.withShift then ...`` - * - onQuestUpdate(questId, stage) - - | Called when a quest is updated. - * - onKeyRelease(key) + * - onKeyRelease(key) - | `Key `_ is released. | Usage example: | ``if key.symbol == 'z' and key.withShift then ...`` @@ -127,6 +120,24 @@ Engine handler is a function defined by a script, that can be called by the engi - | User entered `command` in in-game console. Called if either | `mode` is not default or `command` starts with prefix `lua`. +**Only for local scripts attached to a player** + +.. list-table:: + :widths: 20 80 + * - onFrame(dt) + - | Called every frame (even if the game is paused) right after + | processing user input. Use it only for latency-critical stuff + | and for UI that should work on pause. + | `dt` is simulation time delta (0 when on pause). + * - onKeyPress(key) + - | `Key `_ is pressed. + | Usage example: + | ``if key.symbol == 'z' and key.withShift then ...`` + * - onQuestUpdate(questId, stage) + - | Called when a quest is updated. + + + **Only for menu scripts** .. list-table:: @@ -134,7 +145,3 @@ Engine handler is a function defined by a script, that can be called by the engi * - onStateChanged() - | Called whenever the current game changes | (i. e. the result of `getState `_ changes) - * - | onConsoleCommand( - | mode, command, selectedObject) - - | User entered `command` in in-game console. Called if either - | `mode` is not default or `command` starts with prefix `lua`. From 82a125fb6a55f6f00dfb28ce58fde660f363d499 Mon Sep 17 00:00:00 2001 From: uramer Date: Wed, 10 Jan 2024 19:34:33 +0100 Subject: [PATCH 14/39] Replace onUpdate with onFrame for menu scripts --- apps/openmw/mwlua/luamanagerimp.cpp | 2 +- apps/openmw/mwlua/menuscripts.hpp | 5 +++- apps/openmw/mwworld/datetimemanager.cpp | 4 ++- .../lua-scripting/engine_handlers.rst | 28 +++++++++---------- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 5b5a1b7d0d..8f1abceafc 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -243,7 +243,7 @@ namespace MWLua ? 0.0 : MWBase::Environment::get().getFrameDuration(); mInputActions.update(frameDuration); - mMenuScripts.update(0); + mMenuScripts.onFrame(frameDuration); if (playerScripts) playerScripts->onFrame(frameDuration); mProcessingInputEvents = false; diff --git a/apps/openmw/mwlua/menuscripts.hpp b/apps/openmw/mwlua/menuscripts.hpp index 3bd55952ad..a010317f47 100644 --- a/apps/openmw/mwlua/menuscripts.hpp +++ b/apps/openmw/mwlua/menuscripts.hpp @@ -24,7 +24,7 @@ namespace MWLua : LuaUtil::ScriptsContainer(lua, "Menu") , mInputProcessor(this) { - registerEngineHandlers({ &mStateChanged, &mConsoleCommandHandlers, &mUiModeChanged }); + registerEngineHandlers({ &mOnFrameHandlers, &mStateChanged, &mConsoleCommandHandlers, &mUiModeChanged }); } void processInputEvent(const MWBase::LuaManager::InputEvent& event) @@ -32,6 +32,8 @@ namespace MWLua mInputProcessor.processInputEvent(event); } + void onFrame(float dt) { callEngineHandlers(mOnFrameHandlers, dt); } + void stateChanged() { callEngineHandlers(mStateChanged); } bool consoleCommand(const std::string& consoleMode, const std::string& command) @@ -44,6 +46,7 @@ namespace MWLua private: MWLua::InputProcessor mInputProcessor; + EngineHandlerList mOnFrameHandlers{ "onFrame" }; EngineHandlerList mStateChanged{ "onStateChanged" }; EngineHandlerList mConsoleCommandHandlers{ "onConsoleCommand" }; EngineHandlerList mUiModeChanged{ "_onUiModeChanged" }; diff --git a/apps/openmw/mwworld/datetimemanager.cpp b/apps/openmw/mwworld/datetimemanager.cpp index 78565cef60..69374a77a9 100644 --- a/apps/openmw/mwworld/datetimemanager.cpp +++ b/apps/openmw/mwworld/datetimemanager.cpp @@ -4,6 +4,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" @@ -263,8 +264,9 @@ namespace MWWorld void DateTimeManager::updateIsPaused() { + auto stateManager = MWBase::Environment::get().getStateManager(); auto wm = MWBase::Environment::get().getWindowManager(); mPaused = !mPausedTags.empty() || wm->isConsoleMode() || wm->isPostProcessorHudVisible() - || wm->isInteractiveMessageBoxActive(); + || wm->isInteractiveMessageBoxActive() || stateManager->getState() == MWBase::StateManager::State_NoGame; } } diff --git a/docs/source/reference/lua-scripting/engine_handlers.rst b/docs/source/reference/lua-scripting/engine_handlers.rst index e4ef9b20e4..bcadfeb295 100644 --- a/docs/source/reference/lua-scripting/engine_handlers.rst +++ b/docs/source/reference/lua-scripting/engine_handlers.rst @@ -5,10 +5,16 @@ Engine handlers reference Engine handler is a function defined by a script, that can be called by the engine. - - **Can be defined by any script** +.. list-table:: + :widths: 20 80 + * - onInterfaceOverride(base) + - | Called if the current script has an interface and overrides an interface + | (``base``) of another script. + +**Can be defined by any non-menu script** + .. list-table:: :widths: 20 80 @@ -29,9 +35,6 @@ Engine handler is a function defined by a script, that can be called by the engi | Note that ``onLoad`` means loading a script rather than loading a game. | If a script did not exist when a game was saved onLoad will not be | called, but ``onInit`` will. - * - onInterfaceOverride(base) - - | Called if the current script has an interface and overrides an interface - | (``base``) of another script. **Only for global scripts** @@ -84,8 +87,12 @@ Engine handler is a function defined by a script, that can be called by the engi .. list-table:: :widths: 20 80 - -* - onKeyPress(key) + * - onFrame(dt) + - | Called every frame (even if the game is paused) right after + | processing user input. Use it only for latency-critical stuff + | and for UI that should work on pause. + | `dt` is simulation time delta (0 when on pause). + * - onKeyPress(key) - | `Key `_ is pressed. | Usage example: | ``if key.symbol == 'z' and key.withShift then ...`` @@ -124,19 +131,12 @@ Engine handler is a function defined by a script, that can be called by the engi .. list-table:: :widths: 20 80 - * - onFrame(dt) - - | Called every frame (even if the game is paused) right after - | processing user input. Use it only for latency-critical stuff - | and for UI that should work on pause. - | `dt` is simulation time delta (0 when on pause). * - onKeyPress(key) - | `Key `_ is pressed. | Usage example: | ``if key.symbol == 'z' and key.withShift then ...`` * - onQuestUpdate(questId, stage) - | Called when a quest is updated. - - **Only for menu scripts** From 6917384fc170f5488425fec3dfff0272ae983643 Mon Sep 17 00:00:00 2001 From: uramer Date: Wed, 10 Jan 2024 20:32:21 +0100 Subject: [PATCH 15/39] Don't reset menu-registered setting groups --- files/data/scripts/omw/settings/menu.lua | 13 ++++++++++--- files/data/scripts/omw/settings/player.lua | 1 - 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/files/data/scripts/omw/settings/menu.lua b/files/data/scripts/omw/settings/menu.lua index ff554df768..61fce3015e 100644 --- a/files/data/scripts/omw/settings/menu.lua +++ b/files/data/scripts/omw/settings/menu.lua @@ -47,7 +47,7 @@ local spacedLines = function(count) local content = {} table.insert(content, spacer) table.insert(content, stretchingLine) - for i = 2, count do + for _ = 2, count do table.insert(content, interval) table.insert(content, stretchingLine) end @@ -422,10 +422,14 @@ local function updateGlobalGroups() end)) end +local menuGroups = {} + local function resetGroups() for pageKey, page in pairs(groups) do for groupKey in pairs(page) do - page[groupKey] = nil + if not menuGroups[groupKey] then + page[groupKey] = nil + end end local renderedOptions = renderPage(pages[pageKey]) for k, v in pairs(renderedOptions) do @@ -475,7 +479,10 @@ return { version = 1, registerPage = registerPage, registerRenderer = registerRenderer, - registerGroup = common.registerGroup, + registerGroup = function(options) + common.registerGroup(options) + menuGroups[options.key] = true + end, updateRendererArgument = common.updateRendererArgument, }, engineHandlers = { diff --git a/files/data/scripts/omw/settings/player.lua b/files/data/scripts/omw/settings/player.lua index 3898e733e1..ea3f207df6 100644 --- a/files/data/scripts/omw/settings/player.lua +++ b/files/data/scripts/omw/settings/player.lua @@ -1,4 +1,3 @@ -local storage = require('openmw.storage') local types = require('openmw.types') local self = require('openmw.self') From 5b97a931691095ae325aa2532a731edc2bc513ee Mon Sep 17 00:00:00 2001 From: uramer Date: Wed, 10 Jan 2024 20:32:34 +0100 Subject: [PATCH 16/39] Move camera settings to a menu script --- files/data/builtin.omwscripts | 1 + files/data/scripts/omw/camera/camera.lua | 3 +- .../data/scripts/omw/camera/head_bobbing.lua | 16 +++--- files/data/scripts/omw/camera/settings.lua | 50 +++++++++---------- .../data/scripts/omw/camera/third_person.lua | 22 ++++---- 5 files changed, 45 insertions(+), 47 deletions(-) diff --git a/files/data/builtin.omwscripts b/files/data/builtin.omwscripts index 3e902f6639..6016dee28a 100644 --- a/files/data/builtin.omwscripts +++ b/files/data/builtin.omwscripts @@ -13,6 +13,7 @@ GLOBAL: scripts/omw/usehandlers.lua GLOBAL: scripts/omw/worldeventhandlers.lua PLAYER: scripts/omw/mechanics/playercontroller.lua PLAYER: scripts/omw/playercontrols.lua +MENU: scripts/omw/camera/settings.lua PLAYER: scripts/omw/camera/camera.lua PLAYER: scripts/omw/input/actionbindings.lua PLAYER: scripts/omw/input/smoothmovement.lua diff --git a/files/data/scripts/omw/camera/camera.lua b/files/data/scripts/omw/camera/camera.lua index 6c162f3a25..f5848970dd 100644 --- a/files/data/scripts/omw/camera/camera.lua +++ b/files/data/scripts/omw/camera/camera.lua @@ -5,6 +5,7 @@ local util = require('openmw.util') local self = require('openmw.self') local nearby = require('openmw.nearby') local async = require('openmw.async') +local storage = require('openmw.storage') local I = require('openmw.interfaces') local Actor = require('openmw.types').Actor @@ -28,7 +29,7 @@ input.registerAction { defaultValue = 0, } -local settings = require('scripts.omw.camera.settings').thirdPerson +local settings = storage.playerSection('SettingsOMWCameraThirdPerson') local head_bobbing = require('scripts.omw.camera.head_bobbing') local third_person = require('scripts.omw.camera.third_person') local pov_auto_switch = require('scripts.omw.camera.first_person_auto_switch') diff --git a/files/data/scripts/omw/camera/head_bobbing.lua b/files/data/scripts/omw/camera/head_bobbing.lua index 8972364ebb..c5402ee3dc 100644 --- a/files/data/scripts/omw/camera/head_bobbing.lua +++ b/files/data/scripts/omw/camera/head_bobbing.lua @@ -2,12 +2,13 @@ local camera = require('openmw.camera') local self = require('openmw.self') local util = require('openmw.util') local async = require('openmw.async') +local storage = require('openmw.storage') local Actor = require('openmw.types').Actor local M = {} -local settings = require('scripts.omw.camera.settings').headBobbing +local settings = storage.playerSection('SettingsOMWCameraHeadBobbing') local doubleStepLength, stepHeight, maxRoll @@ -31,7 +32,7 @@ local arcHeight = sampleArc(1) function M.update(dt, smoothedSpeed) local speed = Actor.getCurrentSpeed(self) - speed = speed / (1 + speed / 500) -- limit bobbing frequency if the speed is very high + speed = speed / (1 + speed / 500) -- limit bobbing frequency if the speed is very high totalMovement = totalMovement + speed * dt if not M.enabled or camera.getMode() ~= camera.MODE.FirstPerson then effectWeight = 0 @@ -44,18 +45,17 @@ function M.update(dt, smoothedSpeed) end local doubleStepState = totalMovement / doubleStepLength - doubleStepState = doubleStepState - math.floor(doubleStepState) -- from 0 to 1 during 2 steps - local stepState = math.abs(doubleStepState * 4 - 2) - 1 -- from -1 to 1 on even steps and from 1 to -1 on odd steps - local effect = sampleArc(stepState) / arcHeight -- range from 0 to 1 + doubleStepState = doubleStepState - math.floor(doubleStepState) -- from 0 to 1 during 2 steps + local stepState = math.abs(doubleStepState * 4 - 2) - 1 -- from -1 to 1 on even steps and from 1 to -1 on odd steps + local effect = sampleArc(stepState) / arcHeight -- range from 0 to 1 -- Smoothly reduce the effect to zero when the player stops local coef = math.min(smoothedSpeed / 300, 1) * effectWeight - local zOffset = (0.5 - effect) * coef * stepHeight -- range from -stepHeight/2 to stepHeight/2 - local roll = ((stepState > 0 and 1) or -1) * effect * coef * maxRoll -- range from -maxRoll to maxRoll + local zOffset = (0.5 - effect) * coef * stepHeight -- range from -stepHeight/2 to stepHeight/2 + local roll = ((stepState > 0 and 1) or -1) * effect * coef * maxRoll -- range from -maxRoll to maxRoll camera.setFirstPersonOffset(camera.getFirstPersonOffset() + util.vector3(0, 0, zOffset)) camera.setExtraRoll(camera.getExtraRoll() + roll) end return M - diff --git a/files/data/scripts/omw/camera/settings.lua b/files/data/scripts/omw/camera/settings.lua index be95ce586c..8d1d51f4a8 100644 --- a/files/data/scripts/omw/camera/settings.lua +++ b/files/data/scripts/omw/camera/settings.lua @@ -3,10 +3,10 @@ local async = require('openmw.async') local I = require('openmw.interfaces') I.Settings.registerPage({ - key = 'OMWCamera', - l10n = 'OMWCamera', - name = 'Camera', - description = 'settingsPageDescription', + key = 'OMWCamera', + l10n = 'OMWCamera', + name = 'Camera', + description = 'settingsPageDescription', }) local thirdPersonGroup = 'SettingsOMWCameraThirdPerson' @@ -16,8 +16,8 @@ local function boolSetting(prefix, key, default) return { key = key, renderer = 'checkbox', - name = prefix..key, - description = prefix..key..'Description', + name = prefix .. key, + description = prefix .. key .. 'Description', default = default, } end @@ -26,8 +26,8 @@ local function floatSetting(prefix, key, default) return { key = key, renderer = 'number', - name = prefix..key, - description = prefix..key..'Description', + name = prefix .. key, + description = prefix .. key .. 'Description', default = default, } end @@ -70,33 +70,29 @@ I.Settings.registerGroup({ }, }) -local settings = { - thirdPerson = storage.playerSection(thirdPersonGroup), - headBobbing = storage.playerSection(headBobbingGroup), -} +local thirdPerson = storage.playerSection(thirdPersonGroup) +local headBobbing = storage.playerSection(headBobbingGroup) local function updateViewOverShoulderDisabled() - local shoulderDisabled = not settings.thirdPerson:get('viewOverShoulder') - I.Settings.updateRendererArgument(thirdPersonGroup, 'shoulderOffsetX', {disabled = shoulderDisabled}) - I.Settings.updateRendererArgument(thirdPersonGroup, 'shoulderOffsetY', {disabled = shoulderDisabled}) - I.Settings.updateRendererArgument(thirdPersonGroup, 'autoSwitchShoulder', {disabled = shoulderDisabled}) - I.Settings.updateRendererArgument(thirdPersonGroup, 'zoomOutWhenMoveCoef', {disabled = shoulderDisabled}) + local shoulderDisabled = not thirdPerson:get('viewOverShoulder') + I.Settings.updateRendererArgument(thirdPersonGroup, 'shoulderOffsetX', { disabled = shoulderDisabled }) + I.Settings.updateRendererArgument(thirdPersonGroup, 'shoulderOffsetY', { disabled = shoulderDisabled }) + I.Settings.updateRendererArgument(thirdPersonGroup, 'autoSwitchShoulder', { disabled = shoulderDisabled }) + I.Settings.updateRendererArgument(thirdPersonGroup, 'zoomOutWhenMoveCoef', { disabled = shoulderDisabled }) - local move360Disabled = not settings.thirdPerson:get('move360') - I.Settings.updateRendererArgument(thirdPersonGroup, 'move360TurnSpeed', {disabled = move360Disabled}) + local move360Disabled = not thirdPerson:get('move360') + I.Settings.updateRendererArgument(thirdPersonGroup, 'move360TurnSpeed', { disabled = move360Disabled }) end local function updateHeadBobbingDisabled() - local disabled = not settings.headBobbing:get('enabled') - I.Settings.updateRendererArgument(headBobbingGroup, 'step', {disabled = disabled, min = 1}) - I.Settings.updateRendererArgument(headBobbingGroup, 'height', {disabled = disabled}) - I.Settings.updateRendererArgument(headBobbingGroup, 'roll', {disabled = disabled, min = 0, max = 90}) + local disabled = not headBobbing:get('enabled') + I.Settings.updateRendererArgument(headBobbingGroup, 'step', { disabled = disabled, min = 1 }) + I.Settings.updateRendererArgument(headBobbingGroup, 'height', { disabled = disabled }) + I.Settings.updateRendererArgument(headBobbingGroup, 'roll', { disabled = disabled, min = 0, max = 90 }) end updateViewOverShoulderDisabled() updateHeadBobbingDisabled() -settings.thirdPerson:subscribe(async:callback(updateViewOverShoulderDisabled)) -settings.headBobbing:subscribe(async:callback(updateHeadBobbingDisabled)) - -return settings +thirdPerson:subscribe(async:callback(updateViewOverShoulderDisabled)) +headBobbing:subscribe(async:callback(updateHeadBobbingDisabled)) diff --git a/files/data/scripts/omw/camera/third_person.lua b/files/data/scripts/omw/camera/third_person.lua index 8c68d1596d..9d5004349a 100644 --- a/files/data/scripts/omw/camera/third_person.lua +++ b/files/data/scripts/omw/camera/third_person.lua @@ -3,10 +3,11 @@ local util = require('openmw.util') local self = require('openmw.self') local nearby = require('openmw.nearby') local async = require('openmw.async') +local storage = require('openmw.storage') local Actor = require('openmw.types').Actor -local settings = require('scripts.omw.camera.settings').thirdPerson +local settings = storage.playerSection('SettingsOMWCameraThirdPerson') local MODE = camera.MODE local STATE = { RightShoulder = 0, LeftShoulder = 1, Combat = 2, Swimming = 3 } @@ -31,7 +32,7 @@ local function updateSettings() viewOverShoulder = settings:get('viewOverShoulder') autoSwitchShoulder = settings:get('autoSwitchShoulder') shoulderOffset = util.vector2(settings:get('shoulderOffsetX'), - settings:get('shoulderOffsetY')) + settings:get('shoulderOffsetY')) zoomOutWhenMoveCoef = settings:get('zoomOutWhenMoveCoef') defaultShoulder = (shoulderOffset.x > 0 and STATE.RightShoulder) or STATE.LeftShoulder @@ -46,7 +47,7 @@ local state = defaultShoulder local function ray(from, angle, limit) local to = from + util.transform.rotateZ(angle) * util.vector3(0, limit, 0) - local res = nearby.castRay(from, to, {collisionType = camera.getCollisionType()}) + local res = nearby.castRay(from, to, { collisionType = camera.getCollisionType() }) if res.hit then return (res.hitPos - from):length() else @@ -55,8 +56,8 @@ local function ray(from, angle, limit) end local function trySwitchShoulder() - local limitToSwitch = 120 -- switch to other shoulder if wall is closer than this limit - local limitToSwitchBack = 300 -- switch back to default shoulder if there is no walls at this distance + local limitToSwitch = 120 -- switch to other shoulder if wall is closer than this limit + local limitToSwitchBack = 300 -- switch back to default shoulder if there is no walls at this distance local pos = camera.getTrackedPosition() local rayRight = ray(pos, camera.getYaw() + math.rad(90), limitToSwitchBack + 1) @@ -79,7 +80,7 @@ end local function calculateDistance(smoothedSpeed) local smoothedSpeedSqr = smoothedSpeed * smoothedSpeed return (M.baseDistance + math.max(camera.getPitch(), 0) * 50 - + smoothedSpeedSqr / (smoothedSpeedSqr + 300*300) * zoomOutWhenMoveCoef) + + smoothedSpeedSqr / (smoothedSpeedSqr + 300 * 300) * zoomOutWhenMoveCoef) end local function updateState() @@ -95,7 +96,7 @@ local function updateState() state = defaultShoulder end if (mode == MODE.ThirdPerson or Actor.getCurrentSpeed(self) > 0 or state ~= oldState or noThirdPersonLastFrame) - and (state == STATE.LeftShoulder or state == STATE.RightShoulder) then + and (state == STATE.LeftShoulder or state == STATE.RightShoulder) then if autoSwitchShoulder then trySwitchShoulder() else @@ -108,11 +109,11 @@ local function updateState() -- Player doesn't touch controls for a long time. Transition should be very slow. camera.setFocalTransitionSpeed(0.2) elseif (oldState == STATE.Combat or state == STATE.Combat) and - (mode ~= MODE.Preview or M.standingPreview) then + (mode ~= MODE.Preview or M.standingPreview) then -- Transition to/from combat mode and we are not in preview mode. Should be fast. camera.setFocalTransitionSpeed(5.0) else - camera.setFocalTransitionSpeed(1.0) -- Default transition speed. + camera.setFocalTransitionSpeed(1.0) -- Default transition speed. end if state == STATE.RightShoulder then @@ -149,7 +150,7 @@ function M.update(dt, smoothedSpeed) end M.preferredDistance = calculateDistance(smoothedSpeed) - if noThirdPersonLastFrame then -- just switched to third person view + if noThirdPersonLastFrame then -- just switched to third person view camera.setPreferredThirdPersonDistance(M.preferredDistance) camera.instantTransition() noThirdPersonLastFrame = false @@ -161,4 +162,3 @@ function M.update(dt, smoothedSpeed) end return M - From c6a27d06b0fc9c759b91420dd5077bc0b1ff1d22 Mon Sep 17 00:00:00 2001 From: uramer Date: Wed, 10 Jan 2024 20:40:16 +0100 Subject: [PATCH 17/39] Move controls settings to menu context --- docs/source/luadoc_data_paths.sh | 2 +- files/data/CMakeLists.txt | 3 +- files/data/builtin.omwscripts | 3 +- .../omw/{ => input}/playercontrols.lua | 32 ------------------ files/data/scripts/omw/input/settings.lua | 33 +++++++++++++++++++ 5 files changed, 38 insertions(+), 35 deletions(-) rename files/data/scripts/omw/{ => input}/playercontrols.lua (91%) create mode 100644 files/data/scripts/omw/input/settings.lua diff --git a/docs/source/luadoc_data_paths.sh b/docs/source/luadoc_data_paths.sh index 7bcda5110c..1343ac818c 100755 --- a/docs/source/luadoc_data_paths.sh +++ b/docs/source/luadoc_data_paths.sh @@ -2,7 +2,7 @@ paths=( openmw_aux/*lua scripts/omw/activationhandlers.lua scripts/omw/ai.lua - scripts/omw/playercontrols.lua + scripts/omw/input/playercontrols.lua scripts/omw/camera/camera.lua scripts/omw/mwui/init.lua scripts/omw/settings/player.lua diff --git a/files/data/CMakeLists.txt b/files/data/CMakeLists.txt index e038e9f573..8027cfb6e2 100644 --- a/files/data/CMakeLists.txt +++ b/files/data/CMakeLists.txt @@ -77,7 +77,6 @@ set(BUILTIN_DATA_FILES scripts/omw/console/player.lua scripts/omw/console/menu.lua scripts/omw/mechanics/playercontroller.lua - scripts/omw/playercontrols.lua scripts/omw/settings/menu.lua scripts/omw/settings/player.lua scripts/omw/settings/global.lua @@ -93,6 +92,8 @@ set(BUILTIN_DATA_FILES scripts/omw/ui.lua scripts/omw/usehandlers.lua scripts/omw/worldeventhandlers.lua + scripts/omw/input/settings.lua + scripts/omw/input/playercontrols.lua scripts/omw/input/actionbindings.lua scripts/omw/input/smoothmovement.lua diff --git a/files/data/builtin.omwscripts b/files/data/builtin.omwscripts index 6016dee28a..6d47b96e0a 100644 --- a/files/data/builtin.omwscripts +++ b/files/data/builtin.omwscripts @@ -12,9 +12,10 @@ GLOBAL: scripts/omw/cellhandlers.lua GLOBAL: scripts/omw/usehandlers.lua GLOBAL: scripts/omw/worldeventhandlers.lua PLAYER: scripts/omw/mechanics/playercontroller.lua -PLAYER: scripts/omw/playercontrols.lua MENU: scripts/omw/camera/settings.lua PLAYER: scripts/omw/camera/camera.lua +MENU: scripts/omw/input/settings.lua +PLAYER: scripts/omw/input/playercontrols.lua PLAYER: scripts/omw/input/actionbindings.lua PLAYER: scripts/omw/input/smoothmovement.lua NPC,CREATURE: scripts/omw/ai.lua diff --git a/files/data/scripts/omw/playercontrols.lua b/files/data/scripts/omw/input/playercontrols.lua similarity index 91% rename from files/data/scripts/omw/playercontrols.lua rename to files/data/scripts/omw/input/playercontrols.lua index ec7d0d238e..311b5a16a9 100644 --- a/files/data/scripts/omw/playercontrols.lua +++ b/files/data/scripts/omw/input/playercontrols.lua @@ -9,38 +9,6 @@ local Player = require('openmw.types').Player local I = require('openmw.interfaces') -local settingsGroup = 'SettingsOMWControls' - -local function boolSetting(key, default) - return { - key = key, - renderer = 'checkbox', - name = key, - description = key .. 'Description', - default = default, - } -end - -I.Settings.registerPage({ - key = 'OMWControls', - l10n = 'OMWControls', - name = 'ControlsPage', - description = 'ControlsPageDescription', -}) - -I.Settings.registerGroup({ - key = settingsGroup, - page = 'OMWControls', - l10n = 'OMWControls', - name = 'MovementSettings', - permanentStorage = true, - settings = { - boolSetting('alwaysRun', false), - boolSetting('toggleSneak', false), -- TODO: consider removing this setting when we have the advanced binding UI - boolSetting('smoothControllerMovement', true), - }, -}) - local settings = storage.playerSection('SettingsOMWControls') do diff --git a/files/data/scripts/omw/input/settings.lua b/files/data/scripts/omw/input/settings.lua new file mode 100644 index 0000000000..fdd17616cc --- /dev/null +++ b/files/data/scripts/omw/input/settings.lua @@ -0,0 +1,33 @@ +local I = require('openmw.interfaces') + +local settingsGroup = 'SettingsOMWControls' + +local function boolSetting(key, default) + return { + key = key, + renderer = 'checkbox', + name = key, + description = key .. 'Description', + default = default, + } +end + +I.Settings.registerPage({ + key = 'OMWControls', + l10n = 'OMWControls', + name = 'ControlsPage', + description = 'ControlsPageDescription', +}) + +I.Settings.registerGroup({ + key = settingsGroup, + page = 'OMWControls', + l10n = 'OMWControls', + name = 'MovementSettings', + permanentStorage = true, + settings = { + boolSetting('alwaysRun', false), + boolSetting('toggleSneak', false), -- TODO: consider removing this setting when we have the advanced binding UI + boolSetting('smoothControllerMovement', true), + }, +}) From 79deb5f5595d862881cd4e2ae76a0371bd6f6838 Mon Sep 17 00:00:00 2001 From: uramer Date: Wed, 10 Jan 2024 21:10:10 +0100 Subject: [PATCH 18/39] Remove settings pages in Lua --- apps/openmw/mwlua/luamanagerimp.cpp | 7 ++++++ apps/openmw/mwlua/luamanagerimp.hpp | 1 + apps/openmw/mwlua/uibindings.cpp | 1 + components/lua_ui/registerscriptsettings.hpp | 1 + components/lua_ui/scriptsettings.cpp | 7 +++++- components/lua_ui/util.cpp | 2 -- files/data/scripts/omw/settings/menu.lua | 25 +++++++++++++++----- files/lua_api/openmw/ui.lua | 5 ++++ 8 files changed, 40 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 8f1abceafc..e7f7fa9dde 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include "../mwbase/windowmanager.hpp" @@ -62,6 +63,11 @@ namespace MWLua mGlobalScripts.setSerializer(mGlobalSerializer.get()); } + LuaManager::~LuaManager() + { + LuaUi::clearSettings(); + } + void LuaManager::initConfiguration() { mConfiguration.init(MWBase::Environment::get().getESMStore()->getLuaScriptsCfg()); @@ -551,6 +557,7 @@ namespace MWLua LuaUi::clearGameInterface(); LuaUi::clearMenuInterface(); + LuaUi::clearSettings(); MWBase::Environment::get().getWindowManager()->setConsoleMode(""); MWBase::Environment::get().getL10nManager()->dropCache(); mUiResourceManager.clear(); diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index 965aa67fab..56a30f24e0 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -35,6 +35,7 @@ namespace MWLua LuaManager(const VFS::Manager* vfs, const std::filesystem::path& libsDir); LuaManager(const LuaManager&) = delete; LuaManager(LuaManager&&) = delete; + ~LuaManager(); // Called by engine.cpp when the environment is fully initialized. void init(); diff --git a/apps/openmw/mwlua/uibindings.cpp b/apps/openmw/mwlua/uibindings.cpp index 54d8523b4d..061dc4821a 100644 --- a/apps/openmw/mwlua/uibindings.cpp +++ b/apps/openmw/mwlua/uibindings.cpp @@ -247,6 +247,7 @@ namespace MWLua { "Center", LuaUi::Alignment::Center }, { "End", LuaUi::Alignment::End } })); api["registerSettingsPage"] = &LuaUi::registerSettingsPage; + api["removeSettingsPage"] = &LuaUi::registerSettingsPage; api["texture"] = [luaManager = context.mLuaManager](const sol::table& options) { LuaUi::TextureData data; diff --git a/components/lua_ui/registerscriptsettings.hpp b/components/lua_ui/registerscriptsettings.hpp index fb794468da..ba36aff904 100644 --- a/components/lua_ui/registerscriptsettings.hpp +++ b/components/lua_ui/registerscriptsettings.hpp @@ -8,6 +8,7 @@ namespace LuaUi // implemented in scriptsettings.cpp void registerSettingsPage(const sol::table& options); void clearSettings(); + void removeSettingsPage(std::string_view key); } #endif // !OPENMW_LUAUI_REGISTERSCRIPTSETTINGS diff --git a/components/lua_ui/scriptsettings.cpp b/components/lua_ui/scriptsettings.cpp index e92d1d8958..514e6ce632 100644 --- a/components/lua_ui/scriptsettings.cpp +++ b/components/lua_ui/scriptsettings.cpp @@ -40,6 +40,11 @@ namespace LuaUi allPages.push_back(options); } + void removeSettingsPage(const sol::table& options) + { + std::erase_if(allPages, [options](const sol::table& it) { return it == options; }); + } + void clearSettings() { allPages.clear(); @@ -47,10 +52,10 @@ namespace LuaUi void attachPageAt(size_t index, LuaAdapter* adapter) { + adapter->detach(); if (index < allPages.size()) { ScriptSettingsPage page = parse(allPages[index]); - adapter->detach(); if (page.mElement.get()) adapter->attach(page.mElement); } diff --git a/components/lua_ui/util.cpp b/components/lua_ui/util.cpp index ac5e63e405..fe47de3b1d 100644 --- a/components/lua_ui/util.cpp +++ b/components/lua_ui/util.cpp @@ -46,8 +46,6 @@ namespace LuaUi void clearGameInterface() { - // TODO: move settings clearing logic to Lua? - clearSettings(); while (!Element::sGameElements.empty()) Element::sGameElements.begin()->second->destroy(); } diff --git a/files/data/scripts/omw/settings/menu.lua b/files/data/scripts/omw/settings/menu.lua index 61fce3015e..d20d75bf30 100644 --- a/files/data/scripts/omw/settings/menu.lua +++ b/files/data/scripts/omw/settings/menu.lua @@ -423,17 +423,27 @@ local function updateGlobalGroups() end local menuGroups = {} +local menuPages = {} -local function resetGroups() +local function reset() for pageKey, page in pairs(groups) do for groupKey in pairs(page) do if not menuGroups[groupKey] then page[groupKey] = nil end end - local renderedOptions = renderPage(pages[pageKey]) - for k, v in pairs(renderedOptions) do - pageOptions[pageKey][k] = v + if pageOptions[pageKey] then + pageOptions[pageKey].element.destroy() + if not menuPages[pageKey] then + pageOptions[pageKey].element.destroy() + ui.removeSettingsPage(pageOptions[pageKey]) + pageOptions[pageKey] = nil + else + local renderedOptions = renderPage(pages[pageKey]) + for k, v in pairs(renderedOptions) do + pageOptions[pageKey][k] = v + end + end end end end @@ -477,7 +487,10 @@ return { interfaceName = 'Settings', interface = { version = 1, - registerPage = registerPage, + registerPage = function(options) + registerPage(options) + menuPages[options] = true + end, registerRenderer = registerRenderer, registerGroup = function(options) common.registerGroup(options) @@ -492,7 +505,7 @@ return { if menu.getState() == menu.STATE.Running then updateGlobalGroups() else - resetGroups() + reset() end updatePlayerGroups() end, diff --git a/files/lua_api/openmw/ui.lua b/files/lua_api/openmw/ui.lua index 8582996c4f..a99b1e782e 100644 --- a/files/lua_api/openmw/ui.lua +++ b/files/lua_api/openmw/ui.lua @@ -93,6 +93,11 @@ -- @function [parent=#ui] registerSettingsPage -- @param #SettingsPageOptions page +--- +-- Removes the settings page +-- @function [parent=#ui] removeSettingsPage +-- @param #SettingsPageOptions page must be the exact same table of options as the one passed to registerSettingsPage + --- -- Table with settings page options, passed as an argument to ui.registerSettingsPage -- @type SettingsPageOptions From c2b8e318cff40f8755dd8cee16a29875411707f4 Mon Sep 17 00:00:00 2001 From: uramer Date: Wed, 10 Jan 2024 21:27:44 +0100 Subject: [PATCH 19/39] Move inputBinding renderer to menu context --- .../data/scripts/omw/input/actionbindings.lua | 82 +------------- files/data/scripts/omw/input/settings.lua | 102 ++++++++++++++++++ 2 files changed, 106 insertions(+), 78 deletions(-) diff --git a/files/data/scripts/omw/input/actionbindings.lua b/files/data/scripts/omw/input/actionbindings.lua index 06ded80793..35a467fb52 100644 --- a/files/data/scripts/omw/input/actionbindings.lua +++ b/files/data/scripts/omw/input/actionbindings.lua @@ -134,90 +134,16 @@ function clearBinding(id) end end -local function updateBinding(id, binding) - bindingSection:set(id, binding) +bindingSection:subscribe(async:callback(function(_, id) + if not id then return end + local binding = bindingSection:get(id) clearBinding(id) if binding ~= nil then registerBinding(binding, id) end return id -end +end)) -local interfaceL10n = core.l10n('interface') - -I.Settings.registerRenderer('inputBinding', function(id, set, arg) - if type(id) ~= 'string' then error('inputBinding: must have a string default value') end - if not arg.type then error('inputBinding: type argument is required') end - if not arg.key then error('inputBinding: key argument is required') end - local info = input.actions[arg.key] or input.triggers[arg.key] - if not info then return {} end - - local l10n = core.l10n(info.key) - - local name = { - template = I.MWUI.templates.textNormal, - props = { - text = l10n(info.name), - }, - } - - local description = { - template = I.MWUI.templates.textNormal, - props = { - text = l10n(info.description), - }, - } - - local binding = bindingSection:get(id) - local label = binding and input.getKeyName(binding.code) or interfaceL10n('None') - - local recorder = { - template = I.MWUI.templates.textEditLine, - props = { - readOnly = true, - text = label, - }, - events = { - focusGain = async:callback(function() - if binding == nil then return end - updateBinding(id, nil) - set(id) - end), - keyPress = async:callback(function(key) - if binding ~= nil or key.code == input.KEY.Escape then return end - - local newBinding = { - code = key.code, - type = arg.type, - key = arg.key, - } - updateBinding(id, newBinding) - set(id) - end), - }, - } - - local row = { - type = ui.TYPE.Flex, - props = { - horizontal = true, - }, - content = ui.content { - name, - { props = { size = util.vector2(10, 0) } }, - recorder, - }, - } - local column = { - type = ui.TYPE.Flex, - content = ui.content { - row, - description, - }, - } - - return column -end) local initiated = false diff --git a/files/data/scripts/omw/input/settings.lua b/files/data/scripts/omw/input/settings.lua index fdd17616cc..42fec91f82 100644 --- a/files/data/scripts/omw/input/settings.lua +++ b/files/data/scripts/omw/input/settings.lua @@ -1,3 +1,9 @@ +local core = require('openmw.core') +local input = require('openmw.input') +local storage = require('openmw.storage') +local ui = require('openmw.ui') +local util = require('openmw.util') +local async = require('openmw.async') local I = require('openmw.interfaces') local settingsGroup = 'SettingsOMWControls' @@ -31,3 +37,99 @@ I.Settings.registerGroup({ boolSetting('smoothControllerMovement', true), }, }) + +local interfaceL10n = core.l10n('interface') + +local bindingSection = storage.playerSection('OMWInputBindings') + +local recording = nil + + +I.Settings.registerRenderer('inputBinding', function(id, set, arg) + if type(id) ~= 'string' then error('inputBinding: must have a string default value') end + if not arg.type then error('inputBinding: type argument is required') end + if not arg.key then error('inputBinding: key argument is required') end + local info = input.actions[arg.key] or input.triggers[arg.key] + if not info then return {} end + + local l10n = core.l10n(info.key) + + local name = { + template = I.MWUI.templates.textNormal, + props = { + text = l10n(info.name), + }, + } + + local description = { + template = I.MWUI.templates.textNormal, + props = { + text = l10n(info.description), + }, + } + + local binding = bindingSection:get(id) + local label = interfaceL10n('None') + if binding then label = input.getKeyName(binding.code) end + if recording and recording.id == id then label = interfaceL10n('N/A') end + + local recorder = { + template = I.MWUI.templates.textNormal, + props = { + text = label, + }, + events = { + mouseClick = async:callback(function() + if recording ~= nil then return end + if binding ~= nil then bindingSection:set(id, nil) end + recording = { + id = id, + arg = arg, + refresh = function() set(id) end, + } + recording.refresh() + end), + }, + } + + local row = { + type = ui.TYPE.Flex, + props = { + horizontal = true, + }, + content = ui.content { + name, + { props = { size = util.vector2(10, 0) } }, + recorder, + }, + } + local column = { + type = ui.TYPE.Flex, + content = ui.content { + row, + description, + }, + } + + return column +end) + +return { + engineHandlers = { + onKeyPress = function(key) + if recording == nil then return end + local binding = { + code = key.code, + type = recording.arg.type, + key = recording.arg.key, + } + if key.code == input.KEY.Escape then -- TODO: prevent settings modal from closing + binding.code = nil + end + bindingSection:set(recording.id, binding) + local refresh = recording.refresh + recording = nil + refresh() + end, + } +} From 1afc7ecd589aa79d291140843bdd2fd36c3ff8f8 Mon Sep 17 00:00:00 2001 From: uramer Date: Wed, 10 Jan 2024 22:05:33 +0100 Subject: [PATCH 20/39] Test Lua widgets for text inputs correctly --- apps/openmw/mwgui/windowmanagerimp.cpp | 6 +++++- apps/openmw/mwlua/luamanagerimp.cpp | 8 +++++++- apps/openmw/mwlua/luamanagerimp.hpp | 1 + apps/openmw/mwlua/menuscripts.hpp | 1 - components/lua_ui/textedit.hpp | 3 +++ components/lua_ui/widget.hpp | 2 ++ 6 files changed, 18 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index c6b729332a..e463443b0c 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -51,6 +51,7 @@ #include #include +#include #include @@ -1676,7 +1677,10 @@ namespace MWGui void WindowManager::onKeyFocusChanged(MyGUI::Widget* widget) { - if (widget && widget->castType(false)) + bool isEditBox = widget && widget->castType(false); + LuaUi::WidgetExtension* luaWidget = dynamic_cast(widget); + bool capturesInput = luaWidget ? luaWidget->isTextInput() : isEditBox; + if (widget && capturesInput) SDL_StartTextInput(); else SDL_StopTextInput(); diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index e7f7fa9dde..df1e97b885 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -118,6 +118,7 @@ namespace MWLua = LuaUtil::LuaStorage::initPlayerPackage(mLua.sol(), &mGlobalStorage, &mPlayerStorage); mPlayerStorage.setActive(true); + mGlobalStorage.setActive(false); initConfiguration(); mInitialized = true; @@ -126,6 +127,8 @@ namespace MWLua void LuaManager::loadPermanentStorage(const std::filesystem::path& userConfigPath) { + mPlayerStorage.setActive(true); + mGlobalStorage.setActive(true); const auto globalPath = userConfigPath / "global_storage.bin"; const auto playerPath = userConfigPath / "player_storage.bin"; if (std::filesystem::exists(globalPath)) @@ -236,8 +239,9 @@ namespace MWLua = mPlayer.isEmpty() ? nullptr : dynamic_cast(mPlayer.getRefData().getLuaScripts()); MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); - for (const auto& event : mInputEvents) + for (const auto& event : mMenuInputEvents) mMenuScripts.processInputEvent(event); + mMenuInputEvents.clear(); if (playerScripts && !windowManager->containsMode(MWGui::GM_MainMenu)) { for (const auto& event : mInputEvents) @@ -300,6 +304,7 @@ namespace MWLua mLuaEvents.clear(); mEngineEvents.clear(); mInputEvents.clear(); + mMenuInputEvents.clear(); mObjectLists.clear(); mGlobalScripts.removeAllScripts(); mGlobalScriptsStarted = false; @@ -432,6 +437,7 @@ namespace MWLua { mInputEvents.push_back(event); } + mMenuInputEvents.push_back(event); } MWBase::LuaManager::ActorControls* LuaManager::getActorControls(const MWWorld::Ptr& ptr) const diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index 56a30f24e0..8ae83308d4 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -179,6 +179,7 @@ namespace MWLua LuaEvents mLuaEvents{ mGlobalScripts, mMenuScripts }; EngineEvents mEngineEvents{ mGlobalScripts }; std::vector mInputEvents; + std::vector mMenuInputEvents; std::unique_ptr mGlobalSerializer; std::unique_ptr mLocalSerializer; diff --git a/apps/openmw/mwlua/menuscripts.hpp b/apps/openmw/mwlua/menuscripts.hpp index a010317f47..befa76a3b2 100644 --- a/apps/openmw/mwlua/menuscripts.hpp +++ b/apps/openmw/mwlua/menuscripts.hpp @@ -51,7 +51,6 @@ namespace MWLua EngineHandlerList mConsoleCommandHandlers{ "onConsoleCommand" }; EngineHandlerList mUiModeChanged{ "_onUiModeChanged" }; }; - } #endif // MWLUA_GLOBALSCRIPTS_H diff --git a/components/lua_ui/textedit.hpp b/components/lua_ui/textedit.hpp index 3492a315bc..8f23b51746 100644 --- a/components/lua_ui/textedit.hpp +++ b/components/lua_ui/textedit.hpp @@ -11,6 +11,9 @@ namespace LuaUi { MYGUI_RTTI_DERIVED(LuaTextEdit) + public: + bool isTextInput() override { return mEditBox->getEditStatic(); } + protected: void initialize() override; void deinitialize() override; diff --git a/components/lua_ui/widget.hpp b/components/lua_ui/widget.hpp index c72b64ae3b..591c885ce9 100644 --- a/components/lua_ui/widget.hpp +++ b/components/lua_ui/widget.hpp @@ -73,6 +73,8 @@ namespace LuaUi virtual MyGUI::IntPoint calculatePosition(const MyGUI::IntSize& size); MyGUI::IntCoord calculateCoord(); + virtual bool isTextInput() { return false; } + protected: virtual void initialize(); void registerEvents(MyGUI::Widget* w); From 1092d2058df9b42200fa699aa55415b78b6b8de8 Mon Sep 17 00:00:00 2001 From: uramer Date: Wed, 10 Jan 2024 22:05:44 +0100 Subject: [PATCH 21/39] Load Lua storage before menu scripts might use it --- apps/openmw/engine.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 92483bd8c3..3200a0b6ee 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -893,8 +893,8 @@ void OMW::Engine::prepareEngine() << 100 * static_cast(result.second) / result.first << "%)"; } - mLuaManager->init(); mLuaManager->loadPermanentStorage(mCfgMgr.getUserConfigPath()); + mLuaManager->init(); // starts a separate lua thread if "lua num threads" > 0 mLuaWorker = std::make_unique(*mLuaManager, *mViewer); From 8cc47f53632da1b8f133d3e33cc80b5d53bfb7c3 Mon Sep 17 00:00:00 2001 From: uramer Date: Wed, 10 Jan 2024 23:05:13 +0100 Subject: [PATCH 22/39] Only allow menu scripts to register permanent groups --- files/data/scripts/omw/settings/menu.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/files/data/scripts/omw/settings/menu.lua b/files/data/scripts/omw/settings/menu.lua index d20d75bf30..87c3dc6e6a 100644 --- a/files/data/scripts/omw/settings/menu.lua +++ b/files/data/scripts/omw/settings/menu.lua @@ -493,6 +493,9 @@ return { end, registerRenderer = registerRenderer, registerGroup = function(options) + if not options.permanentStorage then + error('Menu scripts are only allowed to register setting groups with permanentStorage = true') + end common.registerGroup(options) menuGroups[options.key] = true end, From bd54292ff4f75e38c796144459db1253cfd87b91 Mon Sep 17 00:00:00 2001 From: uramer Date: Wed, 10 Jan 2024 23:10:19 +0100 Subject: [PATCH 23/39] Update I.Settings.registerGroup documentation --- files/data/scripts/omw/settings/player.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/files/data/scripts/omw/settings/player.lua b/files/data/scripts/omw/settings/player.lua index ea3f207df6..9f397c923d 100644 --- a/files/data/scripts/omw/settings/player.lua +++ b/files/data/scripts/omw/settings/player.lua @@ -117,7 +117,8 @@ return { end, --- -- @function [parent=#Settings] registerGroup Register a group to be attached to a page, - -- available both in player and global scripts + -- available in player, menu and global scripts + -- Note: menu scripts only allow group with permanentStorage = true, but can render the page before a game is loaded! -- @param #GroupOptions options -- @usage -- I.Settings.registerGroup { @@ -147,7 +148,7 @@ return { registerGroup = common.registerGroup, --- -- @function [parent=#Settings] updateRendererArgument Change the renderer argument of a setting - -- available both in player and global scripts + -- available both in player, menu and global scripts -- @param #string groupKey A settings group key -- @param #string settingKey A setting key -- @param argument A renderer argument From dd6017e81e2fdd1c4bcbec80a5dd1cc763b7d395 Mon Sep 17 00:00:00 2001 From: uramer Date: Thu, 11 Jan 2024 00:55:29 +0100 Subject: [PATCH 24/39] Avoid making engine handler methods public --- apps/openmw/mwlua/inputprocessor.hpp | 23 +++++++++++------------ apps/openmw/mwlua/menuscripts.hpp | 3 ++- apps/openmw/mwlua/playerscripts.hpp | 3 ++- components/lua/scriptscontainer.hpp | 4 +--- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/apps/openmw/mwlua/inputprocessor.hpp b/apps/openmw/mwlua/inputprocessor.hpp index 112d10c750..fe6de5450e 100644 --- a/apps/openmw/mwlua/inputprocessor.hpp +++ b/apps/openmw/mwlua/inputprocessor.hpp @@ -3,18 +3,17 @@ #include -#include -#include #include #include "../mwbase/luamanager.hpp" namespace MWLua { + template class InputProcessor { public: - InputProcessor(LuaUtil::ScriptsContainer* scriptsContainer) + InputProcessor(Container* scriptsContainer) : mScriptsContainer(scriptsContainer) { mScriptsContainer->registerEngineHandlers({ &mKeyPressHandlers, &mKeyReleaseHandlers, @@ -58,15 +57,15 @@ namespace MWLua } private: - LuaUtil::ScriptsContainer* mScriptsContainer; - LuaUtil::ScriptsContainer::EngineHandlerList mKeyPressHandlers{ "onKeyPress" }; - LuaUtil::ScriptsContainer::EngineHandlerList mKeyReleaseHandlers{ "onKeyRelease" }; - LuaUtil::ScriptsContainer::EngineHandlerList mControllerButtonPressHandlers{ "onControllerButtonPress" }; - LuaUtil::ScriptsContainer::EngineHandlerList mControllerButtonReleaseHandlers{ "onControllerButtonRelease" }; - LuaUtil::ScriptsContainer::EngineHandlerList mActionHandlers{ "onInputAction" }; - LuaUtil::ScriptsContainer::EngineHandlerList mTouchpadPressed{ "onTouchPress" }; - LuaUtil::ScriptsContainer::EngineHandlerList mTouchpadReleased{ "onTouchRelease" }; - LuaUtil::ScriptsContainer::EngineHandlerList mTouchpadMoved{ "onTouchMove" }; + Container* mScriptsContainer; + Container::EngineHandlerList mKeyPressHandlers{ "onKeyPress" }; + Container::EngineHandlerList mKeyReleaseHandlers{ "onKeyRelease" }; + Container::EngineHandlerList mControllerButtonPressHandlers{ "onControllerButtonPress" }; + Container::EngineHandlerList mControllerButtonReleaseHandlers{ "onControllerButtonRelease" }; + Container::EngineHandlerList mActionHandlers{ "onInputAction" }; + Container::EngineHandlerList mTouchpadPressed{ "onTouchPress" }; + Container::EngineHandlerList mTouchpadReleased{ "onTouchRelease" }; + Container::EngineHandlerList mTouchpadMoved{ "onTouchMove" }; }; } diff --git a/apps/openmw/mwlua/menuscripts.hpp b/apps/openmw/mwlua/menuscripts.hpp index befa76a3b2..8721224413 100644 --- a/apps/openmw/mwlua/menuscripts.hpp +++ b/apps/openmw/mwlua/menuscripts.hpp @@ -45,7 +45,8 @@ namespace MWLua void uiModeChanged() { callEngineHandlers(mUiModeChanged); } private: - MWLua::InputProcessor mInputProcessor; + friend class MWLua::InputProcessor; + MWLua::InputProcessor mInputProcessor; EngineHandlerList mOnFrameHandlers{ "onFrame" }; EngineHandlerList mStateChanged{ "onStateChanged" }; EngineHandlerList mConsoleCommandHandlers{ "onConsoleCommand" }; diff --git a/apps/openmw/mwlua/playerscripts.hpp b/apps/openmw/mwlua/playerscripts.hpp index bc3bee15ca..ea7baccb76 100644 --- a/apps/openmw/mwlua/playerscripts.hpp +++ b/apps/openmw/mwlua/playerscripts.hpp @@ -48,7 +48,8 @@ namespace MWLua } private: - InputProcessor mInputProcessor; + friend class MWLua::InputProcessor; + InputProcessor mInputProcessor; EngineHandlerList mConsoleCommandHandlers{ "onConsoleCommand" }; EngineHandlerList mOnFrameHandlers{ "onFrame" }; EngineHandlerList mQuestUpdate{ "onQuestUpdate" }; diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp index b3fb0bd376..631b1e58a8 100644 --- a/components/lua/scriptscontainer.hpp +++ b/components/lua/scriptscontainer.hpp @@ -157,8 +157,7 @@ namespace LuaUtil void collectStats(std::vector& stats) const; static int64_t getInstanceCount() { return sInstanceCount; } - public: // TODO: public to be available to MWLua::InputProcessor. Consider other ways of reusing engine handlers - // between containers + protected: struct Handler { int mScriptId; @@ -199,7 +198,6 @@ namespace LuaUtil // a public function (see how ScriptsContainer::update is implemented) that calls `callEngineHandlers`. void registerEngineHandlers(std::initializer_list handlers); - protected: const std::string mNamePrefix; LuaUtil::LuaState& mLua; From b5aca012eb96c41b45356c0c326a73bcfb4c3aba Mon Sep 17 00:00:00 2001 From: uramer Date: Thu, 11 Jan 2024 17:57:11 +0100 Subject: [PATCH 25/39] Fix typo --- docs/source/reference/lua-scripting/overview.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/reference/lua-scripting/overview.rst b/docs/source/reference/lua-scripting/overview.rst index ec5ab7338c..a9025fcf93 100644 --- a/docs/source/reference/lua-scripting/overview.rst +++ b/docs/source/reference/lua-scripting/overview.rst @@ -71,7 +71,7 @@ Global scripts Lua scripts that are not attached to any game object and are always active. Global scripts can not be started or stopped during a game session. Lists of global scripts are defined by `omwscripts` files, which should be :ref:`registered ` in `openmw.cfg`. Menu scripts - Lua scripts that are ran regardless of a game being loaded. They can be used to add features to the main menu and manage save files. + Lua scripts that are ran regardless of a game being loaded. They can be used to add features to the main menu and manage save files. Local scripts Lua scripts that are attached to some game object. A local script is active only if the object it is attached to is in an active cell. There are no limitations to the number of local scripts on one object. Local scripts can be attached to (or detached from) any object at any moment by a global script. In some cases inactive local scripts still can run code (for example during saving and loading), but while inactive they can not see nearby objects. @@ -480,7 +480,7 @@ This is another kind of script-to-script interactions. The differences: There are a few methods for sending events: -- `core.sendGlovalEvent `_ to send events to global scripts +- `core.sendGlobalEvent `_ to send events to global scripts - `GameObject:sendEvent `_ to send events to local scripts attached to a game object - `types.Player.sendMenuEvent `_ to send events to menu scripts of the given player @@ -624,7 +624,7 @@ Also in `openmw_aux`_ is the helper function ``runRepeatedly``, it is implemente local core = require('openmw.core') local time = require('openmw_aux.time') - -- call `doSomething()` at the end of every game day. + -- call `doSomething()` at the end of every game day. -- the second argument (`time.day`) is the interval. -- the periodical evaluation can be stopped at any moment by calling `stopFn()` local timeBeforeMidnight = time.day - core.getGameTime() % time.day From dd09c9b362068a52a577df399b22971d1f645a0a Mon Sep 17 00:00:00 2001 From: uramer Date: Sat, 13 Jan 2024 00:42:55 +0100 Subject: [PATCH 26/39] Don't save global storage if global scripts didn't run --- apps/openmw/mwlua/luamanagerimp.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 6f1d265340..6e62c8b7e3 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -141,7 +141,8 @@ namespace MWLua void LuaManager::savePermanentStorage(const std::filesystem::path& userConfigPath) { - mGlobalStorage.save(userConfigPath / "global_storage.bin"); + if (mGlobalScriptsStarted) + mGlobalStorage.save(userConfigPath / "global_storage.bin"); mPlayerStorage.save(userConfigPath / "player_storage.bin"); } @@ -318,6 +319,7 @@ namespace MWLua mPlayer.getRefData().setLuaScripts(nullptr); mPlayer = MWWorld::Ptr(); } + mGlobalStorage.setActive(true); mGlobalStorage.clearTemporaryAndRemoveCallbacks(); mGlobalStorage.setActive(false); mPlayerStorage.clearTemporaryAndRemoveCallbacks(); From 7cc0eae46117299c4395913019383b0eb78360d9 Mon Sep 17 00:00:00 2001 From: uramer Date: Sat, 13 Jan 2024 00:46:06 +0100 Subject: [PATCH 27/39] Fix Menu Lua settings reset between states --- components/lua_ui/registerscriptsettings.hpp | 2 +- files/data/scripts/omw/settings/common.lua | 3 --- files/data/scripts/omw/settings/global.lua | 1 + files/data/scripts/omw/settings/menu.lua | 28 ++++++++++++-------- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/components/lua_ui/registerscriptsettings.hpp b/components/lua_ui/registerscriptsettings.hpp index ba36aff904..58b033fc59 100644 --- a/components/lua_ui/registerscriptsettings.hpp +++ b/components/lua_ui/registerscriptsettings.hpp @@ -8,7 +8,7 @@ namespace LuaUi // implemented in scriptsettings.cpp void registerSettingsPage(const sol::table& options); void clearSettings(); - void removeSettingsPage(std::string_view key); + void removeSettingsPage(const sol::table& options); } #endif // !OPENMW_LUAUI_REGISTERSCRIPTSETTINGS diff --git a/files/data/scripts/omw/settings/common.lua b/files/data/scripts/omw/settings/common.lua index 9155c64ba7..1d62a54dc4 100644 --- a/files/data/scripts/omw/settings/common.lua +++ b/files/data/scripts/omw/settings/common.lua @@ -6,7 +6,6 @@ local argumentSectionPostfix = 'Arguments' local contextSection = storage.playerSection or storage.globalSection local groupSection = contextSection(groupSectionKey) -groupSection:removeOnExit() local function validateSettingOptions(options) if type(options) ~= 'table' then @@ -109,11 +108,9 @@ end return { getSection = function(global, key) - if global then error('Getting global section') end return (global and storage.globalSection or storage.playerSection)(key) end, getArgumentSection = function(global, key) - if global then error('Getting global section') end return (global and storage.globalSection or storage.playerSection)(key .. argumentSectionPostfix) end, updateRendererArgument = function(groupKey, settingKey, argument) diff --git a/files/data/scripts/omw/settings/global.lua b/files/data/scripts/omw/settings/global.lua index 423c38680b..f7356d15c4 100644 --- a/files/data/scripts/omw/settings/global.lua +++ b/files/data/scripts/omw/settings/global.lua @@ -1,6 +1,7 @@ local storage = require('openmw.storage') local common = require('scripts.omw.settings.common') +common.getSection(true, common.groupSectionKey):removeOnExit() return { interfaceName = 'Settings', diff --git a/files/data/scripts/omw/settings/menu.lua b/files/data/scripts/omw/settings/menu.lua index 87c3dc6e6a..59d15ee2cd 100644 --- a/files/data/scripts/omw/settings/menu.lua +++ b/files/data/scripts/omw/settings/menu.lua @@ -7,6 +7,8 @@ local storage = require('openmw.storage') local I = require('openmw.interfaces') local common = require('scripts.omw.settings.common') +-- :reset on startup instead of :removeOnExit +common.getSection(false, common.groupSectionKey):reset() local renderers = {} local function registerRenderer(name, renderFunction) @@ -276,6 +278,9 @@ local function renderPage(page) local groupLayouts = {} for _, pageGroup in ipairs(sortedGroups) do local group = common.getSection(pageGroup.global, common.groupSectionKey):get(pageGroup.key) + if not group then + error(string.format('%s group "%s" was not found', pageGroup.global and 'Global' or 'Player', pageGroup.key)) + end table.insert(groupLayouts, renderGroup(group, pageGroup.global)) end local groupsLayout = { @@ -425,17 +430,16 @@ end local menuGroups = {} local menuPages = {} -local function reset() +local function resetPlayerGroups() for pageKey, page in pairs(groups) do - for groupKey in pairs(page) do - if not menuGroups[groupKey] then + for groupKey, group in pairs(page) do + if not menuGroups[groupKey] and not group.global then page[groupKey] = nil end end if pageOptions[pageKey] then - pageOptions[pageKey].element.destroy() + pageOptions[pageKey].element:destroy() if not menuPages[pageKey] then - pageOptions[pageKey].element.destroy() ui.removeSettingsPage(pageOptions[pageKey]) pageOptions[pageKey] = nil else @@ -483,6 +487,8 @@ local function registerPage(options) ui.registerSettingsPage(pageOptions[page.key]) end +local lastState + return { interfaceName = 'Settings', interface = { @@ -502,15 +508,15 @@ return { updateRendererArgument = common.updateRendererArgument, }, engineHandlers = { - onLoad = common.onLoad, - onSave = common.onSave, onStateChanged = function() - if menu.getState() == menu.STATE.Running then - updateGlobalGroups() - else - reset() + if lastState == menu.STATE.Running then + resetPlayerGroups() end updatePlayerGroups() + if menu.getState() == menu.STATE.Running then + updateGlobalGroups() + end + lastState = menu.getState() end, }, eventHandlers = { From b9afd7245cd0ceea18658c97df4bd19c86994d17 Mon Sep 17 00:00:00 2001 From: uramer Date: Sat, 13 Jan 2024 00:47:08 +0100 Subject: [PATCH 28/39] Create separate UI api tables for menu and player contexts --- apps/openmw/mwlua/inputprocessor.hpp | 16 ++-- apps/openmw/mwlua/uibindings.cpp | 119 ++++++++++++++------------- components/lua_ui/element.cpp | 17 ++-- components/lua_ui/element.hpp | 18 ++-- components/lua_ui/scriptsettings.hpp | 1 - components/lua_ui/util.cpp | 4 +- 6 files changed, 85 insertions(+), 90 deletions(-) diff --git a/apps/openmw/mwlua/inputprocessor.hpp b/apps/openmw/mwlua/inputprocessor.hpp index fe6de5450e..e005183098 100644 --- a/apps/openmw/mwlua/inputprocessor.hpp +++ b/apps/openmw/mwlua/inputprocessor.hpp @@ -58,14 +58,14 @@ namespace MWLua private: Container* mScriptsContainer; - Container::EngineHandlerList mKeyPressHandlers{ "onKeyPress" }; - Container::EngineHandlerList mKeyReleaseHandlers{ "onKeyRelease" }; - Container::EngineHandlerList mControllerButtonPressHandlers{ "onControllerButtonPress" }; - Container::EngineHandlerList mControllerButtonReleaseHandlers{ "onControllerButtonRelease" }; - Container::EngineHandlerList mActionHandlers{ "onInputAction" }; - Container::EngineHandlerList mTouchpadPressed{ "onTouchPress" }; - Container::EngineHandlerList mTouchpadReleased{ "onTouchRelease" }; - Container::EngineHandlerList mTouchpadMoved{ "onTouchMove" }; + typename Container::EngineHandlerList mKeyPressHandlers{ "onKeyPress" }; + typename Container::EngineHandlerList mKeyReleaseHandlers{ "onKeyRelease" }; + typename Container::EngineHandlerList mControllerButtonPressHandlers{ "onControllerButtonPress" }; + typename Container::EngineHandlerList mControllerButtonReleaseHandlers{ "onControllerButtonRelease" }; + typename Container::EngineHandlerList mActionHandlers{ "onInputAction" }; + typename Container::EngineHandlerList mTouchpadPressed{ "onTouchPress" }; + typename Container::EngineHandlerList mTouchpadReleased{ "onTouchRelease" }; + typename Container::EngineHandlerList mTouchpadMoved{ "onTouchMove" }; }; } diff --git a/apps/openmw/mwlua/uibindings.cpp b/apps/openmw/mwlua/uibindings.cpp index 061dc4821a..354fd83ec2 100644 --- a/apps/openmw/mwlua/uibindings.cpp +++ b/apps/openmw/mwlua/uibindings.cpp @@ -89,39 +89,10 @@ namespace MWLua }(); } - sol::table initUserInterfacePackage(const Context& context) + sol::table registerUiApi(const Context& context, bool menu) { - { - sol::state_view& lua = context.mLua->sol(); - if (lua["openmw_ui"] != sol::nil) - return lua["openmw_ui"]; - } - MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); - auto element = context.mLua->sol().new_usertype("Element"); - element[sol::meta_function::to_string] = [](const LuaUi::Element& element) { - std::stringstream res; - res << "UiElement"; - if (element.mLayer != "") - res << "[" << element.mLayer << "]"; - return res.str(); - }; - element["layout"] = sol::property([](LuaUi::Element& element) { return element.mLayout; }, - [](LuaUi::Element& element, const sol::table& layout) { element.mLayout = layout; }); - element["update"] = [luaManager = context.mLuaManager](const std::shared_ptr& element) { - if (element->mDestroy || element->mUpdate) - return; - element->mUpdate = true; - luaManager->addAction([element] { wrapAction(element, [&] { element->update(); }); }, "Update UI"); - }; - element["destroy"] = [luaManager = context.mLuaManager](const std::shared_ptr& element) { - if (element->mDestroy) - return; - element->mDestroy = true; - luaManager->addAction([element] { wrapAction(element, [&] { element->destroy(); }); }, "Destroy UI"); - }; - sol::table api = context.mLua->newTable(); api["_setHudVisibility"] = [luaManager = context.mLuaManager](bool state) { luaManager->addAction([state] { MWBase::Environment::get().getWindowManager()->setHudVisibility(state); }); @@ -155,36 +126,20 @@ namespace MWLua } }; api["content"] = LuaUi::loadContentConstructor(context.mLua); - api["create"] = [context](const sol::table& layout) { - auto element - = context.mIsMenu ? LuaUi::Element::makeMenuElement(layout) : LuaUi::Element::makeGameElement(layout); - context.mLuaManager->addAction([element] { wrapAction(element, [&] { element->create(); }); }, "Create UI"); + + api["create"] = [luaManager = context.mLuaManager, menu](const sol::table& layout) { + auto element = LuaUi::Element::make(layout, menu); + luaManager->addAction([element] { wrapAction(element, [&] { element->create(); }); }, "Create UI"); return element; }; - api["updateAll"] = [context]() { - if (context.mIsMenu) - { - LuaUi::Element::forEachMenuElement([](LuaUi::Element* e) { e->mUpdate = true; }); - context.mLuaManager->addAction( - []() { LuaUi::Element::forEachMenuElement([](LuaUi::Element* e) { e->update(); }); }, - "Update all menu UI elements"); - } - else - { - LuaUi::Element::forEachGameElement([](LuaUi::Element* e) { e->mUpdate = true; }); - context.mLuaManager->addAction( - []() { LuaUi::Element::forEachGameElement([](LuaUi::Element* e) { e->update(); }); }, - "Update all game UI elements"); - } + + api["updateAll"] = [luaManager = context.mLuaManager, menu]() { + LuaUi::Element::forEach(menu, [](LuaUi::Element* e) { e->mUpdate = true; }); + luaManager->addAction([menu]() { LuaUi::Element::forEach(menu, [](LuaUi::Element* e) { e->update(); }); }, + "Update all menu UI elements"); }; api["_getMenuTransparency"] = []() -> float { return Settings::gui().mMenuTransparency; }; - auto uiLayer = context.mLua->sol().new_usertype("UiLayer"); - uiLayer["name"] = sol::property([](LuaUi::Layer& self) { return self.name(); }); - uiLayer["size"] = sol::property([](LuaUi::Layer& self) { return self.size(); }); - uiLayer[sol::meta_function::to_string] - = [](LuaUi::Layer& self) { return Misc::StringUtils::format("UiLayer(%s)", self.name()); }; - sol::table layersTable = context.mLua->newTable(); layersTable["indexOf"] = [](std::string_view name) -> sol::optional { size_t index = LuaUi::Layer::indexOf(name); @@ -247,7 +202,7 @@ namespace MWLua { "Center", LuaUi::Alignment::Center }, { "End", LuaUi::Alignment::End } })); api["registerSettingsPage"] = &LuaUi::registerSettingsPage; - api["removeSettingsPage"] = &LuaUi::registerSettingsPage; + api["removeSettingsPage"] = &LuaUi::removeSettingsPage; api["texture"] = [luaManager = context.mLuaManager](const sol::table& options) { LuaUi::TextureData data; @@ -325,8 +280,56 @@ namespace MWLua // TODO // api["_showMouseCursor"] = [](bool) {}; + return api; + } + + sol::table initUserInterfacePackage(const Context& context) + { + std::string_view menuCache = "openmw_ui_menu"; + std::string_view gameCache = "openmw_ui_game"; + std::string_view cacheKey = context.mIsMenu ? menuCache : gameCache; + { + sol::state_view& lua = context.mLua->sol(); + if (lua[cacheKey] != sol::nil) + return lua[cacheKey]; + } + + auto element = context.mLua->sol().new_usertype("UiElement"); + element[sol::meta_function::to_string] = [](const LuaUi::Element& element) { + std::stringstream res; + res << "UiElement"; + if (element.mLayer != "") + res << "[" << element.mLayer << "]"; + return res.str(); + }; + element["layout"] = sol::property([](const LuaUi::Element& element) { return element.mLayout; }, + [](LuaUi::Element& element, const sol::table& layout) { element.mLayout = layout; }); + element["update"] = [luaManager = context.mLuaManager](const std::shared_ptr& element) { + if (element->mDestroy || element->mUpdate) + return; + element->mUpdate = true; + luaManager->addAction([element] { wrapAction(element, [&] { element->update(); }); }, "Update UI"); + }; + element["destroy"] = [luaManager = context.mLuaManager](const std::shared_ptr& element) { + if (element->mDestroy) + return; + element->mDestroy = true; + luaManager->addAction( + [element] { wrapAction(element, [&] { LuaUi::Element::erase(element.get()); }); }, "Destroy UI"); + }; + + auto uiLayer = context.mLua->sol().new_usertype("UiLayer"); + uiLayer["name"] = sol::readonly_property([](LuaUi::Layer& self) { return self.name(); }); + uiLayer["size"] = sol::readonly_property([](LuaUi::Layer& self) { return self.size(); }); + uiLayer[sol::meta_function::to_string] + = [](LuaUi::Layer& self) { return Misc::StringUtils::format("UiLayer(%s)", self.name()); }; + + sol::table menuApi = registerUiApi(context, true); + sol::table gameApi = registerUiApi(context, false); + sol::state_view& lua = context.mLua->sol(); - lua["openmw_ui"] = LuaUtil::makeReadOnly(api); - return lua["openmw_ui"]; + lua[menuCache] = LuaUtil::makeReadOnly(menuApi); + lua[gameCache] = LuaUtil::makeReadOnly(gameApi); + return lua[cacheKey]; } } diff --git a/components/lua_ui/element.cpp b/components/lua_ui/element.cpp index 9d124d2a0e..da39cca1b8 100644 --- a/components/lua_ui/element.cpp +++ b/components/lua_ui/element.cpp @@ -240,8 +240,8 @@ namespace LuaUi } } - std::map> Element::sGameElements; std::map> Element::sMenuElements; + std::map> Element::sGameElements; Element::Element(sol::table layout) : mRoot(nullptr) @@ -252,18 +252,19 @@ namespace LuaUi { } - std::shared_ptr Element::makeGameElement(sol::table layout) + std::shared_ptr Element::make(sol::table layout, bool menu) { std::shared_ptr ptr(new Element(std::move(layout))); - sGameElements[ptr.get()] = ptr; + auto& container = menu ? sMenuElements : sGameElements; + container[ptr.get()] = ptr; return ptr; } - std::shared_ptr Element::makeMenuElement(sol::table layout) + void Element::erase(Element* element) { - std::shared_ptr ptr(new Element(std::move(layout))); - sMenuElements[ptr.get()] = ptr; - return ptr; + element->destroy(); + sMenuElements.erase(element); + sGameElements.erase(element); } void Element::create() @@ -311,7 +312,5 @@ namespace LuaUi mRoot = nullptr; mLayout = sol::make_object(mLayout.lua_state(), sol::nil); } - sGameElements.erase(this); - sMenuElements.erase(this); } } diff --git a/components/lua_ui/element.hpp b/components/lua_ui/element.hpp index 0446f448b6..4398a769df 100644 --- a/components/lua_ui/element.hpp +++ b/components/lua_ui/element.hpp @@ -7,21 +7,15 @@ namespace LuaUi { struct Element { - static std::shared_ptr makeGameElement(sol::table layout); - static std::shared_ptr makeMenuElement(sol::table layout); + static std::shared_ptr make(sol::table layout, bool menu); + static void erase(Element* element); template - static void forEachGameElement(Callback callback) + static void forEach(bool menu, Callback callback) { - for (auto& [e, _] : sGameElements) - callback(e); - } - - template - static void forEachMenuElement(Callback callback) - { - for (auto& [e, _] : sMenuElements) - callback(e); + auto& container = menu ? sMenuElements : sGameElements; + for (auto& [_, element] : container) + callback(element.get()); } WidgetExtension* mRoot; diff --git a/components/lua_ui/scriptsettings.hpp b/components/lua_ui/scriptsettings.hpp index ab68bde839..94ed065bbc 100644 --- a/components/lua_ui/scriptsettings.hpp +++ b/components/lua_ui/scriptsettings.hpp @@ -3,7 +3,6 @@ #include #include -#include namespace LuaUi { diff --git a/components/lua_ui/util.cpp b/components/lua_ui/util.cpp index fe47de3b1d..8dfda3d4cb 100644 --- a/components/lua_ui/util.cpp +++ b/components/lua_ui/util.cpp @@ -47,12 +47,12 @@ namespace LuaUi void clearGameInterface() { while (!Element::sGameElements.empty()) - Element::sGameElements.begin()->second->destroy(); + Element::erase(Element::sGameElements.begin()->second.get()); } void clearMenuInterface() { while (!Element::sMenuElements.empty()) - Element::sMenuElements.begin()->second->destroy(); + Element::erase(Element::sMenuElements.begin()->second.get()); } } From 94d782c4bee5a11492648bad4e7761add3518934 Mon Sep 17 00:00:00 2001 From: uramer Date: Sun, 14 Jan 2024 13:38:35 +0100 Subject: [PATCH 29/39] Fix doc typos and add menu package to necessary lists --- docs/source/reference/lua-scripting/api.rst | 1 + docs/source/reference/lua-scripting/engine_handlers.rst | 6 +++++- docs/source/reference/lua-scripting/openmw_menu.rst | 2 +- docs/source/reference/lua-scripting/tables/packages.rst | 2 ++ files/data/scripts/omw/settings/player.lua | 2 +- files/lua_api/openmw/menu.lua | 5 +++++ 6 files changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/source/reference/lua-scripting/api.rst b/docs/source/reference/lua-scripting/api.rst index 6d27db0515..857374b2b7 100644 --- a/docs/source/reference/lua-scripting/api.rst +++ b/docs/source/reference/lua-scripting/api.rst @@ -27,6 +27,7 @@ Lua API reference openmw_camera openmw_postprocessing openmw_debug + openmw_menu openmw_aux_calendar openmw_aux_util openmw_aux_time diff --git a/docs/source/reference/lua-scripting/engine_handlers.rst b/docs/source/reference/lua-scripting/engine_handlers.rst index bcadfeb295..754a63b314 100644 --- a/docs/source/reference/lua-scripting/engine_handlers.rst +++ b/docs/source/reference/lua-scripting/engine_handlers.rst @@ -9,6 +9,7 @@ Engine handler is a function defined by a script, that can be called by the engi .. list-table:: :widths: 20 80 + * - onInterfaceOverride(base) - | Called if the current script has an interface and overrides an interface | (``base``) of another script. @@ -87,6 +88,7 @@ Engine handler is a function defined by a script, that can be called by the engi .. list-table:: :widths: 20 80 + * - onFrame(dt) - | Called every frame (even if the game is paused) right after | processing user input. Use it only for latency-critical stuff @@ -96,7 +98,7 @@ Engine handler is a function defined by a script, that can be called by the engi - | `Key `_ is pressed. | Usage example: | ``if key.symbol == 'z' and key.withShift then ...`` - * - onKeyRelease(key) + * - onKeyRelease(key) - | `Key `_ is released. | Usage example: | ``if key.symbol == 'z' and key.withShift then ...`` @@ -131,6 +133,7 @@ Engine handler is a function defined by a script, that can be called by the engi .. list-table:: :widths: 20 80 + * - onKeyPress(key) - | `Key `_ is pressed. | Usage example: @@ -142,6 +145,7 @@ Engine handler is a function defined by a script, that can be called by the engi .. list-table:: :widths: 20 80 + * - onStateChanged() - | Called whenever the current game changes | (i. e. the result of `getState `_ changes) diff --git a/docs/source/reference/lua-scripting/openmw_menu.rst b/docs/source/reference/lua-scripting/openmw_menu.rst index 587e4337e0..ae1803a4f2 100644 --- a/docs/source/reference/lua-scripting/openmw_menu.rst +++ b/docs/source/reference/lua-scripting/openmw_menu.rst @@ -4,4 +4,4 @@ Package openmw.menu .. include:: version.rst .. raw:: html - :file: generated_html/openmw_ambient.html + :file: generated_html/openmw_menu.html diff --git a/docs/source/reference/lua-scripting/tables/packages.rst b/docs/source/reference/lua-scripting/tables/packages.rst index 67709bbf7b..667b91ef63 100644 --- a/docs/source/reference/lua-scripting/tables/packages.rst +++ b/docs/source/reference/lua-scripting/tables/packages.rst @@ -29,6 +29,8 @@ +------------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`openmw.ui ` | by player scripts | | Controls :ref:`user interface `. | +------------------------------------------------------------+--------------------+---------------------------------------------------------------+ +|:ref:`openmw.menu ` | by menu scripts | | Main menu functionality, such as managing game saves | ++------------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`openmw.camera ` | by player scripts | | Controls camera. | +------------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`openmw.postprocessing `| by player scripts | | Controls post-process shaders. | diff --git a/files/data/scripts/omw/settings/player.lua b/files/data/scripts/omw/settings/player.lua index 9f397c923d..4ec61a73c7 100644 --- a/files/data/scripts/omw/settings/player.lua +++ b/files/data/scripts/omw/settings/player.lua @@ -91,7 +91,7 @@ return { registerPage = registerPage, --- -- @function [parent=#Settings] registerRenderer Register a renderer, - -- only avaialable in menu scripts (DEPRECATED in player scripts) + -- only available in menu scripts (DEPRECATED in player scripts) -- @param #string key -- @param #function renderer A renderer function, receives setting's value, -- a function to change it and an argument from the setting options diff --git a/files/lua_api/openmw/menu.lua b/files/lua_api/openmw/menu.lua index c1a1a65a62..3e2e02954a 100644 --- a/files/lua_api/openmw/menu.lua +++ b/files/lua_api/openmw/menu.lua @@ -9,6 +9,9 @@ -- @field [parent=#STATE] Running -- @field [parent=#STATE] Ended +--- +-- All possible game states returned by @{#menu.getState} +-- @field [parent=#menu] #STATE STATE --- -- Current game state @@ -63,3 +66,5 @@ --- -- Exit the game -- @function [parent=#menu] quit + +return nil From 0a2adfee1667a5239916a9d18b376872557fa327 Mon Sep 17 00:00:00 2001 From: uramer Date: Sun, 14 Jan 2024 16:40:46 +0100 Subject: [PATCH 30/39] SaveInfo.timePlayed field --- apps/openmw/mwlua/menuscripts.cpp | 1 + files/lua_api/openmw/menu.lua | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwlua/menuscripts.cpp b/apps/openmw/mwlua/menuscripts.cpp index 16e57961b1..e4a3962912 100644 --- a/apps/openmw/mwlua/menuscripts.cpp +++ b/apps/openmw/mwlua/menuscripts.cpp @@ -85,6 +85,7 @@ namespace MWLua slotInfo["description"] = slot.mProfile.mDescription; slotInfo["playerName"] = slot.mProfile.mPlayerName; slotInfo["playerLevel"] = slot.mProfile.mPlayerLevel; + slotInfo["timePlayed"] = slot.mProfile.mTimePlayed; sol::table contentFiles(lua, sol::create); for (size_t i = 0; i < slot.mProfile.mContentFiles.size(); ++i) contentFiles[i + 1] = Misc::StringUtils::lowerCase(slot.mProfile.mContentFiles[i]); diff --git a/files/lua_api/openmw/menu.lua b/files/lua_api/openmw/menu.lua index 3e2e02954a..4deb5d00a3 100644 --- a/files/lua_api/openmw/menu.lua +++ b/files/lua_api/openmw/menu.lua @@ -50,6 +50,7 @@ -- @field #string description -- @field #string playerName -- @field #string playerLevel +-- @field #number timePlayed Gameplay time for this saved game. Note: available even with [time played](../modding/settings/saves.html#timeplayed) turned off -- @field #list<#string> contentFiles --- @@ -59,9 +60,9 @@ -- @return #list<#SaveInfo> --- --- List of all available saves +-- List of all available saves, grouped by directory -- @function [parent=#menu] getAllSaves --- @return #list<#SaveInfo> +-- @return #map<#string, #list<#SaveInfo>> --- -- Exit the game From ad5d594c28ad4dd4cdbebaefd662f1c3285523da Mon Sep 17 00:00:00 2001 From: uramer Date: Sat, 27 Jan 2024 14:47:22 +0100 Subject: [PATCH 31/39] Let menu scripts clean up before loading a game --- apps/openmw/mwstate/statemanagerimp.cpp | 4 ++++ files/data/scripts/omw/input/settings.lua | 1 + files/data/scripts/omw/settings/common.lua | 1 - files/data/scripts/omw/settings/menu.lua | 13 +++++++------ 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index 4c4d0de7a1..e80debb998 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -421,6 +421,10 @@ void MWState::StateManager::loadGame(const Character* character, const std::file { try { + // let menu scripts do cleanup + mState = State_Ended; + MWBase::Environment::get().getLuaManager()->gameEnded(); + cleanup(); Log(Debug::Info) << "Reading save file " << filepath.filename(); diff --git a/files/data/scripts/omw/input/settings.lua b/files/data/scripts/omw/input/settings.lua index 42fec91f82..6c1b857131 100644 --- a/files/data/scripts/omw/input/settings.lua +++ b/files/data/scripts/omw/input/settings.lua @@ -47,6 +47,7 @@ local recording = nil I.Settings.registerRenderer('inputBinding', function(id, set, arg) if type(id) ~= 'string' then error('inputBinding: must have a string default value') end + if not arg then error('inputBinding: argument with "key" and "type" is required') end if not arg.type then error('inputBinding: type argument is required') end if not arg.key then error('inputBinding: key argument is required') end local info = input.actions[arg.key] or input.triggers[arg.key] diff --git a/files/data/scripts/omw/settings/common.lua b/files/data/scripts/omw/settings/common.lua index 1d62a54dc4..7ca6628b08 100644 --- a/files/data/scripts/omw/settings/common.lua +++ b/files/data/scripts/omw/settings/common.lua @@ -90,7 +90,6 @@ local function registerGroup(options) } local valueSection = contextSection(options.key) local argumentSection = contextSection(options.key .. argumentSectionPostfix) - argumentSection:removeOnExit() for i, opt in ipairs(options.settings) do local setting = registerSetting(opt) setting.order = i diff --git a/files/data/scripts/omw/settings/menu.lua b/files/data/scripts/omw/settings/menu.lua index 59d15ee2cd..017a48deb2 100644 --- a/files/data/scripts/omw/settings/menu.lua +++ b/files/data/scripts/omw/settings/menu.lua @@ -354,6 +354,8 @@ end local function onGroupRegistered(global, key) local group = common.getSection(global, common.groupSectionKey):get(key) + if not group then return end + groups[group.page] = groups[group.page] or {} local pageGroup = { key = group.key, @@ -364,6 +366,8 @@ local function onGroupRegistered(global, key) if not groups[group.page][pageGroup.key] then common.getSection(global, group.key):subscribe(onSettingChanged(global)) common.getArgumentSection(global, group.key):subscribe(async:callback(function(_, settingKey) + if settingKey == nil then return end + local group = common.getSection(global, common.groupSectionKey):get(group.key) if not group or not pageOptions[group.page] then return end @@ -431,10 +435,12 @@ local menuGroups = {} local menuPages = {} local function resetPlayerGroups() + local settingGroupsSection = storage.playerSection(common.groupSectionKey) for pageKey, page in pairs(groups) do for groupKey, group in pairs(page) do if not menuGroups[groupKey] and not group.global then page[groupKey] = nil + settingGroupsSection:set(groupKey, nil) end end if pageOptions[pageKey] then @@ -487,8 +493,6 @@ local function registerPage(options) ui.registerSettingsPage(pageOptions[page.key]) end -local lastState - return { interfaceName = 'Settings', interface = { @@ -509,14 +513,11 @@ return { }, engineHandlers = { onStateChanged = function() - if lastState == menu.STATE.Running then - resetPlayerGroups() - end + resetPlayerGroups() updatePlayerGroups() if menu.getState() == menu.STATE.Running then updateGlobalGroups() end - lastState = menu.getState() end, }, eventHandlers = { From d9f8c5c3e8b0bf852bdb2e4cb9bf7a6ce029e7a9 Mon Sep 17 00:00:00 2001 From: uramer Date: Sat, 27 Jan 2024 15:03:34 +0100 Subject: [PATCH 32/39] Fix menu setting page key set --- files/data/scripts/omw/settings/menu.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/files/data/scripts/omw/settings/menu.lua b/files/data/scripts/omw/settings/menu.lua index 017a48deb2..9b161df588 100644 --- a/files/data/scripts/omw/settings/menu.lua +++ b/files/data/scripts/omw/settings/menu.lua @@ -499,7 +499,7 @@ return { version = 1, registerPage = function(options) registerPage(options) - menuPages[options] = true + menuPages[options.key] = true end, registerRenderer = registerRenderer, registerGroup = function(options) @@ -514,8 +514,8 @@ return { engineHandlers = { onStateChanged = function() resetPlayerGroups() - updatePlayerGroups() if menu.getState() == menu.STATE.Running then + updatePlayerGroups() updateGlobalGroups() end end, From 2008f35e5794781aa2a2885e0f454c912edcf8c3 Mon Sep 17 00:00:00 2001 From: uramer Date: Sat, 27 Jan 2024 19:09:26 +0100 Subject: [PATCH 33/39] Don't reset player setting groups right after game load, refactor update group functions --- files/data/scripts/omw/settings/menu.lua | 42 +++++++++--------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/files/data/scripts/omw/settings/menu.lua b/files/data/scripts/omw/settings/menu.lua index 9b161df588..6d6d07a20b 100644 --- a/files/data/scripts/omw/settings/menu.lua +++ b/files/data/scripts/omw/settings/menu.lua @@ -396,51 +396,38 @@ local function onGroupRegistered(global, key) end end - -local function updatePlayerGroups() - local playerGroups = storage.playerSection(common.groupSectionKey) - for groupKey in pairs(playerGroups:asTable()) do - onGroupRegistered(false, groupKey) +local function updateGroups(global) + local groupSection = common.getSection(global, common.groupSectionKey) + for groupKey in pairs(groupSection:asTable()) do + onGroupRegistered(global, groupKey) end - playerGroups:subscribe(async:callback(function(_, key) + groupSection:subscribe(async:callback(function(_, key) if key then - onGroupRegistered(false, key) + onGroupRegistered(global, key) else - for groupKey in pairs(playerGroups:asTable()) do - onGroupRegistered(false, groupKey) + for groupKey in pairs(groupSection:asTable()) do + onGroupRegistered(global, groupKey) end end end)) end +local updatePlayerGroups = function() updateGroups(false) end updatePlayerGroups() -local function updateGlobalGroups() - local globalGroups = storage.globalSection(common.groupSectionKey) - for groupKey in pairs(globalGroups:asTable()) do - onGroupRegistered(true, groupKey) - end - globalGroups:subscribe(async:callback(function(_, key) - if key then - onGroupRegistered(true, key) - else - for groupKey in pairs(globalGroups:asTable()) do - onGroupRegistered(true, groupKey) - end - end - end)) -end +local updateGlobalGroups = function() updateGroups(true) end local menuGroups = {} local menuPages = {} local function resetPlayerGroups() - local settingGroupsSection = storage.playerSection(common.groupSectionKey) + print('MENU reset player groups') + local playerGroupsSection = storage.playerSection(common.groupSectionKey) for pageKey, page in pairs(groups) do for groupKey, group in pairs(page) do if not menuGroups[groupKey] and not group.global then page[groupKey] = nil - settingGroupsSection:set(groupKey, nil) + playerGroupsSection:set(groupKey, nil) end end if pageOptions[pageKey] then @@ -513,10 +500,11 @@ return { }, engineHandlers = { onStateChanged = function() - resetPlayerGroups() if menu.getState() == menu.STATE.Running then updatePlayerGroups() updateGlobalGroups() + else + resetPlayerGroups() end end, }, From ad8a05e2a1f1856f6d5bc97026dc030dbf432e66 Mon Sep 17 00:00:00 2001 From: uramer Date: Tue, 30 Jan 2024 18:58:15 +0100 Subject: [PATCH 34/39] Trigger a game ended state handler before loading to allow menu scripts to do cleanup --- apps/openmw/mwstate/statemanagerimp.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index 9f0f8d2928..63190f72c3 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -449,9 +449,12 @@ void MWState::StateManager::loadGame(const Character* character, const std::file { try { - // let menu scripts do cleanup - mState = State_Ended; - MWBase::Environment::get().getLuaManager()->gameEnded(); + if (mState != State_Ended) + { + // let menu scripts do cleanup + mState = State_Ended; + MWBase::Environment::get().getLuaManager()->gameEnded(); + } cleanup(); From 76915ce6e9a9c73ee71a11b47ea304df2a4ec428 Mon Sep 17 00:00:00 2001 From: uramer Date: Tue, 30 Jan 2024 18:58:34 +0100 Subject: [PATCH 35/39] Queue auto started scripts until next update --- apps/openmw/mwlua/luamanagerimp.cpp | 8 ++++++-- apps/openmw/mwlua/luamanagerimp.hpp | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index f695bd294e..15a172388d 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -166,6 +166,10 @@ namespace MWLua mObjectLists.update(); + for (auto scripts : mQueuedAutoStartedScripts) + scripts->addAutoStartedScripts(); + mQueuedAutoStartedScripts.clear(); + std::erase_if(mActiveLocalScripts, [](const LocalScripts* l) { return l->getPtrOrEmpty().isEmpty() || l->getPtrOrEmpty().mRef->isDeleted(); }); @@ -343,7 +347,7 @@ namespace MWLua if (!localScripts) { localScripts = createLocalScripts(ptr); - localScripts->addAutoStartedScripts(); + mQueuedAutoStartedScripts.push_back(localScripts); } mActiveLocalScripts.insert(localScripts); mEngineEvents.addToQueue(EngineEvents::OnActive{ getId(ptr) }); @@ -459,7 +463,7 @@ namespace MWLua if (!autoStartConf.empty()) { localScripts = createLocalScripts(ptr, std::move(autoStartConf)); - localScripts->addAutoStartedScripts(); // TODO: put to a queue and apply on next `update()` + mQueuedAutoStartedScripts.push_back(localScripts); } } if (localScripts) diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index e82c503c3a..22745fb90a 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -176,6 +176,7 @@ namespace MWLua MenuScripts mMenuScripts{ &mLua }; GlobalScripts mGlobalScripts{ &mLua }; std::set mActiveLocalScripts; + std::vector mQueuedAutoStartedScripts; ObjectLists mObjectLists; MWWorld::Ptr mPlayer; From 47d5868e2ccd208287a3e560c9fa7bb021db00f5 Mon Sep 17 00:00:00 2001 From: uramer Date: Tue, 30 Jan 2024 22:05:41 +0100 Subject: [PATCH 36/39] creationTime field in save info --- apps/openmw/mwlua/menuscripts.cpp | 7 +++++++ files/lua_api/openmw/menu.lua | 1 + 2 files changed, 8 insertions(+) diff --git a/apps/openmw/mwlua/menuscripts.cpp b/apps/openmw/mwlua/menuscripts.cpp index e4a3962912..033e56ac97 100644 --- a/apps/openmw/mwlua/menuscripts.cpp +++ b/apps/openmw/mwlua/menuscripts.cpp @@ -89,6 +89,13 @@ namespace MWLua sol::table contentFiles(lua, sol::create); for (size_t i = 0; i < slot.mProfile.mContentFiles.size(); ++i) contentFiles[i + 1] = Misc::StringUtils::lowerCase(slot.mProfile.mContentFiles[i]); + + { + auto system_time = std::chrono::system_clock::now() + - (std::filesystem::file_time_type::clock::now() - slot.mTimeStamp); + slotInfo["creationTime"] = std::chrono::duration(system_time.time_since_epoch()).count(); + } + slotInfo["contentFiles"] = contentFiles; saves[slot.mPath.filename().string()] = slotInfo; } diff --git a/files/lua_api/openmw/menu.lua b/files/lua_api/openmw/menu.lua index 4deb5d00a3..b443a983c9 100644 --- a/files/lua_api/openmw/menu.lua +++ b/files/lua_api/openmw/menu.lua @@ -51,6 +51,7 @@ -- @field #string playerName -- @field #string playerLevel -- @field #number timePlayed Gameplay time for this saved game. Note: available even with [time played](../modding/settings/saves.html#timeplayed) turned off +-- @field #number creationTime Time at which the game was saved, as a timestamp in seconds. Can be passed as the second argument to `os.data`. -- @field #list<#string> contentFiles --- From 130b6349311aa67299536fa976e89be24072b10e Mon Sep 17 00:00:00 2001 From: uramer Date: Thu, 1 Feb 2024 17:52:41 +0100 Subject: [PATCH 37/39] Changelog entries --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b014ca0389..b680fd84f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -170,9 +170,11 @@ Feature #7618: Show the player character's health in the save details Feature #7625: Add some missing console error outputs Feature #7634: Support NiParticleBomb + Feature #7648: Lua Save game API Feature #7652: Sort inactive post processing shaders list properly Feature #7698: Implement sAbsorb, sDamage, sDrain, sFortify and sRestore Feature #7709: Improve resolution selection in Launcher + Feature #7805: Lua Menu context Task #5896: Do not use deprecated MyGUI properties Task #6624: Drop support for saves made prior to 0.45 Task #7113: Move from std::atoi to std::from_char From 5bd641d2dd94443bf24654f429fbfcb0f8faad59 Mon Sep 17 00:00:00 2001 From: Anton Uramer Date: Fri, 2 Feb 2024 12:53:03 +0100 Subject: [PATCH 38/39] Lua API Revision 52 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 28109bd01b..3ea891b56d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,7 +72,7 @@ message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) set(OPENMW_VERSION_MINOR 49) set(OPENMW_VERSION_RELEASE 0) -set(OPENMW_LUA_API_REVISION 51) +set(OPENMW_LUA_API_REVISION 52) set(OPENMW_POSTPROCESSING_API_REVISION 1) set(OPENMW_VERSION_COMMITHASH "") From 784459a652842341a91b74e1d386fb1ecdd87da4 Mon Sep 17 00:00:00 2001 From: uramer Date: Fri, 2 Feb 2024 22:07:58 +0100 Subject: [PATCH 39/39] Clean up the cleanup code --- apps/openmw/mwstate/statemanagerimp.cpp | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index 63190f72c3..e02b6053ad 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -68,6 +68,8 @@ void MWState::StateManager::cleanup(bool force) mLastSavegame.clear(); MWMechanics::CreatureStats::cleanup(); + + endGame(); } MWBase::Environment::get().getLuaManager()->clear(); } @@ -449,13 +451,6 @@ void MWState::StateManager::loadGame(const Character* character, const std::file { try { - if (mState != State_Ended) - { - // let menu scripts do cleanup - mState = State_Ended; - MWBase::Environment::get().getLuaManager()->gameEnded(); - } - cleanup(); Log(Debug::Info) << "Reading save file " << filepath.filename();