Merge branch 'lua' into 'master'

Lua

Closes #5990

See merge request OpenMW/openmw!430
dont-compose-content
psi29a 4 years ago
commit 61f3c528d2

1
.gitignore vendored

@ -72,6 +72,7 @@ components/ui_contentselector.h
docs/mainpage.hpp docs/mainpage.hpp
docs/Doxyfile docs/Doxyfile
docs/DoxyfilePages docs/DoxyfilePages
docs/source/reference/lua-scripting/generated_html
moc_*.cxx moc_*.cxx
*.cxx_parameters *.cxx_parameters
*qrc_launcher.cxx *qrc_launcher.cxx

@ -21,6 +21,7 @@
Bug #6143: Capturing a screenshot makes engine to be a temporary unresponsive 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 #2780: A way to see current OpenMW version in the console
Feature #5489: MCP: Telekinesis fix for activators Feature #5489: MCP: Telekinesis fix for activators
Feature #5996: Support Lua scripts in OpenMW
Feature #6017: Separate persistent and temporary cell references when saving Feature #6017: Separate persistent and temporary cell references when saving
0.47.0 0.47.0

@ -1,4 +1,4 @@
#!/bin/sh -ex #!/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 unzip -o ~/openmw-android-deps -d /usr/lib/android-sdk/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr > /dev/null

@ -15,8 +15,10 @@ ccache --version
cmake --version cmake --version
qmake --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 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 unzip -o ~/openmw-deps.zip -d /private/tmp/openmw-deps > /dev/null
# additional libraries # additional libraries
[ -z "${TRAVIS}" ] && HOMEBREW_NO_AUTO_UPDATE=1 brew install fontconfig [ -z "${TRAVIS}" ] && HOMEBREW_NO_AUTO_UPDATE=1 brew install fontconfig

@ -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" \ "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/lz4_win${BITS}_v1_9_2.7z" \
"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 # Google test and mock
if [ ! -z $TEST_FRAMEWORK ]; then if [ ! -z $TEST_FRAMEWORK ]; then
echo "Google test 1.10.0..." echo "Google test 1.10.0..."
@ -934,6 +939,25 @@ printf "LZ4 1.9.2... "
} }
cd $DEPS cd $DEPS
echo 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 # Google Test and Google Mock
if [ ! -z $TEST_FRAMEWORK ]; then if [ ! -z $TEST_FRAMEWORK ]; then
printf "Google test 1.10.0 ..." printf "Google test 1.10.0 ..."

@ -25,5 +25,6 @@ cmake \
-D BUILD_BSATOOL=TRUE \ -D BUILD_BSATOOL=TRUE \
-D BUILD_ESSIMPORTER=TRUE \ -D BUILD_ESSIMPORTER=TRUE \
-D BUILD_NIFTEST=TRUE \ -D BUILD_NIFTEST=TRUE \
-D USE_LUAJIT=FALSE \
-G"Unix Makefiles" \ -G"Unix Makefiles" \
.. ..

@ -18,10 +18,10 @@ declare -rA GROUPED_DEPS=(
libboost-filesystem-dev libboost-program-options-dev libboost-filesystem-dev libboost-program-options-dev
libboost-system-dev libboost-iostreams-dev libboost-system-dev libboost-iostreams-dev
libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev
libsdl2-dev libqt5opengl5-dev libopenal-dev libunshield-dev libtinyxml-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 ca-certificates
" "
# TODO: add librecastnavigation-dev when debian is ready # TODO: add librecastnavigation-dev when debian is ready

@ -394,6 +394,22 @@ endif()
find_package(SDL2 2.0.9 REQUIRED) find_package(SDL2 2.0.9 REQUIRED)
find_package(OpenAL 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( include_directories(
BEFORE SYSTEM BEFORE SYSTEM
"." "."
@ -403,6 +419,8 @@ include_directories(
${OPENAL_INCLUDE_DIR} ${OPENAL_INCLUDE_DIR}
${OPENGL_INCLUDE_DIR} ${OPENGL_INCLUDE_DIR}
${BULLET_INCLUDE_DIRS} ${BULLET_INCLUDE_DIRS}
${LUA_INCLUDE_DIR}
${SOL_INCLUDE_DIRS}
) )
link_directories(${SDL2_LIBRARY_DIRS} ${Boost_LIBRARY_DIRS}) link_directories(${SDL2_LIBRARY_DIRS} ${Boost_LIBRARY_DIRS})
@ -661,11 +679,10 @@ if (WIN32)
endif() endif()
if (BUILD_OPENMW) if (BUILD_OPENMW)
if (OPENMW_UNITY_BUILD) # \bigobj is required:
set_target_properties(openmw PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD} /bigobj") # 1) for OPENMW_UNITY_BUILD;
else() # 2) to compile lua binginds, because sol3 is heavily templated.
set_target_properties(openmw PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") set_target_properties(openmw PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD} /bigobj")
endif()
endif() endif()
if (BUILD_WIZARD) if (BUILD_WIZARD)
@ -682,6 +699,11 @@ if (WIN32)
#set_target_properties(openmw PROPERTIES LINK_FLAGS_MINSIZEREL "/SUBSYSTEM:WINDOWS") #set_target_properties(openmw PROPERTIES LINK_FLAGS_MINSIZEREL "/SUBSYSTEM:WINDOWS")
endif() 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 # Apple bundling
if (OPENMW_OSX_DEPLOYMENT AND APPLE) if (OPENMW_OSX_DEPLOYMENT AND APPLE)
if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.13 AND CMAKE_VERSION VERSION_LESS 3.13.4) if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.13 AND CMAKE_VERSION VERSION_LESS 3.13.4)

@ -55,6 +55,11 @@ add_openmw_dir (mwscript
animationextensions transformationextensions consoleextensions userextensions 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 add_openmw_dir (mwsound
soundmanagerimp openal_output ffmpeg_decoder sound sound_buffer sound_decoder sound_output soundmanagerimp openal_output ffmpeg_decoder sound sound_buffer sound_decoder sound_output
loudness movieaudiofactory alext efx efx-presets regionsoundselector watersoundupdater volumesettings loudness movieaudiofactory alext efx efx-presets regionsoundselector watersoundupdater volumesettings
@ -146,6 +151,7 @@ target_link_libraries(openmw
"osg-ffmpeg-videoplayer" "osg-ffmpeg-videoplayer"
"oics" "oics"
components components
${LUA_LIBRARIES}
) )
if(OSG_STATIC) if(OSG_STATIC)

@ -1,5 +1,6 @@
#include "engine.hpp" #include "engine.hpp"
#include <condition_variable>
#include <iomanip> #include <iomanip>
#include <fstream> #include <fstream>
#include <chrono> #include <chrono>
@ -46,6 +47,8 @@
#include "mwgui/windowmanagerimp.hpp" #include "mwgui/windowmanagerimp.hpp"
#include "mwlua/luamanagerimp.hpp"
#include "mwscript/scriptmanagerimp.hpp" #include "mwscript/scriptmanagerimp.hpp"
#include "mwscript/interpretercontext.hpp" #include "mwscript/interpretercontext.hpp"
@ -101,6 +104,7 @@ namespace
PhysicsWorker, PhysicsWorker,
World, World,
Gui, Gui,
Lua,
Number, Number,
}; };
@ -138,6 +142,9 @@ namespace
template <> template <>
const UserStats UserStatsValue<UserStatsType::Gui>::sValue {"Gui", "gui"}; const UserStats UserStatsValue<UserStatsType::Gui>::sValue {"Gui", "gui"};
template <>
const UserStats UserStatsValue<UserStatsType::Lua>::sValue {"Lua", "lua"};
template <UserStatsType type> template <UserStatsType type>
struct ForEachUserStatsValue struct ForEachUserStatsValue
{ {
@ -486,6 +493,11 @@ void OMW::Engine::addGroundcoverFile(const std::string& file)
mGroundcoverFiles.emplace_back(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) void OMW::Engine::setSkipMenu (bool skipMenu, bool newGame)
{ {
mSkipMenu = skipMenu; mSkipMenu = skipMenu;
@ -700,6 +712,9 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings)
mViewer->addEventHandler(mScreenCaptureHandler); 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 // Create input and UI first to set up a bootstrapping environment for
// showing a loading screen and keeping the window responsive while doing so // 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<double> (result.second)/result.first << 100*static_cast<double> (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<std::mutex> lk(mMutex);
mUpdateRequest = true;
}
mCV.notify_one();
}
void finishUpdate()
{
if (mThread)
{
std::unique_lock<std::mutex> 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<UserStatsType::Lua> 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<std::mutex> 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<std::thread> mThread;
};
// Initialise and enter main loop. // Initialise and enter main loop.
void OMW::Engine::go() void OMW::Engine::go()
{ {
@ -895,6 +987,8 @@ void OMW::Engine::go()
mEnvironment.getWindowManager()->executeInConsole(mStartupScript); mEnvironment.getWindowManager()->executeInConsole(mStartupScript);
} }
LuaWorker luaWorker(this); // starts a separate lua thread if "lua num threads" > 0
// Start the main rendering loop // Start the main rendering loop
double simulationTime = 0.0; double simulationTime = 0.0;
Misc::FrameRateLimiter frameRateLimiter = Misc::makeFrameRateLimiter(mEnvironment.getFrameRateLimit()); Misc::FrameRateLimiter frameRateLimiter = Misc::makeFrameRateLimiter(mEnvironment.getFrameRateLimit());
@ -920,8 +1014,12 @@ void OMW::Engine::go()
mEnvironment.getWorld()->updateWindowManager(); mEnvironment.getWorld()->updateWindowManager();
luaWorker.allowUpdate(dt); // if there is a separate Lua thread, it starts the update now
mViewer->renderingTraversals(); mViewer->renderingTraversals();
luaWorker.finishUpdate();
bool guiActive = mEnvironment.getWindowManager()->isGuiMode(); bool guiActive = mEnvironment.getWindowManager()->isGuiMode();
if (!guiActive) if (!guiActive)
simulationTime += dt; simulationTime += dt;
@ -943,6 +1041,8 @@ void OMW::Engine::go()
frameRateLimiter.limit(); frameRateLimiter.limit();
} }
luaWorker.join();
// Save user settings // Save user settings
settings.saveUser(settingspath); settings.saveUser(settingspath);

@ -33,6 +33,11 @@ namespace Compiler
class Context; class Context;
} }
namespace MWLua
{
class LuaManager;
}
namespace Files namespace Files
{ {
struct ConfigurationManager; struct ConfigurationManager;
@ -66,6 +71,7 @@ namespace OMW
std::string mCellName; std::string mCellName;
std::vector<std::string> mContentFiles; std::vector<std::string> mContentFiles;
std::vector<std::string> mGroundcoverFiles; std::vector<std::string> mGroundcoverFiles;
std::vector<std::string> mLuaScriptListFiles;
bool mSkipMenu; bool mSkipMenu;
bool mUseSound; bool mUseSound;
bool mCompileAll; bool mCompileAll;
@ -85,6 +91,8 @@ namespace OMW
Compiler::Extensions mExtensions; Compiler::Extensions mExtensions;
Compiler::Context *mScriptContext; Compiler::Context *mScriptContext;
MWLua::LuaManager* mLuaManager;
Files::Collections mFileCollections; Files::Collections mFileCollections;
bool mFSStrict; bool mFSStrict;
Translation::Storage mTranslationDataStorage; Translation::Storage mTranslationDataStorage;
@ -137,6 +145,7 @@ namespace OMW
*/ */
void addContentFile(const std::string& file); void addContentFile(const std::string& file);
void addGroundcoverFile(const std::string& file); void addGroundcoverFile(const std::string& file);
void addLuaScriptListFile(const std::string& file);
/// Disable or enable all sounds /// Disable or enable all sounds
void setSoundUsage(bool soundUsage); void setSoundUsage(bool soundUsage);
@ -185,6 +194,7 @@ namespace OMW
private: private:
Files::ConfigurationManager& mCfgMgr; Files::ConfigurationManager& mCfgMgr;
class LuaWorker;
}; };
} }

