Merge branch 'develop' of https://github.com/sp-tarkov/server-csharp into develop

This commit is contained in:
Chomp
2025-05-08 20:05:44 +01:00
16 changed files with 254 additions and 167 deletions
@@ -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;
@@ -908,7 +908,7 @@ public class LocationLifecycleService
{
// Exclude completed quests
var activeQuestIdsInProfile = profileQuests
.Where(quest => quest.Status is QuestStatusEnum.AvailableForStart or QuestStatusEnum.Success)
.Where(quest => quest.Status is not QuestStatusEnum.AvailableForStart and not QuestStatusEnum.Success)
.Select(status => status.QId);
// Get db details of quests we found above
@@ -28,7 +28,7 @@ public partial class HashUtil(RandomUtil _randomUtil)
objectId[3] = (byte) timestamp;
// Random value (5 bytes)
_randomUtil.Random.NextBytes(objectId.Slice(4, 5));
_randomUtil.NextBytes(objectId.Slice(4, 5));
// Incrementing counter (3 bytes)
// 24-bit counter
@@ -18,6 +18,7 @@ namespace SPTarkov.Server.Core.Utils.Json.Converters;
public class BaseInteractionRequestDataConverter : JsonConverter<BaseInteractionRequestData>
{
private static Dictionary<string, Func<string, BaseInteractionRequestData?>> _modHandlers = new();
public override BaseInteractionRequestData? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var jsonDocument = JsonDocument.ParseValue(ref reader);
@@ -177,12 +178,24 @@ public class BaseInteractionRequestDataConverter : JsonConverter<BaseInteraction
case ItemEventActions.PIN_LOCK:
return JsonSerializer.Deserialize<PinOrLockItemRequest>(jsonText);
default:
if (_modHandlers.TryGetValue(action, out var handler))
{
return handler(jsonText);
}
throw new Exception(
$"Unhandled action type {action}, make sure the BaseInteractionRequestDataConverter has the deserialization for this action handled."
);
}
}
public static void RegisterModDataHandler(string action, Func<string, BaseInteractionRequestData?> handler)
{
if (!_modHandlers.TryAdd(action, handler))
{
throw new Exception($"Unable to register action {action} to BaseInteractionRequestDataConverter as it already exists.");
}
}
public override void Write(Utf8JsonWriter writer, BaseInteractionRequestData value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, options);
@@ -35,7 +35,7 @@ public class RandomUtil(ISptLogger<RandomUtil> _logger, ICloner _cloner)
max -= 1;
}
return max > min ? Random.Next(min, exclusive ? max : max + 1) : min;
return max > min ? Random.Shared.Next(min, exclusive ? max : max + 1) : min;
}
/// <summary>
@@ -64,6 +64,11 @@ public class RandomUtil(ISptLogger<RandomUtil> _logger, ICloner _cloner)
return Random.Next(0, 2) == 1;
}
public void NextBytes(Span<byte> bytes)
{
Random.Shared.NextBytes(bytes);
}
/// <summary>
/// Calculates the percentage of a given number and returns the result.
/// </summary>
+47 -35
View File
@@ -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 -5
View File
@@ -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>();
+19 -19
View File
@@ -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))
+28
View File
@@ -1,3 +1,5 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using SPTarkov.Server.Core.Utils;
using SPTarkov.Server.Core.Utils.Cloners;
using UnitTests.Mock;
@@ -63,4 +65,30 @@ public class HashUtilTests
failMessage
);
}
[TestMethod]
public void MultiThreadedMongoIDGenerationTest()
{
var concurrentBag = new ConcurrentBag<string>();
var random = new Random();
var stopwatch = new Stopwatch();
stopwatch.Start();
Parallel.For(0, 1000, i =>
{
Thread.Sleep(random.Next(0, 10));
var mongoId = _hashUtil.Generate();
concurrentBag.Add(mongoId);
});
stopwatch.Stop();
Console.WriteLine($"Elapsed time: {stopwatch.ElapsedMilliseconds} ms");
var uniqueCount = concurrentBag.Distinct().Count();
var totalCount = concurrentBag.Count;
Assert.AreEqual(
totalCount,
uniqueCount,
$"Expected all generated MongoId's to be unique, but found {totalCount - uniqueCount} duplicates."
);
}
}