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))