diff options
author | unknown <for.oliver.kirkham@gmail.com> | 2023-02-13 13:51:40 +0000 |
---|---|---|
committer | unknown <for.oliver.kirkham@gmail.com> | 2023-02-13 13:51:40 +0000 |
commit | 56372efa9706898c8646376182b544bc6350c5c7 (patch) | |
tree | 08b609e38c758f6c3431be5df6591d0c34d802bc /NorthstarDLL/mods/modmanager.cpp | |
parent | 2c2bd9ebb8f832ff117caca16e5cb3bc806666db (diff) | |
download | NorthstarLauncher-56372efa9706898c8646376182b544bc6350c5c7.tar.gz NorthstarLauncher-56372efa9706898c8646376182b544bc6350c5c7.zip |
refactor modloading and implement initial automatic reload code
Diffstat (limited to 'NorthstarDLL/mods/modmanager.cpp')
-rw-r--r-- | NorthstarDLL/mods/modmanager.cpp | 894 |
1 files changed, 593 insertions, 301 deletions
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)) |