Merge branch 'develop' of https://github.com/sp-tarkov/server-csharp into develop
This commit is contained in:
@@ -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
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
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
|
||||
|
||||
+13
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user