@ -65,6 +65,9 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat
("groundcover", bpo::value<Files::EscapeStringVector>()->default_value(Files::EscapeStringVector(), "") ("groundcover", bpo::value<Files::EscapeStringVector>()->default_value(Files::EscapeStringVector(), "")
->multitoken()->composing(), "groundcover content file(s): esm/esp, or omwgame/omwaddon") ->multitoken()->composing(), "groundcover content file(s): esm/esp, or omwgame/omwaddon")
("lua-scripts", bpo::value<Files::EscapeStringVector>()->default_value(Files::EscapeStringVector(), "")
->multitoken()->composing(), "file(s) with a list of global Lua scripts: omwscripts")
("no-sound", bpo::value<bool>()->implicit_value(true) ("no-sound", bpo::value<bool>()->implicit_value(true)
->default_value(false), "disable all sounds") ->default_value(false), "disable all sounds")
@ -204,6 +207,10 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat
engine.addGroundcoverFile(file); engine.addGroundcoverFile(file);
} }
StringsVector luaScriptLists = variables["lua-scripts"].as<Files::EscapeStringVector>().toStdStringVector();
for (const auto& file : luaScriptLists)
engine.addLuaScriptListFile(file);
// startup-settings // startup-settings
engine.setCell(variables["start"].as<Files::EscapeHashString>().toStdString()); engine.setCell(variables["start"].as<Files::EscapeHashString>().toStdString());
engine.setSkipMenu (variables["skip-menu"].as<bool>(), variables["new-game"].as<bool>()); engine.setSkipMenu (variables["skip-menu"].as<bool>(), variables["new-game"].as<bool>());

@ -13,13 +13,14 @@
#include "inputmanager.hpp" #include "inputmanager.hpp"
#include "windowmanager.hpp" #include "windowmanager.hpp"
#include "statemanager.hpp" #include "statemanager.hpp"
#include "luamanager.hpp"
MWBase::Environment *MWBase::Environment::sThis = nullptr; MWBase::Environment *MWBase::Environment::sThis = nullptr;
MWBase::Environment::Environment() MWBase::Environment::Environment()
: mWorld (nullptr), mSoundManager (nullptr), mScriptManager (nullptr), mWindowManager (nullptr), : mWorld (nullptr), mSoundManager (nullptr), mScriptManager (nullptr), mWindowManager (nullptr),
mMechanicsManager (nullptr), mDialogueManager (nullptr), mJournal (nullptr), mInputManager (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); assert (!sThis);
sThis = this; sThis = this;
@ -76,6 +77,11 @@ void MWBase::Environment::setStateManager (StateManager *stateManager)
mStateManager = stateManager; mStateManager = stateManager;
} }
void MWBase::Environment::setLuaManager (LuaManager *luaManager)
{
mLuaManager = luaManager;
}
void MWBase::Environment::setResourceSystem (Resource::ResourceSystem *resourceSystem) void MWBase::Environment::setResourceSystem (Resource::ResourceSystem *resourceSystem)
{ {
mResourceSystem = resourceSystem; mResourceSystem = resourceSystem;
@ -150,6 +156,12 @@ MWBase::StateManager *MWBase::Environment::getStateManager() const
return mStateManager; return mStateManager;
} }
MWBase::LuaManager *MWBase::Environment::getLuaManager() const
{
assert (mLuaManager);
return mLuaManager;
}
Resource::ResourceSystem *MWBase::Environment::getResourceSystem() const Resource::ResourceSystem *MWBase::Environment::getResourceSystem() const
{ {
return mResourceSystem; return mResourceSystem;
@ -188,6 +200,9 @@ void MWBase::Environment::cleanup()
delete mStateManager; delete mStateManager;
mStateManager = nullptr; mStateManager = nullptr;
delete mLuaManager;
mLuaManager = nullptr;
} }
const MWBase::Environment& MWBase::Environment::get() const MWBase::Environment& MWBase::Environment::get()

@ -22,6 +22,7 @@ namespace MWBase
class InputManager; class InputManager;
class WindowManager; class WindowManager;
class StateManager; class StateManager;
class LuaManager;
/// \brief Central hub for mw-subsystems /// \brief Central hub for mw-subsystems
/// ///
@ -42,6 +43,7 @@ namespace MWBase
Journal *mJournal; Journal *mJournal;
InputManager *mInputManager; InputManager *mInputManager;
StateManager *mStateManager; StateManager *mStateManager;
LuaManager *mLuaManager;
Resource::ResourceSystem *mResourceSystem; Resource::ResourceSystem *mResourceSystem;
float mFrameDuration; float mFrameDuration;
float mFrameRateLimit; float mFrameRateLimit;
@ -76,6 +78,8 @@ namespace MWBase
void setStateManager (StateManager *stateManager); void setStateManager (StateManager *stateManager);
void setLuaManager (LuaManager *luaManager);
void setResourceSystem (Resource::ResourceSystem *resourceSystem); void setResourceSystem (Resource::ResourceSystem *resourceSystem);
void setFrameDuration (float duration); void setFrameDuration (float duration);
@ -102,6 +106,8 @@ namespace MWBase
StateManager *getStateManager() const; StateManager *getStateManager() const;
LuaManager *getLuaManager() const;
Resource::ResourceSystem *getResourceSystem() const; Resource::ResourceSystem *getResourceSystem() const;
float getFrameDuration() const; float getFrameDuration() const;

@ -0,0 +1,78 @@
#ifndef GAME_MWBASE_LUAMANAGER_H
#define GAME_MWBASE_LUAMANAGER_H
#include <SDL_events.h>
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<int, int>&) = 0;
// Drops script cache and reloads all scripts. Calls `onSave` and `onLoad` for every script.
virtual void reloadAllScripts() = 0;
};
}
#endif // GAME_MWBASE_LUAMANAGER_H

@ -129,6 +129,8 @@ namespace MWBase
virtual MWWorld::CellStore *getCell (const ESM::CellId& id) = 0; virtual MWWorld::CellStore *getCell (const ESM::CellId& id) = 0;
virtual bool isCellActive(MWWorld::CellStore* cell) const = 0;
virtual void testExteriorCells() = 0; virtual void testExteriorCells() = 0;
virtual void testInteriorCells() = 0; virtual void testInteriorCells() = 0;

@ -17,6 +17,7 @@
#include "../mwbase/windowmanager.hpp" #include "../mwbase/windowmanager.hpp"
#include "../mwbase/dialoguemanager.hpp" #include "../mwbase/dialoguemanager.hpp"
#include "../mwbase/soundmanager.hpp" #include "../mwbase/soundmanager.hpp"
#include "../mwbase/luamanager.hpp"
#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/creaturestats.hpp"
#include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/npcstats.hpp"
@ -1095,6 +1096,7 @@ namespace MWClass
bool Npc::apply (const MWWorld::Ptr& ptr, const std::string& id, bool Npc::apply (const MWWorld::Ptr& ptr, const std::string& id,
const MWWorld::Ptr& actor) const const MWWorld::Ptr& actor) const
{ {
MWBase::Environment::get().getLuaManager()->appliedToObject(ptr, id, actor);
MWMechanics::CastSpell cast(ptr, ptr); MWMechanics::CastSpell cast(ptr, ptr);
return cast.cast(id); return cast.cast(id);
} }

@ -20,6 +20,7 @@
#include "../mwbase/scriptmanager.hpp" #include "../mwbase/scriptmanager.hpp"
#include "../mwbase/windowmanager.hpp" #include "../mwbase/windowmanager.hpp"
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
#include "../mwbase/luamanager.hpp"
#include "../mwworld/esmstore.hpp" #include "../mwworld/esmstore.hpp"
#include "../mwworld/class.hpp" #include "../mwworld/class.hpp"

@ -6,6 +6,7 @@
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/inputmanager.hpp" #include "../mwbase/inputmanager.hpp"
#include "../mwbase/luamanager.hpp"
#include "../mwbase/windowmanager.hpp" #include "../mwbase/windowmanager.hpp"
#include "../mwworld/player.hpp" #include "../mwworld/player.hpp"
@ -58,6 +59,9 @@ namespace MWInput
if (!input->controlsDisabled() && !consumed) if (!input->controlsDisabled() && !consumed)
mBindingsManager->keyPressed(arg); mBindingsManager->keyPressed(arg);
if (!consumed)
MWBase::Environment::get().getLuaManager()->keyPressed(arg);
input->setJoystickLastUsed(false); input->setJoystickLastUsed(false);
} }

@ -0,0 +1,124 @@
#include "actions.hpp"
#include <components/debug/debuglog.hpp>
#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<bool, MWWorld::InventoryStore::Slots> 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<ObjectId>(item))
{
itemPtr = worldView.getObjectRegistry()->getPtr(std::get<ObjectId>(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<const MWWorld::ContainerStore*>(&store))
{
Log(Debug::Warning) << "Object" << idToString(std::get<ObjectId>(item)) << " is not in inventory";
return false;
}
}
else
{
const std::string& recordId = std::get<std::string>(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);
}
}

@ -0,0 +1,55 @@
#ifndef MWLUA_ACTIONS_H
#define MWLUA_ACTIONS_H
#include <variant>
#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<std::string, ObjectId>; // recordId or ObjectId
using Equipment = std::map<int, Item>; // 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

@ -0,0 +1,60 @@
#include "luabindings.hpp"
namespace sol
{
template <>
struct is_automagical<MWLua::AsyncPackageId> : 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<AsyncPackageId> api = context.mLua->sol().new_usertype<AsyncPackageId>("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);
}
}

@ -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);
}
}

@ -0,0 +1,62 @@
#include "luabindings.hpp"
#include <components/esm/loadcell.hpp>
#include "../mwworld/cellstore.hpp"
namespace MWLua
{
template <class CellT, class ObjectT>
static void initCellBindings(const std::string& prefix, const Context& context)
{
sol::usertype<CellT> cellT = context.mLua->sol().new_usertype<CellT>(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<CellT, GCell>)
{ // 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<LCell, LObject>("L", context);
}
void initCellBindingsForGlobalScripts(const Context& context)
{
initCellBindings<GCell, GObject>("G", context);
}
}

@ -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

@ -0,0 +1,63 @@
#include "eventqueue.hpp"
#include <components/debug/debuglog.hpp>
#include <components/esm/esmreader.hpp>
#include <components/esm/esmwriter.hpp>
#include <components/esm/luascripts.hpp>
#include <components/lua/serialization.hpp>
namespace MWLua
{
template <typename Event>
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<int, int>& 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);
}
}

@ -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<GlobalEvent>;
using LocalEventQueue = std::vector<LocalEvent>;
void loadEvents(sol::state& lua, ESM::ESMReader& esm, GlobalEventQueue&, LocalEventQueue&,
const std::map<int, int>& contentFileMapping, const LuaUtil::UserdataSerializer* serializer);
void saveEvents(ESM::ESMWriter& esm, const GlobalEventQueue&, const LocalEventQueue&);
}
#endif // MWLUA_EVENTQUEUE_H

@ -0,0 +1,36 @@
#ifndef MWLUA_GLOBALSCRIPTS_H
#define MWLUA_GLOBALSCRIPTS_H
#include <memory>
#include <set>
#include <string>
#include <components/lua/luastate.hpp>
#include <components/lua/scriptscontainer.hpp>
#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

