using System.Collections.Concurrent; using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Extensions; 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.Ragfair; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Services; namespace SPTarkov.Server.Core.Utils; [Injectable(InjectionType.Singleton)] public class RagfairOfferHolder( ISptLogger _logger, RagfairServerHelper _ragfairServerHelper, ServerLocalisationService _serverLocalisationService, ItemHelper _itemHelper ) { /// /// Expired offer Ids /// private readonly HashSet _expiredOfferIds = []; /// /// Ragfair offer cache, keyed by offer Id /// private readonly ConcurrentDictionary _offersById = new(); /// /// Offer Ids keyed by tpl /// private readonly ConcurrentDictionary> _offersByTemplate = new(); /// /// Offer ids keyed by trader Id /// private readonly ConcurrentDictionary> _offersByTrader = new(); /// /// Fake player offer ids keyed by itemTPl /// private readonly ConcurrentDictionary> _fakePlayerOffers = new(); private readonly Lock _expiredOfferIdsLock = new(); private readonly Lock _ragfairOperationLock = new(); /// /// Get a ragfair offer by its id /// /// Ragfair offer id /// RagfairOffer public RagfairOffer? GetOfferById(MongoId id) { return _offersById.GetValueOrDefault(id); } /// /// Get a ragfair offer by its id /// /// RagfairOffer public HashSet GetStaleOfferIds() { lock (_expiredOfferIdsLock) { return _expiredOfferIds; } } /// /// Get ragfair offers that match the passed in tpl /// /// Tpl to get offers for /// RagfairOffer list public IEnumerable? GetOffersByTemplate(MongoId templateId) { // Get the offerIds we want to return if (!_offersByTemplate.TryGetValue(templateId, out var offerIds)) { return null; } var result = _offersById.Where(x => offerIds.Contains(x.Key)).Select(x => x.Value); return result; } /// /// Get all offers being sold by a trader /// /// Id of trader to get offers for /// RagfairOffer list public IEnumerable GetOffersByTrader(MongoId traderId) { if (!_offersByTrader.TryGetValue(traderId, out var offerIds)) { return []; } return offerIds.Select(offerId => _offersById.GetValueOrDefault(offerId)).Where(offer => offer != null); } /// /// Get all ragfair offers /// /// RagfairOffer list public List GetOffers() { if (!_offersById.IsEmpty) { return _offersById.Values.ToList(); } return []; } /// /// Add a collection of offers to ragfair /// /// Offers to add public void AddOffers(IEnumerable offers) { foreach (var offer in offers) { AddOffer(offer); } } /// /// Add single offer to ragfair /// /// Offer to add public void AddOffer(RagfairOffer offer) { lock (_ragfairOperationLock) { // Keep generating IDs until we get a unique one while (_offersById.ContainsKey(offer.Id)) { offer.Id = new MongoId(); } var itemTpl = offer.Items?.FirstOrDefault()?.Template ?? new MongoId(); if ( !itemTpl.IsEmpty() // Has tpl && offer.IsFakePlayerOffer() && _fakePlayerOffers.TryGetValue(itemTpl, out var offers) && offers?.Count >= _ragfairServerHelper.GetOfferCountByBaseType(_itemHelper.GetItem(itemTpl).Value.Parent) ) { // If it is an NPC PMC offer AND we have already reached the maximum amount of possible offers // for this template, don't add more return; } if (!_offersById.TryAdd(offer.Id, offer)) { _logger.Warning($"Offer: {offer.Id} already exists"); } if (offer.IsTraderOffer()) { AddOfferByTrader(offer.User.Id, offer.Id); } if (offer.IsFakePlayerOffer()) { AddFakePlayerOffer(itemTpl, offer.Id); } AddOfferByTemplates(itemTpl, offer.Id); } } /// /// Remove an offer from ragfair by id /// /// Offer id to remove /// OPTIONAL - Should trader offers be checked for offer id public void RemoveOffer(MongoId offerId, bool checkTraderOffers = true) { if (!_offersById.TryGetValue(offerId, out var offer)) { _logger.Warning(_serverLocalisationService.GetText("ragfair-unable_to_remove_offer_doesnt_exist", offerId)); return; } if (!_offersById.TryRemove(offer.Id, out _)) { _logger.Warning($"Unable to remove offer by id: {offer.Id} not found"); } if (checkTraderOffers && _offersByTrader.TryGetValue(offer.User.Id, out var traderOfferIds)) { 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} not found"); } } } var rootItem = offer.Items.FirstOrDefault(); if (_offersByTemplate.TryGetValue(rootItem.Template, out var offers)) { offers.Remove(offer.Id); } if (offer.IsFakePlayerOffer() && _fakePlayerOffers.TryGetValue(offer.Items.FirstOrDefault().Template, out var fakePlayerOfferIds)) { fakePlayerOfferIds.Remove(offer.Id); } } /// /// Remove all offers a trader has /// /// Trader id to remove offers from public void RemoveAllOffersByTrader(MongoId traderId) { if (!_offersByTrader.TryGetValue(traderId, out var offerIdsToRemove)) { // 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(); } /// /// Add offer to offersByTemplate cache /// /// Tpl to store offer against /// Offer to store against tpl /// True - offer was added protected bool AddOfferByTemplates(MongoId template, MongoId offerId) { // Look for hashset for tpl first if (_offersByTemplate.TryGetValue(template, out var offerIds)) { offerIds.Add(offerId); return true; } // Add new KvP of tpl and offer id in new hashset if (_offersByTemplate.TryAdd(template, [offerId])) { return true; } _logger.Warning($"Unable to add offer: {offerId} to _offersByTemplate"); return false; } /// /// Cache an offer inside `offersByTrader` by trader id /// /// Trader id to store offer against /// Offer to store against /// True - offer was added protected bool AddOfferByTrader(MongoId trader, MongoId offerId) { // Look for hashset for trader first if (_offersByTrader.TryGetValue(trader, out var traderOfferIds)) { traderOfferIds.Add(offerId); return true; } // Add new KvP of trader and offer id in new hashset if (_offersByTrader.TryAdd(trader, [offerId])) { return true; } _logger.Error($"Unable to add offer: {offerId} to _offersByTrader"); return false; } protected bool AddFakePlayerOffer(MongoId itemTpl, MongoId offerId) { // Look for hashset for trader first if (_fakePlayerOffers.TryGetValue(itemTpl, out var fakePlayerOfferIds)) { fakePlayerOfferIds.Add(offerId); return true; } // Add new KvP of trader and offer id in new hashset if (_fakePlayerOffers.TryAdd(itemTpl, [offerId])) { return true; } _logger.Error($"Unable to add offer: {offerId} to _fakePlayerOffers"); return false; } /// /// Add a stale offers id to _expiredOfferIds collection for later processing /// /// Id of offer to add to stale collection public void FlagOfferAsExpired(MongoId staleOfferId) { lock (_expiredOfferIdsLock) { if (!_expiredOfferIds.Add(staleOfferId)) { _logger.Warning($"Unable to add offer: {staleOfferId} to expired offers"); } } } /// /// Get total count of current expired offers /// /// Number of expired offers public int GetExpiredOfferCount() { lock (_expiredOfferIdsLock) { return _expiredOfferIds.Count; } } /// /// Get an array of arrays of expired offer items + children /// /// Expired offer assorts public IEnumerable> GetExpiredOfferItems() { lock (_expiredOfferIdsLock) { // list of lists of item+children var expiredItems = new List>(); foreach (var expiredOfferId in _expiredOfferIds) { var offer = GetOfferById(expiredOfferId); if (offer is null) { _logger.Warning($"Expired offerId: {expiredOfferId} not found, skipping"); continue; } if (offer.Items?.Count == 0) { _logger.Error($"Expired offerId: {expiredOfferId} has no items, skipping"); continue; } expiredItems.Add(offer.Items); } return expiredItems; } } /// /// Clear out internal expiredOffers dictionary of all items /// public void ResetExpiredOfferIds() { lock (_expiredOfferIdsLock) { _expiredOfferIds.Clear(); } } /// /// Flag offers with an end date set before the passed in timestamp /// /// Timestamp at point offer is 'expired' public void FlagExpiredOffersAfterDate(long timestamp) { lock (_expiredOfferIdsLock) { foreach (var offer in GetOffers()) { if (_expiredOfferIds.Contains(offer.Id) || _ragfairServerHelper.IsTrader(offer.User.Id)) { // Already flagged or trader offer (handled separately), skip continue; } if (offer.IsStale(timestamp)) { if (!_expiredOfferIds.Add(offer.Id)) { _logger.Warning($"Unable to add offer: {offer.Id} to expired offers as it already exists"); } } } } } }