diff --git a/.gitignore b/.gitignore index 1905957d9b..5c07d3e54a 100644 --- a/.gitignore +++ b/.gitignore @@ -72,6 +72,7 @@ components/ui_contentselector.h docs/mainpage.hpp docs/Doxyfile docs/DoxyfilePages +docs/source/reference/lua-scripting/generated_html moc_*.cxx *.cxx_parameters *qrc_launcher.cxx diff --git a/CHANGELOG.md b/CHANGELOG.md index 50de808c29..7cd9bdbba4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Bug #6143: Capturing a screenshot makes engine to be a temporary unresponsive Feature #2780: A way to see current OpenMW version in the console Feature #5489: MCP: Telekinesis fix for activators + Feature #5996: Support Lua scripts in OpenMW Feature #6017: Separate persistent and temporary cell references when saving 0.47.0 diff --git a/CI/before_install.android.sh b/CI/before_install.android.sh index 0243a96092..59d98f48c4 100755 --- a/CI/before_install.android.sh +++ b/CI/before_install.android.sh @@ -1,4 +1,4 @@ #!/bin/sh -ex -curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/android/openmw-android-deps-20201129.zip -o ~/openmw-android-deps.zip +curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/android/openmw-android-deps-20201230.zip -o ~/openmw-android-deps.zip unzip -o ~/openmw-android-deps -d /usr/lib/android-sdk/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr > /dev/null diff --git a/CI/before_install.osx.sh b/CI/before_install.osx.sh index 1ca0fc6119..d962e76086 100755 --- a/CI/before_install.osx.sh +++ b/CI/before_install.osx.sh @@ -15,8 +15,10 @@ ccache --version cmake --version qmake --version +brew install lua + curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20210617.zip -o ~/openmw-deps.zip unzip -o ~/openmw-deps.zip -d /private/tmp/openmw-deps > /dev/null # additional libraries -[ -z "${TRAVIS}" ] && HOMEBREW_NO_AUTO_UPDATE=1 brew install fontconfig \ No newline at end of file +[ -z "${TRAVIS}" ] && HOMEBREW_NO_AUTO_UPDATE=1 brew install fontconfig diff --git a/CI/before_script.msvc.sh b/CI/before_script.msvc.sh index 8d0b8647b4..0a6123505e 100644 --- a/CI/before_script.msvc.sh +++ b/CI/before_script.msvc.sh @@ -575,6 +575,11 @@ if [ -z $SKIP_DOWNLOAD ]; then "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/lz4_win${BITS}_v1_9_2.7z" \ "lz4_win${BITS}_v1_9_2.7z" + # LuaJIT + download "LuaJIT 2.1.0-beta3" \ + "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/LuaJIT-2.1.0-beta3-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" \ + "LuaJIT-2.1.0-beta3-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" + # Google test and mock if [ ! -z $TEST_FRAMEWORK ]; then echo "Google test 1.10.0..." @@ -934,6 +939,25 @@ printf "LZ4 1.9.2... " } cd $DEPS echo +# LuaJIT 2.1.0-beta3 +printf "LuaJIT 2.1.0-beta3... " +{ + if [ -d LuaJIT ]; then + printf "Exists. " + elif [ -z $SKIP_EXTRACT ]; then + rm -rf LuaJIT + eval 7z x -y LuaJIT-2.1.0-beta3-msvc${MSVC_REAL_YEAR}-win${BITS}.7z -o$(real_pwd)/LuaJIT $STRIP + fi + export LUAJIT_DIR="$(real_pwd)/LuaJIT" + add_cmake_opts -DLuaJit_INCLUDE_DIR="${LUAJIT_DIR}/include" \ + -DLuaJit_LIBRARY="${LUAJIT_DIR}/lib/lua51.lib" + for CONFIGURATION in ${CONFIGURATIONS[@]}; do + add_runtime_dlls $CONFIGURATION "$(pwd)/LuaJIT/bin/lua51.dll" + done + echo Done. +} +cd $DEPS +echo # Google Test and Google Mock if [ ! -z $TEST_FRAMEWORK ]; then printf "Google test 1.10.0 ..." diff --git a/CI/before_script.osx.sh b/CI/before_script.osx.sh index 265e05b8ee..27667c1c82 100755 --- a/CI/before_script.osx.sh +++ b/CI/before_script.osx.sh @@ -25,5 +25,6 @@ cmake \ -D BUILD_BSATOOL=TRUE \ -D BUILD_ESSIMPORTER=TRUE \ -D BUILD_NIFTEST=TRUE \ +-D USE_LUAJIT=FALSE \ -G"Unix Makefiles" \ .. diff --git a/CI/install_debian_deps.sh b/CI/install_debian_deps.sh index 2f905314b5..6a7f4a84a7 100755 --- a/CI/install_debian_deps.sh +++ b/CI/install_debian_deps.sh @@ -18,10 +18,10 @@ declare -rA GROUPED_DEPS=( libboost-filesystem-dev libboost-program-options-dev libboost-system-dev libboost-iostreams-dev - + libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libsdl2-dev libqt5opengl5-dev libopenal-dev libunshield-dev libtinyxml-dev - libbullet-dev liblz4-dev libpng-dev libjpeg-dev + libbullet-dev liblz4-dev libpng-dev libjpeg-dev libluajit-5.1-dev ca-certificates " # TODO: add librecastnavigation-dev when debian is ready diff --git a/CMakeLists.txt b/CMakeLists.txt index 4661f673f1..d20e451676 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -394,6 +394,22 @@ endif() find_package(SDL2 2.0.9 REQUIRED) find_package(OpenAL REQUIRED) +option(USE_LUAJIT "Switch Lua/LuaJit (TRUE is highly recommended)" TRUE) +if(USE_LUAJIT) + find_package(LuaJit REQUIRED) + set(LUA_INCLUDE_DIR ${LuaJit_INCLUDE_DIR}) + set(LUA_LIBRARIES ${LuaJit_LIBRARIES}) +else(USE_LUAJIT) + find_package(Lua REQUIRED) + add_compile_definitions(NO_LUAJIT) +endif(USE_LUAJIT) + +# Download sol - C++ library binding to Lua +file(DOWNLOAD + "https://github.com/ThePhD/sol2/releases/download/v3.2.2/sol.hpp" "${OpenMW_BINARY_DIR}/extern/sol3.2.2/sol/sol.hpp" + EXPECTED_MD5 ba113cf458f60672917108e69bb4d958) +set(SOL_INCLUDE_DIRS ${OpenMW_BINARY_DIR}/extern/sol3.2.2 ${OpenMW_SOURCE_DIR}/extern/sol_config) + include_directories( BEFORE SYSTEM "." @@ -403,6 +419,8 @@ include_directories( ${OPENAL_INCLUDE_DIR} ${OPENGL_INCLUDE_DIR} ${BULLET_INCLUDE_DIRS} + ${LUA_INCLUDE_DIR} + ${SOL_INCLUDE_DIRS} ) link_directories(${SDL2_LIBRARY_DIRS} ${Boost_LIBRARY_DIRS}) @@ -661,11 +679,10 @@ if (WIN32) endif() if (BUILD_OPENMW) - if (OPENMW_UNITY_BUILD) - set_target_properties(openmw PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD} /bigobj") - else() - set_target_properties(openmw PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") - endif() + # \bigobj is required: + # 1) for OPENMW_UNITY_BUILD; + # 2) to compile lua binginds, because sol3 is heavily templated. + set_target_properties(openmw PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD} /bigobj") endif() if (BUILD_WIZARD) @@ -682,6 +699,11 @@ if (WIN32) #set_target_properties(openmw PROPERTIES LINK_FLAGS_MINSIZEREL "/SUBSYSTEM:WINDOWS") endif() +if (BUILD_OPENMW AND APPLE) + # Without these flags LuaJit crashes on startup on OSX + set_target_properties(openmw PROPERTIES LINK_FLAGS "-pagezero_size 10000 -image_base 100000000") +endif() + # Apple bundling if (OPENMW_OSX_DEPLOYMENT AND APPLE) if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.13 AND CMAKE_VERSION VERSION_LESS 3.13.4) diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 3fb762d305..3c29baf3aa 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 actions object worldview userdataserializer eventqueue query + luabindings localscripts objectbindings cellbindings asyncbindings camerabindings uibindings + ) + 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 @@ -146,6 +151,7 @@ target_link_libraries(openmw "osg-ffmpeg-videoplayer" "oics" components + ${LUA_LIBRARIES} ) if(OSG_STATIC) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 93d3530c0a..1bd79cdd02 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 { @@ -486,6 +493,11 @@ void OMW::Engine::addGroundcoverFile(const std::string& file) mGroundcoverFiles.emplace_back(file); } +void OMW::Engine::addLuaScriptListFile(const std::string& file) +{ + mLuaScriptListFiles.push_back(file); +} + void OMW::Engine::setSkipMenu (bool skipMenu, bool newGame) { mSkipMenu = skipMenu; @@ -700,6 +712,9 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) mViewer->addEventHandler(mScreenCaptureHandler); + mLuaManager = new MWLua::LuaManager(mVFS.get(), mLuaScriptListFiles); + 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,8 +826,85 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) << 100*static_cast (result.second)/result.first << "%)"; } + + mLuaManager->init(); } +class OMW::Engine::LuaWorker +{ +public: + explicit LuaWorker(Engine* engine) : mEngine(engine) + { + if (Settings::Manager::getInt("lua num threads", "Lua") > 0) + mThread = std::thread([this]{ threadBody(); }); + }; + + void allowUpdate(double dt) + { + mDt = dt; + mIsGuiMode = mEngine->mEnvironment.getWindowManager()->isGuiMode(); + if (!mThread) + return; + { + std::lock_guard lk(mMutex); + mUpdateRequest = true; + } + mCV.notify_one(); + } + + void finishUpdate() + { + if (mThread) + { + std::unique_lock lk(mMutex); + mCV.wait(lk, [&]{ return !mUpdateRequest; }); + } + else + update(); + mEngine->mLuaManager->applyQueuedChanges(); + }; + + void join() + { + if (mThread) + mThread->join(); + } + +private: + void update() + { + const auto& viewer = mEngine->mViewer; + const osg::Timer_t frameStart = viewer->getStartTick(); + const unsigned int frameNumber = viewer->getFrameStamp()->getFrameNumber(); + ScopedProfile profile(frameStart, frameNumber, *osg::Timer::instance(), *viewer->getViewerStats()); + + mEngine->mLuaManager->update(mIsGuiMode, mDt); + } + + void threadBody() + { + while (!mEngine->mViewer->done() && !mEngine->mEnvironment.getStateManager()->hasQuitRequest()) + { + std::unique_lock lk(mMutex); + mCV.wait(lk, [&]{ return mUpdateRequest; }); + + update(); + + mUpdateRequest = false; + lk.unlock(); + mCV.notify_one(); + } + } + + Engine* mEngine; + std::mutex mMutex; + std::condition_variable mCV; + bool mUpdateRequest; + double mDt = 0; + bool mIsGuiMode = false; + std::optional mThread; +}; + // Initialise and enter main loop. void OMW::Engine::go() { @@ -895,6 +987,8 @@ void OMW::Engine::go() mEnvironment.getWindowManager()->executeInConsole(mStartupScript); } + LuaWorker luaWorker(this); // starts a separate lua thread if "lua num threads" > 0 + // Start the main rendering loop double simulationTime = 0.0; Misc::FrameRateLimiter frameRateLimiter = Misc::makeFrameRateLimiter(mEnvironment.getFrameRateLimit()); @@ -920,8 +1014,12 @@ void OMW::Engine::go() mEnvironment.getWorld()->updateWindowManager(); + luaWorker.allowUpdate(dt); // if there is a separate Lua thread, it starts the update now + mViewer->renderingTraversals(); + luaWorker.finishUpdate(); + bool guiActive = mEnvironment.getWindowManager()->isGuiMode(); if (!guiActive) simulationTime += dt; @@ -943,6 +1041,8 @@ void OMW::Engine::go() frameRateLimiter.limit(); } + luaWorker.join(); + // Save user settings settings.saveUser(settingspath); diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index 49ae92abc1..180e06bcbc 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; @@ -66,6 +71,7 @@ namespace OMW std::string mCellName; std::vector mContentFiles; std::vector mGroundcoverFiles; + std::vector mLuaScriptListFiles; bool mSkipMenu; bool mUseSound; bool mCompileAll; @@ -85,6 +91,8 @@ namespace OMW Compiler::Extensions mExtensions; Compiler::Context *mScriptContext; + MWLua::LuaManager* mLuaManager; + Files::Collections mFileCollections; bool mFSStrict; Translation::Storage mTranslationDataStorage; @@ -137,6 +145,7 @@ namespace OMW */ void addContentFile(const std::string& file); void addGroundcoverFile(const std::string& file); + void addLuaScriptListFile(const std::string& file); /// Disable or enable all sounds void setSoundUsage(bool soundUsage); @@ -185,6 +194,7 @@ namespace OMW private: Files::ConfigurationManager& mCfgMgr; + class LuaWorker; }; } diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index 709ffda2cb..324a18bdee 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -65,6 +65,9 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat ("groundcover", bpo::value()->default_value(Files::EscapeStringVector(), "") ->multitoken()->composing(), "groundcover content file(s): esm/esp, or omwgame/omwaddon") + ("lua-scripts", bpo::value()->default_value(Files::EscapeStringVector(), "") + ->multitoken()->composing(), "file(s) with a list of global Lua scripts: omwscripts") + ("no-sound", bpo::value()->implicit_value(true) ->default_value(false), "disable all sounds") @@ -204,6 +207,10 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat engine.addGroundcoverFile(file); } + StringsVector luaScriptLists = variables["lua-scripts"].as().toStdStringVector(); + for (const auto& file : luaScriptLists) + engine.addLuaScriptListFile(file); + // startup-settings engine.setCell(variables["start"].as().toStdString()); engine.setSkipMenu (variables["skip-menu"].as(), variables["new-game"].as()); 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..4e437246c4 --- /dev/null +++ b/apps/openmw/mwbase/luamanager.hpp @@ -0,0 +1,78 @@ +#ifndef GAME_MWBASE_LUAMANAGER_H +#define GAME_MWBASE_LUAMANAGER_H + +#include + +namespace MWWorld +{ + class Ptr; +} + +namespace Loading +{ + class Listener; +} + +namespace ESM +{ + class ESMReader; + class ESMWriter; + struct LuaScripts; +} + +namespace MWBase +{ + + class LuaManager + { + public: + virtual ~LuaManager() = default; + + virtual void newGameStarted() = 0; + virtual void keyPressed(const SDL_KeyboardEvent &arg) = 0; + + virtual void registerObject(const MWWorld::Ptr& ptr) = 0; + virtual void deregisterObject(const MWWorld::Ptr& ptr) = 0; + virtual void objectAddedToScene(const MWWorld::Ptr& ptr) = 0; + virtual void objectRemovedFromScene(const MWWorld::Ptr& ptr) = 0; + virtual void appliedToObject(const MWWorld::Ptr& toPtr, std::string_view recordId, const MWWorld::Ptr& fromPtr) = 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; + + struct ActorControls + { + bool mDisableAI; + bool mControlledFromLua; + + bool mJump; + bool mRun; + float mMovement; + float mSideMovement; + float mTurn; + }; + + virtual ActorControls* getActorControls(const MWWorld::Ptr&) const = 0; + + virtual void clear() = 0; + virtual void setupPlayer(const MWWorld::Ptr&) = 0; + + // Saving + int countSavedGameRecords() const { return 1; }; + virtual void write(ESM::ESMWriter& writer, Loading::Listener& progress) = 0; + virtual void saveLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScripts& data) = 0; + + // Loading from a save + virtual void readRecord(ESM::ESMReader& reader, uint32_t type) = 0; + virtual void loadLocalScripts(const MWWorld::Ptr& ptr, const ESM::LuaScripts& data) = 0; + + // Should be called before loading. The map is used to fix refnums if the order of content files was changed. + virtual void setContentFileMapping(const std::map&) = 0; + + // Drops script cache and reloads all scripts. Calls `onSave` and `onLoad` for every script. + virtual void reloadAllScripts() = 0; + }; + +} + +#endif // GAME_MWBASE_LUAMANAGER_H diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 7afd0697eb..4110c8f489 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -129,6 +129,8 @@ namespace MWBase virtual MWWorld::CellStore *getCell (const ESM::CellId& id) = 0; + virtual bool isCellActive(MWWorld::CellStore* cell) const = 0; + virtual void testExteriorCells() = 0; virtual void testInteriorCells() = 0; diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 6131f86269..5bf81caf9d 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -17,6 +17,7 @@ #include "../mwbase/windowmanager.hpp" #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" @@ -1095,6 +1096,7 @@ namespace MWClass bool Npc::apply (const MWWorld::Ptr& ptr, const std::string& id, const MWWorld::Ptr& actor) const { + MWBase::Environment::get().getLuaManager()->appliedToObject(ptr, id, actor); MWMechanics::CastSpell cast(ptr, ptr); return cast.cast(id); } diff --git a/apps/openmw/mwgui/console.cpp b/apps/openmw/mwgui/console.cpp index b9004fcdbb..3bc2607045 100644 --- a/apps/openmw/mwgui/console.cpp +++ b/apps/openmw/mwgui/console.cpp @@ -20,6 +20,7 @@ #include "../mwbase/scriptmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/class.hpp" diff --git a/apps/openmw/mwinput/keyboardmanager.cpp b/apps/openmw/mwinput/keyboardmanager.cpp index 8540858461..03db584192 100644 --- a/apps/openmw/mwinput/keyboardmanager.cpp +++ b/apps/openmw/mwinput/keyboardmanager.cpp @@ -6,6 +6,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/player.hpp" @@ -58,6 +59,9 @@ namespace MWInput if (!input->controlsDisabled() && !consumed) mBindingsManager->keyPressed(arg); + if (!consumed) + MWBase::Environment::get().getLuaManager()->keyPressed(arg); + input->setJoystickLastUsed(false); } diff --git a/apps/openmw/mwlua/actions.cpp b/apps/openmw/mwlua/actions.cpp new file mode 100644 index 0000000000..95a33fed0d --- /dev/null +++ b/apps/openmw/mwlua/actions.cpp @@ -0,0 +1,124 @@ +#include "actions.hpp" + +#include + +#include "../mwworld/cellstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/inventorystore.hpp" +#include "../mwworld/player.hpp" + +namespace MWLua +{ + + void TeleportAction::apply(WorldView& worldView) const + { + MWWorld::CellStore* cell = worldView.findCell(mCell, mPos); + if (!cell) + { + Log(Debug::Error) << "LuaManager::applyTeleport -> cell not found: '" << mCell << "'"; + return; + } + + MWBase::World* world = MWBase::Environment::get().getWorld(); + MWWorld::Ptr obj = worldView.getObjectRegistry()->getPtr(mObject, false); + const MWWorld::Class& cls = obj.getClass(); + bool isPlayer = obj == world->getPlayerPtr(); + if (cls.isActor()) + cls.getCreatureStats(obj).land(isPlayer); + if (isPlayer) + { + ESM::Position esmPos; + static_assert(sizeof(esmPos) == sizeof(osg::Vec3f) * 2); + std::memcpy(esmPos.pos, &mPos, sizeof(osg::Vec3f)); + std::memcpy(esmPos.rot, &mRot, sizeof(osg::Vec3f)); + world->getPlayer().setTeleported(true); + if (cell->isExterior()) + world->changeToExteriorCell(esmPos, true); + else + world->changeToInteriorCell(mCell, esmPos, true); + } + else + { + MWWorld::Ptr newObj = world->moveObject(obj, cell, mPos.x(), mPos.y(), mPos.z()); + world->rotateObject(newObj, mRot.x(), mRot.y(), mRot.z()); + } + } + + void SetEquipmentAction::apply(WorldView& worldView) const + { + MWWorld::Ptr actor = worldView.getObjectRegistry()->getPtr(mActor, false); + MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); + std::array usedSlots; + std::fill(usedSlots.begin(), usedSlots.end(), false); + + constexpr int anySlot = -1; + auto tryEquipToSlot = [&actor, &store, &usedSlots, &worldView, anySlot](int slot, const Item& item) -> bool + { + auto old_it = slot != anySlot ? store.getSlot(slot) : store.end(); + MWWorld::Ptr itemPtr; + if (std::holds_alternative(item)) + { + itemPtr = worldView.getObjectRegistry()->getPtr(std::get(item), false); + if (old_it != store.end() && *old_it == itemPtr) + return true; // already equipped + if (itemPtr.isEmpty() || itemPtr.getRefData().getCount() == 0 || + itemPtr.getContainerStore() != static_cast(&store)) + { + Log(Debug::Warning) << "Object" << idToString(std::get(item)) << " is not in inventory"; + return false; + } + } + else + { + const std::string& recordId = std::get(item); + if (old_it != store.end() && *old_it->getCellRef().getRefIdPtr() == recordId) + return true; // already equipped + itemPtr = store.search(recordId); + if (itemPtr.isEmpty() || itemPtr.getRefData().getCount() == 0) + { + Log(Debug::Warning) << "There is no object with recordId='" << recordId << "' in inventory"; + return false; + } + } + + auto [allowedSlots, _] = itemPtr.getClass().getEquipmentSlots(itemPtr); + bool requestedSlotIsAllowed = std::find(allowedSlots.begin(), allowedSlots.end(), slot) != allowedSlots.end(); + if (!requestedSlotIsAllowed) + { + auto firstAllowed = std::find_if(allowedSlots.begin(), allowedSlots.end(), [&](int s) { return !usedSlots[s]; }); + if (firstAllowed == allowedSlots.end()) + { + Log(Debug::Warning) << "No suitable slot for " << ptrToString(itemPtr); + return false; + } + slot = *firstAllowed; + } + + // TODO: Refactor InventoryStore to accept Ptr and get rid of this linear search. + MWWorld::ContainerStoreIterator it = std::find(store.begin(), store.end(), itemPtr); + if (it == store.end()) // should never happen + throw std::logic_error("Item not found in container"); + + store.equip(slot, it, actor); + return requestedSlotIsAllowed; // return true if equipped to requested slot and false if slot was changed + }; + + for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) + { + auto old_it = store.getSlot(slot); + auto new_it = mEquipment.find(slot); + if (new_it == mEquipment.end()) + { + if (old_it != store.end()) + store.unequipSlot(slot, actor); + continue; + } + if (tryEquipToSlot(slot, new_it->second)) + usedSlots[slot] = true; + } + for (const auto& [slot, item] : mEquipment) + if (slot >= MWWorld::InventoryStore::Slots) + tryEquipToSlot(anySlot, item); + } + +} diff --git a/apps/openmw/mwlua/actions.hpp b/apps/openmw/mwlua/actions.hpp new file mode 100644 index 0000000000..900b175320 --- /dev/null +++ b/apps/openmw/mwlua/actions.hpp @@ -0,0 +1,55 @@ +#ifndef MWLUA_ACTIONS_H +#define MWLUA_ACTIONS_H + +#include + +#include "object.hpp" +#include "worldview.hpp" + +namespace MWLua +{ + + // Some changes to the game world can not be done from the scripting thread (because it runs in parallel with OSG Cull), + // so we need to queue it and apply from the main thread. All such changes should be implemented as classes inherited + // from MWLua::Action. + + class Action + { + public: + virtual ~Action() {} + virtual void apply(WorldView&) const = 0; + }; + + class TeleportAction final : public Action + { + public: + TeleportAction(ObjectId object, std::string cell, const osg::Vec3f& pos, const osg::Vec3f& rot) + : mObject(object), mCell(std::move(cell)), mPos(pos), mRot(rot) {} + + void apply(WorldView&) const override; + + private: + ObjectId mObject; + std::string mCell; + osg::Vec3f mPos; + osg::Vec3f mRot; + }; + + class SetEquipmentAction final : public Action + { + public: + using Item = std::variant; // recordId or ObjectId + using Equipment = std::map; // slot to item + + SetEquipmentAction(ObjectId actor, Equipment equipment) : mActor(actor), mEquipment(std::move(equipment)) {} + + void apply(WorldView&) const override; + + private: + ObjectId mActor; + Equipment mEquipment; + }; + +} + +#endif // MWLUA_ACTIONS_H diff --git a/apps/openmw/mwlua/asyncbindings.cpp b/apps/openmw/mwlua/asyncbindings.cpp new file mode 100644 index 0000000000..fee6788b89 --- /dev/null +++ b/apps/openmw/mwlua/asyncbindings.cpp @@ -0,0 +1,60 @@ +#include "luabindings.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + + struct TimerCallback + { + AsyncPackageId mAsyncId; + std::string mName; + }; + + sol::function getAsyncPackageInitializer(const Context& context) + { + using TimeUnit = LuaUtil::ScriptsContainer::TimeUnit; + sol::usertype api = context.mLua->sol().new_usertype("AsyncPackage"); + api["registerTimerCallback"] = [](const AsyncPackageId& asyncId, std::string_view name, sol::function callback) + { + asyncId.mContainer->registerTimerCallback(asyncId.mScript, name, std::move(callback)); + return TimerCallback{asyncId, std::string(name)}; + }; + api["newTimerInSeconds"] = [world=context.mWorldView](const AsyncPackageId&, double delay, + const TimerCallback& callback, sol::object callbackArg) + { + callback.mAsyncId.mContainer->setupSerializableTimer( + TimeUnit::SECONDS, world->getGameTimeInSeconds() + delay, + callback.mAsyncId.mScript, callback.mName, std::move(callbackArg)); + }; + api["newTimerInHours"] = [world=context.mWorldView](const AsyncPackageId&, double delay, + const TimerCallback& callback, sol::object callbackArg) + { + callback.mAsyncId.mContainer->setupSerializableTimer( + TimeUnit::HOURS, world->getGameTimeInHours() + delay, + callback.mAsyncId.mScript, callback.mName, std::move(callbackArg)); + }; + api["newUnsavableTimerInSeconds"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback) + { + asyncId.mContainer->setupUnsavableTimer( + TimeUnit::SECONDS, world->getGameTimeInSeconds() + delay, asyncId.mScript, std::move(callback)); + }; + api["newUnsavableTimerInHours"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback) + { + asyncId.mContainer->setupUnsavableTimer( + TimeUnit::HOURS, world->getGameTimeInHours() + delay, asyncId.mScript, std::move(callback)); + }; + + auto initializer = [](sol::table hiddenData) + { + LuaUtil::ScriptsContainer::ScriptId id = hiddenData[LuaUtil::ScriptsContainer::ScriptId::KEY]; + return AsyncPackageId{id.mContainer, id.mPath}; + }; + return sol::make_object(context.mLua->sol(), initializer); + } + +} diff --git a/apps/openmw/mwlua/camerabindings.cpp b/apps/openmw/mwlua/camerabindings.cpp new file mode 100644 index 0000000000..68f2331b9e --- /dev/null +++ b/apps/openmw/mwlua/camerabindings.cpp @@ -0,0 +1,13 @@ +#include "luabindings.hpp" + +namespace MWLua +{ + + sol::table initCameraPackage(const Context& context) + { + sol::table api(context.mLua->sol(), sol::create); + // TODO + return context.mLua->makeReadOnly(api); + } + +} diff --git a/apps/openmw/mwlua/cellbindings.cpp b/apps/openmw/mwlua/cellbindings.cpp new file mode 100644 index 0000000000..a23fb47c32 --- /dev/null +++ b/apps/openmw/mwlua/cellbindings.cpp @@ -0,0 +1,62 @@ +#include "luabindings.hpp" + +#include + +#include "../mwworld/cellstore.hpp" + +namespace MWLua +{ + + template + static void initCellBindings(const std::string& prefix, const Context& context) + { + sol::usertype cellT = context.mLua->sol().new_usertype(prefix + "Cell"); + + cellT[sol::meta_function::equal_to] = [](const CellT& a, const CellT& b) { return a.mStore == b.mStore; }; + cellT[sol::meta_function::to_string] = [](const CellT& c) + { + const ESM::Cell* cell = c.mStore->getCell(); + std::stringstream res; + if (cell->isExterior()) + res << "exterior(" << cell->getGridX() << ", " << cell->getGridY() << ")"; + else + res << "interior(" << cell->mName << ")"; + return res.str(); + }; + + cellT["name"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->mName; }); + cellT["region"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->mRegion; }); + cellT["gridX"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->getGridX(); }); + cellT["gridY"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->getGridY(); }); + cellT["isExterior"] = sol::readonly_property([](const CellT& c) { return c.mStore->isExterior(); }); + cellT["hasWater"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->hasWater(); }); + + cellT["isInSameSpace"] = [](const CellT& c, const ObjectT& obj) + { + const MWWorld::Ptr& ptr = obj.ptr(); + if (!ptr.isInCell()) + return false; + MWWorld::CellStore* cell = ptr.getCell(); + return cell == c.mStore || (cell->isExterior() && c.mStore->isExterior()); + }; + + if constexpr (std::is_same_v) + { // only for global scripts + cellT["selectObjects"] = [context](const CellT& cell, const Queries::Query& query) + { + return GObjectList{selectObjectsFromCellStore(query, cell.mStore, context)}; + }; + } + } + + void initCellBindingsForLocalScripts(const Context& context) + { + initCellBindings("L", context); + } + + void initCellBindingsForGlobalScripts(const Context& context) + { + initCellBindings("G", context); + } + +} diff --git a/apps/openmw/mwlua/context.hpp b/apps/openmw/mwlua/context.hpp new file mode 100644 index 0000000000..b3e3703a46 --- /dev/null +++ b/apps/openmw/mwlua/context.hpp @@ -0,0 +1,30 @@ +#ifndef MWLUA_CONTEXT_H +#define MWLUA_CONTEXT_H + +#include "eventqueue.hpp" + +namespace LuaUtil +{ + class LuaState; + class UserdataSerializer; +} + +namespace MWLua +{ + class LuaManager; + class WorldView; + + struct Context + { + bool mIsGlobal; + LuaManager* mLuaManager; + LuaUtil::LuaState* mLua; + LuaUtil::UserdataSerializer* mSerializer; + WorldView* mWorldView; + LocalEventQueue* mLocalEventQueue; + GlobalEventQueue* mGlobalEventQueue; + }; + +} + +#endif // MWLUA_CONTEXT_H diff --git a/apps/openmw/mwlua/eventqueue.cpp b/apps/openmw/mwlua/eventqueue.cpp new file mode 100644 index 0000000000..1c136551c4 --- /dev/null +++ b/apps/openmw/mwlua/eventqueue.cpp @@ -0,0 +1,63 @@ +#include "eventqueue.hpp" + +#include + +#include +#include +#include + +#include + +namespace MWLua +{ + + template + void saveEvent(ESM::ESMWriter& esm, const ObjectId& dest, const Event& event) + { + esm.writeHNString("LUAE", event.mEventName); + dest.save(esm, true); + if (!event.mEventData.empty()) + saveLuaBinaryData(esm, event.mEventData); + } + + void loadEvents(sol::state& lua, ESM::ESMReader& esm, GlobalEventQueue& globalEvents, LocalEventQueue& localEvents, + const std::map& contentFileMapping, const LuaUtil::UserdataSerializer* serializer) + { + while (esm.isNextSub("LUAE")) + { + std::string name = esm.getHString(); + ObjectId dest; + dest.load(esm, true); + std::string data = loadLuaBinaryData(esm); + try + { + data = LuaUtil::serialize(LuaUtil::deserialize(lua, data, serializer), serializer); + } + catch (std::exception& e) + { + Log(Debug::Error) << "loadEvent: invalid event data: " << e.what(); + } + if (dest.isSet()) + { + auto it = contentFileMapping.find(dest.mContentFile); + if (it != contentFileMapping.end()) + dest.mContentFile = it->second; + localEvents.push_back({dest, std::move(name), std::move(data)}); + } + else + globalEvents.push_back({std::move(name), std::move(data)}); + } + } + + void saveEvents(ESM::ESMWriter& esm, const GlobalEventQueue& globalEvents, const LocalEventQueue& localEvents) + { + ObjectId globalId; + globalId.unset(); // Used as a marker of a global event. + + for (const GlobalEvent& e : globalEvents) + saveEvent(esm, globalId, e); + for (const LocalEvent& e : localEvents) + saveEvent(esm, e.mDest, e); + } + +} diff --git a/apps/openmw/mwlua/eventqueue.hpp b/apps/openmw/mwlua/eventqueue.hpp new file mode 100644 index 0000000000..0e5f2dfcb4 --- /dev/null +++ b/apps/openmw/mwlua/eventqueue.hpp @@ -0,0 +1,43 @@ +#ifndef MWLUA_EVENTQUEUE_H +#define MWLUA_EVENTQUEUE_H + +#include "object.hpp" + +namespace ESM +{ + class ESMReader; + class ESMWriter; +} + +namespace LuaUtil +{ + class UserdataSerializer; +} + +namespace sol +{ + class state; +} + +namespace MWLua +{ + struct GlobalEvent + { + std::string mEventName; + std::string mEventData; + }; + struct LocalEvent + { + ObjectId mDest; + std::string mEventName; + std::string mEventData; + }; + using GlobalEventQueue = std::vector; + using LocalEventQueue = std::vector; + + void loadEvents(sol::state& lua, ESM::ESMReader& esm, GlobalEventQueue&, LocalEventQueue&, + const std::map& contentFileMapping, const LuaUtil::UserdataSerializer* serializer); + void saveEvents(ESM::ESMWriter& esm, const GlobalEventQueue&, const LocalEventQueue&); +} + +#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..d9bb5ff26e --- /dev/null +++ b/apps/openmw/mwlua/localscripts.cpp @@ -0,0 +1,114 @@ +#include "localscripts.hpp" + +#include "../mwworld/ptr.hpp" +#include "../mwworld/class.hpp" +#include "../mwmechanics/aisequence.hpp" +#include "../mwmechanics/aicombat.hpp" + +#include "luamanagerimp.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::mMovement; + controls["sideMovement"] = &ActorControls::mSideMovement; + controls["turn"] = &ActorControls::mTurn; + controls["run"] = &ActorControls::mRun; + controls["jump"] = &ActorControls::mJump; + + 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["isActive"] = [](SelfObject& self) { return &self.mIsActive; }; + selfAPI["setDirectControl"] = [](SelfObject& self, bool v) { self.mControls.mControlledFromLua = v; }; + selfAPI["enableAI"] = [](SelfObject& self, bool v) { self.mControls.mDisableAI = !v; }; + selfAPI["setEquipment"] = [manager=context.mLuaManager](const SelfObject& obj, sol::table equipment) + { + if (!obj.ptr().getClass().hasInventoryStore(obj.ptr())) + { + if (!equipment.empty()) + throw std::runtime_error(ptrToString(obj.ptr()) + " has no equipment slots"); + return; + } + SetEquipmentAction::Equipment eqp; + for (auto& [key, value] : equipment) + { + int slot = key.as(); + if (value.is()) + eqp[slot] = value.as().id(); + else + eqp[slot] = value.as(); + } + manager->addAction(std::make_unique(obj.id(), std::move(eqp))); + }; + selfAPI["getCombatTarget"] = [worldView=context.mWorldView](SelfObject& self) -> sol::optional + { + const MWWorld::Ptr& ptr = self.ptr(); + MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + MWWorld::Ptr target; + if (ai.getCombatTarget(target)) + return LObject(getId(target), worldView->getObjectRegistry()); + else + return {}; + }; + selfAPI["stopCombat"] = [](SelfObject& self) + { + const MWWorld::Ptr& ptr = self.ptr(); + MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + ai.stopCombat(); + }; + selfAPI["startCombat"] = [](SelfObject& self, const LObject& target) + { + const MWWorld::Ptr& ptr = self.ptr(); + MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + ai.stack(MWMechanics::AiCombat(target.ptr()), ptr); + }; + } + + LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj) + : LuaUtil::ScriptsContainer(lua, "L" + idToString(obj.id())), mData(obj) + { + mData.mControls.mControlledFromLua = false; + mData.mControls.mDisableAI = false; + this->addPackage("openmw.self", sol::make_object(lua->sol(), &mData)); + registerEngineHandlers({&mOnActiveHandlers, &mOnInactiveHandlers, &mOnConsumeHandlers}); + } + + void LocalScripts::receiveEngineEvent(const EngineEvent& event, ObjectRegistry*) + { + std::visit([this](auto&& arg) + { + using EventT = std::decay_t; + if constexpr (std::is_same_v) + { + mData.mIsActive = true; + callEngineHandlers(mOnActiveHandlers); + } + else if constexpr (std::is_same_v) + { + mData.mIsActive = false; + callEngineHandlers(mOnInactiveHandlers); + } + else + { + static_assert(std::is_same_v); + callEngineHandlers(mOnConsumeHandlers, arg.mRecordId); + } + }, event); + } + +} diff --git a/apps/openmw/mwlua/localscripts.hpp b/apps/openmw/mwlua/localscripts.hpp new file mode 100644 index 0000000000..80d04b7a40 --- /dev/null +++ b/apps/openmw/mwlua/localscripts.hpp @@ -0,0 +1,55 @@ +#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 void initializeSelfPackage(const Context&); + LocalScripts(LuaUtil::LuaState* lua, const LObject& obj); + + MWBase::LuaManager::ActorControls* getActorControls() { return &mData.mControls; } + + struct SelfObject : public LObject + { + SelfObject(const LObject& obj) : LObject(obj), mIsActive(false) {} + MWBase::LuaManager::ActorControls mControls; + bool mIsActive; + }; + + struct OnActive {}; + struct OnInactive {}; + struct OnConsume + { + std::string mRecordId; + }; + using EngineEvent = std::variant; + + void receiveEngineEvent(const EngineEvent&, ObjectRegistry*); + + protected: + SelfObject mData; + + private: + EngineHandlerList mOnActiveHandlers{"onActive"}; + EngineHandlerList mOnInactiveHandlers{"onInactive"}; + EngineHandlerList mOnConsumeHandlers{"onConsume"}; + }; + +} + +#endif // MWLUA_LOCALSCRIPTS_H diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp new file mode 100644 index 0000000000..ebb24401fc --- /dev/null +++ b/apps/openmw/mwlua/luabindings.cpp @@ -0,0 +1,187 @@ +#include "luabindings.hpp" + +#include + +#include +#include + +#include "../mwworld/inventorystore.hpp" + +#include "eventqueue.hpp" +#include "worldview.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + + static sol::table definitionList(LuaUtil::LuaState& lua, std::initializer_list values) + { + sol::table res(lua.sol(), sol::create); + for (const std::string& v : values) + res[v] = v; + return lua.makeReadOnly(res); + } + + sol::table initCorePackage(const Context& context) + { + auto* lua = context.mLua; + sol::table api(lua->sol(), sol::create); + api["API_VERSION"] = 0; + api["sendGlobalEvent"] = [context](std::string eventName, const sol::object& eventData) + { + context.mGlobalEventQueue->push_back({std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer)}); + }; + api["getGameTimeInSeconds"] = [world=context.mWorldView]() { return world->getGameTimeInSeconds(); }; + api["getGameTimeInHours"] = [world=context.mWorldView]() { return world->getGameTimeInHours(); }; + api["OBJECT_TYPE"] = definitionList(*lua, + { + "Activator", "Armor", "Book", "Clothing", "Creature", "Door", "Ingredient", + "Light", "Miscellaneous", "NPC", "Player", "Potion", "Static", "Weapon" + }); + api["EQUIPMENT_SLOT"] = lua->makeReadOnly(lua->sol().create_table_with( + "Helmet", MWWorld::InventoryStore::Slot_Helmet, + "Cuirass", MWWorld::InventoryStore::Slot_Cuirass, + "Greaves", MWWorld::InventoryStore::Slot_Greaves, + "LeftPauldron", MWWorld::InventoryStore::Slot_LeftPauldron, + "RightPauldron", MWWorld::InventoryStore::Slot_RightPauldron, + "LeftGauntlet", MWWorld::InventoryStore::Slot_LeftGauntlet, + "RightGauntlet", MWWorld::InventoryStore::Slot_RightGauntlet, + "Boots", MWWorld::InventoryStore::Slot_Boots, + "Shirt", MWWorld::InventoryStore::Slot_Shirt, + "Pants", MWWorld::InventoryStore::Slot_Pants, + "Skirt", MWWorld::InventoryStore::Slot_Skirt, + "Robe", MWWorld::InventoryStore::Slot_Robe, + "LeftRing", MWWorld::InventoryStore::Slot_LeftRing, + "RightRing", MWWorld::InventoryStore::Slot_RightRing, + "Amulet", MWWorld::InventoryStore::Slot_Amulet, + "Belt", MWWorld::InventoryStore::Slot_Belt, + "CarriedRight", MWWorld::InventoryStore::Slot_CarriedRight, + "CarriedLeft", MWWorld::InventoryStore::Slot_CarriedLeft, + "Ammunition", MWWorld::InventoryStore::Slot_Ammunition + )); + return lua->makeReadOnly(api); + } + + sol::table initWorldPackage(const Context& context) + { + sol::table api(context.mLua->sol(), sol::create); + WorldView* worldView = context.mWorldView; + api["getCellByName"] = [worldView=context.mWorldView](const std::string& name) -> sol::optional + { + MWWorld::CellStore* cell = worldView->findNamedCell(name); + if (cell) + return GCell{cell}; + else + return sol::nullopt; + }; + api["getExteriorCell"] = [worldView=context.mWorldView](int x, int y) -> sol::optional + { + MWWorld::CellStore* cell = worldView->findExteriorCell(x, y); + if (cell) + return GCell{cell}; + else + return sol::nullopt; + }; + api["activeActors"] = GObjectList{worldView->getActorsInScene()}; + api["selectObjects"] = [context](const Queries::Query& query) + { + ObjectIdList list; + WorldView* worldView = context.mWorldView; + if (query.mQueryType == "activators") + list = worldView->getActivatorsInScene(); + else if (query.mQueryType == "actors") + list = worldView->getActorsInScene(); + else if (query.mQueryType == "containers") + list = worldView->getContainersInScene(); + else if (query.mQueryType == "doors") + list = worldView->getDoorsInScene(); + else if (query.mQueryType == "items") + list = worldView->getItemsInScene(); + return GObjectList{selectObjectsFromList(query, list, context)}; + // TODO: Use sqlite to search objects that are not in the scene + // return GObjectList{worldView->selectObjects(query, false)}; + }; + // TODO: add world.placeNewObject(recordId, cell, pos, [rot]) + return context.mLua->makeReadOnly(api); + } + + sol::table initNearbyPackage(const Context& context) + { + sol::table api(context.mLua->sol(), sol::create); + WorldView* worldView = context.mWorldView; + api["activators"] = LObjectList{worldView->getActivatorsInScene()}; + api["actors"] = LObjectList{worldView->getActorsInScene()}; + api["containers"] = LObjectList{worldView->getContainersInScene()}; + api["doors"] = LObjectList{worldView->getDoorsInScene()}; + api["items"] = LObjectList{worldView->getItemsInScene()}; + api["selectObjects"] = [context](const Queries::Query& query) + { + ObjectIdList list; + WorldView* worldView = context.mWorldView; + if (query.mQueryType == "activators") + list = worldView->getActivatorsInScene(); + else if (query.mQueryType == "actors") + list = worldView->getActorsInScene(); + else if (query.mQueryType == "containers") + list = worldView->getContainersInScene(); + else if (query.mQueryType == "doors") + list = worldView->getDoorsInScene(); + else if (query.mQueryType == "items") + list = worldView->getItemsInScene(); + return LObjectList{selectObjectsFromList(query, list, context)}; + // TODO: Maybe use sqlite + // return LObjectList{worldView->selectObjects(query, true)}; + }; + return context.mLua->makeReadOnly(api); + } + + sol::table initQueryPackage(const Context& context) + { + Queries::registerQueryBindings(context.mLua->sol()); + sol::table query(context.mLua->sol(), sol::create); + for (std::string_view t : ObjectQueryTypes::types) + query[t] = Queries::Query(std::string(t)); + for (const QueryFieldGroup& group : getBasicQueryFieldGroups()) + query[group.mName] = initFieldGroup(context, group); + return query; // makeReadonly is applied by LuaState::addCommonPackage + } + + sol::table initFieldGroup(const Context& context, const QueryFieldGroup& group) + { + sol::table res(context.mLua->sol(), sol::create); + for (const Queries::Field* field : group.mFields) + { + sol::table subgroup = res; + if (field->path().empty()) + throw std::logic_error("Empty path in Queries::Field"); + for (size_t i = 0; i < field->path().size() - 1; ++i) + { + const std::string& name = field->path()[i]; + if (subgroup[name] == sol::nil) + subgroup[name] = context.mLua->makeReadOnly(context.mLua->newTable()); + subgroup = context.mLua->getMutableFromReadOnly(subgroup[name]); + } + subgroup[field->path().back()] = field; + } + return context.mLua->makeReadOnly(res); + } + + void initInputBindings(const Context& context) + { + sol::usertype keyEvent = context.mLua->sol().new_usertype("KeyEvent"); + keyEvent["symbol"] = sol::readonly_property([](const SDL_Keysym& e) { return std::string(1, static_cast(e.sym)); }); + keyEvent["code"] = sol::readonly_property([](const SDL_Keysym& e) -> int { return e.sym; }); + keyEvent["modifiers"] = sol::readonly_property([](const SDL_Keysym& e) -> int { return e.mod; }); + keyEvent["withShift"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_SHIFT; }); + keyEvent["withCtrl"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_CTRL; }); + keyEvent["withAlt"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_ALT; }); + keyEvent["withSuper"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_GUI; }); + } + +} + diff --git a/apps/openmw/mwlua/luabindings.hpp b/apps/openmw/mwlua/luabindings.hpp new file mode 100644 index 0000000000..8be96763a5 --- /dev/null +++ b/apps/openmw/mwlua/luabindings.hpp @@ -0,0 +1,65 @@ +#ifndef MWLUA_LUABINDINGS_H +#define MWLUA_LUABINDINGS_H + +#include +#include +#include + +#include "context.hpp" +#include "eventqueue.hpp" +#include "object.hpp" +#include "query.hpp" +#include "worldview.hpp" + +namespace MWWorld +{ + class CellStore; +} + +namespace MWLua +{ + + sol::table initCorePackage(const Context&); + sol::table initWorldPackage(const Context&); + sol::table initNearbyPackage(const Context&); + sol::table initQueryPackage(const Context&); + + sol::table initFieldGroup(const Context&, const QueryFieldGroup&); + + void initInputBindings(const Context&); + + // Implemented in objectbindings.cpp + void initObjectBindingsForLocalScripts(const Context&); + void initObjectBindingsForGlobalScripts(const Context&); + + // Implemented in cellbindings.cpp + struct LCell // for local scripts + { + MWWorld::CellStore* mStore; + }; + struct GCell // for global scripts + { + MWWorld::CellStore* mStore; + }; + void initCellBindingsForLocalScripts(const Context&); + void initCellBindingsForGlobalScripts(const Context&); + + // Implemented in asyncbindings.cpp + struct AsyncPackageId + { + // TODO: add ObjectId mLocalObject; + LuaUtil::ScriptsContainer* mContainer; + std::string mScript; + }; + sol::function getAsyncPackageInitializer(const Context&); + + // Implemented in camerabindings.cpp + sol::table initCameraPackage(const Context&); + + // Implemented in uibindings.cpp + sol::table initUserInterfacePackage(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..e01273bb84 --- /dev/null +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -0,0 +1,383 @@ +#include "luamanagerimp.hpp" + +#include + +#include +#include +#include + +#include +#include + +#include "../mwbase/windowmanager.hpp" + +#include "../mwworld/class.hpp" +#include "../mwworld/ptr.hpp" + +#include "luabindings.hpp" +#include "userdataserializer.hpp" + +namespace MWLua +{ + + LuaManager::LuaManager(const VFS::Manager* vfs, const std::vector& scriptLists) : mLua(vfs) + { + Log(Debug::Info) << "Lua version: " << LuaUtil::getLuaVersion(); + mGlobalScriptList = LuaUtil::parseOMWScriptsFiles(vfs, scriptLists); + + mGlobalSerializer = createUserdataSerializer(false, mWorldView.getObjectRegistry()); + mLocalSerializer = createUserdataSerializer(true, mWorldView.getObjectRegistry()); + mGlobalLoader = createUserdataSerializer(false, mWorldView.getObjectRegistry(), &mContentFileMapping); + mLocalLoader = createUserdataSerializer(true, mWorldView.getObjectRegistry(), &mContentFileMapping); + + mGlobalScripts.setSerializer(mGlobalSerializer.get()); + + Context context; + context.mIsGlobal = true; + context.mLuaManager = this; + context.mLua = &mLua; + context.mWorldView = &mWorldView; + context.mLocalEventQueue = &mLocalEvents; + context.mGlobalEventQueue = &mGlobalEvents; + context.mSerializer = mGlobalSerializer.get(); + + Context localContext = context; + localContext.mIsGlobal = false; + localContext.mSerializer = mLocalSerializer.get(); + + initObjectBindingsForGlobalScripts(context); + initCellBindingsForGlobalScripts(context); + initObjectBindingsForLocalScripts(localContext); + initCellBindingsForLocalScripts(localContext); + LocalScripts::initializeSelfPackage(localContext); + initInputBindings(localContext); + + mLua.addCommonPackage("openmw.async", getAsyncPackageInitializer(context)); + mLua.addCommonPackage("openmw.util", LuaUtil::initUtilPackage(mLua.sol())); + mLua.addCommonPackage("openmw.core", initCorePackage(context)); + mLua.addCommonPackage("openmw.query", initQueryPackage(context)); + mGlobalScripts.addPackage("openmw.world", initWorldPackage(context)); + mCameraPackage = initCameraPackage(localContext); + mUserInterfacePackage = initUserInterfacePackage(localContext); + mNearbyPackage = initNearbyPackage(localContext); + } + + void LuaManager::init() + { + mKeyPressEvents.clear(); + for (const std::string& path : mGlobalScriptList) + if (mGlobalScripts.addNewScript(path)) + Log(Debug::Info) << "Global script started: " << path; + } + + void LuaManager::update(bool paused, float dt) + { + ObjectRegistry* objectRegistry = mWorldView.getObjectRegistry(); + + if (!mPlayer.isEmpty()) + { + MWWorld::Ptr newPlayerPtr = MWBase::Environment::get().getWorld()->getPlayerPtr(); + if (!(getId(mPlayer) == getId(newPlayerPtr))) + throw std::logic_error("Player Refnum was changed unexpectedly"); + if (!mPlayer.isInCell() || !newPlayerPtr.isInCell() || mPlayer.getCell() != newPlayerPtr.getCell()) + { + mPlayer = newPlayerPtr; // player was moved to another cell, update ptr in registry + objectRegistry->registerPtr(mPlayer); + } + } + 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(); + + { // Update time and process timers + double seconds = mWorldView.getGameTimeInSeconds() + dt; + mWorldView.setGameTimeInSeconds(seconds); + double hours = mWorldView.getGameTimeInHours(); + + mGlobalScripts.processTimers(seconds, hours); + for (LocalScripts* scripts : mActiveLocalScripts) + scripts->processTimers(seconds, hours); + } + + // Receive events + for (GlobalEvent& e : globalEvents) + mGlobalScripts.receiveEvent(e.mEventName, e.mEventData); + for (LocalEvent& e : localEvents) + { + LObject obj(e.mDest, objectRegistry); + LocalScripts* scripts = obj.isValid() ? obj.ptr().getRefData().getLuaScripts() : nullptr; + if (scripts) + scripts->receiveEvent(e.mEventName, e.mEventData); + else + Log(Debug::Debug) << "Ignored event " << e.mEventName << " to L" << idToString(e.mDest) + << ". Object not found or has no attached scripts"; + } + + // Engine handlers in local scripts + PlayerScripts* playerScripts = dynamic_cast(mPlayer.getRefData().getLuaScripts()); + if (playerScripts) + { + for (const SDL_Keysym& key : mKeyPressEvents) + playerScripts->keyPress(key); + } + mKeyPressEvents.clear(); + + for (const LocalEngineEvent& e : mLocalEngineEvents) + { + LObject obj(e.mDest, objectRegistry); + if (!obj.isValid()) + { + Log(Debug::Verbose) << "Can not call engine handlers: object" << idToString(e.mDest) << " is not found"; + continue; + } + LocalScripts* scripts = obj.ptr().getRefData().getLuaScripts(); + if (scripts) + scripts->receiveEngineEvent(e.mEvent, objectRegistry); + } + mLocalEngineEvents.clear(); + + for (LocalScripts* scripts : mActiveLocalScripts) + scripts->update(dt); + + // Engine handlers in global scripts + if (mPlayerChanged) + { + mPlayerChanged = false; + mGlobalScripts.playerAdded(GObject(getId(mPlayer), objectRegistry)); + } + + for (ObjectId id : mActorAddedEvents) + mGlobalScripts.actorActive(GObject(id, objectRegistry)); + mActorAddedEvents.clear(); + + mGlobalScripts.update(dt); + } + + void LuaManager::applyQueuedChanges() + { + MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); + for (const std::string& message : mUIMessages) + windowManager->messageBox(message); + mUIMessages.clear(); + + for (std::unique_ptr& action : mActionQueue) + action->apply(mWorldView); + mActionQueue.clear(); + + if (mTeleportPlayerAction) + mTeleportPlayerAction->apply(mWorldView); + mTeleportPlayerAction.reset(); + } + + void LuaManager::clear() + { + mActiveLocalScripts.clear(); + mLocalEvents.clear(); + mGlobalEvents.clear(); + mKeyPressEvents.clear(); + mActorAddedEvents.clear(); + mLocalEngineEvents.clear(); + mPlayerChanged = false; + mWorldView.clear(); + if (!mPlayer.isEmpty()) + { + mPlayer.getCellRef().unsetRefNum(); + mPlayer.getRefData().setLuaScripts(nullptr); + mPlayer = MWWorld::Ptr(); + } + } + + void LuaManager::setupPlayer(const MWWorld::Ptr& ptr) + { + if (!mPlayer.isEmpty()) + throw std::logic_error("Player is initialized twice"); + mWorldView.objectAddedToScene(ptr); + mPlayer = ptr; + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (!localScripts) + localScripts = createLocalScripts(ptr); + mActiveLocalScripts.insert(localScripts); + mLocalEngineEvents.push_back({getId(ptr), LocalScripts::OnActive{}}); + mPlayerChanged = true; + } + + 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); + mLocalEngineEvents.push_back({getId(ptr), LocalScripts::OnActive{}}); + } + + if (ptr.getClass().isActor() && ptr != mPlayer) + mActorAddedEvents.push_back(getId(ptr)); + } + + void LuaManager::objectRemovedFromScene(const MWWorld::Ptr& ptr) + { + mWorldView.objectRemovedFromScene(ptr); + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (localScripts) + { + mActiveLocalScripts.erase(localScripts); + if (!mWorldView.getObjectRegistry()->getPtr(getId(ptr), true).isEmpty()) + mLocalEngineEvents.push_back({getId(ptr), LocalScripts::OnInactive{}}); + } + } + + void LuaManager::registerObject(const MWWorld::Ptr& ptr) + { + mWorldView.getObjectRegistry()->registerPtr(ptr); + } + + void LuaManager::deregisterObject(const MWWorld::Ptr& ptr) + { + mWorldView.getObjectRegistry()->deregisterPtr(ptr); + } + + void LuaManager::keyPressed(const SDL_KeyboardEvent& arg) + { + mKeyPressEvents.push_back(arg.keysym); + } + + void LuaManager::appliedToObject(const MWWorld::Ptr& toPtr, std::string_view recordId, const MWWorld::Ptr& fromPtr) + { + mLocalEngineEvents.push_back({getId(toPtr), LocalScripts::OnConsume{std::string(recordId)}}); + } + + 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) + { + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (!localScripts) + { + localScripts = createLocalScripts(ptr); + if (ptr.isInCell() && MWBase::Environment::get().getWorld()->isCellActive(ptr.getCell())) + mActiveLocalScripts.insert(localScripts); + } + localScripts->addNewScript(scriptPath); + } + + LocalScripts* LuaManager::createLocalScripts(const MWWorld::Ptr& ptr) + { + std::shared_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") + { + scripts = std::make_shared(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry())); + scripts->addPackage("openmw.ui", mUserInterfacePackage); + scripts->addPackage("openmw.camera", mCameraPackage); + } + else + scripts = std::make_shared(&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(); + } + + void LuaManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) + { + writer.startRecord(ESM::REC_LUAM); + + mWorldView.save(writer); + ESM::LuaScripts globalScripts; + mGlobalScripts.save(globalScripts); + globalScripts.save(writer); + saveEvents(writer, mGlobalEvents, mLocalEvents); + + writer.endRecord(ESM::REC_LUAM); + } + + void LuaManager::readRecord(ESM::ESMReader& reader, uint32_t type) + { + if (type != ESM::REC_LUAM) + throw std::runtime_error("ESM::REC_LUAM is expected"); + + mWorldView.load(reader); + ESM::LuaScripts globalScripts; + globalScripts.load(reader); + loadEvents(mLua.sol(), reader, mGlobalEvents, mLocalEvents, mContentFileMapping, mGlobalLoader.get()); + + mGlobalScripts.setSerializer(mGlobalLoader.get()); + mGlobalScripts.load(globalScripts, false); + mGlobalScripts.setSerializer(mGlobalSerializer.get()); + } + + void LuaManager::saveLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScripts& data) + { + if (ptr.getRefData().getLuaScripts()) + ptr.getRefData().getLuaScripts()->save(data); + else + data.mScripts.clear(); + } + + void LuaManager::loadLocalScripts(const MWWorld::Ptr& ptr, const ESM::LuaScripts& data) + { + if (data.mScripts.empty()) + { + if (ptr.getRefData().getLuaScripts()) + ptr.getRefData().setLuaScripts(nullptr); + return; + } + + mWorldView.getObjectRegistry()->registerPtr(ptr); + LocalScripts* scripts = createLocalScripts(ptr); + + scripts->setSerializer(mLocalLoader.get()); + scripts->load(data, true); + scripts->setSerializer(mLocalSerializer.get()); + + // LiveCellRef is usually copied after loading, so this Ptr will become invalid and should be deregistered. + mWorldView.getObjectRegistry()->deregisterPtr(ptr); + } + + void LuaManager::reloadAllScripts() + { + Log(Debug::Info) << "Reload Lua"; + mLua.dropScriptCache(); + + { // Reload global scripts + ESM::LuaScripts data; + mGlobalScripts.save(data); + mGlobalScripts.removeAllScripts(); + for (const std::string& path : mGlobalScriptList) + if (mGlobalScripts.addNewScript(path)) + Log(Debug::Info) << "Global script restarted: " << path; + mGlobalScripts.load(data, false); + } + + for (const auto& [id, ptr] : mWorldView.getObjectRegistry()->mObjectMapping) + { // Reload local scripts + LocalScripts* scripts = ptr.getRefData().getLuaScripts(); + if (scripts == nullptr) + continue; + ESM::LuaScripts data; + scripts->save(data); + scripts->load(data, true); + } + } + +} diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp new file mode 100644 index 0000000000..df87457b23 --- /dev/null +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -0,0 +1,114 @@ +#ifndef MWLUA_LUAMANAGERIMP_H +#define MWLUA_LUAMANAGERIMP_H + +#include +#include + +#include + +#include "../mwbase/luamanager.hpp" + +#include "actions.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, const std::vector& globalScriptLists); + + // 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 registerObject(const MWWorld::Ptr& ptr) override; + void deregisterObject(const MWWorld::Ptr& ptr) override; + void keyPressed(const SDL_KeyboardEvent &arg) override; + void appliedToObject(const MWWorld::Ptr& toPtr, std::string_view recordId, const MWWorld::Ptr& fromPtr) override; + + 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 + void addLocalScript(const MWWorld::Ptr&, const std::string& scriptPath); + void addAction(std::unique_ptr&& action) { mActionQueue.push_back(std::move(action)); } + void addTeleportPlayerAction(std::unique_ptr&& action) { mTeleportPlayerAction = std::move(action); } + void addUIMessage(std::string_view message) { mUIMessages.emplace_back(message); } + + // Saving + void write(ESM::ESMWriter& writer, Loading::Listener& progress) override; + void saveLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScripts& data) override; + + // Loading from a save + void readRecord(ESM::ESMReader& reader, uint32_t type) override; + void loadLocalScripts(const MWWorld::Ptr& ptr, const ESM::LuaScripts& data) override; + void setContentFileMapping(const std::map& mapping) override { mContentFileMapping = mapping; } + + // Drops script cache and reloads all scripts. Calls `onSave` and `onLoad` for every script. + void reloadAllScripts() override; + + private: + LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr); + + LuaUtil::LuaState mLua; + sol::table mNearbyPackage; + sol::table mUserInterfacePackage; + sol::table mCameraPackage; + + std::vector mGlobalScriptList; + GlobalScripts mGlobalScripts{&mLua}; + std::set mActiveLocalScripts; + WorldView mWorldView; + + bool mPlayerChanged = false; + MWWorld::Ptr mPlayer; + + GlobalEventQueue mGlobalEvents; + LocalEventQueue mLocalEvents; + + std::unique_ptr mGlobalSerializer; + std::unique_ptr mLocalSerializer; + + std::map mContentFileMapping; + std::unique_ptr mGlobalLoader; + std::unique_ptr mLocalLoader; + + std::vector mKeyPressEvents; + std::vector mActorAddedEvents; + + struct LocalEngineEvent + { + ObjectId mDest; + LocalScripts::EngineEvent mEvent; + }; + std::vector mLocalEngineEvents; + + // Queued actions that should be done in main thread. Processed by applyQueuedChanges(). + std::vector> mActionQueue; + std::unique_ptr mTeleportPlayerAction; + std::vector mUIMessages; + }; + +} + +#endif // MWLUA_LUAMANAGERIMP_H diff --git a/apps/openmw/mwlua/object.cpp b/apps/openmw/mwlua/object.cpp new file mode 100644 index 0000000000..696179d003 --- /dev/null +++ b/apps/openmw/mwlua/object.cpp @@ -0,0 +1,155 @@ +#include "object.hpp" + +#include "../mwclass/activator.hpp" +#include "../mwclass/armor.hpp" +#include "../mwclass/book.hpp" +#include "../mwclass/clothing.hpp" +#include "../mwclass/container.hpp" +#include "../mwclass/creature.hpp" +#include "../mwclass/door.hpp" +#include "../mwclass/ingredient.hpp" +#include "../mwclass/light.hpp" +#include "../mwclass/misc.hpp" +#include "../mwclass/npc.hpp" +#include "../mwclass/potion.hpp" +#include "../mwclass/static.hpp" +#include "../mwclass/weapon.hpp" + +namespace MWLua +{ + + std::string idToString(const ObjectId& id) + { + return std::to_string(id.mIndex) + "_" + std::to_string(id.mContentFile); + } + + const static std::map classNames = { + {typeid(MWClass::Activator), "Activator"}, + {typeid(MWClass::Armor), "Armor"}, + {typeid(MWClass::Book), "Book"}, + {typeid(MWClass::Clothing), "Clothing"}, + {typeid(MWClass::Container), "Container"}, + {typeid(MWClass::Creature), "Creature"}, + {typeid(MWClass::Door), "Door"}, + {typeid(MWClass::Ingredient), "Ingredient"}, + {typeid(MWClass::Light), "Light"}, + {typeid(MWClass::Miscellaneous), "Miscellaneous"}, + {typeid(MWClass::Npc), "NPC"}, + {typeid(MWClass::Potion), "Potion"}, + {typeid(MWClass::Static), "Static"}, + {typeid(MWClass::Weapon), "Weapon"}, + }; + + std::string_view getMWClassName(const std::type_index& cls_type, std::string_view fallback) + { + auto it = classNames.find(cls_type); + if (it != classNames.end()) + return it->second; + else + return fallback; + } + + bool isMarker(const MWWorld::Ptr& ptr) + { + std::string_view id = *ptr.getCellRef().getRefIdPtr(); + return id == "prisonmarker" || id == "divinemarker" || id == "templemarker" || id == "northmarker"; + } + + std::string_view getMWClassName(const MWWorld::Ptr& ptr) + { + if (*ptr.getCellRef().getRefIdPtr() == "player") + return "Player"; + if (isMarker(ptr)) + return "Marker"; + return getMWClassName(typeid(ptr.getClass()), ptr.getTypeName()); + } + + std::string ptrToString(const MWWorld::Ptr& ptr) + { + std::string res = "object"; + res.append(idToString(getId(ptr))); + res.append(" ("); + res.append(getMWClassName(ptr)); + res.append(", "); + res.append(*ptr.getCellRef().getRefIdPtr()); + res.append(")"); + return res; + } + + std::string Object::toString() const + { + if (isValid()) + return ptrToString(ptr()); + else + return "object" + idToString(mId) + " (not found)"; + } + + 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 local) + { + MWWorld::Ptr ptr; + auto it = mObjectMapping.find(id); + if (it != mObjectMapping.end()) + ptr = it->second; + if (local) + { + // TODO: Return ptr only if it is active or was active in the previous frame, otherwise return empty. + // Needed because in multiplayer inactive objects will not be synchronized, so an 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..c0b6bf1919 --- /dev/null +++ b/apps/openmw/mwlua/object.hpp @@ -0,0 +1,107 @@ +#ifndef MWLUA_OBJECT_H +#define MWLUA_OBJECT_H + +#include + +#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); + std::string ptrToString(const MWWorld::Ptr& ptr); + bool isMarker(const MWWorld::Ptr& ptr); + std::string_view getMWClassName(const std::type_index& cls_type, std::string_view fallback = "Unknown"); + std::string_view getMWClassName(const MWWorld::Ptr& ptr); + + // 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 local = true, returns non-empty ptr only if it can be used in local scripts + // (i.e. is active or was active in the previous frame). + MWWorld::Ptr getPtr(ObjectId id, bool local); + + // Needed only for saving/loading. + const ObjectId& getLastAssignedId() const { return mLastAssignedId; } + void setLastAssignedId(ObjectId id) { mLastAssignedId = id; } + + private: + friend class Object; + friend class LuaManager; + + 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 { return getMWClassName(ptr()); } + + // 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..d4ae041549 --- /dev/null +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -0,0 +1,340 @@ +#include "luabindings.hpp" + +#include +#include + +#include "../mwclass/door.hpp" + +#include "../mwworld/containerstore.hpp" +#include "../mwworld/inventorystore.hpp" + +#include "eventqueue.hpp" +#include "luamanagerimp.hpp" + +namespace MWLua +{ + template + struct Inventory + { + ObjectT mObj; + }; +} + +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 {}; + template <> + struct is_automagical> : std::false_type {}; + template <> + struct is_automagical> : std::false_type {}; +} + +namespace MWLua +{ + + template + using Cell = std::conditional_t, LCell, GCell>; + + template + static const MWWorld::Ptr& requireClass(const MWWorld::Ptr& ptr) + { + if (typeid(Class) != typeid(ptr.getClass())) + { + std::string msg = "Requires type '"; + msg.append(getMWClassName(typeid(Class))); + msg.append("', but applied to "); + msg.append(ptrToString(ptr)); + throw std::runtime_error(msg); + } + return ptr; + } + + 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); + }; + listT["select"] = [context](const ListT& list, const Queries::Query& query) + { + return ListT{selectObjectsFromList(query, list.mIds, context)}; + }; + } + + 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["cell"] = sol::readonly_property([](const ObjectT& o) -> sol::optional> + { + const MWWorld::Ptr& ptr = o.ptr(); + if (ptr.isInCell()) + return Cell{ptr.getCell()}; + else + return sol::nullopt; + }); + 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["count"] = sol::readonly_property([](const ObjectT& o) { return o.ptr().getRefData().getCount(); }); + 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)}); + }; + + objectT["canMove"] = [](const ObjectT& o) + { + const MWWorld::Class& cls = o.ptr().getClass(); + return cls.getMaxSpeed(o.ptr()) > 0; + }; + objectT["getRunSpeed"] = [](const ObjectT& o) + { + const MWWorld::Class& cls = o.ptr().getClass(); + return cls.getRunSpeed(o.ptr()); + }; + objectT["getWalkSpeed"] = [](const ObjectT& o) + { + const MWWorld::Class& cls = o.ptr().getClass(); + return cls.getWalkSpeed(o.ptr()); + }; + + 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); + }; + + objectT["teleport"] = [luaManager=context.mLuaManager](const GObject& object, std::string_view cell, + const osg::Vec3f& pos, const sol::optional& optRot) + { + MWWorld::Ptr ptr = object.ptr(); + osg::Vec3f rot = optRot ? *optRot : ptr.getRefData().getPosition().asRotationVec3(); + auto action = std::make_unique(object.id(), std::string(cell), pos, rot); + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + luaManager->addTeleportPlayerAction(std::move(action)); + else + luaManager->addAction(std::move(action)); + }; + } + } + + template + static void addDoorBindings(sol::usertype& objectT, const Context& context) + { + auto ptr = [](const ObjectT& o) -> const MWWorld::Ptr& { return requireClass(o.ptr()); }; + + objectT["isTeleport"] = sol::readonly_property([ptr](const ObjectT& o) + { + return ptr(o).getCellRef().getTeleport(); + }); + objectT["destPosition"] = sol::readonly_property([ptr](const ObjectT& o) -> osg::Vec3f + { + return ptr(o).getCellRef().getDoorDest().asVec3(); + }); + objectT["destRotation"] = sol::readonly_property([ptr](const ObjectT& o) -> osg::Vec3f + { + return ptr(o).getCellRef().getDoorDest().asRotationVec3(); + }); + objectT["destCell"] = sol::readonly_property( + [ptr, worldView=context.mWorldView](const ObjectT& o) -> sol::optional> + { + const MWWorld::CellRef& cellRef = ptr(o).getCellRef(); + if (!cellRef.getTeleport()) + return sol::nullopt; + MWWorld::CellStore* cell = worldView->findCell(cellRef.getDestCell(), cellRef.getDoorDest().asVec3()); + if (cell) + return Cell{cell}; + else + return sol::nullopt; + }); + } + + static SetEquipmentAction::Equipment parseEquipmentTable(sol::table equipment) + { + SetEquipmentAction::Equipment eqp; + for (auto& [key, value] : equipment) + { + int slot = key.as(); + if (value.is()) + eqp[slot] = value.as().id(); + else + eqp[slot] = value.as(); + } + return eqp; + } + + template + static void addInventoryBindings(sol::usertype& objectT, const std::string& prefix, const Context& context) + { + using InventoryT = Inventory; + sol::usertype inventoryT = context.mLua->sol().new_usertype(prefix + "Inventory"); + + objectT["getEquipment"] = [context](const ObjectT& o) + { + const MWWorld::Ptr& ptr = o.ptr(); + sol::table equipment(context.mLua->sol(), sol::create); + if (!ptr.getClass().hasInventoryStore(ptr)) + return equipment; + + MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); + for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) + { + auto it = store.getSlot(slot); + if (it == store.end()) + continue; + context.mWorldView->getObjectRegistry()->registerPtr(*it); + equipment[slot] = ObjectT(getId(*it), context.mWorldView->getObjectRegistry()); + } + return equipment; + }; + objectT["isEquipped"] = [](const ObjectT& actor, const ObjectT& item) + { + const MWWorld::Ptr& ptr = actor.ptr(); + if (!ptr.getClass().hasInventoryStore(ptr)) + return false; + MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); + return store.isEquipped(item.ptr()); + }; + + objectT["inventory"] = sol::readonly_property([](const ObjectT& o) { return InventoryT{o}; }); + inventoryT[sol::meta_function::to_string] = + [](const InventoryT& inv) { return "Inventory[" + inv.mObj.toString() + "]"; }; + + auto getWithMask = [context](const InventoryT& inventory, int mask) + { + const MWWorld::Ptr& ptr = inventory.mObj.ptr(); + MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); + ObjectIdList list = std::make_shared>(); + auto it = store.begin(mask); + while (it.getType() != -1) + { + const MWWorld::Ptr& item = *(it++); + context.mWorldView->getObjectRegistry()->registerPtr(item); + list->push_back(getId(item)); + } + return ObjectList{list}; + }; + + inventoryT["getAll"] = + [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_All); }; + inventoryT["getPotions"] = + [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Potion); }; + inventoryT["getApparatuses"] = + [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Apparatus); }; + inventoryT["getArmor"] = + [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Armor); }; + inventoryT["getBooks"] = + [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Book); }; + inventoryT["getClothing"] = + [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Clothing); }; + inventoryT["getIngredients"] = + [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Ingredient); }; + inventoryT["getLights"] = + [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Light); }; + inventoryT["getLockpicks"] = + [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Lockpick); }; + inventoryT["getMiscellaneous"] = + [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Miscellaneous); }; + inventoryT["getProbes"] = + [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Probe); }; + inventoryT["getRepairKits"] = + [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Repair); }; + inventoryT["getWeapons"] = + [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Weapon); }; + + inventoryT["countOf"] = [](const InventoryT& inventory, const std::string& recordId) + { + const MWWorld::Ptr& ptr = inventory.mObj.ptr(); + MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); + return store.count(recordId); + }; + + if constexpr (std::is_same_v) + { // Only for global scripts + objectT["setEquipment"] = [manager=context.mLuaManager](const GObject& obj, sol::table equipment) + { + if (!obj.ptr().getClass().hasInventoryStore(obj.ptr())) + { + if (!equipment.empty()) + throw std::runtime_error(ptrToString(obj.ptr()) + " has no equipment slots"); + return; + } + manager->addAction(std::make_unique(obj.id(), parseEquipmentTable(equipment))); + }; + + // TODO + // obj.inventory:drop(obj2, [count]) + // obj.inventory:drop(recordId, [count]) + // obj.inventory:addNew(recordId, [count]) + // obj.inventory:remove(obj/recordId, [count]) + /*objectT["moveInto"] = [](const GObject& obj, const InventoryT& inventory) {}; + inventoryT["drop"] = [](const InventoryT& inventory) {}; + inventoryT["addNew"] = [](const InventoryT& inventory) {}; + inventoryT["remove"] = [](const InventoryT& inventory) {};*/ + } + } + + template + static void initObjectBindings(const std::string& prefix, const Context& context) + { + sol::usertype objectT = context.mLua->sol().new_usertype(prefix + "Object"); + addBasicBindings(objectT, context); + addDoorBindings(objectT, context); + addInventoryBindings(objectT, prefix, 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..72e064bb9b --- /dev/null +++ b/apps/openmw/mwlua/playerscripts.hpp @@ -0,0 +1,27 @@ +#ifndef MWLUA_PLAYERSCRIPTS_H +#define MWLUA_PLAYERSCRIPTS_H + +#include + +#include "localscripts.hpp" + +namespace MWLua +{ + + class PlayerScripts : public LocalScripts + { + public: + PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) : LocalScripts(lua, obj) + { + registerEngineHandlers({&mKeyPressHandlers}); + } + + void keyPress(const SDL_Keysym& key) { callEngineHandlers(mKeyPressHandlers, key); } + + private: + EngineHandlerList mKeyPressHandlers{"onKeyPress"}; + }; + +} + +#endif // MWLUA_PLAYERSCRIPTS_H diff --git a/apps/openmw/mwlua/query.cpp b/apps/openmw/mwlua/query.cpp new file mode 100644 index 0000000000..c357e56884 --- /dev/null +++ b/apps/openmw/mwlua/query.cpp @@ -0,0 +1,191 @@ +#include "query.hpp" + +#include + +#include + +#include "../mwclass/container.hpp" +#include "../mwworld/cellstore.hpp" + +#include "worldview.hpp" + +namespace MWLua +{ + + static std::vector initBasicFieldGroups() + { + auto createGroup = [](std::string name, const auto& arr) -> QueryFieldGroup + { + std::vector fieldPtrs; + fieldPtrs.reserve(arr.size()); + for (const Queries::Field& field : arr) + fieldPtrs.push_back(&field); + return {std::move(name), std::move(fieldPtrs)}; + }; + static std::array objectFields = { + Queries::Field({"type"}, typeid(std::string)), + Queries::Field({"recordId"}, typeid(std::string)), + Queries::Field({"cell", "name"}, typeid(std::string)), + Queries::Field({"cell", "region"}, typeid(std::string)), + Queries::Field({"cell", "isExterior"}, typeid(bool)), + Queries::Field({"count"}, typeid(int32_t)), + }; + static std::array doorFields = { + Queries::Field({"isTeleport"}, typeid(bool)), + Queries::Field({"destCell", "name"}, typeid(std::string)), + Queries::Field({"destCell", "region"}, typeid(std::string)), + Queries::Field({"destCell", "isExterior"}, typeid(bool)), + }; + return std::vector{ + createGroup("OBJECT", objectFields), + createGroup("DOOR", doorFields), + }; + } + + const std::vector& getBasicQueryFieldGroups() + { + static std::vector fieldGroups = initBasicFieldGroups(); + return fieldGroups; + } + + bool checkQueryConditions(const Queries::Query& query, const ObjectId& id, const Context& context) + { + auto compareFn = [](auto&& a, auto&& b, Queries::Condition::Type t) + { + switch (t) + { + case Queries::Condition::EQUAL: return a == b; + case Queries::Condition::NOT_EQUAL: return a != b; + case Queries::Condition::GREATER: return a > b; + case Queries::Condition::GREATER_OR_EQUAL: return a >= b; + case Queries::Condition::LESSER: return a < b; + case Queries::Condition::LESSER_OR_EQUAL: return a <= b; + default: + throw std::runtime_error("Unsupported condition type"); + } + }; + sol::object obj; + MWWorld::Ptr ptr; + if (context.mIsGlobal) + { + GObject g(id, context.mWorldView->getObjectRegistry()); + if (!g.isValid()) + return false; + ptr = g.ptr(); + obj = sol::make_object(context.mLua->sol(), g); + } + else + { + LObject l(id, context.mWorldView->getObjectRegistry()); + if (!l.isValid()) + return false; + ptr = l.ptr(); + obj = sol::make_object(context.mLua->sol(), l); + } + if (ptr.getRefData().getCount() == 0) + return false; + + // It is important to exclude all markers before checking what class it is. + // For example "prisonmarker" has class "Door" despite that it is only an invisible marker. + if (isMarker(ptr)) + return false; + + const MWWorld::Class& cls = ptr.getClass(); + if (cls.isActivator() != (query.mQueryType == ObjectQueryTypes::ACTIVATORS)) + return false; + if (cls.isActor() != (query.mQueryType == ObjectQueryTypes::ACTORS)) + return false; + if (cls.isDoor() != (query.mQueryType == ObjectQueryTypes::DOORS)) + return false; + if ((typeid(cls) == typeid(MWClass::Container)) != (query.mQueryType == ObjectQueryTypes::CONTAINERS)) + return false; + + std::vector condStack; + for (const Queries::Operation& op : query.mFilter.mOperations) + { + switch(op.mType) + { + case Queries::Operation::PUSH: + { + const Queries::Condition& cond = query.mFilter.mConditions[op.mConditionIndex]; + sol::object fieldObj = obj; + for (const std::string& field : cond.mField->path()) + fieldObj = LuaUtil::getFieldOrNil(fieldObj, field); + bool c; + if (fieldObj == sol::nil) + c = false; + else if (cond.mField->type() == typeid(std::string)) + c = compareFn(fieldObj.as(), std::get(cond.mValue), cond.mType); + else if (cond.mField->type() == typeid(float)) + c = compareFn(fieldObj.as(), std::get(cond.mValue), cond.mType); + else if (cond.mField->type() == typeid(double)) + c = compareFn(fieldObj.as(), std::get(cond.mValue), cond.mType); + else if (cond.mField->type() == typeid(bool)) + c = compareFn(fieldObj.as(), std::get(cond.mValue), cond.mType); + else if (cond.mField->type() == typeid(int32_t)) + c = compareFn(fieldObj.as(), std::get(cond.mValue), cond.mType); + else if (cond.mField->type() == typeid(int64_t)) + c = compareFn(fieldObj.as(), std::get(cond.mValue), cond.mType); + else + throw std::runtime_error("Unknown field type"); + condStack.push_back(c); + break; + } + case Queries::Operation::NOT: + condStack.back() = !condStack.back(); + break; + case Queries::Operation::AND: + { + bool v = condStack.back(); + condStack.pop_back(); + condStack.back() = condStack.back() && v; + break; + } + case Queries::Operation::OR: + { + bool v = condStack.back(); + condStack.pop_back(); + condStack.back() = condStack.back() || v; + break; + } + } + } + return condStack.empty() || condStack.back() != 0; + } + + ObjectIdList selectObjectsFromList(const Queries::Query& query, const ObjectIdList& list, const Context& context) + { + if (!query.mOrderBy.empty() || !query.mGroupBy.empty() || query.mOffset > 0) + throw std::runtime_error("OrderBy, GroupBy, and Offset are not supported"); + + ObjectIdList res = std::make_shared>(); + for (const ObjectId& id : *list) + { + if (static_cast(res->size()) == query.mLimit) + break; + if (checkQueryConditions(query, id, context)) + res->push_back(id); + } + return res; + } + + ObjectIdList selectObjectsFromCellStore(const Queries::Query& query, MWWorld::CellStore* store, const Context& context) + { + if (!query.mOrderBy.empty() || !query.mGroupBy.empty() || query.mOffset > 0) + throw std::runtime_error("OrderBy, GroupBy, and Offset are not supported"); + + ObjectIdList res = std::make_shared>(); + auto visitor = [&](const MWWorld::Ptr& ptr) + { + if (static_cast(res->size()) == query.mLimit) + return false; + context.mWorldView->getObjectRegistry()->registerPtr(ptr); + if (checkQueryConditions(query, getId(ptr), context)) + res->push_back(getId(ptr)); + return static_cast(res->size()) != query.mLimit; + }; + store->forEach(std::move(visitor)); // TODO: maybe use store->forEachType depending on query.mType + return res; + } + +} diff --git a/apps/openmw/mwlua/query.hpp b/apps/openmw/mwlua/query.hpp new file mode 100644 index 0000000000..65bf0c5105 --- /dev/null +++ b/apps/openmw/mwlua/query.hpp @@ -0,0 +1,39 @@ +#ifndef MWLUA_QUERY_H +#define MWLUA_QUERY_H + +#include + +#include + +#include "context.hpp" +#include "object.hpp" + +namespace MWLua +{ + + struct ObjectQueryTypes + { + static constexpr std::string_view ACTIVATORS = "activators"; + static constexpr std::string_view ACTORS = "actors"; + static constexpr std::string_view CONTAINERS = "containers"; + static constexpr std::string_view DOORS = "doors"; + static constexpr std::string_view ITEMS = "items"; + + static constexpr std::string_view types[] = {ACTIVATORS, ACTORS, CONTAINERS, DOORS, ITEMS}; + }; + + struct QueryFieldGroup + { + std::string mName; + std::vector mFields; + }; + const std::vector& getBasicQueryFieldGroups(); + + // TODO: Implement custom fields. QueryFieldGroup registerCustomFields(...); + + ObjectIdList selectObjectsFromList(const Queries::Query& query, const ObjectIdList& list, const Context&); + ObjectIdList selectObjectsFromCellStore(const Queries::Query& query, MWWorld::CellStore* store, const Context&); + +} + +#endif // MWLUA_QUERY_H diff --git a/apps/openmw/mwlua/uibindings.cpp b/apps/openmw/mwlua/uibindings.cpp new file mode 100644 index 0000000000..cb14c41621 --- /dev/null +++ b/apps/openmw/mwlua/uibindings.cpp @@ -0,0 +1,18 @@ +#include "luabindings.hpp" + +#include "luamanagerimp.hpp" + +namespace MWLua +{ + + sol::table initUserInterfacePackage(const Context& context) + { + sol::table api(context.mLua->sol(), sol::create); + api["showMessage"] = [luaManager=context.mLuaManager](std::string_view message) + { + luaManager->addUIMessage(message); + }; + return context.mLua->makeReadOnly(api); + } + +} diff --git a/apps/openmw/mwlua/userdataserializer.cpp b/apps/openmw/mwlua/userdataserializer.cpp new file mode 100644 index 0000000000..6946cd5532 --- /dev/null +++ b/apps/openmw/mwlua/userdataserializer.cpp @@ -0,0 +1,72 @@ +#include "userdataserializer.hpp" + +#include +#include + +#include "object.hpp" + +namespace MWLua +{ + + class Serializer final : public LuaUtil::UserdataSerializer + { + public: + explicit Serializer(bool localSerializer, ObjectRegistry* registry, std::map* contentFileMapping) + : mLocalSerializer(localSerializer), mObjectRegistry(registry), mContentFileMapping(contentFileMapping) {} + + 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 (id.hasContentFile() && mContentFileMapping) + { + auto iter = mContentFileMapping->find(id.mContentFile); + if (iter != mContentFileMapping->end()) + id.mContentFile = iter->second; + } + 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::map* mContentFileMapping; + }; + + std::unique_ptr createUserdataSerializer( + bool local, ObjectRegistry* registry, std::map* contentFileMapping) + { + return std::make_unique(local, registry, contentFileMapping); + } + +} diff --git a/apps/openmw/mwlua/userdataserializer.hpp b/apps/openmw/mwlua/userdataserializer.hpp new file mode 100644 index 0000000000..70af7ebd06 --- /dev/null +++ b/apps/openmw/mwlua/userdataserializer.hpp @@ -0,0 +1,22 @@ +#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. + // contentFileMapping is used only for deserialization. Needed to fix references if the order + // of content files was changed. + std::unique_ptr createUserdataSerializer( + bool local, ObjectRegistry* registry, std::map* contentFileMapping = nullptr); +} + +#endif // MWLUA_USERDATASERIALIZER_H diff --git a/apps/openmw/mwlua/worldview.cpp b/apps/openmw/mwlua/worldview.cpp new file mode 100644 index 0000000000..beaa2d4770 --- /dev/null +++ b/apps/openmw/mwlua/worldview.cpp @@ -0,0 +1,152 @@ +#include "worldview.hpp" + +#include +#include +#include + +#include "../mwclass/container.hpp" + +#include "../mwworld/class.hpp" +#include "../mwworld/timestamp.hpp" + +namespace MWLua +{ + + void WorldView::update() + { + mObjectRegistry.update(); + mActivatorsInScene.updateList(); + mActorsInScene.updateList(); + mContainersInScene.updateList(); + mDoorsInScene.updateList(); + mItemsInScene.updateList(); + } + + void WorldView::clear() + { + mObjectRegistry.clear(); + mActivatorsInScene.clear(); + mActorsInScene.clear(); + mContainersInScene.clear(); + mDoorsInScene.clear(); + mItemsInScene.clear(); + } + + WorldView::ObjectGroup* WorldView::chooseGroup(const MWWorld::Ptr& ptr) + { + // It is important to check `isMarker` first. + // For example "prisonmarker" has class "Door" despite that it is only an invisible marker. + if (isMarker(ptr)) + return nullptr; + const MWWorld::Class& cls = ptr.getClass(); + if (cls.isActivator()) + return &mActivatorsInScene; + if (cls.isActor()) + return &mActorsInScene; + if (cls.isDoor()) + return &mDoorsInScene; + if (typeid(cls) == typeid(MWClass::Container)) + return &mContainersInScene; + if (cls.hasToolTip(ptr)) + return &mItemsInScene; + return nullptr; + } + + void WorldView::objectAddedToScene(const MWWorld::Ptr& ptr) + { + mObjectRegistry.registerPtr(ptr); + ObjectGroup* group = chooseGroup(ptr); + if (group) + addToGroup(*group, ptr); + } + + void WorldView::objectRemovedFromScene(const MWWorld::Ptr& ptr) + { + ObjectGroup* group = chooseGroup(ptr); + if (group) + removeFromGroup(*group, ptr); + } + + double WorldView::getGameTimeInHours() const + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + MWWorld::TimeStamp timeStamp = world->getTimeStamp(); + return static_cast(timeStamp.getDay()) * 24 + timeStamp.getHour(); + } + + void WorldView::load(ESM::ESMReader& esm) + { + esm.getHNT(mGameSeconds, "LUAW"); + ObjectId lastAssignedId; + lastAssignedId.load(esm, true); + mObjectRegistry.setLastAssignedId(lastAssignedId); + } + + void WorldView::save(ESM::ESMWriter& esm) const + { + esm.writeHNT("LUAW", mGameSeconds); + mObjectRegistry.getLastAssignedId().save(esm, true); + } + + 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; + } + + // TODO: If Lua scripts will use several threads at the same time, then `find*Cell` functions should have critical sections. + MWWorld::CellStore* WorldView::findCell(const std::string& name, osg::Vec3f position) + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + bool exterior = name.empty() || world->getExterior(name); + if (exterior) + { + int cellX, cellY; + world->positionToIndex(position.x(), position.y(), cellX, cellY); + return world->getExterior(cellX, cellY); + } + else + return world->getInterior(name); + } + + MWWorld::CellStore* WorldView::findNamedCell(const std::string& name) + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + const ESM::Cell* esmCell = world->getExterior(name); + if (esmCell) + return world->getExterior(esmCell->getGridX(), esmCell->getGridY()); + else + return world->getInterior(name); + } + + MWWorld::CellStore* WorldView::findExteriorCell(int x, int y) + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + return world->getExterior(x, y); + } + +} diff --git a/apps/openmw/mwlua/worldview.hpp b/apps/openmw/mwlua/worldview.hpp new file mode 100644 index 0000000000..ea27e0ff84 --- /dev/null +++ b/apps/openmw/mwlua/worldview.hpp @@ -0,0 +1,81 @@ +#ifndef MWLUA_WORLDVIEW_H +#define MWLUA_WORLDVIEW_H + +#include "object.hpp" + +namespace ESM +{ + class ESMWriter; + class ESMReader; +} + +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. + + // Returns the number of seconds passed from the beginning of the game. + double getGameTimeInSeconds() const { return mGameSeconds; } + void setGameTimeInSeconds(double t) { mGameSeconds = t; } + + // Returns the number of game hours passed from the beginning of the game. + // Note that the number of seconds in a game hour is not fixed. + double getGameTimeInHours() const; + + ObjectIdList getActivatorsInScene() const { return mActivatorsInScene.mList; } + ObjectIdList getActorsInScene() const { return mActorsInScene.mList; } + ObjectIdList getContainersInScene() const { return mContainersInScene.mList; } + ObjectIdList getDoorsInScene() const { return mDoorsInScene.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); + + // Returns list of objects that meets the `query` criteria. + // If onlyActive = true, then search only among the objects that are currently in the scene. + // TODO: ObjectIdList selectObjects(const Queries::Query& query, bool onlyActive); + + MWWorld::CellStore* findCell(const std::string& name, osg::Vec3f position); + MWWorld::CellStore* findNamedCell(const std::string& name); + MWWorld::CellStore* findExteriorCell(int x, int y); + + void load(ESM::ESMReader& esm); + void save(ESM::ESMWriter& esm) const; + + private: + struct ObjectGroup + { + void updateList(); + void clear(); + + bool mChanged = false; + ObjectIdList mList = std::make_shared>(); + std::set mSet; + }; + + ObjectGroup* chooseGroup(const MWWorld::Ptr& ptr); + void addToGroup(ObjectGroup& group, const MWWorld::Ptr& ptr); + void removeFromGroup(ObjectGroup& group, const MWWorld::Ptr& ptr); + + ObjectRegistry mObjectRegistry; + ObjectGroup mActivatorsInScene; + ObjectGroup mActorsInScene; + ObjectGroup mContainersInScene; + ObjectGroup mDoorsInScene; + ObjectGroup mItemsInScene; + + double mGameSeconds = 0; + }; + +} + +#endif // MWLUA_WORLDVIEW_H diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 160cac0dc9..844b2b6987 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -24,6 +24,7 @@ #include "../mwbase/soundmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/statemanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwmechanics/aibreathe.hpp" @@ -1961,6 +1962,8 @@ namespace MWMechanics { bool isPlayer = iter->first == player; CharacterController* ctrl = iter->second->getCharacterController(); + MWBase::LuaManager::ActorControls* luaControls = + MWBase::Environment::get().getLuaManager()->getActorControls(iter->first); float distSqr = (playerPos - iter->first.getRefData().getPosition().asVec3()).length2(); // AI processing is only done within given distance to the player. @@ -2062,7 +2065,7 @@ namespace MWMechanics if (iter->first != player) { CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first); - if (isConscious(iter->first)) + if (isConscious(iter->first) && !(luaControls && luaControls->mDisableAI)) { stats.getAiSequence().execute(iter->first, *ctrl, duration); updateGreetingState(iter->first, *iter->second, timerUpdateHello > 0); @@ -2071,7 +2074,7 @@ namespace MWMechanics } } } - else if (aiActive && iter->first != player && isConscious(iter->first)) + else if (aiActive && iter->first != player && isConscious(iter->first) && !(luaControls && luaControls->mDisableAI)) { CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first); stats.getAiSequence().execute(iter->first, *ctrl, duration, /*outOfRange*/true); @@ -2088,6 +2091,32 @@ namespace MWMechanics if (timerUpdateEquippedLight == 0) updateEquippedLight(iter->first, updateEquippedLightInterval, showTorches); } + + if (luaControls && isConscious(iter->first)) + { + Movement& mov = iter->first.getClass().getMovementSettings(iter->first); + CreatureStats& stats = iter->first.getClass().getCreatureStats(iter->first); + float speedFactor = isPlayer ? 1.f : mov.mSpeedFactor; + osg::Vec2f movement = osg::Vec2f(mov.mPosition[0], mov.mPosition[1]) * speedFactor; + float rotationZ = mov.mRotation[2]; + bool jump = mov.mPosition[2] == 1; + bool runFlag = stats.getMovementFlag(MWMechanics::CreatureStats::Flag_Run); + if (luaControls->mControlledFromLua) + { + mov.mPosition[0] = luaControls->mSideMovement; + mov.mPosition[1] = luaControls->mMovement; + mov.mPosition[2] = luaControls->mJump ? 1 : 0; + mov.mRotation[1] = 0; + mov.mRotation[2] = luaControls->mTurn; + mov.mSpeedFactor = osg::Vec2(luaControls->mMovement, luaControls->mSideMovement).length(); + stats.setMovementFlag(MWMechanics::CreatureStats::Flag_Run, luaControls->mRun); + } + luaControls->mSideMovement = movement.x(); + luaControls->mMovement = movement.y(); + luaControls->mTurn = rotationZ; + luaControls->mJump = jump; + luaControls->mRun = runFlag; + } } } diff --git a/apps/openmw/mwrender/groundcover.cpp b/apps/openmw/mwrender/groundcover.cpp index 9416a48ec8..dd64c851f1 100644 --- a/apps/openmw/mwrender/groundcover.cpp +++ b/apps/openmw/mwrender/groundcover.cpp @@ -225,7 +225,7 @@ namespace MWRender esm.resize(index+1); cell->restore(esm[index], i); ESM::CellRef ref; - ref.mRefNum.mContentFile = ESM::RefNum::RefNum_NoContentFile; + ref.mRefNum.unset(); bool deleted = false; while(cell->getNextRef(esm[index], ref, deleted)) { diff --git a/apps/openmw/mwrender/objectpaging.cpp b/apps/openmw/mwrender/objectpaging.cpp index a5792d56b9..6b5f9a6e34 100644 --- a/apps/openmw/mwrender/objectpaging.cpp +++ b/apps/openmw/mwrender/objectpaging.cpp @@ -422,7 +422,7 @@ namespace MWRender esm.resize(index+1); cell->restore(esm[index], i); ESM::CellRef ref; - ref.mRefNum.mContentFile = ESM::RefNum::RefNum_NoContentFile; + ref.mRefNum.unset(); ESM::MovedCellRef cMRef; cMRef.mRefNum.mIndex = 0; bool deleted = false; diff --git a/apps/openmw/mwscript/docs/vmformat.txt b/apps/openmw/mwscript/docs/vmformat.txt index 7c470fe413..aaba5e5986 100644 --- a/apps/openmw/mwscript/docs/vmformat.txt +++ b/apps/openmw/mwscript/docs/vmformat.txt @@ -480,5 +480,6 @@ op 0x200031d: StartScript, explicit op 0x200031e: GetDistance op 0x200031f: GetDistance, explicit op 0x2000320: Help +op 0x2000321: ReloadLua -opcodes 0x2000321-0x3ffffff unused +opcodes 0x2000322-0x3ffffff unused diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index ca9bcbe002..0b36d8bb48 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -28,6 +28,7 @@ #include "../mwbase/scriptmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/player.hpp" @@ -169,8 +170,6 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); - if(!ptr.isEmpty() && !ptr.mRef->mData.isEnabled()) - ptr.mRef->mData.mPhysicsPostponed = false; MWBase::Environment::get().getWorld()->enable (ptr); } }; @@ -1599,6 +1598,17 @@ namespace MWScript } }; + class OpReloadLua : public Interpreter::Opcode0 + { + public: + + void execute (Interpreter::Runtime& runtime) override + { + MWBase::Environment::get().getLuaManager()->reloadAllScripts(); + runtime.getContext().report("All Lua scripts are reloaded"); + } + }; + void installOpcodes (Interpreter::Interpreter& interpreter) { interpreter.installSegment5 (Compiler::Misc::opcodeMenuMode, new OpMenuMode); @@ -1719,6 +1729,7 @@ namespace MWScript interpreter.installSegment5 (Compiler::Misc::opcodeRepairedOnMeExplicit, new OpRepairedOnMe); interpreter.installSegment5 (Compiler::Misc::opcodeToggleRecastMesh, new OpToggleRecastMesh); interpreter.installSegment5 (Compiler::Misc::opcodeHelp, new OpHelp); + interpreter.installSegment5 (Compiler::Misc::opcodeReloadLua, new OpReloadLua); } } } diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index b242862772..4e4a8e95d6 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -27,6 +27,7 @@ #include "../mwbase/scriptmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/inputmanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwworld/player.hpp" #include "../mwworld/class.hpp" @@ -59,6 +60,7 @@ void MWState::StateManager::cleanup (bool force) MWMechanics::CreatureStats::cleanup(); } + MWBase::Environment::get().getLuaManager()->clear(); } std::map MWState::StateManager::buildContentFileIndexMap (const ESM::ESMReader& reader) @@ -146,7 +148,7 @@ void MWState::StateManager::newGame (bool bypass) { Log(Debug::Info) << "Starting a new game"; MWBase::Environment::get().getScriptManager()->getGlobalScripts().addStartup(); - + MWBase::Environment::get().getLuaManager()->newGameStarted(); MWBase::Environment::get().getWorld()->startNewGame (bypass); mState = State_Running; @@ -249,6 +251,7 @@ void MWState::StateManager::saveGame (const std::string& description, const Slot int recordCount = 1 // saved game header +MWBase::Environment::get().getJournal()->countSavedGameRecords() + +MWBase::Environment::get().getLuaManager()->countSavedGameRecords() +MWBase::Environment::get().getWorld()->countSavedGameRecords() +MWBase::Environment::get().getScriptManager()->getGlobalScripts().countSavedGameRecords() +MWBase::Environment::get().getDialogueManager()->countSavedGameRecords() @@ -272,6 +275,9 @@ void MWState::StateManager::saveGame (const std::string& description, const Slot MWBase::Environment::get().getJournal()->write (writer, listener); MWBase::Environment::get().getDialogueManager()->write (writer, listener); + // LuaManager::write should be called before World::write because world also saves + // local scripts that depend on LuaManager. + MWBase::Environment::get().getLuaManager()->write(writer, listener); MWBase::Environment::get().getWorld()->write (writer, listener); MWBase::Environment::get().getScriptManager()->getGlobalScripts().write (writer, listener); MWBase::Environment::get().getWindowManager()->write(writer, listener); @@ -382,6 +388,7 @@ void MWState::StateManager::loadGame (const Character *character, const std::str throw std::runtime_error("This save file was created using a newer version of OpenMW and is thus not supported. Please upgrade to the newest OpenMW version to load this file."); std::map contentFileMap = buildContentFileIndexMap (reader); + MWBase::Environment::get().getLuaManager()->setContentFileMapping(contentFileMap); Loading::Listener& listener = *MWBase::Environment::get().getWindowManager()->getLoadingScreen(); @@ -480,6 +487,10 @@ void MWState::StateManager::loadGame (const Character *character, const std::str MWBase::Environment::get().getInputManager()->readRecord(reader, n.intval); break; + case ESM::REC_LUAM: + MWBase::Environment::get().getLuaManager()->readRecord(reader, n.intval); + break; + default: // ignore invalid records diff --git a/apps/openmw/mwworld/cellref.cpp b/apps/openmw/mwworld/cellref.cpp index 188a80ae14..5e91ddcc67 100644 --- a/apps/openmw/mwworld/cellref.cpp +++ b/apps/openmw/mwworld/cellref.cpp @@ -1,5 +1,8 @@ #include "cellref.hpp" +#include + +#include #include namespace MWWorld @@ -10,6 +13,26 @@ namespace MWWorld return mCellRef.mRefNum; } + const ESM::RefNum& CellRef::getOrAssignRefNum(ESM::RefNum& lastAssignedRefNum) + { + if (!mCellRef.mRefNum.isSet()) + { + // Generated RefNums have negative mContentFile + assert(lastAssignedRefNum.mContentFile < 0); + lastAssignedRefNum.mIndex++; + if (lastAssignedRefNum.mIndex == 0) // mIndex overflow, so mContentFile should be changed + { + if (lastAssignedRefNum.mContentFile > std::numeric_limits::min()) + lastAssignedRefNum.mContentFile--; + else + Log(Debug::Error) << "RefNum counter overflow in CellRef::getOrAssignRefNum"; + } + mCellRef.mRefNum = lastAssignedRefNum; + mChanged = true; + } + return mCellRef.mRefNum; + } + bool CellRef::hasContentFile() const { return mCellRef.mRefNum.hasContentFile(); diff --git a/apps/openmw/mwworld/cellref.hpp b/apps/openmw/mwworld/cellref.hpp index f9f6dbdda2..6a6ac69c57 100644 --- a/apps/openmw/mwworld/cellref.hpp +++ b/apps/openmw/mwworld/cellref.hpp @@ -25,6 +25,10 @@ namespace MWWorld // Note: Currently unused for items in containers const ESM::RefNum& getRefNum() const; + // Returns RefNum. + // If RefNum is not set, assigns a generated one and changes the "lastAssignedRefNum" counter. + const ESM::RefNum& getOrAssignRefNum(ESM::RefNum& lastAssignedRefNum); + // Set RefNum to its default state. void unsetRefNum(); diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp index 3aaf6061e3..53245be247 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -18,6 +18,7 @@ #include #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" @@ -195,6 +196,8 @@ namespace iter->mData.enable(); MWBase::Environment::get().getWorld()->disable(MWWorld::Ptr(&*iter, cellstore)); } + else + MWBase::Environment::get().getLuaManager()->registerObject(MWWorld::Ptr(&*iter, cellstore)); return; } @@ -206,6 +209,9 @@ namespace MWWorld::LiveCellRef ref (record); ref.load (state); collection.mList.push_back (ref); + + MWWorld::LiveCellRefBase* base = &collection.mList.back(); + MWBase::Environment::get().getLuaManager()->registerObject(MWWorld::Ptr(base, cellstore)); } } @@ -286,16 +292,7 @@ namespace MWWorld if (searchViaRefNum(object.getCellRef().getRefNum()).isEmpty()) throw std::runtime_error("moveTo: object is not in this cell"); - - // Objects with no refnum can't be handled correctly in the merging process that happens - // on a save/load, so do a simple copy & delete for these objects. - if (!object.getCellRef().getRefNum().hasContentFile()) - { - MWWorld::Ptr copied = object.getClass().copyToCell(object, *cellToMoveTo, object.getRefData().getCount()); - object.getRefData().setCount(0); - object.getRefData().setBaseNode(nullptr); - return copied; - } + MWBase::Environment::get().getLuaManager()->registerObject(MWWorld::Ptr(object.getBase(), cellToMoveTo)); MovedRefTracker::iterator found = mMovedHere.find(object.getBase()); if (found != mMovedHere.end()) @@ -615,7 +612,7 @@ namespace MWWorld mCell->restore (esm[index], i); ESM::CellRef ref; - ref.mRefNum.mContentFile = ESM::RefNum::RefNum_NoContentFile; + ref.mRefNum.unset(); // Get each reference in turn ESM::MovedCellRef cMRef; diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 864cb0d9d1..4b9bdf7426 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -34,7 +34,7 @@ namespace readers.resize(index + 1); cell.restore(readers[index], i); ESM::CellRef ref; - ref.mRefNum.mContentFile = ESM::RefNum::RefNum_NoContentFile; + ref.mRefNum.unset(); bool deleted = false; while(cell.getNextRef(readers[index], ref, deleted)) { diff --git a/apps/openmw/mwworld/livecellref.cpp b/apps/openmw/mwworld/livecellref.cpp index 9cf8a0fe04..4203e1ac55 100644 --- a/apps/openmw/mwworld/livecellref.cpp +++ b/apps/openmw/mwworld/livecellref.cpp @@ -5,6 +5,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" +#include "../mwbase/luamanager.hpp" #include "ptr.hpp" #include "class.hpp" @@ -52,6 +53,8 @@ void MWWorld::LiveCellRefBase::loadImp (const ESM::ObjectState& state) Log(Debug::Warning) << "Soul '" << mRef.getSoul() << "' not found, removing the soul from soul gem"; mRef.setSoul(std::string()); } + + MWBase::Environment::get().getLuaManager()->loadLocalScripts(ptr, state.mLuaScripts); } void MWWorld::LiveCellRefBase::saveImp (ESM::ObjectState& state) const @@ -61,6 +64,7 @@ void MWWorld::LiveCellRefBase::saveImp (ESM::ObjectState& state) const ConstPtr ptr (this); mData.write (state, mClass->getScript (ptr)); + MWBase::Environment::get().getLuaManager()->saveLocalScripts(Ptr(const_cast(this)), state.mLuaScripts); mClass->writeAdditionalState (ptr, state); } diff --git a/apps/openmw/mwworld/refdata.cpp b/apps/openmw/mwworld/refdata.cpp index 29c669caf9..7398aef77e 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::shared_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() @@ -130,6 +140,9 @@ namespace MWWorld {} } + RefData::RefData(RefData&& other) noexcept = default; + RefData& RefData::operator=(RefData&& other) noexcept = default; + void RefData::setBaseNode(SceneUtil::PositionAttitudeTransform *base) { mBaseNode = base; diff --git a/apps/openmw/mwworld/refdata.hpp b/apps/openmw/mwworld/refdata.hpp index e2f77d09e0..d90224b9bb 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. @@ -72,7 +78,7 @@ namespace MWWorld /// perform these operations). RefData (const RefData& refData); - RefData (RefData&& other) noexcept = default; + RefData (RefData&& other) noexcept; ~RefData(); @@ -81,7 +87,7 @@ namespace MWWorld /// perform this operations). RefData& operator= (const RefData& refData); - RefData& operator= (RefData&& other) noexcept = default; + RefData& operator= (RefData&& other) noexcept; /// Return base node (can be a null pointer). SceneUtil::PositionAttitudeTransform* getBaseNode(); @@ -96,6 +102,9 @@ namespace MWWorld void setLocals (const ESM::Script& script); + MWLua::LocalScripts* getLuaScripts() { return mLuaScripts.get(); } + void setLuaScripts(std::shared_ptr&&); + void setCount (int count); ///< Set object count (an object pile is a simple object with a count >1). /// diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index d3b381d2af..7d3a6c7893 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -25,6 +25,7 @@ #include "../mwbase/soundmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwrender/renderingmanager.hpp" #include "../mwrender/landmanager.hpp" @@ -138,6 +139,8 @@ namespace if (!physics.getObject(ptr)) ptr.getClass().insertObject (ptr, model, rotation, physics); + + MWBase::Environment::get().getLuaManager()->objectAddedToScene(ptr); } void addObject(const MWWorld::Ptr& ptr, const MWPhysics::PhysicsSystem& physics, DetourNavigator::Navigator& navigator) @@ -385,6 +388,7 @@ namespace MWWorld mRendering.removeActorPath(ptr); mPhysics->remove(ptr); } + MWBase::Environment::get().getLuaManager()->objectRemovedFromScene(ptr); } const auto cellX = cell->getCell()->getGridX(); @@ -1006,6 +1010,7 @@ namespace MWWorld { MWBase::Environment::get().getMechanicsManager()->remove (ptr); MWBase::Environment::get().getSoundManager()->stopSound3D (ptr); + MWBase::Environment::get().getLuaManager()->objectRemovedFromScene(ptr); const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); if (const auto object = mPhysics->getObject(ptr)) { diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 14c7716f2e..8fea737d8d 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -36,6 +36,7 @@ #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/scriptmanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" @@ -569,6 +570,11 @@ namespace MWWorld return getInterior (id.mWorldspace); } + bool World::isCellActive(CellStore* cell) const + { + return mWorldScene->getActiveCells().count(cell) > 0; + } + void World::testExteriorCells() { mWorldScene->testExteriorCells(); @@ -812,7 +818,8 @@ namespace MWWorld void World::enable (const Ptr& reference) { - // enable is a no-op for items in containers + MWBase::Environment::get().getLuaManager()->registerObject(reference); + if (!reference.isInCell()) return; @@ -863,6 +870,7 @@ namespace MWWorld if (reference == getPlayerPtr()) throw std::runtime_error("can not disable player object"); + MWBase::Environment::get().getLuaManager()->deregisterObject(reference); reference.getRefData().disable(); if (reference.getCellRef().getRefNum().hasContentFile()) @@ -2020,7 +2028,7 @@ namespace MWWorld rayToObject = mRendering->castCameraToViewportRay(0.5f, 0.5f, maxDistance, ignorePlayer); facedObject = rayToObject.mHitObject; - if (facedObject.isEmpty() && rayToObject.mHitRefnum.hasContentFile()) + if (facedObject.isEmpty() && rayToObject.mHitRefnum.isSet()) { for (CellStore* cellstore : mWorldScene->getActiveCells()) { @@ -2483,12 +2491,14 @@ namespace MWWorld mNavigator->removeAgent(getPathfindingHalfExtents(getPlayerConstPtr())); mPhysics->remove(getPlayerPtr()); mRendering->removePlayer(getPlayerPtr()); + MWBase::Environment::get().getLuaManager()->objectRemovedFromScene(getPlayerPtr()); mPlayer->set(player); } Ptr ptr = mPlayer->getPlayer(); mRendering->setupPlayer(ptr); + MWBase::Environment::get().getLuaManager()->setupPlayer(ptr); } void World::renderPlayer() diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 2ed69aabdf..87cd14dd5c 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -216,6 +216,8 @@ namespace MWWorld CellStore *getCell (const ESM::CellId& id) override; + bool isCellActive(CellStore* cell) const override; + void testExteriorCells() override; void testInteriorCells() override; diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index d78cb69553..7800efac3d 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -15,6 +15,13 @@ if (GTEST_FOUND AND GMOCK_FOUND) esm/test_fixed_string.cpp esm/variant.cpp + lua/test_lua.cpp + lua/test_scriptscontainer.cpp + lua/test_utilpackage.cpp + lua/test_serialization.cpp + lua/test_querypackage.cpp + lua/test_omwscriptsparser.cpp + misc/test_stringops.cpp misc/test_endianness.cpp @@ -39,7 +46,7 @@ if (GTEST_FOUND AND GMOCK_FOUND) openmw_add_executable(openmw_test_suite openmw_test_suite.cpp ${UNITTEST_SRC_FILES}) - target_link_libraries(openmw_test_suite ${GMOCK_LIBRARIES} components) + target_link_libraries(openmw_test_suite ${GMOCK_LIBRARIES} components ${LUA_LIBRARIES}) # Fix for not visible pthreads functions for linker with glibc 2.15 if (UNIX AND NOT APPLE) target_link_libraries(openmw_test_suite ${CMAKE_THREAD_LIBS_INIT}) diff --git a/apps/openmw_test_suite/lua/test_lua.cpp b/apps/openmw_test_suite/lua/test_lua.cpp new file mode 100644 index 0000000000..69a326060c --- /dev/null +++ b/apps/openmw_test_suite/lua/test_lua.cpp @@ -0,0 +1,167 @@ +#include "gmock/gmock.h" +#include + +#include + +#include "testing_util.hpp" + +namespace +{ + using namespace testing; + + TestFile counterFile(R"X( +x = 42 +return { + get = function() return x end, + inc = function(v) x = x + v end +} +)X"); + + TestFile invalidScriptFile("Invalid script"); + + TestFile testsFile(R"X( +return { + -- should work + sin = function(x) return math.sin(x) end, + requireMathSin = function(x) return require('math').sin(x) end, + useCounter = function() + local counter = require('aaa.counter') + counter.inc(1) + return counter.get() + end, + callRawset = function() + t = {a = 1, b = 2} + rawset(t, 'b', 3) + return t.b + end, + print = print, + + -- should throw an error + incorrectRequire = function() require('counter') end, + modifySystemLib = function() math.sin = 5 end, + rawsetSystemLib = function() rawset(math, 'sin', 5) end, + callLoadstring = function() loadstring('print(1)') end, + setSqr = function() require('sqrlib').sqr = math.sin end, + setOmwName = function() require('openmw').name = 'abc' end, + + -- should work if API is registered + sqr = function(x) return require('sqrlib').sqr(x) end, + apiName = function() return require('test.api').name end +} +)X"); + + struct LuaStateTest : Test + { + std::unique_ptr mVFS = createTestVFS({ + {"aaa/counter.lua", &counterFile}, + {"bbb/tests.lua", &testsFile}, + {"invalid.lua", &invalidScriptFile} + }); + + LuaUtil::LuaState mLua{mVFS.get()}; + }; + + TEST_F(LuaStateTest, Sandbox) + { + sol::table script1 = mLua.runInNewSandbox("aaa/counter.lua"); + + EXPECT_EQ(LuaUtil::call(script1["get"]).get(), 42); + LuaUtil::call(script1["inc"], 3); + EXPECT_EQ(LuaUtil::call(script1["get"]).get(), 45); + + sol::table script2 = mLua.runInNewSandbox("aaa/counter.lua"); + EXPECT_EQ(LuaUtil::call(script2["get"]).get(), 42); + LuaUtil::call(script2["inc"], 1); + EXPECT_EQ(LuaUtil::call(script2["get"]).get(), 43); + + EXPECT_EQ(LuaUtil::call(script1["get"]).get(), 45); + } + + TEST_F(LuaStateTest, ErrorHandling) + { + EXPECT_ERROR(mLua.runInNewSandbox("invalid.lua"), "[string \"invalid.lua\"]:1:"); + } + + TEST_F(LuaStateTest, CustomRequire) + { + sol::table script = mLua.runInNewSandbox("bbb/tests.lua"); + + EXPECT_FLOAT_EQ(LuaUtil::call(script["sin"], 1).get(), + -LuaUtil::call(script["requireMathSin"], -1).get()); + + EXPECT_EQ(LuaUtil::call(script["useCounter"]).get(), 43); + EXPECT_EQ(LuaUtil::call(script["useCounter"]).get(), 44); + { + sol::table script2 = mLua.runInNewSandbox("bbb/tests.lua"); + EXPECT_EQ(LuaUtil::call(script2["useCounter"]).get(), 43); + } + EXPECT_EQ(LuaUtil::call(script["useCounter"]).get(), 45); + + EXPECT_ERROR(LuaUtil::call(script["incorrectRequire"]), "Resource 'counter.lua' not found"); + } + + TEST_F(LuaStateTest, ReadOnly) + { + sol::table script = mLua.runInNewSandbox("bbb/tests.lua"); + + // rawset itself is allowed + EXPECT_EQ(LuaUtil::call(script["callRawset"]).get(), 3); + + // but read-only object can not be modified even with rawset + EXPECT_ERROR(LuaUtil::call(script["rawsetSystemLib"]), "bad argument #1 to 'rawset' (table expected, got userdata)"); + EXPECT_ERROR(LuaUtil::call(script["modifySystemLib"]), "a userdata value"); + + EXPECT_EQ(mLua.getMutableFromReadOnly(mLua.makeReadOnly(script)), script); + } + + TEST_F(LuaStateTest, Print) + { + { + sol::table script = mLua.runInNewSandbox("bbb/tests.lua"); + testing::internal::CaptureStdout(); + LuaUtil::call(script["print"], 1, 2, 3); + std::string output = testing::internal::GetCapturedStdout(); + EXPECT_EQ(output, "[bbb/tests.lua]:\t1\t2\t3\n"); + } + { + sol::table script = mLua.runInNewSandbox("bbb/tests.lua", "prefix"); + testing::internal::CaptureStdout(); + LuaUtil::call(script["print"]); // print with no arguments + std::string output = testing::internal::GetCapturedStdout(); + EXPECT_EQ(output, "prefix[bbb/tests.lua]:\n"); + } + } + + TEST_F(LuaStateTest, UnsafeFunction) + { + sol::table script = mLua.runInNewSandbox("bbb/tests.lua"); + EXPECT_ERROR(LuaUtil::call(script["callLoadstring"]), "a nil value"); + } + + TEST_F(LuaStateTest, ProvideAPI) + { + LuaUtil::LuaState lua(mVFS.get()); + + sol::table api1 = lua.makeReadOnly(lua.sol().create_table_with("name", "api1")); + sol::table api2 = lua.makeReadOnly(lua.sol().create_table_with("name", "api2")); + + sol::table script1 = lua.runInNewSandbox("bbb/tests.lua", "", {{"test.api", api1}}); + + lua.addCommonPackage( + "sqrlib", lua.sol().create_table_with("sqr", [](int x) { return x * x; })); + + sol::table script2 = lua.runInNewSandbox("bbb/tests.lua", "", {{"test.api", api2}}); + + EXPECT_ERROR(LuaUtil::call(script1["sqr"], 3), "Resource 'sqrlib.lua' not found"); + EXPECT_EQ(LuaUtil::call(script2["sqr"], 3).get(), 9); + + EXPECT_EQ(LuaUtil::call(script1["apiName"]).get(), "api1"); + EXPECT_EQ(LuaUtil::call(script2["apiName"]).get(), "api2"); + } + + TEST_F(LuaStateTest, GetLuaVersion) + { + EXPECT_THAT(LuaUtil::getLuaVersion(), HasSubstr("Lua")); + } + +} diff --git a/apps/openmw_test_suite/lua/test_omwscriptsparser.cpp b/apps/openmw_test_suite/lua/test_omwscriptsparser.cpp new file mode 100644 index 0000000000..b1526ef9b6 --- /dev/null +++ b/apps/openmw_test_suite/lua/test_omwscriptsparser.cpp @@ -0,0 +1,59 @@ +#include "gmock/gmock.h" +#include + +#include + +#include "testing_util.hpp" + +namespace +{ + using namespace testing; + + TestFile file1( + "#comment.lua\n" + "\n" + "script1.lua\n" + "some mod/Some Script.lua" + ); + TestFile file2( + "#comment.lua\r\n" + "\r\n" + "script2.lua\r\n" + "some other mod/Some Script.lua\r" + ); + TestFile emptyFile(""); + TestFile invalidFile("Invalid file"); + + struct OMWScriptsParserTest : Test + { + std::unique_ptr mVFS = createTestVFS({ + {"file1.omwscripts", &file1}, + {"file2.omwscripts", &file2}, + {"empty.omwscripts", &emptyFile}, + {"invalid.lua", &file1}, + {"invalid.omwscripts", &invalidFile}, + }); + }; + + TEST_F(OMWScriptsParserTest, Basic) + { + internal::CaptureStdout(); + std::vector res = LuaUtil::parseOMWScriptsFiles( + mVFS.get(), {"file2.omwscripts", "empty.omwscripts", "file1.omwscripts"}); + EXPECT_EQ(internal::GetCapturedStdout(), ""); + EXPECT_THAT(res, ElementsAre("script2.lua", "some other mod/Some Script.lua", + "script1.lua", "some mod/Some Script.lua")); + } + + TEST_F(OMWScriptsParserTest, InvalidFiles) + { + internal::CaptureStdout(); + std::vector res = LuaUtil::parseOMWScriptsFiles( + mVFS.get(), {"invalid.lua", "invalid.omwscripts"}); + EXPECT_EQ(internal::GetCapturedStdout(), + "Script list should have suffix '.omwscripts', got: 'invalid.lua'\n" + "Lua script should have suffix '.lua', got: 'Invalid file'\n"); + EXPECT_THAT(res, ElementsAre()); + } + +} diff --git a/apps/openmw_test_suite/lua/test_querypackage.cpp b/apps/openmw_test_suite/lua/test_querypackage.cpp new file mode 100644 index 0000000000..aeaf992db0 --- /dev/null +++ b/apps/openmw_test_suite/lua/test_querypackage.cpp @@ -0,0 +1,29 @@ +#include "gmock/gmock.h" +#include + +#include + +namespace +{ + using namespace testing; + + TEST(LuaQueryPackageTest, basic) + { + sol::state lua; + lua.open_libraries(sol::lib::base, sol::lib::string); + Queries::registerQueryBindings(lua); + lua["query"] = Queries::Query("test"); + lua["fieldX"] = Queries::Field({ "x" }, typeid(std::string)); + lua["fieldY"] = Queries::Field({ "y" }, typeid(int)); + lua.safe_script("t = query:where(fieldX:eq('abc') + fieldX:like('%abcd%'))"); + lua.safe_script("t = t:where(fieldY:gt(5))"); + lua.safe_script("t = t:orderBy(fieldX)"); + lua.safe_script("t = t:orderByDesc(fieldY)"); + lua.safe_script("t = t:groupBy(fieldY)"); + lua.safe_script("t = t:limit(10):offset(5)"); + EXPECT_EQ( + lua.safe_script("return tostring(t)").get(), + "SELECT test WHERE ((x == \"abc\") OR (x LIKE \"%abcd%\")) AND (y > 5) ORDER BY x, y DESC GROUP BY y LIMIT 10 OFFSET 5"); + } +} + diff --git a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp new file mode 100644 index 0000000000..8f05138782 --- /dev/null +++ b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp @@ -0,0 +1,376 @@ +#include "gmock/gmock.h" +#include + +#include + +#include +#include + +#include "testing_util.hpp" + +namespace +{ + using namespace testing; + + TestFile invalidScript("not a script"); + TestFile incorrectScript("return { incorrectSection = {}, engineHandlers = { incorrectHandler = function() end } }"); + TestFile emptyScript(""); + + TestFile testScript(R"X( +return { + engineHandlers = { onUpdate = function(dt) print(' update ' .. tostring(dt)) end }, + eventHandlers = { + Event1 = function(eventData) print(' event1 ' .. tostring(eventData.x)) end, + Event2 = function(eventData) print(' event2 ' .. tostring(eventData.x)) end, + Print = function() print('print') end + } +} +)X"); + + TestFile stopEventScript(R"X( +return { + eventHandlers = { + Event1 = function(eventData) + print(' event1 ' .. tostring(eventData.x)) + return eventData.x >= 1 + end + } +} +)X"); + + TestFile loadSaveScript(R"X( +x = 0 +y = 0 +return { + engineHandlers = { + onSave = function(state) + return {x = x, y = y} + end, + onLoad = function(state) + x, y = state.x, state.y + end + }, + eventHandlers = { + Set = function(eventData) + eventData.n = eventData.n - 1 + if eventData.n == 0 then + x, y = eventData.x, eventData.y + end + end, + Print = function() + print(x, y) + end + } +} +)X"); + + TestFile interfaceScript(R"X( +return { + interfaceName = "TestInterface", + interface = { + fn = function(x) print('FN', x) end, + value = 3.5 + }, +} +)X"); + + TestFile overrideInterfaceScript(R"X( +local old = require('openmw.interfaces').TestInterface +return { + interfaceName = "TestInterface", + interface = { + fn = function(x) + print('NEW FN', x) + old.fn(x) + end, + value = old.value + 1 + }, +} +)X"); + + TestFile useInterfaceScript(R"X( +local interfaces = require('openmw.interfaces') +return { + engineHandlers = { + onUpdate = function() + interfaces.TestInterface.fn(interfaces.TestInterface.value) + end, + }, +} +)X"); + + struct LuaScriptsContainerTest : Test + { + std::unique_ptr mVFS = createTestVFS({ + {"invalid.lua", &invalidScript}, + {"incorrect.lua", &incorrectScript}, + {"empty.lua", &emptyScript}, + {"test1.lua", &testScript}, + {"test2.lua", &testScript}, + {"stopEvent.lua", &stopEventScript}, + {"loadSave1.lua", &loadSaveScript}, + {"loadSave2.lua", &loadSaveScript}, + {"testInterface.lua", &interfaceScript}, + {"overrideInterface.lua", &overrideInterfaceScript}, + {"useInterface.lua", &useInterfaceScript}, + }); + + LuaUtil::LuaState mLua{mVFS.get()}; + }; + + TEST_F(LuaScriptsContainerTest, VerifyStructure) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + { + testing::internal::CaptureStdout(); + EXPECT_FALSE(scripts.addNewScript("invalid.lua")); + std::string output = testing::internal::GetCapturedStdout(); + EXPECT_THAT(output, HasSubstr("Can't start Test[invalid.lua]")); + } + { + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.addNewScript("incorrect.lua")); + std::string output = testing::internal::GetCapturedStdout(); + EXPECT_THAT(output, HasSubstr("Not supported handler 'incorrectHandler' in Test[incorrect.lua]")); + EXPECT_THAT(output, HasSubstr("Not supported section 'incorrectSection' in Test[incorrect.lua]")); + } + { + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.addNewScript("empty.lua")); + EXPECT_FALSE(scripts.addNewScript("empty.lua")); // already present + EXPECT_EQ(internal::GetCapturedStdout(), ""); + } + } + + TEST_F(LuaScriptsContainerTest, CallHandler) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.addNewScript("test1.lua")); + EXPECT_TRUE(scripts.addNewScript("stopEvent.lua")); + EXPECT_TRUE(scripts.addNewScript("test2.lua")); + scripts.update(1.5f); + EXPECT_EQ(internal::GetCapturedStdout(), "Test[test1.lua]:\t update 1.5\n" + "Test[test2.lua]:\t update 1.5\n"); + } + + TEST_F(LuaScriptsContainerTest, CallEvent) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + EXPECT_TRUE(scripts.addNewScript("test1.lua")); + EXPECT_TRUE(scripts.addNewScript("stopEvent.lua")); + EXPECT_TRUE(scripts.addNewScript("test2.lua")); + + std::string X0 = LuaUtil::serialize(mLua.sol().create_table_with("x", 0.5)); + std::string X1 = LuaUtil::serialize(mLua.sol().create_table_with("x", 1.5)); + + { + 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"); + } + { + testing::internal::CaptureStdout(); + scripts.receiveEvent("Event1", X1); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test2.lua]:\t event1 1.5\n" + "Test[stopEvent.lua]:\t event1 1.5\n" + "Test[test1.lua]:\t event1 1.5\n"); + } + { + testing::internal::CaptureStdout(); + scripts.receiveEvent("Event2", X1); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test2.lua]:\t event2 1.5\n" + "Test[test1.lua]:\t event2 1.5\n"); + } + { + testing::internal::CaptureStdout(); + scripts.receiveEvent("Event1", X0); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test2.lua]:\t event1 0.5\n" + "Test[stopEvent.lua]:\t event1 0.5\n"); + } + { + testing::internal::CaptureStdout(); + scripts.receiveEvent("Event2", X0); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test2.lua]:\t event2 0.5\n" + "Test[test1.lua]:\t event2 0.5\n"); + } + } + + TEST_F(LuaScriptsContainerTest, RemoveScript) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + EXPECT_TRUE(scripts.addNewScript("test1.lua")); + EXPECT_TRUE(scripts.addNewScript("stopEvent.lua")); + EXPECT_TRUE(scripts.addNewScript("test2.lua")); + std::string X = LuaUtil::serialize(mLua.sol().create_table_with("x", 0.5)); + + { + testing::internal::CaptureStdout(); + scripts.update(1.5f); + scripts.receiveEvent("Event1", X); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test1.lua]:\t update 1.5\n" + "Test[test2.lua]:\t update 1.5\n" + "Test[test2.lua]:\t event1 0.5\n" + "Test[stopEvent.lua]:\t event1 0.5\n"); + } + { + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.removeScript("stopEvent.lua")); + EXPECT_FALSE(scripts.removeScript("stopEvent.lua")); // already removed + scripts.update(1.5f); + scripts.receiveEvent("Event1", X); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test1.lua]:\t update 1.5\n" + "Test[test2.lua]:\t update 1.5\n" + "Test[test2.lua]:\t event1 0.5\n" + "Test[test1.lua]:\t event1 0.5\n"); + } + { + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.removeScript("test1.lua")); + scripts.update(1.5f); + scripts.receiveEvent("Event1", X); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test2.lua]:\t update 1.5\n" + "Test[test2.lua]:\t event1 0.5\n"); + } + } + + TEST_F(LuaScriptsContainerTest, Interface) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.addNewScript("testInterface.lua")); + EXPECT_TRUE(scripts.addNewScript("overrideInterface.lua")); + EXPECT_TRUE(scripts.addNewScript("useInterface.lua")); + scripts.update(1.5f); + EXPECT_TRUE(scripts.removeScript("overrideInterface.lua")); + scripts.update(1.5f); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[overrideInterface.lua]:\tNEW FN\t4.5\n" + "Test[testInterface.lua]:\tFN\t4.5\n" + "Test[testInterface.lua]:\tFN\t3.5\n"); + } + + TEST_F(LuaScriptsContainerTest, LoadSave) + { + LuaUtil::ScriptsContainer scripts1(&mLua, "Test"); + LuaUtil::ScriptsContainer scripts2(&mLua, "Test"); + LuaUtil::ScriptsContainer scripts3(&mLua, "Test"); + + EXPECT_TRUE(scripts1.addNewScript("loadSave1.lua")); + EXPECT_TRUE(scripts1.addNewScript("test1.lua")); + EXPECT_TRUE(scripts1.addNewScript("loadSave2.lua")); + + EXPECT_TRUE(scripts3.addNewScript("test2.lua")); + EXPECT_TRUE(scripts3.addNewScript("loadSave2.lua")); + + scripts1.receiveEvent("Set", LuaUtil::serialize(mLua.sol().create_table_with( + "n", 1, + "x", 0.5, + "y", 3.5))); + scripts1.receiveEvent("Set", LuaUtil::serialize(mLua.sol().create_table_with( + "n", 2, + "x", 2.5, + "y", 1.5))); + + ESM::LuaScripts data; + scripts1.save(data); + scripts2.load(data, true); + scripts3.load(data, false); + + { + testing::internal::CaptureStdout(); + scripts2.receiveEvent("Print", ""); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[loadSave2.lua]:\t0.5\t3.5\n" + "Test[test1.lua]:\tprint\n" + "Test[loadSave1.lua]:\t2.5\t1.5\n"); + } + { + testing::internal::CaptureStdout(); + scripts3.receiveEvent("Print", ""); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[loadSave2.lua]:\t0.5\t3.5\n" + "Test[test2.lua]:\tprint\n"); + } + } + + TEST_F(LuaScriptsContainerTest, Timers) + { + using TimeUnit = LuaUtil::ScriptsContainer::TimeUnit; + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + EXPECT_TRUE(scripts.addNewScript("test1.lua")); + EXPECT_TRUE(scripts.addNewScript("test2.lua")); + + int counter1 = 0, counter2 = 0, counter3 = 0, counter4 = 0; + sol::function fn1 = sol::make_object(mLua.sol(), [&]() { counter1++; }); + sol::function fn2 = sol::make_object(mLua.sol(), [&]() { counter2++; }); + sol::function fn3 = sol::make_object(mLua.sol(), [&](int d) { counter3 += d; }); + sol::function fn4 = sol::make_object(mLua.sol(), [&](int d) { counter4 += d; }); + + scripts.registerTimerCallback("test1.lua", "A", fn3); + scripts.registerTimerCallback("test1.lua", "B", fn4); + scripts.registerTimerCallback("test2.lua", "B", fn3); + scripts.registerTimerCallback("test2.lua", "A", fn4); + + scripts.processTimers(1, 2); + + scripts.setupSerializableTimer(TimeUnit::SECONDS, 10, "test1.lua", "B", sol::make_object(mLua.sol(), 3)); + scripts.setupSerializableTimer(TimeUnit::HOURS, 10, "test2.lua", "B", sol::make_object(mLua.sol(), 4)); + scripts.setupSerializableTimer(TimeUnit::SECONDS, 5, "test1.lua", "A", sol::make_object(mLua.sol(), 1)); + scripts.setupSerializableTimer(TimeUnit::HOURS, 5, "test2.lua", "A", sol::make_object(mLua.sol(), 2)); + scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, "test1.lua", "A", sol::make_object(mLua.sol(), 10)); + scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, "test1.lua", "B", sol::make_object(mLua.sol(), 20)); + + scripts.setupUnsavableTimer(TimeUnit::SECONDS, 10, "test2.lua", fn2); + scripts.setupUnsavableTimer(TimeUnit::HOURS, 10, "test1.lua", fn2); + scripts.setupUnsavableTimer(TimeUnit::SECONDS, 5, "test2.lua", fn1); + scripts.setupUnsavableTimer(TimeUnit::HOURS, 5, "test1.lua", fn1); + scripts.setupUnsavableTimer(TimeUnit::SECONDS, 15, "test2.lua", fn1); + + EXPECT_EQ(counter1, 0); + EXPECT_EQ(counter3, 0); + + scripts.processTimers(6, 4); + + EXPECT_EQ(counter1, 1); + EXPECT_EQ(counter3, 1); + EXPECT_EQ(counter4, 0); + + scripts.processTimers(6, 8); + + EXPECT_EQ(counter1, 2); + EXPECT_EQ(counter2, 0); + EXPECT_EQ(counter3, 1); + EXPECT_EQ(counter4, 2); + + scripts.processTimers(11, 12); + + EXPECT_EQ(counter1, 2); + EXPECT_EQ(counter2, 2); + EXPECT_EQ(counter3, 5); + EXPECT_EQ(counter4, 5); + + ESM::LuaScripts data; + scripts.save(data); + scripts.load(data, true); + scripts.registerTimerCallback("test1.lua", "B", fn4); + + testing::internal::CaptureStdout(); + scripts.processTimers(20, 20); + EXPECT_EQ(internal::GetCapturedStdout(), "Test[test1.lua] callTimer failed: Callback 'A' doesn't exist\n"); + + EXPECT_EQ(counter1, 2); + EXPECT_EQ(counter2, 2); + EXPECT_EQ(counter3, 5); + EXPECT_EQ(counter4, 25); + } + +} diff --git a/apps/openmw_test_suite/lua/test_serialization.cpp b/apps/openmw_test_suite/lua/test_serialization.cpp new file mode 100644 index 0000000000..d3c01f6298 --- /dev/null +++ b/apps/openmw_test_suite/lua/test_serialization.cpp @@ -0,0 +1,207 @@ +#include "gmock/gmock.h" +#include + +#include +#include + +#include + +#include + +#include "testing_util.hpp" + +namespace +{ + using namespace testing; + + TEST(LuaSerializationTest, Nil) + { + sol::state lua; + EXPECT_EQ(LuaUtil::serialize(sol::nil), ""); + EXPECT_EQ(LuaUtil::deserialize(lua, ""), sol::nil); + } + + TEST(LuaSerializationTest, Number) + { + sol::state lua; + std::string serialized = LuaUtil::serialize(sol::make_object(lua, 3.14)); + EXPECT_EQ(serialized.size(), 10); // version, type, 8 bytes value + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_FLOAT_EQ(value.as(), 3.14); + } + + TEST(LuaSerializationTest, Boolean) + { + sol::state lua; + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, true)); + EXPECT_EQ(serialized.size(), 3); // version, type, 1 byte value + sol::object value = LuaUtil::deserialize(lua, serialized); + EXPECT_FALSE(value.is()); + ASSERT_TRUE(value.is()); + EXPECT_TRUE(value.as()); + } + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, false)); + EXPECT_EQ(serialized.size(), 3); // version, type, 1 byte value + sol::object value = LuaUtil::deserialize(lua, serialized); + EXPECT_FALSE(value.is()); + ASSERT_TRUE(value.is()); + EXPECT_FALSE(value.as()); + } + } + + TEST(LuaSerializationTest, String) + { + sol::state lua; + std::string_view emptyString = ""; + std::string_view shortString = "abc"; + std::string_view longString = "It is a string with more than 32 characters..........................."; + + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, emptyString)); + EXPECT_EQ(serialized.size(), 2); // version, type + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_EQ(value.as(), emptyString); + } + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, shortString)); + EXPECT_EQ(serialized.size(), 2 + shortString.size()); // version, type, str data + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_EQ(value.as(), shortString); + } + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, longString)); + EXPECT_EQ(serialized.size(), 6 + longString.size()); // version, type, size, str data + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_EQ(value.as(), longString); + } + } + + TEST(LuaSerializationTest, Vector) + { + sol::state lua; + osg::Vec2f vec2(1, 2); + osg::Vec3f vec3(1, 2, 3); + + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, vec2)); + EXPECT_EQ(serialized.size(), 10); // version, type, 2x float + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_EQ(value.as(), vec2); + } + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, vec3)); + EXPECT_EQ(serialized.size(), 14); // version, type, 3x float + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_EQ(value.as(), vec3); + } + } + + TEST(LuaSerializationTest, Table) + { + sol::state lua; + sol::table table(lua, sol::create); + table["aa"] = 1; + table["ab"] = true; + table["nested"] = sol::table(lua, sol::create); + table["nested"]["aa"] = 2; + table["nested"]["bb"] = "something"; + table["nested"][5] = -0.5; + table["nested_empty"] = sol::table(lua, sol::create); + table[1] = osg::Vec2f(1, 2); + table[2] = osg::Vec2f(2, 1); + + std::string serialized = LuaUtil::serialize(table); + EXPECT_EQ(serialized.size(), 123); + sol::table res_table = LuaUtil::deserialize(lua, serialized); + + EXPECT_EQ(res_table.get("aa"), 1); + EXPECT_EQ(res_table.get("ab"), true); + EXPECT_EQ(res_table.get("nested").get("aa"), 2); + EXPECT_EQ(res_table.get("nested").get("bb"), "something"); + EXPECT_FLOAT_EQ(res_table.get("nested").get(5), -0.5); + EXPECT_EQ(res_table.get(1), osg::Vec2f(1, 2)); + EXPECT_EQ(res_table.get(2), osg::Vec2f(2, 1)); + } + + struct TestStruct1 { double a, b; }; + struct TestStruct2 { int a, b; }; + + class TestSerializer final : public LuaUtil::UserdataSerializer + { + bool serialize(LuaUtil::BinaryData& out, const sol::userdata& data) const override + { + if (data.is()) + { + TestStruct1 t = data.as(); + t.a = Misc::toLittleEndian(t.a); + t.b = Misc::toLittleEndian(t.b); + append(out, "ts1", &t, sizeof(t)); + return true; + } + if (data.is()) + { + TestStruct2 t = data.as(); + t.a = Misc::toLittleEndian(t.a); + t.b = Misc::toLittleEndian(t.b); + append(out, "test_struct2", &t, sizeof(t)); + return true; + } + return false; + } + + bool deserialize(std::string_view typeName, std::string_view binaryData, sol::state& lua) const override + { + if (typeName == "ts1") + { + if (sizeof(TestStruct1) != binaryData.size()) + throw std::runtime_error("Incorrect binaryData.size() for TestStruct1: " + std::to_string(binaryData.size())); + TestStruct1 t = *reinterpret_cast(binaryData.data()); + t.a = Misc::fromLittleEndian(t.a); + t.b = Misc::fromLittleEndian(t.b); + sol::stack::push(lua, t); + return true; + } + if (typeName == "test_struct2") + { + if (sizeof(TestStruct2) != binaryData.size()) + throw std::runtime_error("Incorrect binaryData.size() for TestStruct2: " + std::to_string(binaryData.size())); + TestStruct2 t = *reinterpret_cast(binaryData.data()); + t.a = Misc::fromLittleEndian(t.a); + t.b = Misc::fromLittleEndian(t.b); + sol::stack::push(lua, t); + return true; + } + return false; + } + }; + + TEST(LuaSerializationTest, UserdataSerializer) + { + sol::state lua; + sol::table table(lua, sol::create); + table["x"] = TestStruct1{1.5, 2.5}; + table["y"] = TestStruct2{4, 3}; + TestSerializer serializer; + + EXPECT_ERROR(LuaUtil::serialize(table), "Unknown userdata"); + std::string serialized = LuaUtil::serialize(table, &serializer); + EXPECT_ERROR(LuaUtil::deserialize(lua, serialized), "Unknown type:"); + sol::table res = LuaUtil::deserialize(lua, serialized, &serializer); + + TestStruct1 rx = res.get("x"); + TestStruct2 ry = res.get("y"); + EXPECT_EQ(rx.a, 1.5); + EXPECT_EQ(rx.b, 2.5); + EXPECT_EQ(ry.a, 4); + EXPECT_EQ(ry.b, 3); + } + +} diff --git a/apps/openmw_test_suite/lua/test_utilpackage.cpp b/apps/openmw_test_suite/lua/test_utilpackage.cpp new file mode 100644 index 0000000000..afd9fa2d3c --- /dev/null +++ b/apps/openmw_test_suite/lua/test_utilpackage.cpp @@ -0,0 +1,80 @@ +#include "gmock/gmock.h" +#include + +#include + +#include "testing_util.hpp" + +namespace +{ + using namespace testing; + + TEST(LuaUtilPackageTest, Vector2) + { + sol::state lua; + lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); + lua["util"] = LuaUtil::initUtilPackage(lua); + lua.safe_script("v = util.vector2(3, 4)"); + EXPECT_FLOAT_EQ(lua.safe_script("return v.x").get(), 3); + EXPECT_FLOAT_EQ(lua.safe_script("return v.y").get(), 4); + EXPECT_EQ(lua.safe_script("return tostring(v)").get(), "(3, 4)"); + EXPECT_FLOAT_EQ(lua.safe_script("return v:length()").get(), 5); + EXPECT_FLOAT_EQ(lua.safe_script("return v:length2()").get(), 25); + EXPECT_FALSE(lua.safe_script("return util.vector2(1, 2) == util.vector2(1, 3)").get()); + EXPECT_TRUE(lua.safe_script("return util.vector2(1, 2) + util.vector2(2, 5) == util.vector2(3, 7)").get()); + EXPECT_TRUE(lua.safe_script("return util.vector2(1, 2) - util.vector2(2, 5) == -util.vector2(1, 3)").get()); + EXPECT_TRUE(lua.safe_script("return util.vector2(1, 2) == util.vector2(2, 4) / 2").get()); + EXPECT_TRUE(lua.safe_script("return util.vector2(1, 2) * 2 == util.vector2(2, 4)").get()); + EXPECT_FLOAT_EQ(lua.safe_script("return util.vector2(3, 2) * v").get(), 17); + EXPECT_FLOAT_EQ(lua.safe_script("return util.vector2(3, 2):dot(v)").get(), 17); + EXPECT_ERROR(lua.safe_script("v2, len = v.normalize()"), "value is not a valid userdata"); // checks that it doesn't segfault + lua.safe_script("v2, len = v:normalize()"); + EXPECT_FLOAT_EQ(lua.safe_script("return len").get(), 5); + EXPECT_TRUE(lua.safe_script("return v2 == util.vector2(3/5, 4/5)").get()); + lua.safe_script("_, len = util.vector2(0, 0):normalize()"); + EXPECT_FLOAT_EQ(lua.safe_script("return len").get(), 0); + } + + TEST(LuaUtilPackageTest, Vector3) + { + sol::state lua; + lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); + lua["util"] = LuaUtil::initUtilPackage(lua); + lua.safe_script("v = util.vector3(5, 12, 13)"); + EXPECT_FLOAT_EQ(lua.safe_script("return v.x").get(), 5); + EXPECT_FLOAT_EQ(lua.safe_script("return v.y").get(), 12); + EXPECT_FLOAT_EQ(lua.safe_script("return v.z").get(), 13); + EXPECT_EQ(lua.safe_script("return tostring(v)").get(), "(5, 12, 13)"); + EXPECT_FLOAT_EQ(lua.safe_script("return util.vector3(4, 0, 3):length()").get(), 5); + EXPECT_FLOAT_EQ(lua.safe_script("return util.vector3(4, 0, 3):length2()").get(), 25); + EXPECT_FALSE(lua.safe_script("return util.vector3(1, 2, 3) == util.vector3(1, 3, 2)").get()); + EXPECT_TRUE(lua.safe_script("return util.vector3(1, 2, 3) + util.vector3(2, 5, 1) == util.vector3(3, 7, 4)").get()); + EXPECT_TRUE(lua.safe_script("return util.vector3(1, 2, 3) - util.vector3(2, 5, 1) == -util.vector3(1, 3, -2)").get()); + EXPECT_TRUE(lua.safe_script("return util.vector3(1, 2, 3) == util.vector3(2, 4, 6) / 2").get()); + EXPECT_TRUE(lua.safe_script("return util.vector3(1, 2, 3) * 2 == util.vector3(2, 4, 6)").get()); + EXPECT_FLOAT_EQ(lua.safe_script("return util.vector3(3, 2, 1) * v").get(), 5*3 + 12*2 + 13*1); + EXPECT_FLOAT_EQ(lua.safe_script("return util.vector3(3, 2, 1):dot(v)").get(), 5*3 + 12*2 + 13*1); + EXPECT_TRUE(lua.safe_script("return util.vector3(1, 0, 0) ^ util.vector3(0, 1, 0) == util.vector3(0, 0, 1)").get()); + EXPECT_ERROR(lua.safe_script("v2, len = util.vector3(3, 4, 0).normalize()"), "value is not a valid userdata"); + lua.safe_script("v2, len = util.vector3(3, 4, 0):normalize()"); + EXPECT_FLOAT_EQ(lua.safe_script("return len").get(), 5); + EXPECT_TRUE(lua.safe_script("return v2 == util.vector3(3/5, 4/5, 0)").get()); + lua.safe_script("_, len = util.vector3(0, 0, 0):normalize()"); + EXPECT_FLOAT_EQ(lua.safe_script("return len").get(), 0); + } + + TEST(LuaUtilPackageTest, UtilityFunctions) + { + sol::state lua; + lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); + lua["util"] = LuaUtil::initUtilPackage(lua); + lua.safe_script("v = util.vector2(1, 0):rotate(math.rad(120))"); + EXPECT_FLOAT_EQ(lua.safe_script("return v.x").get(), -0.5); + EXPECT_FLOAT_EQ(lua.safe_script("return v.y").get(), 0.86602539); + EXPECT_FLOAT_EQ(lua.safe_script("return util.normalizeAngle(math.pi * 10 + 0.1)").get(), 0.1); + EXPECT_FLOAT_EQ(lua.safe_script("return util.clamp(0.1, 0, 1.5)").get(), 0.1); + EXPECT_FLOAT_EQ(lua.safe_script("return util.clamp(-0.1, 0, 1.5)").get(), 0); + EXPECT_FLOAT_EQ(lua.safe_script("return util.clamp(2.1, 0, 1.5)").get(), 1.5); + } + +} diff --git a/apps/openmw_test_suite/lua/testing_util.hpp b/apps/openmw_test_suite/lua/testing_util.hpp new file mode 100644 index 0000000000..28c4d59930 --- /dev/null +++ b/apps/openmw_test_suite/lua/testing_util.hpp @@ -0,0 +1,59 @@ +#ifndef LUA_TESTING_UTIL_H +#define LUA_TESTING_UTIL_H + +#include + +#include +#include + +namespace +{ + + class TestFile : public VFS::File + { + public: + explicit TestFile(std::string content) : mContent(std::move(content)) {} + + Files::IStreamPtr open() override + { + return std::make_shared(mContent, std::ios_base::in); + } + + private: + const std::string mContent; + }; + + struct TestData : public VFS::Archive + { + std::map mFiles; + + TestData(std::map files) : mFiles(std::move(files)) {} + + void listResources(std::map& out, char (*normalize_function) (char)) override + { + out = mFiles; + } + + bool contains(const std::string& file, char (*normalize_function) (char)) const override + { + return mFiles.count(file) != 0; + } + + std::string getDescription() const override { return "TestData"; } + + }; + + inline std::unique_ptr createTestVFS(std::map files) + { + auto vfs = std::make_unique(true); + vfs->addArchive(new TestData(std::move(files))); + vfs->buildIndex(); + return vfs; + } + + #define EXPECT_ERROR(X, ERR_SUBSTR) try { X; FAIL() << "Expected error"; } \ + catch (std::exception& e) { EXPECT_THAT(e.what(), HasSubstr(ERR_SUBSTR)); } + +} + +#endif // LUA_TESTING_UTIL_H diff --git a/cmake/FindLuaJit.cmake b/cmake/FindLuaJit.cmake new file mode 100644 index 0000000000..0f38da9b4b --- /dev/null +++ b/cmake/FindLuaJit.cmake @@ -0,0 +1,14 @@ +# Once found, defines: +# LuaJit_FOUND +# LuaJit_INCLUDE_DIR +# LuaJit_LIBRARIES + +include(LibFindMacros) + +libfind_pkg_detect(LuaJit luajit + FIND_PATH luajit.h PATH_SUFFIXES luajit luajit-2.1 + FIND_LIBRARY luajit-5.1 luajit + ) + +libfind_process(LuaJit) + diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 43987d6c7b..9d72320ca9 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -28,6 +28,10 @@ endif (GIT_CHECKOUT) # source files +add_component_dir (lua + luastate scriptscontainer utilpackage serialization omwscriptsparser + ) + add_component_dir (settings settings parser ) @@ -80,7 +84,7 @@ add_component_dir (esm loadweap records aipackage effectlist spelllist variant variantimp loadtes3 cellref filter savedgame journalentry queststate locals globalscript player objectstate cellid cellstate globalmap inventorystate containerstate npcstate creaturestate dialoguestate statstate npcstats creaturestats weatherstate quickkeys fogstate spellstate activespells creaturelevliststate doorstate projectilestate debugprofile - aisequence magiceffects util custommarkerstate stolenitems transport animationstate controlsstate mappings + aisequence magiceffects util custommarkerstate stolenitems transport animationstate controlsstate mappings luascripts ) add_component_dir (esmterrain @@ -152,6 +156,10 @@ add_component_dir (fallback fallback validate ) +add_component_dir (queries + query luabindings + ) + if(WIN32) add_component_dir (crashcatcher windows_crashcatcher diff --git a/components/compiler/extensions0.cpp b/components/compiler/extensions0.cpp index 6de35d7137..3dfcadab10 100644 --- a/components/compiler/extensions0.cpp +++ b/components/compiler/extensions0.cpp @@ -338,6 +338,7 @@ namespace Compiler extensions.registerFunction ("repairedonme", 'l', "S", opcodeRepairedOnMe, opcodeRepairedOnMeExplicit); extensions.registerInstruction ("togglerecastmesh", "", opcodeToggleRecastMesh); extensions.registerInstruction ("help", "", opcodeHelp); + extensions.registerInstruction ("reloadlua", "", opcodeReloadLua); } } diff --git a/components/compiler/opcodes.hpp b/components/compiler/opcodes.hpp index 46ee31133c..49adfbe209 100644 --- a/components/compiler/opcodes.hpp +++ b/components/compiler/opcodes.hpp @@ -319,6 +319,7 @@ namespace Compiler const int opcodeGetDisabledExplicit = 0x200031c; const int opcodeStartScriptExplicit = 0x200031d; const int opcodeHelp = 0x2000320; + const int opcodeReloadLua = 0x2000321; } namespace Sky diff --git a/components/esm/cellref.cpp b/components/esm/cellref.cpp index 7ec7e00f1a..81e0b9f557 100644 --- a/components/esm/cellref.cpp +++ b/components/esm/cellref.cpp @@ -24,8 +24,9 @@ void ESM::RefNum::save (ESMWriter &esm, bool wide, const std::string& tag) const esm.writeHNT (tag, *this, 8); else { + if (isSet() && !hasContentFile()) + Log(Debug::Error) << "Generated RefNum can not be saved in 32bit format"; int refNum = (mIndex & 0xffffff) | ((hasContentFile() ? mContentFile : 0xff)<<24); - esm.writeHNT (tag, refNum, 4); } } diff --git a/components/esm/cellref.hpp b/components/esm/cellref.hpp index c2f7ff6de5..f6eff24cbf 100644 --- a/components/esm/cellref.hpp +++ b/components/esm/cellref.hpp @@ -23,9 +23,10 @@ namespace ESM void save (ESMWriter &esm, bool wide = false, const std::string& tag = "FRMR") const; - enum { RefNum_NoContentFile = -1 }; - inline bool hasContentFile() const { return mContentFile != RefNum_NoContentFile; } - inline void unset() { mIndex = 0; mContentFile = RefNum_NoContentFile; } + inline bool hasContentFile() const { return mContentFile >= 0; } + + inline bool isSet() const { return mIndex != 0 || mContentFile != -1; } + inline void unset() { *this = {0, -1}; } // Note: this method should not be used for objects with invalid RefNum // (for example, for objects from disabled plugins in savegames). diff --git a/components/esm/defs.hpp b/components/esm/defs.hpp index 1b623f69f0..7f2fe19cc5 100644 --- a/components/esm/defs.hpp +++ b/components/esm/defs.hpp @@ -164,7 +164,10 @@ enum RecNameInts // format 1 REC_FILT = FourCC<'F','I','L','T'>::value, - REC_DBGP = FourCC<'D','B','G','P'>::value ///< only used in project files + REC_DBGP = FourCC<'D','B','G','P'>::value, ///< only used in project files + + // format 16 - Lua scripts in saved games + REC_LUAM = FourCC<'L','U','A','M'>::value, // LuaManager data }; /// Common subrecords diff --git a/components/esm/globalscript.cpp b/components/esm/globalscript.cpp index 016ea4f0cf..a8a8e79cf5 100644 --- a/components/esm/globalscript.cpp +++ b/components/esm/globalscript.cpp @@ -30,7 +30,7 @@ void ESM::GlobalScript::save (ESMWriter &esm) const if (!mTargetId.empty()) { esm.writeHNOString ("TARG", mTargetId); - if (mTargetRef.hasContentFile()) + if (mTargetRef.isSet()) mTargetRef.save (esm, true, "FRMR"); } } diff --git a/components/esm/luascripts.cpp b/components/esm/luascripts.cpp new file mode 100644 index 0000000000..1dd45ab2b1 --- /dev/null +++ b/components/esm/luascripts.cpp @@ -0,0 +1,80 @@ +#include "luascripts.hpp" + +#include "esmreader.hpp" +#include "esmwriter.hpp" + +// List of all records, that are related to Lua. +// +// Record: +// LUAM - MWLua::LuaManager +// +// Subrecords: +// LUAW - Start of MWLua::WorldView data +// LUAE - Start of MWLua::LocalEvent or MWLua::GlobalEvent (eventName) +// LUAS - Start LuaUtil::ScriptsContainer data (scriptName) +// LUAD - Serialized Lua variable +// LUAT - MWLua::ScriptsContainer::Timer +// LUAC - Name of a timer callback (string) + +void ESM::saveLuaBinaryData(ESMWriter& esm, const std::string& data) +{ + if (data.empty()) + return; + esm.startSubRecord("LUAD"); + esm.write(data.data(), data.size()); + esm.endRecord("LUAD"); +} + +std::string ESM::loadLuaBinaryData(ESMReader& esm) +{ + std::string data; + if (esm.isNextSub("LUAD")) + { + esm.getSubHeader(); + data.resize(esm.getSubSize()); + esm.getExact(data.data(), data.size()); + } + return data; +} + +void ESM::LuaScripts::load(ESMReader& esm) +{ + while (esm.isNextSub("LUAS")) + { + std::string name = esm.getHString(); + std::string data = loadLuaBinaryData(esm); + std::vector timers; + while (esm.isNextSub("LUAT")) + { + esm.getSubHeader(); + LuaTimer timer; + esm.getT(timer.mUnit); + esm.getT(timer.mTime); + timer.mCallbackName = esm.getHNString("LUAC"); + timer.mCallbackArgument = loadLuaBinaryData(esm); + timers.push_back(std::move(timer)); + } + mScripts.push_back({std::move(name), std::move(data), std::move(timers)}); + } +} + +void ESM::LuaScripts::save(ESMWriter& esm) const +{ + for (const LuaScript& script : mScripts) + { + esm.writeHNString("LUAS", script.mScriptPath); + if (!script.mData.empty()) + saveLuaBinaryData(esm, script.mData); + for (const LuaTimer& timer : script.mTimers) + { + esm.startSubRecord("LUAT"); + esm.writeT(timer.mUnit); + esm.writeT(timer.mTime); + esm.endRecord("LUAT"); + esm.writeHNString("LUAC", timer.mCallbackName); + if (!timer.mCallbackArgument.empty()) + saveLuaBinaryData(esm, timer.mCallbackArgument); + + } + } +} diff --git a/components/esm/luascripts.hpp b/components/esm/luascripts.hpp new file mode 100644 index 0000000000..f268f41536 --- /dev/null +++ b/components/esm/luascripts.hpp @@ -0,0 +1,53 @@ +#ifndef OPENMW_ESM_LUASCRIPTS_H +#define OPENMW_ESM_LUASCRIPTS_H + +#include +#include + +namespace ESM +{ + class ESMReader; + class ESMWriter; + + // Storage structure for LuaUtil::ScriptsContainer. This is not a top-level record. + // Used either for global scripts or for local scripts on a specific object. + + struct LuaTimer + { + enum class TimeUnit : bool + { + SECONDS = 0, + HOURS = 1, + }; + + TimeUnit mUnit; + double mTime; + std::string mCallbackName; + std::string mCallbackArgument; // Serialized Lua table. It is a binary data. Can contain '\0'. + }; + + struct LuaScript + { + std::string mScriptPath; + std::string mData; // Serialized Lua table. It is a binary data. Can contain '\0'. + std::vector mTimers; + }; + + struct LuaScripts + { + std::vector mScripts; + + void load (ESMReader &esm); + void save (ESMWriter &esm) const; + }; + + // Saves binary string `data` (can contain '\0') as record LUAD. + void saveLuaBinaryData(ESM::ESMWriter& esm, const std::string& data); + + // Loads LUAD as binary string. If next subrecord is not LUAD, then returns an empty string. + std::string loadLuaBinaryData(ESM::ESMReader& esm); + +} + +#endif + diff --git a/components/esm/objectstate.cpp b/components/esm/objectstate.cpp index 9709bf4ff6..76967e497c 100644 --- a/components/esm/objectstate.cpp +++ b/components/esm/objectstate.cpp @@ -20,6 +20,8 @@ void ESM::ObjectState::load (ESMReader &esm) if (mHasLocals) mLocals.load (esm); + mLuaScripts.load(esm); + mEnabled = 1; esm.getHNOT (mEnabled, "ENAB"); @@ -56,6 +58,8 @@ void ESM::ObjectState::save (ESMWriter &esm, bool inInventory) const mLocals.save (esm); } + mLuaScripts.save(esm); + if (!mEnabled && !inInventory) esm.writeHNT ("ENAB", mEnabled); diff --git a/components/esm/objectstate.hpp b/components/esm/objectstate.hpp index 6b0fca5ea6..b30f44b5e1 100644 --- a/components/esm/objectstate.hpp +++ b/components/esm/objectstate.hpp @@ -6,6 +6,7 @@ #include "cellref.hpp" #include "locals.hpp" +#include "luascripts.hpp" #include "animationstate.hpp" namespace ESM @@ -27,6 +28,7 @@ namespace ESM unsigned char mHasLocals; Locals mLocals; + LuaScripts mLuaScripts; unsigned char mEnabled; int mCount; ESM::Position mPosition; diff --git a/components/esm/savedgame.cpp b/components/esm/savedgame.cpp index 7cb30f2dd2..3f8bf10c56 100644 --- a/components/esm/savedgame.cpp +++ b/components/esm/savedgame.cpp @@ -4,7 +4,7 @@ #include "esmwriter.hpp" unsigned int ESM::SavedGame::sRecordId = ESM::REC_SAVE; -int ESM::SavedGame::sCurrentFormat = 15; +int ESM::SavedGame::sCurrentFormat = 16; void ESM::SavedGame::load (ESMReader &esm) { diff --git a/components/lua/luastate.cpp b/components/lua/luastate.cpp new file mode 100644 index 0000000000..25fa3aead1 --- /dev/null +++ b/components/lua/luastate.cpp @@ -0,0 +1,167 @@ +#include "luastate.hpp" + +#ifndef NO_LUAJIT +#include +#endif // NO_LUAJIT + +#include + +namespace LuaUtil +{ + + static std::string packageNameToPath(std::string_view packageName) + { + std::string res(packageName); + std::replace(res.begin(), res.end(), '.', '/'); + res.append(".lua"); + return res; + } + + static const std::string safeFunctions[] = { + "assert", "error", "ipairs", "next", "pairs", "pcall", "select", "tonumber", "tostring", + "type", "unpack", "xpcall", "rawequal", "rawget", "rawset", "getmetatable", "setmetatable"}; + static const std::string safePackages[] = {"coroutine", "math", "string", "table"}; + + LuaState::LuaState(const VFS::Manager* vfs) : mVFS(vfs) + { + mLua.open_libraries(sol::lib::base, sol::lib::coroutine, sol::lib::math, sol::lib::string, sol::lib::table); + + mLua["math"]["randomseed"](static_cast(std::time(nullptr))); + mLua["math"]["randomseed"] = sol::nil; + + mLua["writeToLog"] = [](std::string_view s) { Log(Debug::Level::Info) << s; }; + mLua.script(R"(printToLog = function(name, ...) + local msg = name + for _, v in ipairs({...}) do + msg = msg .. '\t' .. tostring(v) + end + return writeToLog(msg) + end)"); + mLua.script("printGen = function(name) return function(...) return printToLog(name, ...) end end"); + + // Some fixes for compatibility between different Lua versions + if (mLua["unpack"] == sol::nil) + mLua["unpack"] = mLua["table"]["unpack"]; + else if (mLua["table"]["unpack"] == sol::nil) + mLua["table"]["unpack"] = mLua["unpack"]; + + mSandboxEnv = sol::table(mLua, sol::create); + mSandboxEnv["_VERSION"] = mLua["_VERSION"]; + for (const std::string& s : safeFunctions) + { + if (mLua[s] == sol::nil) throw std::logic_error("Lua function not found: " + s); + mSandboxEnv[s] = mLua[s]; + } + for (const std::string& s : safePackages) + { + if (mLua[s] == sol::nil) throw std::logic_error("Lua package not found: " + s); + mCommonPackages[s] = mSandboxEnv[s] = makeReadOnly(mLua[s]); + } + } + + LuaState::~LuaState() + { + // Should be cleaned before destructing mLua. + mCommonPackages.clear(); + mSandboxEnv = sol::nil; + } + + sol::table LuaState::makeReadOnly(sol::table table) + { + if (table.is()) + return table; // it is already userdata, no sense to wrap it again + + table[sol::meta_function::index] = table; + sol::stack::push(mLua, std::move(table)); + lua_newuserdata(mLua, 0); + lua_pushvalue(mLua, -2); + lua_setmetatable(mLua, -2); + return sol::stack::pop(mLua); + } + + sol::table LuaState::getMutableFromReadOnly(const sol::userdata& ro) + { + sol::stack::push(mLua, ro); + lua_getmetatable(mLua, -1); + sol::table res = sol::stack::pop(mLua); + lua_pop(mLua, 1); + return res; + } + + void LuaState::addCommonPackage(const std::string& packageName, const sol::object& package) + { + if (package.is()) + mCommonPackages[packageName] = package; + else + mCommonPackages[packageName] = makeReadOnly(package); + } + + sol::protected_function_result LuaState::runInNewSandbox( + const std::string& path, const std::string& namePrefix, + const std::map& packages, const sol::object& hiddenData) + { + sol::protected_function script = loadScript(path); + + sol::environment env(mLua, sol::create, mSandboxEnv); + std::string envName = namePrefix + "[" + path + "]:"; + env["print"] = mLua["printGen"](envName); + + sol::table loaded(mLua, sol::create); + for (const auto& [key, value] : mCommonPackages) + loaded[key] = value; + for (const auto& [key, value] : packages) + loaded[key] = value; + env["require"] = [this, env, loaded, hiddenData](std::string_view packageName) + { + sol::table packages = loaded; + sol::object package = packages[packageName]; + if (package == sol::nil) + { + sol::protected_function packageLoader = loadScript(packageNameToPath(packageName)); + sol::set_environment(env, packageLoader); + package = throwIfError(packageLoader()); + if (!package.is()) + throw std::runtime_error("Lua package must return a table."); + packages[packageName] = package; + } + else if (package.is()) + package = packages[packageName] = call(package.as(), hiddenData); + return package; + }; + + sol::set_environment(env, script); + return call(script); + } + + sol::protected_function_result LuaState::throwIfError(sol::protected_function_result&& res) + { + if (!res.valid() && static_cast(res.get_type()) == LUA_TSTRING) + throw std::runtime_error("Lua error: " + res.get()); + else + return std::move(res); + } + + sol::protected_function LuaState::loadScript(const std::string& path) + { + auto iter = mCompiledScripts.find(path); + if (iter != mCompiledScripts.end()) + return mLua.load(iter->second.as_string_view(), path, sol::load_mode::binary); + + std::string fileContent(std::istreambuf_iterator(*mVFS->get(path)), {}); + sol::load_result res = mLua.load(fileContent, path, sol::load_mode::text); + if (!res.valid()) + throw std::runtime_error("Lua error: " + res.get()); + mCompiledScripts[path] = res.get().dump(); + return res; + } + + std::string getLuaVersion() + { + #ifdef NO_LUAJIT + return LUA_RELEASE; + #else + return LUA_RELEASE " (" LUAJIT_VERSION ")"; + #endif + } + +} diff --git a/components/lua/luastate.hpp b/components/lua/luastate.hpp new file mode 100644 index 0000000000..9cb27fb114 --- /dev/null +++ b/components/lua/luastate.hpp @@ -0,0 +1,107 @@ +#ifndef COMPONENTS_LUA_LUASTATE_H +#define COMPONENTS_LUA_LUASTATE_H + +#include + +#include + +#include + +namespace LuaUtil +{ + + std::string getLuaVersion(); + + // Holds Lua state. + // Provides additional features: + // - Load scripts from the virtual filesystem; + // - Caching of loaded scripts; + // - Disable unsafe Lua functions; + // - Run every instance of every script in a separate sandbox; + // - Forbid any interactions between sandboxes except than via provided API; + // - Access to common read-only resources from different sandboxes; + // - Replace standard `require` with a safe version that allows to search + // Lua libraries (only source, no dll's) in the virtual filesystem; + // - Make `print` to add the script name to the every message and + // write to Log rather than directly to stdout; + class LuaState + { + public: + explicit LuaState(const VFS::Manager* vfs); + ~LuaState(); + + // Returns underlying sol::state. + sol::state& sol() { return mLua; } + + // A shortcut to create a new Lua table. + sol::table newTable() { return sol::table(mLua, sol::create); } + + // Makes a table read only (when accessed from Lua) by wrapping it with an empty userdata. + // Needed to forbid any changes in common resources that can accessed from different sandboxes. + sol::table makeReadOnly(sol::table); + sol::table getMutableFromReadOnly(const sol::userdata&); + + // Registers a package that will be available from every sandbox via `require(name)`. + // The package can be either a sol::table with an API or a sol::function. If it is a function, + // it will be evaluated (once per sandbox) the first time when requested. If the package + // is a table, then `makeReadOnly` is applied to it automatically (but not to other tables it contains). + void addCommonPackage(const std::string& packageName, const sol::object& package); + + // Creates a new sandbox, runs a script, and returns the result + // (the result is expected to be an interface of the script). + // Args: + // path: path to the script in the virtual filesystem; + // namePrefix: sandbox name will be "[]". Sandbox name + // will be added to every `print` output. + // packages: additional packages that should be available from the sandbox via `require`. Each package + // should be either a sol::table or a sol::function. If it is a function, it will be evaluated + // (once per sandbox) with the argument 'hiddenData' the first time when requested. + sol::protected_function_result runInNewSandbox(const std::string& path, + const std::string& namePrefix = "", + const std::map& packages = {}, + const sol::object& hiddenData = sol::nil); + + void dropScriptCache() { mCompiledScripts.clear(); } + + private: + static sol::protected_function_result throwIfError(sol::protected_function_result&&); + template + friend sol::protected_function_result call(sol::protected_function fn, Args&&... args); + + sol::protected_function loadScript(const std::string& path); + + sol::state mLua; + sol::table mSandboxEnv; + std::map mCompiledScripts; + std::map mCommonPackages; + const VFS::Manager* mVFS; + }; + + // Should be used for every call of every Lua function. + // It is a workaround for a bug in `sol`. See https://github.com/ThePhD/sol2/issues/1078 + template + sol::protected_function_result call(sol::protected_function fn, Args&&... args) + { + try + { + return LuaState::throwIfError(fn(std::forward(args)...)); + } + catch (std::exception&) { throw; } + catch (...) { throw std::runtime_error("Unknown error"); } + } + + // getFieldOrNil(table, "a", "b", "c") returns table["a"]["b"]["c"] or nil if some of the fields doesn't exist. + template + sol::object getFieldOrNil(const sol::object& table, std::string_view first, const Str&... str) + { + if (!table.is()) + return sol::nil; + if constexpr (sizeof...(str) == 0) + return table.as()[first]; + else + return getFieldOrNil(table.as()[first], str...); + } + +} + +#endif // COMPONENTS_LUA_LUASTATE_H diff --git a/components/lua/omwscriptsparser.cpp b/components/lua/omwscriptsparser.cpp new file mode 100644 index 0000000000..bc73e013db --- /dev/null +++ b/components/lua/omwscriptsparser.cpp @@ -0,0 +1,44 @@ +#include "omwscriptsparser.hpp" + +#include + +#include + +std::vector LuaUtil::parseOMWScriptsFiles(const VFS::Manager* vfs, const std::vector& scriptLists) +{ + auto endsWith = [](std::string_view s, std::string_view suffix) + { + return s.size() >= suffix.size() && std::equal(suffix.rbegin(), suffix.rend(), s.rbegin()); + }; + std::vector res; + for (const std::string& scriptListFile : scriptLists) + { + if (!endsWith(scriptListFile, ".omwscripts")) + { + Log(Debug::Error) << "Script list should have suffix '.omwscripts', got: '" << scriptListFile << "'"; + continue; + } + std::string content(std::istreambuf_iterator(*vfs->get(scriptListFile)), {}); + std::string_view view(content); + while (!view.empty()) + { + size_t pos = 0; + while (pos < view.size() && view[pos] != '\n') + pos++; + std::string_view line = view.substr(0, pos); + view = view.substr(std::min(pos + 1, view.size())); + if (!line.empty() && line.back() == '\r') + line = line.substr(0, pos - 1); + // Lines starting with '#' are comments. + // TODO: Maybe make the parser more robust. It is a bit inconsistent that 'path/#to/file.lua' + // is a valid path, but '#path/to/file.lua' is considered as a comment and ignored. + if (line.empty() || line[0] == '#') + continue; + if (endsWith(line, ".lua")) + res.push_back(std::string(line)); + else + Log(Debug::Error) << "Lua script should have suffix '.lua', got: '" << line.substr(0, 300) << "'"; + } + } + return res; +} diff --git a/components/lua/omwscriptsparser.hpp b/components/lua/omwscriptsparser.hpp new file mode 100644 index 0000000000..1da9f123b2 --- /dev/null +++ b/components/lua/omwscriptsparser.hpp @@ -0,0 +1,14 @@ +#ifndef COMPONENTS_LUA_OMWSCRIPTSPARSER_H +#define COMPONENTS_LUA_OMWSCRIPTSPARSER_H + +#include + +namespace LuaUtil +{ + + // Parses list of `*.omwscripts` files. + std::vector parseOMWScriptsFiles(const VFS::Manager* vfs, const std::vector& scriptLists); + +} + +#endif // COMPONENTS_LUA_OMWSCRIPTSPARSER_H diff --git a/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp new file mode 100644 index 0000000000..a53b1d0404 --- /dev/null +++ b/components/lua/scriptscontainer.cpp @@ -0,0 +1,428 @@ +#include "scriptscontainer.hpp" + +#include + +namespace LuaUtil +{ + static constexpr std::string_view ENGINE_HANDLERS = "engineHandlers"; + static constexpr std::string_view EVENT_HANDLERS = "eventHandlers"; + + static constexpr std::string_view INTERFACE_NAME = "interfaceName"; + static constexpr std::string_view INTERFACE = "interface"; + + static constexpr std::string_view HANDLER_SAVE = "onSave"; + static constexpr std::string_view HANDLER_LOAD = "onLoad"; + + static constexpr std::string_view REGISTERED_TIMER_CALLBACKS = "_timers"; + static constexpr std::string_view TEMPORARY_TIMER_CALLBACKS = "_temp_timers"; + + ScriptsContainer::ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix) : mNamePrefix(namePrefix), mLua(*lua) + { + registerEngineHandlers({&mUpdateHandlers}); + mPublicInterfaces = sol::table(lua->sol(), sol::create); + addPackage("openmw.interfaces", mPublicInterfaces); + } + + void ScriptsContainer::addPackage(const std::string& packageName, sol::object package) + { + API[packageName] = mLua.makeReadOnly(std::move(package)); + } + + bool ScriptsContainer::addNewScript(const std::string& path) + { + if (mScripts.count(path) != 0) + return false; // already present + + try + { + sol::table hiddenData(mLua.sol(), sol::create); + hiddenData[ScriptId::KEY] = ScriptId{this, path}; + hiddenData[REGISTERED_TIMER_CALLBACKS] = mLua.newTable(); + hiddenData[TEMPORARY_TIMER_CALLBACKS] = mLua.newTable(); + mScripts[path].mHiddenData = hiddenData; + sol::object script = mLua.runInNewSandbox(path, mNamePrefix, API, hiddenData); + std::string interfaceName = ""; + sol::object publicInterface = sol::nil; + if (script != sol::nil) + { + for (auto& [key, value] : sol::table(script)) + { + std::string_view sectionName = key.as(); + if (sectionName == ENGINE_HANDLERS) + parseEngineHandlers(value, path); + else if (sectionName == EVENT_HANDLERS) + parseEventHandlers(value, path); + else if (sectionName == INTERFACE_NAME) + interfaceName = value.as(); + else if (sectionName == INTERFACE) + publicInterface = value.as(); + else + Log(Debug::Error) << "Not supported section '" << sectionName << "' in " << mNamePrefix << "[" << path << "]"; + } + } + if (interfaceName.empty() != (publicInterface == sol::nil)) + Log(Debug::Error) << mNamePrefix << "[" << path << "]: 'interfaceName' should always be used together with 'interface'"; + else if (!interfaceName.empty()) + script.as()[INTERFACE] = mPublicInterfaces[interfaceName] = mLua.makeReadOnly(publicInterface); + mScriptOrder.push_back(path); + mScripts[path].mInterface = std::move(script); + return true; + } + catch (std::exception& e) + { + mScripts.erase(path); + Log(Debug::Error) << "Can't start " << mNamePrefix << "[" << path << "]; " << e.what(); + return false; + } + } + + bool ScriptsContainer::removeScript(const std::string& path) + { + auto scriptIter = mScripts.find(path); + if (scriptIter == mScripts.end()) + return false; // no such script + sol::object& script = scriptIter->second.mInterface; + if (getFieldOrNil(script, INTERFACE_NAME) != sol::nil) + { + std::string_view interfaceName = getFieldOrNil(script, INTERFACE_NAME).as(); + if (mPublicInterfaces[interfaceName] == getFieldOrNil(script, INTERFACE)) + { + mPublicInterfaces[interfaceName] = sol::nil; + auto prevIt = mScriptOrder.rbegin(); + while (*prevIt != path) + prevIt++; + prevIt++; + while (prevIt != mScriptOrder.rend()) + { + sol::object& prevScript = mScripts[*(prevIt++)].mInterface; + sol::object prevInterfaceName = getFieldOrNil(prevScript, INTERFACE_NAME); + if (prevInterfaceName != sol::nil && prevInterfaceName.as() == interfaceName) + { + mPublicInterfaces[interfaceName] = getFieldOrNil(prevScript, INTERFACE); + break; + } + } + } + } + sol::object engineHandlers = getFieldOrNil(script, ENGINE_HANDLERS); + if (engineHandlers != sol::nil) + { + for (auto& [key, value] : sol::table(engineHandlers)) + { + std::string_view handlerName = key.as(); + auto handlerIter = mEngineHandlers.find(handlerName); + if (handlerIter == mEngineHandlers.end()) + continue; + std::vector& list = handlerIter->second->mList; + list.erase(std::find(list.begin(), list.end(), value.as())); + } + } + sol::object eventHandlers = getFieldOrNil(script, EVENT_HANDLERS); + if (eventHandlers != sol::nil) + { + for (auto& [key, value] : sol::table(eventHandlers)) + { + EventHandlerList& list = mEventHandlers.find(key.as())->second; + list.erase(std::find(list.begin(), list.end(), value.as())); + } + } + mScripts.erase(scriptIter); + mScriptOrder.erase(std::find(mScriptOrder.begin(), mScriptOrder.end(), path)); + return true; + } + + void ScriptsContainer::parseEventHandlers(sol::table handlers, std::string_view scriptPath) + { + for (auto& [key, value] : handlers) + { + std::string_view eventName = key.as(); + auto it = mEventHandlers.find(eventName); + if (it == mEventHandlers.end()) + it = mEventHandlers.insert({std::string(eventName), EventHandlerList()}).first; + it->second.push_back(value); + } + } + + void ScriptsContainer::parseEngineHandlers(sol::table handlers, std::string_view scriptPath) + { + for (auto& [key, value] : handlers) + { + std::string_view handlerName = key.as(); + if (handlerName == HANDLER_LOAD || handlerName == HANDLER_SAVE) + continue; // save and load are handled separately + auto it = mEngineHandlers.find(handlerName); + if (it == mEngineHandlers.end()) + Log(Debug::Error) << "Not supported handler '" << handlerName << "' in " << mNamePrefix << "[" << scriptPath << "]"; + else + it->second->mList.push_back(value); + } + } + + void ScriptsContainer::receiveEvent(std::string_view eventName, std::string_view eventData) + { + 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 + { + data = LuaUtil::deserialize(mLua.sol(), eventData, mSerializer); + } + catch (std::exception& e) + { + Log(Debug::Error) << mNamePrefix << " can not parse eventData for '" << eventName << "': " << e.what(); + return; + } + EventHandlerList& list = it->second; + for (int i = list.size() - 1; i >= 0; --i) + { + try + { + sol::object res = LuaUtil::call(list[i], data); + if (res != sol::nil && !res.as()) + break; // Skip other handlers if 'false' was returned. + } + catch (std::exception& e) + { + Log(Debug::Error) << mNamePrefix << " eventHandler[" << eventName << "] failed. " << e.what(); + } + } + } + + void ScriptsContainer::registerEngineHandlers(std::initializer_list handlers) + { + for (EngineHandlerList* h : handlers) + mEngineHandlers[h->mName] = h; + } + + void ScriptsContainer::save(ESM::LuaScripts& data) + { + std::map> timers; + auto saveTimerFn = [&](const Timer& timer, TimeUnit timeUnit) + { + if (!timer.mSerializable) + return; + ESM::LuaTimer savedTimer; + savedTimer.mTime = timer.mTime; + savedTimer.mUnit = timeUnit; + savedTimer.mCallbackName = std::get(timer.mCallback); + savedTimer.mCallbackArgument = timer.mSerializedArg; + if (timers.count(timer.mScript) == 0) + timers[timer.mScript] = {}; + timers[timer.mScript].push_back(std::move(savedTimer)); + }; + for (const Timer& timer : mSecondsTimersQueue) + saveTimerFn(timer, TimeUnit::SECONDS); + for (const Timer& timer : mHoursTimersQueue) + saveTimerFn(timer, TimeUnit::HOURS); + data.mScripts.clear(); + for (const std::string& path : mScriptOrder) + { + ESM::LuaScript savedScript; + savedScript.mScriptPath = path; + sol::object handler = getFieldOrNil(mScripts[path].mInterface, ENGINE_HANDLERS, HANDLER_SAVE); + if (handler != sol::nil) + { + try + { + sol::object state = LuaUtil::call(handler); + savedScript.mData = serialize(state, mSerializer); + } + catch (std::exception& e) + { + Log(Debug::Error) << mNamePrefix << "[" << path << "] onSave failed: " << e.what(); + } + } + auto timersIt = timers.find(path); + if (timersIt != timers.end()) + savedScript.mTimers = std::move(timersIt->second); + data.mScripts.push_back(std::move(savedScript)); + } + } + + void ScriptsContainer::load(const ESM::LuaScripts& data, bool resetScriptList) + { + std::map scriptsWithoutSavedData; + if (resetScriptList) + { + removeAllScripts(); + for (const ESM::LuaScript& script : data.mScripts) + addNewScript(script.mScriptPath); + } + else + scriptsWithoutSavedData = mScripts; + mSecondsTimersQueue.clear(); + mHoursTimersQueue.clear(); + for (const ESM::LuaScript& script : data.mScripts) + { + auto iter = mScripts.find(script.mScriptPath); + if (iter == mScripts.end()) + continue; + scriptsWithoutSavedData.erase(iter->first); + iter->second.mHiddenData.get(TEMPORARY_TIMER_CALLBACKS).clear(); + try + { + sol::object handler = getFieldOrNil(iter->second.mInterface, ENGINE_HANDLERS, HANDLER_LOAD); + if (handler != sol::nil) + { + sol::object state = deserialize(mLua.sol(), script.mData, mSerializer); + LuaUtil::call(handler, state); + } + } + catch (std::exception& e) + { + Log(Debug::Error) << mNamePrefix << "[" << script.mScriptPath << "] onLoad failed: " << e.what(); + } + for (const ESM::LuaTimer& savedTimer : script.mTimers) + { + Timer timer; + timer.mCallback = savedTimer.mCallbackName; + timer.mSerializable = true; + timer.mScript = script.mScriptPath; + timer.mTime = savedTimer.mTime; + + try + { + timer.mArg = deserialize(mLua.sol(), savedTimer.mCallbackArgument, mSerializer); + // It is important if the order of content files was changed. The deserialize-serialize procedure + // updates refnums, so timer.mSerializedArg may be not equal to savedTimer.mCallbackArgument. + timer.mSerializedArg = serialize(timer.mArg, mSerializer); + + if (savedTimer.mUnit == TimeUnit::HOURS) + mHoursTimersQueue.push_back(std::move(timer)); + else + mSecondsTimersQueue.push_back(std::move(timer)); + } + catch (std::exception& e) + { + Log(Debug::Error) << mNamePrefix << "[" << script.mScriptPath << "] can not load timer: " << e.what(); + } + } + } + for (auto& [path, script] : scriptsWithoutSavedData) + { + script.mHiddenData.get(TEMPORARY_TIMER_CALLBACKS).clear(); + sol::object handler = getFieldOrNil(script.mInterface, ENGINE_HANDLERS, HANDLER_LOAD); + if (handler == sol::nil) + continue; + try { LuaUtil::call(handler); } + catch (std::exception& e) + { + Log(Debug::Error) << mNamePrefix << "[" << path << "] onLoad failed: " << e.what(); + } + } + std::make_heap(mSecondsTimersQueue.begin(), mSecondsTimersQueue.end()); + std::make_heap(mHoursTimersQueue.begin(), mHoursTimersQueue.end()); + } + + void ScriptsContainer::removeAllScripts() + { + mScripts.clear(); + mScriptOrder.clear(); + for (auto& [_, handlers] : mEngineHandlers) + handlers->mList.clear(); + mEventHandlers.clear(); + mSecondsTimersQueue.clear(); + mHoursTimersQueue.clear(); + + mPublicInterfaces.clear(); + // Assigned by mLua.makeReadOnly, but `clear` removes it, so we need to assign it again. + mPublicInterfaces[sol::meta_function::index] = mPublicInterfaces; + } + + sol::table ScriptsContainer::getHiddenData(const std::string& scriptPath) + { + auto it = mScripts.find(scriptPath); + if (it == mScripts.end()) + throw std::logic_error("ScriptsContainer::getHiddenData: script doesn't exist"); + return it->second.mHiddenData; + } + + void ScriptsContainer::registerTimerCallback(const std::string& scriptPath, std::string_view callbackName, sol::function callback) + { + getHiddenData(scriptPath)[REGISTERED_TIMER_CALLBACKS][callbackName] = std::move(callback); + } + + void ScriptsContainer::insertTimer(std::vector& timerQueue, Timer&& t) + { + timerQueue.push_back(std::move(t)); + std::push_heap(timerQueue.begin(), timerQueue.end()); + } + + void ScriptsContainer::setupSerializableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath, + std::string_view callbackName, sol::object callbackArg) + { + Timer t; + t.mCallback = std::string(callbackName); + t.mScript = scriptPath; + t.mSerializable = true; + t.mTime = time; + t.mArg = callbackArg; + t.mSerializedArg = serialize(t.mArg, mSerializer); + insertTimer(timeUnit == TimeUnit::HOURS ? mHoursTimersQueue : mSecondsTimersQueue, std::move(t)); + } + + void ScriptsContainer::setupUnsavableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath, sol::function callback) + { + Timer t; + t.mScript = scriptPath; + t.mSerializable = false; + t.mTime = time; + + t.mCallback = mTemporaryCallbackCounter; + getHiddenData(scriptPath)[TEMPORARY_TIMER_CALLBACKS][mTemporaryCallbackCounter] = std::move(callback); + mTemporaryCallbackCounter++; + + insertTimer(timeUnit == TimeUnit::HOURS ? mHoursTimersQueue : mSecondsTimersQueue, std::move(t)); + } + + void ScriptsContainer::callTimer(const Timer& t) + { + try + { + sol::table data = getHiddenData(t.mScript); + if (t.mSerializable) + { + const std::string& callbackName = std::get(t.mCallback); + sol::object callback = data[REGISTERED_TIMER_CALLBACKS][callbackName]; + if (!callback.is()) + throw std::logic_error("Callback '" + callbackName + "' doesn't exist"); + LuaUtil::call(callback, t.mArg); + } + else + { + int64_t id = std::get(t.mCallback); + sol::table callbacks = data[TEMPORARY_TIMER_CALLBACKS]; + sol::object callback = callbacks[id]; + if (!callback.is()) + throw std::logic_error("Temporary timer callback doesn't exist"); + LuaUtil::call(callback); + callbacks[id] = sol::nil; + } + } + catch (std::exception& e) + { + Log(Debug::Error) << mNamePrefix << "[" << t.mScript << "] callTimer failed: " << e.what(); + } + } + + void ScriptsContainer::updateTimerQueue(std::vector& timerQueue, double time) + { + while (!timerQueue.empty() && timerQueue.front().mTime <= time) + { + callTimer(timerQueue.front()); + std::pop_heap(timerQueue.begin(), timerQueue.end()); + timerQueue.pop_back(); + } + } + + void ScriptsContainer::processTimers(double gameSeconds, double gameHours) + { + updateTimerQueue(mSecondsTimersQueue, gameSeconds); + updateTimerQueue(mHoursTimersQueue, gameHours); + } + +} diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp new file mode 100644 index 0000000000..7b2b2a7aa9 --- /dev/null +++ b/components/lua/scriptscontainer.hpp @@ -0,0 +1,213 @@ +#ifndef COMPONENTS_LUA_SCRIPTSCONTAINER_H +#define COMPONENTS_LUA_SCRIPTSCONTAINER_H + +#include +#include +#include + +#include +#include + +#include "luastate.hpp" +#include "serialization.hpp" + +namespace LuaUtil +{ + +// ScriptsContainer is a base class for all scripts containers (LocalScripts, +// GlobalScripts, PlayerScripts, etc). Each script runs in a separate sandbox. +// Scripts from different containers can interact to each other only via events. +// Scripts within one container can interact via interfaces (not implemented yet). +// All scripts from one container have the same set of API packages available. +// +// Each script should return a table in a specific format that describes its +// handlers and interfaces. Every section of the table is optional. Basic structure: +// +// local function update(dt) +// print("Update") +// end +// +// local function someEventHandler(eventData) +// print("'SomeEvent' received") +// end +// +// return { +// -- Provides interface for other scripts in the same container +// interfaceName = "InterfaceName", +// interface = { +// someFunction = function() print("someFunction was called from another script") end, +// }, +// +// -- Script interface for the engine. Not available for other script. +// -- An error is printed if unknown handler is specified. +// engineHandlers = { +// onUpdate = update, +// onSave = function() return ... end, +// onLoad = function(state) ... end, -- "state" is the data that was earlier returned by onSave +// +// -- Works only if ScriptsContainer::registerEngineHandler is overloaded in a child class +// -- and explicitly supports 'onSomethingElse' +// onSomethingElse = function() print("something else") end +// }, +// +// -- Handlers for events, sent from other scripts. Engine itself never sent events. Any name can be used for an event. +// eventHandlers = { +// SomeEvent = someEventHandler +// } +// } + + class ScriptsContainer + { + public: + struct ScriptId + { + // ScriptId is stored in hidden data (see getHiddenData) with this key. + constexpr static std::string_view KEY = "_id"; + + ScriptsContainer* mContainer; + std::string mPath; + }; + using TimeUnit = ESM::LuaTimer::TimeUnit; + + // `namePrefix` is a common prefix for all scripts in the container. Used in logs for error messages and `print` output. + ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix); + ScriptsContainer(const ScriptsContainer&) = delete; + ScriptsContainer(ScriptsContainer&&) = delete; + virtual ~ScriptsContainer() {} + + // Adds package that will be available (via `require`) for all scripts in the container. + // Automatically applies LuaState::makeReadOnly to the package. + void addPackage(const std::string& packageName, sol::object package); + + // Finds a file with given path in the virtual file system, starts as a new script, and adds it to the container. + // Returns `true` if the script was successfully added. Otherwise prints an error message and returns `false`. + // `false` can be returned if either file not found or has syntax errors or such script already exists in the container. + bool addNewScript(const std::string& path); + + // Removes script. Returns `true` if it was successfully removed. + bool removeScript(const std::string& path); + void removeAllScripts(); + + // Processes timers. gameSeconds and gameHours are time (in seconds and in game hours) passed from the game start. + void processTimers(double gameSeconds, double gameHours); + + // Calls `onUpdate` (if present) for every script in the container. + // Handlers are called in the same order as scripts were added. + void update(float dt) { callEngineHandlers(mUpdateHandlers, dt); } + + // Calls event handlers `eventName` (if present) for every script. + // If several scripts register handlers for `eventName`, they are called in reverse order. + // If some handler returns `false`, all remaining handlers are ignored. Any other return value + // (including `nil`) has no effect. + void receiveEvent(std::string_view eventName, std::string_view eventData); + + // Serializer defines how to serialize/deserialize userdata. If serializer is not provided, + // only built-in types and types from util package can be serialized. + void setSerializer(const UserdataSerializer* serializer) { mSerializer = serializer; } + + // Calls engineHandler "onSave" for every script and saves the list of the scripts with serialized data to ESM::LuaScripts. + void save(ESM::LuaScripts&); + + // Calls engineHandler "onLoad" for every script with given data. + // If resetScriptList=true, then removes all currently active scripts and runs the scripts that were saved in ESM::LuaScripts. + // If resetScriptList=false, then list of running scripts is not changed, only engineHandlers "onLoad" are called. + void load(const ESM::LuaScripts&, bool resetScriptList); + + // Returns the hidden data of a script. + // Each script has a corresponding "hidden data" - a lua table that is not accessible from the script itself, + // but can be used by built-in packages. It contains ScriptId and can contain any arbitrary data. + sol::table getHiddenData(const std::string& scriptPath); + + // Callbacks for serializable timers should be registered in advance. + // The script with the given path should already present in the container. + void registerTimerCallback(const std::string& scriptPath, std::string_view callbackName, sol::function callback); + + // Sets up a timer, that can be automatically saved and loaded. + // timeUnit - game seconds (TimeUnit::Seconds) or game hours (TimeUnit::Hours). + // time - the absolute game time (in seconds or in hours) when the timer should be executed. + // scriptPath - script path in VFS is used as script id. The script with the given path should already present in the container. + // callbackName - callback (should be registered in advance) for this timer. + // callbackArg - parameter for the callback (should be serializable). + void setupSerializableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath, + std::string_view callbackName, sol::object callbackArg); + + // Creates a timer. `callback` is an arbitrary Lua function. This type of timers is called "unsavable" + // because it can not be stored in saves. I.e. loading a saved game will not fully restore the state. + void setupUnsavableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath, sol::function callback); + + protected: + struct EngineHandlerList + { + std::string_view mName; + std::vector mList; + + // "name" must be string literal + explicit EngineHandlerList(std::string_view name) : mName(name) {} + }; + + // Calls given handlers in direct order. + template + void callEngineHandlers(EngineHandlerList& handlers, const Args&... args) + { + for (sol::protected_function& handler : handlers.mList) + { + try { LuaUtil::call(handler, args...); } + catch (std::exception& e) + { + Log(Debug::Error) << mNamePrefix << " " << handlers.mName << " failed. " << e.what(); + } + } + } + + // To add a new engine handler a derived class should register the corresponding EngineHandlerList and define + // a public function (see how ScriptsContainer::update is implemented) that calls `callEngineHandlers`. + void registerEngineHandlers(std::initializer_list handlers); + + const std::string mNamePrefix; + LuaUtil::LuaState& mLua; + + private: + struct Script + { + sol::object mInterface; // returned value of the script (sol::table or nil) + sol::table mHiddenData; + }; + struct Timer + { + double mTime; + bool mSerializable; + std::string mScript; + std::variant mCallback; // string if serializable, integer otherwise + sol::object mArg; + std::string mSerializedArg; + + bool operator<(const Timer& t) const { return mTime > t.mTime; } + }; + using EventHandlerList = std::vector; + + void parseEngineHandlers(sol::table handlers, std::string_view scriptPath); + void parseEventHandlers(sol::table handlers, std::string_view scriptPath); + + void callTimer(const Timer& t); + void updateTimerQueue(std::vector& timerQueue, double time); + static void insertTimer(std::vector& timerQueue, Timer&& t); + + const UserdataSerializer* mSerializer = nullptr; + std::map API; + + std::vector mScriptOrder; + std::map mScripts; + sol::table mPublicInterfaces; + + EngineHandlerList mUpdateHandlers{"onUpdate"}; + std::map mEngineHandlers; + std::map> mEventHandlers; + + std::vector mSecondsTimersQueue; + std::vector mHoursTimersQueue; + int64_t mTemporaryCallbackCounter = 0; + }; + +} + +#endif // COMPONENTS_LUA_SCRIPTSCONTAINER_H diff --git a/components/lua/serialization.cpp b/components/lua/serialization.cpp new file mode 100644 index 0000000000..53b6fe3b92 --- /dev/null +++ b/components/lua/serialization.cpp @@ -0,0 +1,257 @@ +#include "serialization.hpp" + +#include +#include + +#include + +namespace LuaUtil +{ + + constexpr unsigned char FORMAT_VERSION = 0; + + enum class SerializedType : char + { + NUMBER = 0x0, + LONG_STRING = 0x1, + BOOLEAN = 0x2, + TABLE_START = 0x3, + TABLE_END = 0x4, + + VEC2 = 0x10, + VEC3 = 0x11, + + // All values should be lesser than 0x20 (SHORT_STRING_FLAG). + }; + constexpr unsigned char SHORT_STRING_FLAG = 0x20; // 0b001SSSSS. SSSSS = string length + constexpr unsigned char CUSTOM_FULL_FLAG = 0x40; // 0b01TTTTTT + 32bit dataSize + constexpr unsigned char CUSTOM_COMPACT_FLAG = 0x80; // 0b1SSSSTTT. SSSS = dataSize, TTT = (typeName size - 1) + + static void appendType(BinaryData& out, SerializedType type) + { + out.push_back(static_cast(type)); + } + + template + static void appendValue(BinaryData& out, T v) + { + v = Misc::toLittleEndian(v); + out.append(reinterpret_cast(&v), sizeof(v)); + } + + template + static T getValue(std::string_view& binaryData) + { + if (binaryData.size() < sizeof(T)) + throw std::runtime_error("Unexpected end"); + T v; + std::memcpy(&v, binaryData.data(), sizeof(T)); + binaryData = binaryData.substr(sizeof(T)); + return Misc::fromLittleEndian(v); + } + + static void appendString(BinaryData& out, std::string_view str) + { + if (str.size() < 32) + out.push_back(SHORT_STRING_FLAG | char(str.size())); + else + { + appendType(out, SerializedType::LONG_STRING); + appendValue(out, str.size()); + } + out.append(str.data(), str.size()); + } + + static void appendData(BinaryData& out, const void* data, size_t dataSize) + { + out.append(reinterpret_cast(data), dataSize); + } + + void UserdataSerializer::append(BinaryData& out, std::string_view typeName, const void* data, size_t dataSize) + { + assert(!typeName.empty() && typeName.size() <= 64); + if (typeName.size() <= 8 && dataSize < 16) + { // Compact form: 0b1SSSSTTT. SSSS = dataSize, TTT = (typeName size - 1). + unsigned char t = CUSTOM_COMPACT_FLAG | (dataSize << 3) | (typeName.size() - 1); + out.push_back(t); + } + else + { // Full form: 0b01TTTTTT + 32bit dataSize. + unsigned char t = CUSTOM_FULL_FLAG | (typeName.size() - 1); + out.push_back(t); + appendValue(out, dataSize); + } + out.append(typeName.data(), typeName.size()); + appendData(out, data, dataSize); + } + + static void serializeUserdata(BinaryData& out, const sol::userdata& data, const UserdataSerializer* customSerializer) + { + if (data.is()) + { + appendType(out, SerializedType::VEC2); + osg::Vec2f v = data.as(); + appendValue(out, v.x()); + appendValue(out, v.y()); + return; + } + if (data.is()) + { + appendType(out, SerializedType::VEC3); + osg::Vec3f v = data.as(); + appendValue(out, v.x()); + appendValue(out, v.y()); + appendValue(out, v.z()); + return; + } + if (customSerializer && customSerializer->serialize(out, data)) + return; + else + throw std::runtime_error("Unknown userdata"); + } + + static void serialize(BinaryData& out, const sol::object& obj, const UserdataSerializer* customSerializer, int recursionCounter) + { + if (obj.get_type() == sol::type::lightuserdata) + throw std::runtime_error("light userdata is not allowed to be serialized"); + if (obj.is()) + throw std::runtime_error("functions are not allowed to be serialized"); + else if (obj.is()) + serializeUserdata(out, obj, customSerializer); + else if (obj.is()) + { + if (recursionCounter >= 32) + throw std::runtime_error("Can not serialize more than 32 nested tables. Likely the table contains itself."); + sol::table table = obj; + appendType(out, SerializedType::TABLE_START); + for (auto& [key, value] : table) + { + serialize(out, key, customSerializer, recursionCounter + 1); + serialize(out, value, customSerializer, recursionCounter + 1); + } + appendType(out, SerializedType::TABLE_END); + } + else if (obj.is()) + { + appendType(out, SerializedType::NUMBER); + appendValue(out, obj.as()); + } + else if (obj.is()) + appendString(out, obj.as()); + else if (obj.is()) + { + char v = obj.as() ? 1 : 0; + appendType(out, SerializedType::BOOLEAN); + out.push_back(v); + } else + throw std::runtime_error("Unknown lua type"); + } + + static void deserializeImpl(sol::state& lua, std::string_view& binaryData, const UserdataSerializer* customSerializer) + { + if (binaryData.empty()) + throw std::runtime_error("Unexpected end"); + unsigned char type = binaryData[0]; + binaryData = binaryData.substr(1); + if (type & (CUSTOM_COMPACT_FLAG | CUSTOM_FULL_FLAG)) + { + size_t typeNameSize, dataSize; + if (type & CUSTOM_COMPACT_FLAG) + { // Compact form: 0b1SSSSTTT. SSSS = dataSize, TTT = (typeName size - 1). + typeNameSize = (type & 7) + 1; + dataSize = (type >> 3) & 15; + } + else + { // Full form: 0b01TTTTTT + 32bit dataSize. + typeNameSize = (type & 63) + 1; + dataSize = getValue(binaryData); + } + std::string_view typeName = binaryData.substr(0, typeNameSize); + std::string_view data = binaryData.substr(typeNameSize, dataSize); + binaryData = binaryData.substr(typeNameSize + dataSize); + if (!customSerializer || !customSerializer->deserialize(typeName, data, lua)) + throw std::runtime_error("Unknown type: " + std::string(typeName)); + return; + } + if (type & SHORT_STRING_FLAG) + { + size_t size = type & 0x1f; + sol::stack::push(lua.lua_state(), binaryData.substr(0, size)); + binaryData = binaryData.substr(size); + return; + } + switch (static_cast(type)) + { + case SerializedType::NUMBER: + sol::stack::push(lua.lua_state(), getValue(binaryData)); + return; + case SerializedType::BOOLEAN: + sol::stack::push(lua.lua_state(), getValue(binaryData) != 0); + return; + case SerializedType::LONG_STRING: + { + uint32_t size = getValue(binaryData); + sol::stack::push(lua.lua_state(), binaryData.substr(0, size)); + binaryData = binaryData.substr(size); + return; + } + case SerializedType::TABLE_START: + { + lua_createtable(lua, 0, 0); + while (!binaryData.empty() && binaryData[0] != char(SerializedType::TABLE_END)) + { + deserializeImpl(lua, binaryData, customSerializer); + deserializeImpl(lua, binaryData, customSerializer); + lua_settable(lua, -3); + } + if (binaryData.empty()) + throw std::runtime_error("Unexpected end"); + binaryData = binaryData.substr(1); + return; + } + case SerializedType::TABLE_END: + throw std::runtime_error("Unexpected table end"); + case SerializedType::VEC2: + { + float x = getValue(binaryData); + float y = getValue(binaryData); + sol::stack::push(lua.lua_state(), osg::Vec2f(x, y)); + return; + } + case SerializedType::VEC3: + { + float x = getValue(binaryData); + float y = getValue(binaryData); + float z = getValue(binaryData); + sol::stack::push(lua.lua_state(), osg::Vec3f(x, y, z)); + return; + } + } + throw std::runtime_error("Unknown type: " + std::to_string(type)); + } + + BinaryData serialize(const sol::object& obj, const UserdataSerializer* customSerializer) + { + if (obj == sol::nil) + return ""; + BinaryData res; + res.push_back(FORMAT_VERSION); + serialize(res, obj, customSerializer, 0); + return res; + } + + sol::object deserialize(sol::state& lua, std::string_view binaryData, const UserdataSerializer* customSerializer) + { + if (binaryData.empty()) + return sol::nil; + if (binaryData[0] != FORMAT_VERSION) + throw std::runtime_error("Incorrect version of Lua serialization format: " + + std::to_string(static_cast(binaryData[0]))); + binaryData = binaryData.substr(1); + deserializeImpl(lua, binaryData, customSerializer); + if (!binaryData.empty()) + throw std::runtime_error("Unexpected data after serialized object"); + return sol::stack::pop(lua.lua_state()); + } + +} diff --git a/components/lua/serialization.hpp b/components/lua/serialization.hpp new file mode 100644 index 0000000000..63f93baac8 --- /dev/null +++ b/components/lua/serialization.hpp @@ -0,0 +1,34 @@ +#ifndef COMPONENTS_LUA_SERIALIZATION_H +#define COMPONENTS_LUA_SERIALIZATION_H + +#include + +namespace LuaUtil +{ + + // Note: it can contain \0 + using BinaryData = std::string; + + class UserdataSerializer + { + public: + virtual ~UserdataSerializer() {} + + // Appends serialized sol::userdata to the end of BinaryData. + // Returns false if this type of userdata is not supported by this serializer. + virtual bool serialize(BinaryData&, const sol::userdata&) const = 0; + + // 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. + virtual bool deserialize(std::string_view typeName, std::string_view binaryData, sol::state&) const = 0; + + protected: + static void append(BinaryData&, std::string_view typeName, const void* data, size_t dataSize); + }; + + BinaryData serialize(const sol::object&, const UserdataSerializer* customSerializer = nullptr); + sol::object deserialize(sol::state& lua, std::string_view binaryData, const UserdataSerializer* customSerializer = nullptr); + +} + +#endif // COMPONENTS_LUA_SERIALIZATION_H diff --git a/components/lua/utilpackage.cpp b/components/lua/utilpackage.cpp new file mode 100644 index 0000000000..abcc6d424e --- /dev/null +++ b/components/lua/utilpackage.cpp @@ -0,0 +1,98 @@ +#include "utilpackage.hpp" + +#include +#include + +#include + +#include + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; + + template <> + struct is_automagical : std::false_type {}; +} + +namespace LuaUtil +{ + + sol::table initUtilPackage(sol::state& lua) + { + sol::table util(lua, sol::create); + + // TODO: Add bindings for osg::Matrix + + // Lua bindings for osg::Vec2f + util["vector2"] = [](float x, float y) { return osg::Vec2f(x, y); }; + sol::usertype vec2Type = lua.new_usertype("Vec2"); + vec2Type["x"] = sol::readonly_property([](const osg::Vec2f& v) -> float { return v.x(); } ); + vec2Type["y"] = sol::readonly_property([](const osg::Vec2f& v) -> float { return v.y(); } ); + vec2Type[sol::meta_function::to_string] = [](const osg::Vec2f& v) { + std::stringstream ss; + ss << "(" << v.x() << ", " << v.y() << ")"; + return ss.str(); + }; + vec2Type[sol::meta_function::unary_minus] = [](const osg::Vec2f& a) { return -a; }; + vec2Type[sol::meta_function::addition] = [](const osg::Vec2f& a, const osg::Vec2f& b) { return a + b; }; + vec2Type[sol::meta_function::subtraction] = [](const osg::Vec2f& a, const osg::Vec2f& b) { return a - b; }; + vec2Type[sol::meta_function::equal_to] = [](const osg::Vec2f& a, const osg::Vec2f& b) { return a == b; }; + vec2Type[sol::meta_function::multiplication] = sol::overload( + [](const osg::Vec2f& a, float c) { return a * c; }, + [](const osg::Vec2f& a, const osg::Vec2f& b) { return a * b; }); + vec2Type[sol::meta_function::division] = [](const osg::Vec2f& a, float c) { return a / c; }; + vec2Type["dot"] = [](const osg::Vec2f& a, const osg::Vec2f& b) { return a * b; }; + vec2Type["length"] = &osg::Vec2f::length; + vec2Type["length2"] = &osg::Vec2f::length2; + vec2Type["normalize"] = [](const osg::Vec2f& v) { + float len = v.length(); + if (len == 0) + return std::make_tuple(osg::Vec2f(), 0.f); + else + return std::make_tuple(v * (1.f / len), len); + }; + vec2Type["rotate"] = &Misc::rotateVec2f; + + // Lua bindings for osg::Vec3f + util["vector3"] = [](float x, float y, float z) { return osg::Vec3f(x, y, z); }; + sol::usertype vec3Type = lua.new_usertype("Vec3"); + vec3Type["x"] = sol::readonly_property([](const osg::Vec3f& v) -> float { return v.x(); } ); + vec3Type["y"] = sol::readonly_property([](const osg::Vec3f& v) -> float { return v.y(); } ); + vec3Type["z"] = sol::readonly_property([](const osg::Vec3f& v) -> float { return v.z(); } ); + vec3Type[sol::meta_function::to_string] = [](const osg::Vec3f& v) { + std::stringstream ss; + ss << "(" << v.x() << ", " << v.y() << ", " << v.z() << ")"; + return ss.str(); + }; + vec3Type[sol::meta_function::unary_minus] = [](const osg::Vec3f& a) { return -a; }; + vec3Type[sol::meta_function::addition] = [](const osg::Vec3f& a, const osg::Vec3f& b) { return a + b; }; + vec3Type[sol::meta_function::subtraction] = [](const osg::Vec3f& a, const osg::Vec3f& b) { return a - b; }; + vec3Type[sol::meta_function::equal_to] = [](const osg::Vec3f& a, const osg::Vec3f& b) { return a == b; }; + vec3Type[sol::meta_function::multiplication] = sol::overload( + [](const osg::Vec3f& a, float c) { return a * c; }, + [](const osg::Vec3f& a, const osg::Vec3f& b) { return a * b; }); + vec3Type[sol::meta_function::division] = [](const osg::Vec3f& a, float c) { return a / c; }; + vec3Type[sol::meta_function::involution] = [](const osg::Vec3f& a, const osg::Vec3f& b) { return a ^ b; }; + vec3Type["dot"] = [](const osg::Vec3f& a, const osg::Vec3f& b) { return a * b; }; + vec3Type["cross"] = [](const osg::Vec3f& a, const osg::Vec3f& b) { return a ^ b; }; + vec3Type["length"] = &osg::Vec3f::length; + vec3Type["length2"] = &osg::Vec3f::length2; + vec3Type["normalize"] = [](const osg::Vec3f& v) { + float len = v.length(); + if (len == 0) + return std::make_tuple(osg::Vec3f(), 0.f); + else + return std::make_tuple(v * (1.f / len), len); + }; + + // Utility functions + util["clamp"] = [](float value, float from, float to) { return std::clamp(value, from, to); }; + // NOTE: `util["clamp"] = std::clamp` causes error 'AddressSanitizer: stack-use-after-scope' + util["normalizeAngle"] = &Misc::normalizeAngle; + + return util; + } + +} diff --git a/components/lua/utilpackage.hpp b/components/lua/utilpackage.hpp new file mode 100644 index 0000000000..06996fb96a --- /dev/null +++ b/components/lua/utilpackage.hpp @@ -0,0 +1,13 @@ +#ifndef COMPONENTS_LUA_UTILPACKAGE_H +#define COMPONENTS_LUA_UTILPACKAGE_H + +#include + +namespace LuaUtil +{ + + sol::table initUtilPackage(sol::state&); + +} + +#endif // COMPONENTS_LUA_UTILPACKAGE_H diff --git a/components/queries/luabindings.cpp b/components/queries/luabindings.cpp new file mode 100644 index 0000000000..c830a140f7 --- /dev/null +++ b/components/queries/luabindings.cpp @@ -0,0 +1,118 @@ +#include "luabindings.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; + + template <> + struct is_automagical : std::false_type {}; + + template <> + struct is_automagical : std::false_type {}; +} + +namespace Queries +{ + template + struct CondBuilder + { + Filter operator()(const Field& field, const sol::object& o) + { + FieldValue value; + if (field.type() == typeid(bool) && o.is()) + value = o.as(); + else if (field.type() == typeid(int32_t) && o.is()) + value = o.as(); + else if (field.type() == typeid(int64_t) && o.is()) + value = o.as(); + else if (field.type() == typeid(float) && o.is()) + value = o.as(); + else if (field.type() == typeid(double) && o.is()) + value = o.as(); + else if (field.type() == typeid(std::string) && o.is()) + value = o.as(); + else + throw std::logic_error("Invalid value for field " + field.toString()); + Filter filter; + filter.add({&field, type, value}); + return filter; + } + }; + + void registerQueryBindings(sol::state& lua) + { + sol::usertype field = lua.new_usertype("QueryField"); + sol::usertype filter = lua.new_usertype("QueryFilter"); + sol::usertype query = lua.new_usertype("Query"); + + field[sol::meta_function::to_string] = [](const Field& f) { return f.toString(); }; + field["eq"] = CondBuilder(); + field["neq"] = CondBuilder(); + field["lt"] = CondBuilder(); + field["lte"] = CondBuilder(); + field["gt"] = CondBuilder(); + field["gte"] = CondBuilder(); + field["like"] = CondBuilder(); + + filter[sol::meta_function::to_string] = [](const Filter& filter) { return filter.toString(); }; + filter[sol::meta_function::multiplication] = [](const Filter& a, const Filter& b) + { + Filter res = a; + res.add(b, Operation::AND); + return res; + }; + filter[sol::meta_function::addition] = [](const Filter& a, const Filter& b) + { + Filter res = a; + res.add(b, Operation::OR); + return res; + }; + filter[sol::meta_function::unary_minus] = [](const Filter& a) + { + Filter res = a; + if (!a.mConditions.empty()) + res.mOperations.push_back({Operation::NOT, 0}); + return res; + }; + + query[sol::meta_function::to_string] = [](const Query& q) { return q.toString(); }; + query["where"] = [](const Query& q, const Filter& filter) + { + Query res = q; + res.mFilter.add(filter, Operation::AND); + return res; + }; + query["orderBy"] = [](const Query& q, const Field& field) + { + Query res = q; + res.mOrderBy.push_back({&field, false}); + return res; + }; + query["orderByDesc"] = [](const Query& q, const Field& field) + { + Query res = q; + res.mOrderBy.push_back({&field, true}); + return res; + }; + query["groupBy"] = [](const Query& q, const Field& field) + { + Query res = q; + res.mGroupBy.push_back(&field); + return res; + }; + query["offset"] = [](const Query& q, int64_t offset) + { + Query res = q; + res.mOffset = offset; + return res; + }; + query["limit"] = [](const Query& q, int64_t limit) + { + Query res = q; + res.mLimit = limit; + return res; + }; + } +} + diff --git a/components/queries/luabindings.hpp b/components/queries/luabindings.hpp new file mode 100644 index 0000000000..a23dfa932b --- /dev/null +++ b/components/queries/luabindings.hpp @@ -0,0 +1,8 @@ +#include + +#include "query.hpp" + +namespace Queries +{ + void registerQueryBindings(sol::state& lua); +} diff --git a/components/queries/query.cpp b/components/queries/query.cpp new file mode 100644 index 0000000000..3c7f1517ee --- /dev/null +++ b/components/queries/query.cpp @@ -0,0 +1,185 @@ +#include "query.hpp" + +#include +#include + +namespace Queries +{ + Field::Field(std::vector path, std::type_index type) + : mPath(std::move(path)) + , mType(type) {} + + std::string Field::toString() const + { + std::string result; + for (const std::string& segment : mPath) + { + if (!result.empty()) + result += "."; + result += segment; + } + return result; + } + + std::string toString(const FieldValue& value) + { + return std::visit([](auto&& arg) -> std::string + { + using T = std::decay_t; + if constexpr (std::is_same_v) + { + std::ostringstream oss; + oss << std::quoted(arg); + return oss.str(); + } + else if constexpr (std::is_same_v) + return arg ? "true" : "false"; + else + return std::to_string(arg); + }, value); + } + + std::string Condition::toString() const + { + std::string res; + res += mField->toString(); + switch (mType) + { + case Condition::EQUAL: res += " == "; break; + case Condition::NOT_EQUAL: res += " != "; break; + case Condition::LESSER: res += " < "; break; + case Condition::LESSER_OR_EQUAL: res += " <= "; break; + case Condition::GREATER: res += " > "; break; + case Condition::GREATER_OR_EQUAL: res += " >= "; break; + case Condition::LIKE: res += " LIKE "; break; + } + res += Queries::toString(mValue); + return res; + } + + void Filter::add(const Condition& c, Operation::Type op) + { + mOperations.push_back({Operation::PUSH, mConditions.size()}); + mConditions.push_back(c); + if (mConditions.size() > 1) + mOperations.push_back({op, 0}); + } + + void Filter::add(const Filter& f, Operation::Type op) + { + size_t conditionOffset = mConditions.size(); + size_t operationsBefore = mOperations.size(); + mConditions.insert(mConditions.end(), f.mConditions.begin(), f.mConditions.end()); + mOperations.insert(mOperations.end(), f.mOperations.begin(), f.mOperations.end()); + for (size_t i = operationsBefore; i < mOperations.size(); ++i) + mOperations[i].mConditionIndex += conditionOffset; + if (conditionOffset > 0 && !f.mConditions.empty()) + mOperations.push_back({op, 0}); + } + + std::string Filter::toString() const + { + if(mOperations.empty()) + return ""; + std::vector stack; + auto pop = [&stack](){ auto v = stack.back(); stack.pop_back(); return v; }; + auto push = [&stack](const std::string& s) { stack.push_back(s); }; + for (const Operation& op : mOperations) + { + if(op.mType == Operation::PUSH) + push(mConditions[op.mConditionIndex].toString()); + else if(op.mType == Operation::AND) + { + auto rhs = pop(); + auto lhs = pop(); + std::string res; + res += "("; + res += lhs; + res += ") AND ("; + res += rhs; + res += ")"; + push(res); + } + else if (op.mType == Operation::OR) + { + auto rhs = pop(); + auto lhs = pop(); + std::string res; + res += "("; + res += lhs; + res += ") OR ("; + res += rhs; + res += ")"; + push(res); + } + else if (op.mType == Operation::NOT) + { + std::string res; + res += "NOT ("; + res += pop(); + res += ")"; + push(res); + } + else + throw std::logic_error("Unknown operation type!"); + } + return pop(); + } + + std::string Query::toString() const + { + std::string res; + res += "SELECT "; + res += mQueryType; + + std::string filter = mFilter.toString(); + if(!filter.empty()) + { + res += " WHERE "; + res += filter; + } + + std::string order; + for(const OrderBy& ord : mOrderBy) + { + if(!order.empty()) + order += ", "; + order += ord.mField->toString(); + if(ord.mDescending) + order += " DESC"; + } + if (!order.empty()) + { + res += " ORDER BY "; + res += order; + } + + std::string group; + for (const Field* f: mGroupBy) + { + if (!group.empty()) + group += " ,"; + group += f->toString(); + } + if (!group.empty()) + { + res += " GROUP BY "; + res += group; + } + + if (mLimit != sNoLimit) + { + res += " LIMIT "; + res += std::to_string(mLimit); + } + + if (mOffset != 0) + { + res += " OFFSET "; + res += std::to_string(mOffset); + } + + return res; + } +} + diff --git a/components/queries/query.hpp b/components/queries/query.hpp new file mode 100644 index 0000000000..45144fed62 --- /dev/null +++ b/components/queries/query.hpp @@ -0,0 +1,99 @@ +#ifndef COMPONENTS_QUERIES_QUERY +#define COMPONENTS_QUERIES_QUERY + +#include +#include +#include +#include +#include + +namespace Queries +{ + class Field + { + public: + Field(std::vector path, std::type_index type); + + const std::vector& path() const { return mPath; } + const std::type_index type() const { return mType; } + + std::string toString() const; + + private: + std::vector mPath; + std::type_index mType; + }; + + struct OrderBy + { + const Field* mField; + bool mDescending; + }; + + using FieldValue = std::variant; + std::string toString(const FieldValue& value); + + struct Condition + { + enum Type + { + EQUAL = 0, + NOT_EQUAL = 1, + GREATER = 2, + GREATER_OR_EQUAL = 3, + LESSER = 4, + LESSER_OR_EQUAL = 5, + LIKE = 6, + }; + + std::string toString() const; + + const Field* mField; + Type mType; + FieldValue mValue; + }; + + struct Operation + { + enum Type + { + PUSH = 0, // push condition on stack + NOT = 1, // invert top condition on stack + AND = 2, // logic AND for two top conditions + OR = 3, // logic OR for two top conditions + }; + + Type mType; + size_t mConditionIndex; // used only if mType == PUSH + }; + + struct Filter + { + std::string toString() const; + + // combines with given condition or filter using operation `AND` or `OR`. + void add(const Condition& c, Operation::Type op = Operation::AND); + void add(const Filter& f, Operation::Type op = Operation::AND); + + std::vector mConditions; + std::vector mOperations; // operations on conditions in reverse polish notation + }; + + struct Query + { + static constexpr int64_t sNoLimit = -1; + + Query(std::string type) : mQueryType(std::move(type)) {} + std::string toString() const; + + std::string mQueryType; + Filter mFilter; + std::vector mOrderBy; + std::vector mGroupBy; + int64_t mOffset = 0; + int64_t mLimit = sNoLimit; + }; +} + +#endif // !COMPONENTS_QUERIES_QUERY + diff --git a/docs/source/_static/luadoc.css b/docs/source/_static/luadoc.css new file mode 100644 index 0000000000..aa83013def --- /dev/null +++ b/docs/source/_static/luadoc.css @@ -0,0 +1,113 @@ +#luadoc tt { font-family: monospace; } + +#luadoc p, +#luadoc td, +#luadoc th { font-size: .95em; line-height: 1.2em;} + +#luadoc p, +#luadoc ul +{ margin: 10px 0 0 10px;} + +#luadoc strong { font-weight: bold;} + +#luadoc em { font-style: italic;} + +#luadoc h1 { + font-size: 1.5em; + margin: 25px 0 20px 0; +} +#luadoc h2, +#luadoc h3, +#luadoc h4 { margin: 15px 0 10px 0; } +#luadoc h2 { font-size: 1.25em; } +#luadoc h3 { font-size: 1.15em; } +#luadoc h4 { font-size: 1.06em; } + +#luadoc hr { + color:#cccccc; + background: #00007f; + height: 1px; +} + +#luadoc blockquote { margin-left: 3em; } + +#luadoc ul { list-style-type: disc; } + +#luadoc p.name { + font-family: "Andale Mono", monospace; + padding-top: 1em; +} + +#luadoc p:first-child { + margin-top: 0px; +} + +#luadoc table.function_list { + border-width: 1px; + border-style: solid; + border-color: #cccccc; + border-collapse: collapse; +} +#luadoc table.function_list td { + border-width: 1px; + padding: 3px; + border-style: solid; + border-color: #cccccc; +} + +#luadoc table.function_list td.name { background-color: #f0f0f0; } +#luadoc table.function_list td.summary { width: 100%; } + +#luadoc dl.table dt, +#luadoc dl.function dt {border-top: 1px solid #ccc; padding-top: 1em;} +#luadoc dl.table dd, +#luadoc dl.function dd {padding-bottom: 1em; margin: 10px 0 0 20px;} +#luadoc dl.table h3, +#luadoc dl.function h3 {font-size: .95em;} + + + +#luadoc pre.example { + background-color: #eeffcc; + border: 1px solid #e1e4e5; + padding: 10px; + margin: 10px 0 10px 0; + overflow-x: auto; +} + +#luadoc code { + background-color: inherit; + color: inherit; + border: none; + font-family: monospace; +} + +#luadoc pre.example code { + color: #404040; + background-color: #eeffcc; + border: none; + white-space: pre; + padding: 0px; +} + +#luadoc dt { + background: inherit; + color: inherit; + width: 100%; + padding: 0px; +} + +#luadoc a:not(:link) { + font-weight: bold; + color: #000; + text-decoration: none; + cursor: inherit; +} +#luadoc a:link { font-weight: bold; color: #004080; text-decoration: none; } +#luadoc a:visited { font-weight: bold; color: #006699; text-decoration: none; } +#luadoc a:link:hover { text-decoration: underline; } + +#luadoc dl, +#luadoc dd {margin: 0px; line-height: 1.2em;} +#luadoc li {list-style: bullet;} + diff --git a/docs/source/conf.py b/docs/source/conf.py index 7653b94edf..7f2affbb75 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -13,6 +13,7 @@ # serve to show the default. import os import sys +import subprocess # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -148,7 +149,11 @@ html_theme = 'sphinx_rtd_theme' def setup(app): app.add_stylesheet('figures.css') - + app.add_stylesheet('luadoc.css') + try: + subprocess.call(project_root + '/docs/source/generate_luadoc.sh') + except Exception as e: + print('Can\'t generate Lua API documentation:', e) # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". diff --git a/docs/source/generate_luadoc.sh b/docs/source/generate_luadoc.sh new file mode 100755 index 0000000000..7a238eca5a --- /dev/null +++ b/docs/source/generate_luadoc.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# How to install openmwluadocumentor: + +# sudo apt install luarocks +# git clone https://gitlab.com/ptmikheev/openmw-luadocumentor.git +# cd openmw-luadocumentor/luarocks +# luarocks --local pack openmwluadocumentor-0.1.1-1.rockspec +# luarocks --local install openmwluadocumentor-0.1.1-1.src.rock + +if [ -f /.dockerenv ]; then + # We are inside readthedocs pipeline + echo "Install lua 5.1" + cd ~ + curl -R -O https://www.lua.org/ftp/lua-5.1.5.tar.gz + tar -zxf lua-5.1.5.tar.gz + cd lua-5.1.5/ + make linux + PATH=$PATH:~/lua-5.1.5/src + + echo "Install luarocks" + cd ~ + wget https://luarocks.org/releases/luarocks-2.4.2.tar.gz + tar zxpf luarocks-2.4.2.tar.gz + cd luarocks-2.4.2/ + ./configure --with-lua-bin=$HOME/lua-5.1.5/src --with-lua-include=$HOME/lua-5.1.5/src --prefix=$HOME/luarocks + make build + make install + PATH=$PATH:~/luarocks/bin + + echo "Install openmwluadocumentor" + cd ~ + git clone https://gitlab.com/ptmikheev/openmw-luadocumentor.git + cd openmw-luadocumentor/luarocks + luarocks --local install checks + luarocks --local pack openmwluadocumentor-0.1.1-1.rockspec + luarocks --local install openmwluadocumentor-0.1.1-1.src.rock +fi + +DOCS_SOURCE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +FILES_DIR=$DOCS_SOURCE_DIR/../../files +OUTPUT_DIR=$DOCS_SOURCE_DIR/reference/lua-scripting/generated_html + +rm -f $OUTPUT_DIR/*.html + +cd $FILES_DIR/lua_api +~/.luarocks/bin/openmwluadocumentor -f doc -d $OUTPUT_DIR openmw/*lua + +cd $FILES_DIR/builtin_scripts +~/.luarocks/bin/openmwluadocumentor -f doc -d $OUTPUT_DIR openmw_aux/*lua + diff --git a/docs/source/reference/index.rst b/docs/source/reference/index.rst index cd947745e1..aa6ff1d96f 100644 --- a/docs/source/reference/index.rst +++ b/docs/source/reference/index.rst @@ -6,4 +6,5 @@ Reference Material :maxdepth: 2 modding/index - documentationHowTo \ No newline at end of file + lua-scripting/index + documentationHowTo diff --git a/docs/source/reference/lua-scripting/api.rst b/docs/source/reference/lua-scripting/api.rst new file mode 100644 index 0000000000..3a18f2445a --- /dev/null +++ b/docs/source/reference/lua-scripting/api.rst @@ -0,0 +1,67 @@ +################# +Lua API reference +################# + +.. toctree:: + :hidden: + + engine_handlers + openmw_util + openmw_core + openmw_async + openmw_query + openmw_world + openmw_self + openmw_nearby + openmw_ui + openmw_aux_util + + +- :ref:`Engine handlers reference` +- `Game object reference `_ +- `Cell reference `_ + +**API packages** + +API packages provide functions that can be called by scripts. I.e. it is a script-to-engine interaction. +A package can be loaded with ``require('')``. +It can not be overloaded even if there is a lua file with the same name. +The list of available packages is different for global and for local scripts. +Player scripts are local scripts that are attached to a player. + ++---------------------------------------------------------+--------------------+---------------------------------------------------------------+ +| Package | Can be used | Description | ++=========================================================+====================+===============================================================+ +|:ref:`openmw.interfaces