@ -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<MWBase::LuaManager::ActorControls> : std::false_type {};
template <>
struct is_automagical<MWLua::LocalScripts::SelfObject> : std::false_type {};
}
namespace MWLua
{
void LocalScripts::initializeSelfPackage(const Context& context)
{
using ActorControls = MWBase::LuaManager::ActorControls;
sol::usertype<ActorControls> controls = context.mLua->sol().new_usertype<ActorControls>("ActorControls");
controls["movement"] = &ActorControls::mMovement;
controls["sideMovement"] = &ActorControls::mSideMovement;
controls["turn"] = &ActorControls::mTurn;
controls["run"] = &ActorControls::mRun;
controls["jump"] = &ActorControls::mJump;
sol::usertype<SelfObject> selfAPI =
context.mLua->sol().new_usertype<SelfObject>("SelfObject", sol::base_classes, sol::bases<LObject>());
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<int>();
if (value.is<LObject>())
eqp[slot] = value.as<LObject>().id();
else
eqp[slot] = value.as<std::string>();
}
manager->addAction(std::make_unique<SetEquipmentAction>(obj.id(), std::move(eqp)));
};
selfAPI["getCombatTarget"] = [worldView=context.mWorldView](SelfObject& self) -> sol::optional<LObject>
{
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<decltype(arg)>;
if constexpr (std::is_same_v<EventT, OnActive>)
{
mData.mIsActive = true;
callEngineHandlers(mOnActiveHandlers);
}
else if constexpr (std::is_same_v<EventT, OnInactive>)
{
mData.mIsActive = false;
callEngineHandlers(mOnInactiveHandlers);
}
else
{
static_assert(std::is_same_v<EventT, OnConsume>);
callEngineHandlers(mOnConsumeHandlers, arg.mRecordId);
}
}, event);
}
}

@ -0,0 +1,55 @@
#ifndef MWLUA_LOCALSCRIPTS_H
#define MWLUA_LOCALSCRIPTS_H
#include <memory>
#include <set>
#include <string>
#include <components/lua/luastate.hpp>
#include <components/lua/scriptscontainer.hpp>
#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<OnActive, OnInactive, OnConsume>;
void receiveEngineEvent(const EngineEvent&, ObjectRegistry*);
protected:
SelfObject mData;
private:
EngineHandlerList mOnActiveHandlers{"onActive"};
EngineHandlerList mOnInactiveHandlers{"onInactive"};
EngineHandlerList mOnConsumeHandlers{"onConsume"};
};
}
#endif // MWLUA_LOCALSCRIPTS_H

@ -0,0 +1,187 @@
#include "luabindings.hpp"
#include <SDL_events.h>
#include <components/lua/luastate.hpp>
#include <components/queries/luabindings.hpp>
#include "../mwworld/inventorystore.hpp"
#include "eventqueue.hpp"
#include "worldview.hpp"
namespace sol
{
template <>
struct is_automagical<SDL_Keysym> : std::false_type {};
}
namespace MWLua
{
static sol::table definitionList(LuaUtil::LuaState& lua, std::initializer_list<std::string> 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<GCell>
{
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<GCell>
{
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<SDL_Keysym> keyEvent = context.mLua->sol().new_usertype<SDL_Keysym>("KeyEvent");
keyEvent["symbol"] = sol::readonly_property([](const SDL_Keysym& e) { return std::string(1, static_cast<char>(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; });
}
}

@ -0,0 +1,65 @@
#ifndef MWLUA_LUABINDINGS_H
#define MWLUA_LUABINDINGS_H
#include <components/lua/luastate.hpp>
#include <components/lua/serialization.hpp>
#include <components/lua/scriptscontainer.hpp>
#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

@ -0,0 +1,383 @@
#include "luamanagerimp.hpp"
#include <components/debug/debuglog.hpp>
#include <components/esm/esmreader.hpp>
#include <components/esm/esmwriter.hpp>
#include <components/esm/luascripts.hpp>
#include <components/lua/utilpackage.hpp>
#include <components/lua/omwscriptsparser.hpp>
#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<std::string>& 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<GlobalEvent> globalEvents = std::move(mGlobalEvents);
std::vector<LocalEvent> localEvents = std::move(mLocalEvents);
mGlobalEvents = std::vector<GlobalEvent>();
mLocalEvents = std::vector<LocalEvent>();
{ // 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<PlayerScripts*>(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>& 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<LocalScripts> 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<PlayerScripts>(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry()));
scripts->addPackage("openmw.ui", mUserInterfacePackage);
scripts->addPackage("openmw.camera", mCameraPackage);
}
else
scripts = std::make_shared<LocalScripts>(&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);
}
}
}

@ -0,0 +1,114 @@
#ifndef MWLUA_LUAMANAGERIMP_H
#define MWLUA_LUAMANAGERIMP_H
#include <map>
#include <set>
#include <components/lua/luastate.hpp>
#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<std::string>& 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>&& action) { mActionQueue.push_back(std::move(action)); }
void addTeleportPlayerAction(std::unique_ptr<TeleportAction>&& 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<int, int>& 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<std::string> mGlobalScriptList;
GlobalScripts mGlobalScripts{&mLua};
std::set<LocalScripts*> mActiveLocalScripts;
WorldView mWorldView;
bool mPlayerChanged = false;
MWWorld::Ptr mPlayer;
GlobalEventQueue mGlobalEvents;
LocalEventQueue mLocalEvents;
std::unique_ptr<LuaUtil::UserdataSerializer> mGlobalSerializer;
std::unique_ptr<LuaUtil::UserdataSerializer> mLocalSerializer;
std::map<int, int> mContentFileMapping;
std::unique_ptr<LuaUtil::UserdataSerializer> mGlobalLoader;
std::unique_ptr<LuaUtil::UserdataSerializer> mLocalLoader;
std::vector<SDL_Keysym> mKeyPressEvents;
std::vector<ObjectId> mActorAddedEvents;
struct LocalEngineEvent
{
ObjectId mDest;
LocalScripts::EngineEvent mEvent;
};
std::vector<LocalEngineEvent> mLocalEngineEvents;
// Queued actions that should be done in main thread. Processed by applyQueuedChanges().
std::vector<std::unique_ptr<Action>> mActionQueue;
std::unique_ptr<TeleportAction> mTeleportPlayerAction;
std::vector<std::string> mUIMessages;
};
}
#endif // MWLUA_LUAMANAGERIMP_H

@ -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<std::type_index, std::string_view> 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;
}
}

@ -0,0 +1,107 @@
#ifndef MWLUA_OBJECT_H
#define MWLUA_OBJECT_H
#include <typeindex>
#include <components/esm/cellref.hpp>
#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<ObjectId, MWWorld::Ptr> 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<std::vector<ObjectId>>;
template <typename Obj>
struct ObjectList { ObjectIdList mIds; };
using GObjectList = ObjectList<GObject>;
using LObjectList = ObjectList<LObject>;
}
#endif // MWLUA_OBJECT_H

@ -0,0 +1,340 @@
#include "luabindings.hpp"
#include <components/lua/luastate.hpp>
#include <components/queries/query.hpp>
#include "../mwclass/door.hpp"
#include "../mwworld/containerstore.hpp"
#include "../mwworld/inventorystore.hpp"
#include "eventqueue.hpp"
#include "luamanagerimp.hpp"
namespace MWLua
{
template <typename ObjectT>
struct Inventory
{
ObjectT mObj;
};
}
namespace sol
{
template <>
struct is_automagical<MWLua::LObject> : std::false_type {};
template <>
struct is_automagical<MWLua::GObject> : std::false_type {};
template <>
struct is_automagical<MWLua::LObjectList> : std::false_type {};
template <>
struct is_automagical<MWLua::GObjectList> : std::false_type {};
template <>
struct is_automagical<MWLua::Inventory<MWLua::LObject>> : std::false_type {};
template <>
struct is_automagical<MWLua::Inventory<MWLua::GObject>> : std::false_type {};
}
namespace MWLua
{
template <typename ObjT>
using Cell = std::conditional_t<std::is_same_v<ObjT, LObject>, LCell, GCell>;
template <class Class>
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 <class ObjectT>
static void registerObjectList(const std::string& prefix, const Context& context)
{
using ListT = ObjectList<ObjectT>;
sol::state& lua = context.mLua->sol();
ObjectRegistry* registry = context.mWorldView->getObjectRegistry();
sol::usertype<ListT> listT = lua.new_usertype<ListT>(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<std::tuple<int64_t, ObjectT>>
{
if (i >= 0 && i < static_cast<int64_t>(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 <class ObjectT>
static void addBasicBindings(sol::usertype<ObjectT>& 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<Cell<ObjectT>>
{
const MWWorld::Ptr& ptr = o.ptr();
if (ptr.isInCell())
return Cell<ObjectT>{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<ObjectT, GObject>)
{ // 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<osg::Vec3f>& optRot)
{
MWWorld::Ptr ptr = object.ptr();
osg::Vec3f rot = optRot ? *optRot : ptr.getRefData().getPosition().asRotationVec3();
auto action = std::make_unique<TeleportAction>(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 <class ObjectT>
static void addDoorBindings(sol::usertype<ObjectT>& objectT, const Context& context)
{
auto ptr = [](const ObjectT& o) -> const MWWorld::Ptr& { return requireClass<MWClass::Door>(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<Cell<ObjectT>>
{
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<ObjectT>{cell};
else
return sol::nullopt;
});
}
static SetEquipmentAction::Equipment parseEquipmentTable(sol::table equipment)
{
SetEquipmentAction::Equipment eqp;
for (auto& [key, value] : equipment)
{
int slot = key.as<int>();
if (value.is<GObject>())
eqp[slot] = value.as<GObject>().id();
else
eqp[slot] = value.as<std::string>();
}
return eqp;
}
template <class ObjectT>
static void addInventoryBindings(sol::usertype<ObjectT>& objectT, const std::string& prefix, const Context& context)
{
using InventoryT = Inventory<ObjectT>;
sol::usertype<InventoryT> inventoryT = context.mLua->sol().new_usertype<InventoryT>(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<std::vector<ObjectId>>();
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<ObjectT>{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<ObjectT, GObject>)
{ // 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<SetEquipmentAction>(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 <class ObjectT>
static void initObjectBindings(const std::string& prefix, const Context& context)
{
sol::usertype<ObjectT> objectT = context.mLua->sol().new_usertype<ObjectT>(prefix + "Object");
addBasicBindings<ObjectT>(objectT, context);
addDoorBindings<ObjectT>(objectT, context);
addInventoryBindings<ObjectT>(objectT, prefix, context);
registerObjectList<ObjectT>(prefix, context);
}
void initObjectBindingsForLocalScripts(const Context& context)
{
initObjectBindings<LObject>("L", context);
}
void initObjectBindingsForGlobalScripts(const Context& context)
{
initObjectBindings<GObject>("G", context);
}
}

@ -0,0 +1,27 @@
#ifndef MWLUA_PLAYERSCRIPTS_H
#define MWLUA_PLAYERSCRIPTS_H
#include <SDL_events.h>
#include "localscripts.hpp"
namespace MWLua
{
class PlayerScripts : public LocalScripts
{
public:
PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) : LocalScripts(lua, obj)
{
registerEngineHandlers({&mKeyPressHandlers});
}
void keyPress(const SDL_Keysym& key) { callEngineHandlers(mKeyPressHandlers, key); }
private:
EngineHandlerList mKeyPressHandlers{"onKeyPress"};
};
}
#endif // MWLUA_PLAYERSCRIPTS_H

@ -0,0 +1,191 @@
#include "query.hpp"
#include <sol/sol.hpp>
#include <components/lua/luastate.hpp>
#include "../mwclass/container.hpp"
#include "../mwworld/cellstore.hpp"
#include "worldview.hpp"
namespace MWLua
{
static std::vector<QueryFieldGroup> initBasicFieldGroups()
{
auto createGroup = [](std::string name, const auto& arr) -> QueryFieldGroup
{
std::vector<const Queries::Field*> 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<QueryFieldGroup>{
createGroup("OBJECT", objectFields),
createGroup("DOOR", doorFields),
};
}
const std::vector<QueryFieldGroup>& getBasicQueryFieldGroups()
{
static std::vector<QueryFieldGroup> 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<char> 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::string_view>(), std::get<std::string>(cond.mValue), cond.mType);
else if (cond.mField->type() == typeid(float))
c = compareFn(fieldObj.as<float>(), std::get<float>(cond.mValue), cond.mType);
else if (cond.mField->type() == typeid(double))
c = compareFn(fieldObj.as<double>(), std::get<double>(cond.mValue), cond.mType);
else if (cond.mField->type() == typeid(bool))
c = compareFn(fieldObj.as<bool>(), std::get<bool>(cond.mValue), cond.mType);
else if (cond.mField->type() == typeid(int32_t))
c = compareFn(fieldObj.as<int32_t>(), std::get<int32_t>(cond.mValue), cond.mType);
else if (cond.mField->type() == typeid(int64_t))
c = compareFn(fieldObj.as<int64_t>(), std::get<int64_t>(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<std::vector<ObjectId>>();
for (const ObjectId& id : *list)
{
if (static_cast<int64_t>(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<std::vector<ObjectId>>();
auto visitor = [&](const MWWorld::Ptr& ptr)
{
if (static_cast<int64_t>(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<int64_t>(res->size()) != query.mLimit;
};
store->forEach(std::move(visitor)); // TODO: maybe use store->forEachType<TYPE> depending on query.mType
return res;
}
}

@ -0,0 +1,39 @@
#ifndef MWLUA_QUERY_H
#define MWLUA_QUERY_H
#include <string>
#include <components/queries/query.hpp>
#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<const Queries::Field*> mFields;
};
const std::vector<QueryFieldGroup>& 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

@ -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);
}
}

@ -0,0 +1,72 @@
#include "userdataserializer.hpp"
#include <components/lua/serialization.hpp>
#include <components/misc/endianness.hpp>
#include "object.hpp"
namespace MWLua
{
class Serializer final : public LuaUtil::UserdataSerializer
{
public:
explicit Serializer(bool localSerializer, ObjectRegistry* registry, std::map<int, int>* 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<GObject>() || data.is<LObject>())
{
ObjectId id = data.as<Object>().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<LObject>(lua, LObject(id, mObjectRegistry));
else
sol::stack::push<GObject>(lua, GObject(id, mObjectRegistry));
return true;
}
return false;
}
bool mLocalSerializer;
ObjectRegistry* mObjectRegistry;
std::map<int, int>* mContentFileMapping;
};
std::unique_ptr<LuaUtil::UserdataSerializer> createUserdataSerializer(
bool local, ObjectRegistry* registry, std::map<int, int>* contentFileMapping)
{
return std::make_unique<Serializer>(local, registry, contentFileMapping);
}
}

@ -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<LuaUtil::UserdataSerializer> createUserdataSerializer(
bool local, ObjectRegistry* registry, std::map<int, int>* contentFileMapping = nullptr);
}
#endif // MWLUA_USERDATASERIALIZER_H

@ -0,0 +1,152 @@
#include "worldview.hpp"
#include <components/esm/esmreader.hpp>
#include <components/esm/esmwriter.hpp>
#include <components/esm/loadcell.hpp>
#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<double>(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);
}
}

@ -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::vector<ObjectId>>();
std::set<ObjectId> 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

@ -24,6 +24,7 @@
#include "../mwbase/soundmanager.hpp" #include "../mwbase/soundmanager.hpp"
#include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/statemanager.hpp" #include "../mwbase/statemanager.hpp"
#include "../mwbase/luamanager.hpp"
#include "../mwmechanics/aibreathe.hpp" #include "../mwmechanics/aibreathe.hpp"
@ -1961,6 +1962,8 @@ namespace MWMechanics
{ {
bool isPlayer = iter->first == player; bool isPlayer = iter->first == player;
CharacterController* ctrl = iter->second->getCharacterController(); 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(); float distSqr = (playerPos - iter->first.getRefData().getPosition().asVec3()).length2();
// AI processing is only done within given distance to the player. // AI processing is only done within given distance to the player.
@ -2062,7 +2065,7 @@ namespace MWMechanics
if (iter->first != player) if (iter->first != player)
{ {
CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first); 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); stats.getAiSequence().execute(iter->first, *ctrl, duration);
updateGreetingState(iter->first, *iter->second, timerUpdateHello > 0); 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); CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first);
stats.getAiSequence().execute(iter->first, *ctrl, duration, /*outOfRange*/true); stats.getAiSequence().execute(iter->first, *ctrl, duration, /*outOfRange*/true);
@ -2088,6 +2091,32 @@ namespace MWMechanics
if (timerUpdateEquippedLight == 0) if (timerUpdateEquippedLight == 0)
updateEquippedLight(iter->first, updateEquippedLightInterval, showTorches); 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;
}
} }
} }

@ -225,7 +225,7 @@ namespace MWRender
esm.resize(index+1); esm.resize(index+1);
cell->restore(esm[index], i); cell->restore(esm[index], i);
ESM::CellRef ref; ESM::CellRef ref;
ref.mRefNum.mContentFile = ESM::RefNum::RefNum_NoContentFile; ref.mRefNum.unset();
bool deleted = false; bool deleted = false;
while(cell->getNextRef(esm[index], ref, deleted)) while(cell->getNextRef(esm[index], ref, deleted))
{ {

@ -422,7 +422,7 @@ namespace MWRender
esm.resize(index+1); esm.resize(index+1);
cell->restore(esm[index], i); cell->restore(esm[index], i);
ESM::CellRef ref; ESM::CellRef ref;
ref.mRefNum.mContentFile = ESM::RefNum::RefNum_NoContentFile; ref.mRefNum.unset();
ESM::MovedCellRef cMRef; ESM::MovedCellRef cMRef;
cMRef.mRefNum.mIndex = 0; cMRef.mRefNum.mIndex = 0;
bool deleted = false; bool deleted = false;

@ -480,5 +480,6 @@ op 0x200031d: StartScript, explicit
op 0x200031e: GetDistance op 0x200031e: GetDistance
op 0x200031f: GetDistance, explicit op 0x200031f: GetDistance, explicit
op 0x2000320: Help op 0x2000320: Help
op 0x2000321: ReloadLua
opcodes 0x2000321-0x3ffffff unused opcodes 0x2000322-0x3ffffff unused

@ -28,6 +28,7 @@
#include "../mwbase/scriptmanager.hpp" #include "../mwbase/scriptmanager.hpp"
#include "../mwbase/soundmanager.hpp" #include "../mwbase/soundmanager.hpp"
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
#include "../mwbase/luamanager.hpp"
#include "../mwworld/class.hpp" #include "../mwworld/class.hpp"
#include "../mwworld/player.hpp" #include "../mwworld/player.hpp"
@ -169,8 +170,6 @@ namespace MWScript
void execute (Interpreter::Runtime& runtime) override void execute (Interpreter::Runtime& runtime) override
{ {
MWWorld::Ptr ptr = R()(runtime); MWWorld::Ptr ptr = R()(runtime);
if(!ptr.isEmpty() && !ptr.mRef->mData.isEnabled())
ptr.mRef->mData.mPhysicsPostponed = false;
MWBase::Environment::get().getWorld()->enable (ptr); 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) void installOpcodes (Interpreter::Interpreter& interpreter)
{ {
interpreter.installSegment5 (Compiler::Misc::opcodeMenuMode, new OpMenuMode); interpreter.installSegment5 (Compiler::Misc::opcodeMenuMode, new OpMenuMode);
@ -1719,6 +1729,7 @@ namespace MWScript
interpreter.installSegment5 (Compiler::Misc::opcodeRepairedOnMeExplicit, new OpRepairedOnMe<ExplicitRef>); interpreter.installSegment5 (Compiler::Misc::opcodeRepairedOnMeExplicit, new OpRepairedOnMe<ExplicitRef>);
interpreter.installSegment5 (Compiler::Misc::opcodeToggleRecastMesh, new OpToggleRecastMesh); interpreter.installSegment5 (Compiler::Misc::opcodeToggleRecastMesh, new OpToggleRecastMesh);
interpreter.installSegment5 (Compiler::Misc::opcodeHelp, new OpHelp); interpreter.installSegment5 (Compiler::Misc::opcodeHelp, new OpHelp);
interpreter.installSegment5 (Compiler::Misc::opcodeReloadLua, new OpReloadLua);
} }
} }
} }

@ -27,6 +27,7 @@
#include "../mwbase/scriptmanager.hpp" #include "../mwbase/scriptmanager.hpp"
#include "../mwbase/soundmanager.hpp" #include "../mwbase/soundmanager.hpp"
#include "../mwbase/inputmanager.hpp" #include "../mwbase/inputmanager.hpp"
#include "../mwbase/luamanager.hpp"
#include "../mwworld/player.hpp" #include "../mwworld/player.hpp"
#include "../mwworld/class.hpp" #include "../mwworld/class.hpp"
@ -59,6 +60,7 @@ void MWState::StateManager::cleanup (bool force)
MWMechanics::CreatureStats::cleanup(); MWMechanics::CreatureStats::cleanup();
} }
MWBase::Environment::get().getLuaManager()->clear();
} }
std::map<int, int> MWState::StateManager::buildContentFileIndexMap (const ESM::ESMReader& reader) std::map<int, int> MWState::StateManager::buildContentFileIndexMap (const ESM::ESMReader& reader)
@ -146,7 +148,7 @@ void MWState::StateManager::newGame (bool bypass)
{ {
Log(Debug::Info) << "Starting a new game"; Log(Debug::Info) << "Starting a new game";
MWBase::Environment::get().getScriptManager()->getGlobalScripts().addStartup(); MWBase::Environment::get().getScriptManager()->getGlobalScripts().addStartup();
MWBase::Environment::get().getLuaManager()->newGameStarted();
MWBase::Environment::get().getWorld()->startNewGame (bypass); MWBase::Environment::get().getWorld()->startNewGame (bypass);
mState = State_Running; mState = State_Running;
@ -249,6 +251,7 @@ void MWState::StateManager::saveGame (const std::string& description, const Slot
int recordCount = 1 // saved game header int recordCount = 1 // saved game header
+MWBase::Environment::get().getJournal()->countSavedGameRecords() +MWBase::Environment::get().getJournal()->countSavedGameRecords()
+MWBase::Environment::get().getLuaManager()->countSavedGameRecords()
+MWBase::Environment::get().getWorld()->countSavedGameRecords() +MWBase::Environment::get().getWorld()->countSavedGameRecords()
+MWBase::Environment::get().getScriptManager()->getGlobalScripts().countSavedGameRecords() +MWBase::Environment::get().getScriptManager()->getGlobalScripts().countSavedGameRecords()
+MWBase::Environment::get().getDialogueManager()->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().getJournal()->write (writer, listener);
MWBase::Environment::get().getDialogueManager()->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().getWorld()->write (writer, listener);
MWBase::Environment::get().getScriptManager()->getGlobalScripts().write (writer, listener); MWBase::Environment::get().getScriptManager()->getGlobalScripts().write (writer, listener);
MWBase::Environment::get().getWindowManager()->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."); 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<int, int> contentFileMap = buildContentFileIndexMap (reader); std::map<int, int> contentFileMap = buildContentFileIndexMap (reader);
MWBase::Environment::get().getLuaManager()->setContentFileMapping(contentFileMap);
Loading::Listener& listener = *MWBase::Environment::get().getWindowManager()->getLoadingScreen(); 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); MWBase::Environment::get().getInputManager()->readRecord(reader, n.intval);
break; break;
case ESM::REC_LUAM:
MWBase::Environment::get().getLuaManager()->readRecord(reader, n.intval);
break;
default: default:
// ignore invalid records // ignore invalid records

@ -1,5 +1,8 @@
#include "cellref.hpp" #include "cellref.hpp"
#include <assert.h>
#include <components/debug/debuglog.hpp>
#include <components/esm/objectstate.hpp> #include <components/esm/objectstate.hpp>
namespace MWWorld namespace MWWorld
@ -10,6 +13,26 @@ namespace MWWorld
return mCellRef.mRefNum; 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<int32_t>::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 bool CellRef::hasContentFile() const
{ {
return mCellRef.mRefNum.hasContentFile(); return mCellRef.mRefNum.hasContentFile();

@ -25,6 +25,10 @@ namespace MWWorld
// Note: Currently unused for items in containers // Note: Currently unused for items in containers
const ESM::RefNum& getRefNum() const; 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. // Set RefNum to its default state.
void unsetRefNum(); void unsetRefNum();

@ -18,6 +18,7 @@
#include <components/esm/doorstate.hpp> #include <components/esm/doorstate.hpp>
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/luamanager.hpp"
#include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
@ -195,6 +196,8 @@ namespace
iter->mData.enable(); iter->mData.enable();
MWBase::Environment::get().getWorld()->disable(MWWorld::Ptr(&*iter, cellstore)); MWBase::Environment::get().getWorld()->disable(MWWorld::Ptr(&*iter, cellstore));
} }
else
MWBase::Environment::get().getLuaManager()->registerObject(MWWorld::Ptr(&*iter, cellstore));
return; return;
} }
@ -206,6 +209,9 @@ namespace
MWWorld::LiveCellRef<T> ref (record); MWWorld::LiveCellRef<T> ref (record);
ref.load (state); ref.load (state);
collection.mList.push_back (ref); 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()) if (searchViaRefNum(object.getCellRef().getRefNum()).isEmpty())
throw std::runtime_error("moveTo: object is not in this cell"); throw std::runtime_error("moveTo: object is not in this cell");
MWBase::Environment::get().getLuaManager()->registerObject(MWWorld::Ptr(object.getBase(), cellToMoveTo));
// 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;
}
MovedRefTracker::iterator found = mMovedHere.find(object.getBase()); MovedRefTracker::iterator found = mMovedHere.find(object.getBase());
if (found != mMovedHere.end()) if (found != mMovedHere.end())
@ -615,7 +612,7 @@ namespace MWWorld
mCell->restore (esm[index], i); mCell->restore (esm[index], i);
ESM::CellRef ref; ESM::CellRef ref;
ref.mRefNum.mContentFile = ESM::RefNum::RefNum_NoContentFile; ref.mRefNum.unset();
// Get each reference in turn // Get each reference in turn
ESM::MovedCellRef cMRef; ESM::MovedCellRef cMRef;

