diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index c5c85dae86..1cf87388a6 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -55,6 +55,11 @@ add_openmw_dir (mwscript animationextensions transformationextensions consoleextensions userextensions ) +add_openmw_dir (mwlua + luamanagerimp localscripts object worldview luabindings userdataserializer + objectbindings + ) + add_openmw_dir (mwsound soundmanagerimp openal_output ffmpeg_decoder sound sound_buffer sound_decoder sound_output loudness movieaudiofactory alext efx efx-presets regionsoundselector watersoundupdater volumesettings diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 93d3530c0a..3b7d17e69b 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -1,5 +1,6 @@ #include "engine.hpp" +#include #include #include #include @@ -46,6 +47,8 @@ #include "mwgui/windowmanagerimp.hpp" +#include "mwlua/luamanagerimp.hpp" + #include "mwscript/scriptmanagerimp.hpp" #include "mwscript/interpretercontext.hpp" @@ -101,6 +104,7 @@ namespace PhysicsWorker, World, Gui, + Lua, Number, }; @@ -138,6 +142,9 @@ namespace template <> const UserStats UserStatsValue::sValue {"Gui", "gui"}; + template <> + const UserStats UserStatsValue::sValue {"Lua", "lua"}; + template struct ForEachUserStatsValue { @@ -700,6 +707,9 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) mViewer->addEventHandler(mScreenCaptureHandler); + mLuaManager = new MWLua::LuaManager(mVFS.get()); + mEnvironment.setLuaManager(mLuaManager); + // Create input and UI first to set up a bootstrapping environment for // showing a loading screen and keeping the window responsive while doing so @@ -811,6 +821,8 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) << 100*static_cast (result.second)/result.first << "%)"; } + + mLuaManager->init(); } // Initialise and enter main loop. @@ -895,6 +907,28 @@ void OMW::Engine::go() mEnvironment.getWindowManager()->executeInConsole(mStartupScript); } + // Start Lua scripting thread + std::atomic_bool luaUpdateRequest; + double luaDt = 0; + std::thread scriptingThread([&]() { + const osg::Timer* const timer = osg::Timer::instance(); + osg::Stats* const stats = mViewer->getViewerStats(); + while (!mViewer->done() && !mEnvironment.getStateManager()->hasQuitRequest()) + { + while (!luaUpdateRequest) + std::this_thread::yield(); + + { + const osg::Timer_t frameStart = mViewer->getStartTick(); + const unsigned int frameNumber = mViewer->getFrameStamp()->getFrameNumber(); + ScopedProfile profile(frameStart, frameNumber, *timer, *stats); + + mLuaManager->update(mEnvironment.getWindowManager()->isGuiMode(), luaDt); + } + luaUpdateRequest = false; + } + }); + // Start the main rendering loop double simulationTime = 0.0; Misc::FrameRateLimiter frameRateLimiter = Misc::makeFrameRateLimiter(mEnvironment.getFrameRateLimit()); @@ -920,8 +954,17 @@ void OMW::Engine::go() mEnvironment.getWorld()->updateWindowManager(); + // scriptingThread starts processing Lua scripts + luaDt = dt; + luaUpdateRequest = true; + mViewer->renderingTraversals(); + // wait for scriptingThread to finish + while (luaUpdateRequest) + std::this_thread::yield(); + mLuaManager->applyQueuedChanges(); + bool guiActive = mEnvironment.getWindowManager()->isGuiMode(); if (!guiActive) simulationTime += dt; @@ -943,6 +986,8 @@ void OMW::Engine::go() frameRateLimiter.limit(); } + scriptingThread.join(); + // Save user settings settings.saveUser(settingspath); diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index 49ae92abc1..efa8c688dd 100644 --- a/apps/openmw/engine.hpp +++ b/apps/openmw/engine.hpp @@ -33,6 +33,11 @@ namespace Compiler class Context; } +namespace MWLua +{ + class LuaManager; +} + namespace Files { struct ConfigurationManager; @@ -85,6 +90,8 @@ namespace OMW Compiler::Extensions mExtensions; Compiler::Context *mScriptContext; + MWLua::LuaManager* mLuaManager; + Files::Collections mFileCollections; bool mFSStrict; Translation::Storage mTranslationDataStorage; diff --git a/apps/openmw/mwbase/environment.cpp b/apps/openmw/mwbase/environment.cpp index b7235edd4b..71940f67d2 100644 --- a/apps/openmw/mwbase/environment.cpp +++ b/apps/openmw/mwbase/environment.cpp @@ -13,13 +13,14 @@ #include "inputmanager.hpp" #include "windowmanager.hpp" #include "statemanager.hpp" +#include "luamanager.hpp" MWBase::Environment *MWBase::Environment::sThis = nullptr; MWBase::Environment::Environment() : mWorld (nullptr), mSoundManager (nullptr), mScriptManager (nullptr), mWindowManager (nullptr), mMechanicsManager (nullptr), mDialogueManager (nullptr), mJournal (nullptr), mInputManager (nullptr), - mStateManager (nullptr), mResourceSystem (nullptr), mFrameDuration (0), mFrameRateLimit(0.f) + mStateManager (nullptr), mLuaManager (nullptr), mResourceSystem (nullptr), mFrameDuration (0), mFrameRateLimit(0.f) { assert (!sThis); sThis = this; @@ -76,6 +77,11 @@ void MWBase::Environment::setStateManager (StateManager *stateManager) mStateManager = stateManager; } +void MWBase::Environment::setLuaManager (LuaManager *luaManager) +{ + mLuaManager = luaManager; +} + void MWBase::Environment::setResourceSystem (Resource::ResourceSystem *resourceSystem) { mResourceSystem = resourceSystem; @@ -150,6 +156,12 @@ MWBase::StateManager *MWBase::Environment::getStateManager() const return mStateManager; } +MWBase::LuaManager *MWBase::Environment::getLuaManager() const +{ + assert (mLuaManager); + return mLuaManager; +} + Resource::ResourceSystem *MWBase::Environment::getResourceSystem() const { return mResourceSystem; @@ -188,6 +200,9 @@ void MWBase::Environment::cleanup() delete mStateManager; mStateManager = nullptr; + + delete mLuaManager; + mLuaManager = nullptr; } const MWBase::Environment& MWBase::Environment::get() diff --git a/apps/openmw/mwbase/environment.hpp b/apps/openmw/mwbase/environment.hpp index 3b57e4e7c1..b2600e7dd5 100644 --- a/apps/openmw/mwbase/environment.hpp +++ b/apps/openmw/mwbase/environment.hpp @@ -22,6 +22,7 @@ namespace MWBase class InputManager; class WindowManager; class StateManager; + class LuaManager; /// \brief Central hub for mw-subsystems /// @@ -42,6 +43,7 @@ namespace MWBase Journal *mJournal; InputManager *mInputManager; StateManager *mStateManager; + LuaManager *mLuaManager; Resource::ResourceSystem *mResourceSystem; float mFrameDuration; float mFrameRateLimit; @@ -76,6 +78,8 @@ namespace MWBase void setStateManager (StateManager *stateManager); + void setLuaManager (LuaManager *luaManager); + void setResourceSystem (Resource::ResourceSystem *resourceSystem); void setFrameDuration (float duration); @@ -102,6 +106,8 @@ namespace MWBase StateManager *getStateManager() const; + LuaManager *getLuaManager() const; + Resource::ResourceSystem *getResourceSystem() const; float getFrameDuration() const; diff --git a/apps/openmw/mwbase/luamanager.hpp b/apps/openmw/mwbase/luamanager.hpp new file mode 100644 index 0000000000..ad374c9dba --- /dev/null +++ b/apps/openmw/mwbase/luamanager.hpp @@ -0,0 +1,42 @@ +#ifndef GAME_MWBASE_LUAMANAGER_H +#define GAME_MWBASE_LUAMANAGER_H + +#include + +namespace MWWorld +{ + class Ptr; +} + +namespace MWBase +{ + + class LuaManager + { + public: + virtual ~LuaManager() = default; + + virtual void newGameStarted() = 0; + virtual void objectAddedToScene(const MWWorld::Ptr& ptr) = 0; + virtual void objectRemovedFromScene(const MWWorld::Ptr& ptr) = 0; + virtual void keyPressed(const SDL_KeyboardEvent &arg) = 0; + + struct ActorControls { + bool controlledFromLua; + + bool jump; + bool run; + float movement; + float sideMovement; + float turn; + }; + + virtual const ActorControls* getActorControls(const MWWorld::Ptr&) const = 0; + + virtual void clear() = 0; + virtual void setupPlayer(const MWWorld::Ptr&) = 0; + }; + +} + +#endif // GAME_MWBASE_LUAMANAGER_H diff --git a/apps/openmw/mwlua/eventqueue.hpp b/apps/openmw/mwlua/eventqueue.hpp new file mode 100644 index 0000000000..cd79e05dc0 --- /dev/null +++ b/apps/openmw/mwlua/eventqueue.hpp @@ -0,0 +1,23 @@ +#ifndef MWLUA_EVENTQUEUE_H +#define MWLUA_EVENTQUEUE_H + +#include "object.hpp" + +namespace MWLua +{ + struct GlobalEvent + { + std::string eventName; + std::string eventData; + }; + struct LocalEvent + { + ObjectId dest; + std::string eventName; + std::string eventData; + }; + using GlobalEventQueue = std::vector; + using LocalEventQueue = std::vector; +} + +#endif // MWLUA_EVENTQUEUE_H diff --git a/apps/openmw/mwlua/globalscripts.hpp b/apps/openmw/mwlua/globalscripts.hpp new file mode 100644 index 0000000000..9a371809ac --- /dev/null +++ b/apps/openmw/mwlua/globalscripts.hpp @@ -0,0 +1,36 @@ +#ifndef MWLUA_GLOBALSCRIPTS_H +#define MWLUA_GLOBALSCRIPTS_H + +#include +#include +#include + +#include +#include + +#include "object.hpp" + +namespace MWLua +{ + + class GlobalScripts : public LuaUtil::ScriptsContainer + { + public: + GlobalScripts(LuaUtil::LuaState* lua) : LuaUtil::ScriptsContainer(lua, "Global") + { + registerEngineHandlers({&mActorActiveHandlers, &mNewGameHandlers, &mPlayerAddedHandlers}); + } + + void newGameStarted() { callEngineHandlers(mNewGameHandlers); } + void actorActive(const GObject& obj) { callEngineHandlers(mActorActiveHandlers, obj); } + void playerAdded(const GObject& obj) { callEngineHandlers(mPlayerAddedHandlers, obj); } + + private: + EngineHandlerList mActorActiveHandlers{"onActorActive"}; + EngineHandlerList mNewGameHandlers{"onNewGame"}; + EngineHandlerList mPlayerAddedHandlers{"onPlayerAdded"}; + }; + +} + +#endif // MWLUA_GLOBALSCRIPTS_H diff --git a/apps/openmw/mwlua/localscripts.cpp b/apps/openmw/mwlua/localscripts.cpp new file mode 100644 index 0000000000..1c35b60131 --- /dev/null +++ b/apps/openmw/mwlua/localscripts.cpp @@ -0,0 +1,44 @@ +#include "localscripts.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + + void LocalScripts::initializeSelfPackage(const Context& context) + { + using ActorControls = MWBase::LuaManager::ActorControls; + sol::usertype controls = context.mLua->sol().new_usertype("ActorControls"); + controls["movement"] = &ActorControls::movement; + controls["sideMovement"] = &ActorControls::sideMovement; + controls["turn"] = &ActorControls::turn; + controls["run"] = &ActorControls::run; + controls["jump"] = &ActorControls::jump; + + sol::usertype selfAPI = + context.mLua->sol().new_usertype("SelfObject", sol::base_classes, sol::bases()); + selfAPI[sol::meta_function::to_string] = [](SelfObject& self) { return "openmw.self[" + self.toString() + "]"; }; + selfAPI["object"] = sol::readonly_property([](SelfObject& self) -> LObject { return LObject(self); }); + selfAPI["controls"] = sol::readonly_property([](SelfObject& self) { return &self.mControls; }); + selfAPI["setDirectControl"] = [](SelfObject& self, bool v) { self.mControls.controlledFromLua = v; }; + } + + std::unique_ptr LocalScripts::create(LuaUtil::LuaState* lua, const LObject& obj) + { + return std::unique_ptr(new LocalScripts(lua, obj)); + } + + LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj) + : LuaUtil::ScriptsContainer(lua, "L" + idToString(obj.id())), mData(obj) + { + mData.mControls.controlledFromLua = false; + this->addPackage("openmw.self", sol::make_object(lua->sol(), &mData)); + } + +} diff --git a/apps/openmw/mwlua/localscripts.hpp b/apps/openmw/mwlua/localscripts.hpp new file mode 100644 index 0000000000..c8d85aad58 --- /dev/null +++ b/apps/openmw/mwlua/localscripts.hpp @@ -0,0 +1,39 @@ +#ifndef MWLUA_LOCALSCRIPTS_H +#define MWLUA_LOCALSCRIPTS_H + +#include +#include +#include + +#include +#include + +#include "../mwbase/luamanager.hpp" + +#include "object.hpp" +#include "luabindings.hpp" + +namespace MWLua +{ + + class LocalScripts : public LuaUtil::ScriptsContainer + { + public: + static std::unique_ptr create(LuaUtil::LuaState* lua, const LObject& obj); + static void initializeSelfPackage(const Context&); + + const MWBase::LuaManager::ActorControls* getActorControls() const { return &mData.mControls; } + + struct SelfObject : public LObject + { + SelfObject(const LObject& obj) : LObject(obj) {} + MWBase::LuaManager::ActorControls mControls; + }; + protected: + LocalScripts(LuaUtil::LuaState* lua, const LObject& obj); + SelfObject mData; + }; + +} + +#endif // MWLUA_LOCALSCRIPTS_H diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp new file mode 100644 index 0000000000..4f6519397e --- /dev/null +++ b/apps/openmw/mwlua/luabindings.cpp @@ -0,0 +1,39 @@ +#include "luabindings.hpp" + +#include + +#include "eventqueue.hpp" +#include "worldview.hpp" + +namespace MWLua +{ + + sol::table initCorePackage(const Context& context) + { + sol::table api(context.mLua->sol(), sol::create); + api["sendGlobalEvent"] = [context](std::string eventName, const sol::object& eventData) + { + context.mGlobalEventQueue->push_back({std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer)}); + }; + return context.mLua->makeReadOnly(api); + } + + sol::table initWorldPackage(const Context& context) + { + sol::table api(context.mLua->sol(), sol::create); + WorldView* worldView = context.mWorldView; + api["activeActors"] = GObjectList{worldView->getActorsInScene()}; + return context.mLua->makeReadOnly(api); + } + + sol::table initNearbyPackage(const Context& context) + { + sol::table api(context.mLua->sol(), sol::create); + WorldView* worldView = context.mWorldView; + api["actors"] = LObjectList{worldView->getActorsInScene()}; + api["items"] = LObjectList{worldView->getItemsInScene()}; + return context.mLua->makeReadOnly(api); + } + +} + diff --git a/apps/openmw/mwlua/luabindings.hpp b/apps/openmw/mwlua/luabindings.hpp new file mode 100644 index 0000000000..7e544b7eb4 --- /dev/null +++ b/apps/openmw/mwlua/luabindings.hpp @@ -0,0 +1,36 @@ +#ifndef MWLUA_LUABINDINGS_H +#define MWLUA_LUABINDINGS_H + +#include +#include + +#include "eventqueue.hpp" +#include "object.hpp" +#include "worldview.hpp" + +namespace MWLua +{ + class LuaManager; + + struct Context + { + LuaManager* mLuaManager; + LuaUtil::LuaState* mLua; + LuaUtil::UserdataSerializer* mSerializer; + WorldView* mWorldView; + LocalEventQueue* mLocalEventQueue; + GlobalEventQueue* mGlobalEventQueue; + }; + + sol::table initCorePackage(const Context&); + sol::table initWorldPackage(const Context&); + sol::table initNearbyPackage(const Context&); + + // Implemented in objectbindings.cpp + void initObjectBindingsForLocalScripts(const Context&); + void initObjectBindingsForGlobalScripts(const Context&); + + // openmw.self package is implemented in localscripts.cpp +} + +#endif // MWLUA_LUABINDINGS_H diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp new file mode 100644 index 0000000000..023e95e7b4 --- /dev/null +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -0,0 +1,202 @@ +#include "luamanagerimp.hpp" + +#include +#include + +#include "../mwworld/class.hpp" +#include "../mwworld/ptr.hpp" + +#include "luabindings.hpp" +#include "userdataserializer.hpp" + +namespace MWLua +{ + + LuaManager::LuaManager(const VFS::Manager* vfs) : mLua(vfs) + { + Log(Debug::Info) << "Lua version: " << LuaUtil::getLuaVersion(); + mGlobalSerializer = createUserdataSerializer(false, mWorldView.getObjectRegistry()); + mLocalSerializer = createUserdataSerializer(true, mWorldView.getObjectRegistry()); + mGlobalScripts.setSerializer(mGlobalSerializer.get()); + + Context context; + context.mLuaManager = this; + context.mLua = &mLua; + context.mWorldView = &mWorldView; + context.mLocalEventQueue = &mLocalEvents; + context.mGlobalEventQueue = &mGlobalEvents; + context.mSerializer = mGlobalSerializer.get(); + + Context localContext = context; + localContext.mSerializer = mLocalSerializer.get(); + + initObjectBindingsForGlobalScripts(context); + initObjectBindingsForLocalScripts(localContext); + LocalScripts::initializeSelfPackage(localContext); + + mLua.addCommonPackage("openmw.util", LuaUtil::initUtilPackage(mLua.sol())); + mLua.addCommonPackage("openmw.core", initCorePackage(context)); + mGlobalScripts.addPackage("openmw.world", initWorldPackage(context)); + mNearbyPackage = initNearbyPackage(localContext); + } + + void LuaManager::init() + { + mKeyPressEvents.clear(); + if (mGlobalScripts.addNewScript("test.lua")) + Log(Debug::Info) << "Global script started: test.lua"; + } + + void LuaManager::update(bool paused, float dt) + { + mWorldView.update(); + + if (paused) + { + mKeyPressEvents.clear(); + return; + } + + std::vector globalEvents = std::move(mGlobalEvents); + std::vector localEvents = std::move(mLocalEvents); + mGlobalEvents = std::vector(); + mLocalEvents = std::vector(); + + for (GlobalEvent& e : globalEvents) + mGlobalScripts.receiveEvent(e.eventName, e.eventData); + for (LocalEvent& e : localEvents) + { + LObject obj(e.dest, mWorldView.getObjectRegistry()); + LocalScripts* scripts = obj.isValid() ? obj.ptr().getRefData().getLuaScripts() : nullptr; + if (scripts) + scripts->receiveEvent(e.eventName, e.eventData); + else + Log(Debug::Debug) << "Ignored event " << e.eventName << " to L" << idToString(e.dest) + << ". Object not found or has no attached scripts"; + } + + if (mPlayerChanged) + { + mPlayerChanged = false; + mGlobalScripts.playerAdded(GObject(getId(mPlayer), mWorldView.getObjectRegistry())); + } + + if (mPlayerScripts) + { + for (const SDL_Keysym key : mKeyPressEvents) + mPlayerScripts->keyPress(key.sym, key.mod); + } + mKeyPressEvents.clear(); + + for (ObjectId id : mActorAddedEvents) + mGlobalScripts.actorActive(GObject(id, mWorldView.getObjectRegistry())); + mActorAddedEvents.clear(); + + mGlobalScripts.update(dt); + for (LocalScripts* scripts : mActiveLocalScripts) + scripts->update(dt); + } + + void LuaManager::applyQueuedChanges() + { + } + + void LuaManager::clear() + { + mActiveLocalScripts.clear(); + mLocalEvents.clear(); + mGlobalEvents.clear(); + mKeyPressEvents.clear(); + mActorAddedEvents.clear(); + mPlayerChanged = false; + mPlayerScripts = nullptr; + mWorldView.clear(); + if (!mPlayer.isEmpty()) + { + mPlayer.getCellRef().unsetRefNum(); + mPlayer.getRefData().setLuaScripts(nullptr); + mPlayer = MWWorld::Ptr(); + } + } + + void LuaManager::objectAddedToScene(const MWWorld::Ptr& ptr) + { + mWorldView.objectAddedToScene(ptr); // assigns generated RefNum if it is not set yet. + + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (localScripts) + mActiveLocalScripts.insert(localScripts); + + if (ptr.getClass().isActor() && ptr != mPlayer) + mActorAddedEvents.push_back(getId(ptr)); + } + + void LuaManager::setupPlayer(const MWWorld::Ptr& ptr) + { + if (!mPlayer.isEmpty()) + throw std::logic_error("Player is initialized twice"); + mWorldView.objectAddedToScene(ptr); + mPlayer = ptr; + MWWorld::RefData& refData = ptr.getRefData(); + if (!refData.getLuaScripts()) + createLocalScripts(ptr); + if (!mPlayerScripts) + throw std::logic_error("mPlayerScripts not initialized"); + mActiveLocalScripts.insert(mPlayerScripts); + mPlayerChanged = true; + } + + void LuaManager::objectRemovedFromScene(const MWWorld::Ptr& ptr) + { + mWorldView.objectRemovedFromScene(ptr); + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (localScripts) + mActiveLocalScripts.erase(localScripts); + + // TODO: call mWorldView.objectUnloaded if object is unloaded from memory (does it ever happen?) and ptr becomes invalid. + } + + void LuaManager::keyPressed(const SDL_KeyboardEvent& arg) + { + mKeyPressEvents.push_back(arg.keysym); + } + + const MWBase::LuaManager::ActorControls* LuaManager::getActorControls(const MWWorld::Ptr& ptr) const + { + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (!localScripts) + return nullptr; + return localScripts->getActorControls(); + } + + void LuaManager::addLocalScript(const MWWorld::Ptr& ptr, const std::string& scriptPath) + { + MWWorld::RefData& refData = ptr.getRefData(); + if (!refData.getLuaScripts()) + mActiveLocalScripts.insert(createLocalScripts(ptr)); + refData.getLuaScripts()->addNewScript(scriptPath); + } + + LocalScripts* LuaManager::createLocalScripts(const MWWorld::Ptr& ptr) + { + std::unique_ptr scripts; + // When loading a game, it can be called before LuaManager::setPlayer, + // so we can't just check ptr == mPlayer here. + if (*ptr.getCellRef().getRefIdPtr() == "player") + { + mPlayerScripts = new PlayerScripts(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry())); + scripts = std::unique_ptr(mPlayerScripts); + // TODO: scripts->addPackage("openmw.ui", ...); + // TODO: scripts->addPackage("openmw.camera", ...); + } + else + scripts = LocalScripts::create(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry())); + scripts->addPackage("openmw.nearby", mNearbyPackage); + scripts->setSerializer(mLocalSerializer.get()); + + MWWorld::RefData& refData = ptr.getRefData(); + refData.setLuaScripts(std::move(scripts)); + return refData.getLuaScripts(); + } + +} diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp new file mode 100644 index 0000000000..3420ac5dbb --- /dev/null +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -0,0 +1,78 @@ +#ifndef MWLUA_LUAMANAGERIMP_H +#define MWLUA_LUAMANAGERIMP_H + +#include +#include + +#include + +#include "../mwbase/luamanager.hpp" + +#include "object.hpp" +#include "eventqueue.hpp" +#include "globalscripts.hpp" +#include "localscripts.hpp" +#include "playerscripts.hpp" +#include "worldview.hpp" + +namespace MWLua +{ + + class LuaManager : public MWBase::LuaManager + { + public: + LuaManager(const VFS::Manager* vfs); + ~LuaManager() {} + + // Called by engine.cpp when environment is fully initialized. + void init(); + + // Called by engine.cpp every frame. For performance reasons it works in a separate + // thread (in parallel with osg Cull). Can not use scene graph. + void update(bool paused, float dt); + + // Called by engine.cpp from the main thread. Can use scene graph. + void applyQueuedChanges(); + + // Available everywhere through the MWBase::LuaManager interface. + // LuaManager queues these events and propagates to scripts on the next `update` call. + void newGameStarted() override { mGlobalScripts.newGameStarted(); } + void objectAddedToScene(const MWWorld::Ptr& ptr) override; + void objectRemovedFromScene(const MWWorld::Ptr& ptr) override; + void keyPressed(const SDL_KeyboardEvent &arg) override; + + const MWBase::LuaManager::ActorControls* getActorControls(const MWWorld::Ptr&) const override; + + void clear() override; // should be called before loading game or starting a new game to reset internal state. + void setupPlayer(const MWWorld::Ptr& ptr) override; // Should be called once after each "clear". + + // Used only in luabindings.cpp + void addLocalScript(const MWWorld::Ptr&, const std::string& scriptPath); + + private: + LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr); + + LuaUtil::LuaState mLua; + sol::table mNearbyPackage; + + GlobalScripts mGlobalScripts{&mLua}; + std::set mActiveLocalScripts; + WorldView mWorldView; + + bool mPlayerChanged = false; + MWWorld::Ptr mPlayer; + PlayerScripts* mPlayerScripts = nullptr; + + GlobalEventQueue mGlobalEvents; + LocalEventQueue mLocalEvents; + + std::unique_ptr mGlobalSerializer; + std::unique_ptr mLocalSerializer; + + std::vector mKeyPressEvents; + std::vector mActorAddedEvents; + }; + +} + +#endif // MWLUA_LUAMANAGERIMP_H diff --git a/apps/openmw/mwlua/object.cpp b/apps/openmw/mwlua/object.cpp new file mode 100644 index 0000000000..fa0f9daffc --- /dev/null +++ b/apps/openmw/mwlua/object.cpp @@ -0,0 +1,111 @@ +#include "object.hpp" + +#include +#include + +namespace MWLua +{ + + std::string idToString(const ObjectId& id) + { + return std::to_string(id.mIndex) + "_" + std::to_string(id.mContentFile); + } + + std::string Object::toString() const + { + std::string res = idToString(mId); + if (isValid()) + { + res.append(" ("); + res.append(type()); + res.append(", "); + res.append(*ptr().getCellRef().getRefIdPtr()); + res.append(")"); + } + else + res.append(" (not found)"); + return res; + } + + std::string_view Object::type() const + { + if (*ptr().getCellRef().getRefIdPtr() == "player") + return "Player"; + const std::string& typeName = ptr().getTypeName(); + if (typeName == typeid(ESM::NPC).name()) + return "NPC"; + else if (typeName == typeid(ESM::Creature).name()) + return "Creature"; + else + return typeName; + } + + bool Object::isValid() const + { + if (mLastUpdate < mObjectRegistry->mUpdateCounter) + { + updatePtr(); + mLastUpdate = mObjectRegistry->mUpdateCounter; + } + return !mPtr.isEmpty(); + } + + const MWWorld::Ptr& Object::ptr() const + { + if (!isValid()) + throw std::runtime_error("Object is not available: " + idToString(mId)); + return mPtr; + } + + void ObjectRegistry::update() + { + if (mChanged) + { + mUpdateCounter++; + mChanged = false; + } + } + + void ObjectRegistry::clear() + { + mObjectMapping.clear(); + mChanged = false; + mUpdateCounter = 0; + mLastAssignedId.unset(); + } + + MWWorld::Ptr ObjectRegistry::getPtr(ObjectId id, bool onlyActive) + { + MWWorld::Ptr ptr; + auto it = mObjectMapping.find(id); + if (it != mObjectMapping.end()) + ptr = it->second; + if (onlyActive) + { + // TODO: add flag `isActive` to LiveCellRefBase. Return empty Ptr if the flag is not set. + // Needed because in multiplayer mode inactive objects will not be synchronized, so will likely be out of date. + } + else + { + // TODO: If Ptr is empty then try to load the object from esp/esm. + } + return ptr; + } + + ObjectId ObjectRegistry::registerPtr(const MWWorld::Ptr& ptr) + { + ObjectId id = ptr.getCellRef().getOrAssignRefNum(mLastAssignedId); + mChanged = true; + mObjectMapping[id] = ptr; + return id; + } + + ObjectId ObjectRegistry::deregisterPtr(const MWWorld::Ptr& ptr) + { + ObjectId id = getId(ptr); + mChanged = true; + mObjectMapping.erase(id); + return id; + } + +} diff --git a/apps/openmw/mwlua/object.hpp b/apps/openmw/mwlua/object.hpp new file mode 100644 index 0000000000..59e6534166 --- /dev/null +++ b/apps/openmw/mwlua/object.hpp @@ -0,0 +1,103 @@ +#ifndef MWLUA_OBJECT_H +#define MWLUA_OBJECT_H + +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +#include "../mwworld/ptr.hpp" + +namespace MWLua +{ + // ObjectId is a unique identifier of a game object. + // It can change only if the order of content files was change. + using ObjectId = ESM::RefNum; + inline const ObjectId& getId(const MWWorld::Ptr& ptr) { return ptr.getCellRef().getRefNum(); } + std::string idToString(const ObjectId& id); + + // Holds a mapping ObjectId -> MWWord::Ptr. + class ObjectRegistry + { + public: + ObjectRegistry() { mLastAssignedId.unset(); } + + void update(); // Should be called every frame. + void clear(); // Should be called before starting or loading a new game. + + ObjectId registerPtr(const MWWorld::Ptr& ptr); + ObjectId deregisterPtr(const MWWorld::Ptr& ptr); + + // Returns Ptr by id. If object is not found, returns empty Ptr. + // If onlyActive = true, returns non-empty ptr only if it is registered and is in an active cell. + // If onlyActive = false, tries to load and register the object if it is not loaded yet. + // NOTE: `onlyActive` logic is not yet implemented. + MWWorld::Ptr getPtr(ObjectId id, bool onlyActive); + + // Needed only for saving/loading. + const ObjectId& getLastAssignedId() const { return mLastAssignedId; } + void setLastAssignedId(ObjectId id) { mLastAssignedId = id; } + + private: + friend class Object; + friend class GObject; + friend class LObject; + + bool mChanged = false; + int64_t mUpdateCounter = 0; + std::map mObjectMapping; + ObjectId mLastAssignedId; + }; + + // Lua scripts can't use MWWorld::Ptr directly, because lifetime of a script can be longer than lifetime of Ptr. + // `GObject` and `LObject` are intended to be passed to Lua as a userdata. + // It automatically updates the underlying Ptr when needed. + class Object + { + public: + Object(ObjectId id, ObjectRegistry* reg) : mId(id), mObjectRegistry(reg) {} + virtual ~Object() {} + ObjectId id() const { return mId; } + + std::string toString() const; + std::string_view type() const; + + // Updates and returns the underlying Ptr. Throws an exception if object is not available. + const MWWorld::Ptr& ptr() const; + + // Returns `true` if calling `ptr()` is safe. + bool isValid() const; + + protected: + virtual void updatePtr() const = 0; + + const ObjectId mId; + ObjectRegistry* mObjectRegistry; + + mutable MWWorld::Ptr mPtr; + mutable int64_t mLastUpdate = -1; + }; + + // Used only in local scripts + class LObject : public Object + { + using Object::Object; + void updatePtr() const final { mPtr = mObjectRegistry->getPtr(mId, true); } + }; + + // Used only in global scripts + class GObject : public Object + { + using Object::Object; + void updatePtr() const final { mPtr = mObjectRegistry->getPtr(mId, false); } + }; + + using ObjectIdList = std::shared_ptr>; + template + struct ObjectList { ObjectIdList mIds; }; + using GObjectList = ObjectList; + using LObjectList = ObjectList; + +} + +#endif // MWLUA_OBJECT_H diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp new file mode 100644 index 0000000000..806907ef35 --- /dev/null +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -0,0 +1,106 @@ +#include "luabindings.hpp" + +#include + +#include "eventqueue.hpp" +#include "luamanagerimp.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; + template <> + struct is_automagical : std::false_type {}; + template <> + struct is_automagical : std::false_type {}; + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + + template + static void registerObjectList(const std::string& prefix, const Context& context) + { + using ListT = ObjectList; + sol::state& lua = context.mLua->sol(); + ObjectRegistry* registry = context.mWorldView->getObjectRegistry(); + sol::usertype listT = lua.new_usertype(prefix + "ObjectList"); + listT[sol::meta_function::to_string] = + [](const ListT& list) { return "{" + std::to_string(list.mIds->size()) + " objects}"; }; + listT[sol::meta_function::length] = [](const ListT& list) { return list.mIds->size(); }; + listT[sol::meta_function::index] = [registry](const ListT& list, size_t index) + { + if (index > 0 && index <= list.mIds->size()) + return ObjectT((*list.mIds)[index - 1], registry); + else + throw std::runtime_error("Index out of range"); + }; + listT["ipairs"] = [registry](const ListT& list) + { + auto iter = [registry](const ListT& l, int64_t i) -> sol::optional> + { + if (i >= 0 && i < static_cast(l.mIds->size())) + return std::make_tuple(i + 1, ObjectT((*l.mIds)[i], registry)); + else + return sol::nullopt; + }; + return std::make_tuple(iter, list, 0); + }; + } + + template + static void addBasicBindings(sol::usertype& objectT, const Context& context) + { + objectT["isValid"] = [](const ObjectT& o) { return o.isValid(); }; + objectT["recordId"] = sol::readonly_property([](const ObjectT& o) -> std::string + { + return o.ptr().getCellRef().getRefId(); + }); + objectT["position"] = sol::readonly_property([](const ObjectT& o) -> osg::Vec3f + { + return o.ptr().getRefData().getPosition().asVec3(); + }); + objectT["rotation"] = sol::readonly_property([](const ObjectT& o) -> osg::Vec3f + { + return o.ptr().getRefData().getPosition().asRotationVec3(); + }); + objectT["type"] = sol::readonly_property(&ObjectT::type); + objectT[sol::meta_function::equal_to] = [](const ObjectT& a, const ObjectT& b) { return a.id() == b.id(); }; + objectT[sol::meta_function::to_string] = &ObjectT::toString; + objectT["sendEvent"] = [context](const ObjectT& dest, std::string eventName, const sol::object& eventData) + { + context.mLocalEventQueue->push_back({dest.id(), std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer)}); + }; + + if constexpr (std::is_same_v) + { // Only for global scripts + objectT["addScript"] = [luaManager=context.mLuaManager](const GObject& object, const std::string& path) + { + luaManager->addLocalScript(object.ptr(), path); + }; + } + } + + template + static void initObjectBindings(const std::string& prefix, const Context& context) + { + sol::usertype objectT = context.mLua->sol().new_usertype(prefix + "Object"); + addBasicBindings(objectT, context); + + registerObjectList(prefix, context); + } + + void initObjectBindingsForLocalScripts(const Context& context) + { + initObjectBindings("L", context); + } + + void initObjectBindingsForGlobalScripts(const Context& context) + { + initObjectBindings("G", context); + } + +} + diff --git a/apps/openmw/mwlua/playerscripts.hpp b/apps/openmw/mwlua/playerscripts.hpp new file mode 100644 index 0000000000..9a08f917e7 --- /dev/null +++ b/apps/openmw/mwlua/playerscripts.hpp @@ -0,0 +1,25 @@ +#ifndef MWLUA_PLAYERSCRIPTS_H +#define MWLUA_PLAYERSCRIPTS_H + +#include "localscripts.hpp" + +namespace MWLua +{ + + class PlayerScripts : public LocalScripts + { + public: + PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) : LocalScripts(lua, obj) + { + registerEngineHandlers({&mKeyPressHandlers}); + } + + void keyPress(int sym, int mod) { callEngineHandlers(mKeyPressHandlers, sym, mod); } + + private: + EngineHandlerList mKeyPressHandlers{"onKeyPress"}; + }; + +} + +#endif // MWLUA_PLAYERSCRIPTS_H diff --git a/apps/openmw/mwlua/userdataserializer.cpp b/apps/openmw/mwlua/userdataserializer.cpp new file mode 100644 index 0000000000..a0315d4777 --- /dev/null +++ b/apps/openmw/mwlua/userdataserializer.cpp @@ -0,0 +1,64 @@ +#include "userdataserializer.hpp" + +#include +#include + +#include "object.hpp" + +namespace MWLua +{ + + class Serializer final : public LuaUtil::UserdataSerializer + { + public: + explicit Serializer(bool localSerializer, ObjectRegistry* registry) + : mLocalSerializer(localSerializer), mObjectRegistry(registry) {} + + private: + // Appends serialized sol::userdata to the end of BinaryData. + // Returns false if this type of userdata is not supported by this serializer. + bool serialize(LuaUtil::BinaryData& out, const sol::userdata& data) const override + { + if (data.is() || data.is()) + { + ObjectId id = data.as().id(); + static_assert(sizeof(ObjectId) == 8); + id.mIndex = Misc::toLittleEndian(id.mIndex); + id.mContentFile = Misc::toLittleEndian(id.mContentFile); + append(out, "o", &id, sizeof(ObjectId)); + return true; + } + return false; + } + + // Deserializes userdata of type "typeName" from binaryData. Should push the result on stack using sol::stack::push. + // Returns false if this type is not supported by this serializer. + bool deserialize(std::string_view typeName, std::string_view binaryData, sol::state& lua) const override + { + if (typeName == "o") + { + if (binaryData.size() != sizeof(ObjectId)) + throw std::runtime_error("Incorrect serialization format. Size of ObjectId doesn't match."); + ObjectId id; + std::memcpy(&id, binaryData.data(), sizeof(ObjectId)); + id.mIndex = Misc::fromLittleEndian(id.mIndex); + id.mContentFile = Misc::fromLittleEndian(id.mContentFile); + if (mLocalSerializer) + sol::stack::push(lua, LObject(id, mObjectRegistry)); + else + sol::stack::push(lua, GObject(id, mObjectRegistry)); + return true; + } + return false; + } + + bool mLocalSerializer; + ObjectRegistry* mObjectRegistry; + }; + + std::unique_ptr createUserdataSerializer(bool local, ObjectRegistry* registry) + { + return std::make_unique(local, registry); + } + +} diff --git a/apps/openmw/mwlua/userdataserializer.hpp b/apps/openmw/mwlua/userdataserializer.hpp new file mode 100644 index 0000000000..72fab0f53b --- /dev/null +++ b/apps/openmw/mwlua/userdataserializer.hpp @@ -0,0 +1,19 @@ +#ifndef MWLUA_USERDATASERIALIZER_H +#define MWLUA_USERDATASERIALIZER_H + +#include "object.hpp" + +namespace LuaUtil +{ + class UserdataSerializer; +} + +namespace MWLua +{ + // UserdataSerializer is an extension for components/lua/serialization.hpp + // Needed to serialize references to objects. + // If local=true, then during deserialization creates LObject, otherwise creates GObject. + std::unique_ptr createUserdataSerializer(bool local, ObjectRegistry* registry); +} + +#endif // MWLUA_USERDATASERIALIZER_H diff --git a/apps/openmw/mwlua/worldview.cpp b/apps/openmw/mwlua/worldview.cpp new file mode 100644 index 0000000000..e088dda252 --- /dev/null +++ b/apps/openmw/mwlua/worldview.cpp @@ -0,0 +1,68 @@ +#include "worldview.hpp" + +#include "../mwworld/class.hpp" + +namespace MWLua +{ + + void WorldView::update() + { + mObjectRegistry.update(); + mActorsInScene.updateList(); + mItemsInScene.updateList(); + } + + void WorldView::clear() + { + mObjectRegistry.clear(); + mActorsInScene.clear(); + mItemsInScene.clear(); + } + + void WorldView::objectAddedToScene(const MWWorld::Ptr& ptr) + { + if (ptr.getClass().isActor()) + addToGroup(mActorsInScene, ptr); + else + addToGroup(mItemsInScene, ptr); + } + + void WorldView::objectRemovedFromScene(const MWWorld::Ptr& ptr) + { + if (ptr.getClass().isActor()) + removeFromGroup(mActorsInScene, ptr); + else + removeFromGroup(mItemsInScene, ptr); + } + + void WorldView::ObjectGroup::updateList() + { + if (mChanged) + { + mList->clear(); + for (const ObjectId& id : mSet) + mList->push_back(id); + mChanged = false; + } + } + + void WorldView::ObjectGroup::clear() + { + mChanged = false; + mList->clear(); + mSet.clear(); + } + + void WorldView::addToGroup(ObjectGroup& group, const MWWorld::Ptr& ptr) + { + group.mSet.insert(getId(ptr)); + group.mChanged = true; + } + + void WorldView::removeFromGroup(ObjectGroup& group, const MWWorld::Ptr& ptr) + { + group.mSet.erase(getId(ptr)); + group.mChanged = true; + } + +} diff --git a/apps/openmw/mwlua/worldview.hpp b/apps/openmw/mwlua/worldview.hpp new file mode 100644 index 0000000000..fbfefccdc6 --- /dev/null +++ b/apps/openmw/mwlua/worldview.hpp @@ -0,0 +1,47 @@ +#ifndef MWLUA_WORLDVIEW_H +#define MWLUA_WORLDVIEW_H + +#include "object.hpp" + +namespace MWLua +{ + + // Tracks all used game objects. + class WorldView + { + public: + void update(); // Should be called every frame. + void clear(); // Should be called every time before starting or loading a new game. + + ObjectIdList getActorsInScene() const { return mActorsInScene.mList; } + ObjectIdList getItemsInScene() const { return mItemsInScene.mList; } + + ObjectRegistry* getObjectRegistry() { return &mObjectRegistry; } + + void objectUnloaded(const MWWorld::Ptr& ptr) { mObjectRegistry.deregisterPtr(ptr); } + + void objectAddedToScene(const MWWorld::Ptr& ptr); + void objectRemovedFromScene(const MWWorld::Ptr& ptr); + + private: + struct ObjectGroup + { + void updateList(); + void clear(); + + bool mChanged = false; + ObjectIdList mList = std::make_shared>(); + std::set mSet; + }; + + void addToGroup(ObjectGroup& group, const MWWorld::Ptr& ptr); + void removeFromGroup(ObjectGroup& group, const MWWorld::Ptr& ptr); + + ObjectRegistry mObjectRegistry; + ObjectGroup mActorsInScene; + ObjectGroup mItemsInScene; + }; + +} + +#endif // MWLUA_WORLDVIEW_H diff --git a/apps/openmw/mwworld/refdata.cpp b/apps/openmw/mwworld/refdata.cpp index 9aa40ed4b1..b506671086 100644 --- a/apps/openmw/mwworld/refdata.cpp +++ b/apps/openmw/mwworld/refdata.cpp @@ -8,6 +8,8 @@ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" +#include "../mwlua/localscripts.hpp" + namespace { enum RefDataFlags @@ -21,6 +23,12 @@ enum RefDataFlags namespace MWWorld { + void RefData::setLuaScripts(std::unique_ptr&& scripts) + { + mChanged = true; + mLuaScripts = std::move(scripts); + } + void RefData::copy (const RefData& refData) { mBaseNode = refData.mBaseNode; @@ -36,12 +44,14 @@ namespace MWWorld mAnimationState = refData.mAnimationState; mCustomData = refData.mCustomData ? refData.mCustomData->clone() : nullptr; + mLuaScripts = refData.mLuaScripts; } void RefData::cleanup() { mBaseNode = nullptr; mCustomData = nullptr; + mLuaScripts = nullptr; } RefData::RefData() diff --git a/apps/openmw/mwworld/refdata.hpp b/apps/openmw/mwworld/refdata.hpp index 640670d8e9..3ae6a03293 100644 --- a/apps/openmw/mwworld/refdata.hpp +++ b/apps/openmw/mwworld/refdata.hpp @@ -22,6 +22,11 @@ namespace ESM struct ObjectState; } +namespace MWLua +{ + class LocalScripts; +} + namespace MWWorld { @@ -32,6 +37,7 @@ namespace MWWorld SceneUtil::PositionAttitudeTransform* mBaseNode; MWScript::Locals mLocals; + std::shared_ptr mLuaScripts; /// separate delete flag used for deletion by a content file /// @note not stored in the save game file. @@ -96,6 +102,9 @@ namespace MWWorld void setLocals (const ESM::Script& script); + MWLua::LocalScripts* getLuaScripts() { return mLuaScripts.get(); } + void setLuaScripts(std::unique_ptr&&); + void setCount (int count); ///< Set object count (an object pile is a simple object with a count >1). ///