diff --git a/Libraries/Core/Helpers/RagfairOfferHelper.cs b/Libraries/Core/Helpers/RagfairOfferHelper.cs index 9e9b6cb6..8221d80b 100644 --- a/Libraries/Core/Helpers/RagfairOfferHelper.cs +++ b/Libraries/Core/Helpers/RagfairOfferHelper.cs @@ -1,4 +1,3 @@ -using SptCommon.Annotations; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; using Core.Models.Eft.ItemEvent; @@ -7,10 +6,11 @@ using Core.Models.Eft.Ragfair; using Core.Models.Enums; using Core.Models.Spt.Config; using Core.Models.Utils; -using Core.Utils; -using SptCommon.Extensions; -using Core.Models.Spt.Services; +using Core.Servers; using Core.Services; +using Core.Utils; +using SptCommon.Annotations; +using SptCommon.Extensions; namespace Core.Helpers; @@ -18,11 +18,20 @@ namespace Core.Helpers; public class RagfairOfferHelper( ISptLogger _logger, TimeUtil _timeUtil, + RagfairSortHelper _ragfairSortHelper, + PresetHelper _presetHelper, + PaymentHelper _paymentHelper, + TraderHelper _traderHelper, + ItemHelper _itemHelper, DatabaseService _databaseService, - ProfileHelper _profileHelper) + RagfairOfferService _ragfairOfferService, + ProfileHelper _profileHelper, + ConfigServer _configServer) { + protected RagfairConfig _ragfairConfig = _configServer.GetConfig(); + /// - /// Passthrough to ragfairOfferService.getOffers(), get flea offers a player should see + /// Passthrough to ragfairOfferService.getOffers(), get flea offers a player should see /// /// Data from client /// ragfairHelper.filterCategories() @@ -39,7 +48,7 @@ public class RagfairOfferHelper( } /// - /// Disable offer if item is flagged by tiered flea config + /// Disable offer if item is flagged by tiered flea config /// /// Tiered flea settings from ragfair config /// Ragfair offer to check @@ -48,14 +57,15 @@ public class RagfairOfferHelper( protected void CheckAndLockOfferFromPlayerTieredFlea( TieredFlea tieredFlea, RagfairOffer offer, - string[] tieredFleaLimitTypes, + List tieredFleaLimitTypes, int playerLevel) { throw new NotImplementedException(); } /// - /// Get matching offers that require the desired item and filter out offers from non traders if player is below ragfair unlock level + /// 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 @@ -66,7 +76,7 @@ public class RagfairOfferHelper( } /// - /// Get offers from flea/traders specifically when building weapon preset + /// Get offers from flea/traders specifically when building weapon preset /// /// Search request data /// string array of item tpls to search for @@ -79,64 +89,381 @@ public class RagfairOfferHelper( Dictionary traderAssorts, 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; + + 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 (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.Keys.ToList(), + 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(); + + // 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(possibleOffers); + } + + // 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 + * @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 + */ + private bool IsDisplayableOffer(SearchRequestData searchRequest, List itemsToAdd, + Dictionary traderAssorts, RagfairOffer offer, PmcData pmcProfile, + bool playerIsFleaBanned = false) + { + var offerRootItem = offer.Items[0]; + /** Currency offer is sold for */ + var moneyTypeTpl = offer.Requirements[0].Template; + var isTraderOffer = _databaseService.GetTraders().ContainsKey(offer.User.Id); + + 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 ( + searchRequest.NeededSearchId is not null && + !offer.Requirements.Any(requirement => requirement.Template == searchRequest.NeededSearchId) + ) + { + return false; + } + + // Weapon/equipment search + offer is preset + if ( + searchRequest.BuildItems.Count == 0 && // Prevent equipment loadout searches filtering out presets + searchRequest.BuildCount is not null && + _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.ContainsKey(offer.User.Id)) + { + // trader not visible on flea market + return false; + } + + if ( + !traderAssorts[offer.User.Id].Items.Any(item => { return item.Id == offer.Root; }) + ) + { + // skip (quest) locked items + return false; + } + } + + return true; } /// - /// Get offers that have not exceeded buy limits + /// Get offers that have not exceeded buy limits /// /// offers to process /// Offers protected List GetOffersInsideBuyRestrictionLimits(List possibleOffers) { - throw new NotImplementedException(); + // 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 && + OfferIsFromTrader(offer) && + offer.BuyRestrictionCurrent >= offer.BuyRestrictionMax + ) + { + if (offer.BuyRestrictionCurrent >= offer.BuyRestrictionMax) + { + return false; + } + } + + // Doesnt have buy limits, retrun offer + return true; + } + ) + .ToList(); } /// - /// Check if offer is from trader standing the player does not have + /// 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) { - throw new NotImplementedException(); + 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 + /// 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) { - throw new NotImplementedException(); + var itemIds = offer.Items.Select(x => x.Id).ToList(); + //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 item 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 + /// 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) { - 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 + /// 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) { - 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); + 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 List GetLoyaltyLockedOffers(List offers, PmcData pmcProfile) { - throw new NotImplementedException(); + var loyaltyLockedOffers = new List(); + foreach (var offer in offers.Where(offer => OfferIsFromTrader(offer))) + if (pmcProfile.TradersInfo.TryGetValue(offer.User.Id, out var traderDetails) && + traderDetails.LoyaltyLevel < offer.LoyaltyLevel) + { + loyaltyLockedOffers.Add(offer.Id); + } + + return loyaltyLockedOffers; } /** @@ -155,7 +482,7 @@ public class RagfairOfferHelper( return true; } - foreach (var offer in profileOffers) { + foreach (var offer in profileOffers) if (offer.SellResults?.Count > 0 && timestamp >= offer.SellResults[0].SellTime) { // Checks first item, first is spliced out of array after being processed @@ -170,13 +497,12 @@ public class RagfairOfferHelper( boughtAmount = offer.SellResults[0].Amount.Value; } - var ratingToAdd = (offer.SummaryCost / totalItemsCount) * boughtAmount; + var ratingToAdd = offer.SummaryCost / totalItemsCount * boughtAmount; IncreaseProfileRagfairRating(_profileHelper.GetFullProfile(sessionId), ratingToAdd.Value); CompleteOffer(sessionId, offer, boughtAmount); offer.SellResults.Splice(0, 1); // Remove the sell result object now its been processed } - } return true; } @@ -189,10 +515,9 @@ public class RagfairOfferHelper( public double GetTotalStackCountSize(List> itemsInInventoryToList) { var total = 0d; - foreach (var itemAndChildren in itemsInInventoryToList) { + foreach (var itemAndChildren in itemsInInventoryToList) // Only count the root items stack count in total total += itemAndChildren[0]?.Upd?.StackObjectsCount.GetValueOrDefault(1) ?? 1; - } return total; } @@ -209,12 +534,14 @@ public class RagfairOfferHelper( profile.CharacterData.PmcData.RagfairInfo.IsRatingGrowing = true; if (amountToIncrementBy is null) { - _logger.Warning($"Unable to increment ragfair rating, value was not a number: { amountToIncrementBy}"); + _logger.Warning($"Unable to increment ragfair rating, value was not a number: {amountToIncrementBy}"); return; } + profile.CharacterData.PmcData.RagfairInfo.Rating += - (ragfairGlobalsConfig.RatingIncreaseCount / ragfairGlobalsConfig.RatingSumForIncrease) * + ragfairGlobalsConfig.RatingIncreaseCount / + ragfairGlobalsConfig.RatingSumForIncrease * amountToIncrementBy; } @@ -288,11 +615,29 @@ public class RagfairOfferHelper( */ public bool IsItemFunctional(Item offerRootItem, RagfairOffer offer) { - throw new NotImplementedException(); + // 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; } - + /// - /// Should a ragfair offer be visible to the player + /// Should a ragfair offer be visible to the player /// /// Search request /// ? @@ -313,23 +658,35 @@ public class RagfairOfferHelper( throw new NotImplementedException(); } - public bool DisplayableOfferThatNeedsItem(SearchRequestData searchRequest, RagfairOffer offer) + public bool IsDisplayableOfferThatNeedsItem(SearchRequestData searchRequest, RagfairOffer offer) { - throw new NotImplementedException(); + return offer.Requirements.Any(requirement => requirement.Template == searchRequest.NeededSearchId); } /// - /// Does the passed in item have a condition property + /// Does the passed in item have a condition property /// /// Item to check /// True if has condition - protected bool ConditionItem(Item item) + protected bool IsConditionItem(Item item) { - throw new NotImplementedException(); + // 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 + /// Is items quality value within desired range /// /// Item to check quality of /// Desired minimum quality @@ -337,11 +694,24 @@ public class RagfairOfferHelper( /// True if in range protected bool ItemQualityInRange(Item item, int min, int max) { - throw new NotImplementedException(); + 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 + /// Does this offer come from a trader /// /// Offer to check /// True = from trader diff --git a/Libraries/Core/Services/CustomLocationWaveService.cs b/Libraries/Core/Services/CustomLocationWaveService.cs index b9149d4c..57204438 100644 --- a/Libraries/Core/Services/CustomLocationWaveService.cs +++ b/Libraries/Core/Services/CustomLocationWaveService.cs @@ -80,7 +80,7 @@ public class CustomLocationWaveService( locationBase.BossLocationSpawn.Add(bossWave); _logger.Debug( - $"Added custom boss wave to {mapKvP} of type {bossWave.BossName}, time: {bossWave.Time}, chance: {bossWave.BossChance}, zone: {bossWave.BossZone}" + $"Added custom boss wave to {mapKvP.Key} of type {bossWave.BossName}, time: {bossWave.Time}, chance: {bossWave.BossChance}, zone: {(string.IsNullOrEmpty(bossWave.BossZone) ? "Global" : bossWave.BossZone)}" ); } }