diff options
Diffstat (limited to 'NorthstarDLL/mods/modmanager.cpp')
-rw-r--r-- | NorthstarDLL/mods/modmanager.cpp | 1150 |
1 files changed, 0 insertions, 1150 deletions
diff --git a/NorthstarDLL/mods/modmanager.cpp b/NorthstarDLL/mods/modmanager.cpp deleted file mode 100644 index 8a0eb71d..00000000 --- a/NorthstarDLL/mods/modmanager.cpp +++ /dev/null @@ -1,1150 +0,0 @@ -#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/prettywriter.h" -#include <filesystem> -#include <fstream> -#include <string> -#include <sstream> -#include <vector> -#include <regex> - -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); - - spdlog::info("Loading mod file at path '{}'", modDir.string()); - - // 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(); - spdlog::info("Loading mod '{}'", Name); - - // Don't load blacklisted mods - if (!strstr(GetCommandLineA(), "-nomodblacklist") && MODS_BLACKLIST.find(Name) != std::end(MODS_BLACKLIST)) - { - spdlog::warn("Skipping blacklisted mod \"{}\"!", Name); - return; - } - - 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; - } - - // Parse all array fields - ParseConVars(modJson); - ParseConCommands(modJson); - ParseScripts(modJson); - ParseLocalization(modJson); - ParseDependencies(modJson); - ParsePluginDependencies(modJson); - ParseInitScript(modJson); - - // A mod is remote if it's located in the remote mods folder - m_bIsRemote = m_ModDirectory.generic_string().find(GetRemoteModFolderPath().generic_string()) != std::string::npos; - - m_bWasReadSuccessfully = true; -} - -void Mod::ParseConVars(rapidjson_document& json) -{ - if (!json.HasMember("ConVars")) - return; - - if (!json["ConVars"].IsArray()) - { - spdlog::warn("'ConVars' field is not an array, skipping..."); - return; - } - - for (auto& convarObj : json["ConVars"].GetArray()) - { - if (!convarObj.IsObject()) - { - spdlog::warn("ConVar is not an object, skipping..."); - continue; - } - if (!convarObj.HasMember("Name")) - { - spdlog::warn("ConVar does not have a Name, skipping..."); - continue; - } - // from here on, the ConVar can be referenced by name in logs - if (!convarObj.HasMember("DefaultValue")) - { - spdlog::warn("ConVar '{}' does not have a DefaultValue, skipping...", convarObj["Name"].GetString()); - 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 - convar->Flags |= ParseConVarFlagsString(convar->Name, convarObj["Flags"].GetString()); - } - } - - ConVars.push_back(convar); - - spdlog::info("'{}' contains ConVar '{}'", Name, convar->Name); - } -} - -void Mod::ParseConCommands(rapidjson_document& json) -{ - if (!json.HasMember("ConCommands")) - return; - - if (!json["ConCommands"].IsArray()) - { - spdlog::warn("'ConCommands' field is not an array, skipping..."); - return; - } - - for (auto& concommandObj : json["ConCommands"].GetArray()) - { - if (!concommandObj.IsObject()) - { - spdlog::warn("ConCommand is not an object, skipping..."); - continue; - } - if (!concommandObj.HasMember("Name")) - { - spdlog::warn("ConCommand does not have a Name, skipping..."); - continue; - } - // from here on, the ConCommand can be referenced by name in logs - if (!concommandObj.HasMember("Function")) - { - spdlog::warn("ConCommand '{}' does not have a Function, skipping...", concommandObj["Name"].GetString()); - continue; - } - if (!concommandObj.HasMember("Context")) - { - spdlog::warn("ConCommand '{}' does not have a Context, skipping...", concommandObj["Name"].GetString()); - continue; - } - - // have to allocate this manually, otherwise concommand registration will break - // unfortunately this causes us to leak memory on reload, unsure of a way around this rn - ModConCommand* concommand = new ModConCommand; - concommand->Name = concommandObj["Name"].GetString(); - concommand->Function = concommandObj["Function"].GetString(); - concommand->Context = ScriptContextFromString(concommandObj["Context"].GetString()); - if (concommand->Context == ScriptContext::INVALID) - { - spdlog::warn("ConCommand '{}' has invalid context '{}', skipping...", concommand->Name, concommandObj["Context"].GetString()); - continue; - } - - if (concommandObj.HasMember("HelpString")) - concommand->HelpString = concommandObj["HelpString"].GetString(); - else - concommand->HelpString = ""; - - concommand->Flags = FCVAR_NONE; - - if (concommandObj.HasMember("Flags")) - { - // read raw integer flags - if (concommandObj["Flags"].IsInt()) - { - concommand->Flags = concommandObj["Flags"].GetInt(); - } - else if (concommandObj["Flags"].IsString()) - { - // parse cvar flags from string - // example string: ARCHIVE_PLAYERPROFILE | GAMEDLL - concommand->Flags |= ParseConVarFlagsString(concommand->Name, concommandObj["Flags"].GetString()); - } - } - - ConCommands.push_back(concommand); - - spdlog::info("'{}' contains ConCommand '{}'", Name, concommand->Name); - } -} - -void Mod::ParseScripts(rapidjson_document& json) -{ - if (!json.HasMember("Scripts")) - return; - - if (!json["Scripts"].IsArray()) - { - spdlog::warn("'Scripts' field is not an array, skipping..."); - return; - } - - for (auto& scriptObj : json["Scripts"].GetArray()) - { - if (!scriptObj.IsObject()) - { - spdlog::warn("Script is not an object, skipping..."); - continue; - } - if (!scriptObj.HasMember("Path")) - { - spdlog::warn("Script does not have a Path, skipping..."); - continue; - } - // from here on, the Path for a script is used as it's name in logs - if (!scriptObj.HasMember("RunOn")) - { - // "a RunOn" sounds dumb but anything else doesn't match the format of the warnings... - // this is the best i could think of within 20 seconds - spdlog::warn("Script '{}' does not have a RunOn field, skipping...", scriptObj["Path"].GetString()); - continue; - } - - ModScript script; - - script.Path = scriptObj["Path"].GetString(); - script.RunOn = scriptObj["RunOn"].GetString(); - - if (scriptObj.HasMember("ServerCallback")) - { - if (scriptObj["ServerCallback"].IsObject()) - { - ModScriptCallback callback; - callback.Context = ScriptContext::SERVER; - - if (scriptObj["ServerCallback"].HasMember("Before")) - { - if (scriptObj["ServerCallback"]["Before"].IsString()) - callback.BeforeCallback = scriptObj["ServerCallback"]["Before"].GetString(); - else - spdlog::warn("'Before' ServerCallback for script '{}' is not a string, skipping...", scriptObj["Path"].GetString()); - } - - if (scriptObj["ServerCallback"].HasMember("After")) - { - if (scriptObj["ServerCallback"]["After"].IsString()) - callback.AfterCallback = scriptObj["ServerCallback"]["After"].GetString(); - else - spdlog::warn("'After' ServerCallback for script '{}' is not a string, skipping...", scriptObj["Path"].GetString()); - } - - if (scriptObj["ServerCallback"].HasMember("Destroy")) - { - if (scriptObj["ServerCallback"]["Destroy"].IsString()) - callback.DestroyCallback = scriptObj["ServerCallback"]["Destroy"].GetString(); - else - spdlog::warn( - "'Destroy' ServerCallback for script '{}' is not a string, skipping...", scriptObj["Path"].GetString()); - } - - script.Callbacks.push_back(callback); - } - else - { - spdlog::warn("ServerCallback for script '{}' is not an object, skipping...", scriptObj["Path"].GetString()); - } - } - - if (scriptObj.HasMember("ClientCallback")) - { - if (scriptObj["ClientCallback"].IsObject()) - { - ModScriptCallback callback; - callback.Context = ScriptContext::CLIENT; - - if (scriptObj["ClientCallback"].HasMember("Before")) - { - if (scriptObj["ClientCallback"]["Before"].IsString()) - callback.BeforeCallback = scriptObj["ClientCallback"]["Before"].GetString(); - else - spdlog::warn("'Before' ClientCallback for script '{}' is not a string, skipping...", scriptObj["Path"].GetString()); - } - - if (scriptObj["ClientCallback"].HasMember("After")) - { - if (scriptObj["ClientCallback"]["After"].IsString()) - callback.AfterCallback = scriptObj["ClientCallback"]["After"].GetString(); - else - spdlog::warn("'After' ClientCallback for script '{}' is not a string, skipping...", scriptObj["Path"].GetString()); - } - - if (scriptObj["ClientCallback"].HasMember("Destroy")) - { - if (scriptObj["ClientCallback"]["Destroy"].IsString()) - callback.DestroyCallback = scriptObj["ClientCallback"]["Destroy"].GetString(); - else - spdlog::warn( - "'Destroy' ClientCallback for script '{}' is not a string, skipping...", scriptObj["Path"].GetString()); - } - - script.Callbacks.push_back(callback); - } - else - { - spdlog::warn("ClientCallback for script '{}' is not an object, skipping...", scriptObj["Path"].GetString()); - } - } - - if (scriptObj.HasMember("UICallback")) - { - if (scriptObj["UICallback"].IsObject()) - { - ModScriptCallback callback; - callback.Context = ScriptContext::UI; - - if (scriptObj["UICallback"].HasMember("Before")) - { - if (scriptObj["UICallback"]["Before"].IsString()) - callback.BeforeCallback = scriptObj["UICallback"]["Before"].GetString(); - else - spdlog::warn("'Before' UICallback for script '{}' is not a string, skipping...", scriptObj["Path"].GetString()); - } - - if (scriptObj["UICallback"].HasMember("After")) - { - if (scriptObj["UICallback"]["After"].IsString()) - callback.AfterCallback = scriptObj["UICallback"]["After"].GetString(); - else - spdlog::warn("'After' UICallback for script '{}' is not a string, skipping...", scriptObj["Path"].GetString()); - } - - if (scriptObj["UICallback"].HasMember("Destroy")) - { - if (scriptObj["UICallback"]["Destroy"].IsString()) - callback.DestroyCallback = scriptObj["UICallback"]["Destroy"].GetString(); - else - spdlog::warn("'Destroy' UICallback for script '{}' is not a string, skipping...", scriptObj["Path"].GetString()); - } - - script.Callbacks.push_back(callback); - } - else - { - spdlog::warn("UICallback for script '{}' is not an object, skipping...", scriptObj["Path"].GetString()); - } - } - - Scripts.push_back(script); - - spdlog::info("'{}' contains Script '{}'", Name, script.Path); - } -} - -void Mod::ParseLocalization(rapidjson_document& json) -{ - if (!json.HasMember("Localisation")) - return; - - if (!json["Localisation"].IsArray()) - { - spdlog::warn("'Localisation' field is not an array, skipping..."); - return; - } - - for (auto& localisationStr : json["Localisation"].GetArray()) - { - if (!localisationStr.IsString()) - { - // not a string but we still GetString() to log it :trol: - spdlog::warn("Localisation '{}' is not a string, skipping...", localisationStr.GetString()); - continue; - } - - LocalisationFiles.push_back(localisationStr.GetString()); - - spdlog::info("'{}' registered Localisation '{}'", Name, localisationStr.GetString()); - } -} - -void Mod::ParseDependencies(rapidjson_document& json) -{ - if (!json.HasMember("Dependencies")) - return; - - if (!json["Dependencies"].IsObject()) - { - spdlog::warn("'Dependencies' field is not an object, skipping..."); - return; - } - - for (auto v = json["Dependencies"].MemberBegin(); v != json["Dependencies"].MemberEnd(); v++) - { - if (!v->name.IsString()) - { - spdlog::warn("Dependency constant '{}' is not a string, skipping...", v->name.GetString()); - continue; - } - if (!v->value.IsString()) - { - spdlog::warn("Dependency constant '{}' is not a string, skipping...", v->value.GetString()); - continue; - } - - if (DependencyConstants.find(v->name.GetString()) != DependencyConstants.end() && - v->value.GetString() != DependencyConstants[v->name.GetString()]) - { - // this is fatal because otherwise the mod will probably try to use functions that dont exist, - // which will cause errors further down the line that are harder to debug - spdlog::error( - "'{}' attempted to register a dependency constant '{}' for '{}' that already exists for '{}'. " - "Change the constant name.", - Name, - v->name.GetString(), - v->value.GetString(), - DependencyConstants[v->name.GetString()]); - return; - } - - if (DependencyConstants.find(v->name.GetString()) == DependencyConstants.end()) - DependencyConstants.emplace(v->name.GetString(), v->value.GetString()); - - spdlog::info("'{}' registered dependency constant '{}' for mod '{}'", Name, v->name.GetString(), v->value.GetString()); - } -} - -void Mod::ParsePluginDependencies(rapidjson_document& json) -{ - if (!json.HasMember("PluginDependencies")) - return; - - if (!json["PluginDependencies"].IsArray()) - { - spdlog::warn("'PluginDependencies' field is not an object, skipping..."); - return; - } - - for (auto& name : json["PluginDependencies"].GetArray()) - { - if (!name.IsString()) - continue; - - spdlog::info("Plugin Constant {} defined by {}", name.GetString(), Name); - - PluginDependencyConstants.push_back(name.GetString()); - } -} - -void Mod::ParseInitScript(rapidjson_document& json) -{ - if (!json.HasMember("InitScript")) - return; - - if (!json["InitScript"].IsString()) - { - spdlog::warn("'InitScript' field is not a string, skipping..."); - return; - } - - initScript = json["InitScript"].GetString(); -} - -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(); -} - -struct Test -{ - std::string funcName; - ScriptContext context; -}; - -template <ScriptContext context> auto ModConCommandCallback_Internal(std::string name, const CCommand& command) -{ - if (g_pSquirrel<context>->m_pSQVM && g_pSquirrel<context>->m_pSQVM) - { - if (command.ArgC() == 1) - { - g_pSquirrel<context>->AsyncCall(name); - } - else - { - std::vector<std::string> args; - args.reserve(command.ArgC()); - for (int i = 1; i < command.ArgC(); i++) - args.push_back(command.Arg(i)); - g_pSquirrel<context>->AsyncCall(name, args); - } - } - else - { - spdlog::warn("ConCommand `{}` was called while the associated Squirrel VM `{}` was unloaded", name, GetContextName(context)); - } -} - -auto ModConCommandCallback(const CCommand& command) -{ - ModConCommand* found = nullptr; - auto commandString = std::string(command.GetCommandString()); - - // Finding the first space to remove the command's name - auto firstSpace = commandString.find(' '); - if (firstSpace) - { - commandString = commandString.substr(0, firstSpace); - } - - // Find the mod this command belongs to - for (auto& mod : g_pModManager->m_LoadedMods) - { - auto res = std::find_if( - mod.ConCommands.begin(), - mod.ConCommands.end(), - [&commandString](const ModConCommand* other) { return other->Name == commandString; }); - if (res != mod.ConCommands.end()) - { - found = *res; - break; - } - } - if (!found) - return; - - switch (found->Context) - { - case ScriptContext::CLIENT: - ModConCommandCallback_Internal<ScriptContext::CLIENT>(found->Function, command); - break; - case ScriptContext::SERVER: - ModConCommandCallback_Internal<ScriptContext::SERVER>(found->Function, command); - break; - case ScriptContext::UI: - ModConCommandCallback_Internal<ScriptContext::UI>(found->Function, command); - break; - }; -} - -void ModManager::LoadMods() -{ - if (m_bHasLoadedMods) - UnloadMods(); - - std::vector<fs::path> modDirs; - - // ensure dirs exist - fs::remove_all(GetCompiledAssetsPath()); - fs::create_directories(GetModFolderPath()); - fs::create_directories(GetThunderstoreModFolderPath()); - fs::create_directories(GetRemoteModFolderPath()); - - 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 - std::filesystem::directory_iterator classicModsDir = fs::directory_iterator(GetModFolderPath()); - std::filesystem::directory_iterator remoteModsDir = fs::directory_iterator(GetRemoteModFolderPath()); - std::filesystem::directory_iterator thunderstoreModsDir = fs::directory_iterator(GetThunderstoreModFolderPath()); - - for (fs::directory_entry dir : classicModsDir) - if (fs::exists(dir.path() / "mod.json")) - modDirs.push_back(dir.path()); - - // Special case for Thunderstore and remote mods directories - // Set up regex for `AUTHOR-MOD-VERSION` pattern - std::regex pattern(R"(.*\\([a-zA-Z0-9_]+)-([a-zA-Z0-9_]+)-(\d+\.\d+\.\d+))"); - - for (fs::directory_iterator dirIterator : {thunderstoreModsDir, remoteModsDir}) - { - for (fs::directory_entry dir : dirIterator) - { - fs::path modsDir = dir.path() / "mods"; // Check for mods folder in the Thunderstore mod - // Use regex to match `AUTHOR-MOD-VERSION` pattern - if (!std::regex_match(dir.path().string(), pattern)) - { - spdlog::warn("The following directory did not match 'AUTHOR-MOD-VERSION': {}", dir.path().string()); - continue; // skip loading mod that doesn't match - } - if (fs::exists(modsDir) && fs::is_directory(modsDir)) - { - for (fs::directory_entry subDir : fs::directory_iterator(modsDir)) - { - if (fs::exists(subDir.path() / "mod.json")) - { - modDirs.push_back(subDir.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 file at '{}' does not exist or could not be read, is it installed correctly?", (modDir / "mod.json").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( - "'{}' attempted to register a dependency constant '{}' for '{}' that already exists for '{}'. " - "Change the constant name.", - mod.Name, - pair.first, - pair.second, - m_DependencyConstants[pair.first]); - mod.m_bWasReadSuccessfully = false; - break; - } - if (m_DependencyConstants.find(pair.first) == m_DependencyConstants.end()) - m_DependencyConstants.emplace(pair); - } - - for (std::string& dependency : mod.PluginDependencyConstants) - { - m_PluginDependencyConstants.insert(dependency); - } - - 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) - { - if (mod.m_bEnabled) - spdlog::info("'{}' loaded successfully, version {}", mod.Name, mod.Version); - else - spdlog::info("'{}' loaded successfully, version {} (DISABLED)", mod.Name, mod.Version); - - m_LoadedMods.push_back(mod); - } - else - spdlog::warn("Mod file at '{}' failed to load", (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; }); - - // This is used to check if some mods have a folder but no entry in enabledmods.json - bool newModsDetected = false; - - for (Mod& mod : m_LoadedMods) - { - if (!mod.m_bEnabled) - continue; - - // Add mod entry to enabledmods.json if it doesn't exist - if (!mod.m_bIsRemote && !m_EnabledModsCfg.HasMember(mod.Name.c_str())) - { - m_EnabledModsCfg.AddMember(rapidjson_document::StringRefType(mod.Name.c_str()), true, m_EnabledModsCfg.GetAllocator()); - newModsDetected = true; - } - - // 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) - { - // make sure convar isn't registered yet, unsure if necessary but idk what - // behaviour is for defining same convar multiple times - if (!g_pCVar->FindVar(convar->Name.c_str())) - { - new ConVar(convar->Name.c_str(), convar->DefaultValue.c_str(), convar->Flags, convar->HelpString.c_str()); - } - } - - for (ModConCommand* command : mod.ConCommands) - { - // make sure command isnt't registered multiple times. - if (!g_pCVar->FindCommand(command->Name.c_str())) - { - ConCommand* newCommand = new ConCommand(); - std::string funcName = command->Function; - RegisterConCommand(command->Name.c_str(), ModConCommandCallback, command->HelpString.c_str(), command->Flags); - } - } - - // 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) - (*g_pFilesystem)->m_vtable->MountVPK(*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; - } - } - } - } - } - - // If there are new mods, we write entries accordingly in enabledmods.json - if (newModsDetected) - { - std::ofstream writeStream(GetNorthstarPrefix() + "/enabledmods.json"); - rapidjson::OStreamWrapper writeStreamWrapper(writeStream); - rapidjson::PrettyWriter<rapidjson::OStreamWrapper> writer(writeStreamWrapper); - m_EnabledModsCfg.Accept(writer); - } - - // 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::PrettyWriter<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 GetThunderstoreModFolderPath() -{ - return fs::path(GetNorthstarPrefix() + THUNDERSTORE_MOD_FOLDER_SUFFIX); -} -fs::path GetRemoteModFolderPath() -{ - return fs::path(GetNorthstarPrefix() + REMOTE_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); -} |