From 4b1fc670e46d7ae904384f15c262fa00b6a236a3 Mon Sep 17 00:00:00 2001 From: Chomp Date: Thu, 4 Sep 2025 16:39:01 +0100 Subject: [PATCH] Updated mod loading to use guid as key for `Mod dependencies, LoadBefore, LoadAfter, and Incompatibilties` --- SPTarkov.Server/Modding/ModLoadOrder.cs | 75 ++++++++++++------------- SPTarkov.Server/Modding/ModValidator.cs | 67 +++++++++++----------- 2 files changed, 70 insertions(+), 72 deletions(-) diff --git a/SPTarkov.Server/Modding/ModLoadOrder.cs b/SPTarkov.Server/Modding/ModLoadOrder.cs index d50af1f1..2f9bb16d 100644 --- a/SPTarkov.Server/Modding/ModLoadOrder.cs +++ b/SPTarkov.Server/Modding/ModLoadOrder.cs @@ -5,91 +5,90 @@ namespace SPTarkov.Server.Modding; public class ModLoadOrder(ICloner cloner) { - protected Dictionary loadOrder = new(); - protected Dictionary mods = new(); - protected Dictionary modsAvailable = new(); + protected readonly Dictionary LoadOrder = new(); + protected Dictionary Mods = new(); + protected Dictionary ModsAvailable = new(); public Dictionary SetModList(Dictionary mods) { - this.mods = mods; - modsAvailable = cloner.Clone(this.mods); - loadOrder = new Dictionary(); + this.Mods = mods; + ModsAvailable = cloner.Clone(this.Mods); + LoadOrder.Clear(); var visited = new HashSet(); // invert loadBefore into loadAfter on specified mods - foreach (var (modName, modConfig) in modsAvailable) + foreach (var (modGuid, modConfig) in ModsAvailable) { if ((modConfig.LoadBefore ?? []).Count > 0) { - InvertLoadBefore(modName); + InvertLoadBefore(modGuid); } } - foreach (var modName in modsAvailable.Keys) + foreach (var modGuid in ModsAvailable.Keys) { - GetLoadOrderRecursive(modName, visited); + GetLoadOrderRecursive(modGuid, visited); } - return loadOrder; + return LoadOrder; } public List GetLoadOrder() { - return [.. loadOrder.Keys]; + return [.. LoadOrder.Keys]; } - public HashSet GetModsOnLoadBefore(string mod) + public HashSet GetModsOnLoadBefore(string modGuid) { - if (!mods.TryGetValue(mod, out var config)) + if (!Mods.TryGetValue(modGuid, out var config)) { - throw new Exception($"The mod {mod} does not exist!"); + throw new Exception($"The mod: {modGuid} does not exist!"); } var loadBefore = new HashSet(config.LoadBefore); - - foreach (var loadBeforeMod in loadBefore) + foreach (var loadBeforeModGuid in loadBefore) { - if (!mods.ContainsKey(loadBeforeMod)) + if (!Mods.ContainsKey(loadBeforeModGuid)) { - loadBefore.Remove(loadBeforeMod); + loadBefore.Remove(loadBeforeModGuid); } } return loadBefore; } - protected void InvertLoadBefore(string mod) + protected void InvertLoadBefore(string modGuid) { - var loadBefore = GetModsOnLoadBefore(mod); + var loadBefore = GetModsOnLoadBefore(modGuid); foreach (var loadBeforeMod in loadBefore) { - var loadBeforeModConfig = modsAvailable[loadBeforeMod]; + var loadBeforeModConfig = ModsAvailable[loadBeforeMod]; loadBeforeModConfig.LoadAfter ??= []; - loadBeforeModConfig.LoadAfter.Add(mod); + loadBeforeModConfig.LoadAfter.Add(modGuid); - modsAvailable.Add(loadBeforeMod, loadBeforeModConfig); + ModsAvailable.Add(loadBeforeMod, loadBeforeModConfig); } } - protected void GetLoadOrderRecursive(string mod, HashSet visited) + protected void GetLoadOrderRecursive(string modGuid, HashSet visited) { // Validate package - if (loadOrder.ContainsKey(mod)) + if (LoadOrder.ContainsKey(modGuid)) { return; } - if (visited.Contains(mod)) + if (visited.Contains(modGuid)) { // Additional info to help debug - throw new Exception($"Cyclic dependency detected for mod {mod}!"); + throw new Exception($"Cyclic dependency detected for mod: {modGuid}!"); } // Check dependencies - if (!modsAvailable.TryGetValue(mod, out var config)) + if (!ModsAvailable.TryGetValue(modGuid, out var config)) { throw new Exception("modloader-error_parsing_mod_load_order"); } @@ -99,27 +98,27 @@ public class ModLoadOrder(ICloner cloner) var dependencies = new HashSet(config.ModDependencies.Keys); - foreach (var modAfter in config.LoadAfter) + foreach (var modAfterGuid in config.LoadAfter) { - if (modsAvailable.TryGetValue(modAfter, out var value)) + if (ModsAvailable.TryGetValue(modAfterGuid, out var value)) { - if (value?.LoadAfter?.Contains(mod) ?? false) + if (value?.LoadAfter?.Contains(modGuid) ?? false) { throw new Exception("modloader-load_order_conflict"); } - dependencies.Add(modAfter); + dependencies.Add(modAfterGuid); } } - visited.Add(mod); + visited.Add(modGuid); - foreach (var nextMod in dependencies) + foreach (var nextModGuid in dependencies) { - GetLoadOrderRecursive(nextMod, visited); + GetLoadOrderRecursive(nextModGuid, visited); } - visited.Remove(mod); - loadOrder.Add(mod, config); + visited.Remove(modGuid); + LoadOrder.Add(modGuid, config); } } diff --git a/SPTarkov.Server/Modding/ModValidator.cs b/SPTarkov.Server/Modding/ModValidator.cs index 65dac27d..3e300b48 100644 --- a/SPTarkov.Server/Modding/ModValidator.cs +++ b/SPTarkov.Server/Modding/ModValidator.cs @@ -1,32 +1,26 @@ using SPTarkov.Common.Semver; -using SPTarkov.Server.Core.Models.Spt.Config; using SPTarkov.Server.Core.Models.Spt.Mod; using SPTarkov.Server.Core.Models.Utils; -using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Services; using SPTarkov.Server.Core.Utils; using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel; -using Version = SemanticVersioning.Version; namespace SPTarkov.Server.Modding; public class ModValidator( ISptLogger logger, ServerLocalisationService localisationService, - ConfigServer configServer, ISemVer semVer, ModLoadOrder modLoadOrder, JsonUtil jsonUtil, FileUtil fileUtil ) { - protected readonly string basepath = "user/mods/"; - protected readonly string modOrderPath = "user/mods/order.json"; - protected readonly Dictionary imported = []; - protected readonly Dictionary order = []; - protected readonly HashSet skippedMods = []; - - protected readonly CoreConfig sptConfig = configServer.GetConfig(); + protected const string BasePath = "user/mods/"; + protected const string ModOrderPath = "user/mods/order.json"; + protected readonly Dictionary Imported = []; + protected readonly Dictionary Order = []; + protected readonly HashSet SkippedMods = []; public List ValidateAndSort(IEnumerable mods) { @@ -34,11 +28,13 @@ public class ModValidator( { ValidateMods(mods); - var sortedModLoadOrder = modLoadOrder.SetModList(imported.ToDictionary(m => m.Key, m => m.Value.ModMetadata)); + var sortedModLoadOrder = modLoadOrder.SetModList( + Imported.ToDictionary(m => m.Value.ModMetadata.ModGuid, m => m.Value.ModMetadata) + ); var finalList = new List(); foreach (var orderMod in SortModsLoadOrder()) { - if (!imported.TryGetValue(orderMod, out var loadedMod)) + if (!Imported.TryGetValue(orderMod, out var loadedMod)) { throw new Exception($"Unable to find mod {orderMod} in loaded mods"); } @@ -54,7 +50,7 @@ public class ModValidator( public string GetModPath(string mod) { - return $"{basepath}{mod}/"; + return $"{BasePath}{mod}/"; } protected void ValidateMods(IEnumerable mods) @@ -62,22 +58,22 @@ public class ModValidator( logger.Info(localisationService.GetText("modloader-loading_mods", mods.Count())); // Mod order - if (!fileUtil.FileExists(modOrderPath)) + if (!fileUtil.FileExists(ModOrderPath)) { logger.Info(localisationService.GetText("modloader-mod_order_missing")); // Write file with empty order array to disk - fileUtil.WriteFile(modOrderPath, jsonUtil.Serialize(new ModOrder { Order = [] })); + fileUtil.WriteFile(ModOrderPath, jsonUtil.Serialize(new ModOrder { Order = [] })); } else { - var modOrder = File.ReadAllText(modOrderPath); + var modOrder = File.ReadAllText(ModOrderPath); try { var modOrderArray = jsonUtil.Deserialize(modOrder).Order; for (var i = 0; i < modOrderArray.Count; i++) { - order.Add(modOrderArray[i], i); + Order.Add(modOrderArray[i], i); } } catch (Exception e) @@ -89,7 +85,9 @@ public class ModValidator( // Validate and remove broken mods from mod list var validMods = GetValidMods(mods).ToList(); // ToList now so we can .Sort later - var modPackageData = validMods.ToDictionary(m => m.ModMetadata!.Name!, m => m.ModMetadata!); + // Key to guid for easy comparision later + var modPackageData = validMods.ToDictionary(m => m.ModMetadata.ModGuid, m => m.ModMetadata); + CheckForDuplicateMods(modPackageData); // Used to check all errors before stopping the load execution @@ -115,7 +113,7 @@ public class ModValidator( errorsFound = true; } - // Returns if mod isnt compatible with this verison of spt + // Returns if mod isn't compatible with this version of spt if (!IsModCompatibleWithSpt(modToValidate)) { errorsFound = true; @@ -157,13 +155,13 @@ public class ModValidator( protected int SortMods(SptMod prev, SptMod next, Dictionary missingFromOrderJson) { // mod is not on the list, move the mod to last - if (!order.TryGetValue(prev.ModMetadata!.Name!, out var previndex)) + if (!Order.TryGetValue(prev.ModMetadata!.Name!, out var previndex)) { missingFromOrderJson[prev.ModMetadata.Name!] = true; return 1; } - if (!order.TryGetValue(next.ModMetadata!.Name!, out var nextindex)) + if (!Order.TryGetValue(next.ModMetadata!.Name!, out var nextindex)) { missingFromOrderJson[next.ModMetadata.Name!] = true; return -1; @@ -188,12 +186,12 @@ public class ModValidator( // if there's more than one entry for a given mod it means there's at least 2 mods with the same author and name trying to load. if (groupedMods[name].Count > 1) { - skippedMods.Add(name); + SkippedMods.Add(name); } } // at this point skippedMods only contains mods that are duplicated, so we can just go through every single entry and log it - foreach (var modName in skippedMods) + foreach (var modName in SkippedMods) { logger.Error(localisationService.GetText("modloader-x_duplicates_found", modName)); } @@ -254,7 +252,7 @@ public class ModValidator( public List SortModsLoadOrder() { // if loadorder.json exists: load it, otherwise generate load order - var loadOrderPath = $"{basepath}loadorder.json"; + var loadOrderPath = $"{BasePath}loadorder.json"; if (fileUtil.FileExists(loadOrderPath)) { return jsonUtil.Deserialize>(fileUtil.ReadFile(loadOrderPath)); @@ -270,7 +268,7 @@ public class ModValidator( protected void AddMod(SptMod mod) { // Add mod to imported list - imported.Add(mod.ModMetadata.Name, mod); + Imported.Add(mod.ModMetadata.ModGuid, mod); logger.Info( localisationService.GetText( "modloader-loaded_mod", @@ -291,7 +289,7 @@ public class ModValidator( /// protected bool ShouldSkipMod(AbstractModMetadata pkg) { - return skippedMods.Contains($"{pkg.Author}-{pkg.Name}"); + return SkippedMods.Contains($"{pkg.Author}-{pkg.Name}"); } protected bool AreModDependenciesFulfilled(AbstractModMetadata pkg, Dictionary loadedMods) @@ -334,29 +332,30 @@ public class ModValidator( return true; } - protected bool IsModCompatible(AbstractModMetadata mod, Dictionary loadedMods) + protected bool IsModCompatible(AbstractModMetadata modToCheck, Dictionary loadedMods) { - if (mod.Incompatibilities == null) + if (modToCheck.Incompatibilities == null) { return true; } - foreach (var incompatibleModName in mod.Incompatibilities) + foreach (var incompatibleModGuid in modToCheck.Incompatibilities) { // Raise dependency version incompatible if any incompatible mod is found - if (loadedMods.ContainsKey(incompatibleModName)) + if (loadedMods.ContainsKey(incompatibleModGuid)) { logger.Error( localisationService.GetText( "modloader-incompatible_mod_found", new { - author = mod.Author, - name = mod.Name, - incompatibleModName, + author = modToCheck.Author, + name = modToCheck.Name, + incompatibleModName = incompatibleModGuid, } ) ); + return false; } }