Add components/lua/scriptscontainer and components/esm/luascripts

dont-compose-content
Petr Mikheev 4 years ago
parent 8dbaf6022c
commit 479856f812

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

@ -0,0 +1,375 @@
#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)
{
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);
}
}

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

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

@ -0,0 +1,47 @@
#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
{
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<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

@ -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 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<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 it = mEngineHandlers.find(handlerName);
if (it == mEngineHandlers.end())
continue;
std::vector<sol::protected_function>& list = it->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(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<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, bool inHours)
{
if (!timer.mSerializable)
return;
ESM::LuaTimer savedTimer;
savedTimer.mTime = timer.mTime;
savedTimer.mHours = inHours;
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, 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<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.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<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(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<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,212 @@
#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;
};
// `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<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;
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);
LuaUtil::LuaState& mLua;
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
Loading…
Cancel
Save