From 5af362b0b33ed02bf2701dac7087d0679ab08eeb Mon Sep 17 00:00:00 2001 From: Cj <161484149+CJ-SPT@users.noreply.github.com> Date: Wed, 7 May 2025 15:05:22 -0400 Subject: [PATCH] Implement loading metadata from the assembly --- .../Controllers/GameController.cs | 10 +- .../Controllers/LauncherController.cs | 6 +- .../Controllers/LauncherV2Controller.cs | 6 +- .../Spt/Launcher/LauncherV2ModsResponse.cs | 2 +- .../Models/Spt/Mod/AbstractModMetadata.cs | 119 ++++++++++++++++++ .../Models/Spt/Mod/PackageJsonData.cs | 90 ------------- .../Models/Spt/Mod/SptMod.cs | 4 +- .../Services/BackupService.cs | 2 +- SPTarkov.Server/Modding/ModDllLoader.cs | 82 ++++++------ SPTarkov.Server/Modding/ModLoadOrder.cs | 10 +- SPTarkov.Server/Modding/ModValidator.cs | 38 +++--- 11 files changed, 205 insertions(+), 164 deletions(-) create mode 100644 Libraries/SPTarkov.Server.Core/Models/Spt/Mod/AbstractModMetadata.cs delete mode 100644 Libraries/SPTarkov.Server.Core/Models/Spt/Mod/PackageJsonData.cs diff --git a/Libraries/SPTarkov.Server.Core/Controllers/GameController.cs b/Libraries/SPTarkov.Server.Core/Controllers/GameController.cs index 841ad645..5905e64e 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/GameController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/GameController.cs @@ -478,7 +478,7 @@ public class GameController( { if ( fullProfile.SptData.Mods.Any(m => - m.Author == mod.PackageJson.Author && m.Version == mod.PackageJson.Version && m.Name == mod.PackageJson.Name + m.Author == mod.ModMetadata.Author && m.Version == mod.ModMetadata.Version && m.Name == mod.ModMetadata.Name ) ) { @@ -489,10 +489,10 @@ public class GameController( fullProfile.SptData.Mods.Add( new ModDetails { - Author = mod.PackageJson.Author, - Version = mod.PackageJson.Version, - Name = mod.PackageJson.Name, - Url = mod.PackageJson.Url, + Author = mod.ModMetadata.Author, + Version = mod.ModMetadata.Version, + Name = mod.ModMetadata.Name, + Url = mod.ModMetadata.Url, DateAdded = _timeUtil.GetTimeStamp() } ); diff --git a/Libraries/SPTarkov.Server.Core/Controllers/LauncherController.cs b/Libraries/SPTarkov.Server.Core/Controllers/LauncherController.cs index 6a9492c7..a81bd754 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/LauncherController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/LauncherController.cs @@ -240,14 +240,14 @@ public class LauncherController( /// Get the mods the server has currently loaded /// /// Dictionary of mod name and mod details - public Dictionary GetLoadedServerMods() + public Dictionary GetLoadedServerMods() { var mods = _applicationContext?.GetLatestValue(ContextVariableType.LOADED_MOD_ASSEMBLIES).GetValue>(); - var result = new Dictionary(); + var result = new Dictionary(); foreach (var sptMod in mods) { - result.Add(sptMod.PackageJson.Name, sptMod.PackageJson); + result.Add(sptMod.ModMetadata.Name, sptMod.ModMetadata); } return result; diff --git a/Libraries/SPTarkov.Server.Core/Controllers/LauncherV2Controller.cs b/Libraries/SPTarkov.Server.Core/Controllers/LauncherV2Controller.cs index 14cf8b94..ac941818 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/LauncherV2Controller.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/LauncherV2Controller.cs @@ -156,14 +156,14 @@ public class LauncherV2Controller( /// Gets the Servers loaded mods. /// /// - public Dictionary LoadedMods() + public Dictionary LoadedMods() { var mods = _applicationContext?.GetLatestValue(ContextVariableType.LOADED_MOD_ASSEMBLIES).GetValue>(); - var result = new Dictionary(); + var result = new Dictionary(); foreach (var sptMod in mods) { - result.Add(sptMod.PackageJson.Name, sptMod.PackageJson); + result.Add(sptMod.ModMetadata.Name, sptMod.ModMetadata); } return result; diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Launcher/LauncherV2ModsResponse.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Launcher/LauncherV2ModsResponse.cs index 152baffc..b71d91fc 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Spt/Launcher/LauncherV2ModsResponse.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Launcher/LauncherV2ModsResponse.cs @@ -5,7 +5,7 @@ namespace SPTarkov.Server.Core.Models.Spt.Launcher; public class LauncherV2ModsResponse : IRequestData { - public required Dictionary Response + public required Dictionary Response { get; set; diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/AbstractModMetadata.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/AbstractModMetadata.cs new file mode 100644 index 00000000..4d66ba6b --- /dev/null +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/AbstractModMetadata.cs @@ -0,0 +1,119 @@ +namespace SPTarkov.Server.Core.Models.Spt.Mod; + +/// +/// Represents a collection of metadata used to determine things such as author, version, +/// pre-defined load order and incompatibilities. This record is required to be overriden by all mods. +/// All properties must be overridden. For properties, that you don't need, just assign null. +/// +public abstract record AbstractModMetadata +{ + /// + /// Name of this mod + /// + public abstract string? Name + { + get; + set; + } + + /// + /// Your username + /// + public abstract string? Author + { + get; + set; + } + + /// + /// People who have contributed to this mod + /// + public abstract List? Contributors + { + get; + set; + } + + /// + /// Semantic version of this mod, this uses the semver standard + /// + public abstract string? Version + { + get; + set; + } + + /// + /// SPT version this mod was built for + /// + public abstract string? SptVersion + { + get; + set; + } + + /// + /// List of mods this mod should load before + /// + public abstract List? LoadBefore + { + get; + set; + } + + /// + /// List of mods this mod should load after + /// + public abstract List? LoadAfter + { + get; + set; + } + + /// + /// List of mods not compatible with this mod + /// + public abstract List? Incompatibilities + { + get; + set; + } + + /// + /// Dictionary of mods this mod depends on. + /// + /// Mod dependency is the key, version is the value + /// + public abstract Dictionary? ModDependencies + { + get; + set; + } + + /// + /// Link to this mod's mod page, or GitHub page + /// + public abstract string? Url + { + get; + set; + } + + /// + /// Does this mod load bundles + /// + public abstract bool? IsBundleMod + { + get; + set; + } + + /// + /// Name of the license this mod uses + /// + public abstract string? Licence + { + get; + set; + } +} diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/PackageJsonData.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/PackageJsonData.cs deleted file mode 100644 index 0444706b..00000000 --- a/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/PackageJsonData.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System.Text.Json.Serialization; - -namespace SPTarkov.Server.Core.Models.Spt.Mod; - -public record PackageJsonData -{ - [JsonPropertyName("name")] - public string? Name - { - get; - set; - } - - [JsonPropertyName("author")] - public string? Author - { - get; - set; - } - - [JsonPropertyName("contributors")] - public List? Contributors - { - get; - set; - } - - [JsonPropertyName("version")] - public string? Version - { - get; - set; - } - - [JsonPropertyName("sptVersion")] - public string? SptVersion - { - get; - set; - } - - [JsonPropertyName("loadBefore")] - public List? LoadBefore - { - get; - set; - } - - [JsonPropertyName("loadAfter")] - public List? LoadAfter - { - get; - set; - } - - [JsonPropertyName("incompatibilities")] - public List? Incompatibilities - { - get; - set; - } - - [JsonPropertyName("modDependencies")] - public Dictionary? ModDependencies - { - get; - set; - } - - [JsonPropertyName("url")] - public string? Url - { - get; - set; - } - - [JsonPropertyName("isBundleMod")] - public bool? IsBundleMod - { - get; - set; - } - - [JsonPropertyName("licence")] - public string? Licence - { - get; - set; - } -} diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/SptMod.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/SptMod.cs index 515e66b0..b5556fbb 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/SptMod.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Mod/SptMod.cs @@ -12,8 +12,8 @@ public class SptMod set; } - [JsonPropertyName("packageJson")] - public PackageJsonData? PackageJson + [JsonPropertyName("modMetadata")] + public AbstractModMetadata? ModMetadata { get; set; diff --git a/Libraries/SPTarkov.Server.Core/Services/BackupService.cs b/Libraries/SPTarkov.Server.Core/Services/BackupService.cs index dd380fdb..0dc2d649 100644 --- a/Libraries/SPTarkov.Server.Core/Services/BackupService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/BackupService.cs @@ -316,7 +316,7 @@ public class BackupService foreach (var mod in mods) { - result.Add($"{mod.PackageJson.Author} - {mod.PackageJson.Version ?? ""}"); + result.Add($"{mod.ModMetadata.Author} - {mod.ModMetadata.Version ?? ""}"); } return result; diff --git a/SPTarkov.Server/Modding/ModDllLoader.cs b/SPTarkov.Server/Modding/ModDllLoader.cs index d8ce5801..556fa521 100644 --- a/SPTarkov.Server/Modding/ModDllLoader.cs +++ b/SPTarkov.Server/Modding/ModDllLoader.cs @@ -1,5 +1,5 @@ +using System.Reflection; using System.Runtime.Loader; -using System.Text.Json; using SPTarkov.Server.Core.Models.Spt.Mod; using SPTarkov.Server.Core.Utils; @@ -25,9 +25,8 @@ public class ModDllLoader // foreach directory in /user/mods/ // treat this as the MOD - // should contain a dll and Package.json - // load both - // if either is missing Throw Warning and skip + // should contain a dll + // if dll is missing Throw Warning and skip var modDirectories = Directory.GetDirectories(ModPath); @@ -48,7 +47,7 @@ public class ModDllLoader } /// - /// Check the provided directory path for a dll and package.json file, load into memory + /// Check the provided directory path for a dll, load into memory /// /// Directory path that contains mod files /// SptMod @@ -60,24 +59,8 @@ public class ModDllLoader Assemblies = [] }; var assemblyCount = 0; - var packageJsonCount = 0; foreach (var file in new DirectoryInfo(path).GetFiles()) // Only search top level { - if (string.Equals(file.Name, "package.json", StringComparison.OrdinalIgnoreCase)) - { - packageJsonCount++; - - // Handle package.json - var rawJson = File.ReadAllText(file.FullName); - result.PackageJson = JsonSerializer.Deserialize(rawJson); - if (packageJsonCount > 1) - { - throw new Exception($"More than one package.json file found in path: {path}"); - } - - continue; - } - if (string.Equals(file.Extension, ".dll", StringComparison.OrdinalIgnoreCase)) { assemblyCount++; @@ -85,26 +68,55 @@ public class ModDllLoader } } - if (assemblyCount == 0 && packageJsonCount == 0) - { - throw new Exception($"No Assembly or package.json found in: {Path.GetFullPath(path)}"); - } - - if (packageJsonCount == 0) - { - throw new Exception($"No package.json found in path: {Path.GetFullPath(path)}"); - } - if (assemblyCount == 0) { throw new Exception($"No Assemblies found in path: {Path.GetFullPath(path)}"); } - if (result.PackageJson?.Name == null || result.PackageJson?.Author == null || - result.PackageJson?.Version == null || result.PackageJson?.Licence == null || - result.PackageJson?.SptVersion == null) + result.ModMetadata = LoadModMetadata(result.Assemblies, path); + + if (result.ModMetadata == null) { - throw new Exception($"The package.json file for: {path} is missing one of these properties: name, author, licence, version or sptVersion"); + throw new Exception($"Failed to load mod metadata for: {Path.GetFullPath(path)} \ndid you override `AbstractModMetadata`?"); + } + + if (result.ModMetadata?.Name == null || result.ModMetadata?.Author == null || + result.ModMetadata?.Version == null || result.ModMetadata?.Licence == null || + result.ModMetadata?.SptVersion == null) + { + throw new Exception($"The mod metadata for: {Path.GetFullPath(path)} is missing one of these properties: name, author, licence, version or sptVersion"); + } + + return result; + } + + /// + /// Finds and returns the mod metadata for this mod + /// + /// All mod assemblies + /// Path of the mod directory + /// Mod metadata + /// Thrown if duplicate metadata implementations are found + private static AbstractModMetadata? LoadModMetadata(List assemblies, string path) + { + AbstractModMetadata? result = null; + + foreach (var allAsmModules in assemblies.Select(a => a.Modules)) + { + foreach (var module in allAsmModules) + { + var modMetadata = module.GetTypes().SingleOrDefault(t => typeof(AbstractModMetadata).IsAssignableFrom(t)); + + if (result != null && modMetadata != null) + { + throw new Exception($"Duplicate mod metadata found for mod at path: {Path.GetFullPath(path)}"); + } + + if (modMetadata != null) + { + result = (AbstractModMetadata)Activator.CreateInstance(modMetadata)!; + } + } } return result; diff --git a/SPTarkov.Server/Modding/ModLoadOrder.cs b/SPTarkov.Server/Modding/ModLoadOrder.cs index 76089d15..22a679db 100644 --- a/SPTarkov.Server/Modding/ModLoadOrder.cs +++ b/SPTarkov.Server/Modding/ModLoadOrder.cs @@ -5,15 +5,15 @@ namespace SPTarkov.Server.Modding; public class ModLoadOrder(ICloner cloner) { - protected Dictionary loadOrder = new(); - protected Dictionary mods = new(); - protected Dictionary modsAvailable = new(); + protected Dictionary loadOrder = new(); + protected Dictionary mods = new(); + protected Dictionary modsAvailable = new(); - public Dictionary SetModList(Dictionary mods) + public Dictionary SetModList(Dictionary mods) { this.mods = mods; modsAvailable = cloner.Clone(this.mods); - loadOrder = new Dictionary(); + loadOrder = new Dictionary(); var visited = new HashSet(); diff --git a/SPTarkov.Server/Modding/ModValidator.cs b/SPTarkov.Server/Modding/ModValidator.cs index dfa4fe08..ff4377d5 100644 --- a/SPTarkov.Server/Modding/ModValidator.cs +++ b/SPTarkov.Server/Modding/ModValidator.cs @@ -32,7 +32,7 @@ public class ModValidator( { ValidateMods(mods); - var sortedModLoadOrder = modLoadOrder.SetModList(imported.ToDictionary(m => m.Key, m => m.Value.PackageJson)); + var sortedModLoadOrder = modLoadOrder.SetModList(imported.ToDictionary(m => m.Key, m => m.Value.ModMetadata)); var finalList = new List(); foreach (var orderMod in SortModsLoadOrder()) { @@ -90,7 +90,7 @@ public class ModValidator( // Validate and remove broken mods from mod list var validMods = GetValidMods(mods); - var modPackageData = validMods.ToDictionary(m => m.PackageJson!.Name!, m => m.PackageJson!); + var modPackageData = validMods.ToDictionary(m => m.ModMetadata!.Name!, m => m.ModMetadata!); CheckForDuplicateMods(modPackageData); // Used to check all errors before stopping the load execution @@ -145,7 +145,7 @@ public class ModValidator( // add mods foreach (var mod in validMods) { - if (ShouldSkipMod(mod.PackageJson)) + if (ShouldSkipMod(mod.ModMetadata)) { logger.Warning(localisationService.GetText("modloader-skipped_mod", new { @@ -161,15 +161,15 @@ 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.PackageJson!.Name!, out var previndex)) + if (!order.TryGetValue(prev.ModMetadata!.Name!, out var previndex)) { - missingFromOrderJson[prev.PackageJson.Name!] = true; + missingFromOrderJson[prev.ModMetadata.Name!] = true; return 1; } - if (!order.TryGetValue(next.PackageJson!.Name!, out var nextindex)) + if (!order.TryGetValue(next.ModMetadata!.Name!, out var nextindex)) { - missingFromOrderJson[next.PackageJson.Name!] = true; + missingFromOrderJson[next.ModMetadata.Name!] = true; return -1; } @@ -180,9 +180,9 @@ public class ModValidator( /// Check for duplicate mods loaded, show error if any /// /// Dictionary of mod package.json data - protected void CheckForDuplicateMods(Dictionary modPackageData) + protected void CheckForDuplicateMods(Dictionary modPackageData) { - var groupedMods = new Dictionary>(); + var groupedMods = new Dictionary>(); foreach (var mod in modPackageData.Values) { @@ -219,7 +219,7 @@ public class ModValidator( /// /// Mod to check compatibility with SPT /// True if compatible - protected bool IsModCompatibleWithSpt(PackageJsonData mod) + protected bool IsModCompatibleWithSpt(AbstractModMetadata mod) { var sptVersion = ProgramStatics.SPT_VERSION() ?? sptConfig.SptVersion; var modName = $"{mod.Author}-{mod.Name}"; @@ -272,13 +272,13 @@ public class ModValidator( protected void AddMod(SptMod mod) { // Add mod to imported list - imported.Add(mod.PackageJson.Name, mod); + imported.Add(mod.ModMetadata.Name, mod); logger.Info( localisationService.GetText("modloader-loaded_mod", new { - name = mod.PackageJson.Name, - version = mod.PackageJson.Version, - author = mod.PackageJson.Author + name = mod.ModMetadata.Name, + version = mod.ModMetadata.Version, + author = mod.ModMetadata.Author }) ); } @@ -288,12 +288,12 @@ public class ModValidator( /// /// mod package.json data /// - protected bool ShouldSkipMod(PackageJsonData pkg) + protected bool ShouldSkipMod(AbstractModMetadata pkg) { return skippedMods.Contains($"{pkg.Author}-{pkg.Name}"); } - protected bool AreModDependenciesFulfilled(PackageJsonData pkg, Dictionary loadedMods) + protected bool AreModDependenciesFulfilled(AbstractModMetadata pkg, Dictionary loadedMods) { if (pkg.ModDependencies == null) { @@ -336,7 +336,7 @@ public class ModValidator( return true; } - protected bool IsModCompatible(PackageJsonData mod, Dictionary loadedMods) + protected bool IsModCompatible(AbstractModMetadata mod, Dictionary loadedMods) { var incompatbileModsList = mod.Incompatibilities; if (incompatbileModsList == null) @@ -371,7 +371,7 @@ public class ModValidator( /// true if valid protected bool ValidMod(SptMod mod) { - var modName = mod.PackageJson.Name; + var modName = mod.ModMetadata.Name; var modPath = GetModPath(modName); var modIsCalledBepinEx = string.Equals(modName, "bepinex", StringComparison.OrdinalIgnoreCase); @@ -402,7 +402,7 @@ public class ModValidator( } // Validate mod - var config = mod.PackageJson; + var config = mod.ModMetadata; var issue = false; if (!semVer.IsValid(config.Version))