Lua scripts configuration in omwaddon

This commit is contained in:
Petr Mikheev 2022-05-20 21:47:13 +02:00
parent 58fd560ce9
commit a70d5831c5
26 changed files with 700 additions and 236 deletions

View file

@ -174,7 +174,7 @@ enum RecNameInts : unsigned int
// format 1
REC_FILT = fourCC("FILT"),
REC_DBGP = fourCC("DBGP"), ///< only used in project files
REC_LUAL = fourCC("LUAL"), // LuaScriptsCfg
REC_LUAL = fourCC("LUAL"), // LuaScriptsCfg (only in omwgame or omwaddon)
// format 16 - Lua scripts in saved games
REC_LUAM = fourCC("LUAM"), // LuaManager data

View file

@ -3,6 +3,8 @@
#include "components/esm3/esmreader.hpp"
#include "components/esm3/esmwriter.hpp"
#include <components/lua/serialization.hpp>
// List of all records, that are related to Lua.
//
// Records:
@ -10,13 +12,15 @@
// LUAM - MWLua::LuaManager (in saves)
//
// Subrecords:
// LUAF - LuaScriptCfg::mFlags
// LUAF - LuaScriptCfg::mFlags and ESM::RecNameInts list
// LUAW - Start of MWLua::WorldView data
// LUAE - Start of MWLua::LocalEvent or MWLua::GlobalEvent (eventName)
// LUAS - VFS path to a Lua script
// LUAD - Serialized Lua variable
// LUAT - MWLua::ScriptsContainer::Timer
// LUAC - Name of a timer callback (string)
// LUAR - Attach script to a specific record (LuaScriptCfg::PerRecordCfg)
// LUAI - Attach script to a specific instance (LuaScriptCfg::PerRefCfg)
void ESM::saveLuaBinaryData(ESMWriter& esm, const std::string& data)
{
@ -39,25 +43,120 @@ std::string ESM::loadLuaBinaryData(ESMReader& esm)
return data;
}
static bool readBool(ESM::ESMReader& esm)
{
char c;
esm.getT<char>(c);
return c != 0;
}
void ESM::LuaScriptsCfg::load(ESMReader& esm)
{
while (esm.isNextSub("LUAS"))
{
std::string name = esm.getHString();
uint64_t flags;
esm.getHNT(flags, "LUAF");
std::string data = loadLuaBinaryData(esm);
mScripts.push_back({std::move(name), std::move(data), flags});
mScripts.emplace_back();
ESM::LuaScriptCfg& script = mScripts.back();
script.mScriptPath = esm.getHString();
esm.getSubNameIs("LUAF");
esm.getSubHeader();
if (esm.getSubSize() < 4 || (esm.getSubSize() % 4 != 0))
esm.fail("Incorrect LUAF size");
esm.getT(script.mFlags);
script.mTypes.resize((esm.getSubSize() - 4) / 4);
for (uint32_t& type : script.mTypes)
esm.getT(type);
script.mInitializationData = loadLuaBinaryData(esm);
while (esm.isNextSub("LUAR"))
{
esm.getSubHeader();
script.mRecords.emplace_back();
ESM::LuaScriptCfg::PerRecordCfg& recordCfg = script.mRecords.back();
recordCfg.mRecordId.resize(esm.getSubSize() - 1);
recordCfg.mAttach = readBool(esm);
esm.getExact(recordCfg.mRecordId.data(), static_cast<int>(recordCfg.mRecordId.size()));
recordCfg.mInitializationData = loadLuaBinaryData(esm);
}
while (esm.isNextSub("LUAI"))
{
esm.getSubHeader();
script.mRefs.emplace_back();
ESM::LuaScriptCfg::PerRefCfg& refCfg = script.mRefs.back();
refCfg.mAttach = readBool(esm);
esm.getT<uint32_t>(refCfg.mRefnumIndex);
esm.getT<int32_t>(refCfg.mRefnumContentFile);
refCfg.mInitializationData = loadLuaBinaryData(esm);
}
}
}
void ESM::LuaScriptsCfg::adjustRefNums(const ESMReader& esm)
{
auto adjustRefNumFn = [&esm](int contentFile) -> int
{
if (contentFile == 0)
return esm.getIndex();
else if (contentFile > 0 && contentFile <= static_cast<int>(esm.getParentFileIndices().size()))
return esm.getParentFileIndices()[contentFile - 1];
else
throw std::runtime_error("Incorrect contentFile index");
};
lua_State* L = lua_open();
LuaUtil::BasicSerializer serializer(adjustRefNumFn);
auto adjustLuaData = [&](std::string& data)
{
if (data.empty())
return;
sol::object luaData = LuaUtil::deserialize(L, data, &serializer);
data = LuaUtil::serialize(luaData, &serializer);
};
for (LuaScriptCfg& script : mScripts)
{
adjustLuaData(script.mInitializationData);
for (LuaScriptCfg::PerRecordCfg& recordCfg : script.mRecords)
adjustLuaData(recordCfg.mInitializationData);
for (LuaScriptCfg::PerRefCfg& refCfg : script.mRefs)
{
adjustLuaData(refCfg.mInitializationData);
refCfg.mRefnumContentFile = adjustRefNumFn(refCfg.mRefnumContentFile);
}
}
lua_close(L);
}
void ESM::LuaScriptsCfg::save(ESMWriter& esm) const
{
for (const LuaScriptCfg& script : mScripts)
{
esm.writeHNString("LUAS", script.mScriptPath);
esm.writeHNT("LUAF", script.mFlags);
esm.startSubRecord("LUAF");
esm.writeT<uint32_t>(script.mFlags);
for (uint32_t type : script.mTypes)
esm.writeT<uint32_t>(type);
esm.endRecord("LUAF");
saveLuaBinaryData(esm, script.mInitializationData);
for (const LuaScriptCfg::PerRecordCfg& recordCfg : script.mRecords)
{
esm.startSubRecord("LUAR");
esm.writeT<char>(recordCfg.mAttach ? 1 : 0);
esm.write(recordCfg.mRecordId.data(), recordCfg.mRecordId.size());
esm.endRecord("LUAR");
saveLuaBinaryData(esm, recordCfg.mInitializationData);
}
for (const LuaScriptCfg::PerRefCfg& refCfg : script.mRefs)
{
esm.startSubRecord("LUAI");
esm.writeT<char>(refCfg.mAttach ? 1 : 0);
esm.writeT<uint32_t>(refCfg.mRefnumIndex);
esm.writeT<int32_t>(refCfg.mRefnumContentFile);
esm.endRecord("LUAI");
saveLuaBinaryData(esm, refCfg.mInitializationData);
}
}
}

