aboutsummaryrefslogtreecommitdiff
path: root/primedev
diff options
context:
space:
mode:
authorRémy Raes <raes.remy@gmail.com>2024-11-27 10:55:12 +0100
committerGitHub <noreply@github.com>2024-11-27 10:55:12 +0100
commit21843eeb83fd9c0057a05fbdfbbd76b6e969a9d1 (patch)
tree2d5ffc34d296b75bcc5ebbe0ec6474010ee09402 /primedev
parentfad89b3536e0db610a1c93334e4c7f8a5b00ded2 (diff)
downloadNorthstarLauncher-21843eeb83fd9c0057a05fbdfbbd76b6e969a9d1.tar.gz
NorthstarLauncher-21843eeb83fd9c0057a05fbdfbbd76b6e969a9d1.zip
mods: Move mod content related logic to dedicated package (#829)v1.29.1-rc1
Moves the `Mod` class and related logic to its own source files
Diffstat (limited to 'primedev')
-rw-r--r--primedev/Northstar.cmake2
-rw-r--r--primedev/mods/mod.cpp501
-rw-r--r--primedev/mods/mod.h138
-rw-r--r--primedev/mods/modmanager.cpp500
-rw-r--r--primedev/mods/modmanager.h140
5 files changed, 642 insertions, 639 deletions
diff --git a/primedev/Northstar.cmake b/primedev/Northstar.cmake
index 35383e69..c64b0bc7 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 00000000..a149b611
--- /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<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();
+}
diff --git a/primedev/mods/mod.h b/primedev/mods/mod.h
new file mode 100644
index 00000000..164666b6
--- /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<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:
+ 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<ModScript> Scripts;
+ // convars created by the mod
+ std::vector<ModConVar*> ConVars;
+ // concommands created by the mod
+ std::vector<ModConCommand*> ConCommands;
+ // custom localisation files created by the mod
+ std::vector<std::string> LocalisationFiles;
+ // custom script init.nut
+ std::string initScript;
+
+ // 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;
+ // paks we alias to other rpaks, e.g. to load sp_crashsite paks on the map mp_crashsite
+ std::unordered_map<std::string, std::string> 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<size_t> StarpakPaths;
+
+ std::unordered_map<std::string, std::string> DependencyConstants;
+ std::vector<std::string> 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 0c162de0..aa685c46 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<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
diff --git a/primedev/mods/modmanager.h b/primedev/mods/modmanager.h
index 7859d618..6350a1fc 100644
--- a/primedev/mods/modmanager.h
+++ b/primedev/mods/modmanager.h
@@ -9,6 +9,7 @@
#include <filesystem>
#include <unordered_set>
#include <regex>
+#include "mod.h"
namespace fs = std::filesystem;
@@ -20,145 +21,6 @@ const std::string COMPILED_ASSETS_SUFFIX = "\\runtime\\compiled";
const std::set<std::string> 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<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:
- 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<ModScript> Scripts;
- // convars created by the mod
- std::vector<ModConVar*> ConVars;
- // concommands created by the mod
- std::vector<ModConCommand*> ConCommands;
- // custom localisation files created by the mod
- std::vector<std::string> LocalisationFiles;
- // custom script init.nut
- std::string initScript;
-
- // 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;
- // paks we alias to other rpaks, e.g. to load sp_crashsite paks on the map mp_crashsite
- std::unordered_map<std::string, std::string> 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<size_t> StarpakPaths;
-
- std::unordered_map<std::string, std::string> DependencyConstants;
- std::vector<std::string> 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: