diff --git a/Libraries/SPTarkov.Server.Core/Controllers/InsuranceController.cs b/Libraries/SPTarkov.Server.Core/Controllers/InsuranceController.cs
index 9570cc3a..7d942623 100644
--- a/Libraries/SPTarkov.Server.Core/Controllers/InsuranceController.cs
+++ b/Libraries/SPTarkov.Server.Core/Controllers/InsuranceController.cs
@@ -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(
/// Should item be deleted
protected bool? RollForDelete(string traderId, Item? insuredItem = null)
{
- var trader = _traderHelper.GetTraderById(traderId);
+ var trader = _traderStore.GetTraderById(traderId);
if (trader is null)
{
return null;
diff --git a/Libraries/SPTarkov.Server.Core/DI/OnLoadOrder.cs b/Libraries/SPTarkov.Server.Core/DI/OnLoadOrder.cs
index d9cfcc4b..6eb76cbb 100644
--- a/Libraries/SPTarkov.Server.Core/DI/OnLoadOrder.cs
+++ b/Libraries/SPTarkov.Server.Core/DI/OnLoadOrder.cs
@@ -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;
diff --git a/Libraries/SPTarkov.Server.Core/Helpers/TraderHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/TraderHelper.cs
index 5d52dfaa..561d1dd8 100644
--- a/Libraries/SPTarkov.Server.Core/Helpers/TraderHelper.cs
+++ b/Libraries/SPTarkov.Server.Core/Helpers/TraderHelper.cs
@@ -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;
}
- ///
- /// Get a trader enum key by its value
- ///
- /// Traders id
- /// Traders key
- 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;
- }
-
- ///
- /// 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);
- ///
- /// The trader enum value to validate
- /// The validated trader enum value as a string, or an empty string if invalid
- /// 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;
- }
-
- ///
- /// Does the 'Traders' enum has a value that matches the passed in parameter
- ///
- /// Value to check for
- /// True, values exists in Traders enum as a value
- public bool TraderEnumHasKey(string key)
- {
- return Traders.TradersDictionary.Any(x => x.Value == key);
- }
-
///
/// Accepts a trader id
///
/// Trader id
- /// True if Traders enum has the param as a value
- public bool TraderEnumHasValue(string traderId)
+ /// True if a Trader exists with given ID
+ public bool TraderExists(string traderId)
{
- return Traders.TradersDictionary.ContainsValue(traderId);
+ return _traderStore.GetTraderById(traderId) != null;
}
}
diff --git a/Libraries/SPTarkov.Server.Core/Models/Enums/Traders.cs b/Libraries/SPTarkov.Server.Core/Models/Enums/Traders.cs
index 755cef91..ea2fd28b 100644
--- a/Libraries/SPTarkov.Server.Core/Models/Enums/Traders.cs
+++ b/Libraries/SPTarkov.Server.Core/Models/Enums/Traders.cs
@@ -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 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
}
diff --git a/Libraries/SPTarkov.Server.Core/Models/Trader/ITrader.cs b/Libraries/SPTarkov.Server.Core/Models/Trader/ITrader.cs
new file mode 100644
index 00000000..7fd7b388
--- /dev/null
+++ b/Libraries/SPTarkov.Server.Core/Models/Trader/ITrader.cs
@@ -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>? GetQuestAssort();
+ public abstract TraderBase? GetBase();
+
+ public virtual List? GetSuits()
+ {
+ return null;
+ }
+
+ public virtual List? GetServices()
+ {
+ return null;
+ }
+
+ public virtual Dictionary?>? GetDialogues()
+ {
+ return null;
+ }
+}
diff --git a/Libraries/SPTarkov.Server.Core/Models/Trader/Traders.cs b/Libraries/SPTarkov.Server.Core/Models/Trader/Traders.cs
new file mode 100644
index 00000000..4a0ce1d2
--- /dev/null
+++ b/Libraries/SPTarkov.Server.Core/Models/Trader/Traders.cs
@@ -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;
+}
diff --git a/Libraries/SPTarkov.Server.Core/Services/PaymentService.cs b/Libraries/SPTarkov.Server.Core/Services/PaymentService.cs
index 1c6f0656..6e1d605b 100644
--- a/Libraries/SPTarkov.Server.Core/Services/PaymentService.cs
+++ b/Libraries/SPTarkov.Server.Core/Services/PaymentService.cs
@@ -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();
diff --git a/Libraries/SPTarkov.Server.Core/Services/ProfileFixerService.cs b/Libraries/SPTarkov.Server.Core/Services/ProfileFixerService.cs
index e0be9c67..6a424c3c 100644
--- a/Libraries/SPTarkov.Server.Core/Services/ProfileFixerService.cs
+++ b/Libraries/SPTarkov.Server.Core/Services/ProfileFixerService.cs
@@ -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)
diff --git a/Libraries/SPTarkov.Server.Core/Services/TraderStore.cs b/Libraries/SPTarkov.Server.Core/Services/TraderStore.cs
new file mode 100644
index 00000000..282aeee3
--- /dev/null
+++ b/Libraries/SPTarkov.Server.Core/Services/TraderStore.cs
@@ -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;
+
+///
+/// Source of truth for all default traders as well as any additional trader a server mod may add.
+///
+[Injectable(InjectionType.Singleton, TypePriority = OnLoadOrder.TraderRegistration)]
+public class TraderStore : IOnLoad
+{
+ private readonly DatabaseService _databaseService;
+ private readonly IEnumerable _injectedTraders;
+ private readonly ISptLogger _logger;
+
+ private readonly Dictionary _traders = new();
+
+ public TraderStore(DatabaseService databaseService,
+ IEnumerable injectedTraders,
+ ISptLogger 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";
+ }
+
+ ///
+ /// Returns a trader by given ID.
+ ///
+ ///
+ ///
+ public ITrader? GetTraderById(string traderId)
+ {
+ if (_traders.TryGetValue(traderId, out var trader))
+ {
+ return trader;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Returns all traders in the game, including custom traders.
+ ///
+ ///
+ public IEnumerable GetAllTraders()
+ {
+ return _traders.Values;
+ }
+}