diff options
Diffstat (limited to 'NorthstarDLL/mods')
-rw-r--r-- | NorthstarDLL/mods/compiled/kb_act.cpp | 45 | ||||
-rw-r--r-- | NorthstarDLL/mods/compiled/modkeyvalues.cpp | 107 | ||||
-rw-r--r-- | NorthstarDLL/mods/compiled/modpdef.cpp | 119 | ||||
-rw-r--r-- | NorthstarDLL/mods/compiled/modscriptsrson.cpp | 66 | ||||
-rw-r--r-- | NorthstarDLL/mods/modmanager.cpp | 725 | ||||
-rw-r--r-- | NorthstarDLL/mods/modmanager.h | 154 |
6 files changed, 1216 insertions, 0 deletions
diff --git a/NorthstarDLL/mods/compiled/kb_act.cpp b/NorthstarDLL/mods/compiled/kb_act.cpp new file mode 100644 index 00000000..4a011dc7 --- /dev/null +++ b/NorthstarDLL/mods/compiled/kb_act.cpp @@ -0,0 +1,45 @@ +#include "pch.h" +#include "mods/modmanager.h" +#include "core/filesystem/filesystem.h" + +#include <fstream> + +const char* KB_ACT_PATH = "scripts\\kb_act.lst"; + +// compiles the file kb_act.lst, that defines entries for keybindings in the options menu +void ModManager::BuildKBActionsList() +{ + spdlog::info("Building kb_act.lst"); + + fs::create_directories(GetCompiledAssetsPath() / "scripts"); + std::ofstream soCompiledKeys(GetCompiledAssetsPath() / KB_ACT_PATH, std::ios::binary); + + // write vanilla file's content to compiled file + soCompiledKeys << R2::ReadVPKOriginalFile(KB_ACT_PATH); + + for (Mod& mod : m_LoadedMods) + { + if (!mod.m_bEnabled) + continue; + + // write content of each modded file to compiled file + std::ifstream siModKeys(mod.m_ModDirectory / "kb_act.lst"); + + if (siModKeys.good()) + soCompiledKeys << siModKeys.rdbuf() << std::endl; + + siModKeys.close(); + } + + soCompiledKeys.close(); + + // push to overrides + ModOverrideFile overrideFile; + overrideFile.m_pOwningMod = nullptr; + overrideFile.m_Path = KB_ACT_PATH; + + if (m_ModFiles.find(KB_ACT_PATH) == m_ModFiles.end()) + m_ModFiles.insert(std::make_pair(KB_ACT_PATH, overrideFile)); + else + m_ModFiles[KB_ACT_PATH] = overrideFile; +} diff --git a/NorthstarDLL/mods/compiled/modkeyvalues.cpp b/NorthstarDLL/mods/compiled/modkeyvalues.cpp new file mode 100644 index 00000000..774be0eb --- /dev/null +++ b/NorthstarDLL/mods/compiled/modkeyvalues.cpp @@ -0,0 +1,107 @@ +#include "pch.h" +#include "mods/modmanager.h" +#include "core/filesystem/filesystem.h" + +#include <fstream> + +AUTOHOOK_INIT() + +void ModManager::TryBuildKeyValues(const char* filename) +{ + spdlog::info("Building KeyValues for file {}", filename); + + std::string normalisedPath = g_pModManager->NormaliseModFilePath(fs::path(filename)); + fs::path compiledPath = GetCompiledAssetsPath() / filename; + fs::path compiledDir = compiledPath.parent_path(); + fs::create_directories(compiledDir); + + fs::path kvPath(filename); + std::string ogFilePath = "mod_original_"; + ogFilePath += kvPath.filename().string(); + + std::string newKvs = "// AUTOGENERATED: MOD PATCH KV\n"; + + int patchNum = 0; + + // copy over patch kv files, and add #bases to new file, last mods' patches should be applied first + // note: #include should be identical but it's actually just broken, thanks respawn + for (int64_t i = m_LoadedMods.size() - 1; i > -1; i--) + { + if (!m_LoadedMods[i].m_bEnabled) + continue; + + size_t fileHash = STR_HASH(normalisedPath); + auto modKv = m_LoadedMods[i].KeyValues.find(fileHash); + if (modKv != m_LoadedMods[i].KeyValues.end()) + { + // should result in smth along the lines of #include "mod_patch_5_mp_weapon_car.txt" + + std::string patchFilePath = "mod_patch_"; + patchFilePath += std::to_string(patchNum++); + patchFilePath += "_"; + patchFilePath += kvPath.filename().string(); + + newKvs += "#base \""; + newKvs += patchFilePath; + newKvs += "\"\n"; + + fs::remove(compiledDir / patchFilePath); + + fs::copy_file(m_LoadedMods[i].m_ModDirectory / "keyvalues" / filename, compiledDir / patchFilePath); + } + } + + // add original #base last, #bases don't override preexisting keys, including the ones we've just done + newKvs += "#base \""; + newKvs += ogFilePath; + newKvs += "\"\n"; + + // load original file, so we can parse out the name of the root obj (e.g. WeaponData for weapons) + std::string originalFile = R2::ReadVPKOriginalFile(filename); + + if (!originalFile.length()) + { + spdlog::warn("Tried to patch kv {} but no base kv was found!", ogFilePath); + return; + } + + char rootName[64]; + memset(rootName, 0, sizeof(rootName)); + + // iterate until we hit an ascii char that isn't in a # command or comment to get root obj name + int i = 0; + while (!(originalFile[i] >= 65 && originalFile[i] <= 122)) + { + // if we hit a comment or # thing, iterate until end of line + if (originalFile[i] == '/' || originalFile[i] == '#') + while (originalFile[i] != '\n') + i++; + + i++; + } + + int j = 0; + for (int j = 0; originalFile[i] >= 65 && originalFile[i] <= 122; j++) + rootName[j] = originalFile[i++]; + + // empty kv, all the other stuff gets #base'd + newKvs += rootName; + newKvs += "\n{\n}\n"; + + std::ofstream originalFileWriteStream(compiledDir / ogFilePath, std::ios::binary); + originalFileWriteStream << originalFile; + originalFileWriteStream.close(); + + std::ofstream writeStream(compiledPath, std::ios::binary); + writeStream << newKvs; + writeStream.close(); + + ModOverrideFile overrideFile; + overrideFile.m_pOwningMod = nullptr; + overrideFile.m_Path = normalisedPath; + + if (m_ModFiles.find(normalisedPath) == m_ModFiles.end()) + m_ModFiles.insert(std::make_pair(normalisedPath, overrideFile)); + else + m_ModFiles[normalisedPath] = overrideFile; +} diff --git a/NorthstarDLL/mods/compiled/modpdef.cpp b/NorthstarDLL/mods/compiled/modpdef.cpp new file mode 100644 index 00000000..219c744b --- /dev/null +++ b/NorthstarDLL/mods/compiled/modpdef.cpp @@ -0,0 +1,119 @@ +#include "pch.h" +#include "mods/modmanager.h" +#include "core/filesystem/filesystem.h" + +#include <map> +#include <sstream> +#include <fstream> + +const fs::path MOD_PDEF_SUFFIX = "cfg/server/persistent_player_data_version_231.pdef"; +const char* VPK_PDEF_PATH = "cfg/server/persistent_player_data_version_231.pdef"; + +void ModManager::BuildPdef() +{ + spdlog::info("Building persistent_player_data_version_231.pdef..."); + + fs::path MOD_PDEF_PATH = fs::path(GetCompiledAssetsPath() / MOD_PDEF_SUFFIX); + + fs::remove(MOD_PDEF_PATH); + std::string pdef = R2::ReadVPKOriginalFile(VPK_PDEF_PATH); + + for (Mod& mod : m_LoadedMods) + { + if (!mod.m_bEnabled || !mod.Pdiff.size()) + continue; + + // this code probably isn't going to be pretty lol + // refer to shared/pjson.js for an actual okish parser of the pdiff format + // but pretty much, $ENUM_ADD blocks define members added to preexisting enums + // $PROP_START ends the custom stuff, and from there it's just normal props we append to the pdef + + std::map<std::string, std::vector<std::string>> enumAdds; + + // read pdiff + bool inEnum = false; + bool inProp = false; + std::string currentEnum; + std::string currentLine; + std::istringstream pdiffStream(mod.Pdiff); + + while (std::getline(pdiffStream, currentLine)) + { + if (inProp) + { + // just append to pdef here + pdef += currentLine; + pdef += '\n'; + continue; + } + + // trim leading whitespace + size_t start = currentLine.find_first_not_of(" \n\r\t\f\v"); + size_t end = currentLine.find("//"); + if (end == std::string::npos) + end = currentLine.size() - 1; // last char + + if (!currentLine.size() || !currentLine.compare(start, 2, "//")) + continue; + + if (inEnum) + { + if (!currentLine.compare(start, 9, "$ENUM_END")) + inEnum = false; + else + enumAdds[currentEnum].push_back(currentLine); // only need to push_back current line, if there's syntax errors then game + // pdef parser will handle them + } + else if (!currentLine.compare(start, 9, "$ENUM_ADD")) + { + inEnum = true; + currentEnum = currentLine.substr(start + 10 /*$ENUM_ADD + 1*/, currentLine.size() - end - (start + 10)); + enumAdds.insert(std::make_pair(currentEnum, std::vector<std::string>())); + } + else if (!currentLine.compare(start, 11, "$PROP_START")) + { + inProp = true; + pdef += "\n// $PROP_START "; + pdef += mod.Name; + pdef += "\n"; + } + } + + // add new members to preexisting enums + // note: this code could 100% be messed up if people put //$ENUM_START comments and the like + // could make it protect against this, but honestly not worth atm + for (auto enumAdd : enumAdds) + { + std::string addStr; + for (std::string enumMember : enumAdd.second) + { + addStr += enumMember; + addStr += '\n'; + } + + // start of enum we're adding to + std::string startStr = "$ENUM_START "; + startStr += enumAdd.first; + + // insert enum values into enum + size_t insertIdx = pdef.find("$ENUM_END", pdef.find(startStr)); + pdef.reserve(addStr.size()); + pdef.insert(insertIdx, addStr); + } + } + + fs::create_directories(MOD_PDEF_PATH.parent_path()); + + std::ofstream writeStream(MOD_PDEF_PATH, std::ios::binary); + writeStream << pdef; + writeStream.close(); + + ModOverrideFile overrideFile; + overrideFile.m_pOwningMod = nullptr; + overrideFile.m_Path = VPK_PDEF_PATH; + + if (m_ModFiles.find(VPK_PDEF_PATH) == m_ModFiles.end()) + m_ModFiles.insert(std::make_pair(VPK_PDEF_PATH, overrideFile)); + else + m_ModFiles[VPK_PDEF_PATH] = overrideFile; +} diff --git a/NorthstarDLL/mods/compiled/modscriptsrson.cpp b/NorthstarDLL/mods/compiled/modscriptsrson.cpp new file mode 100644 index 00000000..15fcdd13 --- /dev/null +++ b/NorthstarDLL/mods/compiled/modscriptsrson.cpp @@ -0,0 +1,66 @@ +#include "pch.h" +#include "mods/modmanager.h" +#include "core/filesystem/filesystem.h" +#include "squirrel/squirrel.h" + +#include <fstream> + +const std::string MOD_SCRIPTS_RSON_SUFFIX = "scripts/vscripts/scripts.rson"; +const char* VPK_SCRIPTS_RSON_PATH = "scripts\\vscripts\\scripts.rson"; + +void ModManager::BuildScriptsRson() +{ + spdlog::info("Building custom scripts.rson"); + fs::path MOD_SCRIPTS_RSON_PATH = fs::path(GetCompiledAssetsPath() / MOD_SCRIPTS_RSON_SUFFIX); + fs::remove(MOD_SCRIPTS_RSON_PATH); + + std::string scriptsRson = R2::ReadVPKOriginalFile(VPK_SCRIPTS_RSON_PATH); + scriptsRson += "\n\n// START MODDED SCRIPT CONTENT\n\n"; // newline before we start custom stuff + + for (Mod& mod : m_LoadedMods) + { + if (!mod.m_bEnabled) + continue; + + // this isn't needed at all, just nice to have imo + scriptsRson += "// MOD: "; + scriptsRson += mod.Name; + scriptsRson += ":\n\n"; + + for (ModScript& script : mod.Scripts) + { + /* should create something with this format for each script + When: "CONTEXT" + Scripts: + [ + _coolscript.gnut + ]*/ + + scriptsRson += "When: \""; + scriptsRson += script.RunOn; + scriptsRson += "\"\n"; + + scriptsRson += "Scripts:\n[\n\t"; + scriptsRson += script.Path; + scriptsRson += "\n]\n\n"; + } + } + + fs::create_directories(MOD_SCRIPTS_RSON_PATH.parent_path()); + + std::ofstream writeStream(MOD_SCRIPTS_RSON_PATH, std::ios::binary); + writeStream << scriptsRson; + writeStream.close(); + + ModOverrideFile overrideFile; + overrideFile.m_pOwningMod = nullptr; + overrideFile.m_Path = VPK_SCRIPTS_RSON_PATH; + + if (m_ModFiles.find(VPK_SCRIPTS_RSON_PATH) == m_ModFiles.end()) + m_ModFiles.insert(std::make_pair(VPK_SCRIPTS_RSON_PATH, overrideFile)); + else + m_ModFiles[VPK_SCRIPTS_RSON_PATH] = overrideFile; + + // todo: for preventing dupe scripts in scripts.rson, we could actually parse when conditions with the squirrel vm, just need a way to + // get a result out of squirrelmanager.ExecuteCode this would probably be the best way to do this, imo +} diff --git a/NorthstarDLL/mods/modmanager.cpp b/NorthstarDLL/mods/modmanager.cpp new file mode 100644 index 00000000..2196b118 --- /dev/null +++ b/NorthstarDLL/mods/modmanager.cpp @@ -0,0 +1,725 @@ +#include "pch.h" +#include "modmanager.h" +#include "core/convar/convar.h" +#include "core/convar/concommand.h" +#include "client/audio.h" +#include "masterserver/masterserver.h" +#include "core/filesystem/filesystem.h" +#include "core/filesystem/rpakfilesystem.h" +#include "config/profile.h" + +#include "rapidjson/error/en.h" +#include "rapidjson/document.h" +#include "rapidjson/ostreamwrapper.h" +#include "rapidjson/writer.h" +#include <filesystem> +#include <fstream> +#include <string> +#include <sstream> +#include <vector> + +ModManager* g_pModManager; + +Mod::Mod(fs::path modDir, char* jsonBuf) +{ + m_bWasReadSuccessfully = false; + + m_ModDirectory = modDir; + + rapidjson_document modJson; + modJson.Parse<rapidjson::ParseFlag::kParseCommentsFlag | rapidjson::ParseFlag::kParseTrailingCommasFlag>(jsonBuf); + + // fail if parse error + if (modJson.HasParseError()) + { + spdlog::error( + "Failed reading mod file {}: encountered parse error \"{}\" at offset {}", + (modDir / "mod.json").string(), + GetParseError_En(modJson.GetParseError()), + modJson.GetErrorOffset()); + return; + } + + // fail if it's not a json obj (could be an array, string, etc) + if (!modJson.IsObject()) + { + spdlog::error("Failed reading mod file {}: file is not a JSON object", (modDir / "mod.json").string()); + return; + } + + // basic mod info + // name is required + if (!modJson.HasMember("Name")) + { + spdlog::error("Failed reading mod file {}: missing required member \"Name\"", (modDir / "mod.json").string()); + return; + } + + Name = modJson["Name"].GetString(); + + if (modJson.HasMember("Description")) + Description = modJson["Description"].GetString(); + else + Description = ""; + + if (modJson.HasMember("Version")) + Version = modJson["Version"].GetString(); + else + { + Version = "0.0.0"; + spdlog::warn("Mod file {} is missing a version, consider adding a version", (modDir / "mod.json").string()); + } + + if (modJson.HasMember("DownloadLink")) + DownloadLink = modJson["DownloadLink"].GetString(); + else + DownloadLink = ""; + + if (modJson.HasMember("RequiredOnClient")) + RequiredOnClient = modJson["RequiredOnClient"].GetBool(); + else + RequiredOnClient = false; + + if (modJson.HasMember("LoadPriority")) + LoadPriority = modJson["LoadPriority"].GetInt(); + else + { + spdlog::info("Mod file {} is missing a LoadPriority, consider adding one", (modDir / "mod.json").string()); + LoadPriority = 0; + } + + // mod convars + if (modJson.HasMember("ConVars") && modJson["ConVars"].IsArray()) + { + for (auto& convarObj : modJson["ConVars"].GetArray()) + { + if (!convarObj.IsObject() || !convarObj.HasMember("Name") || !convarObj.HasMember("DefaultValue")) + continue; + + // have to allocate this manually, otherwise convar registration will break + // unfortunately this causes us to leak memory on reload, unsure of a way around this rn + ModConVar* convar = new ModConVar; + convar->Name = convarObj["Name"].GetString(); + convar->DefaultValue = convarObj["DefaultValue"].GetString(); + + if (convarObj.HasMember("HelpString")) + convar->HelpString = convarObj["HelpString"].GetString(); + else + convar->HelpString = ""; + + convar->Flags = FCVAR_NONE; + + if (convarObj.HasMember("Flags")) + { + // read raw integer flags + if (convarObj["Flags"].IsInt()) + convar->Flags = convarObj["Flags"].GetInt(); + else if (convarObj["Flags"].IsString()) + { + // parse cvar flags from string + // example string: ARCHIVE_PLAYERPROFILE | GAMEDLL + + std::string sFlags = convarObj["Flags"].GetString(); + sFlags += '|'; // add additional | so we register the last flag + std::string sCurrentFlag; + + for (int i = 0; i < sFlags.length(); i++) + { + if (isspace(sFlags[i])) + continue; + + // if we encounter a |, add current string as a flag + if (sFlags[i] == '|') + { + bool bHasFlags = false; + int iCurrentFlags; + + for (auto& flagPair : g_PrintCommandFlags) + { + if (!sCurrentFlag.compare(flagPair.second)) + { + iCurrentFlags = flagPair.first; + bHasFlags = true; + break; + } + } + + if (bHasFlags) + convar->Flags |= iCurrentFlags; + else + spdlog::warn("Mod ConVar {} has unknown flag {}", convar->Name, sCurrentFlag); + + sCurrentFlag = ""; + } + else + sCurrentFlag += sFlags[i]; + } + } + } + + ConVars.push_back(convar); + } + } + + // mod scripts + if (modJson.HasMember("Scripts") && modJson["Scripts"].IsArray()) + { + for (auto& scriptObj : modJson["Scripts"].GetArray()) + { + if (!scriptObj.IsObject() || !scriptObj.HasMember("Path") || !scriptObj.HasMember("RunOn")) + continue; + + ModScript script; + + script.Path = scriptObj["Path"].GetString(); + script.RunOn = scriptObj["RunOn"].GetString(); + + if (scriptObj.HasMember("ServerCallback") && scriptObj["ServerCallback"].IsObject()) + { + ModScriptCallback callback; + callback.Context = ScriptContext::SERVER; + + if (scriptObj["ServerCallback"].HasMember("Before") && scriptObj["ServerCallback"]["Before"].IsString()) + callback.BeforeCallback = scriptObj["ServerCallback"]["Before"].GetString(); + + if (scriptObj["ServerCallback"].HasMember("After") && scriptObj["ServerCallback"]["After"].IsString()) + callback.AfterCallback = scriptObj["ServerCallback"]["After"].GetString(); + + script.Callbacks.push_back(callback); + } + + if (scriptObj.HasMember("ClientCallback") && scriptObj["ClientCallback"].IsObject()) + { + ModScriptCallback callback; + callback.Context = ScriptContext::CLIENT; + + if (scriptObj["ClientCallback"].HasMember("Before") && scriptObj["ClientCallback"]["Before"].IsString()) + callback.BeforeCallback = scriptObj["ClientCallback"]["Before"].GetString(); + + if (scriptObj["ClientCallback"].HasMember("After") && scriptObj["ClientCallback"]["After"].IsString()) + callback.AfterCallback = scriptObj["ClientCallback"]["After"].GetString(); + + script.Callbacks.push_back(callback); + } + + if (scriptObj.HasMember("UICallback") && scriptObj["UICallback"].IsObject()) + { + ModScriptCallback callback; + callback.Context = ScriptContext::UI; + + if (scriptObj["UICallback"].HasMember("Before") && scriptObj["UICallback"]["Before"].IsString()) + callback.BeforeCallback = scriptObj["UICallback"]["Before"].GetString(); + + if (scriptObj["UICallback"].HasMember("After") && scriptObj["UICallback"]["After"].IsString()) + callback.AfterCallback = scriptObj["UICallback"]["After"].GetString(); + + script.Callbacks.push_back(callback); + } + + Scripts.push_back(script); + } + } + + if (modJson.HasMember("Localisation") && modJson["Localisation"].IsArray()) + { + for (auto& localisationStr : modJson["Localisation"].GetArray()) + { + if (!localisationStr.IsString()) + continue; + + LocalisationFiles.push_back(localisationStr.GetString()); + } + } + + if (modJson.HasMember("Dependencies") && modJson["Dependencies"].IsObject()) + { + for (auto v = modJson["Dependencies"].MemberBegin(); v != modJson["Dependencies"].MemberEnd(); v++) + { + if (!v->name.IsString() || !v->value.IsString()) + continue; + + spdlog::info("Constant {} defined by {} for mod {}", v->name.GetString(), Name, v->value.GetString()); + if (DependencyConstants.find(v->name.GetString()) != DependencyConstants.end() && + v->value.GetString() != DependencyConstants[v->name.GetString()]) + { + spdlog::error("A dependency constant with the same name already exists for another mod. Change the constant name."); + return; + } + + if (DependencyConstants.find(v->name.GetString()) == DependencyConstants.end()) + DependencyConstants.emplace(v->name.GetString(), v->value.GetString()); + } + } + + m_bWasReadSuccessfully = true; +} + +ModManager::ModManager() +{ + // precaculated string hashes + // note: use backslashes for these, since we use lexically_normal for file paths which uses them + m_hScriptsRsonHash = STR_HASH("scripts\\vscripts\\scripts.rson"); + m_hPdefHash = STR_HASH( + "cfg\\server\\persistent_player_data_version_231.pdef" // this can have multiple versions, but we use 231 so that's what we hash + ); + m_hKBActHash = STR_HASH("scripts\\kb_act.lst"); + + LoadMods(); +} + +void ModManager::LoadMods() +{ + if (m_bHasLoadedMods) + UnloadMods(); + + std::vector<fs::path> modDirs; + + // ensure dirs exist + fs::remove_all(GetCompiledAssetsPath()); + fs::create_directories(GetModFolderPath()); + + m_DependencyConstants.clear(); + + // read enabled mods cfg + std::ifstream enabledModsStream(GetNorthstarPrefix() + "/enabledmods.json"); + std::stringstream enabledModsStringStream; + + if (!enabledModsStream.fail()) + { + while (enabledModsStream.peek() != EOF) + enabledModsStringStream << (char)enabledModsStream.get(); + + enabledModsStream.close(); + m_EnabledModsCfg.Parse<rapidjson::ParseFlag::kParseCommentsFlag | rapidjson::ParseFlag::kParseTrailingCommasFlag>( + enabledModsStringStream.str().c_str()); + + m_bHasEnabledModsCfg = m_EnabledModsCfg.IsObject(); + } + + // get mod directories + for (fs::directory_entry dir : fs::directory_iterator(GetModFolderPath())) + if (fs::exists(dir.path() / "mod.json")) + modDirs.push_back(dir.path()); + + for (fs::path modDir : modDirs) + { + // read mod json file + std::ifstream jsonStream(modDir / "mod.json"); + std::stringstream jsonStringStream; + + // fail if no mod json + if (jsonStream.fail()) + { + spdlog::warn("Mod {} has a directory but no mod.json", modDir.string()); + continue; + } + + while (jsonStream.peek() != EOF) + jsonStringStream << (char)jsonStream.get(); + + jsonStream.close(); + + Mod mod(modDir, (char*)jsonStringStream.str().c_str()); + + for (auto& pair : mod.DependencyConstants) + { + if (m_DependencyConstants.find(pair.first) != m_DependencyConstants.end() && m_DependencyConstants[pair.first] != pair.second) + { + spdlog::error("Constant {} in mod {} already exists in another mod.", pair.first, mod.Name); + mod.m_bWasReadSuccessfully = false; + break; + } + if (m_DependencyConstants.find(pair.first) == m_DependencyConstants.end()) + m_DependencyConstants.emplace(pair); + } + + if (m_bHasEnabledModsCfg && m_EnabledModsCfg.HasMember(mod.Name.c_str())) + mod.m_bEnabled = m_EnabledModsCfg[mod.Name.c_str()].IsTrue(); + else + mod.m_bEnabled = true; + + if (mod.m_bWasReadSuccessfully) + { + spdlog::info("Loaded mod {} successfully", mod.Name); + if (mod.m_bEnabled) + spdlog::info("Mod {} is enabled", mod.Name); + else + spdlog::info("Mod {} is disabled", mod.Name); + + m_LoadedMods.push_back(mod); + } + else + spdlog::warn("Skipping loading mod file {}", (modDir / "mod.json").string()); + } + + // sort by load prio, lowest-highest + std::sort(m_LoadedMods.begin(), m_LoadedMods.end(), [](Mod& a, Mod& b) { return a.LoadPriority < b.LoadPriority; }); + + for (Mod& mod : m_LoadedMods) + { + if (!mod.m_bEnabled) + continue; + + // register convars + // for reloads, this is sorta barebones, when we have a good findconvar method, we could probably reset flags and stuff on + // preexisting convars note: we don't delete convars if they already exist because they're used for script stuff, unfortunately this + // causes us to leak memory on reload, but not much, potentially find a way to not do this at some point + for (ModConVar* convar : mod.ConVars) + if (!R2::g_pCVar->FindVar(convar->Name.c_str())) // make sure convar isn't registered yet, unsure if necessary but idk what + // behaviour is for defining same convar multiple times + new ConVar(convar->Name.c_str(), convar->DefaultValue.c_str(), convar->Flags, convar->HelpString.c_str()); + + // read vpk paths + if (fs::exists(mod.m_ModDirectory / "vpk")) + { + // read vpk cfg + std::ifstream vpkJsonStream(mod.m_ModDirectory / "vpk/vpk.json"); + std::stringstream vpkJsonStringStream; + + bool bUseVPKJson = false; + rapidjson::Document dVpkJson; + + if (!vpkJsonStream.fail()) + { + while (vpkJsonStream.peek() != EOF) + vpkJsonStringStream << (char)vpkJsonStream.get(); + + vpkJsonStream.close(); + dVpkJson.Parse<rapidjson::ParseFlag::kParseCommentsFlag | rapidjson::ParseFlag::kParseTrailingCommasFlag>( + vpkJsonStringStream.str().c_str()); + + bUseVPKJson = !dVpkJson.HasParseError() && dVpkJson.IsObject(); + } + + for (fs::directory_entry file : fs::directory_iterator(mod.m_ModDirectory / "vpk")) + { + // a bunch of checks to make sure we're only adding dir vpks and their paths are good + // note: the game will literally only load vpks with the english prefix + if (fs::is_regular_file(file) && file.path().extension() == ".vpk" && + file.path().string().find("english") != std::string::npos && + file.path().string().find(".bsp.pak000_dir") != std::string::npos) + { + std::string formattedPath = file.path().filename().string(); + + // this really fucking sucks but it'll work + std::string vpkName = formattedPath.substr(strlen("english"), formattedPath.find(".bsp") - 3); + + ModVPKEntry& modVpk = mod.Vpks.emplace_back(); + modVpk.m_bAutoLoad = !bUseVPKJson || (dVpkJson.HasMember("Preload") && dVpkJson["Preload"].IsObject() && + dVpkJson["Preload"].HasMember(vpkName) && dVpkJson["Preload"][vpkName].IsTrue()); + modVpk.m_sVpkPath = (file.path().parent_path() / vpkName).string(); + + if (m_bHasLoadedMods && modVpk.m_bAutoLoad) + (*R2::g_pFilesystem)->m_vtable->MountVPK(*R2::g_pFilesystem, vpkName.c_str()); + } + } + } + + // read rpak paths + if (fs::exists(mod.m_ModDirectory / "paks")) + { + // read rpak cfg + std::ifstream rpakJsonStream(mod.m_ModDirectory / "paks/rpak.json"); + std::stringstream rpakJsonStringStream; + + bool bUseRpakJson = false; + rapidjson::Document dRpakJson; + + if (!rpakJsonStream.fail()) + { + while (rpakJsonStream.peek() != EOF) + rpakJsonStringStream << (char)rpakJsonStream.get(); + + rpakJsonStream.close(); + dRpakJson.Parse<rapidjson::ParseFlag::kParseCommentsFlag | rapidjson::ParseFlag::kParseTrailingCommasFlag>( + rpakJsonStringStream.str().c_str()); + + bUseRpakJson = !dRpakJson.HasParseError() && dRpakJson.IsObject(); + } + + // read pak aliases + if (bUseRpakJson && dRpakJson.HasMember("Aliases") && dRpakJson["Aliases"].IsObject()) + { + for (rapidjson::Value::ConstMemberIterator iterator = dRpakJson["Aliases"].MemberBegin(); + iterator != dRpakJson["Aliases"].MemberEnd(); + iterator++) + { + if (!iterator->name.IsString() || !iterator->value.IsString()) + continue; + + mod.RpakAliases.insert(std::make_pair(iterator->name.GetString(), iterator->value.GetString())); + } + } + + for (fs::directory_entry file : fs::directory_iterator(mod.m_ModDirectory / "paks")) + { + // ensure we're only loading rpaks + if (fs::is_regular_file(file) && file.path().extension() == ".rpak") + { + std::string pakName(file.path().filename().string()); + + ModRpakEntry& modPak = mod.Rpaks.emplace_back(); + modPak.m_bAutoLoad = + !bUseRpakJson || (dRpakJson.HasMember("Preload") && dRpakJson["Preload"].IsObject() && + dRpakJson["Preload"].HasMember(pakName) && dRpakJson["Preload"][pakName].IsTrue()); + + // postload things + if (!bUseRpakJson || + (dRpakJson.HasMember("Postload") && dRpakJson["Postload"].IsObject() && dRpakJson["Postload"].HasMember(pakName))) + modPak.m_sLoadAfterPak = dRpakJson["Postload"][pakName].GetString(); + + modPak.m_sPakName = pakName; + + // read header of file and get the starpak paths + // this is done here as opposed to on starpak load because multiple rpaks can load a starpak + // and there is seemingly no good way to tell which rpak is causing the load of a starpak :/ + + std::ifstream rpakStream(file.path(), std::ios::binary); + + // seek to the point in the header where the starpak reference size is + rpakStream.seekg(0x38, std::ios::beg); + int starpaksSize = 0; + rpakStream.read((char*)&starpaksSize, 2); + + // seek to just after the header + rpakStream.seekg(0x58, std::ios::beg); + // read the starpak reference(s) + std::vector<char> buf(starpaksSize); + rpakStream.read(buf.data(), starpaksSize); + + rpakStream.close(); + + // split the starpak reference(s) into strings to hash + std::string str = ""; + for (int i = 0; i < starpaksSize; i++) + { + // if the current char is null, that signals the end of the current starpak path + if (buf[i] != 0x00) + { + str += buf[i]; + } + else + { + // only add the string we are making if it isnt empty + if (!str.empty()) + { + mod.StarpakPaths.push_back(STR_HASH(str)); + spdlog::info("Mod {} registered starpak '{}'", mod.Name, str); + str = ""; + } + } + } + + // not using atm because we need to resolve path to rpak + // if (m_hasLoadedMods && modPak.m_bAutoLoad) + // g_pPakLoadManager->LoadPakAsync(pakName.c_str()); + } + } + } + + // read keyvalues paths + if (fs::exists(mod.m_ModDirectory / "keyvalues")) + { + for (fs::directory_entry file : fs::recursive_directory_iterator(mod.m_ModDirectory / "keyvalues")) + { + if (fs::is_regular_file(file)) + { + std::string kvStr = + g_pModManager->NormaliseModFilePath(file.path().lexically_relative(mod.m_ModDirectory / "keyvalues")); + mod.KeyValues.emplace(STR_HASH(kvStr), kvStr); + } + } + } + + // read pdiff + if (fs::exists(mod.m_ModDirectory / "mod.pdiff")) + { + std::ifstream pdiffStream(mod.m_ModDirectory / "mod.pdiff"); + + if (!pdiffStream.fail()) + { + std::stringstream pdiffStringStream; + while (pdiffStream.peek() != EOF) + pdiffStringStream << (char)pdiffStream.get(); + + pdiffStream.close(); + + mod.Pdiff = pdiffStringStream.str(); + } + } + + // read bink video paths + if (fs::exists(mod.m_ModDirectory / "media")) + { + for (fs::directory_entry file : fs::recursive_directory_iterator(mod.m_ModDirectory / "media")) + if (fs::is_regular_file(file) && file.path().extension() == ".bik") + mod.BinkVideos.push_back(file.path().filename().string()); + } + + // try to load audio + if (fs::exists(mod.m_ModDirectory / "audio")) + { + for (fs::directory_entry file : fs::directory_iterator(mod.m_ModDirectory / "audio")) + { + if (fs::is_regular_file(file) && file.path().extension().string() == ".json") + { + if (!g_CustomAudioManager.TryLoadAudioOverride(file.path())) + { + spdlog::warn("Mod {} has an invalid audio def {}", mod.Name, file.path().filename().string()); + continue; + } + } + } + } + } + + // in a seperate loop because we register mod files in reverse order, since mods loaded later should have their files prioritised + for (int64_t i = m_LoadedMods.size() - 1; i > -1; i--) + { + if (!m_LoadedMods[i].m_bEnabled) + continue; + + if (fs::exists(m_LoadedMods[i].m_ModDirectory / MOD_OVERRIDE_DIR)) + { + for (fs::directory_entry file : fs::recursive_directory_iterator(m_LoadedMods[i].m_ModDirectory / MOD_OVERRIDE_DIR)) + { + std::string path = + g_pModManager->NormaliseModFilePath(file.path().lexically_relative(m_LoadedMods[i].m_ModDirectory / MOD_OVERRIDE_DIR)); + if (file.is_regular_file() && m_ModFiles.find(path) == m_ModFiles.end()) + { + ModOverrideFile modFile; + modFile.m_pOwningMod = &m_LoadedMods[i]; + modFile.m_Path = path; + m_ModFiles.insert(std::make_pair(path, modFile)); + } + } + } + } + + // build modinfo obj for masterserver + rapidjson_document modinfoDoc; + auto& alloc = modinfoDoc.GetAllocator(); + modinfoDoc.SetObject(); + modinfoDoc.AddMember("Mods", rapidjson::kArrayType, alloc); + + int currentModIndex = 0; + for (Mod& mod : m_LoadedMods) + { + if (!mod.m_bEnabled || (!mod.RequiredOnClient && !mod.Pdiff.size())) + continue; + + modinfoDoc["Mods"].PushBack(rapidjson::kObjectType, modinfoDoc.GetAllocator()); + modinfoDoc["Mods"][currentModIndex].AddMember("Name", rapidjson::StringRef(&mod.Name[0]), modinfoDoc.GetAllocator()); + modinfoDoc["Mods"][currentModIndex].AddMember("Version", rapidjson::StringRef(&mod.Version[0]), modinfoDoc.GetAllocator()); + modinfoDoc["Mods"][currentModIndex].AddMember("RequiredOnClient", mod.RequiredOnClient, modinfoDoc.GetAllocator()); + modinfoDoc["Mods"][currentModIndex].AddMember("Pdiff", rapidjson::StringRef(&mod.Pdiff[0]), modinfoDoc.GetAllocator()); + + currentModIndex++; + } + + rapidjson::StringBuffer buffer; + buffer.Clear(); + rapidjson::Writer<rapidjson::StringBuffer> writer(buffer); + modinfoDoc.Accept(writer); + g_pMasterServerManager->m_sOwnModInfoJson = std::string(buffer.GetString()); + + m_bHasLoadedMods = true; +} + +void ModManager::UnloadMods() +{ + // clean up stuff from mods before we unload + m_ModFiles.clear(); + fs::remove_all(GetCompiledAssetsPath()); + + g_CustomAudioManager.ClearAudioOverrides(); + + if (!m_bHasEnabledModsCfg) + m_EnabledModsCfg.SetObject(); + + for (Mod& mod : m_LoadedMods) + { + // remove all built kvs + for (std::pair<size_t, std::string> kvPaths : mod.KeyValues) + fs::remove(GetCompiledAssetsPath() / fs::path(kvPaths.second).lexically_relative(mod.m_ModDirectory)); + + mod.KeyValues.clear(); + + // write to m_enabledModsCfg + // should we be doing this here or should scripts be doing this manually? + // main issue with doing this here is when we reload mods for connecting to a server, we write enabled mods, which isn't necessarily + // what we wanna do + if (!m_EnabledModsCfg.HasMember(mod.Name.c_str())) + m_EnabledModsCfg.AddMember(rapidjson_document::StringRefType(mod.Name.c_str()), false, m_EnabledModsCfg.GetAllocator()); + + m_EnabledModsCfg[mod.Name.c_str()].SetBool(mod.m_bEnabled); + } + + std::ofstream writeStream(GetNorthstarPrefix() + "/enabledmods.json"); + rapidjson::OStreamWrapper writeStreamWrapper(writeStream); + rapidjson::Writer<rapidjson::OStreamWrapper> writer(writeStreamWrapper); + m_EnabledModsCfg.Accept(writer); + + // do we need to dealloc individual entries in m_loadedMods? idk, rework + m_LoadedMods.clear(); +} + +std::string ModManager::NormaliseModFilePath(const fs::path path) +{ + std::string str = path.lexically_normal().string(); + + // force to lowercase + for (char& c : str) + if (c <= 'Z' && c >= 'A') + c = c - ('Z' - 'z'); + + return str; +} + +void ModManager::CompileAssetsForFile(const char* filename) +{ + size_t fileHash = STR_HASH(NormaliseModFilePath(fs::path(filename))); + + if (fileHash == m_hScriptsRsonHash) + BuildScriptsRson(); + else if (fileHash == m_hPdefHash) + BuildPdef(); + else if (fileHash == m_hKBActHash) + BuildKBActionsList(); + else + { + // check if we should build keyvalues, depending on whether any of our mods have patch kvs for this file + for (Mod& mod : m_LoadedMods) + { + if (!mod.m_bEnabled) + continue; + + if (mod.KeyValues.find(fileHash) != mod.KeyValues.end()) + { + TryBuildKeyValues(filename); + return; + } + } + } +} + +void ConCommand_reload_mods(const CCommand& args) +{ + g_pModManager->LoadMods(); +} + +fs::path GetModFolderPath() +{ + return fs::path(GetNorthstarPrefix() + MOD_FOLDER_SUFFIX); +} +fs::path GetCompiledAssetsPath() +{ + return fs::path(GetNorthstarPrefix() + COMPILED_ASSETS_SUFFIX); +} + +ON_DLL_LOAD_RELIESON("engine.dll", ModManager, (ConCommand, MasterServer), (CModule module)) +{ + g_pModManager = new ModManager; + + RegisterConCommand("reload_mods", ConCommand_reload_mods, "reloads mods", FCVAR_NONE); +} diff --git a/NorthstarDLL/mods/modmanager.h b/NorthstarDLL/mods/modmanager.h new file mode 100644 index 00000000..a77d85bd --- /dev/null +++ b/NorthstarDLL/mods/modmanager.h @@ -0,0 +1,154 @@ +#pragma once +#include "core/convar/convar.h" +#include "core/memalloc.h" +#include "squirrel/squirrel.h" + +#include "rapidjson/document.h" +#include <string> +#include <vector> +#include <filesystem> + +const std::string MOD_FOLDER_SUFFIX = "/mods"; +const std::string REMOTE_MOD_FOLDER_SUFFIX = "/runtime/remote/mods"; +const fs::path MOD_OVERRIDE_DIR = "mod"; +const std::string COMPILED_ASSETS_SUFFIX = "/runtime/compiled"; + +struct ModConVar +{ + public: + std::string Name; + std::string DefaultValue; + std::string HelpString; + int Flags; +}; + +struct ModScriptCallback +{ + public: + ScriptContext Context; + + // called before the codecallback is executed + std::string BeforeCallback; + // called after the codecallback has finished executing + std::string AfterCallback; +}; + +struct ModScript +{ + public: + std::string Path; + std::string RunOn; + + std::vector<ModScriptCallback> Callbacks; +}; + +// these are pretty much identical, could refactor to use the same stuff? +struct ModVPKEntry +{ + public: + bool m_bAutoLoad; + std::string m_sVpkPath; +}; + +struct ModRpakEntry +{ + public: + bool m_bAutoLoad; + std::string m_sPakName; + std::string m_sLoadAfterPak; +}; + +class Mod +{ + public: + // runtime stuff + bool m_bEnabled = true; + bool m_bWasReadSuccessfully = false; + fs::path m_ModDirectory; + // bool m_bIsRemote; + + // mod.json stuff: + + // the mod's name + std::string Name; + // the mod's description + std::string Description; + // the mod's version, should be in semver + std::string Version; + // a download link to the mod, for clients that try to join without the mod + std::string DownloadLink; + + // whether clients need the mod to join servers running this mod + bool RequiredOnClient; + // the priority for this mod's files, mods with prio 0 are loaded first, then 1, then 2, etc + int LoadPriority; + + // custom scripts used by the mod + std::vector<ModScript> Scripts; + // convars created by the mod + std::vector<ModConVar*> ConVars; + // custom localisation files created by the mod + std::vector<std::string> LocalisationFiles; + + // other files: + + std::vector<ModVPKEntry> Vpks; + std::unordered_map<size_t, std::string> KeyValues; + std::vector<std::string> BinkVideos; + std::string Pdiff; // only need one per mod + + std::vector<ModRpakEntry> Rpaks; + std::unordered_map<std::string, std::string> + RpakAliases; // paks we alias to other rpaks, e.g. to load sp_crashsite paks on the map mp_crashsite + std::vector<size_t> StarpakPaths; // starpaks that this mod contains + // there seems to be no nice way to get the rpak that is causing the load of a starpak? + // hashed with STR_HASH + + std::unordered_map<std::string, std::string> DependencyConstants; + + public: + Mod(fs::path modPath, char* jsonBuf); +}; + +struct ModOverrideFile +{ + public: + Mod* m_pOwningMod; + fs::path m_Path; +}; + +class ModManager +{ + private: + bool m_bHasLoadedMods = false; + bool m_bHasEnabledModsCfg; + rapidjson_document m_EnabledModsCfg; + + // precalculated hashes + size_t m_hScriptsRsonHash; + size_t m_hPdefHash; + size_t m_hKBActHash; + + public: + std::vector<Mod> m_LoadedMods; + std::unordered_map<std::string, ModOverrideFile> m_ModFiles; + std::unordered_map<std::string, std::string> m_DependencyConstants; + + public: + ModManager(); + void LoadMods(); + void UnloadMods(); + std::string NormaliseModFilePath(const fs::path path); + void CompileAssetsForFile(const char* filename); + + // compile asset type stuff, these are done in files under runtime/compiled/ + void BuildScriptsRson(); + void TryBuildKeyValues(const char* filename); + void BuildPdef(); + void BuildKBActionsList(); +}; + +fs::path GetModFolderPath(); +fs::path GetCompiledAssetsPath(); + +extern ModManager* g_pModManager; |