View file

@ -13,28 +13,41 @@ namespace ESM
struct LuaScriptCfg
{
using Flags = uint64_t;
static constexpr Flags sGlobal = 1ull << 0;
using Flags = uint32_t;
static constexpr Flags sGlobal = 1ull << 0; // start as a global script
static constexpr Flags sCustom = 1ull << 1; // local; can be attached/detached by a global script
static constexpr Flags sPlayer = 1ull << 2; // auto attach to players
// auto attach for other classes:
static constexpr Flags sActivator = 1ull << 3;
static constexpr Flags sArmor = 1ull << 4;
static constexpr Flags sBook = 1ull << 5;
static constexpr Flags sClothing = 1ull << 6;
static constexpr Flags sContainer = 1ull << 7;
static constexpr Flags sCreature = 1ull << 8;
static constexpr Flags sDoor = 1ull << 9;
static constexpr Flags sIngredient = 1ull << 10;
static constexpr Flags sLight = 1ull << 11;
static constexpr Flags sMiscItem = 1ull << 12;
static constexpr Flags sNPC = 1ull << 13;
static constexpr Flags sPotion = 1ull << 14;
static constexpr Flags sWeapon = 1ull << 15;
std::string mScriptPath;
static constexpr Flags sMerge = 1ull << 3; // merge with configuration for this script from previous content files.
std::string mScriptPath; // VFS path to the script.
std::string mInitializationData; // Serialized Lua table. It is a binary data. Can contain '\0'.
Flags mFlags; // bitwise OR of Flags.
// Auto attach as a local script to objects of specific types (i.e. Container, Door, Activator, etc.)
std::vector<uint32_t> mTypes; // values are ESM::RecNameInts
// Auto attach as a local script to objects with specific recordIds (i.e. specific door type, or an unique NPC)
struct PerRecordCfg
{
bool mAttach; // true - attach, false - don't attach (overrides previous attach)
std::string mRecordId;
// Initialization data for this specific record. If empty than LuaScriptCfg::mInitializationData is used.
std::string mInitializationData;
};
std::vector<PerRecordCfg> mRecords;
// Auto attach as a local script to specific objects by their Refnums. The reference must be defined in the same
// content file as this LuaScriptCfg or in one of its deps.
struct PerRefCfg
{
bool mAttach; // true - attach, false - don't attach (overrides previous attach)
uint32_t mRefnumIndex;
int32_t mRefnumContentFile;
// Initialization data for this specific refnum. If empty than LuaScriptCfg::mInitializationData is used.
std::string mInitializationData;
};
std::vector<PerRefCfg> mRefs;
};
struct LuaScriptsCfg
@ -42,6 +55,8 @@ namespace ESM
std::vector<LuaScriptCfg> mScripts;
void load(ESMReader &esm);
void adjustRefNums(const ESMReader &esm);
void save(ESMWriter &esm) const;
};

