diff --git a/primedev/mods/modmanager.cpp b/primedev/mods/modmanager.cpp index 0c162de0e..0b55c40ed 100644 --- a/primedev/mods/modmanager.cpp +++ b/primedev/mods/modmanager.cpp @@ -522,6 +522,8 @@ void Mod::ParseInitScript(rapidjson_document& json) ModManager::ModManager() { + cfgPath = GetNorthstarPrefix() + "/enabledmods.json"; + // precaculated string hashes // note: use backslashes for these, since we use lexically_normal for file paths which uses them m_hScriptsRsonHash = STR_HASH("scripts\\vscripts\\scripts.rson"); @@ -621,136 +623,98 @@ void ModManager::LoadMods() m_DependencyConstants.clear(); + // File format checks + bool isUsingOldFormat = false; + rapidjson_document oldEnabledModsCfg; + // read enabled mods cfg - std::ifstream enabledModsStream(GetNorthstarPrefix() + "/enabledmods.json"); + std::ifstream enabledModsStream(cfgPath); std::stringstream enabledModsStringStream; - if (!enabledModsStream.fail()) + // create configuration file if does not exist + if (enabledModsStream.fail()) + { + m_EnabledModsCfg.SetObject(); + } + else { while (enabledModsStream.peek() != EOF) enabledModsStringStream << (char)enabledModsStream.get(); - enabledModsStream.close(); m_EnabledModsCfg.Parse( enabledModsStringStream.str().c_str()); - m_bHasEnabledModsCfg = m_EnabledModsCfg.IsObject(); - } - - // get mod directories - std::filesystem::directory_iterator classicModsDir = fs::directory_iterator(GetModFolderPath()); - std::filesystem::directory_iterator remoteModsDir = fs::directory_iterator(GetRemoteModFolderPath()); - std::filesystem::directory_iterator thunderstoreModsDir = fs::directory_iterator(GetThunderstoreModFolderPath()); - - for (fs::directory_entry dir : classicModsDir) - if (fs::exists(dir.path() / "mod.json")) - modDirs.push_back(dir.path()); - - // Special case for Thunderstore and remote mods directories - // Set up regex for `AUTHOR-MOD-VERSION` pattern - std::regex pattern(R"(.*\\([a-zA-Z0-9_]+)-([a-zA-Z0-9_]+)-(\d+\.\d+\.\d+))"); + // Check file format, and rename file if it is not using new format + bool isUsingUnknownFormat = !m_EnabledModsCfg.IsObject() || !m_EnabledModsCfg.HasMember("Northstar.Client"); + isUsingOldFormat = + m_EnabledModsCfg.IsObject() && m_EnabledModsCfg.HasMember("Northstar.Client") && m_EnabledModsCfg["Northstar.Client"].IsBool(); - for (fs::directory_iterator dirIterator : {thunderstoreModsDir, remoteModsDir}) - { - for (fs::directory_entry dir : dirIterator) + if (isUsingUnknownFormat || isUsingOldFormat) { - fs::path modsDir = dir.path() / "mods"; // Check for mods folder in the Thunderstore mod - // Use regex to match `AUTHOR-MOD-VERSION` pattern - if (!std::regex_match(dir.path().string(), pattern)) + spdlog::info( + "==> {} manifesto format detected, renaming it to enabledmods.old.json.", isUsingUnknownFormat ? "Unknown" : "Old"); + int ret = rename(cfgPath.c_str(), (GetNorthstarPrefix() + "/enabledmods.old.json").c_str()); + if (ret) { - spdlog::warn("The following directory did not match 'AUTHOR-MOD-VERSION': {}", dir.path().string()); - continue; // skip loading mod that doesn't match - } - if (fs::exists(modsDir) && fs::is_directory(modsDir)) - { - for (fs::directory_entry subDir : fs::directory_iterator(modsDir)) - { - if (fs::exists(subDir.path() / "mod.json")) - { - modDirs.push_back(subDir.path()); - } - } + spdlog::error("Failed renaming manifesto (error code: {}).", ret); + return; } - } - } - - for (fs::path modDir : modDirs) - { - // read mod json file - std::ifstream jsonStream(modDir / "mod.json"); - std::stringstream jsonStringStream; - - // fail if no mod json - if (jsonStream.fail()) - { - spdlog::warn( - "Mod file at '{}' does not exist or could not be read, is it installed correctly?", (modDir / "mod.json").string()); - continue; - } - - while (jsonStream.peek() != EOF) - jsonStringStream << (char)jsonStream.get(); - jsonStream.close(); - - Mod mod(modDir, (char*)jsonStringStream.str().c_str()); - - for (auto& pair : mod.DependencyConstants) - { - if (m_DependencyConstants.find(pair.first) != m_DependencyConstants.end() && m_DependencyConstants[pair.first] != pair.second) + // Copy old configuration to migrate manifesto to new format + if (isUsingOldFormat) { - spdlog::error( - "'{}' attempted to register a dependency constant '{}' for '{}' that already exists for '{}'. " - "Change the constant name.", - mod.Name, - pair.first, - pair.second, - m_DependencyConstants[pair.first]); - mod.m_bWasReadSuccessfully = false; - break; + oldEnabledModsCfg.CopyFrom(m_EnabledModsCfg, oldEnabledModsCfg.GetAllocator()); } - if (m_DependencyConstants.find(pair.first) == m_DependencyConstants.end()) - m_DependencyConstants.emplace(pair); - } - for (std::string& dependency : mod.PluginDependencyConstants) - { - m_PluginDependencyConstants.insert(dependency); + // Reset current configuration + m_EnabledModsCfg.SetObject(); } - - if (m_bHasEnabledModsCfg && m_EnabledModsCfg.HasMember(mod.Name.c_str())) - mod.m_bEnabled = m_EnabledModsCfg[mod.Name.c_str()].IsTrue(); - else - mod.m_bEnabled = true; - - if (mod.m_bWasReadSuccessfully) - { - if (mod.m_bEnabled) - spdlog::info("'{}' loaded successfully, version {}", mod.Name, mod.Version); - else - spdlog::info("'{}' loaded successfully, version {} (DISABLED)", mod.Name, mod.Version); - - m_LoadedMods.push_back(mod); - } - else - spdlog::warn("Mod file at '{}' failed to load", (modDir / "mod.json").string()); } - // sort by load prio, lowest-highest - std::sort(m_LoadedMods.begin(), m_LoadedMods.end(), [](Mod& a, Mod& b) { return a.LoadPriority < b.LoadPriority; }); + SearchFilesystemForMods(); // This is used to check if some mods have a folder but no entry in enabledmods.json bool newModsDetected = false; for (Mod& mod : m_LoadedMods) { - if (!mod.m_bEnabled) - continue; - // Add mod entry to enabledmods.json if it doesn't exist - if (!mod.m_bIsRemote && m_bHasEnabledModsCfg && !m_EnabledModsCfg.HasMember(mod.Name.c_str())) + bool isModRemote = mod.m_bIsRemote; + bool modEntryExists = m_EnabledModsCfg.HasMember(mod.Name.c_str()); + bool modEntryHasCorrectFormat = modEntryExists && m_EnabledModsCfg[mod.Name.c_str()].IsObject(); + bool modVersionEntryExists = modEntryExists && m_EnabledModsCfg[mod.Name.c_str()].HasMember(mod.Version.c_str()); + + if (!isModRemote && (!modEntryExists || !modVersionEntryExists)) { - m_EnabledModsCfg.AddMember(rapidjson_document::StringRefType(mod.Name.c_str()), true, m_EnabledModsCfg.GetAllocator()); + // Creating mod key (with name) + if (!modEntryHasCorrectFormat) + { + // Adjust wrong format (string instead of object) + if (modEntryExists) + { + m_EnabledModsCfg.RemoveMember(mod.Name.c_str()); + } + m_EnabledModsCfg.AddMember(rapidjson_document::StringRefType(mod.Name.c_str()), false, m_EnabledModsCfg.GetAllocator()); + m_EnabledModsCfg[mod.Name.c_str()].SetObject(); + } + + // Creating version key + if (!modVersionEntryExists) + { + m_EnabledModsCfg[mod.Name.c_str()].AddMember( + rapidjson_document::StringRefType(mod.Version.c_str()), false, m_EnabledModsCfg.GetAllocator()); + } + + // Add mod entry + bool modIsEnabled = mod.m_bEnabled; + // Try to use old manifesto if currently migrating from old format + if (isUsingOldFormat && oldEnabledModsCfg.HasMember(mod.Name.c_str()) && oldEnabledModsCfg[mod.Name.c_str()].IsBool()) + { + modIsEnabled = oldEnabledModsCfg[mod.Name.c_str()].GetBool(); + mod.m_bEnabled = modIsEnabled; + } + m_EnabledModsCfg[mod.Name.c_str()][mod.Version.c_str()].SetBool(modIsEnabled); + newModsDetected = true; } @@ -1022,7 +986,7 @@ void ModManager::LoadMods() // If there are new mods, we write entries accordingly in enabledmods.json if (newModsDetected) { - std::ofstream writeStream(GetNorthstarPrefix() + "/enabledmods.json"); + std::ofstream writeStream(cfgPath); rapidjson::OStreamWrapper writeStreamWrapper(writeStream); rapidjson::PrettyWriter writer(writeStreamWrapper); m_EnabledModsCfg.Accept(writer); @@ -1101,24 +1065,149 @@ void ModManager::UnloadMods() fs::remove(GetCompiledAssetsPath() / fs::path(kvPaths.second).lexically_relative(mod.m_ModDirectory)); mod.KeyValues.clear(); + } + + // save mods configuration to disk + ExportModsConfigurationToFile(); - // 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 + // do we need to dealloc individual entries in m_loadedMods? idk, rework + m_LoadedMods.clear(); +} + +void ModManager::SearchFilesystemForMods() +{ + std::vector modDirs; + m_LoadedMods.clear(); + + // get mod directories + std::filesystem::directory_iterator classicModsDir = fs::directory_iterator(GetModFolderPath()); + std::filesystem::directory_iterator remoteModsDir = fs::directory_iterator(GetRemoteModFolderPath()); + std::filesystem::directory_iterator thunderstoreModsDir = fs::directory_iterator(GetThunderstoreModFolderPath()); + + for (fs::directory_entry dir : classicModsDir) + if (fs::exists(dir.path() / "mod.json")) + modDirs.push_back(dir.path()); + + // Special case for Thunderstore and remote mods directories + // Set up regex for `AUTHOR-MOD-VERSION` pattern + std::regex pattern(R"(.*\\([a-zA-Z0-9_]+)-([a-zA-Z0-9_]+)-(\d+\.\d+\.\d+))"); + + for (fs::directory_iterator dirIterator : {thunderstoreModsDir, remoteModsDir}) + { + for (fs::directory_entry dir : dirIterator) + { + fs::path modsDir = dir.path() / "mods"; // Check for mods folder in the Thunderstore mod + // Use regex to match `AUTHOR-MOD-VERSION` pattern + if (!std::regex_match(dir.path().string(), pattern)) + { + spdlog::warn("The following directory did not match 'AUTHOR-MOD-VERSION': {}", dir.path().string()); + continue; // skip loading mod that doesn't match + } + if (fs::exists(modsDir) && fs::is_directory(modsDir)) + { + for (fs::directory_entry subDir : fs::directory_iterator(modsDir)) + { + if (fs::exists(subDir.path() / "mod.json")) + { + modDirs.push_back(subDir.path()); + } + } + } + } + } + + for (fs::path modDir : modDirs) + { + // read mod json file + std::ifstream jsonStream(modDir / "mod.json"); + std::stringstream jsonStringStream; + + // fail if no mod json + if (jsonStream.fail()) + { + spdlog::warn( + "Mod file at '{}' does not exist or could not be read, is it installed correctly?", (modDir / "mod.json").string()); + continue; + } + + while (jsonStream.peek() != EOF) + jsonStringStream << (char)jsonStream.get(); + + jsonStream.close(); + + Mod mod(modDir, (char*)jsonStringStream.str().c_str()); + + for (auto& pair : mod.DependencyConstants) + { + if (m_DependencyConstants.find(pair.first) != m_DependencyConstants.end() && m_DependencyConstants[pair.first] != pair.second) + { + spdlog::error( + "'{}' attempted to register a dependency constant '{}' for '{}' that already exists for '{}'. " + "Change the constant name.", + mod.Name, + pair.first, + pair.second, + m_DependencyConstants[pair.first]); + mod.m_bWasReadSuccessfully = false; + break; + } + if (m_DependencyConstants.find(pair.first) == m_DependencyConstants.end()) + m_DependencyConstants.emplace(pair); + } + + for (std::string& dependency : mod.PluginDependencyConstants) + { + m_PluginDependencyConstants.insert(dependency); + } + + if (m_EnabledModsCfg.HasMember(mod.Name.c_str()) && m_EnabledModsCfg[mod.Name.c_str()].HasMember(mod.Version)) + { + mod.m_bEnabled = m_EnabledModsCfg[mod.Name.c_str()][mod.Version.c_str()].IsTrue(); + } + else + mod.m_bEnabled = true; + + if (mod.m_bWasReadSuccessfully) + { + if (mod.m_bEnabled) + spdlog::info("'{}' loaded successfully, version {}", mod.Name, mod.Version); + else + spdlog::info("'{}' loaded successfully, version {} (DISABLED)", mod.Name, mod.Version); + + m_LoadedMods.push_back(mod); + } + else + spdlog::warn("Mod file at '{}' failed to load", (modDir / "mod.json").string()); + } + + // sort by load prio, lowest-highest + std::sort(m_LoadedMods.begin(), m_LoadedMods.end(), [](Mod& a, Mod& b) { return a.LoadPriority < b.LoadPriority; }); +} + +void ModManager::ExportModsConfigurationToFile() +{ + m_EnabledModsCfg.SetObject(); + + for (Mod& mod : m_LoadedMods) + { + // Creating mod key (with name) 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()].SetObject(); + } - m_EnabledModsCfg[mod.Name.c_str()].SetBool(mod.m_bEnabled); + // Creating version key + if (!m_EnabledModsCfg[mod.Name.c_str()].HasMember(mod.Version.c_str())) + m_EnabledModsCfg[mod.Name.c_str()].AddMember( + rapidjson_document::StringRefType(mod.Version.c_str()), false, m_EnabledModsCfg.GetAllocator()); + m_EnabledModsCfg[mod.Name.c_str()][mod.Version.c_str()].SetBool(mod.m_bEnabled); } - std::ofstream writeStream(GetNorthstarPrefix() + "/enabledmods.json"); + std::ofstream writeStream(cfgPath); 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(); } std::string ModManager::NormaliseModFilePath(const fs::path path) diff --git a/primedev/mods/modmanager.h b/primedev/mods/modmanager.h index 7859d6184..42980eb56 100644 --- a/primedev/mods/modmanager.h +++ b/primedev/mods/modmanager.h @@ -172,6 +172,7 @@ class ModManager bool m_bHasLoadedMods = false; bool m_bHasEnabledModsCfg; rapidjson_document m_EnabledModsCfg; + std::string cfgPath; // precalculated hashes size_t m_hScriptsRsonHash; @@ -184,6 +185,10 @@ class ModManager std::unordered_map m_DependencyConstants; std::unordered_set m_PluginDependencyConstants; +private: + void SearchFilesystemForMods(); + void ExportModsConfigurationToFile(); + public: ModManager(); void LoadMods(); diff --git a/primedev/scripts/client/scriptmodmenu.cpp b/primedev/scripts/client/scriptmodmenu.cpp index 7a2627c84..6db340726 100644 --- a/primedev/scripts/client/scriptmodmenu.cpp +++ b/primedev/scripts/client/scriptmodmenu.cpp @@ -92,15 +92,21 @@ ADD_SQFUNC("array", NSGetModNames, "", "", ScriptContext::SERVER | Scrip return SQRESULT_NOTNULL; } -ADD_SQFUNC("void", NSSetModEnabled, "string modName, bool enabled", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) +ADD_SQFUNC( + "void", + NSSetModEnabled, + "string modName, string modVersion, bool enabled", + "", + ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) { const SQChar* modName = g_pSquirrel->getstring(sqvm, 1); - const SQBool enabled = g_pSquirrel->getbool(sqvm, 2); + const SQChar* modVersion = g_pSquirrel->getstring(sqvm, 2); + const SQBool enabled = g_pSquirrel->getbool(sqvm, 3); // manual lookup, not super performant but eh not a big deal for (Mod& mod : g_pModManager->m_LoadedMods) { - if (!mod.Name.compare(modName)) + if (!mod.Name.compare(modName) && !mod.Version.compare(modVersion)) { mod.m_bEnabled = enabled; return SQRESULT_NULL;