mirror of https://github.com/OpenMW/openmw.git
Add components/lua/scriptscontainer and components/esm/luascripts
parent
8dbaf6022c
commit
479856f812
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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…
Reference in New Issue