using Core.Helpers; using Core.Models.Eft.Common.Tables; using Core.Models.Eft.Ragfair; using Core.Models.Spt.Config; using Core.Models.Utils; using Core.Servers; using Core.Services; using SptCommon.Annotations; namespace Core.Utils; [Injectable(InjectionType.Singleton)] public class RagfairOfferHolder( ISptLogger logger, RagfairServerHelper ragfairServerHelper, ProfileHelper profileHelper, HashUtil hashUtil, LocalisationService localisationService, ConfigServer configServer) { protected int _maxOffersPerTemplate = configServer.GetConfig().Dynamic.OfferItemCount.Max; protected Dictionary _offersById = new(); protected object _offersByIdLock = new(); protected Dictionary> _offersByTemplate = new(); // key = tplId, value = list of offerIds protected object _offersByTemplateLock = new(); protected Dictionary> _offersByTrader = new(); // key = traderId, value = list of offerIds protected object _offersByTraderLock = new(); protected HashSet _expiredOfferIds = []; protected object _expiredOfferIdsLock = new(); /// /// Get a ragfair offer by its id /// /// Ragfair offer id /// RagfairOffer public RagfairOffer? GetOfferById(string id) { lock (_offersByIdLock) { return _offersById.GetValueOrDefault(id); } } /// /// Get ragfair offers that match the passed in tpl /// /// Tpl to get offers for /// RagfairOffer list public List? GetOffersByTemplate(string templateId) { lock (_offersByTemplateLock) { // 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) .ToList(); return result; } } /// /// Get all offers being sold by a trader /// /// Id of trader to get offers for /// RagfairOffer list public List? GetOffersByTrader(string traderId) { lock (_offersByTraderLock) { if (!_offersByTrader.TryGetValue(traderId, out var offerIds)) { return null; } return offerIds.Select(offerId => _offersById.GetValueOrDefault(offerId)) .Where(offer => offer != null) .ToList(); } } /// /// Get all ragfair offers /// /// RagfairOffer list public List GetOffers() { lock (_offersByIdLock) { if (_offersById.Count > 0) { return _offersById.Values.ToList(); } } return []; } /// /// Add a collection of offers to ragfair /// /// Offers to add public void AddOffers(List offers) { foreach (var offer in offers) { AddOffer(offer); } } /// /// Add single offer to ragfair /// /// Offer to add public void AddOffer(RagfairOffer offer) { lock (_offersByIdLock) { var sellerId = offer.User.Id; // Keep generating IDs until we get a unique one while (_offersById.ContainsKey(offer.Id)) { offer.Id = hashUtil.Generate(); } 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 sellerIsTrader = ragfairServerHelper.IsTrader(sellerId); if (itemTpl != null && !(sellerIsTrader || profileHelper.IsPlayer(sellerId)) && _offersByTemplate.TryGetValue(itemTpl, out var offers) && offers?.Count >= _maxOffersPerTemplate ) { return; } _offersById.Add(offer.Id, offer); if (sellerIsTrader) { AddOfferByTrader(sellerId, 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(string offerId, bool checkTraderOffers = true) { lock (_offersByIdLock) { if (!_offersById.TryGetValue(offerId, out var offer)) { logger.Warning(localisationService.GetText("ragfair-unable_to_remove_offer_doesnt_exist", offerId)); return; } _offersById.Remove(offer.Id); if (checkTraderOffers) { lock (_offersByTraderLock) { if (_offersByTrader.ContainsKey(offer.User.Id)) { _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) { _offersByTrader.Remove(offer.User.Id); } } } } lock (_offersByTemplateLock) { var firstItem = offer.Items.FirstOrDefault(); if (_offersByTemplate.TryGetValue(firstItem.Template, out var offers)) { offers.Remove(offer.Id); } } } } /// /// Remove all offers a trader has /// /// Trader id to remove offers from public void RemoveAllOffersByTrader(string traderId) { lock (_offersByTraderLock) { if (_offersByTrader.TryGetValue(traderId, out var offerIdsToRemove)) { foreach (var offerId in offerIdsToRemove) { _offersById.Remove(offerId); } // Clear out linking table _offersByTrader[traderId].Clear(); } } } /// /// Add offer to offersByTemplate cache /// /// Tpl to store offer against /// Offer to store against tpl protected void AddOfferByTemplates(string template, string offerId) { lock (_offersByTemplateLock) { if (_offersByTemplate.ContainsKey(template)) { _offersByTemplate[template].Add(offerId); } else { _offersByTemplate.Add(template, [offerId]); } } } /// /// Cache an offer inside `offersByTrader` by trader id /// /// Trader id to store offer against /// Offer to store against protected void AddOfferByTrader(string trader, string offerId) { lock (_offersByTraderLock) { if (_offersByTrader.ContainsKey(trader)) { _offersByTrader[trader].Add(offerId); } else { _offersByTrader.Add(trader, [offerId]); } } } /// /// Is the passed in offer stale - end time > passed in time /// /// Offer to check /// Time to check offer against /// True - offer is stale 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 /// /// Id of offer to add to stale collection public void FlagOfferAsExpired(string staleOfferId) { lock (_expiredOfferIdsLock) { _expiredOfferIds.Add(staleOfferId); } } /// /// 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 List> 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($"offerId: {expiredOfferId} was not found !!"); continue; } if (offer?.Items?.Count == 0) { logger.Error($"Unable to process expired offer: {expiredOfferId}, it has no items"); 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 expiry 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 (IsStale(offer, timestamp)) { _expiredOfferIds.Add(offer.Id); } } } } /// /// Remove all offers flagged as stale/expired /// public void RemoveExpiredOffers() { lock (_expiredOfferIdsLock) { foreach (var expiredOfferId in _expiredOfferIds) { RemoveOffer(expiredOfferId, false); } } } }