View file

@ -42,7 +42,7 @@ namespace ESM
/// \brief File header record
struct Header
{
static const int CurrentFormat = 0; // most recent known format
static constexpr int CurrentFormat = 1; // most recent known format
// Defines another files (esm or esp) that this file depends upon.
struct MasterData

View file

@ -16,19 +16,26 @@ namespace LuaUtil
{"GLOBAL", ESM::LuaScriptCfg::sGlobal},
{"CUSTOM", ESM::LuaScriptCfg::sCustom},
{"PLAYER", ESM::LuaScriptCfg::sPlayer},
{"ACTIVATOR", ESM::LuaScriptCfg::sActivator},
{"ARMOR", ESM::LuaScriptCfg::sArmor},
{"BOOK", ESM::LuaScriptCfg::sBook},
{"CLOTHING", ESM::LuaScriptCfg::sClothing},
{"CONTAINER", ESM::LuaScriptCfg::sContainer},
{"CREATURE", ESM::LuaScriptCfg::sCreature},
{"DOOR", ESM::LuaScriptCfg::sDoor},
{"INGREDIENT", ESM::LuaScriptCfg::sIngredient},
{"LIGHT", ESM::LuaScriptCfg::sLight},
{"MISC_ITEM", ESM::LuaScriptCfg::sMiscItem},
{"NPC", ESM::LuaScriptCfg::sNPC},
{"POTION", ESM::LuaScriptCfg::sPotion},
{"WEAPON", ESM::LuaScriptCfg::sWeapon},
};
const std::map<std::string, ESM::RecNameInts, std::less<>> typeTagsByName{
{"ACTIVATOR", ESM::REC_ACTI},
{"ARMOR", ESM::REC_ARMO},
{"BOOK", ESM::REC_BOOK},
{"CLOTHING", ESM::REC_CLOT},
{"CONTAINER", ESM::REC_CONT},
{"CREATURE", ESM::REC_CREA},
{"DOOR", ESM::REC_DOOR},
{"INGREDIENT", ESM::REC_INGR},
{"LIGHT", ESM::REC_LIGH},
{"MISC_ITEM", ESM::REC_MISC},
{"NPC", ESM::REC_NPC_},
{"POTION", ESM::REC_ALCH},
{"WEAPON", ESM::REC_WEAP},
{"APPARATUS", ESM::REC_APPA},
{"LOCKPICK", ESM::REC_LOCK},
{"PROBE", ESM::REC_PROB},
{"REPAIR", ESM::REC_REPA},
};
bool isSpace(char c)
@ -37,43 +44,69 @@ namespace LuaUtil
}
}
const std::vector<int> ScriptsConfiguration::sEmpty;
void ScriptsConfiguration::init(ESM::LuaScriptsCfg cfg)
{
mScripts.clear();
mScriptsByFlag.clear();
mPathToIndex.clear();
// Find duplicates; only the last occurrence will be used.
// Find duplicates; only the last occurrence will be used (unless `sMerge` flag is used).
// Search for duplicates is case insensitive.
std::vector<bool> skip(cfg.mScripts.size(), false);
for (int i = cfg.mScripts.size() - 1; i >= 0; --i)
{
auto [_, inserted] = mPathToIndex.insert_or_assign(
Misc::StringUtils::lowerCase(cfg.mScripts[i].mScriptPath), -1);
if (!inserted || cfg.mScripts[i].mFlags == 0)
skip[i] = true;
}
mPathToIndex.clear();
int index = 0;
for (size_t i = 0; i < cfg.mScripts.size(); ++i)
{
if (skip[i])
const ESM::LuaScriptCfg& script = cfg.mScripts[i];
bool global = script.mFlags & ESM::LuaScriptCfg::sGlobal;
if (global && (script.mFlags & ~ESM::LuaScriptCfg::sMerge) != ESM::LuaScriptCfg::sGlobal)
throw std::runtime_error(std::string("Global script can not have local flags: ") + script.mScriptPath);
if (global && (!script.mTypes.empty() || !script.mRecords.empty() || !script.mRefs.empty()))
throw std::runtime_error(std::string(
"Global script can not have per-type and per-object configuration") + script.mScriptPath);
auto [it, inserted] = mPathToIndex.emplace(
Misc::StringUtils::lowerCase(script.mScriptPath), i);
if (inserted)
continue;
ESM::LuaScriptCfg& s = cfg.mScripts[i];
mPathToIndex[s.mScriptPath] = index; // Stored paths are case sensitive.
ESM::LuaScriptCfg::Flags flags = s.mFlags;
ESM::LuaScriptCfg::Flags flag = 1;
while (flags != 0)
ESM::LuaScriptCfg& oldScript = cfg.mScripts[it->second];
if (global != bool(oldScript.mFlags & ESM::LuaScriptCfg::sGlobal))
throw std::runtime_error(std::string("Flags mismatch for ") + script.mScriptPath);
if (script.mFlags & ESM::LuaScriptCfg::sMerge)
{
if (flags & flag)
mScriptsByFlag[flag].push_back(index);
flags &= ~flag;
flag = flag << 1;
oldScript.mFlags |= (script.mFlags & ~ESM::LuaScriptCfg::sMerge);
if (!script.mInitializationData.empty())
oldScript.mInitializationData = script.mInitializationData;
oldScript.mTypes.insert(oldScript.mTypes.end(), script.mTypes.begin(), script.mTypes.end());
oldScript.mRecords.insert(oldScript.mRecords.end(), script.mRecords.begin(), script.mRecords.end());
oldScript.mRefs.insert(oldScript.mRefs.end(), script.mRefs.begin(), script.mRefs.end());
skip[i] = true;
}
else
skip[it->second] = true;
}
// Filter duplicates
for (size_t i = 0; i < cfg.mScripts.size(); ++i)
{
if (!skip[i])
mScripts.push_back(std::move(cfg.mScripts[i]));
}
// Initialize mappings
mPathToIndex.clear();
for (int i = 0; i < static_cast<int>(mScripts.size()); ++i)
{
const ESM::LuaScriptCfg& s = mScripts[i];
mPathToIndex[s.mScriptPath] = i; // Stored paths are case sensitive.
for (uint32_t t : s.mTypes)
mScriptsPerType[t].push_back(i);
for (const ESM::LuaScriptCfg::PerRecordCfg& r : s.mRecords)
{
std::string_view data = r.mInitializationData.empty() ? s.mInitializationData : r.mInitializationData;
mScriptsPerRecordId[r.mRecordId].push_back(DetailedConf{i, r.mAttach, data});
}
for (const ESM::LuaScriptCfg::PerRefCfg& r : s.mRefs)
{
std::string_view data = r.mInitializationData.empty() ? s.mInitializationData : r.mInitializationData;
mScriptsPerRefNum[ESM::RefNum{r.mRefnumIndex, r.mRefnumContentFile}].push_back(DetailedConf{i, r.mAttach, data});
}
mScripts.push_back(std::move(s));
index++;
}
}
@ -86,14 +119,50 @@ namespace LuaUtil
return std::nullopt;
}
const std::vector<int>& ScriptsConfiguration::getListByFlag(ESM::LuaScriptCfg::Flags type) const
ScriptIdsWithInitializationData ScriptsConfiguration::getConfByFlag(ESM::LuaScriptCfg::Flags flag) const
{
assert(std::bitset<64>(type).count() <= 1);
auto it = mScriptsByFlag.find(type);
if (it != mScriptsByFlag.end())
return it->second;
else
return sEmpty;
ScriptIdsWithInitializationData res;
for (size_t id = 0; id < mScripts.size(); ++id)
{
const ESM::LuaScriptCfg& script = mScripts[id];
if (script.mFlags & flag)
res[id] = script.mInitializationData;
}
return res;
}
ScriptIdsWithInitializationData ScriptsConfiguration::getLocalConf(
uint32_t type, std::string_view recordId, ESM::RefNum refnum) const
{
ScriptIdsWithInitializationData res;
auto typeIt = mScriptsPerType.find(type);
if (typeIt != mScriptsPerType.end())
for (int scriptId : typeIt->second)
res[scriptId] = mScripts[scriptId].mInitializationData;
auto recordIt = mScriptsPerRecordId.find(recordId);
if (recordIt != mScriptsPerRecordId.end())
{
for (const DetailedConf& d : recordIt->second)
{
if (d.mAttach)
res[d.mScriptId] = d.mInitializationData;
else
res.erase(d.mScriptId);
}
}
if (!refnum.hasContentFile())
return res;
auto refIt = mScriptsPerRefNum.find(refnum);
if (refIt == mScriptsPerRefNum.end())
return res;
for (const DetailedConf& d : refIt->second)
{
if (d.mAttach)
res[d.mScriptId] = d.mInitializationData;
else
res.erase(d.mScriptId);
}
return res;
}
void parseOMWScripts(ESM::LuaScriptsCfg& cfg, std::string_view data)
@ -117,53 +186,69 @@ namespace LuaUtil
throw std::runtime_error(Misc::StringUtils::format(
"Lua script should have suffix '.lua', got: %s", std::string(line.substr(0, 300))));
// Split flags and script path
// Split tags and script path
size_t semicolonPos = line.find(':');
if (semicolonPos == std::string::npos)
throw std::runtime_error(Misc::StringUtils::format("No flags found in: %s", std::string(line)));
std::string_view flagsStr = line.substr(0, semicolonPos);
std::string_view tagsStr = line.substr(0, semicolonPos);
std::string_view scriptPath = line.substr(semicolonPos + 1);
while (isSpace(scriptPath[0]))
scriptPath = scriptPath.substr(1);
// Parse flags
ESM::LuaScriptCfg::Flags flags = 0;
size_t flagsPos = 0;
ESM::LuaScriptCfg& script = cfg.mScripts.emplace_back();
script.mScriptPath = std::string(scriptPath);
script.mFlags = 0;
// Parse tags
size_t tagsPos = 0;
while (true)
{
while (flagsPos < flagsStr.size() && (isSpace(flagsStr[flagsPos]) || flagsStr[flagsPos] == ','))
flagsPos++;
size_t startPos = flagsPos;
while (flagsPos < flagsStr.size() && !isSpace(flagsStr[flagsPos]) && flagsStr[flagsPos] != ',')
flagsPos++;
if (startPos == flagsPos)
while (tagsPos < tagsStr.size() && (isSpace(tagsStr[tagsPos]) || tagsStr[tagsPos] == ','))
tagsPos++;
size_t startPos = tagsPos;
while (tagsPos < tagsStr.size() && !isSpace(tagsStr[tagsPos]) && tagsStr[tagsPos] != ',')
tagsPos++;
if (startPos == tagsPos)
break;
std::string_view flagName = flagsStr.substr(startPos, flagsPos - startPos);
auto it = flagsByName.find(flagName);
std::string_view tagName = tagsStr.substr(startPos, tagsPos - startPos);
auto it = flagsByName.find(tagName);
auto typesIt = typeTagsByName.find(tagName);
if (it != flagsByName.end())
flags |= it->second;
script.mFlags |= it->second;
else if (typesIt != typeTagsByName.end())
script.mTypes.push_back(typesIt->second);
else
throw std::runtime_error(Misc::StringUtils::format("Unknown flag '%s' in: %s",
std::string(flagName), std::string(line)));
throw std::runtime_error(Misc::StringUtils::format("Unknown tag '%s' in: %s",
std::string(tagName), std::string(line)));
}
if ((flags & ESM::LuaScriptCfg::sGlobal) && flags != ESM::LuaScriptCfg::sGlobal)
throw std::runtime_error("Global script can not have local flags");
cfg.mScripts.push_back(ESM::LuaScriptCfg{std::string(scriptPath), "", flags});
}
}
std::string scriptCfgToString(const ESM::LuaScriptCfg& script)
{
std::stringstream ss;
if (script.mFlags & ESM::LuaScriptCfg::sMerge)
ss << "+ ";
for (const auto& [flagName, flag] : flagsByName)
{
if (script.mFlags & flag)
ss << flagName << " ";
}
for (uint32_t type : script.mTypes)
{
for (const auto& [tagName, t] : typeTagsByName)
{
if (type == t)
ss << tagName << " ";
}
}
ss << ": " << script.mScriptPath;
if (!script.mInitializationData.empty())
ss << " (with data, " << script.mInitializationData.size() << " bytes)";
ss << " ; data " << script.mInitializationData.size() << " bytes";
if (!script.mRecords.empty())
ss << " ; " << script.mRecords.size() << " records";
if (!script.mRefs.empty())
ss << " ; " << script.mRefs.size() << " objects";
return ss.str();
}