@ -34,7 +34,7 @@ namespace
readers.resize(index + 1); readers.resize(index + 1);
cell.restore(readers[index], i); cell.restore(readers[index], i);
ESM::CellRef ref; ESM::CellRef ref;
ref.mRefNum.mContentFile = ESM::RefNum::RefNum_NoContentFile; ref.mRefNum.unset();
bool deleted = false; bool deleted = false;
while(cell.getNextRef(readers[index], ref, deleted)) while(cell.getNextRef(readers[index], ref, deleted))
{ {

@ -5,6 +5,7 @@
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
#include "../mwbase/luamanager.hpp"
#include "ptr.hpp" #include "ptr.hpp"
#include "class.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"; Log(Debug::Warning) << "Soul '" << mRef.getSoul() << "' not found, removing the soul from soul gem";
mRef.setSoul(std::string()); mRef.setSoul(std::string());
} }
MWBase::Environment::get().getLuaManager()->loadLocalScripts(ptr, state.mLuaScripts);
} }
void MWWorld::LiveCellRefBase::saveImp (ESM::ObjectState& state) const void MWWorld::LiveCellRefBase::saveImp (ESM::ObjectState& state) const
@ -61,6 +64,7 @@ void MWWorld::LiveCellRefBase::saveImp (ESM::ObjectState& state) const
ConstPtr ptr (this); ConstPtr ptr (this);
mData.write (state, mClass->getScript (ptr)); mData.write (state, mClass->getScript (ptr));
MWBase::Environment::get().getLuaManager()->saveLocalScripts(Ptr(const_cast<LiveCellRefBase*>(this)), state.mLuaScripts);
mClass->writeAdditionalState (ptr, state); mClass->writeAdditionalState (ptr, state);
} }

@ -8,6 +8,8 @@
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
#include "../mwlua/localscripts.hpp"
namespace namespace
{ {
enum RefDataFlags enum RefDataFlags
@ -21,6 +23,12 @@ enum RefDataFlags
namespace MWWorld namespace MWWorld
{ {
void RefData::setLuaScripts(std::shared_ptr<MWLua::LocalScripts>&& scripts)
{
mChanged = true;
mLuaScripts = std::move(scripts);
}
void RefData::copy (const RefData& refData) void RefData::copy (const RefData& refData)
{ {
mBaseNode = refData.mBaseNode; mBaseNode = refData.mBaseNode;
@ -36,12 +44,14 @@ namespace MWWorld
mAnimationState = refData.mAnimationState; mAnimationState = refData.mAnimationState;
mCustomData = refData.mCustomData ? refData.mCustomData->clone() : nullptr; mCustomData = refData.mCustomData ? refData.mCustomData->clone() : nullptr;
mLuaScripts = refData.mLuaScripts;
} }
void RefData::cleanup() void RefData::cleanup()
{ {
mBaseNode = nullptr; mBaseNode = nullptr;
mCustomData = nullptr; mCustomData = nullptr;
mLuaScripts = nullptr;
} }
RefData::RefData() 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) void RefData::setBaseNode(SceneUtil::PositionAttitudeTransform *base)
{ {
mBaseNode = base; mBaseNode = base;

@ -22,6 +22,11 @@ namespace ESM
struct ObjectState; struct ObjectState;
} }
namespace MWLua
{
class LocalScripts;
}
namespace MWWorld namespace MWWorld
{ {
@ -32,6 +37,7 @@ namespace MWWorld
SceneUtil::PositionAttitudeTransform* mBaseNode; SceneUtil::PositionAttitudeTransform* mBaseNode;
MWScript::Locals mLocals; MWScript::Locals mLocals;
std::shared_ptr<MWLua::LocalScripts> mLuaScripts;
/// separate delete flag used for deletion by a content file /// separate delete flag used for deletion by a content file
/// @note not stored in the save game file. /// @note not stored in the save game file.
@ -72,7 +78,7 @@ namespace MWWorld
/// perform these operations). /// perform these operations).
RefData (const RefData& refData); RefData (const RefData& refData);
RefData (RefData&& other) noexcept = default; RefData (RefData&& other) noexcept;
~RefData(); ~RefData();
@ -81,7 +87,7 @@ namespace MWWorld
/// perform this operations). /// perform this operations).
RefData& operator= (const RefData& refData); RefData& operator= (const RefData& refData);
RefData& operator= (RefData&& other) noexcept = default; RefData& operator= (RefData&& other) noexcept;
/// Return base node (can be a null pointer). /// Return base node (can be a null pointer).
SceneUtil::PositionAttitudeTransform* getBaseNode(); SceneUtil::PositionAttitudeTransform* getBaseNode();
@ -96,6 +102,9 @@ namespace MWWorld
void setLocals (const ESM::Script& script); void setLocals (const ESM::Script& script);
MWLua::LocalScripts* getLuaScripts() { return mLuaScripts.get(); }
void setLuaScripts(std::shared_ptr<MWLua::LocalScripts>&&);
void setCount (int count); void setCount (int count);
///< Set object count (an object pile is a simple object with a count >1). ///< Set object count (an object pile is a simple object with a count >1).
/// ///

@ -25,6 +25,7 @@
#include "../mwbase/soundmanager.hpp" #include "../mwbase/soundmanager.hpp"
#include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/windowmanager.hpp" #include "../mwbase/windowmanager.hpp"
#include "../mwbase/luamanager.hpp"
#include "../mwrender/renderingmanager.hpp" #include "../mwrender/renderingmanager.hpp"
#include "../mwrender/landmanager.hpp" #include "../mwrender/landmanager.hpp"
@ -138,6 +139,8 @@ namespace
if (!physics.getObject(ptr)) if (!physics.getObject(ptr))
ptr.getClass().insertObject (ptr, model, rotation, physics); 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) void addObject(const MWWorld::Ptr& ptr, const MWPhysics::PhysicsSystem& physics, DetourNavigator::Navigator& navigator)
@ -385,6 +388,7 @@ namespace MWWorld
mRendering.removeActorPath(ptr); mRendering.removeActorPath(ptr);
mPhysics->remove(ptr); mPhysics->remove(ptr);
} }
MWBase::Environment::get().getLuaManager()->objectRemovedFromScene(ptr);
} }
const auto cellX = cell->getCell()->getGridX(); const auto cellX = cell->getCell()->getGridX();
@ -1006,6 +1010,7 @@ namespace MWWorld
{ {
MWBase::Environment::get().getMechanicsManager()->remove (ptr); MWBase::Environment::get().getMechanicsManager()->remove (ptr);
MWBase::Environment::get().getSoundManager()->stopSound3D (ptr); MWBase::Environment::get().getSoundManager()->stopSound3D (ptr);
MWBase::Environment::get().getLuaManager()->objectRemovedFromScene(ptr);
const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); const auto navigator = MWBase::Environment::get().getWorld()->getNavigator();
if (const auto object = mPhysics->getObject(ptr)) if (const auto object = mPhysics->getObject(ptr))
{ {

@ -36,6 +36,7 @@
#include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/windowmanager.hpp" #include "../mwbase/windowmanager.hpp"
#include "../mwbase/scriptmanager.hpp" #include "../mwbase/scriptmanager.hpp"
#include "../mwbase/luamanager.hpp"
#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/creaturestats.hpp"
#include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/npcstats.hpp"
@ -569,6 +570,11 @@ namespace MWWorld
return getInterior (id.mWorldspace); return getInterior (id.mWorldspace);
} }
bool World::isCellActive(CellStore* cell) const
{
return mWorldScene->getActiveCells().count(cell) > 0;
}
void World::testExteriorCells() void World::testExteriorCells()
{ {
mWorldScene->testExteriorCells(); mWorldScene->testExteriorCells();
@ -812,7 +818,8 @@ namespace MWWorld
void World::enable (const Ptr& reference) void World::enable (const Ptr& reference)
{ {
// enable is a no-op for items in containers MWBase::Environment::get().getLuaManager()->registerObject(reference);
if (!reference.isInCell()) if (!reference.isInCell())
return; return;
@ -863,6 +870,7 @@ namespace MWWorld
if (reference == getPlayerPtr()) if (reference == getPlayerPtr())
throw std::runtime_error("can not disable player object"); throw std::runtime_error("can not disable player object");
MWBase::Environment::get().getLuaManager()->deregisterObject(reference);
reference.getRefData().disable(); reference.getRefData().disable();
if (reference.getCellRef().getRefNum().hasContentFile()) if (reference.getCellRef().getRefNum().hasContentFile())
@ -2020,7 +2028,7 @@ namespace MWWorld
rayToObject = mRendering->castCameraToViewportRay(0.5f, 0.5f, maxDistance, ignorePlayer); rayToObject = mRendering->castCameraToViewportRay(0.5f, 0.5f, maxDistance, ignorePlayer);
facedObject = rayToObject.mHitObject; facedObject = rayToObject.mHitObject;
if (facedObject.isEmpty() && rayToObject.mHitRefnum.hasContentFile()) if (facedObject.isEmpty() && rayToObject.mHitRefnum.isSet())
{ {
for (CellStore* cellstore : mWorldScene->getActiveCells()) for (CellStore* cellstore : mWorldScene->getActiveCells())
{ {
@ -2483,12 +2491,14 @@ namespace MWWorld
mNavigator->removeAgent(getPathfindingHalfExtents(getPlayerConstPtr())); mNavigator->removeAgent(getPathfindingHalfExtents(getPlayerConstPtr()));
mPhysics->remove(getPlayerPtr()); mPhysics->remove(getPlayerPtr());
mRendering->removePlayer(getPlayerPtr()); mRendering->removePlayer(getPlayerPtr());
MWBase::Environment::get().getLuaManager()->objectRemovedFromScene(getPlayerPtr());
mPlayer->set(player); mPlayer->set(player);
} }
Ptr ptr = mPlayer->getPlayer(); Ptr ptr = mPlayer->getPlayer();
mRendering->setupPlayer(ptr); mRendering->setupPlayer(ptr);
MWBase::Environment::get().getLuaManager()->setupPlayer(ptr);
} }
void World::renderPlayer() void World::renderPlayer()

@ -216,6 +216,8 @@ namespace MWWorld
CellStore *getCell (const ESM::CellId& id) override; CellStore *getCell (const ESM::CellId& id) override;
bool isCellActive(CellStore* cell) const override;
void testExteriorCells() override; void testExteriorCells() override;
void testInteriorCells() override; void testInteriorCells() override;

@ -15,6 +15,13 @@ if (GTEST_FOUND AND GMOCK_FOUND)
esm/test_fixed_string.cpp esm/test_fixed_string.cpp
esm/variant.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_stringops.cpp
misc/test_endianness.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}) 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 # Fix for not visible pthreads functions for linker with glibc 2.15
if (UNIX AND NOT APPLE) if (UNIX AND NOT APPLE)
target_link_libraries(openmw_test_suite ${CMAKE_THREAD_LIBS_INIT}) target_link_libraries(openmw_test_suite ${CMAKE_THREAD_LIBS_INIT})

@ -0,0 +1,167 @@
#include "gmock/gmock.h"
#include <gtest/gtest.h>
#include <components/lua/luastate.hpp>
#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<VFS::Manager> 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<int>(), 42);
LuaUtil::call(script1["inc"], 3);
EXPECT_EQ(LuaUtil::call(script1["get"]).get<int>(), 45);
sol::table script2 = mLua.runInNewSandbox("aaa/counter.lua");
EXPECT_EQ(LuaUtil::call(script2["get"]).get<int>(), 42);
LuaUtil::call(script2["inc"], 1);
EXPECT_EQ(LuaUtil::call(script2["get"]).get<int>(), 43);
EXPECT_EQ(LuaUtil::call(script1["get"]).get<int>(), 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<float>(),
-LuaUtil::call(script["requireMathSin"], -1).get<float>());
EXPECT_EQ(LuaUtil::call(script["useCounter"]).get<int>(), 43);
EXPECT_EQ(LuaUtil::call(script["useCounter"]).get<int>(), 44);
{
sol::table script2 = mLua.runInNewSandbox("bbb/tests.lua");
EXPECT_EQ(LuaUtil::call(script2["useCounter"]).get<int>(), 43);
}
EXPECT_EQ(LuaUtil::call(script["useCounter"]).get<int>(), 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<int>(), 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<int>(), 9);
EXPECT_EQ(LuaUtil::call(script1["apiName"]).get<std::string>(), "api1");
EXPECT_EQ(LuaUtil::call(script2["apiName"]).get<std::string>(), "api2");
}
TEST_F(LuaStateTest, GetLuaVersion)
{
EXPECT_THAT(LuaUtil::getLuaVersion(), HasSubstr("Lua"));
}
}

@ -0,0 +1,59 @@
#include "gmock/gmock.h"
#include <gtest/gtest.h>
#include <components/lua/omwscriptsparser.hpp>
#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<VFS::Manager> 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<std::string> 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<std::string> 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());
}
}

