From 93871f36c2e36996d880aa68d6445c7d3802c100 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 13 Feb 2023 13:51:40 +0000 Subject: refactor modloading and implement initial automatic reload code --- NorthstarDLL/client/clientvideooverrides.cpp | 5 +- NorthstarDLL/client/modlocalisation.cpp | 14 +- NorthstarDLL/config/profile.cpp | 4 +- NorthstarDLL/config/profile.h | 4 +- NorthstarDLL/core/convar/concommand.cpp | 8 +- NorthstarDLL/core/convar/concommand.h | 4 +- NorthstarDLL/core/filesystem/filesystem.cpp | 5 +- NorthstarDLL/core/filesystem/rpakfilesystem.cpp | 15 +- NorthstarDLL/dllmain.cpp | 2 +- NorthstarDLL/logging/crashhandler.cpp | 11 +- NorthstarDLL/logging/logging.cpp | 2 +- NorthstarDLL/mods/compiled/kb_act.cpp | 5 +- NorthstarDLL/mods/compiled/modkeyvalues.cpp | 5 +- NorthstarDLL/mods/compiled/modscriptsrson.cpp | 5 +- NorthstarDLL/mods/modmanager.cpp | 894 ++++++++++++++++-------- NorthstarDLL/mods/modmanager.h | 106 ++- NorthstarDLL/scripts/client/scriptmodmenu.cpp | 4 +- NorthstarDLL/server/auth/bansystem.cpp | 16 +- NorthstarDLL/squirrel/squirrel.cpp | 18 +- 19 files changed, 715 insertions(+), 412 deletions(-) diff --git a/NorthstarDLL/client/clientvideooverrides.cpp b/NorthstarDLL/client/clientvideooverrides.cpp index d24dfa14..a6178cfc 100644 --- a/NorthstarDLL/client/clientvideooverrides.cpp +++ b/NorthstarDLL/client/clientvideooverrides.cpp @@ -12,11 +12,8 @@ void*, __fastcall, (const char* path, uint32_t flags)) // figure out which mod is handling the bink Mod* pFileOwner = nullptr; - for (Mod& mod : g_pModManager->GetMods()) + for (Mod& mod : g_pModManager->GetMods() | ModManager::FilterEnabled) { - if (!mod.m_bEnabled) - continue; - if (std::find(mod.BinkVideos.begin(), mod.BinkVideos.end(), filename) != mod.BinkVideos.end()) pFileOwner = &mod; } diff --git a/NorthstarDLL/client/modlocalisation.cpp b/NorthstarDLL/client/modlocalisation.cpp index a2387e64..16fff5f7 100644 --- a/NorthstarDLL/client/modlocalisation.cpp +++ b/NorthstarDLL/client/modlocalisation.cpp @@ -25,10 +25,9 @@ void, __fastcall, (void* pVguiLocalize)) // clang-format on { // load all mod localization manually, so we keep track of all files, not just previously loaded ones - for (Mod& mod : g_pModManager->GetMods()) - if (mod.m_bEnabled) - for (std::string& localisationFile : mod.LocalisationFiles) - CLocalize__AddFile(g_pVguiLocalize, localisationFile.c_str(), nullptr, false); + for (Mod& mod : g_pModManager->GetMods() | ModManager::FilterEnabled) + for (std::string& localisationFile : mod.LocalisationFiles) + CLocalize__AddFile(g_pVguiLocalize, localisationFile.c_str(), nullptr, false); spdlog::info("reloading localization..."); CLocalize__ReloadLocalizationFiles(pVguiLocalize); @@ -43,10 +42,9 @@ void, __fastcall, (void* self)) // previously we did this in CLocalize::AddFile, but for some reason it won't properly overwrite localization from // files loaded previously if done there, very weird but this works so whatever - for (Mod& mod : g_pModManager->GetMods()) - if (mod.m_bEnabled) - for (std::string& localisationFile : mod.LocalisationFiles) - CLocalize__AddFile(g_pVguiLocalize, localisationFile.c_str(), nullptr, false); + for (Mod& mod : g_pModManager->GetMods() | ModManager::FilterEnabled) + for (std::string& localisationFile : mod.LocalisationFiles) + CLocalize__AddFile(g_pVguiLocalize, localisationFile.c_str(), nullptr, false); } ON_DLL_LOAD_CLIENT("localize.dll", Localize, (CModule module)) diff --git a/NorthstarDLL/config/profile.cpp b/NorthstarDLL/config/profile.cpp index 08c91208..80ddb59f 100644 --- a/NorthstarDLL/config/profile.cpp +++ b/NorthstarDLL/config/profile.cpp @@ -2,7 +2,7 @@ #include "dedicated/dedicated.h" #include -std::string GetNorthstarPrefix() +fs::path GetNorthstarPrefix() { return NORTHSTAR_FOLDER_PREFIX; } @@ -39,5 +39,5 @@ void InitialiseNorthstarPrefix() // set the console title to show the current profile // dont do this on dedi as title contains useful information on dedi and setting title breaks it as well if (!IsDedicatedServer()) - SetConsoleTitleA((std::string("NorthstarLauncher | ") + NORTHSTAR_FOLDER_PREFIX).c_str()); + SetConsoleTitleA((fs::path("NorthstarLauncher | ") / NORTHSTAR_FOLDER_PREFIX).string().c_str()); } diff --git a/NorthstarDLL/config/profile.h b/NorthstarDLL/config/profile.h index 9f3087fb..8e9d2ce9 100644 --- a/NorthstarDLL/config/profile.h +++ b/NorthstarDLL/config/profile.h @@ -1,7 +1,7 @@ #pragma once #include -static std::string NORTHSTAR_FOLDER_PREFIX; +static fs::path NORTHSTAR_FOLDER_PREFIX; void InitialiseNorthstarPrefix(); -std::string GetNorthstarPrefix(); +fs::path GetNorthstarPrefix(); diff --git a/NorthstarDLL/core/convar/concommand.cpp b/NorthstarDLL/core/convar/concommand.cpp index 67c867f8..fb5e137b 100644 --- a/NorthstarDLL/core/convar/concommand.cpp +++ b/NorthstarDLL/core/convar/concommand.cpp @@ -124,16 +124,18 @@ typedef void (*ConCommandConstructorType)( ConCommand* newCommand, const char* name, FnCommandCallback_t callback, const char* helpString, int flags, void* parent); ConCommandConstructorType ConCommandConstructor; -void RegisterConCommand(const char* name, FnCommandCallback_t callback, const char* helpString, int flags) +ConCommand* RegisterConCommand(const char* name, FnCommandCallback_t callback, const char* helpString, int flags) { spdlog::info("Registering ConCommand {}", name); // no need to free this ever really, it should exist as long as game does ConCommand* newCommand = new ConCommand; ConCommandConstructor(newCommand, name, callback, helpString, flags, nullptr); + + return newCommand; } -void RegisterConCommand( +ConCommand* RegisterConCommand( const char* name, FnCommandCallback_t callback, const char* helpString, int flags, FnCommandCompletionCallback completionCallback) { spdlog::info("Registering ConCommand {}", name); @@ -142,6 +144,8 @@ void RegisterConCommand( ConCommand* newCommand = new ConCommand; ConCommandConstructor(newCommand, name, callback, helpString, flags, nullptr); newCommand->m_pCompletionCallback = completionCallback; + + return newCommand; } ON_DLL_LOAD("engine.dll", ConCommand, (CModule module)) diff --git a/NorthstarDLL/core/convar/concommand.h b/NorthstarDLL/core/convar/concommand.h index c170c6a5..81c3cf6d 100644 --- a/NorthstarDLL/core/convar/concommand.h +++ b/NorthstarDLL/core/convar/concommand.h @@ -134,6 +134,6 @@ class ConCommand : public ConCommandBase int unk1; // 0x005C }; // Size: 0x0060 -void RegisterConCommand(const char* name, void (*callback)(const CCommand&), const char* helpString, int flags); -void RegisterConCommand( +ConCommand* RegisterConCommand(const char* name, void (*callback)(const CCommand&), const char* helpString, int flags); +ConCommand* RegisterConCommand( const char* name, void (*callback)(const CCommand&), const char* helpString, int flags, FnCommandCompletionCallback completionCallback); diff --git a/NorthstarDLL/core/filesystem/filesystem.cpp b/NorthstarDLL/core/filesystem/filesystem.cpp index 3cecc208..4080f911 100644 --- a/NorthstarDLL/core/filesystem/filesystem.cpp +++ b/NorthstarDLL/core/filesystem/filesystem.cpp @@ -146,11 +146,8 @@ HOOK(MountVPKHook, MountVPK, VPKData*, , (IFileSystem * fileSystem, const char* NS::log::fs->info("MountVPK {}", pVpkPath); VPKData* ret = MountVPK(fileSystem, pVpkPath); - for (Mod mod : g_pModManager->GetMods()) + for (Mod mod : g_pModManager->GetMods() | ModManager::FilterEnabled) { - if (!mod.m_bEnabled) - continue; - for (ModVPKEntry& vpkEntry : mod.Vpks) { // if we're autoloading, just load no matter what diff --git a/NorthstarDLL/core/filesystem/rpakfilesystem.cpp b/NorthstarDLL/core/filesystem/rpakfilesystem.cpp index fa71c56f..e8c4232d 100644 --- a/NorthstarDLL/core/filesystem/rpakfilesystem.cpp +++ b/NorthstarDLL/core/filesystem/rpakfilesystem.cpp @@ -89,11 +89,8 @@ void* PakLoadManager::LoadFile(const char* path) void HandlePakAliases(char** map) { // convert the pak being loaded to it's aliased one, e.g. aliasing mp_hub_timeshift => sp_hub_timeshift - for (Mod& mod : g_pModManager->GetMods() | std::views::reverse) + for (Mod& mod : g_pModManager->GetMods() | ModManager::FilterEnabled | std::views::reverse) { - if (!mod.m_bEnabled) - continue; - if (mod.RpakAliases.find(*map) != mod.RpakAliases.end()) { *map = &mod.RpakAliases[*map][0]; @@ -105,11 +102,8 @@ void HandlePakAliases(char** map) void LoadPreloadPaks() { // note, loading from ./ is necessary otherwise paks will load from gamedir/r2/paks - for (Mod& mod : g_pModManager->GetMods()) + for (Mod& mod : g_pModManager->GetMods() | ModManager::FilterEnabled) { - if (!mod.m_bEnabled) - continue; - // need to get a relative path of mod to mod folder fs::path modPakPath("./" / mod.m_ModDirectory / "paks"); @@ -122,11 +116,8 @@ void LoadPreloadPaks() void LoadPostloadPaks(const char* pPath) { // note, loading from ./ is necessary otherwise paks will load from gamedir/r2/paks - for (Mod& mod : g_pModManager->GetMods()) + for (Mod& mod : g_pModManager->GetMods() | ModManager::FilterEnabled) { - if (!mod.m_bEnabled) - continue; - // need to get a relative path of mod to mod folder fs::path modPakPath("./" / mod.m_ModDirectory / "paks"); diff --git a/NorthstarDLL/dllmain.cpp b/NorthstarDLL/dllmain.cpp index a697da5b..91fb94a1 100644 --- a/NorthstarDLL/dllmain.cpp +++ b/NorthstarDLL/dllmain.cpp @@ -45,7 +45,7 @@ bool LoadPlugins() std::vector paths; - std::string pluginPath = GetNorthstarPrefix() + "/plugins"; + fs::path pluginPath = GetNorthstarPrefix() / "plugins"; if (!fs::exists(pluginPath)) { spdlog::warn("Could not find a plugins directory. Skipped loading plugins"); diff --git a/NorthstarDLL/logging/crashhandler.cpp b/NorthstarDLL/logging/crashhandler.cpp index 38d9312f..36f8b714 100644 --- a/NorthstarDLL/logging/crashhandler.cpp +++ b/NorthstarDLL/logging/crashhandler.cpp @@ -33,16 +33,15 @@ void PrintExceptionLog(ExceptionLog& exc) if (g_pModManager) { spdlog::error("Loaded mods: "); - for (const Mod& mod : g_pModManager->GetMods()) - { - if (mod.m_bEnabled) - spdlog::error("{} {}", mod.Name, mod.Version); - } + for (const Mod& mod : g_pModManager->GetMods() | ModManager::FilterEnabled) + spdlog::error("{} {}", mod.Name, mod.Version); } + spdlog::error(exc.cause); // If this was a runtime error, print the message if (exc.runtimeInfo.length() != 0) spdlog::error("\"{}\"", exc.runtimeInfo); + spdlog::error("At: {} + {}", exc.trace[0].name, exc.trace[0].relativeAddress); spdlog::error(""); spdlog::error("Stack trace:"); @@ -235,7 +234,7 @@ void CreateMiniDump(EXCEPTION_POINTERS* exceptionInfo) time_t time = std::time(nullptr); tm currentTime = *std::localtime(&time); std::stringstream stream; - stream << std::put_time(¤tTime, (GetNorthstarPrefix() + "/logs/nsdump%Y-%m-%d %H-%M-%S.dmp").c_str()); + stream << std::put_time(¤tTime, (GetNorthstarPrefix() / "logs/nsdump%Y-%m-%d %H-%M-%S.dmp").string().c_str()); auto hMinidumpFile = CreateFileA(stream.str().c_str(), GENERIC_WRITE, FILE_SHARE_READ, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0); if (hMinidumpFile) diff --git a/NorthstarDLL/logging/logging.cpp b/NorthstarDLL/logging/logging.cpp index f88928e1..edf122be 100644 --- a/NorthstarDLL/logging/logging.cpp +++ b/NorthstarDLL/logging/logging.cpp @@ -46,7 +46,7 @@ void CreateLogFiles() tm currentTime = *std::localtime(&time); std::stringstream stream; - stream << std::put_time(¤tTime, (GetNorthstarPrefix() + "/logs/nslog%Y-%m-%d %H-%M-%S.txt").c_str()); + stream << std::put_time(¤tTime, (GetNorthstarPrefix() / "logs/nslog%Y-%m-%d %H-%M-%S.txt").string().c_str()); auto sink = std::make_shared(stream.str(), false); sink->set_pattern("[%H:%M:%S] [%n] [%l] %v"); for (auto& logger : loggers) diff --git a/NorthstarDLL/mods/compiled/kb_act.cpp b/NorthstarDLL/mods/compiled/kb_act.cpp index 109dc910..ecfb8388 100644 --- a/NorthstarDLL/mods/compiled/kb_act.cpp +++ b/NorthstarDLL/mods/compiled/kb_act.cpp @@ -16,11 +16,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 ff2fafc1..4672d195 100644 --- a/NorthstarDLL/mods/compiled/modkeyvalues.cpp +++ b/NorthstarDLL/mods/compiled/modkeyvalues.cpp @@ -24,11 +24,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 292e9c99..5e837a60 100644 --- a/NorthstarDLL/mods/compiled/modscriptsrson.cpp +++ b/NorthstarDLL/mods/compiled/modscriptsrson.cpp @@ -16,11 +16,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 04484fad..4c05c0ae 100644 --- a/NorthstarDLL/mods/modmanager.cpp +++ b/NorthstarDLL/mods/modmanager.cpp @@ -2,10 +2,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" @@ -19,14 +21,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(jsonBuf); + modJson.Parse(sJson); // fail if parse error if (modJson.HasParseError()) @@ -95,8 +98,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(); @@ -136,12 +137,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()); @@ -159,9 +159,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 @@ -170,6 +168,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); } } @@ -286,6 +291,9 @@ ModManager::ModManager() ); m_hKBActHash = STR_HASH("scripts\\kb_act.lst"); + m_LastModLoadState = nullptr; + m_ModLoadState = new ModLoadState; + LoadMods(); } @@ -310,12 +318,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()) { @@ -341,81 +349,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 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( - 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 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( + enabledModsStringStream.str().c_str()); + + bHasEnabledModsCfg = enabledModsCfg.IsObject(); } + } + + // get mod directories for both local and remote mods + std::vector> 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) { @@ -425,7 +503,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()); @@ -433,353 +511,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( - 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( + 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( - 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( + 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 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 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 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 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 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 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 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 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) @@ -800,10 +1092,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 @@ -830,15 +1122,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 #include #include +#include -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 Vpks; std::unordered_map KeyValues; std::vector BinkVideos; - std::string Pdiff; // only need one per mod + + // todo audio override struct std::vector Rpaks; std::unordered_map @@ -121,22 +116,14 @@ class Mod std::unordered_map 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 m_LoadedMods; - std::unordered_map m_ModFiles; - std::unordered_map 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 m_RegisteredModConVars; + std::unordered_set 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 vAudioOverrides + } m_AssetTypesToReload; + + void CheckModFilesForChanges(); + void ReloadNecessaryModAssets(); + + + struct ModLoadState + { + std::vector m_LoadedMods; + std::unordered_map m_ModFiles; + std::unordered_map 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& GetMods() { - return m_ModLoadState.m_LoadedMods; + return m_ModLoadState->m_LoadedMods; }; inline std::unordered_map& GetModFiles() { - return m_ModLoadState.m_ModFiles; + return m_ModLoadState->m_ModFiles; }; inline std::unordered_map& 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(); diff --git a/NorthstarDLL/scripts/client/scriptmodmenu.cpp b/NorthstarDLL/scripts/client/scriptmodmenu.cpp index c6c53c7f..9d024936 100644 --- a/NorthstarDLL/scripts/client/scriptmodmenu.cpp +++ b/NorthstarDLL/scripts/client/scriptmodmenu.cpp @@ -145,9 +145,9 @@ ADD_SQFUNC( { if (!mod.Name.compare(modName)) { - for (ModConVar* cvar : mod.ConVars) + for (ModConVar& cvar : mod.ConVars) { - g_pSquirrel->pushstring(sqvm, cvar->Name.c_str()); + g_pSquirrel->pushstring(sqvm, cvar.Name.c_str()); g_pSquirrel->arrayappend(sqvm, -2); } diff --git a/NorthstarDLL/server/auth/bansystem.cpp b/NorthstarDLL/server/auth/bansystem.cpp index a438450f..c7c94c1c 100644 --- a/NorthstarDLL/server/auth/bansystem.cpp +++ b/NorthstarDLL/server/auth/bansystem.cpp @@ -15,7 +15,7 @@ ServerBanSystem* g_pBanSystem; void ServerBanSystem::OpenBanlist() { - std::ifstream banlistStream(GetNorthstarPrefix() + "/banlist.txt"); + std::ifstream banlistStream(GetNorthstarPrefix() / "banlist.txt"); if (!banlistStream.fail()) { @@ -45,12 +45,12 @@ void ServerBanSystem::OpenBanlist() } // open write stream for banlist // dont do this to allow for all time access - // m_sBanlistStream.open(GetNorthstarPrefix() + "/banlist.txt", std::ofstream::out | std::ofstream::binary | std::ofstream::app); + // m_sBanlistStream.open(GetNorthstarPrefix() / "banlist.txt", std::ofstream::out | std::ofstream::binary | std::ofstream::app); } void ServerBanSystem::ReloadBanlist() { - std::ifstream fsBanlist(GetNorthstarPrefix() + "/banlist.txt"); + std::ifstream fsBanlist(GetNorthstarPrefix() / "banlist.txt"); if (!fsBanlist.fail()) { @@ -70,18 +70,18 @@ void ServerBanSystem::ClearBanlist() // reopen the file, don't provide std::ofstream::app so it clears on open m_sBanlistStream.close(); - m_sBanlistStream.open(GetNorthstarPrefix() + "/banlist.txt", std::ofstream::out | std::ofstream::binary); + m_sBanlistStream.open(GetNorthstarPrefix() / "banlist.txt", std::ofstream::out | std::ofstream::binary); m_sBanlistStream.close(); } void ServerBanSystem::BanUID(uint64_t uid) { // checking if last char is \n to make sure uids arent getting fucked - std::ifstream fsBanlist(GetNorthstarPrefix() + "/banlist.txt"); + std::ifstream fsBanlist(GetNorthstarPrefix() / "banlist.txt"); std::string content((std::istreambuf_iterator(fsBanlist)), (std::istreambuf_iterator())); fsBanlist.close(); - m_sBanlistStream.open(GetNorthstarPrefix() + "/banlist.txt", std::ofstream::out | std::ofstream::binary | std::ofstream::app); + m_sBanlistStream.open(GetNorthstarPrefix() / "banlist.txt", std::ofstream::out | std::ofstream::binary | std::ofstream::app); if (content.back() != '\n') m_sBanlistStream << std::endl; @@ -100,7 +100,7 @@ void ServerBanSystem::UnbanUID(uint64_t uid) m_vBannedUids.erase(findResult); std::vector banlistText; - std::ifstream fs_readBanlist(GetNorthstarPrefix() + "/banlist.txt"); + std::ifstream fs_readBanlist(GetNorthstarPrefix() / "banlist.txt"); if (!fs_readBanlist.fail()) { @@ -162,7 +162,7 @@ void ServerBanSystem::UnbanUID(uint64_t uid) // open write stream for banlist // without append so we clear the file if (m_sBanlistStream.is_open()) m_sBanlistStream.close(); - m_sBanlistStream.open(GetNorthstarPrefix() + "/banlist.txt", std::ofstream::out | std::ofstream::binary); + m_sBanlistStream.open(GetNorthstarPrefix() / "banlist.txt", std::ofstream::out | std::ofstream::binary); for (std::string updatedLine : banlistText) m_sBanlistStream << updatedLine << std::endl; diff --git a/NorthstarDLL/squirrel/squirrel.cpp b/NorthstarDLL/squirrel/squirrel.cpp index 4e64aecc..06ba445c 100644 --- a/NorthstarDLL/squirrel/squirrel.cpp +++ b/NorthstarDLL/squirrel/squirrel.cpp @@ -193,11 +193,8 @@ template void SquirrelManager::VMCreated(CSquir { bool bWasFound = false; - for (Mod& dependency : g_pModManager->GetMods()) + for (Mod& dependency : g_pModManager->GetMods() | ModManager::FilterEnabled) { - if (!dependency.m_bEnabled) - continue; - if (dependency.Name == pair.second) { bWasFound = true; @@ -207,6 +204,7 @@ template void SquirrelManager::VMCreated(CSquir defconst(m_pSQVM, pair.first.c_str(), bWasFound); } + g_pSquirrel->messageBuffer = new SquirrelMessageBuffer(); } @@ -217,7 +215,7 @@ template void SquirrelManager::VMDestroyed() { NS::log::squirrel_logger()->info("Calling Destroy callbacks for all loaded mods."); - for (const Mod& loadedMod : g_pModManager->m_LoadedMods) + for (const Mod& loadedMod : g_pModManager->GetMods()) { for (const ModScript& script : loadedMod.Scripts) { @@ -471,11 +469,8 @@ template bool __fastcall CallScriptInitCallbackHook(void if (bShouldCallCustomCallbacks) { - for (Mod& mod : g_pModManager->GetMods()) + for (Mod& mod : g_pModManager->GetMods() | ModManager::FilterEnabled) { - if (!mod.m_bEnabled) - continue; - for (ModScript script : mod.Scripts) { for (ModScriptCallback modCallback : script.Callbacks) @@ -498,11 +493,8 @@ template bool __fastcall CallScriptInitCallbackHook(void // run after callbacks if (bShouldCallCustomCallbacks) { - for (Mod& mod : g_pModManager->GetMods()) + for (Mod& mod : g_pModManager->GetMods() | ModManager::FilterEnabled) { - if (!mod.m_bEnabled) - continue; - for (ModScript script : mod.Scripts) { for (ModScriptCallback modCallback : script.Callbacks) -- cgit v1.2.3