feat: Make adding new traders easier (#251)

* Make adding a new trader easier

* Improvements
This commit is contained in:
hulkhan22
2025-05-11 21:12:26 +02:00
committed by GitHub
parent 718015c2a6
commit 1eb4d55a02
9 changed files with 230 additions and 99 deletions
@@ -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;
@@ -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
}
@@ -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;
}
@@ -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;
}
}