@ -0,0 +1,29 @@
#include "gmock/gmock.h"
#include <gtest/gtest.h>
#include <components/queries/luabindings.hpp>
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<std::string>(),
"SELECT test WHERE ((x == \"abc\") OR (x LIKE \"%abcd%\")) AND (y > 5) ORDER BY x, y DESC GROUP BY y LIMIT 10 OFFSET 5");
}
}

@ -0,0 +1,376 @@
#include "gmock/gmock.h"
#include <gtest/gtest.h>
#include <components/esm/luascripts.hpp>
#include <components/lua/luastate.hpp>
#include <components/lua/scriptscontainer.hpp>
#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<VFS::Manager> 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);
}
}

@ -0,0 +1,207 @@
#include "gmock/gmock.h"
#include <gtest/gtest.h>
#include <osg/Vec2f>
#include <osg/Vec3f>
#include <components/lua/serialization.hpp>
#include <components/misc/endianness.hpp>
#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<double>(lua, 3.14));
EXPECT_EQ(serialized.size(), 10); // version, type, 8 bytes value
sol::object value = LuaUtil::deserialize(lua, serialized);
ASSERT_TRUE(value.is<double>());
EXPECT_FLOAT_EQ(value.as<double>(), 3.14);
}
TEST(LuaSerializationTest, Boolean)
{
sol::state lua;
{
std::string serialized = LuaUtil::serialize(sol::make_object<bool>(lua, true));
EXPECT_EQ(serialized.size(), 3); // version, type, 1 byte value
sol::object value = LuaUtil::deserialize(lua, serialized);
EXPECT_FALSE(value.is<double>());
ASSERT_TRUE(value.is<bool>());
EXPECT_TRUE(value.as<bool>());
}
{
std::string serialized = LuaUtil::serialize(sol::make_object<bool>(lua, false));
EXPECT_EQ(serialized.size(), 3); // version, type, 1 byte value
sol::object value = LuaUtil::deserialize(lua, serialized);
EXPECT_FALSE(value.is<double>());
ASSERT_TRUE(value.is<bool>());
EXPECT_FALSE(value.as<bool>());
}
}
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<std::string_view>(lua, emptyString));
EXPECT_EQ(serialized.size(), 2); // version, type
sol::object value = LuaUtil::deserialize(lua, serialized);
ASSERT_TRUE(value.is<std::string>());
EXPECT_EQ(value.as<std::string>(), emptyString);
}
{
std::string serialized = LuaUtil::serialize(sol::make_object<std::string_view>(lua, shortString));
EXPECT_EQ(serialized.size(), 2 + shortString.size()); // version, type, str data
sol::object value = LuaUtil::deserialize(lua, serialized);
ASSERT_TRUE(value.is<std::string>());
EXPECT_EQ(value.as<std::string>(), shortString);
}
{
std::string serialized = LuaUtil::serialize(sol::make_object<std::string_view>(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<std::string>());
EXPECT_EQ(value.as<std::string>(), 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<osg::Vec2f>());
EXPECT_EQ(value.as<osg::Vec2f>(), 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<osg::Vec3f>());
EXPECT_EQ(value.as<osg::Vec3f>(), 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<int>("aa"), 1);
EXPECT_EQ(res_table.get<bool>("ab"), true);
EXPECT_EQ(res_table.get<sol::table>("nested").get<int>("aa"), 2);
EXPECT_EQ(res_table.get<sol::table>("nested").get<std::string>("bb"), "something");
EXPECT_FLOAT_EQ(res_table.get<sol::table>("nested").get<double>(5), -0.5);
EXPECT_EQ(res_table.get<osg::Vec2f>(1), osg::Vec2f(1, 2));
EXPECT_EQ(res_table.get<osg::Vec2f>(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>())
{
TestStruct1 t = data.as<TestStruct1>();
t.a = Misc::toLittleEndian(t.a);
t.b = Misc::toLittleEndian(t.b);
append(out, "ts1", &t, sizeof(t));
return true;
}
if (data.is<TestStruct2>())
{
TestStruct2 t = data.as<TestStruct2>();
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<const TestStruct1*>(binaryData.data());
t.a = Misc::fromLittleEndian(t.a);
t.b = Misc::fromLittleEndian(t.b);
sol::stack::push<TestStruct1>(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<const TestStruct2*>(binaryData.data());
t.a = Misc::fromLittleEndian(t.a);
t.b = Misc::fromLittleEndian(t.b);
sol::stack::push<TestStruct2>(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<TestStruct1>("x");
TestStruct2 ry = res.get<TestStruct2>("y");
EXPECT_EQ(rx.a, 1.5);
EXPECT_EQ(rx.b, 2.5);
EXPECT_EQ(ry.a, 4);
EXPECT_EQ(ry.b, 3);
}
}

@ -0,0 +1,80 @@
#include "gmock/gmock.h"
#include <gtest/gtest.h>
#include <components/lua/utilpackage.hpp>
#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<float>(), 3);
EXPECT_FLOAT_EQ(lua.safe_script("return v.y").get<float>(), 4);
EXPECT_EQ(lua.safe_script("return tostring(v)").get<std::string>(), "(3, 4)");
EXPECT_FLOAT_EQ(lua.safe_script("return v:length()").get<float>(), 5);
EXPECT_FLOAT_EQ(lua.safe_script("return v:length2()").get<float>(), 25);
EXPECT_FALSE(lua.safe_script("return util.vector2(1, 2) == util.vector2(1, 3)").get<bool>());
EXPECT_TRUE(lua.safe_script("return util.vector2(1, 2) + util.vector2(2, 5) == util.vector2(3, 7)").get<bool>());
EXPECT_TRUE(lua.safe_script("return util.vector2(1, 2) - util.vector2(2, 5) == -util.vector2(1, 3)").get<bool>());
EXPECT_TRUE(lua.safe_script("return util.vector2(1, 2) == util.vector2(2, 4) / 2").get<bool>());
EXPECT_TRUE(lua.safe_script("return util.vector2(1, 2) * 2 == util.vector2(2, 4)").get<bool>());
EXPECT_FLOAT_EQ(lua.safe_script("return util.vector2(3, 2) * v").get<float>(), 17);
EXPECT_FLOAT_EQ(lua.safe_script("return util.vector2(3, 2):dot(v)").get<float>(), 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<float>(), 5);
EXPECT_TRUE(lua.safe_script("return v2 == util.vector2(3/5, 4/5)").get<bool>());
lua.safe_script("_, len = util.vector2(0, 0):normalize()");
EXPECT_FLOAT_EQ(lua.safe_script("return len").get<float>(), 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<float>(), 5);
EXPECT_FLOAT_EQ(lua.safe_script("return v.y").get<float>(), 12);
EXPECT_FLOAT_EQ(lua.safe_script("return v.z").get<float>(), 13);
EXPECT_EQ(lua.safe_script("return tostring(v)").get<std::string>(), "(5, 12, 13)");
EXPECT_FLOAT_EQ(lua.safe_script("return util.vector3(4, 0, 3):length()").get<float>(), 5);
EXPECT_FLOAT_EQ(lua.safe_script("return util.vector3(4, 0, 3):length2()").get<float>(), 25);
EXPECT_FALSE(lua.safe_script("return util.vector3(1, 2, 3) == util.vector3(1, 3, 2)").get<bool>());
EXPECT_TRUE(lua.safe_script("return util.vector3(1, 2, 3) + util.vector3(2, 5, 1) == util.vector3(3, 7, 4)").get<bool>());
EXPECT_TRUE(lua.safe_script("return util.vector3(1, 2, 3) - util.vector3(2, 5, 1) == -util.vector3(1, 3, -2)").get<bool>());
EXPECT_TRUE(lua.safe_script("return util.vector3(1, 2, 3) == util.vector3(2, 4, 6) / 2").get<bool>());
EXPECT_TRUE(lua.safe_script("return util.vector3(1, 2, 3) * 2 == util.vector3(2, 4, 6)").get<bool>());
EXPECT_FLOAT_EQ(lua.safe_script("return util.vector3(3, 2, 1) * v").get<float>(), 5*3 + 12*2 + 13*1);
EXPECT_FLOAT_EQ(lua.safe_script("return util.vector3(3, 2, 1):dot(v)").get<float>(), 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<bool>());
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<float>(), 5);
EXPECT_TRUE(lua.safe_script("return v2 == util.vector3(3/5, 4/5, 0)").get<bool>());
lua.safe_script("_, len = util.vector3(0, 0, 0):normalize()");
EXPECT_FLOAT_EQ(lua.safe_script("return len").get<float>(), 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<float>(), -0.5);
EXPECT_FLOAT_EQ(lua.safe_script("return v.y").get<float>(), 0.86602539);
EXPECT_FLOAT_EQ(lua.safe_script("return util.normalizeAngle(math.pi * 10 + 0.1)").get<float>(), 0.1);
EXPECT_FLOAT_EQ(lua.safe_script("return util.clamp(0.1, 0, 1.5)").get<float>(), 0.1);
EXPECT_FLOAT_EQ(lua.safe_script("return util.clamp(-0.1, 0, 1.5)").get<float>(), 0);
EXPECT_FLOAT_EQ(lua.safe_script("return util.clamp(2.1, 0, 1.5)").get<float>(), 1.5);
}
}

@ -0,0 +1,59 @@
#ifndef LUA_TESTING_UTIL_H
#define LUA_TESTING_UTIL_H
#include <sstream>
#include <components/vfs/archive.hpp>
#include <components/vfs/manager.hpp>
namespace
{
class TestFile : public VFS::File
{
public:
explicit TestFile(std::string content) : mContent(std::move(content)) {}
Files::IStreamPtr open() override
{
return std::make_shared<std::stringstream>(mContent, std::ios_base::in);
}
private:
const std::string mContent;
};
struct TestData : public VFS::Archive
{
std::map<std::string, VFS::File*> mFiles;
TestData(std::map<std::string, VFS::File*> files) : mFiles(std::move(files)) {}
void listResources(std::map<std::string, VFS::File*>& 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<VFS::Manager> createTestVFS(std::map<std::string, VFS::File*> files)
{
auto vfs = std::make_unique<VFS::Manager>(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

@ -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)

@ -28,6 +28,10 @@ endif (GIT_CHECKOUT)
# source files # source files
add_component_dir (lua
luastate scriptscontainer utilpackage serialization omwscriptsparser
)
add_component_dir (settings add_component_dir (settings
settings parser settings parser
) )
@ -80,7 +84,7 @@ add_component_dir (esm
loadweap records aipackage effectlist spelllist variant variantimp loadtes3 cellref filter 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 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 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 add_component_dir (esmterrain
@ -152,6 +156,10 @@ add_component_dir (fallback
fallback validate fallback validate
) )
add_component_dir (queries
query luabindings
)
if(WIN32) if(WIN32)
add_component_dir (crashcatcher add_component_dir (crashcatcher
windows_crashcatcher windows_crashcatcher

@ -338,6 +338,7 @@ namespace Compiler
extensions.registerFunction ("repairedonme", 'l', "S", opcodeRepairedOnMe, opcodeRepairedOnMeExplicit); extensions.registerFunction ("repairedonme", 'l', "S", opcodeRepairedOnMe, opcodeRepairedOnMeExplicit);
extensions.registerInstruction ("togglerecastmesh", "", opcodeToggleRecastMesh); extensions.registerInstruction ("togglerecastmesh", "", opcodeToggleRecastMesh);
extensions.registerInstruction ("help", "", opcodeHelp); extensions.registerInstruction ("help", "", opcodeHelp);
extensions.registerInstruction ("reloadlua", "", opcodeReloadLua);
} }
} }

@ -319,6 +319,7 @@ namespace Compiler
const int opcodeGetDisabledExplicit = 0x200031c; const int opcodeGetDisabledExplicit = 0x200031c;
const int opcodeStartScriptExplicit = 0x200031d; const int opcodeStartScriptExplicit = 0x200031d;
const int opcodeHelp = 0x2000320; const int opcodeHelp = 0x2000320;
const int opcodeReloadLua = 0x2000321;
} }
namespace Sky namespace Sky

@ -24,8 +24,9 @@ void ESM::RefNum::save (ESMWriter &esm, bool wide, const std::string& tag) const
esm.writeHNT (tag, *this, 8); esm.writeHNT (tag, *this, 8);
else else
{ {
if (isSet() && !hasContentFile())
Log(Debug::Error) << "Generated RefNum can not be saved in 32bit format";
int refNum = (mIndex & 0xffffff) | ((hasContentFile() ? mContentFile : 0xff)<<24); int refNum = (mIndex & 0xffffff) | ((hasContentFile() ? mContentFile : 0xff)<<24);
esm.writeHNT (tag, refNum, 4); esm.writeHNT (tag, refNum, 4);
} }
} }

@ -23,9 +23,10 @@ namespace ESM
void save (ESMWriter &esm, bool wide = false, const std::string& tag = "FRMR") const; void save (ESMWriter &esm, bool wide = false, const std::string& tag = "FRMR") const;
enum { RefNum_NoContentFile = -1 }; inline bool hasContentFile() const { return mContentFile >= 0; }
inline bool hasContentFile() const { return mContentFile != RefNum_NoContentFile; }
inline void unset() { mIndex = 0; mContentFile = RefNum_NoContentFile; } 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 // Note: this method should not be used for objects with invalid RefNum
// (for example, for objects from disabled plugins in savegames). // (for example, for objects from disabled plugins in savegames).

@ -164,7 +164,10 @@ enum RecNameInts
// format 1 // format 1
REC_FILT = FourCC<'F','I','L','T'>::value, 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 /// Common subrecords

@ -30,7 +30,7 @@ void ESM::GlobalScript::save (ESMWriter &esm) const
if (!mTargetId.empty()) if (!mTargetId.empty())
{ {
esm.writeHNOString ("TARG", mTargetId); esm.writeHNOString ("TARG", mTargetId);
if (mTargetRef.hasContentFile()) if (mTargetRef.isSet())
mTargetRef.save (esm, true, "FRMR"); mTargetRef.save (esm, true, "FRMR");
} }
} }

@ -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<LuaTimer> 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);
}
}
}

@ -0,0 +1,53 @@
#ifndef OPENMW_ESM_LUASCRIPTS_H
#define OPENMW_ESM_LUASCRIPTS_H
#include <vector>
#include <string>
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<LuaTimer> mTimers;
};
struct LuaScripts
{
std::vector<LuaScript> 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

@ -20,6 +20,8 @@ void ESM::ObjectState::load (ESMReader &esm)
if (mHasLocals) if (mHasLocals)
mLocals.load (esm); mLocals.load (esm);
mLuaScripts.load(esm);
mEnabled = 1; mEnabled = 1;
esm.getHNOT (mEnabled, "ENAB"); esm.getHNOT (mEnabled, "ENAB");
@ -56,6 +58,8 @@ void ESM::ObjectState::save (ESMWriter &esm, bool inInventory) const
mLocals.save (esm); mLocals.save (esm);
} }
mLuaScripts.save(esm);
if (!mEnabled && !inInventory) if (!mEnabled && !inInventory)
esm.writeHNT ("ENAB", mEnabled); esm.writeHNT ("ENAB", mEnabled);

@ -6,6 +6,7 @@
#include "cellref.hpp" #include "cellref.hpp"
#include "locals.hpp" #include "locals.hpp"
#include "luascripts.hpp"
#include "animationstate.hpp" #include "animationstate.hpp"
namespace ESM namespace ESM
@ -27,6 +28,7 @@ namespace ESM
unsigned char mHasLocals; unsigned char mHasLocals;
Locals mLocals; Locals mLocals;
LuaScripts mLuaScripts;
unsigned char mEnabled; unsigned char mEnabled;
int mCount; int mCount;
ESM::Position mPosition; ESM::Position mPosition;

@ -4,7 +4,7 @@
#include "esmwriter.hpp" #include "esmwriter.hpp"
unsigned int ESM::SavedGame::sRecordId = ESM::REC_SAVE; unsigned int ESM::SavedGame::sRecordId = ESM::REC_SAVE;
int ESM::SavedGame::sCurrentFormat = 15; int ESM::SavedGame::sCurrentFormat = 16;
void ESM::SavedGame::load (ESMReader &esm) void ESM::SavedGame::load (ESMReader &esm)
{ {

@ -0,0 +1,167 @@
#include "luastate.hpp"
#ifndef NO_LUAJIT
#include <luajit.h>
#endif // NO_LUAJIT
#include <components/debug/debuglog.hpp>
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<unsigned>(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<sol::userdata>())
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<sol::table>(mLua);
}
sol::table LuaState::getMutableFromReadOnly(const sol::userdata& ro)
{
sol::stack::push(mLua, ro);
lua_getmetatable(mLua, -1);
sol::table res = sol::stack::pop<sol::table>(mLua);
lua_pop(mLua, 1);
return res;
}
void LuaState::addCommonPackage(const std::string& packageName, const sol::object& package)
{
if (package.is<sol::function>())
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<std::string, sol::object>& 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<sol::table>())
throw std::runtime_error("Lua package must return a table.");
packages[packageName] = package;
}
else if (package.is<sol::function>())
package = packages[packageName] = call(package.as<sol::protected_function>(), 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<int>(res.get_type()) == LUA_TSTRING)
throw std::runtime_error("Lua error: " + res.get<std::string>());
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<char>(*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<std::string>());
mCompiledScripts[path] = res.get<sol::function>().dump();
return res;
}
std::string getLuaVersion()
{
#ifdef NO_LUAJIT
return LUA_RELEASE;
#else
return LUA_RELEASE " (" LUAJIT_VERSION ")";
#endif
}
}

@ -0,0 +1,107 @@
#ifndef COMPONENTS_LUA_LUASTATE_H
#define COMPONENTS_LUA_LUASTATE_H
#include <map>
#include <sol/sol.hpp>
#include <components/vfs/manager.hpp>
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 "<namePrefix>[<filePath>]". 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<std::string, sol::object>& packages = {},
const sol::object& hiddenData = sol::nil);
void dropScriptCache() { mCompiledScripts.clear(); }
private:
static sol::protected_function_result throwIfError(sol::protected_function_result&&);
template <typename... Args>
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<std::string, sol::bytecode> mCompiledScripts;
std::map<std::string, sol::object> 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 <typename... Args>
sol::protected_function_result call(sol::protected_function fn, Args&&... args)
{
try
{
return LuaState::throwIfError(fn(std::forward<Args>(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 <class... Str>
sol::object getFieldOrNil(const sol::object& table, std::string_view first, const Str&... str)
{
if (!table.is<sol::table>())
return sol::nil;
if constexpr (sizeof...(str) == 0)
return table.as<sol::table>()[first];
else
return getFieldOrNil(table.as<sol::table>()[first], str...);
}
}
#endif // COMPONENTS_LUA_LUASTATE_H

@ -0,0 +1,44 @@
#include "omwscriptsparser.hpp"
#include <algorithm>
#include <components/debug/debuglog.hpp>
std::vector<std::string> LuaUtil::parseOMWScriptsFiles(const VFS::Manager* vfs, const std::vector<std::string>& 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<std::string> 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<char>(*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;
}

@ -0,0 +1,14 @@
#ifndef COMPONENTS_LUA_OMWSCRIPTSPARSER_H
#define COMPONENTS_LUA_OMWSCRIPTSPARSER_H
#include <components/vfs/manager.hpp>
namespace LuaUtil
{
// Parses list of `*.omwscripts` files.
std::vector<std::string> parseOMWScriptsFiles(const VFS::Manager* vfs, const std::vector<std::string>& scriptLists);
}
#endif // COMPONENTS_LUA_OMWSCRIPTSPARSER_H

@ -0,0 +1,428 @@
#include "scriptscontainer.hpp"
#include <components/esm/luascripts.hpp>
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<std::string_view>();
if (sectionName == ENGINE_HANDLERS)
parseEngineHandlers(value, path);
else if (sectionName == EVENT_HANDLERS)
parseEventHandlers(value, path);
else if (sectionName == INTERFACE_NAME)
interfaceName = value.as<std::string>();
else if (sectionName == INTERFACE)
publicInterface = value.as<sol::table>();
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<sol::table>()[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<std::string_view>();
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<std::string_view>() == 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<std::string_view>();
auto handlerIter = mEngineHandlers.find(handlerName);
if (handlerIter == mEngineHandlers.end())
continue;
std::vector<sol::protected_function>& list = handlerIter->second->mList;
list.erase(std::find(list.begin(), list.end(), value.as<sol::protected_function>()));
}
}
sol::object eventHandlers = getFieldOrNil(script, EVENT_HANDLERS);
if (eventHandlers != sol::nil)
{
for (auto& [key, value] : sol::table(eventHandlers))
{
EventHandlerList& list = mEventHandlers.find(key.as<std::string_view>())->second;
list.erase(std::find(list.begin(), list.end(), value.as<sol::protected_function>()));
}
}
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<std::string_view>();
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<std::string_view>();
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<bool>())
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<EngineHandlerList*> handlers)
{
for (EngineHandlerList* h : handlers)
mEngineHandlers[h->mName] = h;
}
void ScriptsContainer::save(ESM::LuaScripts& data)
{
std::map<std::string, std::vector<ESM::LuaTimer>> 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<std::string>(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<std::string, Script> 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<sol::table>(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<sol::table>(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<Timer>& 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<std::string>(t.mCallback);
sol::object callback = data[REGISTERED_TIMER_CALLBACKS][callbackName];
if (!callback.is<sol::function>())
throw std::logic_error("Callback '" + callbackName + "' doesn't exist");
LuaUtil::call(callback, t.mArg);
}
else
{
int64_t id = std::get<int64_t>(t.mCallback);
sol::table callbacks = data[TEMPORARY_TIMER_CALLBACKS];
sol::object callback = callbacks[id];
if (!callback.is<sol::function>())
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<Timer>& 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);
}
}

@ -0,0 +1,213 @@
#ifndef COMPONENTS_LUA_SCRIPTSCONTAINER_H
#define COMPONENTS_LUA_SCRIPTSCONTAINER_H
#include <map>
#include <set>
#include <string>
#include <components/debug/debuglog.hpp>
#include <components/esm/luascripts.hpp>
#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<sol::protected_function> mList;
// "name" must be string literal
explicit EngineHandlerList(std::string_view name) : mName(name) {}
};
// Calls given handlers in direct order.
template <typename... Args>
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<EngineHandlerList*> 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<std::string, int64_t> 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<sol::protected_function>;
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<Timer>& timerQueue, double time);
static void insertTimer(std::vector<Timer>& timerQueue, Timer&& t);
const UserdataSerializer* mSerializer = nullptr;
std::map<std::string, sol::object> API;
std::vector<std::string> mScriptOrder;
std::map<std::string, Script> mScripts;
sol::table mPublicInterfaces;
EngineHandlerList mUpdateHandlers{"onUpdate"};
std::map<std::string_view, EngineHandlerList*> mEngineHandlers;
std::map<std::string, EventHandlerList, std::less<>> mEventHandlers;
std::vector<Timer> mSecondsTimersQueue;
std::vector<Timer> mHoursTimersQueue;
int64_t mTemporaryCallbackCounter = 0;
};
}
#endif // COMPONENTS_LUA_SCRIPTSCONTAINER_H

@ -0,0 +1,257 @@
#include "serialization.hpp"
#include <osg/Vec2f>
#include <osg/Vec3f>
#include <components/misc/endianness.hpp>
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<char>(type));
}
template <typename T>
static void appendValue(BinaryData& out, T v)
{
v = Misc::toLittleEndian(v);
out.append(reinterpret_cast<const char*>(&v), sizeof(v));
}
template <typename T>
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<uint32_t>(out, str.size());
}
out.append(str.data(), str.size());
}
static void appendData(BinaryData& out, const void* data, size_t dataSize)
{
out.append(reinterpret_cast<const char*>(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<uint32_t>(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<osg::Vec2f>())
{
appendType(out, SerializedType::VEC2);
osg::Vec2f v = data.as<osg::Vec2f>();
appendValue<float>(out, v.x());
appendValue<float>(out, v.y());
return;
}
if (data.is<osg::Vec3f>())
{
appendType(out, SerializedType::VEC3);
osg::Vec3f v = data.as<osg::Vec3f>();
appendValue<float>(out, v.x());
appendValue<float>(out, v.y());
appendValue<float>(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<sol::function>())
throw std::runtime_error("functions are not allowed to be serialized");
else if (obj.is<sol::userdata>())
serializeUserdata(out, obj, customSerializer);
else if (obj.is<sol::lua_table>())
{
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<double>())
{
appendType(out, SerializedType::NUMBER);
appendValue<double>(out, obj.as<double>());
}
else if (obj.is<std::string_view>())
appendString(out, obj.as<std::string_view>());
else if (obj.is<bool>())
{
char v = obj.as<bool>() ? 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<uint32_t>(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<std::string_view>(lua.lua_state(), binaryData.substr(0, size));
binaryData = binaryData.substr(size);
return;
}
switch (static_cast<SerializedType>(type))
{
case SerializedType::NUMBER:
sol::stack::push<double>(lua.lua_state(), getValue<double>(binaryData));
return;
case SerializedType::BOOLEAN:
sol::stack::push<bool>(lua.lua_state(), getValue<char>(binaryData) != 0);
return;
case SerializedType::LONG_STRING:
{
uint32_t size = getValue<uint32_t>(binaryData);
sol::stack::push<std::string_view>(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<float>(binaryData);
float y = getValue<float>(binaryData);
sol::stack::push<osg::Vec2f>(lua.lua_state(), osg::Vec2f(x, y));
return;
}
case SerializedType::VEC3:
{
float x = getValue<float>(binaryData);
float y = getValue<float>(binaryData);
float z = getValue<float>(binaryData);
sol::stack::push<osg::Vec3f>(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<unsigned>(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<sol::object>(lua.lua_state());
}
}

@ -0,0 +1,34 @@
#ifndef COMPONENTS_LUA_SERIALIZATION_H
#define COMPONENTS_LUA_SERIALIZATION_H
#include <sol/sol.hpp>
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

@ -0,0 +1,98 @@
#include "utilpackage.hpp"
#include <algorithm>
#include <sstream>
#include <osg/Vec3f>
#include <components/misc/mathutil.hpp>
namespace sol
{
template <>
struct is_automagical<osg::Vec2f> : std::false_type {};
template <>
struct is_automagical<osg::Vec3f> : 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<osg::Vec2f> vec2Type = lua.new_usertype<osg::Vec2f>("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<osg::Vec3f> vec3Type = lua.new_usertype<osg::Vec3f>("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<float>` causes error 'AddressSanitizer: stack-use-after-scope'
util["normalizeAngle"] = &Misc::normalizeAngle;
return util;
}
}

@ -0,0 +1,13 @@
#ifndef COMPONENTS_LUA_UTILPACKAGE_H
#define COMPONENTS_LUA_UTILPACKAGE_H
#include <sol/sol.hpp>
namespace LuaUtil
{
sol::table initUtilPackage(sol::state&);
}
#endif // COMPONENTS_LUA_UTILPACKAGE_H

@ -0,0 +1,118 @@
#include "luabindings.hpp"
namespace sol
{
template <>
struct is_automagical<Queries::Field> : std::false_type {};
template <>
struct is_automagical<Queries::Filter> : std::false_type {};
template <>
struct is_automagical<Queries::Query> : std::false_type {};
}
namespace Queries
{
template <Condition::Type type>
struct CondBuilder
{
Filter operator()(const Field& field, const sol::object& o)
{
FieldValue value;
if (field.type() == typeid(bool) && o.is<bool>())
value = o.as<bool>();
else if (field.type() == typeid(int32_t) && o.is<int32_t>())
value = o.as<int32_t>();
else if (field.type() == typeid(int64_t) && o.is<int64_t>())
value = o.as<int64_t>();
else if (field.type() == typeid(float) && o.is<float>())
value = o.as<float>();
else if (field.type() == typeid(double) && o.is<double>())
value = o.as<double>();
else if (field.type() == typeid(std::string) && o.is<std::string>())
value = o.as<std::string>();
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> field = lua.new_usertype<Field>("QueryField");
sol::usertype<Filter> filter = lua.new_usertype<Filter>("QueryFilter");
sol::usertype<Query> query = lua.new_usertype<Query>("Query");
field[sol::meta_function::to_string] = [](const Field& f) { return f.toString(); };
field["eq"] = CondBuilder<Condition::EQUAL>();
field["neq"] = CondBuilder<Condition::NOT_EQUAL>();
field["lt"] = CondBuilder<Condition::LESSER>();
field["lte"] = CondBuilder<Condition::LESSER_OR_EQUAL>();
field["gt"] = CondBuilder<Condition::GREATER>();
field["gte"] = CondBuilder<Condition::GREATER_OR_EQUAL>();
field["like"] = CondBuilder<Condition::LIKE>();
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;
};
}
}

@ -0,0 +1,8 @@
#include <sol/sol.hpp>
#include "query.hpp"
namespace Queries
{
void registerQueryBindings(sol::state& lua);
}

@ -0,0 +1,185 @@
#include "query.hpp"
#include <sstream>
#include <iomanip>
namespace Queries
{
Field::Field(std::vector<std::string> 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<decltype(arg)>;
if constexpr (std::is_same_v<T, std::string>)
{
std::ostringstream oss;
oss << std::quoted(arg);
return oss.str();
}
else if constexpr (std::is_same_v<T, bool>)
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<std::string> 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;
}
}

@ -0,0 +1,99 @@
#ifndef COMPONENTS_QUERIES_QUERY
#define COMPONENTS_QUERIES_QUERY
#include <string>
#include <vector>
#include <typeindex>
#include <variant>
#include <stdexcept>
namespace Queries
{
class Field
{
public:
Field(std::vector<std::string> path, std::type_index type);
const std::vector<std::string>& path() const { return mPath; }
const std::type_index type() const { return mType; }
std::string toString() const;
private:
std::vector<std::string> mPath;
std::type_index mType;
};
struct OrderBy
{
const Field* mField;
bool mDescending;
};
using FieldValue = std::variant<bool, int32_t, int64_t, float, double, std::string>;
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<Condition> mConditions;
std::vector<Operation> 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<OrderBy> mOrderBy;
std::vector<const Field*> mGroupBy;
int64_t mOffset = 0;
int64_t mLimit = sNoLimit;
};
}
#endif // !COMPONENTS_QUERIES_QUERY

@ -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;}

@ -13,6 +13,7 @@
# serve to show the default. # serve to show the default.
import os import os
import sys import sys
import subprocess
# If extensions (or modules to document with autodoc) are in another directory, # 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 # 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): def setup(app):
app.add_stylesheet('figures.css') 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 # The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation". # "<project> v<release> documentation".

@ -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

@ -6,4 +6,5 @@ Reference Material
:maxdepth: 2 :maxdepth: 2
modding/index modding/index
documentationHowTo lua-scripting/index
documentationHowTo

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save