From caf89af3f1a79bd7de6b59667c4227df95221c07 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 26 Jan 2025 19:46:22 +0000 Subject: [PATCH] blah --- Libraries/Core/Helpers/RagfairOfferHelper.cs | 885 +++++++++++++++--- .../Core/Models/Spt/Config/RagfairConfig.cs | 4 +- 2 files changed, 742 insertions(+), 147 deletions(-) diff --git a/Libraries/Core/Helpers/RagfairOfferHelper.cs b/Libraries/Core/Helpers/RagfairOfferHelper.cs index ee751e7b..957a5bd4 100644 --- a/Libraries/Core/Helpers/RagfairOfferHelper.cs +++ b/Libraries/Core/Helpers/RagfairOfferHelper.cs @@ -1,3 +1,4 @@ +using System.Runtime.InteropServices.JavaScript; using SptCommon.Annotations; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; @@ -6,128 +7,399 @@ using Core.Models.Eft.Profile; using Core.Models.Eft.Ragfair; using Core.Models.Enums; using Core.Models.Spt.Config; +using Core.Models.Utils; +using Core.Routers; +using Core.Servers; +using Core.Services; +using Core.Utils; +using SptCommon.Extensions; namespace Core.Helpers; [Injectable] -public class RagfairOfferHelper +public class RagfairOfferHelper( + ISptLogger logger, + TimeUtil timeUtil, + HashUtil hashUtil, + EventOutputHolder eventOutputHolder, + DatabaseService databaseService, + TraderHelper traderHelper, + SaveServer saveServer, + ItemHelper itemHelper, + BotHelper botHelper, + PaymentHelper paymentHelper, + PresetHelper presetHelper, + ProfileHelper profileHelper, + QuestHelper questHelper, + RagfairServerHelper ragfairServerHelper, + RagfairSortHelper ragfairSortHelper, + RagfairHelper ragfairHelper, + RagfairOfferService ragfairOfferService, + RagfairRequiredItemsService ragfairRequiredItemsService, + LocaleService localeService, + LocalisationService localisationService, + MailSendService mailSendService, + ConfigServer configServer +) { - /// - /// Passthrough to ragfairOfferService.getOffers(), get flea offers a player should see - /// - /// Data from client - /// ragfairHelper.filterCategories() - /// Trader assorts - /// Player profile - /// Offers the player should see + protected static string goodSoldTemplate = "5bdabfb886f7743e152e867e 0"; // Your {soldItem} {itemCount} items were bought by {buyerNickname}. + protected RagfairConfig ragfairConfig = configServer.GetConfig(); + protected QuestConfig questConfig = configServer.GetConfig(); + protected BotConfig botConfig = configServer.GetConfig(); + + /** + * Passthrough to ragfairOfferService.getOffers(), get flea offers a player should see + * @param searchRequest Data from client + * @param itemsToAdd ragfairHelper.filterCategories() + * @param traderAssorts Trader assorts + * @param pmcData Player profile + * @returns Offers the player should see + */ public List GetValidOffers( SearchRequestData searchRequest, List itemsToAdd, Dictionary traderAssorts, - PmcData pmcData) + PmcData pmcData + ) { - throw new NotImplementedException(); + var playerIsFleaBanned = profileHelper.PlayerIsFleaBanned(pmcData); + var tieredFlea = ragfairConfig.TieredFlea; + var tieredFleaLimitTypes = tieredFlea.UnlocksType.Keys; + 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 && !OfferIsFromTrader(offer)) { + CheckAndLockOfferFromPlayerTieredFlea(tieredFlea, offer, tieredFleaLimitTypes, pmcData.Info.Level); + } + + return true; + }); } - /// - /// Disable offer if item is flagged by tiered flea config - /// - /// Tiered flea settings from ragfair config - /// Ragfair offer to check - /// Dict of item types with player level to be viewable - /// Level of player viewing offer + /** + * Disable offer if item is flagged by tiered flea config + * @param tieredFlea Tiered flea settings from ragfair config + * @param offer Ragfair offer to check + * @param tieredFleaLimitTypes Dict of item types with player level to be viewable + * @param playerLevel Level of player viewing offer + */ protected void CheckAndLockOfferFromPlayerTieredFlea( TieredFlea tieredFlea, RagfairOffer offer, - string[] tieredFleaLimitTypes, - int playerLevel) + List tieredFleaLimitTypes, + int playerLevel + ) { - throw new NotImplementedException(); + var offerItemTpl = offer.Items[0].Template; + if (tieredFlea?.AmmoTplUnlocks != null && itemHelper.IsOfBaseclass(offerItemTpl, BaseClasses.AMMO)) { + var unlockLevel = tieredFlea.AmmoTplUnlocks[offerItemTpl]; + if (unlockLevel != null && playerLevel < unlockLevel) { + offer.Locked = true; + return; + } + } + + // Check for a direct level requirement for the offer item + var itemLevelRequirement = tieredFlea.UnlocksTpl[offerItemTpl]; + if (itemLevelRequirement != null) { + if (playerLevel < itemLevelRequirement) { + offer.Locked = true; + + return; + } + } + + // Optimisation - Ensure the item has at least one of the limited base types + if (itemHelper.IsOfBaseclasses(offerItemTpl, tieredFleaLimitTypes)) { + // Loop over all flea types to find the matching one + foreach (var tieredItemType in tieredFleaLimitTypes) { + if (itemHelper.IsOfBaseclass(offerItemTpl, tieredItemType)) { + if (playerLevel < tieredFlea.UnlocksType[tieredItemType]) { + offer.Locked = true; + } + break; + } + } + } } - /// - /// 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 + /** + * Get matching offers that require the desired item and filter out offers from non traders if player is below ragfair unlock level + * @param searchRequest Search request from client + * @param pmcDataPlayer profile + * @returns Matching IRagfairOffer objects + */ public List GetOffersThatRequireItem(SearchRequestData searchRequest, PmcData pmcData) { - throw new NotImplementedException(); + // Get all offers that requre the desired item and filter out offers from non traders if player below ragifar unlock + var requiredOffers = ragfairRequiredItemsService.GetRequiredItemsById(searchRequest.NeededSearchId); + var tieredFlea = ragfairConfig.TieredFlea; + var tieredFleaLimitTypes = tieredFlea.UnlocksType.Keys.ToList(); + + return requiredOffers.Where(offer => { + if (!PassesSearchFilterCriteria(searchRequest, offer, pmcData)) { + return false; + } + + if (tieredFlea.Enabled && !OfferIsFromTrader(offer)) { + CheckAndLockOfferFromPlayerTieredFlea(tieredFlea, offer, tieredFleaLimitTypes, pmcData.Info.Level.Value); + } + + return true; + }); } - /// - /// 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 + /** + * Get offers from flea/traders specifically when building weapon preset + * @param searchRequest Search request data + * @param itemsToAdd string array of item tpls to search for + * @param traderAssorts All trader assorts player can access/buy + * @param pmcData Player profile + * @returns IRagfairOffer array + */ public List GetOffersForBuild( SearchRequestData searchRequest, List itemsToAdd, Dictionary traderAssorts, - PmcData pmcData) + PmcData pmcData + ) { - throw new NotImplementedException(); + var offersMap = new Dictionary>(); + var offersToReturn = new List(); + var playerIsFleaBanned = profileHelper.PlayerIsFleaBanned(pmcData); + var tieredFlea = ragfairConfig.TieredFlea; + var tieredFleaLimitTypes = tieredFlea.UnlocksType.Keys.ToList(); + + foreach (var desiredItemTpl in searchRequest.BuildItems.Keys) { + var matchingOffers = ragfairOfferService.GetOffersOfType(desiredItemTpl); + if (matchingOffers == null) { + // No offers found for this item, skip + continue; + } + foreach (var offer in matchingOffers) { + // Dont show pack offers + if (offer.SellInOnePiece ?? false) { + continue; + } + + if (!PassesSearchFilterCriteria(searchRequest, offer, pmcData)) { + continue; + } + + if (!IsDisplayableOffer( + searchRequest, + itemsToAdd, + traderAssorts, + offer, + pmcData, + playerIsFleaBanned) + ) { + continue; + } + + if (OfferIsFromTrader(offer)) { + 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 && !OfferIsFromTrader(offer)) { + CheckAndLockOfferFromPlayerTieredFlea( + tieredFlea, + offer, + tieredFleaLimitTypes, + pmcData.Info.Level.Value + ); + + // Do not add offer to build if user does not have access to it + if (offer.Locked ?? 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 + foreach (var possibleOffers in offersMap.Values) { + // 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 + possibleOffers = possibleOffers.Where((offer) => !(offer.Locked || lockedOffers.includes(offer._id))); + + // Exclude trader offers over their buy restriction limit + possibleOffers = getOffersInsideBuyRestrictionLimits(possibleOffers); + } + + // Sort offers by price and pick the best + var offer = ragfairSortHelper.sortOffers(possibleOffers, RagfairSort.PRICE, 0)[0]; + offersToReturn.push(offer); + } + + return offersToReturn; } - /// - /// Get offers that have not exceeded buy limits - /// - /// offers to process - /// Offers - protected List GetOffersInsideBuyRestrictionLimits(List possibleOffers) - { - throw new NotImplementedException(); + /** + * Get offers that have not exceeded buy limits + * @param possibleOffers offers to process + * @returns 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 != null && + OfferIsFromTrader(offer) && + offer.BuyRestrictionCurrent >= offer.BuyRestrictionMax + ) { + if (offer.BuyRestrictionCurrent >= offer.BuyRestrictionMax) { + return false; + } + } + + // Doesnt have buy limits, retrun offer + return true; + }); } - /// - /// 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 + /** + * Check if offer is from trader standing the player does not have + * @param offer Offer to check + * @param pmcProfile Player profile + * @returns True if item is locked, false if item is purchaseable + */ protected bool TraderOfferLockedBehindLoyaltyLevel(RagfairOffer offer, PmcData pmcProfile) { - throw new NotImplementedException(); + var userTraderSettings = pmcProfile.TradersInfo[offer.User.Id]; + + 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 + /** + * Check if offer item is quest locked for current player by looking at sptQuestLocked property in traders barter_scheme + * @param offer Offer to check is quest locked + * @param traderAssorts all trader assorts for player + * @returns true if quest locked + */ public bool TraderOfferItemQuestLocked(RagfairOffer offer, Dictionary traderAssorts) { - throw new NotImplementedException(); + return offer.Items?.Any( + i => + traderAssorts[offer.User.Id] + .BarterScheme[i.Id] + ?.Any((bs1) => bs1?.Any((bs2) => bs2.SptQuestLocked ?? false) ?? false) ?? + false + ) ?? false; } - /// - /// Has trader offer ran out of stock to sell to player - /// - /// Offer to check stock of - /// true if out of stock + /** + * Has trader offer ran out of stock to sell to player + * @param offer Offer to check stock of + * @returns true if out of stock + */ protected bool TraderOutOfStock(RagfairOffer offer) { - throw new NotImplementedException(); + if (offer?.Items?.Count == 0) { + return true; + } + + return offer.Items[0]?.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 + /** + * Check if trader offers' BuyRestrictionMax value has been reached + * @param offer Offer to check restriction properties of + * @returns true if restriction reached, false if no restrictions/not reached + */ protected bool TraderBuyRestrictionReached(RagfairOffer offer) { - throw new NotImplementedException(); + 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); + + // No trader assort data + if (assortData == null) { + 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 == null) { + return false; + } + + // No restriction values + // Can't use !assortData.upd.BuyRestrictionX as value could be 0 + var assortUpd = assortData.Upd; + if (assortUpd.BuyRestrictionMax == null || assortUpd.BuyRestrictionCurrent == null) { + return false; + } + + // Current equals max, limit reached + if (assortUpd.BuyRestrictionCurrent >= assortUpd.BuyRestrictionMax) { + return true; + } + + return false; } + /** + * Get an array of flea offers that are inaccessible to player due to their inadequate loyalty level + * @param offers Offers to check + * @param pmcProfile Players profile with trader loyalty levels + * @returns Array of offer ids player cannot see + */ protected List GetLoyaltyLockedOffers(List offers, PmcData pmcProfile) { - throw new NotImplementedException(); + var loyaltyLockedOffers = new List(); + foreach (var offer in offers.Where((offer) => OfferIsFromTrader(offer))) { + var traderDetails = pmcProfile.TradersInfo[offer.User.Id]; + if (traderDetails.LoyaltyLevel < offer.LoyaltyLevel) { + loyaltyLockedOffers.Add(offer.Id); + } + } + + return loyaltyLockedOffers; } /** @@ -135,9 +407,38 @@ public class RagfairOfferHelper * @param sessionID Session id to process offers for * @returns true = complete */ - public void ProcessOffersOnProfile(string sessionID) + public bool ProcessOffersOnProfile(string sessionID) { - Console.WriteLine($"actually implement me plz: owo: ProcessOffersOnProfile"); + var timestamp = timeUtil.GetTimeStamp(); + var profileOffers = GetProfileOffers(sessionID); + + // No offers, don't do anything + if (!profileOffers?.length) { + return true; + } + + foreach (var offer in profileOffers.values()) { + if (offer.sellResult?.length > 0 && timestamp >= offer.sellResult[0].sellTime) { + // Checks first item, first is spliced out of array after being processed + // Item sold + var totalItemsCount = 1; + var boughtAmount = 1; + + if (!offer.sellInOnePiece) { + // offer.items.reduce((sum, item) => sum + item.upd?.StackObjectsCount ?? 0, 0); + totalItemsCount = getTotalStackCountSize([offer.items]); + boughtAmount = offer.sellResult[0].amount; + } + + var ratingToAdd = (offer.summaryCost / totalItemsCount) * boughtAmount; + increaseProfileRagfairRating(saveServer.getProfile(sessionID), ratingToAdd); + + completeOffer(sessionID, offer, boughtAmount); + offer.sellResult.splice(0, 1); // Remove the sell result object now its been processed + } + } + + return true; } /** @@ -147,7 +448,14 @@ public class RagfairOfferHelper */ public int GetTotalStackCountSize(List> itemsInInventoryToList) { - throw new NotImplementedException(); + var total = 0D; + foreach (var itemAndChildren in itemsInInventoryToList) { + // Only count the root items stack count in total + var rootItem = itemAndChildren[0]; + total += rootItem.Upd?.StackObjectsCount ?? 1; + } + + return (int) total; } /** @@ -155,19 +463,35 @@ public class RagfairOfferHelper * @param sessionId Profile to update * @param amountToIncrementBy Raw amount to add to players ragfair rating (excluding the reputation gain multiplier) */ - public void IncreaseProfileRagfairRating(SptProfile profile, int amountToIncrementBy) + public void IncreaseProfileRagfairRating(SptProfile profile, double? amountToIncrementBy) { - throw new NotImplementedException(); + var ragfairGlobalsConfig = databaseService.GetGlobals().Configuration.RagFair; + + profile.CharacterData.PmcData.RagfairInfo.IsRatingGrowing = true; + if (amountToIncrementBy == 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 * @param sessionID Session id - * @returns List of ragfair offers + * @returns Array of ragfair offers */ protected List GetProfileOffers(string sessionID) { - throw new NotImplementedException(); + var profile = profileHelper.GetPmcProfile(sessionID); + + if (profile.RagfairInfo == null || profile.RagfairInfo.Offers == null) { + return []; + } + + return profile.RagfairInfo.Offers; } /** @@ -177,19 +501,89 @@ public class RagfairOfferHelper */ protected void DeleteOfferById(string sessionID, string offerId) { - throw new NotImplementedException(); + var profileRagfairInfo = saveServer.GetProfile(sessionID).CharacterData.PmcData.RagfairInfo; + var index = profileRagfairInfo.Offers.FindIndex((o) => o.Id == offerId); + profileRagfairInfo.Offers.Splice(index, 1); + + // Also delete from ragfair + ragfairOfferService.RemoveOfferById(offerId); } /** * Complete the selling of players' offer - * @param sessionID Session id + * @param offerOwnerSessionId Session id * @param offer Sold offer details * @param boughtAmount Amount item was purchased for - * @returns ItemEventRouterResponse + * @returns IItemEventRouterResponse */ - public ItemEventRouterResponse CompleteOffer(string sessionID, RagfairOffer offer, int boughtAmount) + public ItemEventRouterResponse CompleteOffer( + string offerOwnerSessionId, + RagfairOffer offer, + int boughtAmount + ) { - throw new NotImplementedException(); + var itemTpl = offer.Items[0].Template; + var paymentItemsToSendToPlayer = new List(); + var offerStackCount = (int) offer.Items[0].Upd.StackObjectsCount; + var sellerProfile = profileHelper.GetPmcProfile(offerOwnerSessionId); + + // Pack or ALL items of a multi-offer were bought - remove entire ofer + if (offer.SellInOnePiece ?? false || boughtAmount == offerStackCount) { + DeleteOfferById(offerOwnerSessionId, offer.Id); + } else { + var offerRootItem = offer.Items[0]; + + // Reduce offer root items stack count + offerRootItem.Upd.StackObjectsCount -= boughtAmount; + } + + // Assemble payment to send to seller now offer was purchased + foreach (var requirement in offer.Requirements) { + // Create an item template item + var requestedItem = new Item { + Id = hashUtil.Generate(), + Template = requirement.Template, + 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 ?? false) { + var presetItems = ragfairServerHelper.GetPresetItemsByTpl(item); + if (presetItems.Count != 0) { + outItems.Add(presetItems[0]); + } + } + + paymentItemsToSendToPlayer = [..paymentItemsToSendToPlayer, ..outItems]; + } + } + + var ragfairDetails = { + offerId: offer._id, + count: offer.sellInOnePiece ? offerStackCount : boughtAmount, // pack-offers NEED to to be the full item count otherwise it only removes 1 from the pack, leaving phantom offer on client ui + handbookId: itemTpl, + }; + + mailSendService.SendDirectNpcMessageToPlayer( + offerOwnerSessionId, + traderHelper.GetTraderById(Traders.RAGMAN), + MessageType.FLEAMARKET_MESSAGE, + GetLocalisedOfferSoldMessage(itemTpl, boughtAmount), + paymentItemsToSendToPlayer, + timeUtil.GetHoursAsSeconds(questHelper.GetMailItemRedeemTimeHoursForProfile(sellerProfile)), + null, + ragfairDetails, + ); + + // Adjust sellers sell sum values + sellerProfile.RagfairInfo.sellSum ||= 0; + sellerProfile.RagfairInfo.sellSum += offer.summaryCost; + + return eventOutputHolder.getOutput(offerOwnerSessionId); } /** @@ -198,9 +592,31 @@ public class RagfairOfferHelper * @param boughtAmount How many were purchased * @returns Localised message text */ - protected string GetLocalisedOfferSoldMessage(string itemTpl, int boughtAmount) - { - throw new NotImplementedException(); + protected getLocalisedOfferSoldMessage(itemTpl: string, boughtAmount: number): string { + // Generate a message to inform that item was sold + var globalLocales = localeService.getLocaleDb(); + var soldMessageLocaleGuid = globalLocales[RagfairOfferHelper.goodSoldTemplate]; + if (!soldMessageLocaleGuid) { + logger.error( + localisationService.getText( + "ragfair-unable_to_find_locale_by_key", + RagfairOfferHelper.goodSoldTemplate, + ), + ); + } + + // Used to replace tokens in sold message sent to player + var tplVars: ISystemData = { + soldItem: globalLocales["${itemTpl} Name"] || itemTpl, + buyerNickname: botHelper.getPmcNicknameOfMaxLength(botConfig.botNameLengthLimit), + itemCount: boughtAmount, + }; + + var offerSoldMessageText = soldMessageLocaleGuid.replace(/{\w+}/g, (matched) => { + return tplVars[matched.replace(/{|}/g, "")]; + }); + + return offerSoldMessageText.replace(/"/g, ""); } /** @@ -210,9 +626,94 @@ public class RagfairOfferHelper * @param pmcData Player profile * @returns True if offer passes criteria */ - protected bool PassesSearchFilterCriteria(SearchRequestData searchRequest, RagfairOffer offer, PmcData pmcData) - { - throw new NotImplementedException(); + protected passesSearchFilterCriteria( + searchRequest: ISearchRequestData, + offer: IRagfairOffer, + pmcData: IPmcData, + ): boolean { + var isDefaultUserOffer = offer.user.memberType === MemberCategory.DEFAULT; + var offerRootItem = offer.items[0]; + var moneyTypeTpl = offer.requirements[0]._tpl; + var isTraderOffer = offerIsFromTrader(offer); + + if (pmcData.Info.Level < databaseService.getGlobals().config.RagFair.minUserLevel && isDefaultUserOffer) { + // Skip item if player is < global unlock level (default is 15) and item is from a dynamically generated source + return false; + } + + 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 && + offer.endTime - timeUtil.getTimestamp() > TimeUtil.ONE_HOUR_AS_SECONDS + ) { + // offer expires within an hour + return false; + } + + if (searchRequest.quantityFrom > 0 && searchRequest.quantityFrom >= offerRootItem.upd.StackObjectsCount) { + // too little items to offer + return false; + } + + if (searchRequest.quantityTo > 0 && searchRequest.quantityTo <= offerRootItem.upd.StackObjectsCount) { + // too many items to offer + return false; + } + + if (searchRequest.onlyFunctional && !isItemFunctional(offerRootItem, offer)) { + // don't include non-functional items + return false; + } + + if (offer.items.length === 1) { + // Single item + if ( + isConditionItem(offerRootItem) && + !itemQualityInRange(offerRootItem, searchRequest.conditionFrom, searchRequest.conditionTo) + ) { + 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(moneyTypeTpl)) { + var currencies = ["all", "RUB", "USD", "EUR"]; + + if (ragfairHelper.getCurrencyTag(moneyTypeTpl) !== currencies[searchRequest.currency]) { + // don't include item paid in wrong currency + 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; } /** @@ -221,67 +722,161 @@ public class RagfairOfferHelper * @param offer Flea offer to check * @returns True if the given item is functional */ - public bool IsItemFunctional(Item offerRootItem, RagfairOffer offer) - { - throw new NotImplementedException(); - } - - /// - /// Should a ragfair offer be visible to the player - /// - /// Search request - /// ? - /// Trader assort items - used for filtering out locked trader items - /// The flea offer - /// Player profile - /// Optional parameter - /// True = should be shown to player - public bool DisplayableOffer( - SearchRequestData searchRequest, - List itemsToAdd, - Dictionary traderAssorts, - RagfairOffer offer, - PmcData pmcProfile, - bool? playerIsFleaBanned = null - ) - { - throw new NotImplementedException(); + public isItemFunctional(offerRootItem: IItem, offer: IRagfairOffer): boolean { + // Non-preset weapons/armor are always functional + if (!presetHelper.hasPreset(offerRootItem._tpl)) { + return true; + } + + // For armor items that can hold mods, make sure the item count is atleast the amount of required plates + if (itemHelper.armorItemCanHoldMods(offerRootItem._tpl)) { + var offerRootTemplate = itemHelper.getItem(offerRootItem._tpl)[1]; + var requiredPlateCount = offerRootTemplate._props.Slots?.filter((item) => item._required)?.length; + + return offer.items.length > requiredPlateCount; + } + + // For other presets, make sure the offer has more than 1 item + return offer.items.length > 1; } - public bool DisplayableOfferThatNeedsItem(SearchRequestData searchRequest, RagfairOffer offer) - { - throw new NotImplementedException(); + /** + * Should a ragfair offer be visible to the player + * @param searchRequest Search request + * @param itemsToAdd ? + * @param traderAssorts Trader assort items - used for filtering out locked trader items + * @param offer The flea offer + * @param pmcProfile Player profile + * @returns True = should be shown to player + */ + public isDisplayableOffer( + searchRequest: ISearchRequestData, + itemsToAdd: string[], + traderAssorts: Record, + offer: IRagfairOffer, + pmcProfile: IPmcData, + playerIsFleaBanned?: boolean, + ): boolean { + var offerRootItem = offer.items[0]; + /** Currency offer is sold for */ + var moneyTypeTpl = offer.requirements[0]._tpl; + var isTraderOffer = offer.user.id in databaseService.getTraders(); + + if (!isTraderOffer && playerIsFleaBanned) { + return false; + } + + // Offer root items tpl not in searched for array + if (!itemsToAdd?.includes(offerRootItem._tpl)) { + // skip items we shouldn't include + return false; + } + + // Performing a required search and offer doesn't have requirement for item + if ( + searchRequest.neededSearchId && + !offer.requirements.some((requirement) => requirement._tpl === searchRequest.neededSearchId) + ) { + return false; + } + + // Weapon/equipment search + offer is preset + if ( + Object.keys(searchRequest.buildItems).length === 0 && // Prevent equipment loadout searches filtering out presets + searchRequest.buildCount && + presetHelper.hasPreset(offerRootItem._tpl) + ) { + 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 && !paymentHelper.isMoneyTpl(moneyTypeTpl)) { + // Don't include barter offers + return false; + } + + if (JSType.Number.isNaN(offer.requirementsCost)) { + // 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 (!(offer.user.id in traderAssorts)) { + // trader not visible on flea market + return false; + } + + if ( + !traderAssorts[offer.user.id].items.some((item) => { + return item._id === offer.root; + }) + ) { + // skip (quest) locked items + return false; + } + } + + return true; } - /// - /// Does the passed in item have a condition property - /// - /// Item to check - /// True if has condition - protected bool ConditionItem(Item item) - { - throw new NotImplementedException(); + public isDisplayableOfferThatNeedsItem(searchRequest: ISearchRequestData, offer: IRagfairOffer): boolean { + if (offer.requirements.some((requirement) => requirement._tpl === searchRequest.neededSearchId)) { + return true; + } + + return false; } - /// - /// 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) - { - throw new NotImplementedException(); + /** + * Does the passed in item have a condition property + * @param item Item to check + * @returns True if has condition + */ + protected isConditionItem(item: IItem): boolean { + // thanks typescript, undefined assertion is not returnable since it + // tries to return a multitype object + return !!( + item.upd.MedKit || + item.upd.Repairable || + item.upd.Resource || + item.upd.FoodDrink || + item.upd.Key || + item.upd.RepairKit + ); } - /// - /// Does this offer come from a trader - /// - /// Offer to check - /// True = from trader - public bool OfferIsFromTrader(RagfairOffer offer) - { - return offer.User.MemberType == MemberCategory.TRADER; + /** + * Is items quality value within desired range + * @param item Item to check quality of + * @param min Desired minimum quality + * @param max Desired maximum quality + * @returns True if in range + */ + protected itemQualityInRange(item: IItem, min: number, max: number): boolean { + 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; + } + + /** + * Does this offer come from a trader + * @param offer Offer to check + * @returns True = from trader + */ + public offerIsFromTrader(offer: IRagfairOffer) { + return offer.user.memberType === MemberCategory.TRADER; } } diff --git a/Libraries/Core/Models/Spt/Config/RagfairConfig.cs b/Libraries/Core/Models/Spt/Config/RagfairConfig.cs index 84055f74..5c3b0b53 100644 --- a/Libraries/Core/Models/Spt/Config/RagfairConfig.cs +++ b/Libraries/Core/Models/Spt/Config/RagfairConfig.cs @@ -387,7 +387,7 @@ public record TieredFlea /// key: tpl, value: playerlevel /// [JsonPropertyName("unlocksTpl")] - public Dictionary UnlocksTpl { get; set; } + public Dictionary UnlocksTpl { get; set; } /// /// key: item type id, value: playerlevel @@ -396,7 +396,7 @@ public record TieredFlea public Dictionary UnlocksType { get; set; } [JsonPropertyName("ammoTplUnlocks")] - public Dictionary AmmoTplUnlocks { get; set; } + public Dictionary AmmoTplUnlocks { get; set; } [JsonPropertyName("ammoTiersEnabled")] public bool AmmoTiersEnabled { get; set; }