using SPTarkov.Common.Extensions; using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Extensions; using SPTarkov.Server.Core.Models.Common; 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.Profile; using SPTarkov.Server.Core.Models.Eft.Ragfair; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Spt.Config; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Routers; using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Services; using SPTarkov.Server.Core.Utils; namespace SPTarkov.Server.Core.Helpers; [Injectable] public class RagfairOfferHelper( ISptLogger logger, TimeUtil timeUtil, BotHelper botHelper, RagfairSortHelper ragfairSortHelper, PresetHelper presetHelper, RagfairHelper ragfairHelper, PaymentHelper paymentHelper, TraderHelper traderHelper, QuestHelper questHelper, RagfairServerHelper ragfairServerHelper, ItemHelper itemHelper, DatabaseService databaseService, RagfairOfferService ragfairOfferService, LocaleService localeService, ServerLocalisationService serverLocalisationService, MailSendService mailSendService, RagfairRequiredItemsService ragfairRequiredItemsService, ProfileHelper profileHelper, EventOutputHolder eventOutputHolder, ConfigServer configServer ) { protected const string _goodSoldTemplate = "5bdabfb886f7743e152e867e 0"; // Your {soldItem} {itemCount} items were bought by {buyerNickname}. protected readonly BotConfig _botConfig = configServer.GetConfig(); protected readonly RagfairConfig _ragfairConfig = configServer.GetConfig(); /// /// Pass through to ragfairOfferService.getOffers(), get flea offers a player should see /// /// Data from client /// ragfairHelper.filterCategories() /// Trader assorts /// Player profile /// Offers the player should see public List GetValidOffers( SearchRequestData searchRequest, List itemsToAdd, Dictionary traderAssorts, PmcData pmcData ) { var playerIsFleaBanned = pmcData.PlayerIsFleaBanned(timeUtil.GetTimeStamp()); var tieredFlea = _ragfairConfig.TieredFlea; var tieredFleaLimitTypes = tieredFlea.UnlocksType; return ragfairOfferService .GetOffers() .Where(offer => { if (!PassesSearchFilterCriteria(searchRequest, offer, pmcData)) { return false; } var isDisplayable = IsDisplayableOffer(searchRequest, itemsToAdd, traderAssorts, offer, pmcData, playerIsFleaBanned); if (!isDisplayable) { return false; } // Not trader offer + tiered flea enabled if (tieredFlea.Enabled && !offer.IsTraderOffer()) { CheckAndLockOfferFromPlayerTieredFlea( tieredFlea, offer, tieredFleaLimitTypes.Keys.ToHashSet(), pmcData.Info.Level.Value ); } return true; }) .ToList(); } /// /// Disable offer if item is flagged by tiered flea config based on player level /// /// Tiered flea settings from ragfair config /// Ragfair offer to evaluate /// List of item types flagged with a required player level /// Current level of player viewing offer protected void CheckAndLockOfferFromPlayerTieredFlea( TieredFlea tieredFlea, RagfairOffer offer, HashSet tieredFleaLimitTypes, int playerLevel ) { var offerItemTpl = offer.Items.FirstOrDefault().Template; // Check if offer item is ammo if (tieredFlea.AmmoTplUnlocks is not null && itemHelper.IsOfBaseclass(offerItemTpl, BaseClasses.AMMO)) { // Check if ammo is flagged with a level requirement if (tieredFlea.AmmoTplUnlocks.TryGetValue(offerItemTpl, out var unlockLevel) && playerLevel < unlockLevel) { // Lock the offer if player's level is below the ammo's unlock requirement offer.Locked = true; return; } } // Check for a direct level requirement for the offer item if (tieredFlea.UnlocksTpl.TryGetValue(offerItemTpl, out var itemLevelRequirement)) { if (playerLevel < itemLevelRequirement) { // Lock the offer if player's level is below the item's specific requirement offer.Locked = true; return; } } // Optimisation - Skip further checks if the item type isn't in the restricted types list if (!itemHelper.IsOfBaseclasses(offerItemTpl, tieredFleaLimitTypes)) { return; } // Check if the item belongs to any restricted type and if player level is insufficient if ( tieredFleaLimitTypes .Where(tieredItemType => itemHelper.IsOfBaseclass(offerItemTpl, tieredItemType)) .Any(tieredItemType => playerLevel < tieredFlea.UnlocksType[tieredItemType]) ) { // Players level is below matching types requirement, flag as locked offer.Locked = true; } } /// /// Get matching offers that require the desired item and filter out offers from non traders if player is below ragfair /// unlock level /// /// Search request from client /// Player profile /// Matching RagfairOffer objects public List GetOffersThatRequireItem(SearchRequestData searchRequest, PmcData pmcData) { // Get all offers that require the desired item and filter out offers from non traders if player below ragfair unlock var offerIDsForItem = ragfairRequiredItemsService.GetRequiredOffersById(searchRequest.NeededSearchId.Value); var tieredFlea = _ragfairConfig.TieredFlea; var tieredFleaLimitTypes = tieredFlea.UnlocksType; var result = new List(); foreach ( var offer in offerIDsForItem .Select(ragfairOfferService.GetOfferByOfferId) .Where(offer => PassesSearchFilterCriteria(searchRequest, offer, pmcData)) ) { if (tieredFlea.Enabled && !offer.IsTraderOffer()) { CheckAndLockOfferFromPlayerTieredFlea(tieredFlea, offer, tieredFleaLimitTypes.Keys.ToHashSet(), pmcData.Info.Level.Value); } result.Add(offer); } return result; } /// /// Get offers from flea/traders specifically when building weapon preset /// /// Search request data /// string array of item tpls to search for /// All trader assorts player can access/buy /// Player profile /// RagfairOffer array public List GetOffersForBuild( SearchRequestData searchRequest, List itemsToAdd, Dictionary traderAssorts, PmcData pmcData ) { var offersMap = new Dictionary>(); var offersToReturn = new List(); var playerIsFleaBanned = pmcData.PlayerIsFleaBanned(timeUtil.GetTimeStamp()); var tieredFlea = _ragfairConfig.TieredFlea; var tieredFleaLimitTypes = tieredFlea.UnlocksType; foreach (var desiredItemTpl in searchRequest.BuildItems) { var matchingOffers = ragfairOfferService.GetOffersOfType(desiredItemTpl.Key); if (matchingOffers is null) // No offers found for this item, skip { continue; } foreach (var offer in matchingOffers) { // Don't show pack offers if (offer.SellInOnePiece.GetValueOrDefault(false)) { continue; } if (!PassesSearchFilterCriteria(searchRequest, offer, pmcData)) { continue; } if (!IsDisplayableOffer(searchRequest, itemsToAdd, traderAssorts, offer, pmcData, playerIsFleaBanned)) { continue; } if (offer.IsTraderOffer()) { if (TraderBuyRestrictionReached(offer)) { continue; } if (TraderOutOfStock(offer)) { continue; } if (TraderOfferItemQuestLocked(offer, traderAssorts)) { continue; } if (TraderOfferLockedBehindLoyaltyLevel(offer, pmcData)) { continue; } } // Tiered flea and not trader offer if (tieredFlea.Enabled && !offer.IsTraderOffer()) { CheckAndLockOfferFromPlayerTieredFlea( tieredFlea, offer, tieredFleaLimitTypes.Keys.ToHashSet(), pmcData.Info.Level.Value ); // Do not add offer to build if user does not have access to it if (offer.Locked.GetValueOrDefault(false)) { continue; } } var key = offer.Items[0].Template; if (!offersMap.ContainsKey(key)) { offersMap.Add(key, []); } offersMap[key].Add(offer); } } // Get best offer for each item to show on screen var offersToSort = new List(); foreach (var possibleOffers in offersMap.Values) { // prepare temp list for offers offersToSort.Clear(); offersToSort.AddRange(possibleOffers); // Remove offers with locked = true (quest locked) when > 1 possible offers // single trader item = shows greyed out // multiple offers for item = is greyed out if (possibleOffers.Count > 1) { var lockedOffers = GetLoyaltyLockedOffers(possibleOffers, pmcData); // Exclude locked offers + above loyalty locked offers if at least 1 was found offersToSort = possibleOffers .Where(offer => !(offer.Locked.GetValueOrDefault(false) || lockedOffers.Contains(offer.Id))) .ToList(); // Exclude trader offers over their buy restriction limit offersToSort = GetOffersInsideBuyRestrictionLimits(offersToSort); } // Sort offers by price and pick the best var offer = ragfairSortHelper.SortOffers(offersToSort, RagfairSort.PRICE)[0]; offersToReturn.Add(offer); } return offersToReturn; } /// /// Should a ragfair offer be visible to the player /// /// Client request /// /// Trader assort items - used for filtering out locked trader items /// Flea offer /// Player profile /// Player cannot view flea yet/ever /// True = should be shown to player private bool IsDisplayableOffer( SearchRequestData searchRequest, List itemsToAdd, Dictionary traderAssorts, RagfairOffer offer, PmcData pmcProfile, bool playerIsFleaBanned = false ) { var offerRootItem = offer.Items.FirstOrDefault(); // Currency offer is sold for var moneyTypeTpl = offer.Requirements.FirstOrDefault().TemplateId; var isTraderOffer = offer.IsTraderOffer(); if (!isTraderOffer && playerIsFleaBanned) { return false; } // Offer root items tpl not in searched for array if (!itemsToAdd.Contains(offerRootItem.Template)) // skip items we shouldn't include { return false; } // Performing a required search and offer doesn't have requirement for item if ( !string.IsNullOrEmpty(searchRequest.NeededSearchId) && !offer.Requirements.Any(requirement => requirement.TemplateId == searchRequest.NeededSearchId) ) { return false; } // Weapon/equipment search + offer is preset if ( searchRequest.BuildItems.Count == 0 && // Prevent equipment loadout searches filtering out presets searchRequest.BuildCount.GetValueOrDefault(0) > 0 && presetHelper.HasPreset(offerRootItem.Template) ) { return false; } // commented out as required search "which is for checking offers that are barters" // has info.removeBartering as true, this if statement removed barter items. if (searchRequest.RemoveBartering.GetValueOrDefault(false) && !paymentHelper.IsMoneyTpl(moneyTypeTpl)) // Don't include barter offers { return false; } if (offer.RequirementsCost is null) // Don't include offers with undefined or NaN in it { return false; } // Handle trader items to remove items that are not available to the user right now // e.g. required search for "lamp" shows 4 items, 3 of which are not available to a new player // filter those out if (isTraderOffer) { if (!traderAssorts.TryGetValue(offer.User.Id, out var assort)) // trader not visible on flea market { return false; } if (!assort.Items.Any(item => item.Id == offer.Root)) // skip (quest) locked items { return false; } } return true; } /// /// Get offers that have not exceeded buy limits /// /// offers to process /// Offers protected List GetOffersInsideBuyRestrictionLimits(List possibleOffers) { // Check offer has buy limit + is from trader + current buy count is at or over max return possibleOffers .Where(offer => { if (offer.BuyRestrictionMax is null && offer.IsTraderOffer() && offer.BuyRestrictionCurrent >= offer.BuyRestrictionMax) { if (offer.BuyRestrictionCurrent >= offer.BuyRestrictionMax) { return false; } } // Doesn't have buy limits, return offer return true; }) .ToList(); } /// /// Check if offer is from trader standing the player does not have /// /// Offer to check /// Player profile /// True if item is locked, false if item is purchaseable protected bool TraderOfferLockedBehindLoyaltyLevel(RagfairOffer offer, PmcData pmcProfile) { if (!pmcProfile.TradersInfo.TryGetValue(offer.User.Id, out var userTraderSettings)) { logger.Warning($"Trader: {offer.User.Id} not found in profile, assuming offer is not locked being loyalty level"); return false; } return userTraderSettings.LoyaltyLevel < offer.LoyaltyLevel; } /// /// Check if offer item is quest locked for current player by looking at sptQuestLocked property in traders /// barter_scheme /// /// Offer to check is quest locked /// all trader assorts for player /// true if quest locked public bool TraderOfferItemQuestLocked(RagfairOffer offer, Dictionary traderAssorts) { var itemIds = offer.Items.Select(x => x.Id).ToHashSet(); //foreach (var item in offer.Items) //{ // traderAssorts.TryGetValue(offer.User.Id, out var assorts); // foreach (var barterKvP in assorts.BarterScheme.Where(x => itemIds.Contains(x.Key))) // { // foreach (var subBarter in barterKvP.Value) // { // if (subBarter.Any(subBarter => subBarter.SptQuestLocked.GetValueOrDefault(false))) // { // return true; // } // } // } //} foreach (var _ in offer.Items) { traderAssorts.TryGetValue(offer.User.Id, out var assorts); if ( assorts .BarterScheme.Where(x => itemIds.Contains(x.Key)) .Any(barterKvP => barterKvP.Value.Any(subBarter => subBarter.Any(subBarter => subBarter.SptQuestLocked.GetValueOrDefault(false))) ) ) { return true; } } // Fallback, nothing found return false; } /// /// Has trader offer ran out of stock to sell to player /// /// Offer to check stock of /// true if out of stock protected bool TraderOutOfStock(RagfairOffer offer) { if (offer.Items?.Count == 0) { return true; } return offer.Items.FirstOrDefault()?.Upd?.StackObjectsCount == 0; } /// /// Check if trader offers' BuyRestrictionMax value has been reached /// /// Offer to check restriction properties of /// true if restriction reached, false if no restrictions/not reached protected bool TraderBuyRestrictionReached(RagfairOffer offer) { var traderAssorts = traderHelper.GetTraderAssortsByTraderId(offer.User.Id).Items; // Find item being purchased from traders assorts var assortData = traderAssorts.FirstOrDefault(item => item.Id == offer.Items[0].Id); if (assortData is null) { // No trader assort data logger.Warning( $"Unable to find trader: " + $"${offer.User.Nickname}assort for item: {itemHelper.GetItemName(offer.Items[0].Template)} " + $"{offer.Items[0].Template}, cannot check if buy restriction reached" ); return false; } if (assortData.Upd is null) // No Upd = no chance of limits { return false; } // No restriction values // Can't use !assortData.upd.BuyRestrictionX as value could be 0 if (assortData.Upd.BuyRestrictionMax is null || assortData.Upd.BuyRestrictionCurrent is null) { return false; } // Current equals max, limit reached if (assortData.Upd.BuyRestrictionCurrent >= assortData.Upd.BuyRestrictionMax) { return true; } return false; } protected HashSet GetLoyaltyLockedOffers(IEnumerable offers, PmcData pmcProfile) { var loyaltyLockedOffers = new HashSet(); foreach (var offer in offers.Where(x => x.IsTraderOffer())) { if (pmcProfile.TradersInfo.TryGetValue(offer.User.Id, out var traderDetails) && traderDetails.LoyaltyLevel < offer.LoyaltyLevel) { loyaltyLockedOffers.Add(offer.Id); } } return loyaltyLockedOffers; } /// /// Process all player-listed flea offers for a desired profile /// /// Session id to process offers for /// true = complete public bool ProcessOffersOnProfile(MongoId sessionId) { var currentTimestamp = timeUtil.GetTimeStamp(); var profileOffers = GetProfileOffers(sessionId); // No offers, don't do anything if (!profileOffers.Any()) { return true; } // Index backwards as CompleteOffer() can delete offer object for (var index = profileOffers.Count - 1; index >= 0; index--) { var offer = profileOffers[index]; if (currentTimestamp > offer.EndTime) { // Offer has expired before selling, skip as it will be processed in RemoveExpiredOffers() continue; } if (offer.SellResults is null || !offer.SellResults.Any() || currentTimestamp < offer.SellResults.FirstOrDefault()?.SellTime) { // 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 root item StackObjectsCount properties of an array of items /// /// items to sum up /// Total stack count public double GetTotalStackCountSize(IEnumerable> itemsInInventoryToSumStackCount) { return itemsInInventoryToSumStackCount.Sum(itemAndChildren => itemAndChildren.FirstOrDefault()?.Upd?.StackObjectsCount.GetValueOrDefault(1) ?? 1 ); } /// /// 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; profile.CharacterData.PmcData.RagfairInfo.IsRatingGrowing = true; if (amountToIncrementBy is null) { logger.Warning($"Unable to increment ragfair rating, value was not a number: {amountToIncrementBy}"); return; } profile.CharacterData.PmcData.RagfairInfo.Rating += ragfairGlobalsConfig.RatingIncreaseCount / ragfairGlobalsConfig.RatingSumForIncrease * amountToIncrementBy; } /// /// Return all offers a player has listed on a desired profile /// /// Session/Player id /// List of ragfair offers protected List GetProfileOffers(MongoId sessionId) { var profile = profileHelper.GetPmcProfile(sessionId); if (profile.RagfairInfo?.Offers is null) { return []; } return profile.RagfairInfo.Offers; } /// /// Delete an offer from a desired profile and from ragfair offers /// /// Session id of profile to delete offer from /// Id of offer to delete protected void DeleteOfferById(MongoId sessionId, MongoId offerId) { var profileRagfairInfo = profileHelper.GetPmcProfile(sessionId).RagfairInfo; var offerIndex = profileRagfairInfo.Offers.FindIndex(o => o.Id == offerId); if (offerIndex == -1) { logger.Warning($"Unable to find offer: {offerId} in profile: {sessionId}, unable to delete"); } if (offerIndex >= 0) { profileRagfairInfo.Offers.Splice(offerIndex, 1); } // Also delete from ragfair ragfairOfferService.RemoveOfferById(offerId); } /// /// Complete the selling of players' offer /// /// Session/Player id /// Sold offer details /// Amount item was purchased for /// ItemEventRouterResponse public ItemEventRouterResponse CompleteOffer(MongoId offerOwnerSessionId, RagfairOffer offer, int boughtAmount) { // Pack or ALL items of a multi-offer were bought - remove entire offer if (offer.SellInOnePiece.GetValueOrDefault(false) || boughtAmount == offer.Quantity) { DeleteOfferById(offerOwnerSessionId, offer.Id); } else { // Partial purchase, reduce quantity by amount purchased offer.Quantity -= boughtAmount; } // 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 var requestedItem = new Item { Id = new MongoId(), Template = requirement.TemplateId, Upd = new Upd { StackObjectsCount = requirement.Count * boughtAmount }, }; var stacks = itemHelper.SplitStack(requestedItem); foreach (var item in stacks) { var outItems = new List { item }; // TODO - is this code used?, may have been when adding barters to flea was still possible for player if (requirement.OnlyFunctional.GetValueOrDefault(false)) { var presetItems = ragfairServerHelper.GetPresetItemsByTpl(item); if (presetItems.Count > 0) { outItems.Add(presetItems[0]); } } paymentItemsToSendToPlayer.AddRange(outItems); } } var ragfairDetails = new MessageContentRagfair { OfferId = offer.Id, // pack-offers NEED to be the full item count, // otherwise it only removes 1 from the pack, leaving phantom offer on client ui Count = offer.SellInOnePiece.GetValueOrDefault(false) ? offerStackCount.Value : boughtAmount, HandbookId = itemTpl, }; var storageTimeSeconds = timeUtil.GetHoursAsSeconds((int)questHelper.GetMailItemRedeemTimeHoursForProfile(sellerProfile)); mailSendService.SendDirectNpcMessageToPlayer( offerOwnerSessionId, Traders.RAGMAN, MessageType.FleamarketMessage, GetLocalisedOfferSoldMessage(itemTpl, boughtAmount), paymentItemsToSendToPlayer, storageTimeSeconds, null, ragfairDetails ); // Adjust sellers sell sum values sellerProfile.RagfairInfo.SellSum ??= 0; sellerProfile.RagfairInfo.SellSum += offer.SummaryCost; return eventOutputHolder.GetOutput(offerOwnerSessionId); } /// /// Get a localised message for when players offer has sold on flea /// /// Item sold /// /// Localised string protected string GetLocalisedOfferSoldMessage(MongoId itemTpl, int boughtAmount) { // Generate a message to inform that item was sold var globalLocales = localeService.GetLocaleDb(); if (!globalLocales.TryGetValue(_goodSoldTemplate, out var soldMessageLocaleGuid)) { logger.Error(serverLocalisationService.GetText("ragfair-unable_to_find_locale_by_key", _goodSoldTemplate)); } // Used to replace tokens in sold message sent to player var messageKey = $"{itemTpl.ToString()} Name"; var hasKey = globalLocales.TryGetValue(messageKey, out var value); var tplVars = new SystemData { SoldItem = hasKey ? value : itemTpl, BuyerNickname = botHelper.GetPmcNicknameOfMaxLength(_botConfig.BotNameLengthLimit), ItemCount = boughtAmount, }; // Node searches for anything inside {property}: e.g.: "Your {soldItem} {itemCount} items were bought by {buyerNickname}." // each part the takes the inside "Key" and gets it from the tplVars object // 'Your Kalashnikov AKS-74U 5.45x39 assault rifle 1 items were bought by HB.' // then seems to replace any " with nothing // Seems to be much simpler just replacing each key like this. soldMessageLocaleGuid = soldMessageLocaleGuid.Replace("{soldItem}", tplVars.SoldItem); soldMessageLocaleGuid = soldMessageLocaleGuid.Replace("{itemCount}", tplVars.ItemCount.ToString()); soldMessageLocaleGuid = soldMessageLocaleGuid.Replace("{buyerNickname}", tplVars.BuyerNickname); return soldMessageLocaleGuid; } /// /// Check an offer passes the various search criteria the player requested /// /// Client search request /// Offer to check /// Player profile /// True if offer passes criteria protected bool PassesSearchFilterCriteria(SearchRequestData searchRequest, RagfairOffer offer, PmcData pmcData) { var isDefaultUserOffer = offer.User.MemberType == MemberCategory.Default; var offerRootItem = offer.Items.FirstOrDefault(); var offerMoneyTypeTpl = offer.Requirements.FirstOrDefault().TemplateId; if (pmcData.Info.Level < databaseService.GetGlobals().Configuration.RagFair.MinUserLevel && isDefaultUserOffer) // Skip item if player is < global unlock level (default is 15) and item is from a dynamically generated source { return false; } var isTraderOffer = offer.IsTraderOffer(); if (searchRequest.OfferOwnerType == OfferOwnerType.TRADEROWNERTYPE && !isTraderOffer) // don't include player offers { return false; } if (searchRequest.OfferOwnerType == OfferOwnerType.PLAYEROWNERTYPE && isTraderOffer) // don't include trader offers { return false; } if (searchRequest.OneHourExpiration.GetValueOrDefault(false) && offer.EndTime - timeUtil.GetTimeStamp() > TimeUtil.OneHourAsSeconds) // offer expires within an hour { return false; } if (searchRequest.QuantityFrom > 0 && offerRootItem.Upd.StackObjectsCount < searchRequest.QuantityFrom) // Too few items to offer { return false; } if (searchRequest.QuantityTo > 0 && offerRootItem.Upd.StackObjectsCount > searchRequest.QuantityTo) // Too many items to offer { return false; } if (searchRequest.OnlyFunctional.GetValueOrDefault(false) && !IsItemFunctional(offerRootItem, offer)) // Don't include non-functional items { return false; } if (offer.Items.Count == 1) { // Counts quality % using the offer items current durability compared to its possible max, not current max // Single item if ( IsConditionItem(offerRootItem) && !ItemQualityInRange(offerRootItem, searchRequest.ConditionFrom.Value, searchRequest.ConditionTo.Value) ) { return false; } } else { var itemQualityPercent = itemHelper.GetItemQualityModifierForItems(offer.Items) * 100; if (itemQualityPercent < searchRequest.ConditionFrom) { return false; } if (itemQualityPercent > searchRequest.ConditionTo) { return false; } } if (searchRequest.Currency > 0 && paymentHelper.IsMoneyTpl(offerMoneyTypeTpl)) { // Only want offers with specific currency if ( ragfairHelper.GetCurrencyTag(offerMoneyTypeTpl) != ragfairHelper.GetCurrencyTag(searchRequest.Currency.GetValueOrDefault(0)) ) { // Offer is for different currency to what search params allow, skip return false; } } if (searchRequest.PriceFrom > 0 && searchRequest.PriceFrom >= offer.RequirementsCost) // price is too low { return false; } if (searchRequest.PriceTo > 0 && searchRequest.PriceTo <= offer.RequirementsCost) // price is too high { return false; } // Passes above checks, search criteria filters have not filtered offer out return true; } /// /// Check that the passed in offer item is functional /// /// The root item of the offer /// Flea offer to check /// True if the given item is functional public bool IsItemFunctional(Item offerRootItem, RagfairOffer offer) { // Non-preset weapons/armor are always functional if (!presetHelper.HasPreset(offerRootItem.Template)) { return true; } // For armor items that can hold mods, make sure the item count is at least the amount of required plates if (itemHelper.ArmorItemCanHoldMods(offerRootItem.Template)) { var offerRootTemplate = itemHelper.GetItem(offerRootItem.Template).Value; var requiredPlateCount = offerRootTemplate.Properties.Slots?.Where(item => item.Required.GetValueOrDefault(false)).Count(); return offer.Items.Count > requiredPlateCount; } // For other presets, make sure the offer has more than 1 item return offer.Items.Count > 1; } /// /// Does the passed in item have a condition property /// /// Item to check /// True if has condition protected bool IsConditionItem(Item item) { // thanks typescript, undefined assertion is not returnable since it // tries to return a multi-type object if (item.Upd is null) { return false; } return item.Upd.MedKit is not null || item.Upd.Repairable is not null || item.Upd.Resource is not null || item.Upd.FoodDrink is not null || item.Upd.Key is not null || item.Upd.RepairKit is not null; } /// /// Is items quality value within desired range /// /// Item to check quality of /// Desired minimum quality /// Desired maximum quality /// True if in range protected bool ItemQualityInRange(Item item, int min, int max) { var itemQualityPercentage = 100 * itemHelper.GetItemQualityModifier(item); if (min > 0 && min > itemQualityPercentage) // Item condition too low { return false; } if (max < 100 && max <= itemQualityPercentage) // Item condition too high { return false; } return true; } }