Updated mod loading to use guid as key for Mod dependencies, LoadBefore, LoadAfter, and Incompatibilties

This commit is contained in:
Chomp
2025-09-04 16:39:01 +01:00
parent 87a0c41060
commit 4b1fc670e4
2 changed files with 70 additions and 72 deletions
+37 -38
View File
@@ -5,91 +5,90 @@ namespace SPTarkov.Server.Modding;
public class ModLoadOrder(ICloner cloner) public class ModLoadOrder(ICloner cloner)
{ {
protected Dictionary<string, AbstractModMetadata> loadOrder = new(); protected readonly Dictionary<string, AbstractModMetadata> LoadOrder = new();
protected Dictionary<string, AbstractModMetadata> mods = new(); protected Dictionary<string, AbstractModMetadata> Mods = new();
protected Dictionary<string, AbstractModMetadata> modsAvailable = new(); protected Dictionary<string, AbstractModMetadata> ModsAvailable = new();
public Dictionary<string, AbstractModMetadata> SetModList(Dictionary<string, AbstractModMetadata> mods) public Dictionary<string, AbstractModMetadata> SetModList(Dictionary<string, AbstractModMetadata> mods)
{ {
this.mods = mods; this.Mods = mods;
modsAvailable = cloner.Clone(this.mods); ModsAvailable = cloner.Clone(this.Mods);
loadOrder = new Dictionary<string, AbstractModMetadata>(); LoadOrder.Clear();
var visited = new HashSet<string>(); var visited = new HashSet<string>();
// invert loadBefore into loadAfter on specified mods // invert loadBefore into loadAfter on specified mods
foreach (var (modName, modConfig) in modsAvailable) foreach (var (modGuid, modConfig) in ModsAvailable)
{ {
if ((modConfig.LoadBefore ?? []).Count > 0) 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<string> GetLoadOrder() public List<string> GetLoadOrder()
{ {
return [.. loadOrder.Keys]; return [.. LoadOrder.Keys];
} }
public HashSet<string> GetModsOnLoadBefore(string mod) public HashSet<string> 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<string>(config.LoadBefore); var loadBefore = new HashSet<string>(config.LoadBefore);
foreach (var loadBeforeModGuid in loadBefore)
foreach (var loadBeforeMod in loadBefore)
{ {
if (!mods.ContainsKey(loadBeforeMod)) if (!Mods.ContainsKey(loadBeforeModGuid))
{ {
loadBefore.Remove(loadBeforeMod); loadBefore.Remove(loadBeforeModGuid);
} }
} }
return loadBefore; 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) foreach (var loadBeforeMod in loadBefore)
{ {
var loadBeforeModConfig = modsAvailable[loadBeforeMod]; var loadBeforeModConfig = ModsAvailable[loadBeforeMod];
loadBeforeModConfig.LoadAfter ??= []; loadBeforeModConfig.LoadAfter ??= [];
loadBeforeModConfig.LoadAfter.Add(mod); loadBeforeModConfig.LoadAfter.Add(modGuid);
modsAvailable.Add(loadBeforeMod, loadBeforeModConfig); ModsAvailable.Add(loadBeforeMod, loadBeforeModConfig);
} }
} }
protected void GetLoadOrderRecursive(string mod, HashSet<string> visited) protected void GetLoadOrderRecursive(string modGuid, HashSet<string> visited)
{ {
// Validate package // Validate package
if (loadOrder.ContainsKey(mod)) if (LoadOrder.ContainsKey(modGuid))
{ {
return; return;
} }
if (visited.Contains(mod)) if (visited.Contains(modGuid))
{ {
// Additional info to help debug // 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 // 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"); throw new Exception("modloader-error_parsing_mod_load_order");
} }
@@ -99,27 +98,27 @@ public class ModLoadOrder(ICloner cloner)
var dependencies = new HashSet<string>(config.ModDependencies.Keys); var dependencies = new HashSet<string>(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"); 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); visited.Remove(modGuid);
loadOrder.Add(mod, config); LoadOrder.Add(modGuid, config);
} }
} }
+33 -34
View File
@@ -1,32 +1,26 @@
using SPTarkov.Common.Semver; using SPTarkov.Common.Semver;
using SPTarkov.Server.Core.Models.Spt.Config;
using SPTarkov.Server.Core.Models.Spt.Mod; using SPTarkov.Server.Core.Models.Spt.Mod;
using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Models.Utils;
using SPTarkov.Server.Core.Servers;
using SPTarkov.Server.Core.Services; using SPTarkov.Server.Core.Services;
using SPTarkov.Server.Core.Utils; using SPTarkov.Server.Core.Utils;
using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel; using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel;
using Version = SemanticVersioning.Version;
namespace SPTarkov.Server.Modding; namespace SPTarkov.Server.Modding;
public class ModValidator( public class ModValidator(
ISptLogger<ModValidator> logger, ISptLogger<ModValidator> logger,
ServerLocalisationService localisationService, ServerLocalisationService localisationService,
ConfigServer configServer,
ISemVer semVer, ISemVer semVer,
ModLoadOrder modLoadOrder, ModLoadOrder modLoadOrder,
JsonUtil jsonUtil, JsonUtil jsonUtil,
FileUtil fileUtil FileUtil fileUtil
) )
{ {
protected readonly string basepath = "user/mods/"; protected const string BasePath = "user/mods/";
protected readonly string modOrderPath = "user/mods/order.json"; protected const string ModOrderPath = "user/mods/order.json";
protected readonly Dictionary<string, SptMod> imported = []; protected readonly Dictionary<string, SptMod> Imported = [];
protected readonly Dictionary<string, int> order = []; protected readonly Dictionary<string, int> Order = [];
protected readonly HashSet<string> skippedMods = []; protected readonly HashSet<string> SkippedMods = [];
protected readonly CoreConfig sptConfig = configServer.GetConfig<CoreConfig>();
public List<SptMod> ValidateAndSort(IEnumerable<SptMod> mods) public List<SptMod> ValidateAndSort(IEnumerable<SptMod> mods)
{ {
@@ -34,11 +28,13 @@ public class ModValidator(
{ {
ValidateMods(mods); 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<SptMod>(); var finalList = new List<SptMod>();
foreach (var orderMod in SortModsLoadOrder()) 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"); throw new Exception($"Unable to find mod {orderMod} in loaded mods");
} }
@@ -54,7 +50,7 @@ public class ModValidator(
public string GetModPath(string mod) public string GetModPath(string mod)
{ {
return $"{basepath}{mod}/"; return $"{BasePath}{mod}/";
} }
protected void ValidateMods(IEnumerable<SptMod> mods) protected void ValidateMods(IEnumerable<SptMod> mods)
@@ -62,22 +58,22 @@ public class ModValidator(
logger.Info(localisationService.GetText("modloader-loading_mods", mods.Count())); logger.Info(localisationService.GetText("modloader-loading_mods", mods.Count()));
// Mod order // Mod order
if (!fileUtil.FileExists(modOrderPath)) if (!fileUtil.FileExists(ModOrderPath))
{ {
logger.Info(localisationService.GetText("modloader-mod_order_missing")); logger.Info(localisationService.GetText("modloader-mod_order_missing"));
// Write file with empty order array to disk // 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 else
{ {
var modOrder = File.ReadAllText(modOrderPath); var modOrder = File.ReadAllText(ModOrderPath);
try try
{ {
var modOrderArray = jsonUtil.Deserialize<ModOrder>(modOrder).Order; var modOrderArray = jsonUtil.Deserialize<ModOrder>(modOrder).Order;
for (var i = 0; i < modOrderArray.Count; i++) for (var i = 0; i < modOrderArray.Count; i++)
{ {
order.Add(modOrderArray[i], i); Order.Add(modOrderArray[i], i);
} }
} }
catch (Exception e) catch (Exception e)
@@ -89,7 +85,9 @@ public class ModValidator(
// Validate and remove broken mods from mod list // Validate and remove broken mods from mod list
var validMods = GetValidMods(mods).ToList(); // ToList now so we can .Sort later 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); CheckForDuplicateMods(modPackageData);
// Used to check all errors before stopping the load execution // Used to check all errors before stopping the load execution
@@ -115,7 +113,7 @@ public class ModValidator(
errorsFound = true; 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)) if (!IsModCompatibleWithSpt(modToValidate))
{ {
errorsFound = true; errorsFound = true;
@@ -157,13 +155,13 @@ public class ModValidator(
protected int SortMods(SptMod prev, SptMod next, Dictionary<string, bool> missingFromOrderJson) protected int SortMods(SptMod prev, SptMod next, Dictionary<string, bool> missingFromOrderJson)
{ {
// mod is not on the list, move the mod to last // 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; missingFromOrderJson[prev.ModMetadata.Name!] = true;
return 1; 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; missingFromOrderJson[next.ModMetadata.Name!] = true;
return -1; 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 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) 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 // 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)); logger.Error(localisationService.GetText("modloader-x_duplicates_found", modName));
} }
@@ -254,7 +252,7 @@ public class ModValidator(
public List<string> SortModsLoadOrder() public List<string> SortModsLoadOrder()
{ {
// if loadorder.json exists: load it, otherwise generate load order // if loadorder.json exists: load it, otherwise generate load order
var loadOrderPath = $"{basepath}loadorder.json"; var loadOrderPath = $"{BasePath}loadorder.json";
if (fileUtil.FileExists(loadOrderPath)) if (fileUtil.FileExists(loadOrderPath))
{ {
return jsonUtil.Deserialize<List<string>>(fileUtil.ReadFile(loadOrderPath)); return jsonUtil.Deserialize<List<string>>(fileUtil.ReadFile(loadOrderPath));
@@ -270,7 +268,7 @@ public class ModValidator(
protected void AddMod(SptMod mod) protected void AddMod(SptMod mod)
{ {
// Add mod to imported list // Add mod to imported list
imported.Add(mod.ModMetadata.Name, mod); Imported.Add(mod.ModMetadata.ModGuid, mod);
logger.Info( logger.Info(
localisationService.GetText( localisationService.GetText(
"modloader-loaded_mod", "modloader-loaded_mod",
@@ -291,7 +289,7 @@ public class ModValidator(
/// <returns></returns> /// <returns></returns>
protected bool ShouldSkipMod(AbstractModMetadata pkg) 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<string, AbstractModMetadata> loadedMods) protected bool AreModDependenciesFulfilled(AbstractModMetadata pkg, Dictionary<string, AbstractModMetadata> loadedMods)
@@ -334,29 +332,30 @@ public class ModValidator(
return true; return true;
} }
protected bool IsModCompatible(AbstractModMetadata mod, Dictionary<string, AbstractModMetadata> loadedMods) protected bool IsModCompatible(AbstractModMetadata modToCheck, Dictionary<string, AbstractModMetadata> loadedMods)
{ {
if (mod.Incompatibilities == null) if (modToCheck.Incompatibilities == null)
{ {
return true; return true;
} }
foreach (var incompatibleModName in mod.Incompatibilities) foreach (var incompatibleModGuid in modToCheck.Incompatibilities)
{ {
// Raise dependency version incompatible if any incompatible mod is found // Raise dependency version incompatible if any incompatible mod is found
if (loadedMods.ContainsKey(incompatibleModName)) if (loadedMods.ContainsKey(incompatibleModGuid))
{ {
logger.Error( logger.Error(
localisationService.GetText( localisationService.GetText(
"modloader-incompatible_mod_found", "modloader-incompatible_mod_found",
new new
{ {
author = mod.Author, author = modToCheck.Author,
name = mod.Name, name = modToCheck.Name,
incompatibleModName, incompatibleModName = incompatibleModGuid,
} }
) )
); );
return false; return false;
} }
} }