From 541f3264093727d68a74805fef8fdda7890f9d23 Mon Sep 17 00:00:00 2001 From: Chris Adamson Date: Thu, 29 May 2025 11:25:09 -0500 Subject: [PATCH] added a lock for trader buy method (#303) * added a lock for trader buy method * moved the lock higher --- .../Helpers/TradeHelper.cs | 368 +++++++++--------- 1 file changed, 187 insertions(+), 181 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Helpers/TradeHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/TradeHelper.cs index d522baf7..92a07782 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/TradeHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/TradeHelper.cs @@ -32,6 +32,8 @@ public class TradeHelper( ICloner _cloner ) { + protected static Lock buyLock = new(); + /// /// Buy item from flea or trader /// @@ -48,203 +50,207 @@ public class TradeHelper( ItemEventRouterResponse output ) { - List offerItems = []; - Action? buyCallback; - - if (string.Equals(buyRequestData.TransactionId, "ragfair", StringComparison.OrdinalIgnoreCase)) + lock (buyLock) { - // Called when player purchases PMC offer from ragfair - buyCallback = buyCount => + List offerItems = []; + Action? buyCallback; + + if (string.Equals(buyRequestData.TransactionId, "ragfair", StringComparison.OrdinalIgnoreCase)) { - var allOffers = _ragfairServer.GetOffers(); - - // We store ragfair offerId in buyRequestData.item_id - var offerWithItem = allOffers.FirstOrDefault(x => x.Id == buyRequestData.ItemId); - var itemPurchased = offerWithItem.Items.FirstOrDefault(); - - // Ensure purchase does not exceed trader item limit - var assortHasBuyRestrictions = _itemHelper.HasBuyRestrictions(itemPurchased); - if (assortHasBuyRestrictions) + // Called when player purchases PMC offer from ragfair + buyCallback = buyCount => { - CheckPurchaseIsWithinTraderItemLimit( - sessionID, - pmcData, - buyRequestData.TransactionId, - itemPurchased, - buyRequestData.ItemId, - buyCount - ); + var allOffers = _ragfairServer.GetOffers(); + + // We store ragfair offerId in buyRequestData.item_id + var offerWithItem = allOffers.FirstOrDefault(x => x.Id == buyRequestData.ItemId); + var itemPurchased = offerWithItem.Items.FirstOrDefault(); + + // Ensure purchase does not exceed trader item limit + var assortHasBuyRestrictions = _itemHelper.HasBuyRestrictions(itemPurchased); + if (assortHasBuyRestrictions) + { + CheckPurchaseIsWithinTraderItemLimit( + sessionID, + pmcData, + buyRequestData.TransactionId, + itemPurchased, + buyRequestData.ItemId, + buyCount + ); + + // Decrement trader item count + var itemPurchaseDetails = new PurchaseDetails + { + Items = + [ + new PurchaseItems + { + ItemId = buyRequestData.ItemId, + Count = buyCount + } + ], + TraderId = buyRequestData.TransactionId + }; + _traderHelper.AddTraderPurchasesToPlayerProfile(sessionID, itemPurchaseDetails, itemPurchased); + } + }; + + // buyCallback = BuyCallback1; + // Get raw offer from ragfair, clone to prevent altering offer itself + var allOffers = _ragfairServer.GetOffers(); + var offerWithItemCloned = _cloner.Clone(allOffers.FirstOrDefault(x => x.Id == buyRequestData.ItemId)); + offerItems = offerWithItemCloned.Items; + } + else if (buyRequestData.TransactionId == Traders.FENCE) + { + buyCallback = buyCount => + { + // Update assort/flea item values + var traderAssorts = _traderHelper.GetTraderAssortsByTraderId(buyRequestData.TransactionId).Items; + var itemPurchased = traderAssorts.FirstOrDefault(assort => assort.Id == buyRequestData.ItemId); // Decrement trader item count - var itemPurchaseDetails = new PurchaseDetails - { - Items = - [ - new PurchaseItems - { - ItemId = buyRequestData.ItemId, - Count = buyCount - } - ], - TraderId = buyRequestData.TransactionId - }; - _traderHelper.AddTraderPurchasesToPlayerProfile(sessionID, itemPurchaseDetails, itemPurchased); - } - }; + itemPurchased.Upd.StackObjectsCount -= buyCount; - // buyCallback = BuyCallback1; - // Get raw offer from ragfair, clone to prevent altering offer itself - var allOffers = _ragfairServer.GetOffers(); - var offerWithItemCloned = _cloner.Clone(allOffers.FirstOrDefault(x => x.Id == buyRequestData.ItemId)); - offerItems = offerWithItemCloned.Items; - } - else if (buyRequestData.TransactionId == Traders.FENCE) - { - buyCallback = buyCount => - { - // Update assort/flea item values - var traderAssorts = _traderHelper.GetTraderAssortsByTraderId(buyRequestData.TransactionId).Items; - var itemPurchased = traderAssorts.FirstOrDefault(assort => assort.Id == buyRequestData.ItemId); + _fenceService.AmendOrRemoveFenceOffer(buyRequestData.ItemId, buyCount); + }; - // Decrement trader item count - itemPurchased.Upd.StackObjectsCount -= buyCount; - - _fenceService.AmendOrRemoveFenceOffer(buyRequestData.ItemId, buyCount); - }; - - var fenceItems = _fenceService.GetRawFenceAssorts().Items; - var rootItemIndex = fenceItems.FindIndex(item => item.Id == buyRequestData.ItemId); - if (rootItemIndex == -1) - { - if (_logger.IsLogEnabled(LogLevel.Debug)) + var fenceItems = _fenceService.GetRawFenceAssorts().Items; + var rootItemIndex = fenceItems.FindIndex(item => item.Id == buyRequestData.ItemId); + if (rootItemIndex == -1) { - _logger.Debug($"Tried to buy item {buyRequestData.ItemId} from fence that no longer exists"); + if (_logger.IsLogEnabled(LogLevel.Debug)) + { + _logger.Debug($"Tried to buy item {buyRequestData.ItemId} from fence that no longer exists"); + } + + var message = _localisationService.GetText("ragfair-offer_no_longer_exists"); + _httpResponseUtil.AppendErrorToOutput(output, message); + + return; } - var message = _localisationService.GetText("ragfair-offer_no_longer_exists"); - _httpResponseUtil.AppendErrorToOutput(output, message); + offerItems = _itemHelper.FindAndReturnChildrenAsItems(fenceItems, buyRequestData.ItemId); + } + else + { + buyCallback = buyCount => + { + // Update assort/flea item values + var traderAssorts = _traderHelper.GetTraderAssortsByTraderId(buyRequestData.TransactionId).Items; + var itemPurchased = traderAssorts.FirstOrDefault(item => item.Id == buyRequestData.ItemId); + // Ensure purchase does not exceed trader item limit + var assortHasBuyRestrictions = _itemHelper.HasBuyRestrictions(itemPurchased); + if (assortHasBuyRestrictions) + // Will throw error if check fails + { + CheckPurchaseIsWithinTraderItemLimit( + sessionID, + pmcData, + buyRequestData.TransactionId, + itemPurchased, + buyRequestData.ItemId, + buyCount + ); + } + + // Check if trader has enough stock + if (itemPurchased.Upd.StackObjectsCount < buyCount) + { + throw new Exception( + $"Unable to purchase {buyCount} items, this would exceed the remaining stock left {itemPurchased.Upd.StackObjectsCount} from the traders assort: {buyRequestData.TransactionId} this refresh" + ); + } + + // Decrement trader item count + itemPurchased.Upd.StackObjectsCount -= buyCount; + + if (assortHasBuyRestrictions) + { + var itemPurchaseDat = new PurchaseDetails + { + Items = + [ + new PurchaseItems + { + ItemId = buyRequestData.ItemId, + Count = buyCount + } + ], + TraderId = buyRequestData.TransactionId + }; + + _traderHelper.AddTraderPurchasesToPlayerProfile(sessionID, itemPurchaseDat, itemPurchased); + } + }; + + // Get all trader assort items + var traderItems = _traderAssortHelper.GetAssort(sessionID, buyRequestData.TransactionId).Items; + + // Get item + children for purchase + var relevantItems = _itemHelper.FindAndReturnChildrenAsItems(traderItems, buyRequestData.ItemId); + if (relevantItems.Count == 0) + { + _logger.Error( + $"Purchased trader: {buyRequestData.TransactionId} offer: {buyRequestData.ItemId} has no items"); + } + + offerItems.AddRange(relevantItems); + } + + // Get item details from db + var itemDbDetails = _itemHelper.GetItem(offerItems.FirstOrDefault().Template).Value; + var itemMaxStackSize = itemDbDetails.Properties.StackMaxSize; + var itemsToSendTotalCount = buyRequestData.Count; + var itemsToSendRemaining = itemsToSendTotalCount; + + // Construct array of items to send to player + List> itemsToSendToPlayer = []; + while (itemsToSendRemaining > 0) + { + var offerClone = _cloner.Clone(offerItems); + // Handle stackable items that have a max stack size limit + var itemCountToSend = Math.Min(itemMaxStackSize ?? 0, itemsToSendRemaining ?? 0); + offerClone.FirstOrDefault().Upd.StackObjectsCount = itemCountToSend; + + // Prevent any collisions + _itemHelper.RemapRootItemId(offerClone); + if (offerClone.Count > 1) + { + _itemHelper.ReparentItemAndChildren(offerClone.FirstOrDefault(), offerClone); + } + + itemsToSendToPlayer.Add(offerClone); + + // Remove amount of items added to player stash + itemsToSendRemaining -= itemCountToSend; + } + + // Construct request + var request = new AddItemsDirectRequest + { + ItemsWithModsToAdd = itemsToSendToPlayer, + FoundInRaid = foundInRaid, + Callback = buyCallback, + UseSortingTable = false + }; + + // Add items + their children to stash + _inventoryHelper.AddItemsToStash(sessionID, request, pmcData, output); + if (output.Warnings?.Count > 0) + { return; } - offerItems = _itemHelper.FindAndReturnChildrenAsItems(fenceItems, buyRequestData.ItemId); - } - else - { - buyCallback = buyCount => + /// Pay for purchase + _paymentService.PayMoney(pmcData, buyRequestData, sessionID, output); + if (output.Warnings?.Count > 0) { - // Update assort/flea item values - var traderAssorts = _traderHelper.GetTraderAssortsByTraderId(buyRequestData.TransactionId).Items; - var itemPurchased = traderAssorts.FirstOrDefault(item => item.Id == buyRequestData.ItemId); - - // Ensure purchase does not exceed trader item limit - var assortHasBuyRestrictions = _itemHelper.HasBuyRestrictions(itemPurchased); - if (assortHasBuyRestrictions) - // Will throw error if check fails - { - CheckPurchaseIsWithinTraderItemLimit( - sessionID, - pmcData, - buyRequestData.TransactionId, - itemPurchased, - buyRequestData.ItemId, - buyCount - ); - } - - // Check if trader has enough stock - if (itemPurchased.Upd.StackObjectsCount < buyCount) - { - throw new Exception( - $"Unable to purchase {buyCount} items, this would exceed the remaining stock left {itemPurchased.Upd.StackObjectsCount} from the traders assort: {buyRequestData.TransactionId} this refresh" - ); - } - - // Decrement trader item count - itemPurchased.Upd.StackObjectsCount -= buyCount; - - if (assortHasBuyRestrictions) - { - var itemPurchaseDat = new PurchaseDetails - { - Items = - [ - new PurchaseItems - { - ItemId = buyRequestData.ItemId, - Count = buyCount - } - ], - TraderId = buyRequestData.TransactionId - }; - - _traderHelper.AddTraderPurchasesToPlayerProfile(sessionID, itemPurchaseDat, itemPurchased); - } - }; - - // Get all trader assort items - var traderItems = _traderAssortHelper.GetAssort(sessionID, buyRequestData.TransactionId).Items; - - // Get item + children for purchase - var relevantItems = _itemHelper.FindAndReturnChildrenAsItems(traderItems, buyRequestData.ItemId); - if (relevantItems.Count == 0) - { - _logger.Error($"Purchased trader: {buyRequestData.TransactionId} offer: {buyRequestData.ItemId} has no items"); + var errorMessage = $"Transaction failed: {output.Warnings.FirstOrDefault().ErrorMessage}"; + _httpResponseUtil.AppendErrorToOutput(output, errorMessage, BackendErrorCodes.UnknownTradingError); } - - offerItems.AddRange(relevantItems); - } - - // Get item details from db - var itemDbDetails = _itemHelper.GetItem(offerItems.FirstOrDefault().Template).Value; - var itemMaxStackSize = itemDbDetails.Properties.StackMaxSize; - var itemsToSendTotalCount = buyRequestData.Count; - var itemsToSendRemaining = itemsToSendTotalCount; - - // Construct array of items to send to player - List> itemsToSendToPlayer = []; - while (itemsToSendRemaining > 0) - { - var offerClone = _cloner.Clone(offerItems); - // Handle stackable items that have a max stack size limit - var itemCountToSend = Math.Min(itemMaxStackSize ?? 0, itemsToSendRemaining ?? 0); - offerClone.FirstOrDefault().Upd.StackObjectsCount = itemCountToSend; - - // Prevent any collisions - _itemHelper.RemapRootItemId(offerClone); - if (offerClone.Count > 1) - { - _itemHelper.ReparentItemAndChildren(offerClone.FirstOrDefault(), offerClone); - } - - itemsToSendToPlayer.Add(offerClone); - - // Remove amount of items added to player stash - itemsToSendRemaining -= itemCountToSend; - } - - // Construct request - var request = new AddItemsDirectRequest - { - ItemsWithModsToAdd = itemsToSendToPlayer, - FoundInRaid = foundInRaid, - Callback = buyCallback, - UseSortingTable = false - }; - - // Add items + their children to stash - _inventoryHelper.AddItemsToStash(sessionID, request, pmcData, output); - if (output.Warnings?.Count > 0) - { - return; - } - - /// Pay for purchase - _paymentService.PayMoney(pmcData, buyRequestData, sessionID, output); - if (output.Warnings?.Count > 0) - { - var errorMessage = $"Transaction failed: {output.Warnings.FirstOrDefault().ErrorMessage}"; - _httpResponseUtil.AppendErrorToOutput(output, errorMessage, BackendErrorCodes.UnknownTradingError); } }