View file

@ -5,9 +5,11 @@
#include <optional>
#include "components/esm/luascripts.hpp"
#include "components/esm3/cellref.hpp"
namespace LuaUtil
{
using ScriptIdsWithInitializationData = std::map<int, std::string_view>;
class ScriptsConfiguration
{
@ -18,13 +20,27 @@ namespace LuaUtil
const ESM::LuaScriptCfg& operator[](int id) const { return mScripts[id]; }
std::optional<int> findId(std::string_view path) const;
const std::vector<int>& getListByFlag(ESM::LuaScriptCfg::Flags type) const;
bool isCustomScript(int id) const { return mScripts[id].mFlags & ESM::LuaScriptCfg::sCustom; }
ScriptIdsWithInitializationData getGlobalConf() const { return getConfByFlag(ESM::LuaScriptCfg::sGlobal); }
ScriptIdsWithInitializationData getPlayerConf() const { return getConfByFlag(ESM::LuaScriptCfg::sPlayer); }
ScriptIdsWithInitializationData getLocalConf(uint32_t type, std::string_view recordId, ESM::RefNum refnum) const;
private:
ScriptIdsWithInitializationData getConfByFlag(ESM::LuaScriptCfg::Flags flag) const;
std::vector<ESM::LuaScriptCfg> mScripts;
std::map<std::string, int, std::less<>> mPathToIndex;
std::map<ESM::LuaScriptCfg::Flags, std::vector<int>> mScriptsByFlag;
static const std::vector<int> sEmpty;
struct DetailedConf
{
int mScriptId;
bool mAttach;
std::string_view mInitializationData;
};
std::map<uint32_t, std::vector<int>> mScriptsPerType;
std::map<std::string, std::vector<DetailedConf>, std::less<>> mScriptsPerRecordId;
std::map<ESM::RefNum, std::vector<DetailedConf>> mScriptsPerRefNum;
};
// Parse ESM::LuaScriptsCfg from text and add to `cfg`.

View file

@ -15,8 +15,8 @@ namespace LuaUtil
static constexpr std::string_view HANDLER_LOAD = "onLoad";
static constexpr std::string_view HANDLER_INTERFACE_OVERRIDE = "onInterfaceOverride";
ScriptsContainer::ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix, ESM::LuaScriptCfg::Flags autoStartMode)
: mNamePrefix(namePrefix), mLua(*lua), mAutoStartMode(autoStartMode)
ScriptsContainer::ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix)
: mNamePrefix(namePrefix), mLua(*lua)
{
registerEngineHandlers({&mUpdateHandlers});
mPublicInterfaces = sol::table(lua->sol(), sol::create);
@ -35,22 +35,23 @@ namespace LuaUtil
bool ScriptsContainer::addCustomScript(int scriptId)
{
assert(mLua.getConfiguration()[scriptId].mFlags & ESM::LuaScriptCfg::sCustom);
const ScriptsConfiguration& conf = mLua.getConfiguration();
assert(conf.isCustomScript(scriptId));
std::optional<sol::function> onInit, onLoad;
bool ok = addScript(scriptId, onInit, onLoad);
if (ok && onInit)
callOnInit(scriptId, *onInit);
callOnInit(scriptId, *onInit, conf[scriptId].mInitializationData);
return ok;
}
void ScriptsContainer::addAutoStartedScripts()
{
for (int scriptId : mLua.getConfiguration().getListByFlag(mAutoStartMode))
for (const auto& [scriptId, data] : mAutoStartScripts)
{
std::optional<sol::function> onInit, onLoad;
bool ok = addScript(scriptId, onInit, onLoad);
if (ok && onInit)
callOnInit(scriptId, *onInit);
callOnInit(scriptId, *onInit, data);
}
}
@ -296,11 +297,10 @@ namespace LuaUtil
mEngineHandlers[h->mName] = h;
}
void ScriptsContainer::callOnInit(int scriptId, const sol::function& onInit)
void ScriptsContainer::callOnInit(int scriptId, const sol::function& onInit, std::string_view data)
{
try
{
const std::string& data = mLua.getConfiguration()[scriptId].mInitializationData;
LuaUtil::call(onInit, deserialize(mLua.sol(), data, mSerializer));
}
catch (std::exception& e) { printError(scriptId, "onInit failed", e); }
@ -352,9 +352,14 @@ namespace LuaUtil
removeAllScripts();
const ScriptsConfiguration& cfg = mLua.getConfiguration();
std::map<int, const ESM::LuaScript*> scripts;
for (int scriptId : mLua.getConfiguration().getListByFlag(mAutoStartMode))
scripts[scriptId] = nullptr;
struct ScriptInfo
{
std::string_view mInitData;
const ESM::LuaScript* mSavedData;
};
std::map<int, ScriptInfo> scripts;
for (const auto& [scriptId, initData] : mAutoStartScripts)
scripts[scriptId] = {initData, nullptr};
for (const ESM::LuaScript& s : data.mScripts)
{
std::optional<int> scriptId = cfg.findId(s.mScriptPath);
@ -363,37 +368,38 @@ namespace LuaUtil
Log(Debug::Verbose) << "Ignoring " << mNamePrefix << "[" << s.mScriptPath << "]; script not registered";
continue;
}
if (!(cfg[*scriptId].mFlags & (ESM::LuaScriptCfg::sCustom | mAutoStartMode)))
{
auto it = scripts.find(*scriptId);
if (it != scripts.end())
it->second.mSavedData = &s;
else if (cfg.isCustomScript(*scriptId))
scripts[*scriptId] = {cfg[*scriptId].mInitializationData, &s};
else
Log(Debug::Verbose) << "Ignoring " << mNamePrefix << "[" << s.mScriptPath << "]; this script is not allowed here";
continue;
}
scripts[*scriptId] = &s;
}
for (const auto& [scriptId, savedScript] : scripts)
for (const auto& [scriptId, scriptInfo] : scripts)
{
std::optional<sol::function> onInit, onLoad;
if (!addScript(scriptId, onInit, onLoad))
continue;
if (savedScript == nullptr)
if (scriptInfo.mSavedData == nullptr)
{
if (onInit)
callOnInit(scriptId, *onInit);
callOnInit(scriptId, *onInit, scriptInfo.mInitData);
continue;
}
if (onLoad)
{
try
{
sol::object state = deserialize(mLua.sol(), savedScript->mData, mSerializer);
sol::object state = deserialize(mLua.sol(), scriptInfo.mSavedData->mData, mSavedDataDeserializer);
sol::object initializationData =
deserialize(mLua.sol(), mLua.getConfiguration()[scriptId].mInitializationData, mSerializer);
deserialize(mLua.sol(), scriptInfo.mInitData, mSerializer);
LuaUtil::call(*onLoad, state, initializationData);
}
catch (std::exception& e) { printError(scriptId, "onLoad failed", e); }
}
for (const ESM::LuaTimer& savedTimer : savedScript->mTimers)
for (const ESM::LuaTimer& savedTimer : scriptInfo.mSavedData->mTimers)
{
Timer timer;
timer.mCallback = savedTimer.mCallbackName;
@ -403,7 +409,7 @@ namespace LuaUtil
try
{
timer.mArg = deserialize(mLua.sol(), savedTimer.mCallbackArgument, mSerializer);
timer.mArg = deserialize(mLua.sol(), savedTimer.mCallbackArgument, mSavedDataDeserializer);
// 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);

View file

@ -76,15 +76,16 @@ namespace LuaUtil
using TimerType = ESM::LuaTimer::Type;
// `namePrefix` is a common prefix for all scripts in the container. Used in logs for error messages and `print` output.
// `autoStartMode` specifies the list of scripts that should be autostarted in this container; the list itself is
// stored in ScriptsConfiguration: lua->getConfiguration().getListByFlag(autoStartMode).
ScriptsContainer(LuaState* lua, std::string_view namePrefix, ESM::LuaScriptCfg::Flags autoStartMode = 0);
// `autoStartScripts` specifies the list of scripts that should be autostarted in this container;
// the script names themselves are stored in ScriptsConfiguration.
ScriptsContainer(LuaState* lua, std::string_view namePrefix);
ScriptsContainer(const ScriptsContainer&) = delete;
ScriptsContainer(ScriptsContainer&&) = delete;
virtual ~ScriptsContainer();
ESM::LuaScriptCfg::Flags getAutoStartMode() const { return mAutoStartMode; }
void setAutoStartConf(ScriptIdsWithInitializationData conf) { mAutoStartScripts = std::move(conf); }
const ScriptIdsWithInitializationData& getAutoStartConf() const { return mAutoStartScripts; }
// Adds package that will be available (via `require`) for all scripts in the container.
// Automatically applies LuaUtil::makeReadOnly to the package.
@ -115,6 +116,9 @@ namespace LuaUtil
// only built-in types and types from util package can be serialized.
void setSerializer(const UserdataSerializer* serializer) { mSerializer = serializer; }
// Special deserializer to use when load data from saves. Can be used to remap content files in Refnums.
void setSavedDataDeserializer(const UserdataSerializer* serializer) { mSavedDataDeserializer = serializer; }
// Starts scripts according to `autoStartMode` and calls `onInit` for them. Not needed if `load` is used.
void addAutoStartedScripts();
@ -216,7 +220,7 @@ namespace LuaUtil
void printError(int scriptId, std::string_view msg, const std::exception& e);
const std::string& scriptPath(int scriptId) const { return mLua.getConfiguration()[scriptId].mScriptPath; }
void callOnInit(int scriptId, const sol::function& onInit);
void callOnInit(int scriptId, const sol::function& onInit, std::string_view data);
void callTimer(const Timer& t);
void updateTimerQueue(std::vector<Timer>& timerQueue, double time);
static void insertTimer(std::vector<Timer>& timerQueue, Timer&& t);
@ -225,8 +229,9 @@ namespace LuaUtil
void insertInterface(int scriptId, const Script& script);
void removeInterface(int scriptId, const Script& script);
ESM::LuaScriptCfg::Flags mAutoStartMode;
ScriptIdsWithInitializationData mAutoStartScripts;
const UserdataSerializer* mSerializer = nullptr;
const UserdataSerializer* mSavedDataDeserializer = nullptr;
std::map<std::string, sol::object> mAPI;
std::map<int, Script> mScripts;

View file

@ -96,6 +96,42 @@ namespace LuaUtil
appendData(out, data, dataSize);
}
void UserdataSerializer::appendRefNum(BinaryData& out, ESM::RefNum refnum)
{
static_assert(sizeof(ESM::RefNum) == 8);
refnum.mIndex = Misc::toLittleEndian(refnum.mIndex);
refnum.mContentFile = Misc::toLittleEndian(refnum.mContentFile);
append(out, sRefNumTypeName, &refnum, sizeof(ESM::RefNum));
}
bool BasicSerializer::serialize(BinaryData& out, const sol::userdata& data) const
{
appendRefNum(out, data.as<ESM::RefNum>());
return true;
}
bool BasicSerializer::deserialize(std::string_view typeName, std::string_view binaryData, lua_State* lua) const
{
if (typeName != sRefNumTypeName)
return false;
ESM::RefNum refnum = loadRefNum(binaryData);
if (mAdjustContentFilesIndexFn)
refnum.mContentFile = mAdjustContentFilesIndexFn(refnum.mContentFile);
sol::stack::push<ESM::RefNum>(lua, refnum);
return true;
}
ESM::RefNum UserdataSerializer::loadRefNum(std::string_view data)
{
if (data.size() != sizeof(ESM::RefNum))
throw std::runtime_error("Incorrect serialization format. Size of RefNum doesn't match.");
ESM::RefNum refnum;
std::memcpy(&refnum, data.data(), sizeof(ESM::RefNum));
refnum.mIndex = Misc::fromLittleEndian(refnum.mIndex);
refnum.mContentFile = Misc::fromLittleEndian(refnum.mContentFile);
return refnum;
}
static void serializeUserdata(BinaryData& out, const sol::userdata& data, const UserdataSerializer* customSerializer)
{
if (data.is<osg::Vec2f>())

View file

@ -3,6 +3,8 @@
#include <sol/sol.hpp>
#include <components/esm3/cellref.hpp>
namespace LuaUtil
{
@ -24,6 +26,26 @@ namespace LuaUtil
protected:
static void append(BinaryData&, std::string_view typeName, const void* data, size_t dataSize);
static constexpr std::string_view sRefNumTypeName = "o";
static void appendRefNum(BinaryData&, ESM::RefNum);
static ESM::RefNum loadRefNum(std::string_view data);
};
// Serializer that can load Lua data from content files and saved games, but doesn't depend on apps/openmw.
// Instead of LObject/GObject (that are defined in apps/openmw) it loads refnums directly as ESM::RefNum.
class BasicSerializer final : public UserdataSerializer
{
public:
BasicSerializer() = default;
explicit BasicSerializer(std::function<int(int)> adjustContentFileIndexFn) :
mAdjustContentFilesIndexFn(std::move(adjustContentFileIndexFn)) {}
private:
bool serialize(LuaUtil::BinaryData& out, const sol::userdata& data) const override;
bool deserialize(std::string_view typeName, std::string_view binaryData, lua_State* lua) const override;
std::function<int(int)> mAdjustContentFilesIndexFn;
};
BinaryData serialize(const sol::object&, const UserdataSerializer* customSerializer = nullptr);