From 1dd7a1525558b733fccc10ba1cca1a46b8e30d4a Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Tue, 5 Sep 2023 22:45:38 +0200 Subject: [PATCH] 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, } } -