diff --git a/primedev/Northstar.cmake b/primedev/Northstar.cmake index 35383e69c..c64b0bc7d 100644 --- a/primedev/Northstar.cmake +++ b/primedev/Northstar.cmake @@ -85,6 +85,8 @@ add_library( "mods/compiled/modkeyvalues.cpp" "mods/compiled/modpdef.cpp" "mods/compiled/modscriptsrson.cpp" + "mods/mod.cpp" + "mods/mod.h" "mods/modmanager.cpp" "mods/modmanager.h" "mods/modsavefiles.cpp" diff --git a/primedev/mods/mod.cpp b/primedev/mods/mod.cpp new file mode 100644 index 000000000..a149b6113 --- /dev/null +++ b/primedev/mods/mod.cpp @@ -0,0 +1,501 @@ +#include "rapidjson/error/en.h" + +Mod::Mod(fs::path modDir, char* jsonBuf) +{ + m_bWasReadSuccessfully = false; + + m_ModDirectory = modDir; + + rapidjson_document modJson; + modJson.Parse(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(); +} diff --git a/primedev/mods/mod.h b/primedev/mods/mod.h new file mode 100644 index 000000000..164666b6a --- /dev/null +++ b/primedev/mods/mod.h @@ -0,0 +1,138 @@ +class Mod; + +struct ModConVar +{ +public: + std::string Name; + std::string DefaultValue; + std::string HelpString; + int Flags; +}; + +struct ModConCommand +{ +public: + std::string Name; + std::string Function; + std::string HelpString; + ScriptContext Context; + 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; + // called right before the vm is destroyed. + std::string DestroyCallback; +}; + +struct ModScript +{ +public: + std::string Path; + std::string RunOn; + + std::vector 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: + ModRpakEntry(Mod& parent) + : m_parent(parent) + , m_loadRegex("^thisMatchesNothing^") // discord couldnt give me a funny string + { + } + + Mod& m_parent; + std::string m_pakName; + std::regex m_loadRegex; + + // these exist purely for backwards compatibility, i don't really like them anymore + + // Preload, loads before the first rpak is loaded + bool m_preload = false; + // Postload, this rpak depends on an rpak with this hash + size_t m_dependentPakHash; +}; + +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 Scripts; + // convars created by the mod + std::vector ConVars; + // concommands created by the mod + std::vector ConCommands; + // custom localisation files created by the mod + std::vector LocalisationFiles; + // custom script init.nut + std::string initScript; + + // other files: + + std::vector Vpks; + std::unordered_map KeyValues; + std::vector BinkVideos; + std::string Pdiff; // only need one per mod + + std::vector Rpaks; + // paks we alias to other rpaks, e.g. to load sp_crashsite paks on the map mp_crashsite + std::unordered_map RpakAliases; + // 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::vector StarpakPaths; + + std::unordered_map DependencyConstants; + std::vector PluginDependencyConstants; + +public: + Mod(fs::path modPath, char* jsonBuf); + +private: + void ParseConVars(rapidjson_document& json); + void ParseConCommands(rapidjson_document& json); + void ParseScripts(rapidjson_document& json); + void ParseLocalization(rapidjson_document& json); + void ParseDependencies(rapidjson_document& json); + void ParsePluginDependencies(rapidjson_document& json); + void ParseInitScript(rapidjson_document& json); +}; diff --git a/primedev/mods/modmanager.cpp b/primedev/mods/modmanager.cpp index 0c42ba217..43ee53ee9 100644 --- a/primedev/mods/modmanager.cpp +++ b/primedev/mods/modmanager.cpp @@ -20,506 +20,6 @@ ModManager* g_pModManager; -Mod::Mod(fs::path modDir, char* jsonBuf) -{ - m_bWasReadSuccessfully = false; - - m_ModDirectory = modDir; - - rapidjson_document modJson; - modJson.Parse(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 diff --git a/primedev/mods/modmanager.h b/primedev/mods/modmanager.h index 68a7c5d0f..2beb4a06a 100644 --- a/primedev/mods/modmanager.h +++ b/primedev/mods/modmanager.h @@ -9,6 +9,7 @@ #include #include #include +#include "mod.h" namespace fs = std::filesystem; @@ -21,145 +22,6 @@ const std::string COMPILED_ASSETS_SUFFIX = "\\runtime\\compiled"; const std::set MODS_BLACKLIST = {"Mod Settings"}; -class Mod; - -struct ModConVar -{ -public: - std::string Name; - std::string DefaultValue; - std::string HelpString; - int Flags; -}; - -struct ModConCommand -{ -public: - std::string Name; - std::string Function; - std::string HelpString; - ScriptContext Context; - 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; - // called right before the vm is destroyed. - std::string DestroyCallback; -}; - -struct ModScript -{ -public: - std::string Path; - std::string RunOn; - - std::vector 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: - ModRpakEntry(Mod& parent) - : m_parent(parent) - , m_loadRegex("^thisMatchesNothing^") // discord couldnt give me a funny string - { - } - - Mod& m_parent; - std::string m_pakName; - std::regex m_loadRegex; - - // these exist purely for backwards compatibility, i don't really like them anymore - - // Preload, loads before the first rpak is loaded - bool m_preload = false; - // Postload, this rpak depends on an rpak with this hash - size_t m_dependentPakHash; -}; - -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 Scripts; - // convars created by the mod - std::vector ConVars; - // concommands created by the mod - std::vector ConCommands; - // custom localisation files created by the mod - std::vector LocalisationFiles; - // custom script init.nut - std::string initScript; - - // other files: - - std::vector Vpks; - std::unordered_map KeyValues; - std::vector BinkVideos; - std::string Pdiff; // only need one per mod - - std::vector Rpaks; - // paks we alias to other rpaks, e.g. to load sp_crashsite paks on the map mp_crashsite - std::unordered_map RpakAliases; - // 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::vector StarpakPaths; - - std::unordered_map DependencyConstants; - std::vector PluginDependencyConstants; - -public: - Mod(fs::path modPath, char* jsonBuf); - -private: - void ParseConVars(rapidjson_document& json); - void ParseConCommands(rapidjson_document& json); - void ParseScripts(rapidjson_document& json); - void ParseLocalization(rapidjson_document& json); - void ParseDependencies(rapidjson_document& json); - void ParsePluginDependencies(rapidjson_document& json); - void ParseInitScript(rapidjson_document& json); -}; - struct ModOverrideFile { public: