Updated mod loading to use guid as key for Mod dependencies, LoadBefore, LoadAfter, and Incompatibilties
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user