From 87eacf774afc2a9d2696c519f03772e05b719d0d Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Sun, 9 Jul 2023 08:42:09 +0200 Subject: [PATCH] Control GUI modes from Lua --- apps/openmw/mwbase/luamanager.hpp | 5 + apps/openmw/mwbase/windowmanager.hpp | 6 + apps/openmw/mwgui/alchemywindow.hpp | 2 + apps/openmw/mwgui/bookwindow.hpp | 2 + apps/openmw/mwgui/companionwindow.hpp | 2 + apps/openmw/mwgui/container.hpp | 2 + apps/openmw/mwgui/dialogue.hpp | 2 + apps/openmw/mwgui/enchantingdialog.hpp | 2 + apps/openmw/mwgui/inventorywindow.hpp | 2 + apps/openmw/mwgui/jailscreen.hpp | 2 + apps/openmw/mwgui/journalwindow.hpp | 2 + apps/openmw/mwgui/levelupdialog.hpp | 2 + apps/openmw/mwgui/mapwindow.hpp | 2 + apps/openmw/mwgui/merchantrepair.hpp | 2 + apps/openmw/mwgui/quickkeysmenu.hpp | 2 + apps/openmw/mwgui/recharge.hpp | 2 + apps/openmw/mwgui/repair.hpp | 2 + apps/openmw/mwgui/scrollwindow.hpp | 2 + apps/openmw/mwgui/spellbuyingwindow.hpp | 2 + apps/openmw/mwgui/spellcreationdialog.hpp | 2 + apps/openmw/mwgui/spellwindow.hpp | 2 + apps/openmw/mwgui/statswindow.hpp | 2 + apps/openmw/mwgui/tradewindow.hpp | 2 + apps/openmw/mwgui/trainingwindow.hpp | 2 + apps/openmw/mwgui/travelwindow.hpp | 2 + apps/openmw/mwgui/waitdialog.hpp | 2 + apps/openmw/mwgui/windowbase.cpp | 1 + apps/openmw/mwgui/windowbase.hpp | 5 + apps/openmw/mwgui/windowmanagerimp.cpp | 54 +++++ apps/openmw/mwgui/windowmanagerimp.hpp | 9 + apps/openmw/mwlua/luabindings.cpp | 2 +- apps/openmw/mwlua/luamanagerimp.cpp | 9 + apps/openmw/mwlua/luamanagerimp.hpp | 1 + apps/openmw/mwlua/playerscripts.hpp | 12 +- apps/openmw/mwlua/uibindings.cpp | 117 ++++++++++- .../lua/test_scriptscontainer.cpp | 3 +- components/lua/scriptscontainer.cpp | 4 - docs/source/luadoc_data_paths.sh | 1 + docs/source/reference/lua-scripting/api.rst | 5 + .../source/reference/lua-scripting/events.rst | 16 ++ .../reference/lua-scripting/interface_ui.rst | 6 + .../reference/lua-scripting/overview.rst | 4 + files/data/CMakeLists.txt | 1 + files/data/builtin.omwscripts | 3 + files/data/scripts/omw/ui.lua | 187 ++++++++++++++++++ files/lua_api/openmw/interfaces.lua | 3 + 46 files changed, 487 insertions(+), 15 deletions(-) create mode 100644 docs/source/reference/lua-scripting/interface_ui.rst create mode 100644 files/data/scripts/omw/ui.lua diff --git a/apps/openmw/mwbase/luamanager.hpp b/apps/openmw/mwbase/luamanager.hpp index 5aef1763f0..12a894d343 100644 --- a/apps/openmw/mwbase/luamanager.hpp +++ b/apps/openmw/mwbase/luamanager.hpp @@ -7,6 +7,7 @@ #include +#include "../mwgui/mode.hpp" #include namespace MWWorld @@ -52,6 +53,10 @@ namespace MWBase virtual void objectActivated(const MWWorld::Ptr& object, const MWWorld::Ptr& actor) = 0; virtual void exteriorCreated(MWWorld::CellStore& cell) = 0; virtual void questUpdated(const ESM::RefId& questId, int stage) = 0; + + // `arg` is either forwarded from MWGui::pushGuiMode or empty + virtual void uiModeChanged(const MWWorld::Ptr& arg) = 0; + // TODO: notify LuaManager about other events // virtual void objectOnHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, // const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) = 0; diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index 3412e7e701..7d5521da80 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -378,6 +378,12 @@ namespace MWBase /// Same as viewer->getCamera()->getCullMask(), provided for consistency. virtual uint32_t getCullMask() = 0; + + // Used in Lua bindings + virtual const std::vector& getGuiModeStack() const = 0; + virtual void setDisabledByLua(std::string_view windowId, bool disabled) = 0; + virtual std::vector getAllWindowIds() const = 0; + virtual std::vector getAllowedWindowIds(MWGui::GuiMode mode) const = 0; }; } diff --git a/apps/openmw/mwgui/alchemywindow.hpp b/apps/openmw/mwgui/alchemywindow.hpp index 681de0e316..39ea5ec9b3 100644 --- a/apps/openmw/mwgui/alchemywindow.hpp +++ b/apps/openmw/mwgui/alchemywindow.hpp @@ -30,6 +30,8 @@ namespace MWGui void onResChange(int, int) override { center(); } + std::string_view getWindowIdForLua() const override { return "Alchemy"; } + private: static const float sCountChangeInitialPause; // in seconds static const float sCountChangeInterval; // in seconds diff --git a/apps/openmw/mwgui/bookwindow.hpp b/apps/openmw/mwgui/bookwindow.hpp index bf210c0b12..5a3dfdf584 100644 --- a/apps/openmw/mwgui/bookwindow.hpp +++ b/apps/openmw/mwgui/bookwindow.hpp @@ -19,6 +19,8 @@ namespace MWGui void onResChange(int, int) override { center(); } + std::string_view getWindowIdForLua() const override { return "Book"; } + protected: void onNextPageButtonClicked(MyGUI::Widget* sender); void onPrevPageButtonClicked(MyGUI::Widget* sender); diff --git a/apps/openmw/mwgui/companionwindow.hpp b/apps/openmw/mwgui/companionwindow.hpp index 40fac26eb5..c85044b472 100644 --- a/apps/openmw/mwgui/companionwindow.hpp +++ b/apps/openmw/mwgui/companionwindow.hpp @@ -30,6 +30,8 @@ namespace MWGui void onFrame(float dt) override; void clear() override { resetReference(); } + std::string_view getWindowIdForLua() const override { return "Companion"; } + private: ItemView* mItemView; SortFilterItemModel* mSortModel; diff --git a/apps/openmw/mwgui/container.hpp b/apps/openmw/mwgui/container.hpp index a2c2587222..555fa8e1ae 100644 --- a/apps/openmw/mwgui/container.hpp +++ b/apps/openmw/mwgui/container.hpp @@ -38,6 +38,8 @@ namespace MWGui void treatNextOpenAsLoot() { mTreatNextOpenAsLoot = true; } + std::string_view getWindowIdForLua() const override { return "Container"; } + private: DragAndDrop* mDragAndDrop; diff --git a/apps/openmw/mwgui/dialogue.hpp b/apps/openmw/mwgui/dialogue.hpp index 800b67c347..1b79cadca5 100644 --- a/apps/openmw/mwgui/dialogue.hpp +++ b/apps/openmw/mwgui/dialogue.hpp @@ -165,6 +165,8 @@ namespace MWGui void onClose() override; + std::string_view getWindowIdForLua() const override { return "Dialogue"; } + protected: void updateTopicsPane(); bool isCompanion(const MWWorld::Ptr& actor); diff --git a/apps/openmw/mwgui/enchantingdialog.hpp b/apps/openmw/mwgui/enchantingdialog.hpp index f9d891b2dc..4c720a11fc 100644 --- a/apps/openmw/mwgui/enchantingdialog.hpp +++ b/apps/openmw/mwgui/enchantingdialog.hpp @@ -33,6 +33,8 @@ namespace MWGui void resetReference() override; + std::string_view getWindowIdForLua() const override { return "EnchantingDialog"; } + protected: void onReferenceUnavailable() override; void notifyEffectsChanged() override; diff --git a/apps/openmw/mwgui/inventorywindow.hpp b/apps/openmw/mwgui/inventorywindow.hpp index c302925fba..f3d8e3dcd6 100644 --- a/apps/openmw/mwgui/inventorywindow.hpp +++ b/apps/openmw/mwgui/inventorywindow.hpp @@ -65,6 +65,8 @@ namespace MWGui /// Cycle to previous/next weapon void cycle(bool next); + std::string_view getWindowIdForLua() const override { return "Inventory"; } + protected: void onTitleDoubleClicked() override; diff --git a/apps/openmw/mwgui/jailscreen.hpp b/apps/openmw/mwgui/jailscreen.hpp index 42a70abf44..3c21987597 100644 --- a/apps/openmw/mwgui/jailscreen.hpp +++ b/apps/openmw/mwgui/jailscreen.hpp @@ -16,6 +16,8 @@ namespace MWGui bool exit() override { return false; } + std::string_view getWindowIdForLua() const override { return "JailScreen"; } + private: int mDays; diff --git a/apps/openmw/mwgui/journalwindow.hpp b/apps/openmw/mwgui/journalwindow.hpp index 6e037a3989..22e7048acf 100644 --- a/apps/openmw/mwgui/journalwindow.hpp +++ b/apps/openmw/mwgui/journalwindow.hpp @@ -29,6 +29,8 @@ namespace MWGui /// show/hide the journal window void setVisible(bool newValue) override = 0; + + std::string_view getWindowIdForLua() const override { return "Journal"; } }; } diff --git a/apps/openmw/mwgui/levelupdialog.hpp b/apps/openmw/mwgui/levelupdialog.hpp index f3a80ebb9d..486390679b 100644 --- a/apps/openmw/mwgui/levelupdialog.hpp +++ b/apps/openmw/mwgui/levelupdialog.hpp @@ -15,6 +15,8 @@ namespace MWGui void onOpen() override; + std::string_view getWindowIdForLua() const override { return "LevelUpDialog"; } + private: struct Widgets { diff --git a/apps/openmw/mwgui/mapwindow.hpp b/apps/openmw/mwgui/mapwindow.hpp index 20d898e967..5afc8c7c8a 100644 --- a/apps/openmw/mwgui/mapwindow.hpp +++ b/apps/openmw/mwgui/mapwindow.hpp @@ -268,6 +268,8 @@ namespace MWGui void asyncPrepareSaveMap(); + std::string_view getWindowIdForLua() const override { return "Map"; } + private: void onDragStart(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onMouseDrag(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); diff --git a/apps/openmw/mwgui/merchantrepair.hpp b/apps/openmw/mwgui/merchantrepair.hpp index c2fa1cc28a..ffe5b86bdb 100644 --- a/apps/openmw/mwgui/merchantrepair.hpp +++ b/apps/openmw/mwgui/merchantrepair.hpp @@ -16,6 +16,8 @@ namespace MWGui void setPtr(const MWWorld::Ptr& actor) override; + std::string_view getWindowIdForLua() const override { return "MerchantRepair"; } + private: MyGUI::ScrollView* mList; MyGUI::Button* mOkButton; diff --git a/apps/openmw/mwgui/quickkeysmenu.hpp b/apps/openmw/mwgui/quickkeysmenu.hpp index ff2cc083f4..904029b9a0 100644 --- a/apps/openmw/mwgui/quickkeysmenu.hpp +++ b/apps/openmw/mwgui/quickkeysmenu.hpp @@ -44,6 +44,8 @@ namespace MWGui void readRecord(ESM::ESMReader& reader, uint32_t type); void clear() override; + std::string_view getWindowIdForLua() const override { return "QuickKeys"; } + private: struct keyData { diff --git a/apps/openmw/mwgui/recharge.hpp b/apps/openmw/mwgui/recharge.hpp index f7afde4571..f8a037d2db 100644 --- a/apps/openmw/mwgui/recharge.hpp +++ b/apps/openmw/mwgui/recharge.hpp @@ -26,6 +26,8 @@ namespace MWGui void setPtr(const MWWorld::Ptr& gem) override; + std::string_view getWindowIdForLua() const override { return "Recharge"; } + protected: ItemChargeView* mBox; diff --git a/apps/openmw/mwgui/repair.hpp b/apps/openmw/mwgui/repair.hpp index db608d9b8b..093a10e3fa 100644 --- a/apps/openmw/mwgui/repair.hpp +++ b/apps/openmw/mwgui/repair.hpp @@ -23,6 +23,8 @@ namespace MWGui void setPtr(const MWWorld::Ptr& item) override; + std::string_view getWindowIdForLua() const override { return "Repair"; } + protected: ItemChargeView* mRepairBox; diff --git a/apps/openmw/mwgui/scrollwindow.hpp b/apps/openmw/mwgui/scrollwindow.hpp index a8d22a64b0..7daea98894 100644 --- a/apps/openmw/mwgui/scrollwindow.hpp +++ b/apps/openmw/mwgui/scrollwindow.hpp @@ -22,6 +22,8 @@ namespace MWGui void onResChange(int, int) override { center(); } + std::string_view getWindowIdForLua() const override { return "Scroll"; } + protected: void onCloseButtonClicked(MyGUI::Widget* _sender); void onTakeButtonClicked(MyGUI::Widget* _sender); diff --git a/apps/openmw/mwgui/spellbuyingwindow.hpp b/apps/openmw/mwgui/spellbuyingwindow.hpp index 66919e5d33..257b8a0df9 100644 --- a/apps/openmw/mwgui/spellbuyingwindow.hpp +++ b/apps/openmw/mwgui/spellbuyingwindow.hpp @@ -30,6 +30,8 @@ namespace MWGui void onResChange(int, int) override { center(); } + std::string_view getWindowIdForLua() const override { return "SpellBuying"; } + protected: MyGUI::Button* mCancelButton; MyGUI::TextBox* mPlayerGold; diff --git a/apps/openmw/mwgui/spellcreationdialog.hpp b/apps/openmw/mwgui/spellcreationdialog.hpp index 6cd1e2a3ac..bab1064ee3 100644 --- a/apps/openmw/mwgui/spellcreationdialog.hpp +++ b/apps/openmw/mwgui/spellcreationdialog.hpp @@ -158,6 +158,8 @@ namespace MWGui void setPtr(const MWWorld::Ptr& actor) override; + std::string_view getWindowIdForLua() const override { return "SpellCreationDialog"; } + protected: void onReferenceUnavailable() override; diff --git a/apps/openmw/mwgui/spellwindow.hpp b/apps/openmw/mwgui/spellwindow.hpp index 69519a47ef..e35c5cdc4c 100644 --- a/apps/openmw/mwgui/spellwindow.hpp +++ b/apps/openmw/mwgui/spellwindow.hpp @@ -23,6 +23,8 @@ namespace MWGui /// Cycle to next/previous spell void cycle(bool next); + std::string_view getWindowIdForLua() const override { return "Magic"; } + protected: MyGUI::Widget* mEffectBox; diff --git a/apps/openmw/mwgui/statswindow.hpp b/apps/openmw/mwgui/statswindow.hpp index 92926c1180..1e4109b572 100644 --- a/apps/openmw/mwgui/statswindow.hpp +++ b/apps/openmw/mwgui/statswindow.hpp @@ -45,6 +45,8 @@ namespace MWGui void onOpen() override { onWindowResize(mMainWidget->castType()); } + std::string_view getWindowIdForLua() const override { return "Stats"; } + private: void addSkills(const std::vector& skills, const std::string& titleId, const std::string& titleDefault, MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2); diff --git a/apps/openmw/mwgui/tradewindow.hpp b/apps/openmw/mwgui/tradewindow.hpp index bc6c4488fd..7d5fd399df 100644 --- a/apps/openmw/mwgui/tradewindow.hpp +++ b/apps/openmw/mwgui/tradewindow.hpp @@ -47,6 +47,8 @@ namespace MWGui typedef MyGUI::delegates::MultiDelegate<> EventHandle_TradeDone; EventHandle_TradeDone eventTradeDone; + std::string_view getWindowIdForLua() const override { return "Trade"; } + private: ItemView* mItemView; SortFilterItemModel* mSortModel; diff --git a/apps/openmw/mwgui/trainingwindow.hpp b/apps/openmw/mwgui/trainingwindow.hpp index 70df7a1cd7..ee13f24b23 100644 --- a/apps/openmw/mwgui/trainingwindow.hpp +++ b/apps/openmw/mwgui/trainingwindow.hpp @@ -31,6 +31,8 @@ namespace MWGui void clear() override { resetReference(); } + std::string_view getWindowIdForLua() const override { return "Training"; } + protected: void onReferenceUnavailable() override; diff --git a/apps/openmw/mwgui/travelwindow.hpp b/apps/openmw/mwgui/travelwindow.hpp index ff492950f0..6d7c1c7376 100644 --- a/apps/openmw/mwgui/travelwindow.hpp +++ b/apps/openmw/mwgui/travelwindow.hpp @@ -19,6 +19,8 @@ namespace MWGui void setPtr(const MWWorld::Ptr& actor) override; + std::string_view getWindowIdForLua() const override { return "Travel"; } + protected: MyGUI::Button* mCancelButton; MyGUI::TextBox* mPlayerGold; diff --git a/apps/openmw/mwgui/waitdialog.hpp b/apps/openmw/mwgui/waitdialog.hpp index 488989fc88..3d66584f54 100644 --- a/apps/openmw/mwgui/waitdialog.hpp +++ b/apps/openmw/mwgui/waitdialog.hpp @@ -43,6 +43,8 @@ namespace MWGui WindowBase* getProgressBar() { return &mProgressBar; } + std::string_view getWindowIdForLua() const override { return "WaitDialog"; } + protected: MyGUI::TextBox* mDateTimeText; MyGUI::TextBox* mRestText; diff --git a/apps/openmw/mwgui/windowbase.cpp b/apps/openmw/mwgui/windowbase.cpp index d072efb677..4c191eaeb8 100644 --- a/apps/openmw/mwgui/windowbase.cpp +++ b/apps/openmw/mwgui/windowbase.cpp @@ -48,6 +48,7 @@ void WindowBase::onDoubleClick(MyGUI::Widget* _sender) void WindowBase::setVisible(bool visible) { + visible = visible && !mDisabledByLua; bool wasVisible = mMainWidget->getVisible(); mMainWidget->setVisible(visible); diff --git a/apps/openmw/mwgui/windowbase.hpp b/apps/openmw/mwgui/windowbase.hpp index 26201b5ff6..88b46b0bd2 100644 --- a/apps/openmw/mwgui/windowbase.hpp +++ b/apps/openmw/mwgui/windowbase.hpp @@ -49,11 +49,16 @@ namespace MWGui virtual void onDeleteCustomData(const MWWorld::Ptr& ptr) {} + virtual std::string_view getWindowIdForLua() const { return ""; } + void setDisabledByLua(bool disabled) { mDisabledByLua = disabled; } + protected: virtual void onTitleDoubleClicked(); private: void onDoubleClick(MyGUI::Widget* _sender); + + bool mDisabledByLua = false; }; /* diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index 63a63d0883..5f0abaffea 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -525,6 +525,13 @@ namespace MWGui mStatsWatcher->addListener(mHud); mStatsWatcher->addListener(mStatsWindow); mStatsWatcher->addListener(mCharGen.get()); + + for (auto& window : mWindows) + { + std::string_view id = window->getWindowIdForLua(); + if (!id.empty()) + mLuaIdToWindow.emplace(id, window.get()); + } } void WindowManager::setNewGame(bool newgame) @@ -1277,6 +1284,7 @@ namespace MWGui mKeyboardNavigation->restoreFocus(mode); updateVisible(); + MWBase::Environment::get().getLuaManager()->uiModeChanged(arg); } void WindowManager::setCullMask(uint32_t mask) @@ -1309,6 +1317,7 @@ namespace MWGui mGuiModeStates[mode].update(false); if (!noSound) playSound(mGuiModeStates[mode].mCloseSound); + MWBase::Environment::get().getLuaManager()->uiModeChanged(MWWorld::Ptr()); } if (!mGuiModes.empty()) @@ -1343,6 +1352,7 @@ namespace MWGui } updateVisible(); + MWBase::Environment::get().getLuaManager()->uiModeChanged(MWWorld::Ptr()); } void WindowManager::goToJail(int days) @@ -1748,7 +1758,10 @@ namespace MWGui mPlayerBounty = -1; for (const auto& window : mWindows) + { window->clear(); + window->setDisabledByLua(false); + } if (mLocalMapRender) mLocalMapRender->clear(); @@ -2334,4 +2347,45 @@ namespace MWGui { mMap->asyncPrepareSaveMap(); } + + void WindowManager::setDisabledByLua(std::string_view windowId, bool disabled) + { + mLuaIdToWindow.at(windowId)->setDisabledByLua(disabled); + updateVisible(); + } + + std::vector WindowManager::getAllWindowIds() const + { + std::vector res; + for (const auto& [id, _] : mLuaIdToWindow) + res.push_back(id); + return res; + } + + std::vector WindowManager::getAllowedWindowIds(GuiMode mode) const + { + std::vector res; + if (mode == GM_Inventory) + { + if (mAllowed & GW_Map) + res.push_back(mMap->getWindowIdForLua()); + if (mAllowed & GW_Inventory) + res.push_back(mInventoryWindow->getWindowIdForLua()); + if (mAllowed & GW_Magic) + res.push_back(mSpellWindow->getWindowIdForLua()); + if (mAllowed & GW_Stats) + res.push_back(mStatsWindow->getWindowIdForLua()); + } + else + { + auto it = mGuiModeStates.find(mode); + if (it != mGuiModeStates.end()) + { + for (const auto* w : it->second.mWindows) + if (!w->getWindowIdForLua().empty()) + res.push_back(w->getWindowIdForLua()); + } + } + return res; + } } diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index 68eb099a56..6074c90231 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -387,6 +387,12 @@ namespace MWGui void asyncPrepareSaveMap() override; + // Used in Lua bindings + const std::vector& getGuiModeStack() const override { return mGuiModes; } + void setDisabledByLua(std::string_view windowId, bool disabled) override; + std::vector getAllWindowIds() const override; + std::vector getAllowedWindowIds(GuiMode mode) const override; + private: unsigned int mOldUpdateMask; unsigned int mOldCullMask; @@ -451,6 +457,9 @@ namespace MWGui std::vector> mWindows; + // Mapping windowId -> Window; used by Lua bindings. + std::map mLuaIdToWindow; + Translation::Storage& mTranslationDataStorage; std::unique_ptr mCharGen; diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp index ec0f09b59b..a360f5c855 100644 --- a/apps/openmw/mwlua/luabindings.cpp +++ b/apps/openmw/mwlua/luabindings.cpp @@ -121,7 +121,7 @@ namespace MWLua { auto* lua = context.mLua; sol::table api(lua->sol(), sol::create); - api["API_REVISION"] = 43; + api["API_REVISION"] = 44; api["quit"] = [lua]() { Log(Debug::Warning) << "Quit requested by a Lua script.\n" << lua->debugTraceback(); MWBase::Environment::get().getStateManager()->requestQuit(); diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index f2705501e5..20aefa12c7 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -311,6 +311,15 @@ namespace MWLua mGlobalScriptsStarted = true; } + void LuaManager::uiModeChanged(const MWWorld::Ptr& arg) + { + if (mPlayer.isEmpty()) + return; + PlayerScripts* playerScripts = dynamic_cast(mPlayer.getRefData().getLuaScripts()); + if (playerScripts) + playerScripts->uiModeChanged(arg); + } + void LuaManager::objectAddedToScene(const MWWorld::Ptr& ptr) { mObjectLists.objectAddedToScene(ptr); // assigns generated RefNum if it is not set yet. diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index bb7a2535ed..11fbc70d50 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -83,6 +83,7 @@ namespace MWLua } void objectTeleported(const MWWorld::Ptr& ptr) override; void questUpdated(const ESM::RefId& questId, int stage) override; + void uiModeChanged(const MWWorld::Ptr& arg) override; MWBase::LuaManager::ActorControls* getActorControls(const MWWorld::Ptr&) const override; diff --git a/apps/openmw/mwlua/playerscripts.hpp b/apps/openmw/mwlua/playerscripts.hpp index 58eb955f7d..de48064734 100644 --- a/apps/openmw/mwlua/playerscripts.hpp +++ b/apps/openmw/mwlua/playerscripts.hpp @@ -20,7 +20,7 @@ namespace MWLua { registerEngineHandlers({ &mConsoleCommandHandlers, &mKeyPressHandlers, &mKeyReleaseHandlers, &mControllerButtonPressHandlers, &mControllerButtonReleaseHandlers, &mActionHandlers, &mOnFrameHandlers, - &mTouchpadPressed, &mTouchpadReleased, &mTouchpadMoved, &mQuestUpdate }); + &mTouchpadPressed, &mTouchpadReleased, &mTouchpadMoved, &mQuestUpdate, &mUiModeChanged }); } void processInputEvent(const MWBase::LuaManager::InputEvent& event) @@ -65,6 +65,15 @@ namespace MWLua return !mConsoleCommandHandlers.mList.empty(); } + // `arg` is either forwarded from MWGui::pushGuiMode or empty + void uiModeChanged(const MWWorld::Ptr& arg) + { + if (arg.isEmpty()) + callEngineHandlers(mUiModeChanged); + else + callEngineHandlers(mUiModeChanged, LObject(arg)); + } + private: EngineHandlerList mConsoleCommandHandlers{ "onConsoleCommand" }; EngineHandlerList mKeyPressHandlers{ "onKeyPress" }; @@ -77,6 +86,7 @@ namespace MWLua EngineHandlerList mTouchpadReleased{ "onTouchRelease" }; EngineHandlerList mTouchpadMoved{ "onTouchMove" }; EngineHandlerList mQuestUpdate{ "onQuestUpdate" }; + EngineHandlerList mUiModeChanged{ "_onUiModeChanged" }; }; } diff --git a/apps/openmw/mwlua/uibindings.cpp b/apps/openmw/mwlua/uibindings.cpp index fca46fd7e6..eba761ed97 100644 --- a/apps/openmw/mwlua/uibindings.cpp +++ b/apps/openmw/mwlua/uibindings.cpp @@ -45,10 +45,55 @@ namespace MWLua { return i + 1; } + + const std::unordered_map modeToName{ + { MWGui::GM_Settings, "SettingsMenu" }, + { MWGui::GM_Inventory, "Interface" }, + { MWGui::GM_Container, "Container" }, + { MWGui::GM_Companion, "Companion" }, + { MWGui::GM_MainMenu, "MainMenu" }, + { MWGui::GM_Journal, "Journal" }, + { MWGui::GM_Scroll, "Scroll" }, + { MWGui::GM_Book, "Book" }, + { MWGui::GM_Alchemy, "Alchemy" }, + { MWGui::GM_Repair, "Repair" }, + { MWGui::GM_Dialogue, "Dialogue" }, + { MWGui::GM_Barter, "Barter" }, + { MWGui::GM_Rest, "Rest" }, + { MWGui::GM_SpellBuying, "SpellBuying" }, + { MWGui::GM_Travel, "Travel" }, + { MWGui::GM_SpellCreation, "SpellCreation" }, + { MWGui::GM_Enchanting, "Enchanting" }, + { MWGui::GM_Recharge, "Recharge" }, + { MWGui::GM_Training, "Training" }, + { MWGui::GM_MerchantRepair, "MerchantRepair" }, + { MWGui::GM_Levelup, "LevelUp" }, + { MWGui::GM_Name, "ChargenName" }, + { MWGui::GM_Race, "ChargenRace" }, + { MWGui::GM_Birth, "ChargenBirth" }, + { MWGui::GM_Class, "ChargenClass" }, + { MWGui::GM_ClassGenerate, "ChargenClassGenerate" }, + { MWGui::GM_ClassPick, "ChargenClassPick" }, + { MWGui::GM_ClassCreate, "ChargenClassCreate" }, + { MWGui::GM_Review, "ChargenClassReview" }, + { MWGui::GM_Loading, "Loading" }, + { MWGui::GM_LoadingWallpaper, "LoadingWallpaper" }, + { MWGui::GM_Jail, "Jail" }, + { MWGui::GM_QuickKeysMenu, "QuickKeysMenu" }, + }; + + const auto nameToMode = [] { + std::unordered_map res; + for (const auto& [mode, name] : modeToName) + res[name] = mode; + return res; + }(); } sol::table initUserInterfacePackage(const Context& context) { + MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); + auto element = context.mLua->sol().new_usertype("Element"); element["layout"] = sol::property([](LuaUi::Element& element) { return element.mLayout; }, [](LuaUi::Element& element, const sol::table& layout) { element.mLayout = layout; }); @@ -78,19 +123,18 @@ namespace MWLua = [luaManager = context.mLuaManager](const std::string& message, const Misc::Color& color) { luaManager->addInGameConsoleMessage(message + "\n", color); }; - api["setConsoleMode"] = [luaManager = context.mLuaManager](std::string_view mode) { - luaManager->addAction( - [mode = std::string(mode)] { MWBase::Environment::get().getWindowManager()->setConsoleMode(mode); }); + api["setConsoleMode"] = [luaManager = context.mLuaManager, windowManager](std::string_view mode) { + luaManager->addAction([mode = std::string(mode), windowManager] { windowManager->setConsoleMode(mode); }); }; - api["setConsoleSelectedObject"] = [luaManager = context.mLuaManager](const sol::object& obj) { - const auto wm = MWBase::Environment::get().getWindowManager(); + api["setConsoleSelectedObject"] = [luaManager = context.mLuaManager, windowManager](const sol::object& obj) { if (obj == sol::nil) - luaManager->addAction([wm] { wm->setConsoleSelectedObject(MWWorld::Ptr()); }); + luaManager->addAction([windowManager] { windowManager->setConsoleSelectedObject(MWWorld::Ptr()); }); else { if (!obj.is()) throw std::runtime_error("Game object expected"); - luaManager->addAction([wm, obj = obj.as()] { wm->setConsoleSelectedObject(obj.ptr()); }); + luaManager->addAction( + [windowManager, obj = obj.as()] { windowManager->setConsoleSelectedObject(obj.ptr()); }); } }; api["content"] = LuaUi::loadContentConstructor(context.mLua); @@ -189,6 +233,65 @@ namespace MWLua Settings::Manager::getInt("resolution x", "Video"), Settings::Manager::getInt("resolution y", "Video")); }; + api["_getAllUiModes"] = [](sol::this_state lua) { + sol::table res(lua, sol::create); + for (const auto& [_, name] : modeToName) + res[name] = name; + return res; + }; + api["_getUiModeStack"] = [windowManager](sol::this_state lua) { + sol::table res(lua, sol::create); + int i = 1; + for (MWGui::GuiMode m : windowManager->getGuiModeStack()) + res[i++] = modeToName.at(m); + return res; + }; + api["_setUiModeStack"] + = [windowManager, luaManager = context.mLuaManager](sol::table modes, sol::optional arg) { + std::vector newStack(modes.size()); + for (unsigned i = 0; i < newStack.size(); ++i) + newStack[i] = nameToMode.at(LuaUtil::cast(modes[i + 1])); + luaManager->addAction( + [windowManager, newStack, arg]() { + MWWorld::Ptr ptr; + if (arg.has_value()) + ptr = arg->ptr(); + const std::vector& stack = windowManager->getGuiModeStack(); + unsigned common = 0; + while (common < std::min(stack.size(), newStack.size()) && stack[common] == newStack[common]) + common++; + // TODO: Maybe disallow opening/closing special modes (main menu, settings, loading screen) + // from player scripts. Add new Lua context "menu" that can do it. + for (unsigned i = stack.size() - common; i > 0; i--) + windowManager->popGuiMode(); + if (common == newStack.size() && !newStack.empty() && arg.has_value()) + windowManager->pushGuiMode(newStack.back(), ptr); + for (unsigned i = common; i < newStack.size(); ++i) + windowManager->pushGuiMode(newStack[i], ptr); + }, + "Set UI modes"); + }; + api["_getAllWindowIds"] = [windowManager](sol::this_state lua) { + sol::table res(lua, sol::create); + for (std::string_view name : windowManager->getAllWindowIds()) + res[name] = name; + return res; + }; + api["_getAllowedWindows"] = [windowManager](sol::this_state lua, std::string_view mode) { + sol::table res(lua, sol::create); + for (std::string_view name : windowManager->getAllowedWindowIds(nameToMode.at(mode))) + res[name] = name; + return res; + }; + api["_setWindowDisabled"] + = [windowManager, luaManager = context.mLuaManager](std::string_view window, bool disabled) { + luaManager->addAction([=]() { windowManager->setDisabledByLua(window, disabled); }); + }; + + // TODO + // api["_showHUD"] = [](bool) {}; + // api["_showMouseCursor"] = [](bool) {}; + return LuaUtil::makeReadOnly(api); } } diff --git a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp index d79e32d671..dc99caefda 100644 --- a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp +++ b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp @@ -204,8 +204,7 @@ CUSTOM, PLAYER: useInterface.lua { testing::internal::CaptureStdout(); scripts.receiveEvent("SomeEvent", X1); - EXPECT_EQ(internal::GetCapturedStdout(), - "Test has received event 'SomeEvent', but there are no handlers for this event\n"); + EXPECT_EQ(internal::GetCapturedStdout(), ""); } { testing::internal::CaptureStdout(); diff --git a/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp index 1a37970ad8..2a1a755422 100644 --- a/components/lua/scriptscontainer.cpp +++ b/components/lua/scriptscontainer.cpp @@ -297,11 +297,7 @@ namespace LuaUtil { auto it = mEventHandlers.find(eventName); if (it == mEventHandlers.end()) - { - Log(Debug::Warning) << mNamePrefix << " has received event '" << eventName - << "', but there are no handlers for this event"; return; - } sol::object data; try { diff --git a/docs/source/luadoc_data_paths.sh b/docs/source/luadoc_data_paths.sh index 91bfcbd48d..c4322dbc0a 100755 --- a/docs/source/luadoc_data_paths.sh +++ b/docs/source/luadoc_data_paths.sh @@ -6,5 +6,6 @@ paths=( scripts/omw/camera/camera.lua scripts/omw/mwui/init.lua scripts/omw/settings/player.lua + scripts/omw/ui.lua ) printf '%s\n' "${paths[@]}" \ No newline at end of file diff --git a/docs/source/reference/lua-scripting/api.rst b/docs/source/reference/lua-scripting/api.rst index e60f22b7fa..76092fec3f 100644 --- a/docs/source/reference/lua-scripting/api.rst +++ b/docs/source/reference/lua-scripting/api.rst @@ -34,6 +34,7 @@ Lua API reference interface_controls interface_mwui interface_settings + interface_ui iterables @@ -90,3 +91,7 @@ Interfaces of built-in scripts * - :ref:`MWUI ` - by player scripts - Morrowind-style UI templates. + * - :ref:`UI ` + - by player scripts + - | High-level UI modes interface. Allows to override parts + | of the interface. diff --git a/docs/source/reference/lua-scripting/events.rst b/docs/source/reference/lua-scripting/events.rst index 3d443b2811..7d383c491e 100644 --- a/docs/source/reference/lua-scripting/events.rst +++ b/docs/source/reference/lua-scripting/events.rst @@ -1,6 +1,9 @@ Built-in events =============== +Actor events +------------ + Any script can send to any actor (except player, for player will be ignored) events ``StartAIPackage`` and ``RemoveAIPackages``. The effect is equivalent to calling ``interfaces.AI.startPackage`` or ``interfaces.AI.removePackages`` in a local script on this actor. @@ -11,3 +14,16 @@ Examples: actor:sendEvent('StartAIPackage', {type='Combat', target=self.object}) actor:sendEvent('RemoveAIPackages', 'Pursue') +UI events +--------- + +Every time UI mode is changed built-in scripts send to player the event ``UiModeChanged`` with arguments ``mode`` (same as ``I.UI.getMode()``) +and ``arg`` (for example in the mode ``Book`` the argument is the book the player is reading). + +.. code-block:: Lua + + eventHandlers = { + UiModeChanged = function(data) + print('UiModeChanged to', data.mode, '('..tostring(data.arg)..')') + end + } diff --git a/docs/source/reference/lua-scripting/interface_ui.rst b/docs/source/reference/lua-scripting/interface_ui.rst new file mode 100644 index 0000000000..8c3ac679e5 --- /dev/null +++ b/docs/source/reference/lua-scripting/interface_ui.rst @@ -0,0 +1,6 @@ +Interface UI +============ + +.. raw:: html + :file: generated_html/scripts_omw_ui.html + diff --git a/docs/source/reference/lua-scripting/overview.rst b/docs/source/reference/lua-scripting/overview.rst index 283664b2c4..d0eb0a4cb2 100644 --- a/docs/source/reference/lua-scripting/overview.rst +++ b/docs/source/reference/lua-scripting/overview.rst @@ -481,6 +481,10 @@ The order in which the scripts are started is important. So if one mod should ov * - :ref:`MWUI ` - by player scripts - Morrowind-style UI templates. + * - :ref:`UI ` + - by player scripts + - | High-level UI modes interface. Allows to override parts + | of the interface. Event system ============ diff --git a/files/data/CMakeLists.txt b/files/data/CMakeLists.txt index ef50b4c783..f52be02451 100644 --- a/files/data/CMakeLists.txt +++ b/files/data/CMakeLists.txt @@ -89,6 +89,7 @@ set(BUILTIN_DATA_FILES scripts/omw/mwui/textEdit.lua scripts/omw/mwui/space.lua scripts/omw/mwui/init.lua + scripts/omw/ui.lua shaders/adjustments.omwfx shaders/bloomlinear.omwfx diff --git a/files/data/builtin.omwscripts b/files/data/builtin.omwscripts index 23cee0c2d8..8999a98f80 100644 --- a/files/data/builtin.omwscripts +++ b/files/data/builtin.omwscripts @@ -13,6 +13,9 @@ PLAYER: scripts/omw/playercontrols.lua PLAYER: scripts/omw/camera/camera.lua NPC,CREATURE: scripts/omw/ai.lua +# User interface +PLAYER: scripts/omw/ui.lua + # Lua console PLAYER: scripts/omw/console/player.lua GLOBAL: scripts/omw/console/global.lua diff --git a/files/data/scripts/omw/ui.lua b/files/data/scripts/omw/ui.lua new file mode 100644 index 0000000000..b9f842238c --- /dev/null +++ b/files/data/scripts/omw/ui.lua @@ -0,0 +1,187 @@ +local ui = require('openmw.ui') +local util = require('openmw.util') +local self = require('openmw.self') + +local MODE = ui._getAllUiModes() +local WINDOW = ui._getAllWindowIds() + +local replacedWindows = {} +local hiddenWindows = {} +local modeStack = {} + +local function registerWindow(window, showFn, hideFn) + if not WINDOW[window] then + error('At the moment it is only possible to override existing windows. Window "'.. + tostring(window)..'" not found.') + end + ui._setWindowDisabled(window, true) + if replacedWindows[window] then + replacedWindows[window].hideFn() + end + replacedWindows[window] = {showFn = showFn, hideFn = hideFn, visible = false} + hiddenWindows[window] = nil +end + +local function updateHidden(mode, options) + local toHide = {} + if options and options.windows then + for _, w in pairs(ui._getAllowedWindows(mode)) do + toHide[w] = true + end + for _, w in pairs(options.windows) do + toHide[w] = nil + end + end + for w, _ in pairs(hiddenWindows) do + if toHide[w] then + toHide[w] = nil + else + hiddenWindows[w] = nil + if not replacedWindows[w] then + ui._setWindowDisabled(w, false) + end + end + end + for w, _ in pairs(toHide) do + hiddenWindows[w] = true + if not replacedWindows[w] then + ui._setWindowDisabled(w, true) + end + end +end + +local function setMode(mode, options) + if mode then + updateHidden(mode, options) + ui._setUiModeStack({mode}, options and options.target) + else + ui._setUiModeStack({}) + end +end + +local function addMode(mode, options) + updateHidden(mode, options) + modeStack[#modeStack + 1] = mode + ui._setUiModeStack(modeStack, options and options.target) +end + +local function removeMode(mode) + local sizeBefore = #modeStack + local j = 1 + for i = 1, sizeBefore do + if modeStack[i] ~= mode then + modeStack[j] = modeStack[i] + j = j + 1 + end + end + for i = j, sizeBefore do modeStack[i] = nil end + if sizeBefore > #modeStack then + ui._setUiModeStack(modeStack) + end +end + +local function onUiModeChanged(arg) + local newStack = ui._getUiModeStack() + for i = 1, math.max(#modeStack, #newStack) do + modeStack[i] = newStack[i] + end + for w, state in pairs(replacedWindows) do + if state.visible then + state.hideFn() + state.visible = false + end + end + local mode = newStack[#newStack] + if mode then + for _, w in pairs(ui._getAllowedWindows(mode)) do + local state = replacedWindows[w] + if state and not hiddenWindows[w] then + state.showFn(arg) + state.visible = true + end + end + end + self:sendEvent('UiModeChanged', {mode = mode, arg = arg}) +end + +return { + interfaceName = 'UI', + --- + -- @module UI + -- @usage require('openmw.interfaces').UI + interface = { + --- Interface version + -- @field [parent=#UI] #number version + version = 0, + + --- All available UI modes. + -- Use `view(I.UI.MODE)` in `luap` console mode to see the list. + -- @field [parent=#UI] #table MODE + MODE = util.makeStrictReadOnly(MODE), + + --- All windows. + -- Use `view(I.UI.WINDOW)` in `luap` console mode to see the list. + -- @field [parent=#UI] #table WINDOW + WINDOW = util.makeStrictReadOnly(WINDOW), + + --- Register new implementation for the window with given name; overrides previous implementation. + -- Adding new windows is not supported yet. At the moment it is only possible to override built-in windows. + -- @function [parent=#UI] registerWindow + -- @param #string windowName + -- @param #function showFn Callback that will be called when the window should become visible + -- @param #function hideFn Callback that will be called when the window should be hidden + registerWindow = registerWindow, + + --- Returns windows that can be shown in given mode. + -- @function [parent=#UI] getWindowsForMode + -- @param #string mode + -- @return #table + getWindowsForMode = ui._getAllowedWindows, + + --- Stack of currently active modes + -- @field [parent=#UI] modes + modes = util.makeReadOnly(modeStack), + + --- Get current mode (nil if all windows are closed), equivalent to `I.UI.modes[#I.UI.modes]` + -- @function [parent=#UI] getMode + -- @return #string + getMode = function() return modeStack[#modeStack] end, + + --- Drop all active modes and set mode. + -- @function [parent=#UI] setMode + -- @param #string mode (optional) New mode + -- @param #table options (optional) Table with keys 'windows' and/or 'target' (see example). + -- @usage I.UI.setMode() -- drop all modes + -- @usage I.UI.setMode('Interface') -- drop all modes and open interface + -- @usage -- Drop all modes, open interface, but show only the map window. + -- I.UI.setMode('Interface', {windows = {'Map'}}) + setMode = setMode, + + --- Add mode to stack without dropping other active modes. + -- @function [parent=#UI] addMode + -- @param #string mode New mode + -- @param #table options (optional) Table with keys 'windows' and/or 'target' (see example). + -- @usage I.UI.addMode('Journal') -- open journal without dropping active modes. + -- @usage -- Open barter with an NPC + -- I.UI.addMode('Barter', {target = actor}) + addMode = addMode, + + --- Remove the specified mode from active modes. + -- @function [parent=#UI] removeMode + -- @param #string mode Mode to drop + removeMode = removeMode, + + -- TODO + -- registerHudElement = function(name, showFn, hideFn) end, + -- showHud = function(bool) end, + -- isHudVisible = function() end, + -- showHudElement = function(name, bool) end, + -- hudElements, -- map from element name to its visibility + }, + engineHandlers = { + _onUiModeChanged = onUiModeChanged, + }, + eventHandlers = { + UiModeChanged = function() end, + }, +} diff --git a/files/lua_api/openmw/interfaces.lua b/files/lua_api/openmw/interfaces.lua index 827b9e57d7..5048fc2f3e 100644 --- a/files/lua_api/openmw/interfaces.lua +++ b/files/lua_api/openmw/interfaces.lua @@ -17,6 +17,9 @@ --- -- @field [parent=#interfaces] scripts.omw.settings.player#scripts.omw.settings.player Settings +--- +-- @field [parent=#interfaces] scripts.omw.ui#scripts.omw.ui UI + --- -- @function [parent=#interfaces] __index -- @param #interfaces self