using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Extensions; using SPTarkov.Server.Core.Generators; using SPTarkov.Server.Core.Helpers; using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Eft.Game; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Spt.Config; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Services; using SPTarkov.Server.Core.Utils; namespace SPTarkov.Server.Core.Controllers; [Injectable] public class TraderController( ISptLogger logger, TimeUtil timeUtil, DatabaseService databaseService, TraderAssortHelper traderAssortHelper, ProfileHelper profileHelper, TraderHelper traderHelper, PaymentHelper paymentHelper, RagfairPriceService ragfairPriceService, TraderPurchasePersisterService traderPurchasePersisterService, FenceService fenceService, FenceBaseAssortGenerator fenceBaseAssortGenerator, ConfigServer configServer ) { protected readonly TraderConfig TraderConfig = configServer.GetConfig(); /// /// Runs when onLoad event is fired /// Iterate over traders, ensure a pristine copy of their assorts is stored in traderAssortService /// Store timestamp of next assort refresh in nextResupply property of traders .base object /// public void Load() { var nextHourTimestamp = timeUtil.GetTimeStampOfNextHour(); var traderResetStartsWithServer = TraderConfig.TradersResetFromServerStart; var traders = databaseService.GetTraders(); foreach (var (traderId, trader) in traders) { if (traderId == Traders.LIGHTHOUSEKEEPER) { continue; } if (traderId == Traders.FENCE) { fenceBaseAssortGenerator.GenerateFenceBaseAssorts(); fenceService.GenerateFenceAssorts(); continue; } // Adjust price by traderPriceMultiplier config property if (!TraderConfig.TraderPriceMultiplier.Approx(1, 0.001)) { AdjustTraderItemPrices(trader, TraderConfig.TraderPriceMultiplier); } traderPurchasePersisterService.RemoveStalePurchasesFromProfiles(traderId); // Set to next hour on clock or current time + 60 minutes trader.Base.NextResupply = traderResetStartsWithServer ? (int)traderHelper.GetNextUpdateTimestamp(trader.Base.Id) : (int)nextHourTimestamp; } } /// /// Adjust trader item prices based on config value multiplier /// only applies to items sold for currency /// /// Trader to adjust prices of /// Coef to apply to traders' items' prices protected void AdjustTraderItemPrices(Trader trader, double multiplier) { foreach (var kvp in trader.Assort?.BarterScheme) { var barterSchemeItem = kvp.Value?.FirstOrDefault()?.FirstOrDefault(); if ( barterSchemeItem?.Template != null && paymentHelper.IsMoneyTpl(barterSchemeItem.Template) ) { barterSchemeItem.Count += Math.Round(barterSchemeItem?.Count * multiplier ?? 0D, 2); } } } /// /// Runs when onUpdate is fired /// If current time is > nextResupply(expire) time of trader, refresh traders assorts and /// Fence is handled slightly differently /// /// True if ran successfully public bool Update() { foreach (var (traderId, trader) in databaseService.GetTables().Traders) { if (traderId == Traders.LIGHTHOUSEKEEPER) { continue; } if (traderId == Traders.FENCE) { if (fenceService.NeedsPartialRefresh()) { fenceService.GenerateFenceAssorts(); } continue; } if (!traderAssortHelper.TraderAssortsHaveExpired(traderId)) { // Trader is still active, nothing else to do continue; } // Trader needs to be refreshed traderAssortHelper.ResetExpiredTrader(trader); // Reset purchase data per trader as they have independent reset times traderPurchasePersisterService.ResetTraderPurchasesStoredInProfile(traderId); } return true; } /// /// Handle client/trading/api/traderSettings /// /// session id /// Return a list of all traders public List GetAllTraders(MongoId sessionId) { var traders = new List(); var pmcData = profileHelper.GetPmcProfile(sessionId); foreach (var (traderId, trader) in databaseService.GetTables().Traders) { traderHelper.GetTrader(traderId, sessionId); if (trader.Base is null) { logger.Warning($"No trader with id: {traderId} found, skipping"); continue; } traders.Add(trader.Base); if (pmcData?.Info != null) { traderHelper.LevelUp(traderId, pmcData); } } traders.Sort(SortByTraderId); return traders; } /// /// Order traders by their traderId (tid) /// /// First trader to compare /// Second trader to compare /// 1,-1 or 0 protected static int SortByTraderId(TraderBase traderA, TraderBase traderB) { return string.CompareOrdinal(traderA.Id, traderB.Id); } /// /// Handle client/trading/api/getTrader /// /// Session/Player id /// /// public TraderBase? GetTrader(MongoId sessionId, MongoId traderId) { return traderHelper.GetTrader(sessionId, traderId); } /// /// Handle client/trading/api/getTraderAssort /// /// Session/Player id /// /// public TraderAssort GetAssort(MongoId sessionId, MongoId traderId) { return traderAssortHelper.GetAssort(sessionId, traderId); } /// /// Handle client/items/prices/TRADERID /// /// public GetItemPricesResponse GetItemPrices(MongoId sessionId, MongoId traderId) { var handbookPrices = ragfairPriceService.GetAllStaticPrices(); return new GetItemPricesResponse { SupplyNextTime = traderHelper.GetNextUpdateTimestamp(traderId), Prices = handbookPrices, CurrencyCourses = new Dictionary { { Money.ROUBLES, handbookPrices[Money.ROUBLES] }, { Money.EUROS, handbookPrices[Money.EUROS] }, { Money.DOLLARS, handbookPrices[Money.DOLLARS] }, }, }; } }