From 89f4d10faa6c3df418bfdffc524e8cff0ca87896 Mon Sep 17 00:00:00 2001 From: Cj <161484149+CJ-SPT@users.noreply.github.com> Date: Wed, 18 Jun 2025 08:14:35 -0400 Subject: [PATCH] Refactor SellItemToTrader logic (#403) * Refactor SellItemToTrader logic * add comments and make QuestHelper a singleton * add localization for error * grammar --- .../SPT_Data/database/locales/server/en.json | 1 + .../Helpers/QuestHelper.cs | 145 +++++++++++++++++- .../Helpers/TradeHelper.cs | 84 +--------- 3 files changed, 143 insertions(+), 87 deletions(-) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/database/locales/server/en.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/database/locales/server/en.json index d55243ef..95da578e 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/database/locales/server/en.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/database/locales/server/en.json @@ -604,6 +604,7 @@ "quest-no_skill_found": "Skill %s not found", "quest-unable_to_find_compare_condition": "Unrecognised Comparison Method: %s", "quest-unable_to_find_quest_in_db": "Quest id: {{questId}} with type: {{questType}} not found in database", + "quest-unable_to_find_quest_in_db_no_type": "Quest id: {{questId}} not found in db", "quest-unable_to_find_quest_in_db_no_quest_rewards": "Unable to find quest: %s in db, unable to give quest rewards to player", "quest-unable_to_find_repeatable_to_replace": "Unable to find repeatable quest in profile to replace, skipping", "quest-unable_to_find_trader_in_profile": "Unable to find trader: %s in profile", diff --git a/Libraries/SPTarkov.Server.Core/Helpers/QuestHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/QuestHelper.cs index f3cabdcf..edfd0404 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/QuestHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/QuestHelper.cs @@ -5,6 +5,7 @@ using SPTarkov.Server.Core.Models.Eft.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Eft.ItemEvent; using SPTarkov.Server.Core.Models.Eft.Quests; +using SPTarkov.Server.Core.Models.Eft.Trade; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Spt.Config; using SPTarkov.Server.Core.Models.Utils; @@ -17,7 +18,7 @@ using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel; namespace SPTarkov.Server.Core.Helpers; -[Injectable] +[Injectable(InjectionType.Singleton)] public class QuestHelper( ISptLogger _logger, TimeUtil _timeUtil, @@ -39,6 +40,10 @@ public class QuestHelper( protected readonly HashSet _startedOrAvailToFinish = [QuestStatusEnum.Started, QuestStatusEnum.AvailableForFinish]; protected readonly QuestConfig _questConfig = _configServer.GetConfig(); + // We need to keep track of quests with `SellItemToTrader` finish conditions to avoid expensive lookups during trading. + // NOTE: DO NOT ACCESS ME DIRECTLY! use GetSellToTraderQuests() + protected readonly Dictionary> _sellToTraderQuestConditionCache = []; + /// /// Get status of a quest in player profile by its id /// @@ -260,10 +265,10 @@ public class QuestHelper( } /** - * - * @param pmcData - * @param newState - * @param acceptedQuest + * + * @param pmcData + * @param newState + * @param acceptedQuest */ /// @@ -664,6 +669,134 @@ public class QuestHelper( return updatedQuest; } + + /// + /// Get all quests with finish condition `SellItemToTrader`. + /// The first time this method is called it will cache the conditions by quest id in `_sellToTraderQuestConditionCache` and return that thereafter. + /// + /// List of quests with `SellItemToTrader` finish conditions + protected Dictionary> GetSellToTraderQuests() + { + // Cache is hydrated, return it. + if (_sellToTraderQuestConditionCache.Count != 0) + { + return _sellToTraderQuestConditionCache; + } + + // Hydrate the cache. + foreach (var quest in GetQuestsFromDb()) + { + foreach (var cond in quest.Conditions.AvailableForFinish) + { + if (cond.ConditionType != "SellItemToTrader") + { + continue; + } + + if (!_sellToTraderQuestConditionCache.TryGetValue(quest.Id, out var questConditions)) + { + questConditions ??= []; + questConditions.Add(cond); + + _sellToTraderQuestConditionCache.Add(quest.Id, questConditions); + continue; + } + + questConditions.Add(cond); + } + } + + if (_logger.IsLogEnabled(LogLevel.Debug)) + { + _logger.Debug($"_sellToTraderQuestConditionCache hydrated with {_sellToTraderQuestConditionCache.Count} quests"); + } + + return _sellToTraderQuestConditionCache; + } + + /// + /// Get all active condition counters for `SellItemToTrader` conditions + /// + /// Profile to check + /// List of active TaskConditionCounters + protected List? GetActiveSellToTraderConditionCounters(PmcData pmcData) + { + return pmcData.TaskConditionCounters?.Values.Where(condition => GetSellToTraderQuests().ContainsKey(condition.SourceId) + && condition.Type == "SellItemToTrader").ToList(); + } + + /// + /// Look over all active conditions and increment them as needed + /// + /// profile selling the items + /// profile to recieve the money + /// request with items to sell + public void IncrementSoldToTraderCounters( + PmcData profileWithItemsToSell, + PmcData profileToReceiveMoney, + ProcessSellTradeRequestData sellRequest + ) + { + var activeConditionCounters = GetActiveSellToTraderConditionCounters(profileToReceiveMoney); + + // No active conditions, exit + if (activeConditionCounters is null || activeConditionCounters.Count == 0) + { + return; + } + + var sellToTraderQuests = GetSellToTraderQuests(); + foreach (var counter in activeConditionCounters) + { + // Condition is in profile, but quest doesn't exist in database + if (!sellToTraderQuests.TryGetValue(counter.SourceId, out var conditions)) + { + _logger.Error(_localisationService.GetText("unable_to_find_quest_in_db_no_type", counter.SourceId)); + continue; + } + + foreach (var condition in conditions) + { + IncrementSoldToTraderCounter(profileWithItemsToSell, counter, condition, sellRequest); + } + } + } + + /// + /// Increment an individual condition counter + /// + /// Profile selling the items + /// condition counter to increment + /// quest condtion to check for valid items on + /// sell request of items sold + protected void IncrementSoldToTraderCounter( + PmcData profileWithItemsToSell, + TaskConditionCounter taskCounter, + QuestCondition questCondition, + ProcessSellTradeRequestData sellRequest + ) + { + var itemsTplsThatIncrement = questCondition.Target; + foreach (var itemSoldToTrader in sellRequest.Items) + { + // Get sold items' details from profile + var itemDetails = profileWithItemsToSell.Inventory?.Items?.FirstOrDefault(inventoryItem => inventoryItem.Id == itemSoldToTrader.Id + ); + if (itemDetails is null) + { + _logger.Error(_localisationService.GetText("trader-unable_to_find_inventory_item_for_selltotrader_counter", taskCounter.SourceId)); + + continue; + } + + // Is sold item on the increment list + if (itemsTplsThatIncrement.List.Contains(itemDetails.Template)) + { + taskCounter.Value += itemSoldToTrader.Count; + } + } + } + /// /// Fail a quest in a player profile /// @@ -1007,7 +1140,7 @@ public class QuestHelper( var completedQuestId = request.QuestId; // Keep a copy of player quest statuses from their profile (Must be gathered prior to applyQuestReward() & failQuests()) - var clientQuestsClone = _cloner.Clone(GetClientQuests(sessionID)); + var clientQuestsClone = _cloner.Clone(GetClientQuests(sessionID)); const QuestStatusEnum newQuestState = QuestStatusEnum.Success; UpdateQuestState(pmcData, newQuestState, completedQuestId); diff --git a/Libraries/SPTarkov.Server.Core/Helpers/TradeHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/TradeHelper.cs index 8ffc1c97..429615c6 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/TradeHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/TradeHelper.cs @@ -21,6 +21,7 @@ public class TradeHelper( DatabaseService _databaseService, TraderHelper _traderHelper, ItemHelper _itemHelper, + QuestHelper _questHelper, PaymentService _paymentService, FenceService _fenceService, LocalisationService _localisationService, @@ -270,14 +271,8 @@ public class TradeHelper( ItemEventRouterResponse output ) { - // TODO - make more generic to support all quests that have this condition type - // Try to reduce perf hit as this is expensive to do every sale - // MUST OCCUR PRIOR TO ITEMS BEING REMOVED FROM INVENTORY - if (sellRequest.TransactionId == Traders.RAGMAN) - // Edge case, `Circulate` quest needs to track when certain items are sold to him - { - IncrementCirculateSoldToTraderCounter(profileWithItemsToSell, profileToReceiveMoney, sellRequest); - } + // Check for and increment SoldToTrader condition counters + _questHelper.IncrementSoldToTraderCounters(profileWithItemsToSell, profileToReceiveMoney, sellRequest); const string pattern = @"\s+"; @@ -318,79 +313,6 @@ public class TradeHelper( _paymentService.GiveProfileMoney(profileToReceiveMoney, sellRequest.Price, sellRequest, output, sessionID); } - protected void IncrementCirculateSoldToTraderCounter( - PmcData profileWithItemsToSell, - PmcData profileToReceiveMoney, - ProcessSellTradeRequestData sellRequest - ) - { - const string circulateQuestId = "6663149f1d3ec95634095e75"; - var activeCirculateQuest = profileToReceiveMoney.Quests.FirstOrDefault(quest => quest.QId == circulateQuestId && quest.Status == QuestStatusEnum.Started - ); - - // Player not on Circulate quest ,exit - if (activeCirculateQuest is null) - { - return; - } - - // Find related task condition - var taskCondition = profileToReceiveMoney.TaskConditionCounters?.Values.FirstOrDefault(condition => - condition.SourceId == circulateQuestId && condition.Type == "SellItemToTrader" - ); - - // No relevant condition in profile, nothing to increment - if (taskCondition is null) - { - _logger.Error($"Unable to find `sellToTrader` task counter for {circulateQuestId} quest in profile, skipping"); - - return; - } - - // Condition exists in profile - var circulateQuestDb = _databaseService.GetQuests(); - if (!circulateQuestDb.TryGetValue(circulateQuestId, out _)) - { - _logger.Error($"Unable to find quest: {circulateQuestId} in db, skipping"); - - return; - } - - // Get sellToTrader condition from quest - var sellItemToTraderCondition = circulateQuestDb.GetValueOrDefault(circulateQuestId)? - .Conditions?.AvailableForFinish?.FirstOrDefault(condition => condition.ConditionType == "SellItemToTrader" - ); - - // Quest doesn't have a sellItemToTrader condition, nothing to do - if (sellItemToTraderCondition is null) - { - _logger.Error(_localisationService.GetText("quest-unable_to_find_selltotrader_counter", circulateQuestId)); - - return; - } - - // Iterate over items sold to trader - var itemsTplsThatIncrement = sellItemToTraderCondition.Target; - foreach (var itemSoldToTrader in sellRequest.Items) - { - // Get sold items' details from profile - var itemDetails = profileWithItemsToSell.Inventory?.Items?.FirstOrDefault(inventoryItem => inventoryItem.Id == itemSoldToTrader.Id - ); - if (itemDetails is null) - { - _logger.Error(_localisationService.GetText("trader-unable_to_find_inventory_item_for_selltotrader_counter", circulateQuestId)); - - continue; - } - - // Is sold item on the increment list - if (itemsTplsThatIncrement.List.Contains(itemDetails.Template)) - { - taskCondition.Value += itemSoldToTrader.Count; - } - } - } - /// /// Traders allow a limited number of purchases per refresh cycle (default 60 mins) ///