diff --git a/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs b/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs index 25753397..8bfed10d 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs @@ -1088,7 +1088,7 @@ public class RagfairController var output = _eventOutputHolder.GetOutput(sessionId); var pmcData = _profileHelper.GetPmcProfile(sessionId); - var playerProfileOffers = pmcData.RagfairInfo.Offers; + var playerProfileOffers = pmcData?.RagfairInfo?.Offers; if (playerProfileOffers is null) { _logger.Warning( @@ -1133,6 +1133,8 @@ public class RagfairController playerOffer.EndTime = (long?) Math.Round((double) newEndTime); } + _logger.Debug($"Flagged player offer: {offerId} for expiry in: {TimeSpan.FromTicks(playerOffer.EndTime.Value).ToString()}"); + return output; } diff --git a/Libraries/SPTarkov.Server.Core/Helpers/RagfairOfferHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/RagfairOfferHelper.cs index 797878c4..7d0f42bd 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/RagfairOfferHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/RagfairOfferHelper.cs @@ -599,7 +599,7 @@ public class RagfairOfferHelper( */ public bool ProcessOffersOnProfile(string sessionId) { - var timestamp = _timeUtil.GetTimeStamp(); + var currentTimestamp = _timeUtil.GetTimeStamp(); var profileOffers = GetProfileOffers(sessionId); // No offers, don't do anything @@ -612,56 +612,59 @@ public class RagfairOfferHelper( for (var index = profileOffers.Count - 1; index >= 0; index--) { var offer = profileOffers[index]; - var firstSellResult = offer.SellResults?.FirstOrDefault(); - if (offer.SellResults?.Count > 0 && timestamp >= offer.SellResults[0].SellTime) + if (offer.SellResults is null || offer.SellResults.Count == 0 || currentTimestamp < offer.SellResults.FirstOrDefault()?.SellTime) { - // Checks first item, first is spliced out of array after being processed - // Item sold - var totalItemsCount = 1d; - var boughtAmount = 1; - - if (!offer.SellInOnePiece.GetValueOrDefault(false)) - { - // offer.items.reduce((sum, item) => sum + item.upd?.StackObjectsCount ?? 0, 0); - totalItemsCount = GetTotalStackCountSize([offer.Items]); - boughtAmount = firstSellResult.Amount.Value; - } - - var ratingToAdd = offer.SummaryCost / totalItemsCount * boughtAmount; - IncreaseProfileRagfairRating(_profileHelper.GetFullProfile(sessionId), ratingToAdd.Value); - - offer.SellResults.Remove(firstSellResult); // Remove the sell result object now it has been processed - - // Can delete offer object, must run last - CompleteOffer(sessionId, offer, boughtAmount); + // Not sold / too early to check + continue; } + + var firstSellResult = offer.SellResults?.FirstOrDefault(); + if (firstSellResult is null) + { + continue; + } + + // Checks first item, first is spliced out of array after being processed + // Item sold + var totalItemsCount = 1d; + var boughtAmount = 1; + + // Does item need to be re-stacked + if (!offer.SellInOnePiece.GetValueOrDefault(false)) + { + // offer.items.reduce((sum, item) => sum + item.upd?.StackObjectsCount ?? 0, 0); + totalItemsCount = GetTotalStackCountSize([offer.Items]); + boughtAmount = firstSellResult.Amount ?? boughtAmount; + } + + var ratingToAdd = offer.SummaryCost / totalItemsCount * boughtAmount; + IncreaseProfileRagfairRating(_profileHelper.GetFullProfile(sessionId), ratingToAdd.Value); + + // Remove the sell result object now it has been processed + offer.SellResults.Remove(firstSellResult); + + // Can delete offer object, must run last + CompleteOffer(sessionId, offer, boughtAmount); } return true; } - /** - * Count up all rootitem StackObjectsCount properties of an array of items - * @param itemsInInventoryToList items to sum up - * @returns Total stack count - */ - public double GetTotalStackCountSize(List> itemsInInventoryToList) + /// + /// Count up all root item StackObjectsCount properties of an array of items + /// + /// items to sum up + /// Total stack count + public double GetTotalStackCountSize(List> itemsInInventoryToSumStackCount) { - var total = 0d; - foreach (var itemAndChildren in itemsInInventoryToList) - // Only count the root items stack count in total - { - total += itemAndChildren[0]?.Upd?.StackObjectsCount.GetValueOrDefault(1) ?? 1; - } - - return total; + return itemsInInventoryToSumStackCount.Sum(itemAndChildren => itemAndChildren.FirstOrDefault()?.Upd?.StackObjectsCount.GetValueOrDefault(1) ?? 1); } - /** - * Add amount to players ragfair rating - * @param sessionId Profile to update - * @param amountToIncrementBy Raw amount to add to players ragfair rating (excluding the reputation gain multiplier) - */ + /// + /// Add amount to players ragfair rating + /// + /// Profile to update + /// Raw amount to add to players ragfair rating (excluding the reputation gain multiplier) public void IncreaseProfileRagfairRating(SptProfile profile, double? amountToIncrementBy) { var ragfairGlobalsConfig = _databaseService.GetGlobals().Configuration.RagFair; @@ -680,11 +683,11 @@ public class RagfairOfferHelper( amountToIncrementBy; } - /** - * Return all offers a player has listed on a desired profile - * @param sessionId Session id - * @returns List of ragfair offers - */ + /// + /// Return all offers a player has listed on a desired profile + /// + /// Session/Player id + /// List of ragfair offers protected List GetProfileOffers(string sessionId) { var profile = _profileHelper.GetPmcProfile(sessionId); @@ -721,21 +724,15 @@ public class RagfairOfferHelper( _ragfairOfferService.RemoveOfferById(offerId); } - /** - * Complete the selling of players' offer - * @param sessionID Session id - * @param offer Sold offer details - * @param boughtAmount Amount item was purchased for - * @returns ItemEventRouterResponse - */ + /// + /// Complete the selling of players' offer + /// + /// Session/Player id + /// Sold offer details + /// Amount item was purchased for + /// ItemEventRouterResponse public ItemEventRouterResponse CompleteOffer(string offerOwnerSessionId, RagfairOffer offer, int boughtAmount) { - var rootItem = offer.Items.FirstOrDefault(); - var itemTpl = rootItem.Template; - var paymentItemsToSendToPlayer = new List(); - var offerStackCount = rootItem.Upd.StackObjectsCount; - var sellerProfile = _profileHelper.GetPmcProfile(offerOwnerSessionId); - // Pack or ALL items of a multi-offer were bought - remove entire offer if (offer.SellInOnePiece.GetValueOrDefault(false) || boughtAmount == offer.Quantity) { @@ -748,6 +745,11 @@ public class RagfairOfferHelper( } // Assemble payment to send to seller now offer was purchased + var sellerProfile = _profileHelper.GetPmcProfile(offerOwnerSessionId); + var rootItem = offer.Items.FirstOrDefault(); + var itemTpl = rootItem.Template; + var offerStackCount = rootItem.Upd.StackObjectsCount; + var paymentItemsToSendToPlayer = new List(); foreach (var requirement in offer.Requirements) { // Create an item template item @@ -792,14 +794,14 @@ public class RagfairOfferHelper( HandbookId = itemTpl }; - var storagetime = _timeUtil.GetHoursAsSeconds((int) _questHelper.GetMailItemRedeemTimeHoursForProfile(sellerProfile)); + var storageTimeSeconds = _timeUtil.GetHoursAsSeconds((int) _questHelper.GetMailItemRedeemTimeHoursForProfile(sellerProfile)); _mailSendService.SendDirectNpcMessageToPlayer( offerOwnerSessionId, Traders.RAGMAN, MessageType.FleamarketMessage, GetLocalisedOfferSoldMessage(itemTpl, boughtAmount), paymentItemsToSendToPlayer, - storagetime, + storageTimeSeconds, null, ragfairDetails ); diff --git a/Libraries/SPTarkov.Server.Core/Servers/RagfairServer.cs b/Libraries/SPTarkov.Server.Core/Servers/RagfairServer.cs index 4dca0d05..1915b599 100644 --- a/Libraries/SPTarkov.Server.Core/Servers/RagfairServer.cs +++ b/Libraries/SPTarkov.Server.Core/Servers/RagfairServer.cs @@ -22,7 +22,7 @@ public class RagfairServer( ConfigServer _configServer ) { - protected RagfairConfig _ragfairConfig = _configServer.GetConfig(); + protected readonly RagfairConfig _ragfairConfig = _configServer.GetConfig(); public void Load() { @@ -33,7 +33,7 @@ public class RagfairServer( public void Update() { - // Generate trader offers + // Generate/refresh trader offers var traders = GetUpdateableTraders(); foreach (var traderId in traders) { @@ -45,6 +45,7 @@ public class RagfairServer( if (_ragfairOfferService.TraderOffersNeedRefreshing(traderId)) { + // Trader has passed its offer cycle time, update stock and set offer times _ragfairOfferGenerator.GenerateFleaOffersForTrader(traderId); } } diff --git a/Libraries/SPTarkov.Server.Core/Services/RagfairOfferService.cs b/Libraries/SPTarkov.Server.Core/Services/RagfairOfferService.cs index 52abab2e..e92c55b8 100644 --- a/Libraries/SPTarkov.Server.Core/Services/RagfairOfferService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/RagfairOfferService.cs @@ -8,6 +8,7 @@ using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Utils; using SPTarkov.Server.Core.Utils.Cloners; +using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel; namespace SPTarkov.Server.Core.Services; @@ -92,7 +93,7 @@ public class RagfairOfferService( if (offer.Quantity <= 0) { // Offer is gone and now 'stale', need to be flagged as stale or removed if PMC offer - ProcessStaleOffer(offerId); + ProcessStaleOffer(offer.Id); } } @@ -162,34 +163,35 @@ public class RagfairOfferService( ProcessStaleOffer(offerId); } - // Clear out expired offer ids now we've regenerated them + // Clear out expired offer ids now we've processed them above ragfairOfferHolder.ResetExpiredOfferIds(); } /// - /// Remove stale offer from flea + /// Remove stale offer from flea + /// Send offer items back when its player offer + /// Skip trader offers - we want those to remain in 'expired' state until trader refresh /// /// Stale offer id to process protected void ProcessStaleOffer(string staleOfferId) { var staleOffer = ragfairOfferHolder.GetOfferById(staleOfferId); - var isTrader = ragfairServerHelper.IsTrader(staleOffer.User.Id); - var isPlayer = profileHelper.IsPlayer(staleOffer.User.Id.RegexReplace("^pmc", "")); // Skip trader offers, managed by RagfairServer.Update() + should remain on flea as 'expired' - if (isTrader) + if (ragfairServerHelper.IsTrader(staleOffer.User.Id)) { return; } // Handle dynamic offer from PMCs + var isPlayer = profileHelper.IsPlayer(staleOffer.User.Id.RegexReplace("^pmc", "")); if (!isPlayer) { // Not trader/player offer - ragfairOfferHolder.FlagOfferAsExpired(staleOfferId); + ragfairOfferHolder.FlagOfferAsExpired(staleOffer.Id); } - // Handle player offer - item(s) need returning/XP/rep adjusting. Checking if offer has actually expired or not. + // Handle player offer: item(s) need returning/XP/rep adjusting. Checking if offer has actually expired or not. if (isPlayer && staleOffer.EndTime <= timeUtil.GetTimeStamp()) { ReturnUnsoldPlayerOffer(staleOffer); @@ -198,7 +200,7 @@ public class RagfairOfferService( } // Remove expired offer from global flea pool - RemoveOfferById(staleOfferId); + RemoveOfferById(staleOffer.Id); } /// @@ -268,6 +270,11 @@ public class RagfairOfferService( ragfairServerHelper.ReturnItems(offerCreatorProfile.SessionId, unstackedItems); offerCreatorProfile.RagfairInfo.Offers.Splice(indexOfOfferInProfile, 1); + + if (logger.IsLogEnabled(LogLevel.Debug)) + { + logger.Debug($"Returned offer: {{playerOffer.Id}} items to player"); + } } /// @@ -318,7 +325,7 @@ public class RagfairOfferService( // Ensure items IDs are unique to prevent collisions when added to player inventory var reparentedItemAndChildren = itemHelper.ReparentItemAndChildren( - itemAndChildrenClone[0], + itemAndChildrenClone.FirstOrDefault(), itemAndChildrenClone ); itemHelper.RemapRootItemId(reparentedItemAndChildren); diff --git a/Libraries/SPTarkov.Server.Core/Utils/RagfairOfferHolder.cs b/Libraries/SPTarkov.Server.Core/Utils/RagfairOfferHolder.cs index cd0c334d..e742fc11 100644 --- a/Libraries/SPTarkov.Server.Core/Utils/RagfairOfferHolder.cs +++ b/Libraries/SPTarkov.Server.Core/Utils/RagfairOfferHolder.cs @@ -22,9 +22,9 @@ public class RagfairOfferHolder( protected readonly Lock _ragfairOperationLock = new(); protected readonly HashSet _expiredOfferIds = []; - protected ConcurrentDictionary _offersById = new(); - protected ConcurrentDictionary> _offersByTemplate = new(); // key = tplId, value = list of offerIds - protected ConcurrentDictionary> _offersByTrader = new(); // key = traderId, value = list of offerIds + protected readonly ConcurrentDictionary _offersById = new(); + protected readonly ConcurrentDictionary> _offersByTemplate = new(); // key = tplId, value = list of offerIds + protected readonly ConcurrentDictionary> _offersByTrader = new(); // key = traderId, value = list of offerIds /// /// Get a ragfair offer by its id @@ -71,11 +71,11 @@ public class RagfairOfferHolder( /// /// Id of trader to get offers for /// RagfairOffer list - public List? GetOffersByTrader(string traderId) + public List GetOffersByTrader(string traderId) { if (!_offersByTrader.TryGetValue(traderId, out var offerIds)) { - return null; + return []; } return offerIds @@ -118,7 +118,6 @@ public class RagfairOfferHolder( { lock (_ragfairOperationLock) { - var sellerId = offer.User.Id; // Keep generating IDs until we get a unique one while (_offersById.ContainsKey(offer.Id)) { @@ -126,17 +125,19 @@ public class RagfairOfferHolder( } var itemTpl = offer.Items?.FirstOrDefault()?.Template; - // If it is an NPC PMC offer AND we have already reached the maximum amount of possible offers - // for this template, just don't add in more + + var sellerId = offer.User.Id; var sellerIsTrader = _ragfairServerHelper.IsTrader(sellerId); - var itemSoldDb = _itemHelper.GetItem(itemTpl); + var itemSoldTemplate = _itemHelper.GetItem(itemTpl); if ( !string.IsNullOrEmpty(itemTpl) && !(sellerIsTrader || _profileHelper.IsPlayer(sellerId)) && _offersByTemplate.TryGetValue(itemTpl, out var offers) - && offers?.Count >= _ragfairServerHelper.GetOfferCountByBaseType(itemSoldDb.Value.Parent) + && offers?.Count >= _ragfairServerHelper.GetOfferCountByBaseType(itemSoldTemplate.Value.Parent) ) { + // If it is an NPC PMC offer AND we have already reached the maximum amount of possible offers + // for this template, just don't add in more return; } @@ -164,32 +165,32 @@ public class RagfairOfferHolder( if (!_offersById.TryGetValue(offerId, out var offer)) { _logger.Warning(_localisationService.GetText("ragfair-unable_to_remove_offer_doesnt_exist", offerId)); + return; } if (!_offersById.TryRemove(offer.Id, out _)) { - _logger.Warning($"Unable to remove offer: {offer.Id}"); + _logger.Warning($"Unable to remove offer by id: {offer.Id} not found"); } - if (checkTraderOffers && _offersByTrader.ContainsKey(offer.User.Id)) + if (checkTraderOffers && _offersByTrader.TryGetValue(offer.User.Id, out var traderOfferIds)) { - _offersByTrader[offer.User.Id].Remove(offer.Id); - // This was causing a memory leak, we need to make sure that we remove - // the user ID from the cached offers after they dont have anything else - // on the flea placed. We regenerate the ID for the NPC users, making it - // continuously grow otherwise - if (_offersByTrader[offer.User.Id].Count == 0) + traderOfferIds.Remove(offer.Id); + + if (traderOfferIds.Count == 0) { + // Potential memory leak + // Users with no offers were never cleaned up if (!_offersByTrader.TryRemove(offer.User.Id, out _)) { - _logger.Warning($"Unable to remove Trader offer: {offer.Id}"); + _logger.Warning($"Unable to remove Trader offer: {offer.Id} not found"); } } } - var firstItem = offer.Items.FirstOrDefault(); - if (_offersByTemplate.TryGetValue(firstItem.Template, out var offers)) + var rootItem = offer.Items.FirstOrDefault(); + if (_offersByTemplate.TryGetValue(rootItem.Template, out var offers)) { offers.Remove(offer.Id); } @@ -201,19 +202,22 @@ public class RagfairOfferHolder( /// Trader id to remove offers from public void RemoveAllOffersByTrader(string traderId) { - if (_offersByTrader.TryGetValue(traderId, out var offerIdsToRemove)) + if (!_offersByTrader.TryGetValue(traderId, out var offerIdsToRemove)) { - foreach (var offerId in offerIdsToRemove) - { - if (!_offersById.TryRemove(offerId, out _)) - { - _logger.Warning($"Unable to remove offer: {offerId}"); - } - } - - // Clear out linking table - _offersByTrader[traderId].Clear(); + // No trader, nothing to do + return; } + + foreach (var offerId in offerIdsToRemove) + { + if (!_offersById.TryRemove(offerId, out _)) + { + _logger.Warning($"Unable to remove offer: {offerId}"); + } + } + + // Clear out linking table + _offersByTrader[traderId].Clear(); } /// @@ -223,9 +227,10 @@ public class RagfairOfferHolder( /// Offer to store against tpl protected void AddOfferByTemplates(string template, string offerId) { - if (_offersByTemplate.ContainsKey(template)) + if (_offersByTemplate.TryGetValue(template, out var offerIds)) { - _offersByTemplate[template].Add(offerId); + offerIds.Add(offerId); + return; } @@ -242,9 +247,10 @@ public class RagfairOfferHolder( /// Offer to store against protected void AddOfferByTrader(string trader, string offerId) { - if (_offersByTrader.ContainsKey(trader)) + if (_offersByTrader.TryGetValue(trader, out var traderOfferIds)) { - _offersByTrader[trader].Add(offerId); + traderOfferIds.Add(offerId); + return; } @@ -260,18 +266,13 @@ public class RagfairOfferHolder( /// Offer to check /// Time to check offer against /// True - offer is stale - protected bool IsStale(RagfairOffer? offer, long time) + protected bool IsStale(RagfairOffer offer, long time) { - if (offer is null) - { - return false; - } - return offer.EndTime < time || (offer.Quantity ?? 0) < 1; } /// - /// Add a stale offers id to collection for later use + /// Add a stale offers id to _expiredOfferIds collection for later processing /// /// Id of offer to add to stale collection public void FlagOfferAsExpired(string staleOfferId) @@ -366,18 +367,4 @@ public class RagfairOfferHolder( } } } - - /// - /// Remove all offers flagged as stale/expired - /// - public void RemoveExpiredOffers() - { - lock (_expiredOfferIdsLock) - { - foreach (var expiredOfferId in _expiredOfferIds) - { - RemoveOffer(expiredOfferId, false); - } - } - } }