#include "configuration.hpp" #include #include #include #include #include #include #include namespace LuaUtil { namespace { const std::map> flagsByName{ { "GLOBAL", ESM::LuaScriptCfg::sGlobal }, { "CUSTOM", ESM::LuaScriptCfg::sCustom }, { "PLAYER", ESM::LuaScriptCfg::sPlayer }, { "MENU", ESM::LuaScriptCfg::sMenu }, }; 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) { return std::isspace(static_cast(c)); } } void ScriptsConfiguration::init(ESM::LuaScriptsCfg cfg) { mScripts.clear(); mPathToIndex.clear(); // 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 (size_t i = 0; i < cfg.mScripts.size(); ++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.value()); 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.value()); auto [it, inserted] = mPathToIndex.emplace(script.mScriptPath, i); if (inserted) continue; 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.value()); if (script.mFlags & ESM::LuaScriptCfg::sMerge) { 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 }); } } } std::optional ScriptsConfiguration::findId(VFS::Path::NormalizedView path) const { auto it = mPathToIndex.find(path); if (it != mPathToIndex.end()) return it->second; else return std::nullopt; } ScriptIdsWithInitializationData ScriptsConfiguration::getConfByFlag(ESM::LuaScriptCfg::Flags flag) const { 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, const ESM::RefId& 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) { while (!data.empty()) { // Get next line std::string_view line = data.substr(0, data.find('\n')); data = data.substr(std::min(line.size() + 1, data.size())); if (!line.empty() && line.back() == '\r') line = line.substr(0, line.size() - 1); while (!line.empty() && isSpace(line[0])) line = line.substr(1); if (line.empty() || line[0] == '#') // Skip empty lines and comments continue; while (!line.empty() && isSpace(line.back())) line = line.substr(0, line.size() - 1); if (!Misc::StringUtils::ciEndsWith(line, ".lua")) throw std::runtime_error(Misc::StringUtils::format( "Lua script should have suffix '.lua', got: %s", std::string(line.substr(0, 300)))); // 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 tagsStr = line.substr(0, semicolonPos); std::string_view scriptPath = line.substr(semicolonPos + 1); while (isSpace(scriptPath[0])) scriptPath = scriptPath.substr(1); ESM::LuaScriptCfg& script = cfg.mScripts.emplace_back(); script.mScriptPath = VFS::Path::Normalized(scriptPath); script.mFlags = 0; // Parse tags size_t tagsPos = 0; while (true) { 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 tagName = tagsStr.substr(startPos, tagsPos - startPos); auto it = flagsByName.find(tagName); auto typesIt = typeTagsByName.find(tagName); if (it != flagsByName.end()) script.mFlags |= it->second; else if (typesIt != typeTagsByName.end()) script.mTypes.push_back(typesIt->second); else throw std::runtime_error( Misc::StringUtils::format("Unknown tag '%s' in: %s", std::string(tagName), std::string(line))); } } } 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 << " ; 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(); } }