Merge pull request #248 from CJ-SPT/mod-metadata
Implement loading mod metadata from the mod's assembly
This commit is contained in:
@@ -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()
|
||||
}
|
||||
);
|
||||
|
||||
@@ -240,14 +240,14 @@ public class LauncherController(
|
||||
/// Get the mods the server has currently loaded
|
||||
/// </summary>
|
||||
/// <returns>Dictionary of mod name and mod details</returns>
|
||||
public Dictionary<string, PackageJsonData> GetLoadedServerMods()
|
||||
public Dictionary<string, AbstractModMetadata> GetLoadedServerMods()
|
||||
{
|
||||
var mods = _applicationContext?.GetLatestValue(ContextVariableType.LOADED_MOD_ASSEMBLIES).GetValue<List<SptMod>>();
|
||||
var result = new Dictionary<string, PackageJsonData>();
|
||||
var result = new Dictionary<string, AbstractModMetadata>();
|
||||
|
||||
foreach (var sptMod in mods)
|
||||
{
|
||||
result.Add(sptMod.PackageJson.Name, sptMod.PackageJson);
|
||||
result.Add(sptMod.ModMetadata.Name, sptMod.ModMetadata);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -156,14 +156,14 @@ public class LauncherV2Controller(
|
||||
/// Gets the Servers loaded mods.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Dictionary<string, PackageJsonData> LoadedMods()
|
||||
public Dictionary<string, AbstractModMetadata> LoadedMods()
|
||||
{
|
||||
var mods = _applicationContext?.GetLatestValue(ContextVariableType.LOADED_MOD_ASSEMBLIES).GetValue<List<SptMod>>();
|
||||
var result = new Dictionary<string, PackageJsonData>();
|
||||
var result = new Dictionary<string, AbstractModMetadata>();
|
||||
|
||||
foreach (var sptMod in mods)
|
||||
{
|
||||
result.Add(sptMod.PackageJson.Name, sptMod.PackageJson);
|
||||
result.Add(sptMod.ModMetadata.Name, sptMod.ModMetadata);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace SPTarkov.Server.Core.Models.Spt.Launcher;
|
||||
|
||||
public class LauncherV2ModsResponse : IRequestData
|
||||
{
|
||||
public required Dictionary<string, PackageJsonData> Response
|
||||
public required Dictionary<string, AbstractModMetadata> Response
|
||||
{
|
||||
get;
|
||||
set;
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
namespace SPTarkov.Server.Core.Models.Spt.Mod;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public abstract record AbstractModMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of this mod
|
||||
/// </summary>
|
||||
public abstract string? Name
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Your username
|
||||
/// </summary>
|
||||
public abstract string? Author
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// People who have contributed to this mod
|
||||
/// </summary>
|
||||
public abstract List<string>? Contributors
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Semantic version of this mod, this uses the semver standard
|
||||
/// </summary>
|
||||
public abstract string? Version
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SPT version this mod was built for
|
||||
/// </summary>
|
||||
public abstract string? SptVersion
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// List of mods this mod should load before
|
||||
/// </summary>
|
||||
public abstract List<string>? LoadBefore
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// List of mods this mod should load after
|
||||
/// </summary>
|
||||
public abstract List<string>? LoadAfter
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// List of mods not compatible with this mod
|
||||
/// </summary>
|
||||
public abstract List<string>? Incompatibilities
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dictionary of mods this mod depends on.
|
||||
///
|
||||
/// Mod dependency is the key, version is the value
|
||||
/// </summary>
|
||||
public abstract Dictionary<string, string>? ModDependencies
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Link to this mod's mod page, or GitHub page
|
||||
/// </summary>
|
||||
public abstract string? Url
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does this mod load bundles
|
||||
/// </summary>
|
||||
public abstract bool? IsBundleMod
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Name of the license this mod uses
|
||||
/// </summary>
|
||||
public abstract string? Licence
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
@@ -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<string>? Contributors
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[JsonPropertyName("version")]
|
||||
public string? Version
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[JsonPropertyName("sptVersion")]
|
||||
public string? SptVersion
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[JsonPropertyName("loadBefore")]
|
||||
public List<string>? LoadBefore
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[JsonPropertyName("loadAfter")]
|
||||
public List<string>? LoadAfter
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[JsonPropertyName("incompatibilities")]
|
||||
public List<string>? Incompatibilities
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[JsonPropertyName("modDependencies")]
|
||||
public Dictionary<string, string>? ModDependencies
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[JsonPropertyName("url")]
|
||||
public string? Url
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[JsonPropertyName("isBundleMod")]
|
||||
public bool? IsBundleMod
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[JsonPropertyName("licence")]
|
||||
public string? Licence
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@ public class SptMod
|
||||
set;
|
||||
}
|
||||
|
||||
[JsonPropertyName("packageJson")]
|
||||
public PackageJsonData? PackageJson
|
||||
[JsonPropertyName("modMetadata")]
|
||||
public AbstractModMetadata? ModMetadata
|
||||
{
|
||||
get;
|
||||
set;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <param name="path">Directory path that contains mod files</param>
|
||||
/// <returns>SptMod</returns>
|
||||
@@ -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<PackageJsonData>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds and returns the mod metadata for this mod
|
||||
/// </summary>
|
||||
/// <param name="assemblies">All mod assemblies</param>
|
||||
/// <param name="path">Path of the mod directory</param>
|
||||
/// <returns>Mod metadata</returns>
|
||||
/// <exception cref="Exception">Thrown if duplicate metadata implementations are found</exception>
|
||||
private static AbstractModMetadata? LoadModMetadata(List<Assembly> 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;
|
||||
|
||||
@@ -5,15 +5,15 @@ namespace SPTarkov.Server.Modding;
|
||||
|
||||
public class ModLoadOrder(ICloner cloner)
|
||||
{
|
||||
protected Dictionary<string, PackageJsonData> loadOrder = new();
|
||||
protected Dictionary<string, PackageJsonData> mods = new();
|
||||
protected Dictionary<string, PackageJsonData> modsAvailable = new();
|
||||
protected Dictionary<string, AbstractModMetadata> loadOrder = new();
|
||||
protected Dictionary<string, AbstractModMetadata> mods = new();
|
||||
protected Dictionary<string, AbstractModMetadata> modsAvailable = new();
|
||||
|
||||
public Dictionary<string, PackageJsonData> SetModList(Dictionary<string, PackageJsonData> mods)
|
||||
public Dictionary<string, AbstractModMetadata> SetModList(Dictionary<string, AbstractModMetadata> mods)
|
||||
{
|
||||
this.mods = mods;
|
||||
modsAvailable = cloner.Clone(this.mods);
|
||||
loadOrder = new Dictionary<string, PackageJsonData>();
|
||||
loadOrder = new Dictionary<string, AbstractModMetadata>();
|
||||
|
||||
var visited = new HashSet<string>();
|
||||
|
||||
|
||||
@@ -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<SptMod>();
|
||||
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<string, bool> 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
|
||||
/// </summary>
|
||||
/// <param name="modPackageData">Dictionary of mod package.json data</param>
|
||||
protected void CheckForDuplicateMods(Dictionary<string, PackageJsonData> modPackageData)
|
||||
protected void CheckForDuplicateMods(Dictionary<string, AbstractModMetadata> modPackageData)
|
||||
{
|
||||
var groupedMods = new Dictionary<string, List<PackageJsonData>>();
|
||||
var groupedMods = new Dictionary<string, List<AbstractModMetadata>>();
|
||||
|
||||
foreach (var mod in modPackageData.Values)
|
||||
{
|
||||
@@ -219,7 +219,7 @@ public class ModValidator(
|
||||
/// </summary>
|
||||
/// <param name="mod">Mod to check compatibility with SPT</param>
|
||||
/// <returns>True if compatible</returns>
|
||||
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(
|
||||
/// </summary>
|
||||
/// <param name="pkg">mod package.json data</param>
|
||||
/// <returns></returns>
|
||||
protected bool ShouldSkipMod(PackageJsonData pkg)
|
||||
protected bool ShouldSkipMod(AbstractModMetadata pkg)
|
||||
{
|
||||
return skippedMods.Contains($"{pkg.Author}-{pkg.Name}");
|
||||
}
|
||||
|
||||
protected bool AreModDependenciesFulfilled(PackageJsonData pkg, Dictionary<string, PackageJsonData> loadedMods)
|
||||
protected bool AreModDependenciesFulfilled(AbstractModMetadata pkg, Dictionary<string, AbstractModMetadata> loadedMods)
|
||||
{
|
||||
if (pkg.ModDependencies == null)
|
||||
{
|
||||
@@ -336,7 +336,7 @@ public class ModValidator(
|
||||
return true;
|
||||
}
|
||||
|
||||
protected bool IsModCompatible(PackageJsonData mod, Dictionary<string, PackageJsonData> loadedMods)
|
||||
protected bool IsModCompatible(AbstractModMetadata mod, Dictionary<string, AbstractModMetadata> loadedMods)
|
||||
{
|
||||
var incompatbileModsList = mod.Incompatibilities;
|
||||
if (incompatbileModsList == null)
|
||||
@@ -371,7 +371,7 @@ public class ModValidator(
|
||||
/// <returns>true if valid</returns>
|
||||
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))
|
||||
|
||||
Reference in New Issue
Block a user