aboutsummaryrefslogtreecommitdiff
path: root/NorthstarDLL/mods
diff options
context:
space:
mode:
authorunknown <for.oliver.kirkham@gmail.com>2023-02-13 13:51:40 +0000
committerunknown <for.oliver.kirkham@gmail.com>2023-02-13 13:51:40 +0000
commit56372efa9706898c8646376182b544bc6350c5c7 (patch)
tree08b609e38c758f6c3431be5df6591d0c34d802bc /NorthstarDLL/mods
parent2c2bd9ebb8f832ff117caca16e5cb3bc806666db (diff)
downloadNorthstarLauncher-56372efa9706898c8646376182b544bc6350c5c7.tar.gz
NorthstarLauncher-56372efa9706898c8646376182b544bc6350c5c7.zip
refactor modloading and implement initial automatic reload code
Diffstat (limited to 'NorthstarDLL/mods')
-rw-r--r--NorthstarDLL/mods/compiled/kb_act.cpp5
-rw-r--r--NorthstarDLL/mods/compiled/modkeyvalues.cpp5
-rw-r--r--NorthstarDLL/mods/compiled/modscriptsrson.cpp5
-rw-r--r--NorthstarDLL/mods/modmanager.cpp894
-rw-r--r--NorthstarDLL/mods/modmanager.h106
5 files changed, 670 insertions, 345 deletions
diff --git a/NorthstarDLL/mods/compiled/kb_act.cpp b/NorthstarDLL/mods/compiled/kb_act.cpp
index f3ad460f..83f469f4 100644
--- a/NorthstarDLL/mods/compiled/kb_act.cpp
+++ b/NorthstarDLL/mods/compiled/kb_act.cpp
@@ -17,11 +17,8 @@ void ModManager::BuildKBActionsList()
// write vanilla file's content to compiled file
soCompiledKeys << R2::ReadVPKOriginalFile(KB_ACT_PATH);
- for (Mod& mod : GetMods())
+ for (Mod& mod : GetMods() | ModManager::FilterEnabled)
{
- if (!mod.m_bEnabled)
- continue;
-
// write content of each modded file to compiled file
std::ifstream siModKeys(mod.m_ModDirectory / "kb_act.lst");
diff --git a/NorthstarDLL/mods/compiled/modkeyvalues.cpp b/NorthstarDLL/mods/compiled/modkeyvalues.cpp
index 051b193e..df35de64 100644
--- a/NorthstarDLL/mods/compiled/modkeyvalues.cpp
+++ b/NorthstarDLL/mods/compiled/modkeyvalues.cpp
@@ -25,11 +25,8 @@ void ModManager::TryBuildKeyValues(const char* filename)
// copy over patch kv files, and add #bases to new file, last mods' patches should be applied first
// note: #include should be identical but it's actually just broken, thanks respawn
- for (Mod& mod : GetMods() | std::views::reverse)
+ for (Mod& mod : GetMods() | ModManager::FilterEnabled | std::views::reverse)
{
- if (mod.m_bEnabled)
- continue;
-
size_t fileHash = STR_HASH(normalisedPath);
auto modKv = mod.KeyValues.find(fileHash);
if (modKv != mod.KeyValues.end())
diff --git a/NorthstarDLL/mods/compiled/modscriptsrson.cpp b/NorthstarDLL/mods/compiled/modscriptsrson.cpp
index b24aabe9..1a27aefa 100644
--- a/NorthstarDLL/mods/compiled/modscriptsrson.cpp
+++ b/NorthstarDLL/mods/compiled/modscriptsrson.cpp
@@ -17,11 +17,8 @@ void ModManager::BuildScriptsRson()
std::string scriptsRson = R2::ReadVPKOriginalFile(VPK_SCRIPTS_RSON_PATH);
scriptsRson += "\n\n// START MODDED SCRIPT CONTENT\n\n"; // newline before we start custom stuff
- for (Mod& mod : GetMods())
+ for (Mod& mod : GetMods() | ModManager::FilterEnabled)
{
- if (!mod.m_bEnabled)
- continue;
-
// this isn't needed at all, just nice to have imo
scriptsRson += "// MOD: ";
scriptsRson += mod.Name;
diff --git a/NorthstarDLL/mods/modmanager.cpp b/NorthstarDLL/mods/modmanager.cpp
index 5011f0fb..1c3c416d 100644
--- a/NorthstarDLL/mods/modmanager.cpp
+++ b/NorthstarDLL/mods/modmanager.cpp
@@ -3,10 +3,12 @@
#include "core/convar/convar.h"
#include "core/convar/concommand.h"
#include "client/audio.h"
+#include "engine/r2engine.h"
#include "masterserver/masterserver.h"
#include "core/filesystem/filesystem.h"
#include "core/filesystem/rpakfilesystem.h"
#include "config/profile.h"
+#include "dedicated/dedicated.h"
#include "rapidjson/error/en.h"
#include "rapidjson/document.h"
@@ -20,14 +22,15 @@
ModManager* g_pModManager;
-Mod::Mod(fs::path modDir, char* jsonBuf)
+Mod::Mod(fs::path modDir, std::string sJson, bool bRemote)
{
m_bWasReadSuccessfully = false;
m_ModDirectory = modDir;
+ m_bRemote = bRemote;
rapidjson_document modJson;
- modJson.Parse<rapidjson::ParseFlag::kParseCommentsFlag | rapidjson::ParseFlag::kParseTrailingCommasFlag>(jsonBuf);
+ modJson.Parse<rapidjson::ParseFlag::kParseCommentsFlag | rapidjson::ParseFlag::kParseTrailingCommasFlag>(sJson);
// fail if parse error
if (modJson.HasParseError())
@@ -96,8 +99,6 @@ Mod::Mod(fs::path modDir, char* jsonBuf)
if (!convarObj.IsObject() || !convarObj.HasMember("Name") || !convarObj.HasMember("DefaultValue"))
continue;
- // have to allocate this manually, otherwise convar registration will break
- // unfortunately this causes us to leak memory on reload, unsure of a way around this rn
ModConVar convar;
convar.Name = convarObj["Name"].GetString();
convar.DefaultValue = convarObj["DefaultValue"].GetString();
@@ -137,12 +138,11 @@ Mod::Mod(fs::path modDir, char* jsonBuf)
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;
concommand.Name = concommandObj["Name"].GetString();
concommand.Function = concommandObj["Function"].GetString();
concommand.Context = ScriptContextFromString(concommandObj["Context"].GetString());
+
if (concommand.Context == ScriptContext::INVALID)
{
spdlog::warn("Mod ConCommand {} has invalid context {}", concommand.Name, concommandObj["Context"].GetString());
@@ -160,9 +160,7 @@ Mod::Mod(fs::path modDir, char* jsonBuf)
{
// read raw integer flags
if (concommandObj["Flags"].IsInt())
- {
concommand.Flags = concommandObj["Flags"].GetInt();
- }
else if (concommandObj["Flags"].IsString())
{
// parse cvar flags from string
@@ -171,6 +169,13 @@ Mod::Mod(fs::path modDir, char* jsonBuf)
}
}
+ // for commands, client should always be FCVAR_CLIENTDLL, and server should always be FCVAR_GAMEDLL
+ if (concommand.Context == ScriptContext::CLIENT)
+ concommand.Flags |= FCVAR_CLIENTDLL;
+ else if (concommand.Context == ScriptContext::SERVER)
+ concommand.Flags |= FCVAR_GAMEDLL;
+
+
ConCommands.push_back(concommand);
}
}
@@ -287,6 +292,9 @@ ModManager::ModManager()
);
m_hKBActHash = STR_HASH("scripts\\kb_act.lst");
+ m_LastModLoadState = nullptr;
+ m_ModLoadState = new ModLoadState;
+
LoadMods();
}
@@ -311,12 +319,12 @@ auto ModConCommandCallback(const CCommand& command)
std::string sCommandName = command.Arg(0);
// Find the mod this command belongs to
- for (Mod& mod : g_pModManager->GetMods())
+ for (Mod& mod : g_pModManager->GetMods() | ModManager::FilterEnabled)
{
auto res = std::find_if(
mod.ConCommands.begin(),
mod.ConCommands.end(),
- [&sCommandName](const ModConCommand* other) { return other->Name == sCommandName; });
+ [&sCommandName](const ModConCommand& other) { return other.Name == sCommandName; });
if (res != mod.ConCommands.end())
{
@@ -342,81 +350,151 @@ auto ModConCommandCallback(const CCommand& command)
};
}
+
+
+
void ModManager::LoadMods()
{
+ // reset state of all currently loaded mods, if we've loaded once already
if (m_bHasLoadedMods)
UnloadMods();
- std::vector<fs::path> modDirs;
-
// ensure dirs exist
- fs::remove_all(GetCompiledAssetsPath());
fs::create_directories(GetModFolderPath());
fs::create_directories(GetRemoteModFolderPath());
- m_DependencyConstants.clear();
+ // load definitions (mod.json files)
+ LoadModDefinitions();
- // read enabled mods cfg
- std::ifstream enabledModsStream(GetNorthstarPrefix() + "/enabledmods.json");
- std::stringstream enabledModsStringStream;
+ // install mods (load all files)
+ InstallMods();
- if (!enabledModsStream.fail())
+ // write json storing currently enabled mods
+ SaveEnabledMods();
+
+ // 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 : GetMods())
{
- while (enabledModsStream.peek() != EOF)
- enabledModsStringStream << (char)enabledModsStream.get();
+ if (!mod.m_bEnabled || !mod.RequiredOnClient) // (!mod.RequiredOnClient && !mod.Pdiff.size())
+ continue;
- enabledModsStream.close();
- m_EnabledModsCfg.Parse<rapidjson::ParseFlag::kParseCommentsFlag | rapidjson::ParseFlag::kParseTrailingCommasFlag>(
- enabledModsStringStream.str().c_str());
+ 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());
- m_bHasEnabledModsCfg = m_EnabledModsCfg.IsObject();
+ currentModIndex++;
}
- // get mod directories
- std::filesystem::directory_iterator classicModsDir = fs::directory_iterator(GetModFolderPath());
- std::filesystem::directory_iterator remoteModsDir = fs::directory_iterator(GetRemoteModFolderPath());
+ rapidjson::StringBuffer buffer;
+ buffer.Clear();
+ rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
+ modinfoDoc.Accept(writer);
+ g_pMasterServerManager->m_sOwnModInfoJson = std::string(buffer.GetString());
- for (std::filesystem::directory_iterator modIterator : {classicModsDir, remoteModsDir})
- for (fs::directory_entry dir : modIterator)
- if (fs::exists(dir.path() / "mod.json"))
- modDirs.push_back(dir.path());
+ // don't need this anymore
+ delete m_LastModLoadState;
+ m_LastModLoadState = nullptr;
- for (fs::path modDir : modDirs)
+ m_bHasLoadedMods = true;
+}
+
+void ModManager::LoadModDefinitions()
+{
+ bool bHasEnabledModsCfg = false;
+ rapidjson_document enabledModsCfg;
+
+ // read enabled mods cfg
{
- // read mod json file
- std::ifstream jsonStream(modDir / "mod.json");
- std::stringstream jsonStringStream;
+ std::ifstream enabledModsStream(GetNorthstarPrefix() / "enabledmods.json");
+ std::stringstream enabledModsStringStream;
- // fail if no mod json
- if (jsonStream.fail())
+ if (!enabledModsStream.fail())
{
- spdlog::warn("Mod {} has a directory but no mod.json", modDir.string());
- continue;
+ while (enabledModsStream.peek() != EOF)
+ enabledModsStringStream << (char)enabledModsStream.get();
+
+ enabledModsStream.close();
+ enabledModsCfg.Parse<rapidjson::ParseFlag::kParseCommentsFlag | rapidjson::ParseFlag::kParseTrailingCommasFlag>(
+ enabledModsStringStream.str().c_str());
+
+ bHasEnabledModsCfg = enabledModsCfg.IsObject();
}
+ }
+
+ // get mod directories for both local and remote mods
+ std::vector<std::tuple<fs::path, bool>> vModDirs;
+ for (fs::directory_entry dir : fs::directory_iterator(GetModFolderPath()))
+ if (fs::exists(dir.path() / "mod.json"))
+ vModDirs.push_back({dir.path(), false});
+
+ for (fs::directory_entry dir : fs::directory_iterator(GetRemoteModFolderPath()))
+ if (fs::exists(dir.path() / "mod.json"))
+ vModDirs.push_back({dir.path(), true});
+
+ for (auto remoteOrLocalModDir : vModDirs)
+ {
+ fs::path modDir = std::get<0>(remoteOrLocalModDir);
+ bool bRemote = std::get<1>(remoteOrLocalModDir);
- while (jsonStream.peek() != EOF)
- jsonStringStream << (char)jsonStream.get();
+ std::string sJsonString;
- jsonStream.close();
+ // read mod json file
+ {
+ std::stringstream jsonStringStream;
+ std::ifstream jsonStream(modDir / "mod.json");
+
+ // fail if no mod json
+ if (jsonStream.fail())
+ {
+ spdlog::warn("Mod {} has a directory but no mod.json", modDir.string());
+ continue;
+ }
- Mod mod(modDir, (char*)jsonStringStream.str().c_str());
+ while (jsonStream.peek() != EOF)
+ jsonStringStream << (char)jsonStream.get();
+
+ jsonStream.close();
+ sJsonString = jsonStringStream.str();
+ }
+ // read mod
+ Mod mod(modDir, sJsonString, bRemote);
+
+ // maybe this should be in InstallMods()? unsure
for (auto& pair : mod.DependencyConstants)
{
- if (m_DependencyConstants.find(pair.first) != m_DependencyConstants.end() && m_DependencyConstants[pair.first] != pair.second)
+ if (m_ModLoadState->m_DependencyConstants.find(pair.first) != m_ModLoadState->m_DependencyConstants.end() &&
+ m_ModLoadState->m_DependencyConstants[pair.first] != pair.second)
{
spdlog::error("Constant {} in mod {} already exists in another mod.", pair.first, mod.Name);
mod.m_bWasReadSuccessfully = false;
break;
}
- if (m_DependencyConstants.find(pair.first) == m_DependencyConstants.end())
- m_DependencyConstants.emplace(pair);
+
+ if (m_ModLoadState->m_DependencyConstants.find(pair.first) == m_ModLoadState->m_DependencyConstants.end())
+ m_ModLoadState->m_DependencyConstants.emplace(pair);
}
- if (m_bHasEnabledModsCfg && m_EnabledModsCfg.HasMember(mod.Name.c_str()))
- mod.m_bEnabled = m_EnabledModsCfg[mod.Name.c_str()].IsTrue();
+ if (!bRemote)
+ {
+ if (bHasEnabledModsCfg && enabledModsCfg.HasMember(mod.Name.c_str()))
+ mod.m_bEnabled = enabledModsCfg[mod.Name.c_str()].IsTrue();
+ else
+ mod.m_bEnabled = true;
+ }
else
- mod.m_bEnabled = true;
+ {
+ // todo: need custom logic for deciding whether to enable remote mods, but should be off by default
+ // in the future, remote mods should only be enabled explicitly at runtime, never based on any file or persistent state
+ mod.m_bEnabled = false;
+ }
if (mod.m_bWasReadSuccessfully)
{
@@ -426,7 +504,7 @@ void ModManager::LoadMods()
else
spdlog::info("Mod {} is disabled", mod.Name);
- m_LoadedMods.push_back(mod);
+ m_ModLoadState->m_LoadedMods.push_back(mod);
}
else
spdlog::warn("Skipping loading mod file {}", (modDir / "mod.json").string());
@@ -434,353 +512,567 @@ void ModManager::LoadMods()
// sort by load prio, lowest-highest
std::sort(
- m_ModLoadState.m_LoadedMods.begin(),
- m_ModLoadState.m_LoadedMods.end(),
+ m_ModLoadState->m_LoadedMods.begin(),
+ m_ModLoadState->m_LoadedMods.end(),
[](Mod& a, Mod& b) { return a.LoadPriority < b.LoadPriority; });
+}
- for (Mod& mod : m_LoadedMods)
+#pragma region Mod asset installation funcs
+void ModManager::InstallModCvars(Mod& mod)
+{
+ // register convars
+ for (ModConVar convar : mod.ConVars)
{
- if (!mod.m_bEnabled)
- continue;
+ ConVar* pVar = R2::g_pCVar->FindVar(convar.Name.c_str());
- // register convars
- for (ModConVar convar : mod.ConVars)
+ // make sure convar isn't registered yet, if it is then modify its flags, helpstring etc
+ if (!pVar)
{
- ConVar* pVar = R2::g_pCVar->FindVar(convar.Name.c_str());
+ // allocate there here, we can delete later if needed
+ int nNameSize = convar.Name.size();
+ char* pName = new char[nNameSize + 1];
+ strncpy_s(pName, nNameSize + 1, convar.Name.c_str(), convar.Name.size());
- // make sure convar isn't registered yet, if it is then modify its flags, helpstring etc
- if (!pVar)
- new ConVar(convar.Name.c_str(), convar.DefaultValue.c_str(), convar.Flags, convar.HelpString.c_str());
- else
- {
- // TODO: should probably make sure this is actually a mod convar we're messing with
+ int nDefaultValueSize = convar.DefaultValue.size();
+ char* pDefaultValue = new char[nDefaultValueSize + 1];
+ strncpy_s(pDefaultValue, nDefaultValueSize + 1, convar.DefaultValue.c_str(), convar.DefaultValue.size());
- pVar->m_ConCommandBase.m_nFlags = convar.Flags;
+ int nHelpSize = convar.HelpString.size();
+ char* pHelpString = new char[nHelpSize + 1];
+ strncpy_s(pHelpString, nHelpSize + 1, convar.HelpString.c_str(), convar.HelpString.size());
- // unfortunately this leaks memory and we can't really not leak memory because we don't know who allocated this
- // so we can't delete it without risking a crash
- if (convar.HelpString.compare(pVar->GetHelpText()))
- {
- int nHelpSize = convar.HelpString.size();
- char* pNewHelpString = new char[nHelpSize + 1];
- strncpy_s(pNewHelpString, nHelpSize + 1, convar.HelpString.c_str(), convar.HelpString.size());
- pVar->m_ConCommandBase.m_pszHelpString = pNewHelpString;
- }
-
- if (convar.DefaultValue.compare(pVar->m_pszDefaultValue))
- {
- int nDefaultValueSize = convar.DefaultValue.size();
- char* pNewDefaultValueString = new char[nDefaultValueSize + 1];
- strncpy_s(pNewDefaultValueString, nDefaultValueSize + 1, convar.DefaultValue.c_str(), convar.DefaultValue.size());
- pVar->m_pszDefaultValue = pNewDefaultValueString;
- pVar->SetValue(pNewDefaultValueString);
- }
- }
+ pVar = new ConVar(pName, pDefaultValue, convar.Flags, pHelpString);
+ m_RegisteredModConVars.insert(pVar);
}
-
- for (ModConCommand command : mod.ConCommands)
+ else
{
- // make sure command isnt't registered multiple times.
- if (!R2::g_pCVar->FindCommand(command.Name.c_str()))
+ // not a mod cvar, don't let us edit it!
+ if (!m_RegisteredModConVars.contains(pVar))
{
- std::string funcName = command.Function;
- RegisterConCommand(command.Name.c_str(), ModConCommandCallback, command.HelpString.c_str(), command.Flags);
+ spdlog::warn("Mod {} tried to create ConVar {} that was already defined in native code!", mod.Name, convar.Name);
+ continue;
}
- }
- // 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;
+ pVar->m_ConCommandBase.m_nFlags = convar.Flags;
- bool bUseVPKJson = false;
- rapidjson::Document dVpkJson;
+ if (convar.HelpString.compare(pVar->GetHelpText()))
+ {
+ int nHelpSize = convar.HelpString.size();
+ char* pNewHelpString = new char[nHelpSize + 1];
+ strncpy_s(pNewHelpString, nHelpSize + 1, convar.HelpString.c_str(), convar.HelpString.size());
- if (!vpkJsonStream.fail())
+ // delete old, assign new
+ delete pVar->m_ConCommandBase.m_pszHelpString;
+ pVar->m_ConCommandBase.m_pszHelpString = pNewHelpString;
+ }
+
+ if (convar.DefaultValue.compare(pVar->m_pszDefaultValue))
{
- while (vpkJsonStream.peek() != EOF)
- vpkJsonStringStream << (char)vpkJsonStream.get();
+ bool bIsDefaultValue = !strcmp(pVar->GetString(), pVar->m_pszDefaultValue);
- vpkJsonStream.close();
- dVpkJson.Parse<rapidjson::ParseFlag::kParseCommentsFlag | rapidjson::ParseFlag::kParseTrailingCommasFlag>(
- vpkJsonStringStream.str().c_str());
+ int nDefaultValueSize = convar.DefaultValue.size();
+ char* pNewDefaultValue = new char[nDefaultValueSize + 1];
+ strncpy_s(pNewDefaultValue, nDefaultValueSize + 1, convar.DefaultValue.c_str(), convar.DefaultValue.size());
- bUseVPKJson = !dVpkJson.HasParseError() && dVpkJson.IsObject();
+ // delete old, assign new
+ delete pVar->m_pszDefaultValue;
+ pVar->m_pszDefaultValue = pNewDefaultValue;
+
+ if (bIsDefaultValue) // only set value if it's currently default value, if changed then don't
+ pVar->SetValue(pNewDefaultValue);
}
+ }
+ }
+
+ // register command
+ for (ModConCommand command : mod.ConCommands)
+ {
+ // make sure command isnt't registered multiple times.
+ ConCommand* pCommand = R2::g_pCVar->FindCommand(command.Name.c_str());
- for (fs::directory_entry file : fs::directory_iterator(mod.m_ModDirectory / "vpk"))
+ if (!pCommand)
+ {
+ // allocate there here, we can delete later if needed
+ int nNameSize = command.Name.size();
+ char* pName = new char[nNameSize + 1];
+ strncpy_s(pName, nNameSize + 1, command.Name.c_str(), command.Name.size());
+
+ int nHelpSize = command.HelpString.size();
+ char* pHelpString = new char[nHelpSize + 1];
+ strncpy_s(pHelpString, nHelpSize + 1, command.HelpString.c_str(), command.HelpString.size());
+
+ pCommand = RegisterConCommand(pName, ModConCommandCallback, pHelpString, command.Flags);
+ m_RegisteredModConCommands.insert(pCommand);
+ }
+ else
+ {
+ if (!m_RegisteredModConCommands.contains(pCommand))
{
- // 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();
+ spdlog::warn("Mod {} tried to create ConCommand {} that was already defined in native code!", mod.Name, command.Name);
+ continue;
+ }
- // this really fucking sucks but it'll work
- std::string vpkName = formattedPath.substr(strlen("english"), formattedPath.find(".bsp") - 3);
+ pCommand->m_nFlags = command.Flags;
- 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 (command.HelpString.compare(pCommand->GetHelpText()))
+ {
+ int nHelpSize = command.HelpString.size();
+ char* pNewHelpString = new char[nHelpSize + 1];
+ strncpy_s(pNewHelpString, nHelpSize + 1, command.HelpString.c_str(), command.HelpString.size());
- if (m_bHasLoadedMods && modVpk.m_bAutoLoad)
- (*R2::g_pFilesystem)->m_vtable->MountVPK(*R2::g_pFilesystem, vpkName.c_str());
- }
+ // delete old, assign new
+ delete pCommand->m_pszHelpString;
+ pCommand->m_pszHelpString = pNewHelpString;
}
}
+ }
+}
+
+void ModManager::InstallModVpks(Mod& mod)
+{
+ // 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;
- // read rpak paths
- if (fs::exists(mod.m_ModDirectory / "paks"))
+ bool bUseVPKJson = false;
+ rapidjson::Document dVpkJson;
+
+ if (!vpkJsonStream.fail())
{
- // read rpak cfg
- std::ifstream rpakJsonStream(mod.m_ModDirectory / "paks/rpak.json");
- std::stringstream rpakJsonStringStream;
+ while (vpkJsonStream.peek() != EOF)
+ vpkJsonStringStream << (char)vpkJsonStream.get();
+
+ vpkJsonStream.close();
+ dVpkJson.Parse<rapidjson::ParseFlag::kParseCommentsFlag | rapidjson::ParseFlag::kParseTrailingCommasFlag>(
+ vpkJsonStringStream.str().c_str());
- bool bUseRpakJson = false;
- rapidjson::Document dRpakJson;
+ bUseVPKJson = !dVpkJson.HasParseError() && dVpkJson.IsObject();
+ }
- if (!rpakJsonStream.fail())
+ 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)
{
- while (rpakJsonStream.peek() != EOF)
- rpakJsonStringStream << (char)rpakJsonStream.get();
+ std::string formattedPath = file.path().filename().string();
- rpakJsonStream.close();
- dRpakJson.Parse<rapidjson::ParseFlag::kParseCommentsFlag | rapidjson::ParseFlag::kParseTrailingCommasFlag>(
- rpakJsonStringStream.str().c_str());
+ // this really fucking sucks but it'll work
+ std::string vpkName = formattedPath.substr(strlen("english"), formattedPath.find(".bsp") - 3);
- bUseRpakJson = !dRpakJson.HasParseError() && dRpakJson.IsObject();
+ ModVPKEntry& modVpk = mod.Vpks.emplace_back();
+ modVpk.m_bAutoLoad = !bUseVPKJson || (dVpkJson.HasMember("Preload") && dVpkJson["Preload"].IsObject() &&
+ dVpkJson["Preload"].HasMember(vpkName) && dVpkJson["Preload"][vpkName].IsTrue());
+ modVpk.m_sVpkPath = (file.path().parent_path() / vpkName).string();
+
+ if (m_bHasLoadedMods && modVpk.m_bAutoLoad)
+ (*R2::g_pFilesystem)->m_vtable->MountVPK(*R2::g_pFilesystem, vpkName.c_str());
}
+ }
+ }
+}
+
+void ModManager::InstallModRpaks(Mod& mod)
+{
+ // 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());
- // read pak aliases
- if (bUseRpakJson && dRpakJson.HasMember("Aliases") && dRpakJson["Aliases"].IsObject())
+ 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++)
{
- for (rapidjson::Value::ConstMemberIterator iterator = dRpakJson["Aliases"].MemberBegin();
- iterator != dRpakJson["Aliases"].MemberEnd();
- iterator++)
- {
- if (!iterator->name.IsString() || !iterator->value.IsString())
- continue;
+ if (!iterator->name.IsString() || !iterator->value.IsString())
+ continue;
- mod.RpakAliases.insert(std::make_pair(iterator->name.GetString(), iterator->value.GetString()));
- }
+ mod.RpakAliases.insert(std::make_pair(iterator->name.GetString(), iterator->value.GetString()));
}
+ }
- for (fs::directory_entry file : fs::directory_iterator(mod.m_ModDirectory / "paks"))
+ 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")
{
- // ensure we're only loading rpaks
- if (fs::is_regular_file(file) && file.path().extension() == ".rpak")
- {
- std::string pakName(file.path().filename().string());
+ 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());
+ 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();
+ // 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;
+ 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 :/
+ // 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);
+ 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 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);
+ // 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();
+ rpakStream.close();
- // split the starpak reference(s) into strings to hash
- std::string str = "";
- for (int i = 0; i < starpaksSize; i++)
+ // 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)
{
- // if the current char is null, that signals the end of the current starpak path
- if (buf[i] != 0x00)
- {
- str += buf[i];
- }
- else
+ str += buf[i];
+ }
+ else
+ {
+ // only add the string we are making if it isnt empty
+ if (!str.empty())
{
- // 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 = "";
- }
+ 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());
}
+
+ // 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"))
+void ModManager::InstallModKeyValues(Mod& mod)
+{
+ // read keyvalues paths
+ if (fs::exists(mod.m_ModDirectory / "keyvalues"))
+ {
+ for (fs::directory_entry file : fs::recursive_directory_iterator(mod.m_ModDirectory / "keyvalues"))
{
- for (fs::directory_entry file : fs::recursive_directory_iterator(mod.m_ModDirectory / "keyvalues"))
+ if (fs::is_regular_file(file))
{
- 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);
+ }
+ }
+ }
+}
+
+void ModManager::InstallModBinks(Mod& mod)
+{
+ // 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());
+ }
+}
+
+void ModManager::InstallModAudioOverrides(Mod& mod)
+{
+ // 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()))
{
- std::string kvStr =
- g_pModManager->NormaliseModFilePath(file.path().lexically_relative(mod.m_ModDirectory / "keyvalues"));
- mod.KeyValues.emplace(STR_HASH(kvStr), kvStr);
+ spdlog::warn("Mod {} has an invalid audio def {}", mod.Name, file.path().filename().string());
+ continue;
}
}
}
+ }
+}
- // read pdiff
- if (fs::exists(mod.m_ModDirectory / "mod.pdiff"))
+void ModManager::InstallModFileOverrides(Mod& mod)
+{
+ // install all "normal" file overrides (source/vpk filesystem) e.g. files in Northstar.CustomServers/mod
+ if (fs::exists(mod.m_ModDirectory / MOD_OVERRIDE_DIR))
+ {
+ for (fs::directory_entry file : fs::recursive_directory_iterator(mod.m_ModDirectory / MOD_OVERRIDE_DIR))
{
- std::ifstream pdiffStream(mod.m_ModDirectory / "mod.pdiff");
-
- if (!pdiffStream.fail())
+ std::string path = g_pModManager->NormaliseModFilePath(file.path().lexically_relative(mod.m_ModDirectory / MOD_OVERRIDE_DIR));
+ if (file.is_regular_file() && m_ModLoadState->m_ModFiles.find(path) == m_ModLoadState->m_ModFiles.end())
{
- std::stringstream pdiffStringStream;
- while (pdiffStream.peek() != EOF)
- pdiffStringStream << (char)pdiffStream.get();
-
- pdiffStream.close();
-
- mod.Pdiff = pdiffStringStream.str();
+ ModOverrideFile modFile;
+ modFile.m_pOwningMod = &mod;
+ modFile.m_Path = path;
+ modFile.m_tLastWriteTime = fs::last_write_time(file.path()); // need real path for this
+ m_ModLoadState->m_ModFiles.insert(std::make_pair(path, modFile));
}
}
+ }
+}
+#pragma endregion
+
+void ModManager::CheckModFilesForChanges()
+{
+ // normal mod files
+ {
+ // check which file overrides have changed
+ // we need to trigger a reload of a given asset if
+ // a) the asset was overriden previously but has changed owner
+ // b) the asset no longer has any overrides (use vanilla file)
+ // c) the asset was using vanilla file but isn't anymore
- // read bink video paths
- if (fs::exists(mod.m_ModDirectory / "media"))
+ std::vector<ModOverrideFile*> vpChangedFiles;
+
+ // check currently loaded mods for any removed or updated files vs last load
+ for (auto& filePair : m_ModLoadState->m_ModFiles)
{
- 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());
+ auto findFile = m_LastModLoadState->m_ModFiles.find(filePair.first);
+ if (findFile == m_LastModLoadState->m_ModFiles.end() || findFile->second.m_tLastWriteTime != filePair.second.m_tLastWriteTime)
+ vpChangedFiles.push_back(&filePair.second);
}
- // try to load audio
- if (fs::exists(mod.m_ModDirectory / "audio"))
+ // check last load for any files removed
+ for (auto& filePair : m_LastModLoadState->m_ModFiles)
+ if (filePair.second.m_pOwningMod != nullptr &&
+ m_ModLoadState->m_ModFiles.find(filePair.first) == m_ModLoadState->m_ModFiles.end())
+ vpChangedFiles.push_back(&filePair.second);
+
+ for (ModOverrideFile* pChangedFile : vpChangedFiles)
{
- for (fs::directory_entry file : fs::directory_iterator(mod.m_ModDirectory / "audio"))
+ if (IsDedicatedServer())
{
- if (fs::is_regular_file(file) && file.path().extension().string() == ".json")
+ // could check localisation here? but what's the point, localisation shouldn't be in mod fs
+ // if (m_AssetTypesToReload.bLocalisation)
+
+ if (!m_AssetTypesToReload.bAimAssistSettings && !pChangedFile->m_Path.parent_path().compare("cfg/aimassist/"))
+ {
+ m_AssetTypesToReload.bAimAssistSettings = true;
+ continue;
+ }
+
+ if (!m_AssetTypesToReload.bMaterials && !pChangedFile->m_Path.parent_path().compare("materials/"))
+ {
+ m_AssetTypesToReload.bMaterials = true;
+ continue;
+ }
+
+ if (!m_AssetTypesToReload.bUiScript)
{
- if (!g_CustomAudioManager.TryLoadAudioOverride(file.path()))
+
+ // TODO: need to check whether any ui scripts have changed
+
+ if (!pChangedFile->m_Path.parent_path().compare("resource/ui/"))
{
- spdlog::warn("Mod {} has an invalid audio def {}", mod.Name, file.path().filename().string());
+ m_AssetTypesToReload.bUiScript = true;
continue;
}
}
}
- }
- }
- // in a seperate loop because we register mod files in reverse order, since mods loaded later should have their files prioritised
- for (int64_t i = m_LoadedMods.size() - 1; i > -1; i--)
- {
- if (!m_LoadedMods[i].m_bEnabled)
- continue;
+ if (!m_AssetTypesToReload.bModels && !pChangedFile->m_Path.parent_path().compare("models/"))
+ {
+ m_AssetTypesToReload.bModels = true;
+ 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))
+ // could also check this but no point as it should only be changed from mod keyvalues
+ // if (!m_AssetTypesToReload.bPlaylists && !pChangedFile->m_Path.compare("playlists_v2.txt"))
+
+ // we also check these on change of mod keyvalues
+ if (!m_AssetTypesToReload.bWeaponSettings && !pChangedFile->m_Path.parent_path().compare("scripts/weapons/"))
{
- 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));
- }
+ m_AssetTypesToReload.bWeaponSettings = true;
+ continue;
+ }
+
+ if (!m_AssetTypesToReload.bPlayerSettings && !pChangedFile->m_Path.parent_path().compare("scripts/players/"))
+ {
+ m_AssetTypesToReload.bPlayerSettings = true;
+ continue;
+ }
+
+ // maybe also aibehaviour?
+ if (!m_AssetTypesToReload.bAiSettings && !pChangedFile->m_Path.parent_path().compare("scripts/aisettings/"))
+ {
+ m_AssetTypesToReload.bAiSettings = true;
+ continue;
+ }
+
+ if (!m_AssetTypesToReload.bDamageDefs && !pChangedFile->m_Path.parent_path().compare("scripts/damage/"))
+ {
+ m_AssetTypesToReload.bDamageDefs = true;
+ continue;
}
}
}
- // build modinfo obj for masterserver
- rapidjson_document modinfoDoc;
- auto& alloc = modinfoDoc.GetAllocator();
- modinfoDoc.SetObject();
- modinfoDoc.AddMember("Mods", rapidjson::kArrayType, alloc);
+ // keyvalues
+
+ //if (!m_AssetTypesToReload.bWeaponSettings && kvStr.compare("scripts/weapons/"))
+ //{
+ // m_AssetTypesToReload.bWeaponSettings = true;
+ // continue;
+ //}
+ //
+ //if (!m_AssetTypesToReload.bPlayerSettings && kvStr.compare("scripts/players/"))
+ //{
+ // m_AssetTypesToReload.bPlayerSettings = true;
+ // continue;
+ //}
+ //
+ //// maybe also aibehaviour?
+ //if (!m_AssetTypesToReload.bAiSettings && kvStr.compare("scripts/aisettings/"))
+ //{
+ // m_AssetTypesToReload.bAiSettings = true;
+ // continue;
+ //}
+ //
+ //if (!m_AssetTypesToReload.bDamageDefs && kvStr.compare("scripts/damage/"))
+ //{
+ // m_AssetTypesToReload.bDamageDefs = true;
+ // continue;
+ //}
+}
- int currentModIndex = 0;
- for (Mod& mod : m_LoadedMods)
+void ModManager::ReloadNecessaryModAssets()
+{
+ std::vector<std::string> vReloadCommands;
+
+ if (m_AssetTypesToReload.bUiScript)
+ vReloadCommands.push_back("uiscript_reset");
+
+ if (m_AssetTypesToReload.bLocalisation)
+ vReloadCommands.push_back("reload_localization");
+
+ // after we reload_localization, we need to loadPlaylists, to keep playlist localisation
+ if (m_AssetTypesToReload.bPlaylists || m_AssetTypesToReload.bLocalisation)
+ vReloadCommands.push_back("loadPlaylists");
+
+ if (m_AssetTypesToReload.bAimAssistSettings)
+ vReloadCommands.push_back("ReloadAimAssistSettings");
+
+ // need to reimplement mat_reloadmaterials for this
+ //if (m_AssetTypesToReload.bMaterials)
+ // R2::Cbuf_AddText(R2::Cbuf_GetCurrentPlayer(), "mat_reloadmaterials", R2::cmd_source_t::kCommandSrcCode);
+
+ //if (m_AssetTypesToReload.bWeaponSettings)
+ //if (m_AssetTypesToReload.bPlayerSettings)
+ //if (m_AssetTypesToReload.bAiSettings)
+ //if (m_AssetTypesToReload.bDamageDefs)
+
+ if (m_AssetTypesToReload.bModels)
+ spdlog::warn("Need to reload models but can't without a restart!");
+
+ for (std::string& sReloadCommand : vReloadCommands)
{
- if (!mod.m_bEnabled || (!mod.RequiredOnClient && !mod.Pdiff.size()))
- continue;
+ spdlog::info("Executing command {} for asset reload", sReloadCommand);
+ R2::Cbuf_AddText(R2::Cbuf_GetCurrentPlayer(), sReloadCommand.c_str(), R2::cmd_source_t::kCommandSrcCode);
+ }
- 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());
+ R2::Cbuf_Execute();
+}
- currentModIndex++;
+void ModManager::InstallMods()
+{
+ for (Mod& mod : GetMods() | FilterEnabled)
+ {
+ InstallModCvars(mod);
+ InstallModVpks(mod);
+ InstallModRpaks(mod);
+ InstallModKeyValues(mod);
+ InstallModBinks(mod);
+ InstallModAudioOverrides(mod);
}
- rapidjson::StringBuffer buffer;
- buffer.Clear();
- rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
- modinfoDoc.Accept(writer);
- g_pMasterServerManager->m_sOwnModInfoJson = std::string(buffer.GetString());
+ // in a seperate loop because we register mod files in reverse order, since mods loaded later should have their files prioritised
+ for (Mod& mod : GetMods() | FilterEnabled | std::views::reverse)
+ InstallModFileOverrides(mod);
- m_bHasLoadedMods = true;
+ if (m_bHasLoadedMods) // only reload assets after initial load
+ {
+ CheckModFilesForChanges();
+ ReloadNecessaryModAssets();
+ }
+}
+
+void ModManager::SaveEnabledMods()
+{
+ // write from scratch every time, don't include unnecessary mods
+ rapidjson_document enabledModsCfg;
+ enabledModsCfg.SetObject();
+
+ // add values
+ for (Mod& mod : GetMods())
+ enabledModsCfg.AddMember(rapidjson_document::StringRefType(mod.Name.c_str()), mod.m_bEnabled, enabledModsCfg.GetAllocator());
+
+ // write
+ std::ofstream sWriteStream(GetNorthstarPrefix() / "enabledmods.json");
+ rapidjson::OStreamWrapper sWriteStreamWrapper(sWriteStream);
+ rapidjson::PrettyWriter<rapidjson::OStreamWrapper> writer(sWriteStreamWrapper);
+ enabledModsCfg.Accept(writer);
}
void ModManager::UnloadMods()
{
+ // save last state so we know what we need to reload
m_LastModLoadState = m_ModLoadState;
+ m_ModLoadState = new ModLoadState;
+
+ // reset assets to reload
+ m_AssetTypesToReload.bUiScript = false;
+ m_AssetTypesToReload.bLocalisation = false;
+ m_AssetTypesToReload.bPlaylists = false;
+ m_AssetTypesToReload.bAimAssistSettings = false;
+ m_AssetTypesToReload.bMaterials = false;
+ m_AssetTypesToReload.bRPaks = false;
+ m_AssetTypesToReload.bWeaponSettings = false;
+ m_AssetTypesToReload.bPlayerSettings = false;
+ m_AssetTypesToReload.bAiSettings = false;
+ m_AssetTypesToReload.bDamageDefs = false;
+ m_AssetTypesToReload.bModels = false;
// 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));
-
- // 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();
+ // TODO: remove, should only reload required overrides, and don't do it here
+ g_CustomAudioManager.ClearAudioOverrides();
}
std::string ModManager::NormaliseModFilePath(const fs::path path)
@@ -801,10 +1093,10 @@ void ModManager::CompileAssetsForFile(const char* filename)
if (fileHash == m_hScriptsRsonHash)
BuildScriptsRson();
- else if (fileHash == m_hPdefHash)
- {
- // BuildPdef(); todo
- }
+ //else if (fileHash == m_hPdefHash)
+ //{
+ // // BuildPdef(); todo
+ //}
else if (fileHash == m_hKBActHash)
BuildKBActionsList();
else
@@ -831,15 +1123,15 @@ void ConCommand_reload_mods(const CCommand& args)
fs::path GetModFolderPath()
{
- return fs::path(GetNorthstarPrefix() + MOD_FOLDER_SUFFIX);
+ return GetNorthstarPrefix() / MOD_FOLDER_SUFFIX;
}
fs::path GetRemoteModFolderPath()
{
- return fs::path(GetNorthstarPrefix() + REMOTE_MOD_FOLDER_SUFFIX);
+ return GetNorthstarPrefix() / REMOTE_MOD_FOLDER_SUFFIX;
}
fs::path GetCompiledAssetsPath()
{
- return fs::path(GetNorthstarPrefix() + COMPILED_ASSETS_SUFFIX);
+ return GetNorthstarPrefix() / COMPILED_ASSETS_SUFFIX;
}
ON_DLL_LOAD_RELIESON("engine.dll", ModManager, (ConCommand, MasterServer), (CModule module))
diff --git a/NorthstarDLL/mods/modmanager.h b/NorthstarDLL/mods/modmanager.h
index d44a202f..0bb696e8 100644
--- a/NorthstarDLL/mods/modmanager.h
+++ b/NorthstarDLL/mods/modmanager.h
@@ -7,15 +7,15 @@
#include <string>
#include <vector>
#include <filesystem>
+#include <unordered_set>
-const std::string MOD_FOLDER_SUFFIX = "/mods";
-const std::string REMOTE_MOD_FOLDER_SUFFIX = "/runtime/remote/mods";
+const std::string MOD_FOLDER_SUFFIX = "mods";
+const std::string REMOTE_MOD_FOLDER_SUFFIX = "runtime/remote/mods";
const fs::path MOD_OVERRIDE_DIR = "mod";
-const std::string COMPILED_ASSETS_SUFFIX = "/runtime/compiled";
+const std::string COMPILED_ASSETS_SUFFIX = "runtime/compiled";
struct ModConVar
{
- public:
std::string Name;
std::string DefaultValue;
std::string HelpString;
@@ -24,7 +24,6 @@ struct ModConVar
struct ModConCommand
{
- public:
std::string Name;
std::string Function;
std::string HelpString;
@@ -34,7 +33,6 @@ struct ModConCommand
struct ModScriptCallback
{
- public:
ScriptContext Context;
// called before the codecallback is executed
@@ -47,7 +45,6 @@ struct ModScriptCallback
struct ModScript
{
- public:
std::string Path;
std::string RunOn;
@@ -57,27 +54,24 @@ struct ModScript
// these are pretty much identical, could refactor to use the same stuff?
struct ModVPKEntry
{
- public:
bool m_bAutoLoad;
std::string m_sVpkPath;
};
struct ModRpakEntry
{
- public:
bool m_bAutoLoad;
std::string m_sPakName;
std::string m_sLoadAfterPak;
};
-class Mod
+struct Mod
{
- public:
// runtime stuff
bool m_bEnabled = true;
bool m_bWasReadSuccessfully = false;
fs::path m_ModDirectory;
- // bool m_bIsRemote;
+ bool m_bRemote; // whether the mod was automatically downloaded
// mod.json stuff:
@@ -109,7 +103,8 @@ class Mod
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
+
+ // todo audio override struct
std::vector<ModRpakEntry> Rpaks;
std::unordered_map<std::string, std::string>
@@ -121,22 +116,14 @@ class Mod
std::unordered_map<std::string, std::string> DependencyConstants;
public:
- Mod(fs::path modPath, char* jsonBuf);
+ Mod(fs::path modPath, std::string sJson, bool bRemote);
};
struct ModOverrideFile
{
- public:
Mod* m_pOwningMod; // don't need to explicitly clean this up
fs::path m_Path;
-};
-
-class ModLoadState
-{
- public:
- std::vector<Mod> m_LoadedMods;
- std::unordered_map<std::string, ModOverrideFile> m_ModFiles;
- std::unordered_map<std::string, std::string> m_DependencyConstants;
+ fs::file_time_type m_tLastWriteTime;
};
class ModManager
@@ -144,45 +131,100 @@ class ModManager
private:
bool m_bHasLoadedMods = false;
bool m_bHasEnabledModsCfg;
- rapidjson_document m_EnabledModsCfg;
// precalculated hashes
size_t m_hScriptsRsonHash;
size_t m_hPdefHash;
size_t m_hKBActHash;
- public:
- ModLoadState m_LastModLoadState;
- ModLoadState m_ModLoadState;
+ void LoadModDefinitions();
+ void SaveEnabledMods();
+ void InstallMods();
+
+ // mod installation funcs
+ void InstallModCvars(Mod& mod);
+ void InstallModVpks(Mod& mod);
+ void InstallModRpaks(Mod& mod);
+ void InstallModKeyValues(Mod& mod);
+ void InstallModBinks(Mod& mod);
+ void InstallModAudioOverrides(Mod& mod);
+
+ void InstallModFileOverrides(Mod& mod);
+
+ void UnloadMods();
+
+ std::unordered_set<ConVar*> m_RegisteredModConVars;
+ std::unordered_set<ConCommand*> m_RegisteredModConCommands;
+
+
+ struct
+ {
+ // assets types we need to reload completely after mods are reloaded (can't reload individually)
+ bool bUiScript = false; // also includes .menu files
+ bool bLocalisation = false;
+ bool bPlaylists = false;
+ bool bAimAssistSettings = false;
+ bool bMaterials = false; // vmts
+ bool bWeaponSettings = false;
+ bool bPlayerSettings = false;
+ bool bAiSettings = false;
+ bool bDamageDefs = false; // damagedefs
+
+ // can't actually reload this atm, just print a warning (todo, could maybe restart client to ensure loaded?)
+ bool bModels = false;
+
+ bool bRPaks = false;
+
+ // assets that we can reload individually
+ //std::vector<ModAudioOverride> vAudioOverrides
+ } m_AssetTypesToReload;
+
+ void CheckModFilesForChanges();
+ void ReloadNecessaryModAssets();
+
+
+ struct ModLoadState
+ {
+ std::vector<Mod> m_LoadedMods;
+ std::unordered_map<std::string, ModOverrideFile> m_ModFiles;
+ std::unordered_map<std::string, std::string> m_DependencyConstants;
+ };
+
+ // unfortunately need to be ptrs, so we can copy m_ModLoadState => m_LastModLoadState
+ // without ptrs to any elements (only ModOverrideFile::m_pOwningMod atm) decaying
+ // unfortunately means we need to manually manage memory of them, but oh well
+ ModLoadState* m_LastModLoadState;
+ ModLoadState* m_ModLoadState;
public:
ModManager();
void LoadMods();
- void UnloadMods();
std::string NormaliseModFilePath(const fs::path path);
void CompileAssetsForFile(const char* filename);
// getters
inline std::vector<Mod>& GetMods()
{
- return m_ModLoadState.m_LoadedMods;
+ return m_ModLoadState->m_LoadedMods;
};
inline std::unordered_map<std::string, ModOverrideFile>& GetModFiles()
{
- return m_ModLoadState.m_ModFiles;
+ return m_ModLoadState->m_ModFiles;
};
inline std::unordered_map<std::string, std::string>& GetDependencyConstants()
{
- return m_ModLoadState.m_DependencyConstants;
+ return m_ModLoadState->m_DependencyConstants;
};
// compile asset type stuff, these are done in files under runtime/compiled/
void BuildScriptsRson();
void TryBuildKeyValues(const char* filename);
- void BuildPdef();
void BuildKBActionsList();
+
+ // for std::views::filter, e.g. for (Mod& mod : g_pModManager::GetMods() | ModManager::FilterEnabled)
+ static inline constexpr auto FilterEnabled = std::views::filter([](Mod& mod) { return mod.m_bEnabled; });
};
fs::path GetModFolderPath();