From 479856f8123cbcfe80e3f768bf8d4069070409ec Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Tue, 12 Jan 2021 23:18:49 +0100 Subject: [PATCH] Add components/lua/scriptscontainer and components/esm/luascripts --- apps/openmw_test_suite/CMakeLists.txt | 1 + .../lua/test_scriptscontainer.cpp | 375 +++++++++++++++ components/CMakeLists.txt | 4 +- components/esm/luascripts.cpp | 80 ++++ components/esm/luascripts.hpp | 47 ++ components/lua/scriptscontainer.cpp | 428 ++++++++++++++++++ components/lua/scriptscontainer.hpp | 212 +++++++++ 7 files changed, 1145 insertions(+), 2 deletions(-) create mode 100644 apps/openmw_test_suite/lua/test_scriptscontainer.cpp create mode 100644 components/esm/luascripts.cpp create mode 100644 components/esm/luascripts.hpp create mode 100644 components/lua/scriptscontainer.cpp create mode 100644 components/lua/scriptscontainer.hpp diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index 8c54af0e00..f9d0e8140f 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -16,6 +16,7 @@ if (GTEST_FOUND AND GMOCK_FOUND) esm/variant.cpp lua/test_lua.cpp + lua/test_scriptscontainer.cpp lua/test_utilpackage.cpp lua/test_serialization.cpp diff --git a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp new file mode 100644 index 0000000000..072ad334e8 --- /dev/null +++ b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp @@ -0,0 +1,375 @@ +#include "gmock/gmock.h" +#include + +#include + +#include +#include + +#include "testing_util.hpp" + +namespace +{ + using namespace testing; + + TestFile invalidScript("not a script"); + TestFile incorrectScript("return { incorrectSection = {}, engineHandlers = { incorrectHandler = function() end } }"); + TestFile emptyScript(""); + + TestFile testScript(R"X( +return { + engineHandlers = { onUpdate = function(dt) print(' update ' .. tostring(dt)) end }, + eventHandlers = { + Event1 = function(eventData) print(' event1 ' .. tostring(eventData.x)) end, + Event2 = function(eventData) print(' event2 ' .. tostring(eventData.x)) end, + Print = function() print('print') end + } +} +)X"); + + TestFile stopEventScript(R"X( +return { + eventHandlers = { + Event1 = function(eventData) + print(' event1 ' .. tostring(eventData.x)) + return eventData.x >= 1 + end + } +} +)X"); + + TestFile loadSaveScript(R"X( +x = 0 +y = 0 +return { + engineHandlers = { + onSave = function(state) + return {x = x, y = y} + end, + onLoad = function(state) + x, y = state.x, state.y + end + }, + eventHandlers = { + Set = function(eventData) + eventData.n = eventData.n - 1 + if eventData.n == 0 then + x, y = eventData.x, eventData.y + end + end, + Print = function() + print(x, y) + end + } +} +)X"); + + TestFile interfaceScript(R"X( +return { + interfaceName = "TestInterface", + interface = { + fn = function(x) print('FN', x) end, + value = 3.5 + }, +} +)X"); + + TestFile overrideInterfaceScript(R"X( +local old = require('openmw.interfaces').TestInterface +return { + interfaceName = "TestInterface", + interface = { + fn = function(x) + print('NEW FN', x) + old.fn(x) + end, + value = old.value + 1 + }, +} +)X"); + + TestFile useInterfaceScript(R"X( +local interfaces = require('openmw.interfaces') +return { + engineHandlers = { + onUpdate = function() + interfaces.TestInterface.fn(interfaces.TestInterface.value) + end, + }, +} +)X"); + + struct LuaScriptsContainerTest : Test + { + std::unique_ptr mVFS = createTestVFS({ + {"invalid.lua", &invalidScript}, + {"incorrect.lua", &incorrectScript}, + {"empty.lua", &emptyScript}, + {"test1.lua", &testScript}, + {"test2.lua", &testScript}, + {"stopEvent.lua", &stopEventScript}, + {"loadSave1.lua", &loadSaveScript}, + {"loadSave2.lua", &loadSaveScript}, + {"testInterface.lua", &interfaceScript}, + {"overrideInterface.lua", &overrideInterfaceScript}, + {"useInterface.lua", &useInterfaceScript}, + }); + + LuaUtil::LuaState mLua{mVFS.get()}; + }; + + TEST_F(LuaScriptsContainerTest, VerifyStructure) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + { + testing::internal::CaptureStdout(); + EXPECT_FALSE(scripts.addNewScript("invalid.lua")); + std::string output = testing::internal::GetCapturedStdout(); + EXPECT_THAT(output, HasSubstr("Can't start Test[invalid.lua]")); + } + { + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.addNewScript("incorrect.lua")); + std::string output = testing::internal::GetCapturedStdout(); + EXPECT_THAT(output, HasSubstr("Not supported handler 'incorrectHandler' in Test[incorrect.lua]")); + EXPECT_THAT(output, HasSubstr("Not supported section 'incorrectSection' in Test[incorrect.lua]")); + } + { + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.addNewScript("empty.lua")); + EXPECT_FALSE(scripts.addNewScript("empty.lua")); // already present + EXPECT_EQ(internal::GetCapturedStdout(), ""); + } + } + + TEST_F(LuaScriptsContainerTest, CallHandler) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.addNewScript("test1.lua")); + EXPECT_TRUE(scripts.addNewScript("stopEvent.lua")); + EXPECT_TRUE(scripts.addNewScript("test2.lua")); + scripts.update(1.5f); + EXPECT_EQ(internal::GetCapturedStdout(), "Test[test1.lua]:\t update 1.5\n" + "Test[test2.lua]:\t update 1.5\n"); + } + + TEST_F(LuaScriptsContainerTest, CallEvent) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + EXPECT_TRUE(scripts.addNewScript("test1.lua")); + EXPECT_TRUE(scripts.addNewScript("stopEvent.lua")); + EXPECT_TRUE(scripts.addNewScript("test2.lua")); + + std::string X0 = LuaUtil::serialize(mLua.sol().create_table_with("x", 0.5)); + std::string X1 = LuaUtil::serialize(mLua.sol().create_table_with("x", 1.5)); + + { + testing::internal::CaptureStdout(); + scripts.receiveEvent("SomeEvent", X1); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test has received event 'SomeEvent', but there are no handlers for this event\n"); + } + { + testing::internal::CaptureStdout(); + scripts.receiveEvent("Event1", X1); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test2.lua]:\t event1 1.5\n" + "Test[stopEvent.lua]:\t event1 1.5\n" + "Test[test1.lua]:\t event1 1.5\n"); + } + { + testing::internal::CaptureStdout(); + scripts.receiveEvent("Event2", X1); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test2.lua]:\t event2 1.5\n" + "Test[test1.lua]:\t event2 1.5\n"); + } + { + testing::internal::CaptureStdout(); + scripts.receiveEvent("Event1", X0); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test2.lua]:\t event1 0.5\n" + "Test[stopEvent.lua]:\t event1 0.5\n"); + } + { + testing::internal::CaptureStdout(); + scripts.receiveEvent("Event2", X0); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test2.lua]:\t event2 0.5\n" + "Test[test1.lua]:\t event2 0.5\n"); + } + } + + TEST_F(LuaScriptsContainerTest, RemoveScript) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + EXPECT_TRUE(scripts.addNewScript("test1.lua")); + EXPECT_TRUE(scripts.addNewScript("stopEvent.lua")); + EXPECT_TRUE(scripts.addNewScript("test2.lua")); + std::string X = LuaUtil::serialize(mLua.sol().create_table_with("x", 0.5)); + + { + testing::internal::CaptureStdout(); + scripts.update(1.5f); + scripts.receiveEvent("Event1", X); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test1.lua]:\t update 1.5\n" + "Test[test2.lua]:\t update 1.5\n" + "Test[test2.lua]:\t event1 0.5\n" + "Test[stopEvent.lua]:\t event1 0.5\n"); + } + { + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.removeScript("stopEvent.lua")); + EXPECT_FALSE(scripts.removeScript("stopEvent.lua")); // already removed + scripts.update(1.5f); + scripts.receiveEvent("Event1", X); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test1.lua]:\t update 1.5\n" + "Test[test2.lua]:\t update 1.5\n" + "Test[test2.lua]:\t event1 0.5\n" + "Test[test1.lua]:\t event1 0.5\n"); + } + { + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.removeScript("test1.lua")); + scripts.update(1.5f); + scripts.receiveEvent("Event1", X); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test2.lua]:\t update 1.5\n" + "Test[test2.lua]:\t event1 0.5\n"); + } + } + + TEST_F(LuaScriptsContainerTest, Interface) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.addNewScript("testInterface.lua")); + EXPECT_TRUE(scripts.addNewScript("overrideInterface.lua")); + EXPECT_TRUE(scripts.addNewScript("useInterface.lua")); + scripts.update(1.5f); + EXPECT_TRUE(scripts.removeScript("overrideInterface.lua")); + scripts.update(1.5f); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[overrideInterface.lua]:\tNEW FN\t4.5\n" + "Test[testInterface.lua]:\tFN\t4.5\n" + "Test[testInterface.lua]:\tFN\t3.5\n"); + } + + TEST_F(LuaScriptsContainerTest, LoadSave) + { + LuaUtil::ScriptsContainer scripts1(&mLua, "Test"); + LuaUtil::ScriptsContainer scripts2(&mLua, "Test"); + LuaUtil::ScriptsContainer scripts3(&mLua, "Test"); + + EXPECT_TRUE(scripts1.addNewScript("loadSave1.lua")); + EXPECT_TRUE(scripts1.addNewScript("test1.lua")); + EXPECT_TRUE(scripts1.addNewScript("loadSave2.lua")); + + EXPECT_TRUE(scripts3.addNewScript("test2.lua")); + EXPECT_TRUE(scripts3.addNewScript("loadSave2.lua")); + + scripts1.receiveEvent("Set", LuaUtil::serialize(mLua.sol().create_table_with( + "n", 1, + "x", 0.5, + "y", 3.5))); + scripts1.receiveEvent("Set", LuaUtil::serialize(mLua.sol().create_table_with( + "n", 2, + "x", 2.5, + "y", 1.5))); + + ESM::LuaScripts data; + scripts1.save(data); + scripts2.load(data, true); + scripts3.load(data, false); + + { + testing::internal::CaptureStdout(); + scripts2.receiveEvent("Print", ""); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[loadSave2.lua]:\t0.5\t3.5\n" + "Test[test1.lua]:\tprint\n" + "Test[loadSave1.lua]:\t2.5\t1.5\n"); + } + { + testing::internal::CaptureStdout(); + scripts3.receiveEvent("Print", ""); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[loadSave2.lua]:\t0.5\t3.5\n" + "Test[test2.lua]:\tprint\n"); + } + } + + TEST_F(LuaScriptsContainerTest, Timers) + { + 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(false, 10, "test1.lua", "B", sol::make_object(mLua.sol(), 3)); + scripts.setupSerializableTimer(true, 10, "test2.lua", "B", sol::make_object(mLua.sol(), 4)); + scripts.setupSerializableTimer(false, 5, "test1.lua", "A", sol::make_object(mLua.sol(), 1)); + scripts.setupSerializableTimer(true, 5, "test2.lua", "A", sol::make_object(mLua.sol(), 2)); + scripts.setupSerializableTimer(false, 15, "test1.lua", "A", sol::make_object(mLua.sol(), 10)); + scripts.setupSerializableTimer(false, 15, "test1.lua", "B", sol::make_object(mLua.sol(), 20)); + + scripts.setupUnsavableTimer(false, 10, "test2.lua", fn2); + scripts.setupUnsavableTimer(true, 10, "test1.lua", fn2); + scripts.setupUnsavableTimer(false, 5, "test2.lua", fn1); + scripts.setupUnsavableTimer(true, 5, "test1.lua", fn1); + scripts.setupUnsavableTimer(false, 15, "test2.lua", fn1); + + EXPECT_EQ(counter1, 0); + EXPECT_EQ(counter3, 0); + + scripts.processTimers(6, 4); + + EXPECT_EQ(counter1, 1); + EXPECT_EQ(counter3, 1); + EXPECT_EQ(counter4, 0); + + scripts.processTimers(6, 8); + + EXPECT_EQ(counter1, 2); + EXPECT_EQ(counter2, 0); + EXPECT_EQ(counter3, 1); + EXPECT_EQ(counter4, 2); + + scripts.processTimers(11, 12); + + EXPECT_EQ(counter1, 2); + EXPECT_EQ(counter2, 2); + EXPECT_EQ(counter3, 5); + EXPECT_EQ(counter4, 5); + + ESM::LuaScripts data; + scripts.save(data); + scripts.load(data, true); + scripts.registerTimerCallback("test1.lua", "B", fn4); + + testing::internal::CaptureStdout(); + scripts.processTimers(20, 20); + EXPECT_EQ(internal::GetCapturedStdout(), "Test[test1.lua] callTimer failed: Callback 'A' doesn't exist\n"); + + EXPECT_EQ(counter1, 2); + EXPECT_EQ(counter2, 2); + EXPECT_EQ(counter3, 5); + EXPECT_EQ(counter4, 25); + } + +} diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 6724a9fa29..8b66baab28 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -29,7 +29,7 @@ endif (GIT_CHECKOUT) # source files add_component_dir (lua - luastate utilpackage serialization + luastate scriptscontainer utilpackage serialization ) add_component_dir (settings @@ -84,7 +84,7 @@ add_component_dir (esm loadweap records aipackage effectlist spelllist variant variantimp loadtes3 cellref filter savedgame journalentry queststate locals globalscript player objectstate cellid cellstate globalmap inventorystate containerstate npcstate creaturestate dialoguestate statstate npcstats creaturestats weatherstate quickkeys fogstate spellstate activespells creaturelevliststate doorstate projectilestate debugprofile - aisequence magiceffects util custommarkerstate stolenitems transport animationstate controlsstate mappings + aisequence magiceffects util custommarkerstate stolenitems transport animationstate controlsstate mappings luascripts ) add_component_dir (esmterrain diff --git a/components/esm/luascripts.cpp b/components/esm/luascripts.cpp new file mode 100644 index 0000000000..41d25ee8be --- /dev/null +++ b/components/esm/luascripts.cpp @@ -0,0 +1,80 @@ +#include "luascripts.hpp" + +#include "esmreader.hpp" +#include "esmwriter.hpp" + +// List of all records, that are related to Lua. +// +// Record: +// LUAM - MWLua::LuaManager +// +// Subrecords: +// LUAW - Start of MWLua::WorldView data +// LUAE - Start of MWLua::LocalEvent or MWLua::GlobalEvent (eventName) +// LUAS - Start LuaUtil::ScriptsContainer data (scriptName) +// LUAD - Serialized Lua variable +// LUAT - MWLua::ScriptsContainer::Timer +// LUAC - Name of a timer callback (string) + +void ESM::saveLuaBinaryData(ESMWriter& esm, const std::string& data) +{ + if (data.empty()) + return; + esm.startSubRecord("LUAD"); + esm.write(data.data(), data.size()); + esm.endRecord("LUAD"); +} + +std::string ESM::loadLuaBinaryData(ESMReader& esm) +{ + std::string data; + if (esm.isNextSub("LUAD")) + { + esm.getSubHeader(); + data.resize(esm.getSubSize()); + esm.getExact(data.data(), data.size()); + } + return data; +} + +void ESM::LuaScripts::load(ESMReader& esm) +{ + while (esm.isNextSub("LUAS")) + { + std::string name = esm.getHString(); + std::string data = loadLuaBinaryData(esm); + std::vector timers; + while (esm.isNextSub("LUAT")) + { + esm.getSubHeader(); + LuaTimer timer; + esm.getT(timer.mHours); + 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.mHours); + esm.writeT(timer.mTime); + esm.endRecord("LUAT"); + esm.writeHNString("LUAC", timer.mCallbackName); + if (!timer.mCallbackArgument.empty()) + saveLuaBinaryData(esm, timer.mCallbackArgument); + + } + } +} diff --git a/components/esm/luascripts.hpp b/components/esm/luascripts.hpp new file mode 100644 index 0000000000..ef3553e2fb --- /dev/null +++ b/components/esm/luascripts.hpp @@ -0,0 +1,47 @@ +#ifndef OPENMW_ESM_LUASCRIPTS_H +#define OPENMW_ESM_LUASCRIPTS_H + +#include +#include + +namespace ESM +{ + class ESMReader; + class ESMWriter; + + // Storage structure for LuaUtil::ScriptsContainer. This is not a top-level record. + // Used either for global scripts or for local scripts on a specific object. + + struct LuaTimer + { + double mTime; + bool mHours; // false - game seconds, true - game hours + std::string mCallbackName; + std::string mCallbackArgument; // Serialized Lua table. It is a binary data. Can contain '\0'. + }; + + struct LuaScript + { + std::string mScriptPath; + std::string mData; // Serialized Lua table. It is a binary data. Can contain '\0'. + std::vector mTimers; + }; + + struct LuaScripts + { + std::vector mScripts; + + void load (ESMReader &esm); + void save (ESMWriter &esm) const; + }; + + // Saves binary string `data` (can contain '\0') as record LUAD. + void saveLuaBinaryData(ESM::ESMWriter& esm, const std::string& data); + + // Loads LUAD as binary string. If next subrecord is not LUAD, then returns an empty string. + std::string loadLuaBinaryData(ESM::ESMReader& esm); + +} + +#endif + diff --git a/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp new file mode 100644 index 0000000000..cb2ab0a97e --- /dev/null +++ b/components/lua/scriptscontainer.cpp @@ -0,0 +1,428 @@ +#include "scriptscontainer.hpp" + +#include + +namespace LuaUtil +{ + static constexpr std::string_view ENGINE_HANDLERS = "engineHandlers"; + static constexpr std::string_view EVENT_HANDLERS = "eventHandlers"; + + static constexpr std::string_view INTERFACE_NAME = "interfaceName"; + static constexpr std::string_view INTERFACE = "interface"; + + static constexpr std::string_view HANDLER_SAVE = "onSave"; + static constexpr std::string_view HANDLER_LOAD = "onLoad"; + + static constexpr std::string_view REGISTERED_TIMER_CALLBACKS = "_timers"; + static constexpr std::string_view TEMPORARY_TIMER_CALLBACKS = "_temp_timers"; + + ScriptsContainer::ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix) : mNamePrefix(namePrefix), mLua(*lua) + { + registerEngineHandlers({&mUpdateHandlers}); + mPublicInterfaces = sol::table(lua->sol(), sol::create); + addPackage("openmw.interfaces", mPublicInterfaces); + } + + void ScriptsContainer::addPackage(const std::string& packageName, sol::object package) + { + API[packageName] = mLua.makeReadOnly(std::move(package)); + } + + bool ScriptsContainer::addNewScript(const std::string& path) + { + if (mScripts.count(path) != 0) + return false; // already present + + try + { + sol::table hiddenData(mLua.sol(), sol::create); + hiddenData[ScriptId::KEY] = ScriptId{this, path}; + hiddenData[REGISTERED_TIMER_CALLBACKS] = mLua.newTable(); + hiddenData[TEMPORARY_TIMER_CALLBACKS] = mLua.newTable(); + mScripts[path].mHiddenData = hiddenData; + sol::object script = mLua.runInNewSandbox(path, mNamePrefix, API, hiddenData); + std::string interfaceName = ""; + sol::object publicInterface = sol::nil; + if (script != sol::nil) + { + for (auto& [key, value] : sol::table(script)) + { + std::string_view sectionName = key.as(); + if (sectionName == ENGINE_HANDLERS) + parseEngineHandlers(value, path); + else if (sectionName == EVENT_HANDLERS) + parseEventHandlers(value, path); + else if (sectionName == INTERFACE_NAME) + interfaceName = value.as(); + else if (sectionName == INTERFACE) + publicInterface = value.as(); + else + Log(Debug::Error) << "Not supported section '" << sectionName << "' in " << mNamePrefix << "[" << path << "]"; + } + } + if (interfaceName.empty() != (publicInterface == sol::nil)) + Log(Debug::Error) << mNamePrefix << "[" << path << "]: 'interfaceName' should always be used together with 'interface'"; + else if (!interfaceName.empty()) + script.as()[INTERFACE] = mPublicInterfaces[interfaceName] = mLua.makeReadOnly(publicInterface); + mScriptOrder.push_back(path); + mScripts[path].mInterface = std::move(script); + return true; + } + catch (std::exception& e) + { + mScripts.erase(path); + Log(Debug::Error) << "Can't start " << mNamePrefix << "[" << path << "]; " << e.what(); + return false; + } + } + + bool ScriptsContainer::removeScript(const std::string& path) + { + auto it = mScripts.find(path); + if (it == mScripts.end()) + return false; // no such script + sol::object& script = it->second.mInterface; + if (getFieldOrNil(script, INTERFACE_NAME) != sol::nil) + { + std::string_view interfaceName = getFieldOrNil(script, INTERFACE_NAME).as(); + if (mPublicInterfaces[interfaceName] == getFieldOrNil(script, INTERFACE)) + { + mPublicInterfaces[interfaceName] = sol::nil; + auto prevIt = mScriptOrder.rbegin(); + while (*prevIt != path) + prevIt++; + prevIt++; + while (prevIt != mScriptOrder.rend()) + { + sol::object& prevScript = mScripts[*(prevIt++)].mInterface; + sol::object prevInterfaceName = getFieldOrNil(prevScript, INTERFACE_NAME); + if (prevInterfaceName != sol::nil && prevInterfaceName.as() == interfaceName) + { + mPublicInterfaces[interfaceName] = getFieldOrNil(prevScript, INTERFACE); + break; + } + } + } + } + sol::object engineHandlers = getFieldOrNil(script, ENGINE_HANDLERS); + if (engineHandlers != sol::nil) + { + for (auto& [key, value] : sol::table(engineHandlers)) + { + std::string_view handlerName = key.as(); + auto it = mEngineHandlers.find(handlerName); + if (it == mEngineHandlers.end()) + continue; + std::vector& list = it->second->mList; + list.erase(std::find(list.begin(), list.end(), value.as())); + } + } + sol::object eventHandlers = getFieldOrNil(script, EVENT_HANDLERS); + if (eventHandlers != sol::nil) + { + for (auto& [key, value] : sol::table(eventHandlers)) + { + EventHandlerList& list = mEventHandlers.find(key.as())->second; + list.erase(std::find(list.begin(), list.end(), value.as())); + } + } + mScripts.erase(it); + mScriptOrder.erase(std::find(mScriptOrder.begin(), mScriptOrder.end(), path)); + return true; + } + + void ScriptsContainer::parseEventHandlers(sol::table handlers, std::string_view scriptPath) + { + for (auto& [key, value] : handlers) + { + std::string_view eventName = key.as(); + auto it = mEventHandlers.find(eventName); + if (it == mEventHandlers.end()) + it = mEventHandlers.insert({std::string(eventName), EventHandlerList()}).first; + it->second.push_back(value); + } + } + + void ScriptsContainer::parseEngineHandlers(sol::table handlers, std::string_view scriptPath) + { + for (auto& [key, value] : handlers) + { + std::string_view handlerName = key.as(); + if (handlerName == HANDLER_LOAD || handlerName == HANDLER_SAVE) + continue; // save and load are handled separately + auto it = mEngineHandlers.find(handlerName); + if (it == mEngineHandlers.end()) + Log(Debug::Error) << "Not supported handler '" << handlerName << "' in " << mNamePrefix << "[" << scriptPath << "]"; + else + it->second->mList.push_back(value); + } + } + + void ScriptsContainer::receiveEvent(std::string_view eventName, std::string_view eventData) + { + auto it = mEventHandlers.find(eventName); + if (it == mEventHandlers.end()) + { + Log(Debug::Warning) << mNamePrefix << " has received event '" << eventName << "', but there are no handlers for this event"; + return; + } + sol::object data; + try + { + data = LuaUtil::deserialize(mLua.sol(), eventData, mSerializer); + } + catch (std::exception& e) + { + Log(Debug::Error) << mNamePrefix << " can not parse eventData for '" << eventName << "': " << e.what(); + return; + } + EventHandlerList& list = it->second; + for (int i = list.size() - 1; i >= 0; --i) + { + try + { + sol::object res = LuaUtil::call(list[i], data); + if (res != sol::nil && !res.as()) + break; // Skip other handlers if 'false' was returned. + } + catch (std::exception& e) + { + Log(Debug::Error) << mNamePrefix << " eventHandler[" << eventName << "] failed. " << e.what(); + } + } + } + + void ScriptsContainer::registerEngineHandlers(std::initializer_list handlers) + { + for (EngineHandlerList* h : handlers) + mEngineHandlers[h->mName] = h; + } + + void ScriptsContainer::save(ESM::LuaScripts& data) + { + std::map> timers; + auto saveTimerFn = [&](const Timer& timer, bool inHours) + { + if (!timer.mSerializable) + return; + ESM::LuaTimer savedTimer; + savedTimer.mTime = timer.mTime; + savedTimer.mHours = inHours; + savedTimer.mCallbackName = std::get(timer.mCallback); + savedTimer.mCallbackArgument = timer.mSerializedArg; + if (timers.count(timer.mScript) == 0) + timers[timer.mScript] = {}; + timers[timer.mScript].push_back(std::move(savedTimer)); + }; + for (const Timer& timer : mSecondsTimersQueue) + saveTimerFn(timer, false); + for (const Timer& timer : mHoursTimersQueue) + saveTimerFn(timer, true); + data.mScripts.clear(); + for (const std::string& path : mScriptOrder) + { + ESM::LuaScript savedScript; + savedScript.mScriptPath = path; + sol::object handler = getFieldOrNil(mScripts[path].mInterface, ENGINE_HANDLERS, HANDLER_SAVE); + if (handler != sol::nil) + { + try + { + sol::object state = LuaUtil::call(handler); + savedScript.mData = serialize(state, mSerializer); + } + catch (std::exception& e) + { + Log(Debug::Error) << mNamePrefix << "[" << path << "] onSave failed: " << e.what(); + } + } + auto timersIt = timers.find(path); + if (timersIt != timers.end()) + savedScript.mTimers = std::move(timersIt->second); + data.mScripts.push_back(std::move(savedScript)); + } + } + + void ScriptsContainer::load(const ESM::LuaScripts& data, bool resetScriptList) + { + std::map scriptsWithoutSavedData; + if (resetScriptList) + { + removeAllScripts(); + for (const ESM::LuaScript& script : data.mScripts) + addNewScript(script.mScriptPath); + } + else + scriptsWithoutSavedData = mScripts; + mSecondsTimersQueue.clear(); + mHoursTimersQueue.clear(); + for (const ESM::LuaScript& script : data.mScripts) + { + auto iter = mScripts.find(script.mScriptPath); + if (iter == mScripts.end()) + continue; + scriptsWithoutSavedData.erase(iter->first); + iter->second.mHiddenData.get(TEMPORARY_TIMER_CALLBACKS).clear(); + try + { + sol::object handler = getFieldOrNil(iter->second.mInterface, ENGINE_HANDLERS, HANDLER_LOAD); + if (handler != sol::nil) + { + sol::object state = deserialize(mLua.sol(), script.mData, mSerializer); + LuaUtil::call(handler, state); + } + } + catch (std::exception& e) + { + Log(Debug::Error) << mNamePrefix << "[" << script.mScriptPath << "] onLoad failed: " << e.what(); + } + for (const ESM::LuaTimer& savedTimer : script.mTimers) + { + Timer timer; + timer.mCallback = savedTimer.mCallbackName; + timer.mSerializable = true; + timer.mScript = script.mScriptPath; + timer.mTime = savedTimer.mTime; + + try + { + timer.mArg = deserialize(mLua.sol(), savedTimer.mCallbackArgument, mSerializer); + // It is important if the order of content files was changed. The deserialize-serialize procedure + // updates refnums, so timer.mSerializedArg may be not equal to savedTimer.mCallbackArgument. + timer.mSerializedArg = serialize(timer.mArg, mSerializer); + + if (savedTimer.mHours) + mHoursTimersQueue.push_back(std::move(timer)); + else + mSecondsTimersQueue.push_back(std::move(timer)); + } + catch (std::exception& e) + { + Log(Debug::Error) << mNamePrefix << "[" << script.mScriptPath << "] can not load timer: " << e.what(); + } + } + } + for (auto& [path, script] : scriptsWithoutSavedData) + { + script.mHiddenData.get(TEMPORARY_TIMER_CALLBACKS).clear(); + sol::object handler = getFieldOrNil(script.mInterface, ENGINE_HANDLERS, HANDLER_LOAD); + if (handler == sol::nil) + continue; + try { LuaUtil::call(handler); } + catch (std::exception& e) + { + Log(Debug::Error) << mNamePrefix << "[" << path << "] onLoad failed: " << e.what(); + } + } + std::make_heap(mSecondsTimersQueue.begin(), mSecondsTimersQueue.end()); + std::make_heap(mHoursTimersQueue.begin(), mHoursTimersQueue.end()); + } + + void ScriptsContainer::removeAllScripts() + { + mScripts.clear(); + mScriptOrder.clear(); + for (auto& [_, handlers] : mEngineHandlers) + handlers->mList.clear(); + mEventHandlers.clear(); + mSecondsTimersQueue.clear(); + mHoursTimersQueue.clear(); + + mPublicInterfaces.clear(); + // Assigned by mLua.makeReadOnly, but `clear` removes it, so we need to assign it again. + mPublicInterfaces[sol::meta_function::index] = mPublicInterfaces; + } + + sol::table ScriptsContainer::getHiddenData(const std::string& scriptPath) + { + auto it = mScripts.find(scriptPath); + if (it == mScripts.end()) + throw std::logic_error("ScriptsContainer::getHiddenData: script doesn't exist"); + return it->second.mHiddenData; + } + + void ScriptsContainer::registerTimerCallback(const std::string& scriptPath, std::string_view callbackName, sol::function callback) + { + getHiddenData(scriptPath)[REGISTERED_TIMER_CALLBACKS][callbackName] = std::move(callback); + } + + void ScriptsContainer::insertTimer(std::vector& timerQueue, Timer&& t) + { + timerQueue.push_back(std::move(t)); + std::push_heap(timerQueue.begin(), timerQueue.end()); + } + + void ScriptsContainer::setupSerializableTimer(bool inHours, 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(inHours ? mHoursTimersQueue : mSecondsTimersQueue, std::move(t)); + } + + void ScriptsContainer::setupUnsavableTimer(bool inHours, 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(inHours ? mHoursTimersQueue : mSecondsTimersQueue, std::move(t)); + } + + void ScriptsContainer::callTimer(const Timer& t) + { + try + { + sol::table data = getHiddenData(t.mScript); + if (t.mSerializable) + { + const std::string& callbackName = std::get(t.mCallback); + sol::object callback = data[REGISTERED_TIMER_CALLBACKS][callbackName]; + if (!callback.is()) + throw std::logic_error("Callback '" + callbackName + "' doesn't exist"); + LuaUtil::call(callback, t.mArg); + } + else + { + int64_t id = std::get(t.mCallback); + sol::table callbacks = data[TEMPORARY_TIMER_CALLBACKS]; + sol::object callback = callbacks[id]; + if (!callback.is()) + throw std::logic_error("Temporary timer callback doesn't exist"); + LuaUtil::call(callback); + callbacks[id] = sol::nil; + } + } + catch (std::exception& e) + { + Log(Debug::Error) << mNamePrefix << "[" << t.mScript << "] callTimer failed: " << e.what(); + } + } + + void ScriptsContainer::updateTimerQueue(std::vector& timerQueue, double time) + { + while (!timerQueue.empty() && timerQueue.front().mTime <= time) + { + callTimer(timerQueue.front()); + std::pop_heap(timerQueue.begin(), timerQueue.end()); + timerQueue.pop_back(); + } + } + + void ScriptsContainer::processTimers(double gameSeconds, double gameHours) + { + updateTimerQueue(mSecondsTimersQueue, gameSeconds); + updateTimerQueue(mHoursTimersQueue, gameHours); + } + +} diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp new file mode 100644 index 0000000000..a5cb7d3e58 --- /dev/null +++ b/components/lua/scriptscontainer.hpp @@ -0,0 +1,212 @@ +#ifndef COMPONENTS_LUA_SCRIPTSCONTAINER_H +#define COMPONENTS_LUA_SCRIPTSCONTAINER_H + +#include +#include +#include + +#include +#include + +#include "luastate.hpp" +#include "serialization.hpp" + +namespace LuaUtil +{ + +// ScriptsContainer is a base class for all scripts containers (LocalScripts, +// GlobalScripts, PlayerScripts, etc). Each script runs in a separate sandbox. +// Scripts from different containers can interact to each other only via events. +// Scripts within one container can interact via interfaces (not implemented yet). +// All scripts from one container have the same set of API packages available. +// +// Each script should return a table in a specific format that describes its +// handlers and interfaces. Every section of the table is optional. Basic structure: +// +// local function update(dt) +// print("Update") +// end +// +// local function someEventHandler(eventData) +// print("'SomeEvent' received") +// end +// +// return { +// -- Provides interface for other scripts in the same container +// interfaceName = "InterfaceName", +// interface = { +// someFunction = function() print("someFunction was called from another script") end, +// }, +// +// -- Script interface for the engine. Not available for other script. +// -- An error is printed if unknown handler is specified. +// engineHandlers = { +// onUpdate = update, +// onSave = function() return ... end, +// onLoad = function(state) ... end, -- "state" is the data that was earlier returned by onSave +// +// -- Works only if ScriptsContainer::registerEngineHandler is overloaded in a child class +// -- and explicitly supports 'onSomethingElse' +// onSomethingElse = function() print("something else") end +// }, +// +// -- Handlers for events, sent from other scripts. Engine itself never sent events. Any name can be used for an event. +// eventHandlers = { +// SomeEvent = someEventHandler +// } +// } + + class ScriptsContainer + { + public: + struct ScriptId + { + // ScriptId is stored in hidden data (see getHiddenData) with this key. + constexpr static std::string_view KEY = "_id"; + + ScriptsContainer* mContainer; + std::string mPath; + }; + + // `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. + // inHours - false if time unit is game seconds and true if time unit if game 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(bool inHours, 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(bool inHours, double time, const std::string& scriptPath, sol::function callback); + + protected: + struct EngineHandlerList + { + std::string_view mName; + std::vector mList; + + // "name" must be string literal + explicit EngineHandlerList(std::string_view name) : mName(name) {} + }; + + // Calls given handlers in direct order. + template + void callEngineHandlers(EngineHandlerList& handlers, const Args&... args) + { + for (sol::protected_function& handler : handlers.mList) + { + try { LuaUtil::call(handler, args...); } + catch (std::exception& e) + { + Log(Debug::Error) << mNamePrefix << " " << handlers.mName << " failed. " << e.what(); + } + } + } + + // To add a new engine handler a derived class should register the corresponding EngineHandlerList and define + // a public function (see how ScriptsContainer::update is implemented) that calls `callEngineHandlers`. + void registerEngineHandlers(std::initializer_list handlers); + + const std::string mNamePrefix; + + private: + struct Script + { + sol::object mInterface; // returned value of the script (sol::table or nil) + sol::table mHiddenData; + }; + struct Timer + { + double mTime; + bool mSerializable; + std::string mScript; + std::variant mCallback; // string if serializable, integer otherwise + sol::object mArg; + std::string mSerializedArg; + + bool operator<(const Timer& t) const { return mTime > t.mTime; } + }; + using EventHandlerList = std::vector; + + void parseEngineHandlers(sol::table handlers, std::string_view scriptPath); + void parseEventHandlers(sol::table handlers, std::string_view scriptPath); + + void callTimer(const Timer& t); + void updateTimerQueue(std::vector& timerQueue, double time); + static void insertTimer(std::vector& timerQueue, Timer&& t); + + LuaUtil::LuaState& mLua; + const UserdataSerializer* mSerializer = nullptr; + std::map API; + + std::vector mScriptOrder; + std::map mScripts; + sol::table mPublicInterfaces; + + EngineHandlerList mUpdateHandlers{"onUpdate"}; + std::map mEngineHandlers; + std::map> mEventHandlers; + + std::vector mSecondsTimersQueue; + std::vector mHoursTimersQueue; + int64_t mTemporaryCallbackCounter = 0; + }; + +} + +#endif // COMPONENTS_LUA_SCRIPTSCONTAINER_H