From a70d5831c5ef1a61f0171c5fa76112b423047a7c Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Fri, 20 May 2022 21:47:13 +0200 Subject: [PATCH] Lua scripts configuration in omwaddon --- apps/opencs/model/doc/savingstages.cpp | 5 +- apps/opencs/model/world/metadata.cpp | 4 +- apps/openmw/mwlua/globalscripts.hpp | 3 +- apps/openmw/mwlua/localscripts.cpp | 4 +- apps/openmw/mwlua/localscripts.hpp | 2 +- apps/openmw/mwlua/luamanagerimp.cpp | 40 ++-- apps/openmw/mwlua/luamanagerimp.hpp | 3 +- apps/openmw/mwlua/objectbindings.cpp | 3 +- apps/openmw/mwlua/playerscripts.hpp | 2 +- apps/openmw/mwlua/types/types.cpp | 63 ++--- apps/openmw/mwlua/types/types.hpp | 4 - apps/openmw/mwlua/userdataserializer.cpp | 15 +- apps/openmw/mwworld/esmstore.cpp | 2 +- .../lua/test_configuration.cpp | 209 ++++++++++++++++- .../lua/test_scriptscontainer.cpp | 39 ++-- components/esm/defs.hpp | 2 +- components/esm/luascripts.cpp | 113 ++++++++- components/esm/luascripts.hpp | 49 ++-- components/esm3/loadtes3.hpp | 2 +- components/lua/configuration.cpp | 217 ++++++++++++------ components/lua/configuration.hpp | 22 +- components/lua/scriptscontainer.cpp | 52 +++-- components/lua/scriptscontainer.hpp | 17 +- components/lua/serialization.cpp | 36 +++ components/lua/serialization.hpp | 22 ++ .../reference/lua-scripting/overview.rst | 6 +- 26 files changed, 700 insertions(+), 236 deletions(-) diff --git a/apps/opencs/model/doc/savingstages.cpp b/apps/opencs/model/doc/savingstages.cpp index bba5aea92f..6fdf75eb84 100644 --- a/apps/opencs/model/doc/savingstages.cpp +++ b/apps/opencs/model/doc/savingstages.cpp @@ -53,7 +53,10 @@ void CSMDoc::WriteHeaderStage::perform (int stage, Messages& messages) mState.getWriter().setAuthor (""); mState.getWriter().setDescription (""); mState.getWriter().setRecordCount (0); - mState.getWriter().setFormat (ESM::Header::CurrentFormat); + + // ESM::Header::CurrentFormat is `1` but since new records are not yet used in opencs + // we use the format `0` for compatibility with old versions. + mState.getWriter().setFormat(0); } else { diff --git a/apps/opencs/model/world/metadata.cpp b/apps/opencs/model/world/metadata.cpp index acee441883..e7d6c8900c 100644 --- a/apps/opencs/model/world/metadata.cpp +++ b/apps/opencs/model/world/metadata.cpp @@ -6,7 +6,9 @@ void CSMWorld::MetaData::blank() { - mFormat = ESM::Header::CurrentFormat; + // ESM::Header::CurrentFormat is `1` but since new records are not yet used in opencs + // we use the format `0` for compatibility with old versions. + mFormat = 0; mAuthor.clear(); mDescription.clear(); } diff --git a/apps/openmw/mwlua/globalscripts.hpp b/apps/openmw/mwlua/globalscripts.hpp index 2b40aabd9d..5f8dc1ecf0 100644 --- a/apps/openmw/mwlua/globalscripts.hpp +++ b/apps/openmw/mwlua/globalscripts.hpp @@ -16,8 +16,7 @@ namespace MWLua class GlobalScripts : public LuaUtil::ScriptsContainer { public: - GlobalScripts(LuaUtil::LuaState* lua) : - LuaUtil::ScriptsContainer(lua, "Global", ESM::LuaScriptCfg::sGlobal) + GlobalScripts(LuaUtil::LuaState* lua) : LuaUtil::ScriptsContainer(lua, "Global") { registerEngineHandlers({ &mObjectActiveHandlers, diff --git a/apps/openmw/mwlua/localscripts.cpp b/apps/openmw/mwlua/localscripts.cpp index 948bc1e869..1224411453 100644 --- a/apps/openmw/mwlua/localscripts.cpp +++ b/apps/openmw/mwlua/localscripts.cpp @@ -148,8 +148,8 @@ namespace MWLua }; } - LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj, ESM::LuaScriptCfg::Flags autoStartMode) - : LuaUtil::ScriptsContainer(lua, "L" + idToString(obj.id()), autoStartMode), mData(obj) + LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj) + : LuaUtil::ScriptsContainer(lua, "L" + idToString(obj.id())), mData(obj) { this->addPackage("openmw.self", sol::make_object(lua->sol(), &mData)); registerEngineHandlers({&mOnActiveHandlers, &mOnInactiveHandlers, &mOnConsumeHandlers, &mOnActivatedHandlers}); diff --git a/apps/openmw/mwlua/localscripts.hpp b/apps/openmw/mwlua/localscripts.hpp index b5bd8f95c8..58cf421d4a 100644 --- a/apps/openmw/mwlua/localscripts.hpp +++ b/apps/openmw/mwlua/localscripts.hpp @@ -20,7 +20,7 @@ namespace MWLua { public: static void initializeSelfPackage(const Context&); - LocalScripts(LuaUtil::LuaState* lua, const LObject& obj, ESM::LuaScriptCfg::Flags autoStartMode); + LocalScripts(LuaUtil::LuaState* lua, const LObject& obj); MWBase::LuaManager::ActorControls* getActorControls() { return &mData.mControls; } diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 3525906aac..7e15faee47 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -52,6 +52,7 @@ namespace MWLua Log(Debug::Verbose) << "Lua scripts configuration (" << mConfiguration.size() << " scripts):"; for (size_t i = 0; i < mConfiguration.size(); ++i) Log(Debug::Verbose) << "#" << i << " " << LuaUtil::scriptCfgToString(mConfiguration[i]); + mGlobalScripts.setAutoStartConf(mConfiguration.getGlobalConf()); } void LuaManager::init() @@ -306,7 +307,7 @@ namespace MWLua LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); if (!localScripts) { - localScripts = createLocalScripts(ptr, ESM::LuaScriptCfg::sPlayer); + localScripts = createLocalScripts(ptr); localScripts->addAutoStartedScripts(); } mActiveLocalScripts.insert(localScripts); @@ -336,10 +337,11 @@ namespace MWLua LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); if (!localScripts) { - ESM::LuaScriptCfg::Flags flag = getLuaScriptFlag(ptr); - if (!mConfiguration.getListByFlag(flag).empty()) + LuaUtil::ScriptIdsWithInitializationData autoStartConf = + mConfiguration.getLocalConf(ptr.getLuaType(), ptr.getCellRef().getRefId(), getId(ptr)); + if (!autoStartConf.empty()) { - localScripts = createLocalScripts(ptr, flag); + localScripts = createLocalScripts(ptr, std::move(autoStartConf)); localScripts->addAutoStartedScripts(); // TODO: put to a queue and apply on next `update()` } } @@ -398,7 +400,7 @@ namespace MWLua LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); if (!localScripts) { - localScripts = createLocalScripts(ptr, getLuaScriptFlag(ptr)); + localScripts = createLocalScripts(ptr); localScripts->addAutoStartedScripts(); if (ptr.isInCell() && MWBase::Environment::get().getWorld()->isCellActive(ptr.getCell())) mActiveLocalScripts.insert(localScripts); @@ -406,16 +408,18 @@ namespace MWLua localScripts->addCustomScript(scriptId); } - LocalScripts* LuaManager::createLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScriptCfg::Flags flag) + LocalScripts* LuaManager::createLocalScripts(const MWWorld::Ptr& ptr, + std::optional autoStartConf) { assert(mInitialized); - assert(flag != ESM::LuaScriptCfg::sGlobal); - assert(ptr.getType() != ESM::REC_STAT); std::shared_ptr scripts; - if (flag == ESM::LuaScriptCfg::sPlayer) + uint32_t type = ptr.getLuaType(); + if (type == ESM::REC_STAT) + throw std::runtime_error("Lua scripts on static objects are not allowed"); + else if (type == ESM::REC_INTERNAL_PLAYER) { - assert(ptr.getCellRef().getRefId() == "player"); scripts = std::make_shared(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry())); + scripts->setAutoStartConf(mConfiguration.getPlayerConf()); scripts->addPackage("openmw.ui", mUserInterfacePackage); scripts->addPackage("openmw.camera", mCameraPackage); scripts->addPackage("openmw.input", mInputPackage); @@ -426,7 +430,10 @@ namespace MWLua } else { - scripts = std::make_shared(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry()), flag); + scripts = std::make_shared(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry())); + if (!autoStartConf.has_value()) + autoStartConf = mConfiguration.getLocalConf(type, ptr.getCellRef().getRefId(), getId(ptr)); + scripts->setAutoStartConf(std::move(*autoStartConf)); scripts->addPackage("openmw.settings", mLocalSettingsPackage); scripts->addPackage("openmw.storage", mLocalStoragePackage); } @@ -461,9 +468,8 @@ namespace MWLua globalScripts.load(reader); loadEvents(mLua.sol(), reader, mGlobalEvents, mLocalEvents, mContentFileMapping, mGlobalLoader.get()); - mGlobalScripts.setSerializer(mGlobalLoader.get()); + mGlobalScripts.setSavedDataDeserializer(mGlobalLoader.get()); mGlobalScripts.load(globalScripts); - mGlobalScripts.setSerializer(mGlobalSerializer.get()); mGlobalScriptsStarted = true; } @@ -485,11 +491,11 @@ namespace MWLua } mWorldView.getObjectRegistry()->registerPtr(ptr); - LocalScripts* scripts = createLocalScripts(ptr, getLuaScriptFlag(ptr)); + LocalScripts* scripts = createLocalScripts(ptr); - scripts->setSerializer(mLocalLoader.get()); - scripts->load(data); scripts->setSerializer(mLocalSerializer.get()); + scripts->setSavedDataDeserializer(mLocalLoader.get()); + scripts->load(data); // LiveCellRef is usually copied after loading, so this Ptr will become invalid and should be deregistered. mWorldView.getObjectRegistry()->deregisterPtr(ptr); @@ -506,6 +512,7 @@ namespace MWLua initConfiguration(); { // Reload global scripts + mGlobalScripts.setSavedDataDeserializer(mGlobalSerializer.get()); ESM::LuaScripts data; mGlobalScripts.save(data); mGlobalScripts.load(data); @@ -516,6 +523,7 @@ namespace MWLua LocalScripts* scripts = ptr.getRefData().getLuaScripts(); if (scripts == nullptr) continue; + scripts->setSavedDataDeserializer(mLocalSerializer.get()); ESM::LuaScripts data; scripts->save(data); scripts->load(data); diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index 871ab0af26..e1eaac9ff8 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -123,7 +123,8 @@ namespace MWLua private: void initConfiguration(); - LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScriptCfg::Flags); + LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr, + std::optional autoStartConf = std::nullopt); bool mInitialized = false; bool mGlobalScriptsStarted = false; diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp index 113e3acde5..1b0436ab7f 100644 --- a/apps/openmw/mwlua/objectbindings.cpp +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -222,8 +222,7 @@ namespace MWLua LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); if (!localScripts || !localScripts->hasScript(*scriptId)) throw std::runtime_error("There is no script " + std::string(path) + " on " + ptrToString(ptr)); - ESM::LuaScriptCfg::Flags flags = cfg[*scriptId].mFlags; - if ((flags & (localScripts->getAutoStartMode() | ESM::LuaScriptCfg::sCustom)) != ESM::LuaScriptCfg::sCustom) + if (localScripts->getAutoStartConf().count(*scriptId) > 0) throw std::runtime_error("Autostarted script can not be removed: " + std::string(path)); localScripts->removeScript(*scriptId); }; diff --git a/apps/openmw/mwlua/playerscripts.hpp b/apps/openmw/mwlua/playerscripts.hpp index eb248ccc40..b8173f6b7b 100644 --- a/apps/openmw/mwlua/playerscripts.hpp +++ b/apps/openmw/mwlua/playerscripts.hpp @@ -15,7 +15,7 @@ namespace MWLua class PlayerScripts : public LocalScripts { public: - PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) : LocalScripts(lua, obj, ESM::LuaScriptCfg::sPlayer) + PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) : LocalScripts(lua, obj) { registerEngineHandlers({ &mConsoleCommandHandlers, &mKeyPressHandlers, &mKeyReleaseHandlers, diff --git a/apps/openmw/mwlua/types/types.cpp b/apps/openmw/mwlua/types/types.cpp index 6533696896..4ff72d7cea 100644 --- a/apps/openmw/mwlua/types/types.cpp +++ b/apps/openmw/mwlua/types/types.cpp @@ -35,33 +35,27 @@ namespace MWLua namespace { - struct LuaObjectTypeInfo - { - std::string_view mName; - ESM::LuaScriptCfg::Flags mFlag = 0; - }; - - const static std::unordered_map luaObjectTypeInfo = { - {ESM::REC_INTERNAL_PLAYER, {ObjectTypeName::Player, ESM::LuaScriptCfg::sPlayer}}, - {ESM::REC_INTERNAL_MARKER, {ObjectTypeName::Marker}}, - {ESM::REC_ACTI, {ObjectTypeName::Activator, ESM::LuaScriptCfg::sActivator}}, - {ESM::REC_ARMO, {ObjectTypeName::Armor, ESM::LuaScriptCfg::sArmor}}, - {ESM::REC_BOOK, {ObjectTypeName::Book, ESM::LuaScriptCfg::sBook}}, - {ESM::REC_CLOT, {ObjectTypeName::Clothing, ESM::LuaScriptCfg::sClothing}}, - {ESM::REC_CONT, {ObjectTypeName::Container, ESM::LuaScriptCfg::sContainer}}, - {ESM::REC_CREA, {ObjectTypeName::Creature, ESM::LuaScriptCfg::sCreature}}, - {ESM::REC_DOOR, {ObjectTypeName::Door, ESM::LuaScriptCfg::sDoor}}, - {ESM::REC_INGR, {ObjectTypeName::Ingredient, ESM::LuaScriptCfg::sIngredient}}, - {ESM::REC_LIGH, {ObjectTypeName::Light, ESM::LuaScriptCfg::sLight}}, - {ESM::REC_MISC, {ObjectTypeName::MiscItem, ESM::LuaScriptCfg::sMiscItem}}, - {ESM::REC_NPC_, {ObjectTypeName::NPC, ESM::LuaScriptCfg::sNPC}}, - {ESM::REC_ALCH, {ObjectTypeName::Potion, ESM::LuaScriptCfg::sPotion}}, - {ESM::REC_STAT, {ObjectTypeName::Static}}, - {ESM::REC_WEAP, {ObjectTypeName::Weapon, ESM::LuaScriptCfg::sWeapon}}, - {ESM::REC_APPA, {ObjectTypeName::Apparatus}}, - {ESM::REC_LOCK, {ObjectTypeName::Lockpick}}, - {ESM::REC_PROB, {ObjectTypeName::Probe}}, - {ESM::REC_REPA, {ObjectTypeName::Repair}}, + const static std::unordered_map luaObjectTypeInfo = { + {ESM::REC_INTERNAL_PLAYER, ObjectTypeName::Player}, + {ESM::REC_INTERNAL_MARKER, ObjectTypeName::Marker}, + {ESM::REC_ACTI, ObjectTypeName::Activator}, + {ESM::REC_ARMO, ObjectTypeName::Armor}, + {ESM::REC_BOOK, ObjectTypeName::Book}, + {ESM::REC_CLOT, ObjectTypeName::Clothing}, + {ESM::REC_CONT, ObjectTypeName::Container}, + {ESM::REC_CREA, ObjectTypeName::Creature}, + {ESM::REC_DOOR, ObjectTypeName::Door}, + {ESM::REC_INGR, ObjectTypeName::Ingredient}, + {ESM::REC_LIGH, ObjectTypeName::Light}, + {ESM::REC_MISC, ObjectTypeName::MiscItem}, + {ESM::REC_NPC_, ObjectTypeName::NPC}, + {ESM::REC_ALCH, ObjectTypeName::Potion}, + {ESM::REC_STAT, ObjectTypeName::Static}, + {ESM::REC_WEAP, ObjectTypeName::Weapon}, + {ESM::REC_APPA, ObjectTypeName::Apparatus}, + {ESM::REC_LOCK, ObjectTypeName::Lockpick}, + {ESM::REC_PROB, ObjectTypeName::Probe}, + {ESM::REC_REPA, ObjectTypeName::Repair}, }; } @@ -70,7 +64,7 @@ namespace MWLua { auto it = luaObjectTypeInfo.find(type); if (it != luaObjectTypeInfo.end()) - return it->second.mName; + return it->second; else return fallback; } @@ -80,15 +74,6 @@ namespace MWLua return getLuaObjectTypeName(static_cast(ptr.getLuaType()), /*fallback=*/ptr.getTypeDescription()); } - ESM::LuaScriptCfg::Flags getLuaScriptFlag(const MWWorld::Ptr& ptr) - { - auto it = luaObjectTypeInfo.find(static_cast(ptr.getLuaType())); - if (it != luaObjectTypeInfo.end()) - return it->second.mFlag; - else - return 0; - } - const MWWorld::Ptr& verifyType(ESM::RecNameInts recordType, const MWWorld::Ptr& ptr) { if (ptr.getType() != recordType) @@ -179,9 +164,9 @@ namespace MWLua sol::table typeToPackage = getTypeToPackageTable(context.mLua->sol()); sol::table packageToType = getPackageToTypeTable(context.mLua->sol()); - for (const auto& [type, v] : luaObjectTypeInfo) + for (const auto& [type, name] : luaObjectTypeInfo) { - sol::object t = types[v.mName]; + sol::object t = types[name]; if (t == sol::nil) continue; typeToPackage[type] = t; diff --git a/apps/openmw/mwlua/types/types.hpp b/apps/openmw/mwlua/types/types.hpp index 63df9a37f1..11d31ee673 100644 --- a/apps/openmw/mwlua/types/types.hpp +++ b/apps/openmw/mwlua/types/types.hpp @@ -17,10 +17,6 @@ namespace MWLua sol::table getTypeToPackageTable(lua_State* L); sol::table getPackageToTypeTable(lua_State* L); - // Each script has a set of flags that controls to which objects the script should be - // automatically attached. This function maps each object types to one of the flags. - ESM::LuaScriptCfg::Flags getLuaScriptFlag(const MWWorld::Ptr& ptr); - sol::table initTypesPackage(const Context& context); // used in initTypesPackage diff --git a/apps/openmw/mwlua/userdataserializer.cpp b/apps/openmw/mwlua/userdataserializer.cpp index b675774c53..3aabbfe0ac 100644 --- a/apps/openmw/mwlua/userdataserializer.cpp +++ b/apps/openmw/mwlua/userdataserializer.cpp @@ -21,11 +21,7 @@ namespace MWLua { if (data.is() || data.is()) { - ObjectId id = data.as().id(); - static_assert(sizeof(ObjectId) == 8); - id.mIndex = Misc::toLittleEndian(id.mIndex); - id.mContentFile = Misc::toLittleEndian(id.mContentFile); - append(out, "o", &id, sizeof(ObjectId)); + appendRefNum(out, data.as().id()); return true; } return false; @@ -35,14 +31,9 @@ namespace MWLua // Returns false if this type is not supported by this serializer. bool deserialize(std::string_view typeName, std::string_view binaryData, lua_State* lua) const override { - if (typeName == "o") + if (typeName == sRefNumTypeName) { - if (binaryData.size() != sizeof(ObjectId)) - throw std::runtime_error("Incorrect serialization format. Size of ObjectId doesn't match."); - ObjectId id; - std::memcpy(&id, binaryData.data(), sizeof(ObjectId)); - id.mIndex = Misc::fromLittleEndian(id.mIndex); - id.mContentFile = Misc::fromLittleEndian(id.mContentFile); + ObjectId id = loadRefNum(binaryData); if (id.hasContentFile() && mContentFileMapping) { auto iter = mContentFileMapping->find(id.mContentFile); diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index bd93cc7c4e..c611155cf4 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -191,7 +191,7 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener, ESM::Dialo { ESM::LuaScriptsCfg cfg; cfg.load(esm); - // TODO: update refnums in cfg.mScripts[].mInitializationData according to load order + cfg.adjustRefNums(esm); mLuaContent.push_back(std::move(cfg)); } else { diff --git a/apps/openmw_test_suite/lua/test_configuration.cpp b/apps/openmw_test_suite/lua/test_configuration.cpp index 4869d42284..7170f33849 100644 --- a/apps/openmw_test_suite/lua/test_configuration.cpp +++ b/apps/openmw_test_suite/lua/test_configuration.cpp @@ -1,14 +1,31 @@ #include "gmock/gmock.h" #include +#include + +#include +#include +#include #include +#include #include "../testing_util.hpp" namespace { - TEST(LuaConfigurationTest, ValidConfiguration) + using testing::ElementsAre; + using testing::Pair; + + std::vector> asVector(const LuaUtil::ScriptIdsWithInitializationData& d) + { + std::vector> res; + for (const auto& [k, v] : d) + res.emplace_back(k, std::string(v)); + return res; + } + + TEST(LuaConfigurationTest, ValidOMWScripts) { ESM::LuaScriptsCfg cfg; LuaUtil::parseOMWScripts(cfg, R"X( @@ -19,40 +36,208 @@ namespace PLAYER :my_mod/player.lua CUSTOM : my_mod/some_other_script.lua NPC , CREATURE PLAYER : my_mod/some_other_script.lua)X"); - LuaUtil::parseOMWScripts(cfg, ":my_mod/player.LUA \r\nCONTAINER,CUSTOM: my_mod/container.lua\r\n"); + LuaUtil::parseOMWScripts(cfg, ":my_mod/player.LUA \r\nCREATURE,CUSTOM: my_mod/creature.lua\r\n"); ASSERT_EQ(cfg.mScripts.size(), 6); EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[0]), "GLOBAL : my_mod/#some_global_script.lua"); EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[1]), "PLAYER : my_mod/player.lua"); EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[2]), "CUSTOM : my_mod/some_other_script.lua"); - EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[3]), "CREATURE NPC PLAYER : my_mod/some_other_script.lua"); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[3]), "PLAYER NPC CREATURE : my_mod/some_other_script.lua"); EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[4]), ": my_mod/player.LUA"); - EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[5]), "CONTAINER CUSTOM : my_mod/container.lua"); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[5]), "CUSTOM CREATURE : my_mod/creature.lua"); LuaUtil::ScriptsConfiguration conf; conf.init(std::move(cfg)); - ASSERT_EQ(conf.size(), 3); + ASSERT_EQ(conf.size(), 4); EXPECT_EQ(LuaUtil::scriptCfgToString(conf[0]), "GLOBAL : my_mod/#some_global_script.lua"); // cfg.mScripts[1] is overridden by cfg.mScripts[4] // cfg.mScripts[2] is overridden by cfg.mScripts[3] - EXPECT_EQ(LuaUtil::scriptCfgToString(conf[1]), "CREATURE NPC PLAYER : my_mod/some_other_script.lua"); - // cfg.mScripts[4] is removed because there are no flags - EXPECT_EQ(LuaUtil::scriptCfgToString(conf[2]), "CONTAINER CUSTOM : my_mod/container.lua"); + EXPECT_EQ(LuaUtil::scriptCfgToString(conf[1]), "PLAYER NPC CREATURE : my_mod/some_other_script.lua"); + EXPECT_EQ(LuaUtil::scriptCfgToString(conf[2]), ": my_mod/player.LUA"); + EXPECT_EQ(LuaUtil::scriptCfgToString(conf[3]), "CUSTOM CREATURE : my_mod/creature.lua"); + EXPECT_THAT(asVector(conf.getGlobalConf()), ElementsAre(Pair(0, ""))); + EXPECT_THAT(asVector(conf.getPlayerConf()), ElementsAre(Pair(1, ""))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_CONT, "something", ESM::RefNum())), ElementsAre()); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_NPC_, "something", ESM::RefNum())), ElementsAre(Pair(1, ""))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_CREA, "something", ESM::RefNum())), ElementsAre(Pair(1, ""), Pair(3, ""))); + + // Check that initialization cleans old data cfg = ESM::LuaScriptsCfg(); conf.init(std::move(cfg)); - ASSERT_EQ(conf.size(), 0); + EXPECT_EQ(conf.size(), 0); } - TEST(LuaConfigurationTest, Errors) + TEST(LuaConfigurationTest, InvalidOMWScripts) { ESM::LuaScriptsCfg cfg; EXPECT_ERROR(LuaUtil::parseOMWScripts(cfg, "GLOBAL: something"), "Lua script should have suffix '.lua', got: GLOBAL: something"); EXPECT_ERROR(LuaUtil::parseOMWScripts(cfg, "something.lua"), "No flags found in: something.lua"); - EXPECT_ERROR(LuaUtil::parseOMWScripts(cfg, "GLOBAL, PLAYER: something.lua"), - "Global script can not have local flags"); + + cfg.mScripts.clear(); + EXPECT_NO_THROW(LuaUtil::parseOMWScripts(cfg, "GLOBAL, PLAYER: something.lua")); + LuaUtil::ScriptsConfiguration conf; + EXPECT_ERROR(conf.init(std::move(cfg)), "Global script can not have local flags"); } + TEST(LuaConfigurationTest, ConfInit) + { + ESM::LuaScriptsCfg cfg; + ESM::LuaScriptCfg& script1 = cfg.mScripts.emplace_back(); + script1.mScriptPath = "Script1.lua"; + script1.mInitializationData = "data1"; + script1.mFlags = ESM::LuaScriptCfg::sPlayer; + script1.mTypes.push_back(ESM::REC_CREA); + script1.mRecords.push_back({true, "record1", "dataRecord1"}); + script1.mRefs.push_back({true, 2, 3, ""}); + script1.mRefs.push_back({true, 2, 4, ""}); + + ESM::LuaScriptCfg& script2 = cfg.mScripts.emplace_back(); + script2.mScriptPath = "Script2.lua"; + script2.mFlags = ESM::LuaScriptCfg::sCustom; + script2.mTypes.push_back(ESM::REC_CONT); + + ESM::LuaScriptCfg& script1Extra = cfg.mScripts.emplace_back(); + script1Extra.mScriptPath = "script1.LUA"; + script1Extra.mFlags = ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sMerge; + script1Extra.mTypes.push_back(ESM::REC_NPC_); + script1Extra.mRecords.push_back({false, "rat", ""}); + script1Extra.mRecords.push_back({true, "record2", ""}); + script1Extra.mRefs.push_back({true, 3, 5, "dataRef35"}); + script1Extra.mRefs.push_back({false, 2, 3, ""}); + + LuaUtil::ScriptsConfiguration conf; + conf.init(cfg); + ASSERT_EQ(conf.size(), 2); + EXPECT_EQ(LuaUtil::scriptCfgToString(conf[0]), + "CUSTOM PLAYER CREATURE NPC : Script1.lua ; data 5 bytes ; 3 records ; 4 objects"); + EXPECT_EQ(LuaUtil::scriptCfgToString(conf[1]), "CUSTOM CONTAINER : Script2.lua"); + + EXPECT_THAT(asVector(conf.getPlayerConf()), ElementsAre(Pair(0, "data1"))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_CONT, "something", ESM::RefNum())), ElementsAre(Pair(1, ""))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_CREA, "guar", ESM::RefNum())), ElementsAre(Pair(0, "data1"))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_CREA, "rat", ESM::RefNum())), ElementsAre()); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_DOOR, "record1", ESM::RefNum())), ElementsAre(Pair(0, "dataRecord1"))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_DOOR, "record2", ESM::RefNum())), ElementsAre(Pair(0, "data1"))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_NPC_, "record3", {1, 1})), ElementsAre(Pair(0, "data1"))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_NPC_, "record3", {2, 3})), ElementsAre()); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_NPC_, "record3", {3, 5})), ElementsAre(Pair(0, "dataRef35"))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_CONT, "record4", {2, 4})), ElementsAre(Pair(0, "data1"), Pair(1, ""))); + + ESM::LuaScriptCfg& script3 = cfg.mScripts.emplace_back(); + script3.mScriptPath = "script1.lua"; + script3.mFlags = ESM::LuaScriptCfg::sGlobal; + EXPECT_ERROR(conf.init(cfg), "Flags mismatch for script1.lua"); + } + + TEST(LuaConfigurationTest, Serialization) + { + sol::state lua; + LuaUtil::BasicSerializer serializer; + + ESM::ESMWriter writer; + writer.setAuthor(""); + writer.setDescription(""); + writer.setRecordCount(1); + writer.setFormat(ESM::Header::CurrentFormat); + writer.setVersion(); + writer.addMaster("morrowind.esm", 0); + + ESM::LuaScriptsCfg cfg; + std::string luaData; + { + sol::table data(lua, sol::create); + data["number"] = 5; + data["string"] = "some value"; + data["fargoth"] = ESM::RefNum{128964, 1}; + luaData = LuaUtil::serialize(data, &serializer); + } + { + ESM::LuaScriptCfg& script = cfg.mScripts.emplace_back(); + script.mScriptPath = "test_global.lua"; + script.mFlags = ESM::LuaScriptCfg::sGlobal; + script.mInitializationData = luaData; + } + { + ESM::LuaScriptCfg& script = cfg.mScripts.emplace_back(); + script.mScriptPath = "test_local.lua"; + script.mFlags = ESM::LuaScriptCfg::sMerge; + script.mTypes.push_back(ESM::REC_DOOR); + script.mTypes.push_back(ESM::REC_MISC); + script.mRecords.push_back({true, "rat", luaData}); + script.mRecords.push_back({false, "chargendoorjournal", ""}); + script.mRefs.push_back({true, 128964, 1, ""}); + script.mRefs.push_back({true, 128962, 1, luaData}); + } + + std::stringstream stream; + writer.save(stream); + writer.startRecord(ESM::REC_LUAL); + cfg.save(writer); + writer.endRecord(ESM::REC_LUAL); + writer.close(); + std::string serializedOMWAddon = stream.str(); + + { + // Save for manual testing. + std::ofstream f(TestingOpenMW::outputFilePath("lua_conf_test.omwaddon"), std::ios::binary); + f << serializedOMWAddon; + f.close(); + } + + ESM::ESMReader reader; + reader.open(std::make_unique(serializedOMWAddon), "lua_conf_test.omwaddon"); + ASSERT_EQ(reader.getRecordCount(), 1); + ASSERT_EQ(reader.getRecName().toInt(), ESM::REC_LUAL); + reader.getRecHeader(); + ESM::LuaScriptsCfg loadedCfg; + loadedCfg.load(reader); + + ASSERT_EQ(loadedCfg.mScripts.size(), cfg.mScripts.size()); + for (size_t i = 0; i < cfg.mScripts.size(); ++i) + { + EXPECT_EQ(loadedCfg.mScripts[i].mScriptPath, cfg.mScripts[i].mScriptPath); + EXPECT_EQ(loadedCfg.mScripts[i].mFlags, cfg.mScripts[i].mFlags); + EXPECT_EQ(loadedCfg.mScripts[i].mInitializationData, cfg.mScripts[i].mInitializationData); + ASSERT_EQ(loadedCfg.mScripts[i].mTypes.size(), cfg.mScripts[i].mTypes.size()); + for (size_t j = 0; j < cfg.mScripts[i].mTypes.size(); ++j) + EXPECT_EQ(loadedCfg.mScripts[i].mTypes[j], cfg.mScripts[i].mTypes[j]); + ASSERT_EQ(loadedCfg.mScripts[i].mRecords.size(), cfg.mScripts[i].mRecords.size()); + for (size_t j = 0; j < cfg.mScripts[i].mRecords.size(); ++j) + { + EXPECT_EQ(loadedCfg.mScripts[i].mRecords[j].mAttach, cfg.mScripts[i].mRecords[j].mAttach); + EXPECT_EQ(loadedCfg.mScripts[i].mRecords[j].mRecordId, cfg.mScripts[i].mRecords[j].mRecordId); + EXPECT_EQ(loadedCfg.mScripts[i].mRecords[j].mInitializationData, cfg.mScripts[i].mRecords[j].mInitializationData); + } + ASSERT_EQ(loadedCfg.mScripts[i].mRefs.size(), cfg.mScripts[i].mRefs.size()); + for (size_t j = 0; j < cfg.mScripts[i].mRefs.size(); ++j) + { + EXPECT_EQ(loadedCfg.mScripts[i].mRefs[j].mAttach, cfg.mScripts[i].mRefs[j].mAttach); + EXPECT_EQ(loadedCfg.mScripts[i].mRefs[j].mRefnumIndex, cfg.mScripts[i].mRefs[j].mRefnumIndex); + EXPECT_EQ(loadedCfg.mScripts[i].mRefs[j].mRefnumContentFile, cfg.mScripts[i].mRefs[j].mRefnumContentFile); + EXPECT_EQ(loadedCfg.mScripts[i].mRefs[j].mInitializationData, cfg.mScripts[i].mRefs[j].mInitializationData); + } + } + + { + ESM::ReadersCache readers(4); + readers.get(0)->openRaw(std::make_unique("dummyData"), "a.omwaddon"); + readers.get(1)->openRaw(std::make_unique("dummyData"), "b.omwaddon"); + readers.get(2)->openRaw(std::make_unique("dummyData"), "Morrowind.esm"); + readers.get(3)->openRaw(std::make_unique("dummyData"), "c.omwaddon"); + reader.setIndex(3); + reader.resolveParentFileIndices(readers); + } + loadedCfg.adjustRefNums(reader); + EXPECT_EQ(loadedCfg.mScripts[1].mRefs[0].mRefnumIndex, cfg.mScripts[1].mRefs[0].mRefnumIndex); + EXPECT_EQ(loadedCfg.mScripts[1].mRefs[0].mRefnumContentFile, 2); + { + sol::table data = LuaUtil::deserialize(lua.lua_state(), loadedCfg.mScripts[1].mRefs[1].mInitializationData, &serializer); + ESM::RefNum adjustedRef = data["fargoth"].get(); + EXPECT_EQ(adjustedRef.mIndex, 128964); + EXPECT_EQ(adjustedRef.mContentFile, 2); + } + } } diff --git a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp index 18aeffbbf2..84b632913c 100644 --- a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp +++ b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp @@ -135,17 +135,19 @@ return { LuaScriptsContainerTest() { ESM::LuaScriptsCfg cfg; - cfg.mScripts.push_back({"invalid.lua", "", ESM::LuaScriptCfg::sCustom}); - cfg.mScripts.push_back({"incorrect.lua", "", ESM::LuaScriptCfg::sCustom}); - cfg.mScripts.push_back({"empty.lua", "", ESM::LuaScriptCfg::sCustom}); - cfg.mScripts.push_back({"test1.lua", "", ESM::LuaScriptCfg::sCustom}); - cfg.mScripts.push_back({"stopEvent.lua", "", ESM::LuaScriptCfg::sCustom}); - cfg.mScripts.push_back({"test2.lua", "", ESM::LuaScriptCfg::sCustom}); - cfg.mScripts.push_back({"loadSave1.lua", "", ESM::LuaScriptCfg::sNPC}); - cfg.mScripts.push_back({"loadSave2.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sNPC}); - cfg.mScripts.push_back({"testInterface.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sPlayer}); - cfg.mScripts.push_back({"overrideInterface.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sPlayer}); - cfg.mScripts.push_back({"useInterface.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sPlayer}); + LuaUtil::parseOMWScripts(cfg, R"X( +CUSTOM: invalid.lua +CUSTOM: incorrect.lua +CUSTOM: empty.lua +CUSTOM: test1.lua +CUSTOM: stopEvent.lua +CUSTOM: test2.lua +NPC: loadSave1.lua +CUSTOM, NPC: loadSave2.lua +CUSTOM, PLAYER: testInterface.lua +CUSTOM, PLAYER: overrideInterface.lua +CUSTOM, PLAYER: useInterface.lua +)X"); mCfg.init(std::move(cfg)); } }; @@ -278,7 +280,8 @@ return { TEST_F(LuaScriptsContainerTest, AutoStart) { - LuaUtil::ScriptsContainer scripts(&mLua, "Test", ESM::LuaScriptCfg::sPlayer); + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + scripts.setAutoStartConf(mCfg.getPlayerConf()); testing::internal::CaptureStdout(); scripts.addAutoStartedScripts(); scripts.update(1.5f); @@ -291,7 +294,8 @@ return { TEST_F(LuaScriptsContainerTest, Interface) { - LuaUtil::ScriptsContainer scripts(&mLua, "Test", ESM::LuaScriptCfg::sCreature); + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + scripts.setAutoStartConf(mCfg.getLocalConf(ESM::REC_CREA, "", ESM::RefNum())); int addIfaceId = *mCfg.findId("testInterface.lua"); int overrideIfaceId = *mCfg.findId("overrideInterface.lua"); int useIfaceId = *mCfg.findId("useInterface.lua"); @@ -318,9 +322,12 @@ return { TEST_F(LuaScriptsContainerTest, LoadSave) { - LuaUtil::ScriptsContainer scripts1(&mLua, "Test", ESM::LuaScriptCfg::sNPC); - LuaUtil::ScriptsContainer scripts2(&mLua, "Test", ESM::LuaScriptCfg::sNPC); - LuaUtil::ScriptsContainer scripts3(&mLua, "Test", ESM::LuaScriptCfg::sPlayer); + LuaUtil::ScriptsContainer scripts1(&mLua, "Test"); + LuaUtil::ScriptsContainer scripts2(&mLua, "Test"); + LuaUtil::ScriptsContainer scripts3(&mLua, "Test"); + scripts1.setAutoStartConf(mCfg.getLocalConf(ESM::REC_NPC_, "", ESM::RefNum())); + scripts2.setAutoStartConf(mCfg.getLocalConf(ESM::REC_NPC_, "", ESM::RefNum())); + scripts3.setAutoStartConf(mCfg.getPlayerConf()); scripts1.addAutoStartedScripts(); EXPECT_TRUE(scripts1.addCustomScript(*mCfg.findId("test1.lua"))); diff --git a/components/esm/defs.hpp b/components/esm/defs.hpp index 04019c65e7..f3e7e9506c 100644 --- a/components/esm/defs.hpp +++ b/components/esm/defs.hpp @@ -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 diff --git a/components/esm/luascripts.cpp b/components/esm/luascripts.cpp index 17c5e2843a..d541482ad6 100644 --- a/components/esm/luascripts.cpp +++ b/components/esm/luascripts.cpp @@ -3,6 +3,8 @@ #include "components/esm3/esmreader.hpp" #include "components/esm3/esmwriter.hpp" +#include + // 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(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(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(refCfg.mRefnumIndex); + esm.getT(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(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(script.mFlags); + for (uint32_t type : script.mTypes) + esm.writeT(type); + esm.endRecord("LUAF"); saveLuaBinaryData(esm, script.mInitializationData); + for (const LuaScriptCfg::PerRecordCfg& recordCfg : script.mRecords) + { + esm.startSubRecord("LUAR"); + esm.writeT(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(refCfg.mAttach ? 1 : 0); + esm.writeT(refCfg.mRefnumIndex); + esm.writeT(refCfg.mRefnumContentFile); + esm.endRecord("LUAI"); + saveLuaBinaryData(esm, refCfg.mInitializationData); + } } } diff --git a/components/esm/luascripts.hpp b/components/esm/luascripts.hpp index 985d756fce..c3bb94ac70 100644 --- a/components/esm/luascripts.hpp +++ b/components/esm/luascripts.hpp @@ -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 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 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 mRefs; }; struct LuaScriptsCfg @@ -42,6 +55,8 @@ namespace ESM std::vector mScripts; void load(ESMReader &esm); + void adjustRefNums(const ESMReader &esm); + void save(ESMWriter &esm) const; }; diff --git a/components/esm3/loadtes3.hpp b/components/esm3/loadtes3.hpp index 8f5ed68413..ea41ce9fb6 100644 --- a/components/esm3/loadtes3.hpp +++ b/components/esm3/loadtes3.hpp @@ -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 diff --git a/components/lua/configuration.cpp b/components/lua/configuration.cpp index 172d958029..b4889c44d1 100644 --- a/components/lua/configuration.cpp +++ b/components/lua/configuration.cpp @@ -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> 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 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 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(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& 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(); } diff --git a/components/lua/configuration.hpp b/components/lua/configuration.hpp index 5baa4f24cf..87159068be 100644 --- a/components/lua/configuration.hpp +++ b/components/lua/configuration.hpp @@ -5,9 +5,11 @@ #include #include "components/esm/luascripts.hpp" +#include "components/esm3/cellref.hpp" namespace LuaUtil { + using ScriptIdsWithInitializationData = std::map; class ScriptsConfiguration { @@ -18,13 +20,27 @@ namespace LuaUtil const ESM::LuaScriptCfg& operator[](int id) const { return mScripts[id]; } std::optional findId(std::string_view path) const; - const std::vector& 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 mScripts; std::map> mPathToIndex; - std::map> mScriptsByFlag; - static const std::vector sEmpty; + + struct DetailedConf + { + int mScriptId; + bool mAttach; + std::string_view mInitializationData; + }; + std::map> mScriptsPerType; + std::map, std::less<>> mScriptsPerRecordId; + std::map> mScriptsPerRefNum; }; // Parse ESM::LuaScriptsCfg from text and add to `cfg`. diff --git a/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp index 11ca28de08..9e20a215fa 100644 --- a/components/lua/scriptscontainer.cpp +++ b/components/lua/scriptscontainer.cpp @@ -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 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 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 scripts; - for (int scriptId : mLua.getConfiguration().getListByFlag(mAutoStartMode)) - scripts[scriptId] = nullptr; + struct ScriptInfo + { + std::string_view mInitData; + const ESM::LuaScript* mSavedData; + }; + std::map scripts; + for (const auto& [scriptId, initData] : mAutoStartScripts) + scripts[scriptId] = {initData, nullptr}; for (const ESM::LuaScript& s : data.mScripts) { std::optional 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 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); diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp index f64ec97722..1599966376 100644 --- a/components/lua/scriptscontainer.hpp +++ b/components/lua/scriptscontainer.hpp @@ -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& timerQueue, double time); static void insertTimer(std::vector& 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 mAPI; std::map mScripts; diff --git a/components/lua/serialization.cpp b/components/lua/serialization.cpp index d0bbbc9dc5..f1ee7c1aae 100644 --- a/components/lua/serialization.cpp +++ b/components/lua/serialization.cpp @@ -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()); + 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(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()) diff --git a/components/lua/serialization.hpp b/components/lua/serialization.hpp index d685bb2ad6..03de88cf8e 100644 --- a/components/lua/serialization.hpp +++ b/components/lua/serialization.hpp @@ -3,6 +3,8 @@ #include +#include + 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 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 mAdjustContentFilesIndexFn; }; BinaryData serialize(const sol::object&, const UserdataSerializer* customSerializer = nullptr); diff --git a/docs/source/reference/lua-scripting/overview.rst b/docs/source/reference/lua-scripting/overview.rst index 1bffb97e17..4cceb26088 100644 --- a/docs/source/reference/lua-scripting/overview.rst +++ b/docs/source/reference/lua-scripting/overview.rst @@ -165,7 +165,11 @@ Possible flags are: - ``MISC_ITEM`` - a local script that will be automatically attached to any miscellaneous item; - ``NPC`` - a local script that will be automatically attached to any NPC; - ``POTION`` - a local script that will be automatically attached to any potion; -- ``WEAPON`` - a local script that will be automatically attached to any weapon. +- ``WEAPON`` - a local script that will be automatically attached to any weapon; +- ``APPARATUS`` - a local script that will be automatically attached to any apparatus; +- ``LOCKPICK`` - a local script that will be automatically attached to any lockpick; +- ``PROBE`` - a local script that will be automatically attached to any probe tool; +- ``REPAIR`` - a local script that will be automatically attached to any repair tool. Several flags (except ``GLOBAL``) can be used with a single script. Use space or comma as a separator.