From c792582376c41a70d4118cb53404ff64edb1f8fc Mon Sep 17 00:00:00 2001 From: Tobias Tribble Date: Mon, 12 Jun 2023 21:35:00 -0500 Subject: [PATCH] Add Lua bindings for journal --- apps/openmw/mwbase/journal.hpp | 8 ++ apps/openmw/mwbase/luamanager.hpp | 2 + apps/openmw/mwdialogue/journalimp.cpp | 14 ++ apps/openmw/mwdialogue/journalimp.hpp | 11 +- apps/openmw/mwdialogue/quest.cpp | 5 + apps/openmw/mwlua/luamanagerimp.cpp | 11 ++ apps/openmw/mwlua/luamanagerimp.hpp | 1 + apps/openmw/mwlua/playerscripts.hpp | 4 +- apps/openmw/mwlua/types/player.cpp | 136 +++++++++++++++++- .../lua-scripting/engine_handlers.rst | 2 + files/lua_api/openmw/types.lua | 28 ++++ 11 files changed, 218 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwbase/journal.hpp b/apps/openmw/mwbase/journal.hpp index aed5817e98..fe6d3a6f25 100644 --- a/apps/openmw/mwbase/journal.hpp +++ b/apps/openmw/mwbase/journal.hpp @@ -49,6 +49,11 @@ namespace MWBase virtual ~Journal() {} + virtual MWDialogue::Quest& getQuest(const ESM::RefId& id) = 0; + ///< Gets the quest requested. Creates it and inserts it in quests if it does not yet exist. + virtual MWDialogue::Quest* getQuestPtr(const ESM::RefId& id) = 0; + ///< Gets a pointer to the requested quest. Will return nullptr if the quest has not been started. + virtual void addEntry(const ESM::RefId& id, int index, const MWWorld::Ptr& actor) = 0; ///< Add a journal entry. /// @param actor Used as context for replacing of escape sequences (%name, etc). @@ -56,6 +61,9 @@ namespace MWBase virtual void setJournalIndex(const ESM::RefId& id, int index) = 0; ///< Set the journal index without adding an entry. + virtual int getQuestCount() const = 0; + ///< Get the count of quests stored. + virtual int getJournalIndex(const ESM::RefId& id) const = 0; ///< Get the journal index. diff --git a/apps/openmw/mwbase/luamanager.hpp b/apps/openmw/mwbase/luamanager.hpp index ca0220c6fa..d9ce24a119 100644 --- a/apps/openmw/mwbase/luamanager.hpp +++ b/apps/openmw/mwbase/luamanager.hpp @@ -24,6 +24,7 @@ namespace ESM { class ESMReader; class ESMWriter; + class RefId; struct LuaScripts; } @@ -49,6 +50,7 @@ namespace MWBase virtual void itemConsumed(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) = 0; 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; // 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/mwdialogue/journalimp.cpp b/apps/openmw/mwdialogue/journalimp.cpp index 369e029e77..64f298f54e 100644 --- a/apps/openmw/mwdialogue/journalimp.cpp +++ b/apps/openmw/mwdialogue/journalimp.cpp @@ -31,6 +31,16 @@ namespace MWDialogue return iter->second; } + Quest* Journal::getQuestPtr(const ESM::RefId& id) + { + TQuestContainer::iterator iter = mQuests.find(id); + if (iter == mQuests.end()) + { + return nullptr; + } + + return &(iter->second); + } Topic& Journal::getTopic(const ESM::RefId& id) { @@ -135,6 +145,10 @@ namespace MWDialogue mTopics.erase(mTopics.find(topicId)); // All responses removed -> remove topic } + int Journal::getQuestCount() const + { + return static_cast(mQuests.size()); + } int Journal::getJournalIndex(const ESM::RefId& id) const { TQuestContainer::const_iterator iter = mQuests.find(id); diff --git a/apps/openmw/mwdialogue/journalimp.hpp b/apps/openmw/mwdialogue/journalimp.hpp index b7b0c3151c..96e36fcc56 100644 --- a/apps/openmw/mwdialogue/journalimp.hpp +++ b/apps/openmw/mwdialogue/journalimp.hpp @@ -15,8 +15,6 @@ namespace MWDialogue TTopicContainer mTopics; private: - Quest& getQuest(const ESM::RefId& id); - Topic& getTopic(const ESM::RefId& id); bool isThere(const ESM::RefId& topicId, const ESM::RefId& infoId = ESM::RefId()) const; @@ -26,10 +24,19 @@ namespace MWDialogue void clear() override; + Quest* getQuestPtr(const ESM::RefId& id) override; + ///< Gets a pointer to the requested quest. Will return nullptr if the quest has not been started. + + Quest& getQuest(const ESM::RefId& id) override; + ///< Gets the quest requested. Attempts to create it and inserts it in quests if it does not yet exist. + void addEntry(const ESM::RefId& id, int index, const MWWorld::Ptr& actor) override; ///< Add a journal entry. /// @param actor Used as context for replacing of escape sequences (%name, etc). + int getQuestCount() const override; + ///< Get the count of saved quests. + void setJournalIndex(const ESM::RefId& id, int index) override; ///< Set the journal index without adding an entry. diff --git a/apps/openmw/mwdialogue/quest.cpp b/apps/openmw/mwdialogue/quest.cpp index 0fdb918859..a5f77959f0 100644 --- a/apps/openmw/mwdialogue/quest.cpp +++ b/apps/openmw/mwdialogue/quest.cpp @@ -4,6 +4,7 @@ #include +#include "../mwbase/luamanager.hpp" #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" @@ -52,6 +53,7 @@ namespace MWDialogue void Quest::setIndex(int index) { // The index must be set even if no related journal entry was found + MWBase::Environment::get().getLuaManager()->questUpdated(mTopic, index); mIndex = index; } @@ -80,7 +82,10 @@ namespace MWDialogue mFinished = info->mQuestStatus == ESM::DialInfo::QS_Finished; if (info->mData.mJournalIndex > mIndex) + { mIndex = info->mData.mJournalIndex; + MWBase::Environment::get().getLuaManager()->questUpdated(mTopic, mIndex); + } for (TEntryIter iter(mEntries.begin()); iter != mEntries.end(); ++iter) if (iter->mInfoId == entry.mInfoId) diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 285a378eaf..d19f6985f3 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -179,6 +179,17 @@ namespace MWLua } } + void LuaManager::questUpdated(const ESM::RefId& questId, int stage) + { + if (mPlayer.isEmpty()) + return; // The game is not started yet. + PlayerScripts* playerScripts = dynamic_cast(mPlayer.getRefData().getLuaScripts()); + if (playerScripts) + { + playerScripts->onQuestUpdate(questId.serializeText(), stage); + } + } + void LuaManager::synchronizedUpdate() { if (mPlayer.isEmpty()) diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index 2b23182f41..e73a850d63 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -75,6 +75,7 @@ namespace MWLua { mEngineEvents.addToQueue(EngineEvents::OnNewExterior{ cell }); } + void questUpdated(const ESM::RefId& questId, int stage) 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 577e93a553..58eb955f7d 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 }); + &mTouchpadPressed, &mTouchpadReleased, &mTouchpadMoved, &mQuestUpdate }); } void processInputEvent(const MWBase::LuaManager::InputEvent& event) @@ -56,6 +56,7 @@ namespace MWLua } void onFrame(float dt) { callEngineHandlers(mOnFrameHandlers, dt); } + void onQuestUpdate(std::string_view questId, int stage) { callEngineHandlers(mQuestUpdate, questId, stage); } bool consoleCommand( const std::string& consoleMode, const std::string& command, const sol::object& selectedObject) @@ -75,6 +76,7 @@ namespace MWLua EngineHandlerList mTouchpadPressed{ "onTouchPress" }; EngineHandlerList mTouchpadReleased{ "onTouchRelease" }; EngineHandlerList mTouchpadMoved{ "onTouchMove" }; + EngineHandlerList mQuestUpdate{ "onQuestUpdate" }; }; } diff --git a/apps/openmw/mwlua/types/player.cpp b/apps/openmw/mwlua/types/player.cpp index 4203e68d06..abc3da6827 100644 --- a/apps/openmw/mwlua/types/player.cpp +++ b/apps/openmw/mwlua/types/player.cpp @@ -1,10 +1,143 @@ #include "types.hpp" +#include "../luamanagerimp.hpp" +#include +#include #include #include namespace MWLua { + struct Quests + { + bool mMutable = false; + MWWorld::SafePtr::Id playerId; + using Iterator = typename MWBase::Journal::TQuestIter; + Iterator mIterator; + MWBase::Journal* const journal = MWBase::Environment::get().getJournal(); + void reset() { mIterator = journal->questBegin(); } + bool isEnd() const { return mIterator == journal->questEnd(); } + void advance() { mIterator++; } + }; + struct Quest + { + ESM::RefId mQuestId; + bool mMutable = false; + }; +} + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + + void addPlayerQuestBindings(sol::table& player, const Context& context) + { + MWBase::Journal* const journal = MWBase::Environment::get().getJournal(); + + // Quests + player["quests"] = [](const Object& player) { + MWBase::World* world = MWBase::Environment::get().getWorld(); + Quests q = {}; + if (player.ptr() != world->getPlayerPtr()) + throw std::runtime_error("Must provide a player!"); + if (dynamic_cast(&player)) + q.mMutable = true; + q.playerId = player.id(); + return q; + }; + sol::usertype quests = context.mLua->sol().new_usertype("Quests"); + quests[sol::meta_function::to_string] + = [](const Quests& quests) { return "Quests[" + quests.playerId.toString() + "]"; }; + quests[sol::meta_function::length] = [journal]() { return journal->getQuestCount(); }; + quests[sol::meta_function::index] = sol::overload([](const Quests& quests, std::string_view index) -> Quest { + Quest q; + q.mQuestId = ESM::RefId::deserializeText(index); + q.mMutable = quests.mMutable; + return q; + }); + quests[sol::meta_function::pairs] = [](sol::this_state ts, Quests& self) { + sol::state_view lua(ts); + self.reset(); + return sol::as_function([lua, &self]() mutable -> std::pair { + if (!self.isEnd()) + { + Quest q; + q.mQuestId = (self.mIterator->first); + q.mMutable = self.mMutable; + auto result = sol::make_object(lua, q); + auto index = sol::make_object(lua, self.mIterator->first); + self.advance(); + return { index, result }; + } + else + { + return { sol::lua_nil, sol::lua_nil }; + } + }); + }; + + // Quest Functions + auto getQuestStage = [journal](const Quest& q) -> int { + auto quest = journal->getQuestPtr(q.mQuestId); + if (quest == nullptr) + return -1; + return journal->getJournalIndex(q.mQuestId); + }; + auto setQuestStage = [context](const Quest& q, int stage) { + if (!q.mMutable) + throw std::runtime_error("Value can only be changed in global scripts!"); + context.mLuaManager->addAction( + [q, stage] { MWBase::Environment::get().getJournal()->setJournalIndex(q.mQuestId, stage); }, + "setQuestStageAction"); + }; + + // Player quests + sol::usertype quest = context.mLua->sol().new_usertype("Quest"); + quest[sol::meta_function::to_string] + = [](const Quest& quest) { return "Quest [" + quest.mQuestId.serializeText() + "]"; }; + quest["stage"] = sol::property(getQuestStage, setQuestStage); + quest["name"] = sol::readonly_property([journal](const Quest& q) -> sol::optional { + auto quest = journal->getQuestPtr(q.mQuestId); + if (quest == nullptr) + return sol::nullopt; + return quest->getName(); + }); + quest["id"] = sol::readonly_property([](const Quest& q) -> std::string { return q.mQuestId.serializeText(); }); + quest["isFinished"] = sol::property( + [journal](const Quest& q) -> bool { + auto quest = journal->getQuestPtr(q.mQuestId); + if (quest == nullptr) + return false; + return quest->isFinished(); + }, + [journal, context](const Quest& q, bool finished) { + if (!q.mMutable) + throw std::runtime_error("Value can only be changed in global scripts!"); + context.mLuaManager->addAction( + [q, finished, journal] { journal->getQuest(q.mQuestId).setFinished(finished); }, + "setQuestFinishedAction"); + }); + quest["addJournalEntry"] = [context](const Quest& q, const GObject& actor, int stage) { + MWWorld::Ptr ptr = actor.ptr(); + + // The journal mwscript function has a try function here, we will make the lua function throw an + // error. However, the addAction will cause it to error outside of this function. + context.mLuaManager->addAction( + [ptr, q, stage] { MWBase::Environment::get().getJournal()->addEntry(q.mQuestId, stage, ptr); }, + "addJournalEntryAction"); + }; + } void addPlayerBindings(sol::table player, const Context& context) { @@ -12,5 +145,6 @@ namespace MWLua const MWWorld::Class& cls = o.ptr().getClass(); return cls.getNpcStats(o.ptr()).getBounty(); }; + addPlayerQuestBindings(player, context); } -} +} \ No newline at end of file diff --git a/docs/source/reference/lua-scripting/engine_handlers.rst b/docs/source/reference/lua-scripting/engine_handlers.rst index 0da1cef5d3..1beb990062 100644 --- a/docs/source/reference/lua-scripting/engine_handlers.rst +++ b/docs/source/reference/lua-scripting/engine_handlers.rst @@ -89,6 +89,8 @@ Engine handler is a function defined by a script, that can be called by the engi - | `Key `_ is pressed. | Usage example: | ``if key.symbol == 'z' and key.withShift then ...`` + * - onQuestUpdate(questId,stage) + - | Called when a quest is updated. * - onKeyRelease(key) - | `Key `_ is released. | Usage example: diff --git a/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index 55f3888c90..ea08c63189 100644 --- a/files/lua_api/openmw/types.lua +++ b/files/lua_api/openmw/types.lua @@ -717,6 +717,34 @@ -- @param openmw.core#GameObject actor -- @return #number +--- +-- Returns a list containing quests @{<#PlayerQuest>} for the specified player, indexed by quest ID. +-- @function [parent=#Player] quests +-- @param openmw.core#GameObject player +-- @return #list<#PlayerQuest> +-- @usage -- Getting the quest for a specified index +-- stage = types.Player.quests(playerRef)["ms_fargothring].stage +-- --Get the name of all started quests +-- for x, quest in pairs(types.Player.quests(playerRef)) do print (quest.name) end +-- --Start a new quest, add it to the player's quest list but don't add any journal entries +-- types.Player.quests(playerRef)["ms_fargothring].stage = 0 + +--- @{#PlayerQuest} + +--- +-- @type PlayerQuest +-- @field #string id The quest ID. +-- @field #number stage The quest Stage. May only be changed by global scripts. Returns -1 if the quest has not been started or does not exist. +-- @field #bool isFinished Returns true if the quest is complete, false if not. +-- @field #string name The Quest's user friendly name. Not all quests have this. Will be nil if the quest has not been started. + +--- +-- Sets the quest stage for the given quest, on the given player, and adds the entry to the journal, if there is an entry at the specified stage. Can only be used in global scripts. +-- @function [parent=#PlayerQuest] addJournalEntry +-- @param self +-- @param openmw.core#GameObject actor The actor who is the source of the journal entry, can be the same as player, their name is used in a similar manner as in dialogue. +-- @param #number stage Quest Stage + --- @{#Armor} functions -- @field [parent=#types] #Armor Armor