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; + } +}