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

This commit is contained in:
Alex
2025-05-15 19:53:34 +01:00
70 changed files with 886 additions and 347 deletions
@@ -0,0 +1,46 @@
using System.Reflection.Emit;
namespace SPTarkov.Reflection.CodeWrapper;
public class Code
{
public OpCode OpCode { get; }
public Type? CallerType { get; }
public object? OperandTarget { get; }
public Type[]? Parameters { get; }
public bool HasOperand { get; }
public Code(OpCode opCode)
{
OpCode = opCode;
HasOperand = false;
}
public Code(OpCode opCode, object operandTarget)
{
OpCode = opCode;
OperandTarget = operandTarget;
HasOperand = true;
}
public Code(OpCode opCode, Type callerType)
{
OpCode = opCode;
CallerType = callerType;
HasOperand = true;
}
public Code(OpCode opCode, Type callerType, object operandTarget, Type[] parameters = null)
{
OpCode = opCode;
CallerType = callerType;
OperandTarget = operandTarget;
Parameters = parameters;
HasOperand = true;
}
public virtual Label? GetLabel()
{
return null;
}
}
@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Emit;
using HarmonyLib;
namespace SPTarkov.Reflection.CodeWrapper;
/// <summary>
/// Helper class to generate IL code for transpilers
/// </summary>
public class CodeGenerator
{
public static List<CodeInstruction> GenerateInstructions(List<Code> codes)
{
var list = new List<CodeInstruction>();
foreach (Code code in codes)
{
list.Add(ParseCode(code));
}
return list;
}
private static CodeInstruction ParseCode(Code code)
{
if (!code.HasOperand)
{
return new CodeInstruction(code.OpCode) { labels = GetLabelList(code) };
}
if (code.OpCode == OpCodes.Ldfld || code.OpCode == OpCodes.Ldflda || code.OpCode == OpCodes.Stfld)
{
return new CodeInstruction(code.OpCode, AccessTools.Field(code.CallerType, code.OperandTarget as string)) { labels = GetLabelList(code) };
}
if (code.OpCode == OpCodes.Call || code.OpCode == OpCodes.Callvirt)
{
return new CodeInstruction(code.OpCode, AccessTools.Method(code.CallerType, code.OperandTarget as string, code.Parameters)) { labels = GetLabelList(code) };
}
if (code.OpCode == OpCodes.Box)
{
return new CodeInstruction(code.OpCode, code.CallerType) { labels = GetLabelList(code) };
}
if (code.OpCode == OpCodes.Br || code.OpCode == OpCodes.Brfalse || code.OpCode == OpCodes.Brtrue || code.OpCode == OpCodes.Brtrue_S
|| code.OpCode == OpCodes.Brfalse_S || code.OpCode == OpCodes.Br_S)
{
return new CodeInstruction(code.OpCode, code.OperandTarget) { labels = GetLabelList(code) };
}
if (code.OpCode == OpCodes.Ldftn)
{
return new CodeInstruction(code.OpCode, AccessTools.Method(code.CallerType, code.OperandTarget as string, code.Parameters)) { labels = GetLabelList(code) };
}
if (code.OpCode == OpCodes.Newobj)
{
return new CodeInstruction(code.OpCode, code.CallerType.GetConstructors().FirstOrDefault(x => x.GetParameters().Length == code.Parameters.Length)) { labels = GetLabelList(code) };
}
throw new ArgumentException($"Code with OpCode {code.OpCode.ToString()} is not supported.");
}
private static List<Label> GetLabelList(Code code)
{
if (code.GetLabel() == null)
{
return new List<Label>();
}
return [ (Label)code.GetLabel() ];
}
}
@@ -0,0 +1,33 @@
using System.Reflection.Emit;
namespace SPTarkov.Reflection.CodeWrapper;
public class CodeWithLabel : Code
{
public Label Label { get; }
public CodeWithLabel(OpCode opCode, Label label) : base(opCode)
{
Label = label;
}
public CodeWithLabel(OpCode opCode, Label label, object operandTarget) : base(opCode, operandTarget)
{
Label = label;
}
public CodeWithLabel(OpCode opCode, Label label, Type callerType) : base(opCode, callerType)
{
Label = label;
}
public CodeWithLabel(OpCode opCode, Label label, Type callerType, object operandTarget, Type[] parameters = null) : base(opCode, callerType, operandTarget, parameters)
{
Label = label;
}
public override Label? GetLabel()
{
return Label;
}
}
@@ -0,0 +1,132 @@
using System.Reflection;
using HarmonyLib;
namespace SPTarkov.Reflection.Patching;
public abstract class AbstractPatch
{
private readonly Harmony _harmony;
private readonly List<HarmonyMethod> _prefixList;
private readonly List<HarmonyMethod> _postfixList;
private readonly List<HarmonyMethod> _transpilerList;
private readonly List<HarmonyMethod> _finalizerList;
private readonly List<HarmonyMethod> _ilManipulatorList;
/// <summary>
/// Constructor
/// </summary>
/// <param name="name">Name</param>
protected AbstractPatch(string? name = null)
{
_harmony = new Harmony(name ?? GetType().Name);
_prefixList = GetPatchMethods(typeof(PatchPrefixAttribute));
_postfixList = GetPatchMethods(typeof(PatchPostfixAttribute));
_transpilerList = GetPatchMethods(typeof(PatchTranspilerAttribute));
_finalizerList = GetPatchMethods(typeof(PatchFinalizerAttribute));
_ilManipulatorList = GetPatchMethods(typeof(PatchIlManipulatorAttribute));
if (_prefixList.Count == 0
&& _postfixList.Count == 0
&& _transpilerList.Count == 0
&& _finalizerList.Count == 0
&& _ilManipulatorList.Count == 0)
{
throw new Exception($"{_harmony.Id}: At least one of the patch methods must be specified");
}
}
/// <summary>
/// Get original method
/// </summary>
/// <returns>Method</returns>
protected abstract MethodBase GetTargetMethod();
/// <summary>
/// Get HarmonyMethod from string
/// </summary>
/// <param name="attributeType">Attribute type</param>
/// <returns>Method</returns>
private List<HarmonyMethod> GetPatchMethods(Type attributeType)
{
var T = GetType();
var methods = new List<HarmonyMethod>();
foreach (var method in T.GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public |
BindingFlags.DeclaredOnly))
{
if (method.GetCustomAttribute(attributeType) != null)
{
methods.Add(new HarmonyMethod(method));
}
}
return methods;
}
/// <summary>
/// Apply patch to target
/// </summary>
public void Enable()
{
var target = GetTargetMethod();
if (target == null)
{
throw new InvalidOperationException($"{_harmony.Id}: TargetMethod is null");
}
try
{
foreach (var prefix in _prefixList)
{
_harmony.Patch(target, prefix: prefix);
}
foreach (var postfix in _postfixList)
{
_harmony.Patch(target, postfix: postfix);
}
foreach (var transpiler in _transpilerList)
{
_harmony.Patch(target, transpiler: transpiler);
}
foreach (var finalizer in _finalizerList)
{
_harmony.Patch(target, finalizer: finalizer);
}
foreach (var ilmanipulator in _ilManipulatorList)
{
_harmony.Patch(target, ilmanipulator: ilmanipulator);
}
}
catch (Exception ex)
{
throw new Exception($"{_harmony.Id}:", ex);
}
}
/// <summary>
/// Remove applied patch from target
/// </summary>
public void Disable()
{
var target = GetTargetMethod();
if (target == null)
{
throw new InvalidOperationException($"{_harmony.Id}: TargetMethod is null");
}
try
{
_harmony.Unpatch(target, HarmonyPatchType.All, _harmony.Id);
}
catch (Exception ex)
{
throw new Exception($"{_harmony.Id}:", ex);
}
}
}
@@ -0,0 +1,27 @@
namespace SPTarkov.Reflection.Patching
{
[AttributeUsage(AttributeTargets.Method)]
public class PatchPrefixAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Method)]
public class PatchPostfixAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Method)]
public class PatchTranspilerAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Method)]
public class PatchFinalizerAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Method)]
public class PatchIlManipulatorAttribute : Attribute
{
}
}
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\Build.props"/>
<PropertyGroup>
<PackageId>SPTarkov.Reflection</PackageId>
<Authors>Single Player Tarkov</Authors>
<Description>Reflection library for the Single Player Tarkov server.</Description>
<Copyright>Copyright (c) Single Player Tarkov 2025</Copyright>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl>https://sp-tarkov.com</PackageProjectUrl>
<RepositoryUrl>https://github.com/sp-tarkov/server-csharp</RepositoryUrl>
<ImplicitUsings>enable</ImplicitUsings>
<OutputType>Library</OutputType>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<None Include="..\..\LICENSE" Pack="true" Visible="false" PackagePath=""/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="HarmonyX" Version="2.14.0"/>
</ItemGroup>
</Project>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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()
}
);
@@ -31,7 +31,6 @@ public class InsuranceController(
ItemHelper _itemHelper,
ProfileHelper _profileHelper,
WeightedRandomHelper _weightedRandomHelper,
TraderHelper _traderHelper,
PaymentService _paymentService,
InsuranceService _insuranceService,
DatabaseService _databaseService,
@@ -39,6 +38,7 @@ public class InsuranceController(
RagfairPriceService _ragfairPriceService,
LocalisationService _localisationService,
SaveServer _saveServer,
TraderStore _traderStore,
ConfigServer _configServer,
ICloner _cloner
)
@@ -643,7 +643,7 @@ public class InsuranceController(
/// <returns>Should item be deleted</returns>
protected bool? RollForDelete(string traderId, Item? insuredItem = null)
{
var trader = _traderHelper.GetTraderById(traderId);
var trader = _traderStore.GetTraderById(traderId);
if (trader is null)
{
return null;
@@ -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;
@@ -7,6 +7,7 @@ public static class OnLoadOrder
public const int PostSptDatabase = 2;
public const int GameCallbacks = 100;
public const int PostDBModLoader = 200;
public const int TraderRegistration = 250;
public const int HandbookCallbacks = 300;
public const int HttpCallbacks = 400;
public const int SaveCallbacks = 500;
@@ -25,6 +25,7 @@ public class TraderHelper(
PlayerService _playerService,
LocalisationService _localisationService,
FenceService _fenceService,
TraderStore _traderStore,
TimeUtil _timeUtil,
RandomUtil _randomUtil,
ConfigServer _configServer
@@ -520,16 +521,16 @@ public class TraderHelper(
}
// Init dict and fill
foreach (var traderName in Traders.TradersDictionary)
foreach (var trader in _traderStore.GetAllTraders())
{
// Skip some traders
if (traderName.Value == Traders.FENCE)
if (trader.Id == Traders.FENCE)
{
continue;
}
// Get assorts for trader, skip trader if no assorts found
var traderAssorts = _databaseService.GetTrader(traderName.Value).Assort;
var traderAssorts = _databaseService.GetTrader(trader.Id).Assort;
if (traderAssorts is null)
{
continue;
@@ -566,10 +567,10 @@ public class TraderHelper(
{
// Find largest trader price for item
var highestPrice = 1d; // Default price
foreach (var trader in Traders.TradersDictionary)
foreach (var trader in _traderStore.GetAllTraders())
{
// Get trader and check buy category allows tpl
var traderBase = _databaseService.GetTrader(trader.Value).Base;
var traderBase = _databaseService.GetTrader(trader.Id).Base;
// Skip traders that don't sell this category of item
if (traderBase is null || !_itemHelper.IsOfBaseclasses(tpl, traderBase.ItemsBuy.Category))
@@ -595,63 +596,13 @@ public class TraderHelper(
return highestPrice;
}
/// <summary>
/// Get a trader enum key by its value
/// </summary>
/// <param name="traderId">Traders id</param>
/// <returns>Traders key</returns>
public TradersEnum? GetTraderById(string traderId)
{
var kvp = Traders.TradersDictionary.Where(x => x.Value == traderId);
if (!kvp.Any())
{
_logger.Error(_localisationService.GetText("trader-unable_to_find_trader_in_enum", traderId));
return null;
}
return kvp.FirstOrDefault().Key;
}
/// <summary>
/// Validates that the provided traderEnumValue exists in the Traders enum. If the value is valid, it returns the
/// same enum value, effectively serving as a trader ID; otherwise, it logs an error and returns an empty string.
/// This method provides a runtime check to prevent undefined behavior when using the enum as a dictionary key.
/// For example, instead of this:
/// const traderId = Traders[Traders.PRAPOR];
/// You can use safely use this:
/// const traderId = this.traderHelper.getValidTraderIdByEnumValue(Traders.PRAPOR);
/// </summary>
/// <param name="traderEnumValue">The trader enum value to validate</param>
/// <returns>The validated trader enum value as a string, or an empty string if invalid</returns>
/// TODO: might not be needed
public string GetValidTraderIdByEnumValue(string traderEnumValue)
{
var traderId = _databaseService.GetTraders();
var id = traderId.FirstOrDefault(x =>
x.Value.Base.Id == traderEnumValue || string.Equals(x.Value.Base.Nickname, traderEnumValue, StringComparison.OrdinalIgnoreCase)).Key;
return id;
}
/// <summary>
/// Does the 'Traders' enum has a value that matches the passed in parameter
/// </summary>
/// <param name="key">Value to check for</param>
/// <returns>True, values exists in Traders enum as a value</returns>
public bool TraderEnumHasKey(string key)
{
return Traders.TradersDictionary.Any(x => x.Value == key);
}
/// <summary>
/// Accepts a trader id
/// </summary>
/// <param name="traderId">Trader id</param>
/// <returns>True if Traders enum has the param as a value</returns>
public bool TraderEnumHasValue(string traderId)
/// <returns>True if a Trader exists with given ID</returns>
public bool TraderExists(string traderId)
{
return Traders.TradersDictionary.ContainsValue(traderId);
return _traderStore.GetTraderById(traderId) != null;
}
}
@@ -13,38 +13,4 @@ public static class Traders
public const string LIGHTHOUSEKEEPER = "638f541a29ffd1183d187f57";
public const string BTR = "656f0f98d80a697f855d34b1";
public const string REF = "6617beeaa9cfa777ca915b7c";
public static Dictionary<TradersEnum, string> TradersDictionary
{
get;
set;
} = new()
{
{ TradersEnum.Prapor, "54cb50c76803fa8b248b4571" },
{ TradersEnum.Therapist, "54cb57776803fa99248b456e" },
{ TradersEnum.Fence, "579dc571d53a0658a154fbec" },
{ TradersEnum.Skier, "58330581ace78e27b8b10cee" },
{ TradersEnum.Peacekeeper, "5935c25fb3acc3127c3d8cd9" },
{ TradersEnum.Mechanic, "5a7c2eca46aef81a7ca2145d" },
{ TradersEnum.Ragman, "5ac3b934156ae10c4430e83c" },
{ TradersEnum.Jaeger, "5c0647fdd443bc2504c2d371" },
{ TradersEnum.LighthouseKeeper, "638f541a29ffd1183d187f57" },
{ TradersEnum.Btr, "656f0f98d80a697f855d34b1" },
{ TradersEnum.Ref, "6617beeaa9cfa777ca915b7c" }
};
}
public enum TradersEnum
{
Prapor,
Therapist,
Fence,
Skier,
Peacekeeper,
Mechanic,
Ragman,
Jaeger,
LighthouseKeeper,
Btr,
Ref
}
@@ -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;
@@ -0,0 +1,34 @@
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
using SPTarkov.Server.Core.Models.Spt.Services;
namespace SPTarkov.Server.Core.Models;
public interface ITrader
{
public string Name { get; }
public string Id { get; }
}
public abstract record ICustomTrader : ITrader
{
public abstract string Name { get; }
public abstract string Id { get; }
public abstract TraderAssort? GetAssort();
public abstract Dictionary<string, Dictionary<string, string>>? GetQuestAssort();
public abstract TraderBase? GetBase();
public virtual List<Suit>? GetSuits()
{
return null;
}
public virtual List<TraderServiceModel>? GetServices()
{
return null;
}
public virtual Dictionary<string, List<string>?>? GetDialogues()
{
return null;
}
}
@@ -0,0 +1,81 @@
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Models.Enums;
namespace SPTarkov.Server.Core.Models;
[Injectable]
public record Prapor() : ITrader
{
public string Name { get; } = "Prapor";
public string Id { get; } = Traders.PRAPOR;
}
[Injectable]
public record Therapist() : ITrader
{
public string Name { get; } = "Therapist";
public string Id { get; } = Traders.THERAPIST;
}
[Injectable]
public record Fence() : ITrader
{
public string Name { get; } = "Fence";
public string Id { get; } = Traders.FENCE;
}
[Injectable]
public record Skier() : ITrader
{
public string Name { get; } = "Skier";
public string Id { get; } = Traders.SKIER;
}
[Injectable]
public record Peacekeeper() : ITrader
{
public string Name { get; } = "Peacekeeper";
public string Id { get; } = Traders.PEACEKEEPER;
}
[Injectable]
public record Mechanic() : ITrader
{
public string Name { get; } = "Mechanic";
public string Id { get; } = Traders.MECHANIC;
}
[Injectable]
public record Ragman() : ITrader
{
public string Name { get; } = "Ragman";
public string Id { get; } = Traders.RAGMAN;
}
[Injectable]
public record Jaeger() : ITrader
{
public string Name { get; } = "Jaeger";
public string Id { get; } = Traders.JAEGER;
}
[Injectable]
public record LighthouseKeeper() : ITrader
{
public string Name { get; } = "LighthouseKeeper";
public string Id { get; } = Traders.LIGHTHOUSEKEEPER;
}
[Injectable]
public record Btr() : ITrader
{
public string Name { get; } = "Btr";
public string Id { get; } = Traders.BTR;
}
[Injectable]
public record Ref() : ITrader
{
public string Name { get; } = "Ref";
public string Id { get; } = Traders.REF;
}
@@ -17,6 +17,7 @@
<ItemGroup>
<ProjectReference Include="..\SPTarkov.DI\SPTarkov.DI.csproj"/>
<ProjectReference Include="..\SPTarkov.Reflection\SPTarkov.Reflection.csproj"/>
</ItemGroup>
<ItemGroup>
@@ -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;
@@ -1045,20 +1045,18 @@ public class FenceService(
int loyaltyLevel
)
{
var failedAttemptsCount = 0;
var weaponPresetsAddedCount = 0;
if (desiredWeaponPresetsCount > 0)
{
var weaponPresetRootItems = baseFenceAssort.Items.Where(item =>
item.Upd?.SptPresetId != null && itemHelper.IsOfBaseclass(item.Template, BaseClasses.WEAPON)
item.Upd?.SptPresetId is not null
&& itemHelper.IsOfBaseclass(item.Template, BaseClasses.WEAPON)
&& !traderConfig.Fence.Blacklist.Contains(item.Template)
);
while (weaponPresetsAddedCount < desiredWeaponPresetsCount)
{
var randomPresetRoot = randomUtil.GetArrayValue(weaponPresetRootItems);
if (traderConfig.Fence.Blacklist.Contains(randomPresetRoot.Template))
{
continue;
}
var rootItemDb = itemHelper.GetItem(randomPresetRoot.Template).Value;
var presetWithChildrenClone = _cloner.Clone(
@@ -1067,17 +1065,26 @@ public class FenceService(
RandomiseItemUpdProperties(rootItemDb, presetWithChildrenClone[0]);
// Simulate players listing weapons with parts removed
RemoveRandomModsOfItem(presetWithChildrenClone);
// Check chosen item is below price cap
var itemPrice = handbookHelper.GetTemplatePriceForItems(presetWithChildrenClone) *
// Check chosen preset is below listing cap in config
var presetPrice = handbookHelper.GetTemplatePriceForItems(presetWithChildrenClone) *
itemHelper.GetItemQualityModifierForItems(presetWithChildrenClone);
if (traderConfig.Fence.ItemCategoryRoublePriceLimit.TryGetValue(rootItemDb.Parent, out var priceLimitRouble))
{
if (itemPrice > priceLimitRouble)
if (presetPrice > priceLimitRouble)
// Too expensive, try again
{
failedAttemptsCount++;
if (failedAttemptsCount > 25)
{
logger.Warning($"Unable to add: {desiredWeaponPresetsCount} presets to Fence as all presets found after 25 attempts were too expensive.");
break;
}
continue;
}
}
@@ -1085,7 +1092,7 @@ public class FenceService(
itemHelper.ReparentItemAndChildren(presetWithChildrenClone[0], presetWithChildrenClone);
itemHelper.RemapRootItemId(presetWithChildrenClone);
// Remapping IDs causes parentid to be altered
// Remapping IDs causes parentId to be altered, fix
presetWithChildrenClone[0].ParentId = "hideout";
assorts.SptItems.Add(presetWithChildrenClone);
@@ -1098,7 +1105,7 @@ public class FenceService(
new BarterScheme
{
Template = Money.ROUBLES,
Count = Math.Round(itemPrice)
Count = Math.Round(presetPrice)
}
]
];
@@ -1445,16 +1452,14 @@ public class FenceService(
{
itemToAdjust.Upd.MedKit = new UpdMedKit
{
HpResource = randomUtil.GetInt(1, (int) itemDetails.Properties.MaxHpResource)
HpResource = randomUtil.GetInt(1, itemDetails.Properties.MaxHpResource.Value)
};
}
// Randomise armor durability
if (
(itemDetails.Parent == BaseClasses.ARMORED_EQUIPMENT ||
itemDetails.Parent == BaseClasses.FACECOVER ||
itemDetails.Parent == BaseClasses.ARMOR_PLATE) &&
(itemDetails.Properties.MaxDurability ?? 0) > 0
itemDetails.Parent is BaseClasses.ARMORED_EQUIPMENT or BaseClasses.FACECOVER or BaseClasses.ARMOR_PLATE
&& itemDetails.Properties.MaxDurability.GetValueOrDefault(0) > 0
)
{
var values = GetRandomisedArmorDurabilityValues(
@@ -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
@@ -42,7 +42,7 @@ public class PaymentService(
{
// May need to convert to trader currency
var trader = _traderHelper.GetTrader(request.TransactionId, sessionID);
var payToTrader = _traderHelper.TraderEnumHasValue(request.TransactionId);
var payToTrader = _traderHelper.TraderExists(request.TransactionId);
// Track the amounts of each type of currency involved in the trade.
var currencyAmounts = new Dictionary<string, double?>();
@@ -666,7 +666,7 @@ public class ProfileFixerService(
foreach (var activeQuest in repeatable.ActiveQuests)
{
if (!_traderHelper.TraderEnumHasValue(activeQuest.TraderId))
if (!_traderHelper.TraderExists(activeQuest.TraderId))
{
_logger.Error(_localisationService.GetText("fixer-trader_found", activeQuest.TraderId));
if (_coreConfig.Fixes.RemoveModItemsFromProfile)
@@ -701,7 +701,7 @@ public class ProfileFixerService(
}
foreach (var TraderPurchaseKvP in fullProfile.TraderPurchases
.Where(TraderPurchase => !_traderHelper.TraderEnumHasValue(TraderPurchase.Key)))
.Where(TraderPurchase => !_traderHelper.TraderExists(TraderPurchase.Key)))
{
_logger.Error(_localisationService.GetText("fixer-trader_found", TraderPurchaseKvP.Key));
if (_coreConfig.Fixes.RemoveModItemsFromProfile)
@@ -893,7 +893,7 @@ public class ProfileFixerService(
foreach (var traderKvP in fullProfile.CharacterData?.PmcData?.TradersInfo)
{
var traderId = traderKvP.Key;
if (!_traderHelper.TraderEnumHasValue(traderId))
if (!_traderHelper.TraderExists(traderId))
{
_logger.Error(_localisationService.GetText("fixer-trader_found", traderId));
if (_coreConfig.Fixes.RemoveInvalidTradersFromProfile)
@@ -907,7 +907,7 @@ public class ProfileFixerService(
foreach (var traderKvP in fullProfile.CharacterData.ScavData?.TradersInfo)
{
var traderId = traderKvP.Key;
if (!_traderHelper.TraderEnumHasValue(traderId))
if (!_traderHelper.TraderExists(traderId))
{
_logger.Error(_localisationService.GetText("fixer-trader_found", traderId));
if (_coreConfig.Fixes.RemoveInvalidTradersFromProfile)
@@ -0,0 +1,98 @@
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.DI;
using SPTarkov.Server.Core.Models;
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
using SPTarkov.Server.Core.Models.Utils;
namespace SPTarkov.Server.Core.Services;
/// <summary>
/// Source of truth for all default traders as well as any additional trader a server mod may add.
/// </summary>
[Injectable(InjectionType.Singleton, TypePriority = OnLoadOrder.TraderRegistration)]
public class TraderStore : IOnLoad
{
private readonly DatabaseService _databaseService;
private readonly IEnumerable<ITrader> _injectedTraders;
private readonly ISptLogger<TraderStore> _logger;
private readonly Dictionary<string, ITrader> _traders = new();
public TraderStore(DatabaseService databaseService,
IEnumerable<ITrader> injectedTraders,
ISptLogger<TraderStore> logger)
{
_databaseService = databaseService;
_injectedTraders = injectedTraders;
_logger = logger;
}
public Task OnLoad()
{
_logger.Info("Importing traders...");
var customTraders = 0;
foreach (var trader in _injectedTraders)
{
if (trader is ICustomTrader customTrader)
{
try
{
var dbTrader = new Trader()
{
Assort = customTrader.GetAssort(),
Base = customTrader.GetBase(),
QuestAssort = customTrader.GetQuestAssort(),
Dialogue = customTrader.GetDialogues(),
Suits = customTrader.GetSuits(),
Services = customTrader.GetServices(),
};
_databaseService.GetTraders().Add(trader.Id, dbTrader);
_traders.Add(trader.Id, trader);
_logger.Info($"Loaded custom trader: {trader.Name}");
customTraders++;
}
catch (Exception e)
{
_logger.Error("Failed to load custom trader", e);
}
}
else
{
_traders.Add(trader.Id, trader);
}
}
_logger.Info($"Importing traders complete {(customTraders == 0 ? "" : $"[{customTraders} traders loaded]")}");
return Task.CompletedTask;
}
public string GetRoute()
{
return "spt-trader-registration";
}
/// <summary>
/// Returns a trader by given ID.
/// </summary>
/// <param name="traderId"></param>
/// <returns></returns>
public ITrader? GetTraderById(string traderId)
{
if (_traders.TryGetValue(traderId, out var trader))
{
return trader;
}
return null;
}
/// <summary>
/// Returns all traders in the game, including custom traders.
/// </summary>
/// <returns></returns>
public IEnumerable<ITrader> GetAllTraders()
{
return _traders.Values;
}
}
@@ -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>
@@ -1,29 +0,0 @@
using System.Reflection;
using HarmonyLib;
using SPTarkov.Server.Core.Utils;
namespace SPTarkov.Server.Modding;
public class HarmonyBootstrapper
{
public static void LoadAllPatches(List<Assembly> assemblies)
{
if (!ProgramStatics.MODS())
{
return;
}
var hamony = new Harmony("SPT");
foreach (var assembly in assemblies)
{
try
{
hamony.PatchAll(assembly);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
}
+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))
-2
View File
@@ -29,8 +29,6 @@ public static class Program
// validate and sort mods, this will also discard any mods that are invalid
var sortedLoadedMods = ValidateMods(mods);
// for harmony, we use the original list, as some mods may only be bepinex patches only
HarmonyBootstrapper.LoadAllPatches(mods.SelectMany(asm => asm.Assemblies).ToList());
var diHandler = new DependencyInjectionHandler(builder.Services);
// register SPT components
-1
View File
@@ -24,7 +24,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="HarmonyX" Version="2.14.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.1"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.1"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1"/>
+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."
);
}
}
+7
View File
@@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HideoutCraftQuestIdGenerato
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "Benchmarks\Benchmarks.csproj", "{6884273A-72E9-4035-B5BE-EE101C69F5F5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SPTarkov.Reflection", "Libraries\SPTarkov.Reflection\SPTarkov.Reflection.csproj", "{9073A593-A2F5-471E-9678-B896A7226FD4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -67,6 +69,10 @@ Global
{6884273A-72E9-4035-B5BE-EE101C69F5F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6884273A-72E9-4035-B5BE-EE101C69F5F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6884273A-72E9-4035-B5BE-EE101C69F5F5}.Release|Any CPU.Build.0 = Release|Any CPU
{9073A593-A2F5-471E-9678-B896A7226FD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9073A593-A2F5-471E-9678-B896A7226FD4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9073A593-A2F5-471E-9678-B896A7226FD4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9073A593-A2F5-471E-9678-B896A7226FD4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -78,5 +84,6 @@ Global
{DB049C81-DEC0-490D-AC06-7AF4DC8C0571} = {F084DDFD-89F3-44F9-89C3-5CA11F4CDEEF}
{4B973AC0-0C60-4853-9AF7-7CB69127473E} = {F084DDFD-89F3-44F9-89C3-5CA11F4CDEEF}
{C24B1FEB-F8AC-434E-998D-5DA4D1687295} = {587959C2-5AFA-4B77-B327-566610F9A289}
{9073A593-A2F5-471E-9678-B896A7226FD4} = {F084DDFD-89F3-44F9-89C3-5CA11F4CDEEF}
EndGlobalSection
EndGlobal