From 3eaf6887a120da4832eb28d51c1d5b90eae52608 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 24 Jan 2025 12:16:17 +0000 Subject: [PATCH 1/2] FenseService --- Libraries/Core/Helpers/TradeHelper.cs | 2 +- .../Eft/Inventory/AddItemsDirectRequest.cs | 2 +- .../Core/Models/Spt/Config/TraderConfig.cs | 6 +- Libraries/Core/Services/FenceService.cs | 1568 +++++++++++++---- Libraries/Core/Utils/RandomUtil.cs | 11 +- 5 files changed, 1252 insertions(+), 337 deletions(-) diff --git a/Libraries/Core/Helpers/TradeHelper.cs b/Libraries/Core/Helpers/TradeHelper.cs index 6c6f40a9..25f86dd6 100644 --- a/Libraries/Core/Helpers/TradeHelper.cs +++ b/Libraries/Core/Helpers/TradeHelper.cs @@ -56,7 +56,7 @@ public class TradeHelper( ) { List offerItems = []; - Action? buyCallback; + Action? buyCallback; if (buyRequestData.TransactionId.ToLower() == "ragfair") { diff --git a/Libraries/Core/Models/Eft/Inventory/AddItemsDirectRequest.cs b/Libraries/Core/Models/Eft/Inventory/AddItemsDirectRequest.cs index 91e8d7a3..f67087f5 100644 --- a/Libraries/Core/Models/Eft/Inventory/AddItemsDirectRequest.cs +++ b/Libraries/Core/Models/Eft/Inventory/AddItemsDirectRequest.cs @@ -16,7 +16,7 @@ public record AddItemsDirectRequest /// Runs after EACH item with children is added [JsonPropertyName("callback")] - public Action? Callback { get; set; } + public Action? Callback { get; set; } /// Should sorting table be used when no space found in stash [JsonPropertyName("useSortingTable")] diff --git a/Libraries/Core/Models/Spt/Config/TraderConfig.cs b/Libraries/Core/Models/Spt/Config/TraderConfig.cs index ec1a7900..c37f3c4d 100644 --- a/Libraries/Core/Models/Spt/Config/TraderConfig.cs +++ b/Libraries/Core/Models/Spt/Config/TraderConfig.cs @@ -83,7 +83,7 @@ public record FenceConfig /** Key: item tpl */ [JsonPropertyName("itemStackSizeOverrideMinMax")] - public Dictionary ItemStackSizeOverrideMinMax { get; set; } + public Dictionary ItemStackSizeOverrideMinMax { get; set; } [JsonPropertyName("itemTypeLimits")] public Dictionary ItemTypeLimits { get; set; } @@ -97,11 +97,11 @@ public record FenceConfig /** Max rouble price before item is not listed on flea */ [JsonPropertyName("itemCategoryRoublePriceLimit")] - public Dictionary ItemCategoryRoublePriceLimit { get; set; } + public Dictionary ItemCategoryRoublePriceLimit { get; set; } /** Each slotid with % to be removed prior to listing on fence */ [JsonPropertyName("presetSlotsToRemoveChancePercent")] - public Dictionary PresetSlotsToRemoveChancePercent { get; set; } + public Dictionary PresetSlotsToRemoveChancePercent { get; set; } /** Block seasonal items from appearing when season is inactive */ [JsonPropertyName("blacklistSeasonalItems")] diff --git a/Libraries/Core/Services/FenceService.cs b/Libraries/Core/Services/FenceService.cs index c90b868e..70be1b0f 100644 --- a/Libraries/Core/Services/FenceService.cs +++ b/Libraries/Core/Services/FenceService.cs @@ -1,540 +1,1448 @@ +using System.Runtime.InteropServices.JavaScript; +using Core.Helpers; +using Core.Models.Common; using SptCommon.Annotations; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; +using Core.Models.Enums; using Core.Models.Spt.Config; using Core.Models.Spt.Fence; +using Core.Servers; +using Core.Utils; +using Core.Utils.Cloners; +using SptCommon.Extensions; namespace Core.Services; [Injectable(InjectionType.Singleton)] public class FenceService( - DatabaseService _databaseService -) + ILogger logger, + TimeUtil timeUtil, + RandomUtil randomUtil, + DatabaseService databaseService, + HandbookHelper handbookHelper, + ItemHelper itemHelper, + PresetHelper presetHelper, + LocalisationService localisationService, + ConfigServer configServer, + ICloner cloner + ) { - /// - /// Replace main fence assort with new assort - /// - /// New assorts to replace old with + protected TraderConfig traderConfig = configServer.GetConfig(); + + /** Time when some items in assort will be replaced */ + protected long nextPartialRefreshTimestamp; + + /** Main assorts you see at all rep levels */ + protected TraderAssort? fenceAssort = null; + + /** Assorts shown on a separate tab when you max out fence rep */ + protected TraderAssort? fenceDiscountAssort = null; + + /** Desired baseline counts - Hydrated on initial assort generation as part of generateFenceAssorts() */ + protected FenceAssortGenerationValues desiredAssortCounts; + + protected HashSet fenceItemUpdCompareProperties = + [ + "Buff", + "Repairable", + "RecodableComponent", + "Key", + "Resource", + "MedKit", + "FoodDrink", + "Dogtag", + "RepairKit", + ]; + + + /** + * Replace main fence assort with new assort + * @param assort New assorts to replace old with + */ public void SetFenceAssort(TraderAssort assort) { - throw new NotImplementedException(); + fenceAssort = assort; } - /// - /// Replace discount fence assort with new assort - /// - /// New assorts to replace old with + /** + * Replace discount fence assort with new assort + * @param assort New assorts to replace old with + */ public void SetDiscountFenceAssort(TraderAssort assort) { - throw new NotImplementedException(); + fenceDiscountAssort = assort; } - /// - /// Get main fence assort - /// - /// TraderAssort - public TraderAssort GetMainFenceAssort() + /** + * Get main fence assort + * @return ITraderAssort + */ + public TraderAssort? GetMainFenceAssort() { - throw new NotImplementedException(); + return fenceAssort; } - /// - /// Get discount fence assort - /// - /// TraderAssort - public TraderAssort GetDiscountFenceAssort() + /** + * Get discount fence assort + * @return ITraderAssort + */ + public TraderAssort? GetDiscountFenceAssort() { - throw new NotImplementedException(); + return fenceDiscountAssort; } - /// - /// Replace high rep level fence assort with new assort - /// - /// New assorts to replace old with + /** + * Replace high rep level fence assort with new assort + * @param discountAssort New assorts to replace old with + */ public void SetFenceDiscountAssort(TraderAssort discountAssort) { - throw new NotImplementedException(); + fenceDiscountAssort = discountAssort; } - /// - /// Get assorts player can purchase - /// Adjust prices based on fence level of player - /// - /// Player profile - /// TraderAssort + /** + * Get assorts player can purchase + * Adjust prices based on fence level of player + * @param pmcProfile Player profile + * @returns ITraderAssort + */ public TraderAssort GetFenceAssorts(PmcData pmcProfile) { - throw new NotImplementedException(); + if (traderConfig.Fence.RegenerateAssortsOnRefresh) { + // Using base assorts made earlier, do some alterations and store in fenceAssort + GenerateFenceAssorts(); + } + + // Clone assorts so we can adjust prices before sending to client + var assort = cloner.Clone(fenceAssort); + AdjustAssortItemPricesByConfigMultiplier(assort, 1, traderConfig.Fence.PresetPriceMult); + + // merge normal fence assorts + discount assorts if player standing is large enough + if (pmcProfile.TradersInfo[Traders.FENCE].Standing >= 6) { + var discountAssort = cloner.Clone(fenceDiscountAssort); + AdjustAssortItemPricesByConfigMultiplier( + discountAssort, + traderConfig.Fence.DiscountOptions.ItemPriceMult, + traderConfig.Fence.DiscountOptions.PresetPriceMult + ); + var mergedAssorts = MergeAssorts(assort, discountAssort); + + return mergedAssorts; + } + + return assort; } - /// - /// Adds to fence assort a single item (with its children) - /// - /// the items to add with all its childrens - /// the most parent item of the array + /** + * Adds to fence assort a single item (with its children) + * @param items the items to add with all its childrens + * @param mainItem the most parent item of the array + */ public void AddItemsToFenceAssort(List items, Item mainItem) { - throw new NotImplementedException(); + // HUGE THANKS TO LACYWAY AND LEAVES FOR PROVIDING THIS SOLUTION FOR SPT TO IMPLEMENT!! + // Copy the item and its children + var clonedItems = cloner.Clone(itemHelper.FindAndReturnChildrenAsItems(items, mainItem.Id)); + var root = clonedItems[0]; + + var cost = GetItemPrice(root.Template, clonedItems); + + // Fix IDs + clonedItems = itemHelper.ReparentItemAndChildren(root, clonedItems); + root.ParentId = "hideout"; + if (root.Upd?.SpawnedInSession != null) { + root.Upd.SpawnedInSession = false; + } + + // Clean up the items + // We may need to find an alternative to nodes: delete root.location; + root.Location = null; + + var createAssort = new CreateFenceAssortsResult() + { SptItems = [], BarterScheme = new(), LoyalLevelItems = new() }; + createAssort.BarterScheme[root.Id] = [[new BarterScheme() { Count = cost, Template = Money.ROUBLES }]]; + createAssort.SptItems.Add(clonedItems); + createAssort.LoyalLevelItems[root.Id] = 1; + + UpdateFenceAssorts(createAssort, fenceAssort); } - /// - /// Calculates the overall price for an item (with all its children) - /// - /// the item tpl to calculate the fence price for - /// the items (with its children) to calculate fence price for - /// the fence price of the item - public double GetItemPrice(string itemTpl, List items) + /** + * Calculates the overall price for an item (with all its children) + * @param itemTpl the item tpl to calculate the fence price for + * @param items the items (with its children) to calculate fence price for + * @returns the fence price of the item + */ + public double? GetItemPrice(string itemTpl, List items) { - throw new NotImplementedException(); + return itemHelper.IsOfBaseclass(itemTpl, BaseClasses.AMMO_BOX) + ? GetAmmoBoxPrice(items) * traderConfig.Fence.ItemPriceMult + : handbookHelper.GetTemplatePrice(itemTpl) * traderConfig.Fence.ItemPriceMult; } - /// - /// Calculate the overall price for an ammo box, where only one item is - /// the ammo box itself and every other items are the bullets in that box - /// - /// the ammo box (and all its children ammo items) - /// the price of the ammo box - protected double GetAmmoBoxPrice(List items) + /** + * Calculate the overall price for an ammo box, where only one item is + * the ammo box itself and every other items are the bullets in that box + * @param items the ammo box (and all its children ammo items) + * @returns the price of the ammo box + */ + protected double? GetAmmoBoxPrice(List items) { - throw new NotImplementedException(); + double? total = 0D; + foreach (var item in items) { + if (itemHelper.IsOfBaseclass(item.Template, BaseClasses.AMMO)) { + total += handbookHelper.GetTemplatePrice(item.Template) * (item.Upd?.StackObjectsCount ?? 1); + } + } + + return total; } - /// - /// Adjust all items contained inside an assort by a multiplier - /// - /// (clone)Assort that contains items with prices to adjust - /// multipler to use on items - /// preset multipler to use on presets + /** + * Adjust all items contained inside an assort by a multiplier + * @param assort (clone)Assort that contains items with prices to adjust + * @param itemMultipler multipler to use on items + * @param presetMultiplier preset multipler to use on presets + */ protected void AdjustAssortItemPricesByConfigMultiplier( TraderAssort assort, double itemMultipler, - double presetMultiplier) + double presetMultiplier + ) { - throw new NotImplementedException(); + foreach (var item in assort.Items) { + // Skip sub-items when adjusting prices + if (item.SlotId != "hideout") { + continue; + } + + AdjustItemPriceByModifier(item, assort, itemMultipler, presetMultiplier); + } } - /// - /// Merge two trader assort files together - /// - /// assort 1# - /// assort #2 - /// merged assort + /** + * Merge two trader assort files together + * @param firstAssort assort 1# + * @param secondAssort assort #2 + * @returns merged assort + */ protected TraderAssort MergeAssorts(TraderAssort firstAssort, TraderAssort secondAssort) { - throw new NotImplementedException(); + foreach (var itemId in secondAssort.BarterScheme.Keys) { + firstAssort.BarterScheme[itemId] = secondAssort.BarterScheme[itemId]; + } + + foreach (var item in secondAssort.Items) { + firstAssort.Items.Add(item); + } + + foreach (var itemId in secondAssort.LoyalLevelItems.Keys) { + firstAssort.LoyalLevelItems[itemId] = secondAssort.LoyalLevelItems[itemId]; + } + + return firstAssort; } - /// - /// Adjust assorts price by a modifier - /// - /// assort item details - /// assort to be modified - /// value to multiply item price by - /// value to multiply preset price by + /** + * Adjust assorts price by a modifier + * @param item assort item details + * @param assort assort to be modified + * @param modifier value to multiply item price by + * @param presetModifier value to multiply preset price by + */ protected void AdjustItemPriceByModifier( Item item, TraderAssort assort, double modifier, - double presetModifier) + double presetModifier + ) { - throw new NotImplementedException(); + // Is preset + if (item.Upd?.SptPresetId != null) { + if (assort.BarterScheme?[item.Id] != null) { + assort.BarterScheme[item.Id][0][0].Count *= presetModifier; + } + } else if (assort.BarterScheme?[item.Id] != null) { + assort.BarterScheme[item.Id][0][0].Count *= modifier; + } else { + logger.LogWarning($"adjustItemPriceByModifier() - no action taken for item: {item.Template}"); + } } - /// - /// Get fence assorts with no price adjustments based on fence rep - /// - /// TraderAssort - public TraderAssort GetRawFenceAssorts() - { - throw new NotImplementedException(); + /** + * Get fence assorts with no price adjustments based on fence rep + * @returns ITraderAssort + */ + public TraderAssort GetRawFenceAssorts() { + return MergeAssorts(cloner.Clone(fenceAssort), cloner.Clone(fenceDiscountAssort)); } - /// - /// Does fence need to perform a partial refresh because its passed the refresh timer defined in trader.json - /// - /// true if it needs a partial refresh + /** + * Does fence need to perform a partial refresh because its passed the refresh timer defined in trader.json + * @returns true if it needs a partial refresh + */ public bool NeedsPartialRefresh() { - throw new NotImplementedException(); + return timeUtil.GetTimeStamp() > nextPartialRefreshTimestamp; } - /// - /// Replace a percentage of fence assorts with freshly generated items - /// + /** + * Replace a percentage of fence assorts with freshly generated items + */ public void PerformPartialRefresh() { - throw new NotImplementedException(); + var itemCountToReplace = GetCountOfItemsToReplace(traderConfig.Fence.AssortSize); + var discountItemCountToReplace = GetCountOfItemsToReplace( + traderConfig.Fence.DiscountOptions.AssortSize + ); + + // Simulate players buying items + DeleteRandomAssorts(itemCountToReplace, fenceAssort); + DeleteRandomAssorts(discountItemCountToReplace, fenceDiscountAssort); + + var normalItemCountsToGenerate = GetItemCountsToGenerate( + fenceAssort.Items, + desiredAssortCounts.Normal + ); + var newItems = CreateAssorts(normalItemCountsToGenerate, 1); + + // Push newly generated assorts into existing data + UpdateFenceAssorts(newItems, fenceAssort); + + var discountItemCountsToGenerate = GetItemCountsToGenerate( + fenceDiscountAssort.Items, + desiredAssortCounts.Discount + ); + var newDiscountItems = CreateAssorts(discountItemCountsToGenerate, 2); + + // Push newly generated discount assorts into existing data + UpdateFenceAssorts(newDiscountItems, fenceDiscountAssort); + + // Add new barter items to fence barter scheme + foreach (var barterItemKey in newItems.BarterScheme.Keys) { + fenceAssort.BarterScheme[barterItemKey] = newItems.BarterScheme[barterItemKey]; + } + + // Add loyalty items to fence assorts loyalty object + foreach (var loyaltyItemKey in newItems.LoyalLevelItems.Keys) { + fenceAssort.LoyalLevelItems[loyaltyItemKey] = newItems.LoyalLevelItems[loyaltyItemKey]; + } + + // Add new barter items to fence assorts discounted barter scheme + foreach (var barterItemKey in newDiscountItems.BarterScheme.Keys) { + fenceDiscountAssort.BarterScheme[barterItemKey] = newDiscountItems.BarterScheme[barterItemKey]; + } + + // Add loyalty items to fence discount assorts loyalty object + foreach (var loyaltyItemKey in newDiscountItems.LoyalLevelItems.Keys) { + fenceDiscountAssort.LoyalLevelItems[loyaltyItemKey] = newDiscountItems.LoyalLevelItems[loyaltyItemKey]; + } + + // Reset the clock + IncrementPartialRefreshTime(); } - /// - /// Handle the process of folding new assorts into existing assorts, when a new assort exists already, increment its StackObjectsCount instead - /// - /// Assorts to fold into existing fence assorts - /// Current fence assorts new assorts will be added to + /** + * Handle the process of folding new assorts into existing assorts, when a new assort exists already, increment its StackObjectsCount instead + * @param newFenceAssorts Assorts to fold into existing fence assorts + * @param existingFenceAssorts Current fence assorts new assorts will be added to + */ protected void UpdateFenceAssorts( CreateFenceAssortsResult newFenceAssorts, TraderAssort existingFenceAssorts ) { - throw new NotImplementedException(); + foreach (var itemWithChildren in newFenceAssorts.SptItems) + { + // Find the root item + var newRootItem = itemWithChildren.FirstOrDefault((item) => item.SlotId == "hideout"); + if (newRootItem == null) { + var firstItem = itemWithChildren.FirstOrDefault((x) => x != null); + logger.LogError($"Unable to process fence assort as root item is missing, {firstItem?.Template}, skipping"); + continue; + } + + // Find a matching root item with same tpl in existing assort + var existingRootItem = existingFenceAssorts.Items.FirstOrDefault((item) => item.Template == newRootItem.Template && item.SlotId == "hideout"); + + // Check if same type of item exists + its on list of item types to always stack + if (existingRootItem != null && ItemInPreventDupeCategoryList(newRootItem.Template)) + { + var existingFullItemTree = itemHelper.FindAndReturnChildrenAsItems( + existingFenceAssorts.Items, + existingRootItem.Id + ); + if (itemHelper.isSameItems(itemWithChildren, existingFullItemTree, fenceItemUpdCompareProperties)) + { + // Guard against a missing stack count + if (existingRootItem.Upd?.StackObjectsCount == null) { + existingRootItem.Upd.StackObjectsCount = 1; + } + + // Merge new items count into existing, dont add new loyalty/barter data as it already exists + existingRootItem.Upd.StackObjectsCount += newRootItem?.Upd?.StackObjectsCount ?? 1; + + continue; + } + } + + // if the Upd doesnt exist just initialize it + if (newRootItem.Upd == null) { + newRootItem.Upd = new(); + } + // New assort to be added to existing assorts + existingFenceAssorts.Items.AddRange(itemWithChildren); + existingFenceAssorts.BarterScheme[newRootItem.Id] = newFenceAssorts.BarterScheme[newRootItem.Id]; + existingFenceAssorts.LoyalLevelItems[newRootItem.Id] = newFenceAssorts.LoyalLevelItems[newRootItem.Id]; + + } } - /// - /// Increment fence next refresh timestamp by current timestamp + partialRefreshTimeSeconds from config - /// + /** + * Increment fence next refresh timestamp by current timestamp + partialRefreshTimeSeconds from config + */ protected void IncrementPartialRefreshTime() { - throw new NotImplementedException(); + nextPartialRefreshTimestamp = timeUtil.GetTimeStamp() + traderConfig.Fence.PartialRefreshTimeSeconds; } - /// - /// Get values that will hydrate the passed in assorts back to the desired counts - /// - /// Current assorts after items have been removed - /// Base counts assorts should be adjusted to - /// GenerationAssortValues object with adjustments needed to reach desired state + /** + * Get values that will hydrate the passed in assorts back to the desired counts + * @param assortItems Current assorts after items have been removed + * @param generationValues Base counts assorts should be adjusted to + * @returns IGenerationAssortValues object with adjustments needed to reach desired state + */ protected GenerationAssortValues GetItemCountsToGenerate( - Item[] assortItems, + List assortItems, GenerationAssortValues generationValues ) { - throw new NotImplementedException(); + var allRootItems = assortItems.Where((item) => item.SlotId == "hideout"); + var rootPresetItems = allRootItems.Where((item) => item?.Upd?.SptPresetId != null); + + // Get count of weapons + var currentWeaponPresetCount = rootPresetItems.Aggregate(0, (count, item) => itemHelper.IsOfBaseclass(item.Template, BaseClasses.WEAPON) ? count + 1 : count); + + // Get count of equipment + var currentEquipmentPresetCount = rootPresetItems.Aggregate(0, (count, item) => itemHelper.ArmorItemCanHoldMods(item.Template) ? count + 1 : count); + + // Normal item count is total count minus weapon + armor count + var nonPresetItemAssortCount = allRootItems.Count() - (currentWeaponPresetCount + currentEquipmentPresetCount); + + // Get counts of items to generate, never var values fall below 0 + var itemCountToGenerate = Math.Max(generationValues.Item.Value - nonPresetItemAssortCount, 0); + var weaponCountToGenerate = Math.Max(generationValues.WeaponPreset.Value - currentWeaponPresetCount, 0); + var equipmentCountToGenerate = Math.Max(generationValues.EquipmentPreset.Value - currentEquipmentPresetCount, 0); + + return new GenerationAssortValues { + Item = itemCountToGenerate, + WeaponPreset = weaponCountToGenerate, + EquipmentPreset = equipmentCountToGenerate + }; } - /// - /// Delete desired number of items from assort (including children) - /// - /// - /// + /** + * Delete desired number of items from assort (including children) + * @param itemCountToReplace + * @param discountItemCountToReplace + */ protected void DeleteRandomAssorts(int itemCountToReplace, TraderAssort assort) { - throw new NotImplementedException(); + if (assort?.Items?.Count > 0) { + var rootItems = assort.Items.Where((item) => item.SlotId == "hideout").ToList(); + for (var index = 0; index < itemCountToReplace; index++) { + RemoveRandomItemFromAssorts(assort, rootItems); + } + } } - /// - /// Choose an item at random and remove it + mods from assorts - /// - /// Trader assort to remove item from - /// Pool of root items to pick from to remove - protected void RemoveRandomItemFromAssorts(TraderAssort assort, Item[] rootItems) + /** + * Choose an item at random and remove it + mods from assorts + * @param assort Trader assort to remove item from + * @param rootItems Pool of root items to pick from to remove + */ + protected void RemoveRandomItemFromAssorts(TraderAssort assort, List rootItems) { - throw new NotImplementedException(); + var rootItemToAdjust = randomUtil.GetArrayValue(rootItems); + + // Items added by mods may not have a Upd object, assume item stack size is 1 + var stackSize = rootItemToAdjust.Upd?.StackObjectsCount ?? 1; + + // Get a random count of the chosen item to remove + var itemCountToRemove = randomUtil.GetInt(1, (int) stackSize); + + var isEntireStackToBeRemoved = Math.Abs(itemCountToRemove - stackSize) < 0.1; + + // Partial stack reduction + if (!isEntireStackToBeRemoved) { + if (rootItemToAdjust.Upd == null) { + logger.LogWarning($"Fence Item: {rootItemToAdjust.Template} lacks a Upd object, adding"); + rootItemToAdjust.Upd = new (); + } + + // Reduce stack to at smallest, 1 + rootItemToAdjust.Upd.StackObjectsCount -= Math.Max(1, itemCountToRemove); + + return; + } + + // Remove item + child mods (if any) + var itemWithChildren = itemHelper.FindAndReturnChildrenAsItems(assort.Items, rootItemToAdjust.Id); + foreach (var itemToDelete in itemWithChildren) { + // Delete item from assort items array + assort.Items.Splice(assort.Items.IndexOf(itemToDelete), 1); + } + + // Need to remove item from all areas of trader assort + // delete assort.barter_scheme[rootItemToAdjust._id]; + // delete assort.loyal_level_items[rootItemToAdjust._id]; + assort.BarterScheme.Remove(rootItemToAdjust.Id); + assort.LoyalLevelItems.Remove(rootItemToAdjust.Id); } - /// - /// Get an integer rounded count of items to replace based on percentrage from traderConfig value - /// - /// total item count - /// rounded int of items to replace - protected int GetCountOfItemsToReplace(int totalItemCount) + /** + * Get an integer rounded count of items to replace based on percentrage from traderConfig value + * @param totalItemCount total item count + * @returns rounded int of items to replace + */ + protected int GetCountOfItemsToReplace(int totalItemCount) { - throw new NotImplementedException(); + return (int) Math.Round(totalItemCount * (traderConfig.Fence.PartialRefreshChangePercent / 100)); } - /// - /// Get the count of items fence offers - /// - /// int + /** + * Get the count of items fence offers + * @returns number + */ public int GetOfferCount() { - throw new NotImplementedException(); + if ((fenceAssort?.Items?.Count ?? 0) == 0) { + return 0; + } + + return fenceAssort.Items.Count; } - /// - /// Create trader assorts for fence and store in fenceService cache - /// Uses fence base cache generated on server start as a base - /// + /** + * Create trader assorts for fence and store in fenceService cache + * Uses fence base cache generatedon server start as a base + */ public void GenerateFenceAssorts() { - // TODO: actually implement - return; + // Reset refresh time now assorts are being generated + IncrementPartialRefreshTime(); + + // Choose assort counts using config + CreateInitialFenceAssortGenerationValues(); + + // Create basic fence assort + var assorts = CreateAssorts(desiredAssortCounts.Normal, 1); + + // Store in fenceAssort + SetFenceAssort(ConvertIntoFenceAssort(assorts)); + + // Create level 2 assorts accessible at rep level 6 + var discountAssorts = CreateAssorts(desiredAssortCounts.Discount, 2); + + // Store in fenceDiscountAssort + SetFenceDiscountAssort(ConvertIntoFenceAssort(discountAssorts)); } - /// - /// Convert the intermediary assort data generated into format client can process - /// - /// Generated assorts that will be converted - /// TraderAssort + /** + * Convert the intermediary assort data generated into format client can process + * @param intermediaryAssorts Generated assorts that will be converted + * @returns ITraderAssort + */ protected TraderAssort ConvertIntoFenceAssort(CreateFenceAssortsResult intermediaryAssorts) { - throw new NotImplementedException(); + var result = CreateFenceAssortSkeleton(); + foreach (var itemWithChilden in intermediaryAssorts.SptItems) { + result.Items.AddRange(itemWithChilden); + } + + result.BarterScheme = intermediaryAssorts.BarterScheme; + result.LoyalLevelItems = intermediaryAssorts.LoyalLevelItems; + + return result; } - /// - /// Create object that contains calculated fence assort item values to make based on config - /// Stored in this.DesiredAssortCounts - /// + /** + * Create object that contains calculated fence assort item values to make based on config + * Stored in desiredAssortCounts + */ protected void CreateInitialFenceAssortGenerationValues() { - throw new NotImplementedException(); + var result = new FenceAssortGenerationValues() { + Normal = new GenerationAssortValues() { Item = 0, WeaponPreset = 0, EquipmentPreset = 0 }, + Discount = new GenerationAssortValues() { Item = 0, WeaponPreset = 0, EquipmentPreset = 0 } + }; + + result.Normal.Item = traderConfig.Fence.AssortSize; + + result.Normal.WeaponPreset = randomUtil.GetInt( + (int) traderConfig.Fence.WeaponPresetMinMax.Min, + (int) traderConfig.Fence.WeaponPresetMinMax.Max + ); + + result.Normal.EquipmentPreset = randomUtil.GetInt( + (int) traderConfig.Fence.EquipmentPresetMinMax.Min, + (int) traderConfig.Fence.EquipmentPresetMinMax.Max + ); + + result.Discount.Item = traderConfig.Fence.DiscountOptions.AssortSize; + + result.Discount.WeaponPreset = randomUtil.GetInt( + (int) traderConfig.Fence.DiscountOptions.WeaponPresetMinMax.Min, + (int) traderConfig.Fence.DiscountOptions.WeaponPresetMinMax.Max + ); + + result.Discount.EquipmentPreset = randomUtil.GetInt( + (int) traderConfig.Fence.DiscountOptions.EquipmentPresetMinMax.Min, + (int) traderConfig.Fence.DiscountOptions.EquipmentPresetMinMax.Max + ); + + desiredAssortCounts = result; } - /// - /// Create skeleton to hold assort items - /// - /// TraderAssort object + /** + * Create skeleton to hold assort items + * @returns ITraderAssort object + */ protected TraderAssort CreateFenceAssortSkeleton() { - throw new NotImplementedException(); + return new TraderAssort() { + Items = [], + BarterScheme = new(), + LoyalLevelItems = new(), + NextResupply = GetNextFenceUpdateTimestamp(), + }; } - /// - /// Hydrate assorts parameter object with generated assorts - /// - /// Number of assorts to generate - /// Loyalty level to set new item to - /// CreateFenceAssortsResult + /** + * Hydrate assorts parameter object with generated assorts + * @param assortCount Number of assorts to generate + * @param assorts object to add created assorts to + */ protected CreateFenceAssortsResult CreateAssorts(GenerationAssortValues itemCounts, int loyaltyLevel) { - throw new NotImplementedException(); + var result = new CreateFenceAssortsResult() { SptItems = [], BarterScheme = new(), LoyalLevelItems = new() }; + + var baseFenceAssortClone = cloner.Clone(databaseService.GetTrader(Traders.FENCE).Assort); + var itemTypeLimitCounts = InitItemLimitCounter(traderConfig.Fence.ItemTypeLimits); + + if (itemCounts.Item > 0) { + AddItemAssorts(itemCounts.Item, result, baseFenceAssortClone, itemTypeLimitCounts, loyaltyLevel); + } + + if (itemCounts.WeaponPreset > 0 || itemCounts.EquipmentPreset > 0) { + // Add presets + AddPresetsToAssort( + itemCounts.WeaponPreset, + itemCounts.EquipmentPreset, + result, + baseFenceAssortClone, + loyaltyLevel + ); + } + + return result; } - /// - /// Add item assorts to existing assort data - /// - /// Number to add - /// Assorts data to add to - /// Base data to draw from - /// - /// Loyalty level to set new item to + /** + * Add item assorts to existing assort data + * @param assortCount Number to add + * @param assorts Assorts data to add to + * @param baseFenceAssortClone Base data to draw from + * @param itemTypeLimits + * @param loyaltyLevel Loyalty level to set new item to + */ protected void AddItemAssorts( - int assortCount, + int? assortCount, CreateFenceAssortsResult assorts, TraderAssort baseFenceAssortClone, Dictionary itemTypeLimits, int loyaltyLevel ) { - throw new NotImplementedException(); + var priceLimits = traderConfig.Fence.ItemCategoryRoublePriceLimit; + var assortRootItems = baseFenceAssortClone.Items.Where(item => item.ParentId == "hideout" && item.Upd?.SptPresetId == null).ToList(); + if (assortRootItems.Count == 0) { + logger.LogError("Unable to add assorts to Fence as no root items exist in items being added"); + return; + } + + for (var i = 0; i < assortCount; i++) { + var chosenBaseAssortRoot = randomUtil.GetArrayValue(assortRootItems); + if (chosenBaseAssortRoot == null) { + logger.LogError(localisationService.GetText("fence-unable_to_find_assort_by_id")); + continue; + } + var desiredAssortItemAndChildrenClone = cloner.Clone(itemHelper.FindAndReturnChildrenAsItems(baseFenceAssortClone.Items, chosenBaseAssortRoot.Id)); + + var itemDbDetails = itemHelper.GetItem(chosenBaseAssortRoot.Template).Value; + var itemLimitCount = GetMatchingItemLimit(itemTypeLimits, itemDbDetails.Id); + if (itemLimitCount?.current >= itemLimitCount?.max) { + // Skip adding item as assort as limit reached, decrement i counter so we still get another item + i--; + continue; + } + + var itemIsPreset = presetHelper.IsPreset(chosenBaseAssortRoot.Id); + + var price = baseFenceAssortClone.BarterScheme?[chosenBaseAssortRoot.Id][0][0].Count; + if (price == 0 || (price == 1 && !itemIsPreset) || price == 100) { + // Don't allow "special" items / presets + i--; + continue; + } + + if (price > priceLimits[itemDbDetails.Parent]) { + // Too expensive for fence, try another item + i--; + continue; + } + + // Increment count as item is being added + if (itemLimitCount.HasValue) { + var value = itemLimitCount.Value; + value.current += 1; + } + + // MUST randomise Ids as its possible to add the same base fence assort twice = duplicate IDs = dead client + desiredAssortItemAndChildrenClone = itemHelper.ReplaceIDs(desiredAssortItemAndChildrenClone); + itemHelper.RemapRootItemId(desiredAssortItemAndChildrenClone); + + var rootItemBeingAdded = desiredAssortItemAndChildrenClone[0]; + + // Set stack size based on possible overrides, e.g. ammos, otherwise set to 1 + rootItemBeingAdded.Upd.StackObjectsCount = GetSingleItemStackCount(itemDbDetails); + + // Only randomise Upd values for single + var isSingleStack = Math.Abs((rootItemBeingAdded.Upd?.StackObjectsCount ?? 0) - 1) < 0.1; + if (isSingleStack) { + RandomiseItemUpdProperties(itemDbDetails, rootItemBeingAdded); + } + + // Skip items already in the assort if it exists in the prevent duplicate list + var existingItemThatMatches = GetMatchingItem(rootItemBeingAdded, itemDbDetails, assorts.SptItems); + var shouldBeStacked = ItemShouldBeForceStacked(existingItemThatMatches, itemDbDetails); + if (shouldBeStacked && existingItemThatMatches != null) { + // Decrement loop counter so another items gets added + i--; + existingItemThatMatches.Upd.StackObjectsCount++; + + continue; + } + + // Add mods to armors so they dont show as red in the trade screen + if (itemHelper.ItemRequiresSoftInserts(rootItemBeingAdded.Template)) { + RandomiseArmorModDurability(desiredAssortItemAndChildrenClone, itemDbDetails); + } + + assorts.SptItems.Add(desiredAssortItemAndChildrenClone); + + assorts.BarterScheme[rootItemBeingAdded.Id] = cloner.Clone(baseFenceAssortClone.BarterScheme[chosenBaseAssortRoot.Id]); + + // Only adjust item price by quality for solo items, never multi-stack + if (isSingleStack) { + AdjustItemPriceByQuality(assorts.BarterScheme, rootItemBeingAdded, itemDbDetails); + } + + assorts.LoyalLevelItems[rootItemBeingAdded.Id] = loyaltyLevel; + } } - /// - /// Find an assort item that matches the first parameter, also matches based on upd properties - /// e.g. salewa hp resource units left - /// - /// item to look for a match against - /// Db details of matching item - /// Items to search through - /// Matching assort item - protected virtual Item GetMatchingItem( + /** + * Find an assort item that matches the first parameter, also matches based on Upd properties + * e.g. salewa hp resource units left + * @param rootItemBeingAdded item to look for a match against + * @param itemDbDetails Db details of matching item + * @param itemsWithChildren Items to search through + * @returns Matching assort item + */ + protected Item? GetMatchingItem( Item rootItemBeingAdded, TemplateItem itemDbDetails, - List> itemsWithChildren) + List> itemsWithChildren + ) { - throw new NotImplementedException(); + // Get matching root items + var matchingItems = itemsWithChildren + .Where((itemWithChildren) => itemWithChildren.FirstOrDefault((item) => item.Template == rootItemBeingAdded.Template && item.ParentId == "hideout") != null).SelectMany(i => i).ToList(); + if (matchingItems.Count == 0) { + // Nothing matches by tpl and is root item, exit early + return null; + } + + var isMedical = itemHelper.IsOfBaseclasses(rootItemBeingAdded.Template, [ + BaseClasses.MEDICAL, + BaseClasses.MEDKIT + ]); + var isGearAndHasSlots = + itemHelper.IsOfBaseclasses(rootItemBeingAdded.Template, [ + BaseClasses.ARMORED_EQUIPMENT, + BaseClasses.SEARCHABLE_ITEM + ]) && (itemDbDetails.Properties.Slots?.Count ?? 0) > 0; + + // Only one match and its not medical or armored gear + if (matchingItems.Count == 1 && !(isMedical || isGearAndHasSlots)) { + return matchingItems[0]; + } + + // Items have sub properties that need to be checked against + foreach (var item in matchingItems) { + if (isMedical && rootItemBeingAdded.Upd?.MedKit?.HpResource == item.Upd?.MedKit?.HpResource) { + // e.g. bandages with multiple use + // Both undefined === both max resoruce left + return item; + } + + // Armors/helmets etc + if ( + isGearAndHasSlots && + rootItemBeingAdded.Upd.Repairable?.Durability == item.Upd.Repairable?.Durability && + rootItemBeingAdded.Upd.Repairable?.MaxDurability == item.Upd.Repairable?.MaxDurability + ) { + return item; + } + } + + return null; } - /// - /// Should this item be forced into only 1 stack on fence - /// - /// Existing item from fence assort - /// Item we want to add db details - /// True item should be force stacked - protected virtual bool ItemShouldBeForceStacked(Item existingItem, TemplateItem itemDbDetails) + /** + * Should this item be forced into only 1 stack on fence + * @param existingItem Existing item from fence assort + * @param itemDbDetails Item we want to add db details + * @returns True item should be force stacked + */ + protected bool ItemShouldBeForceStacked(Item? existingItem, TemplateItem itemDbDetails) { - throw new NotImplementedException(); + // No existing item in assort + if (existingItem == null) { + return false; + } + + // Don't stack child items, only root items + if (existingItem.ParentId != "hideout") { + return false; + } + + return ItemInPreventDupeCategoryList(itemDbDetails.Id); } - protected virtual bool ItemInPreventDupeCategoryList(string tpl) - { - throw new NotImplementedException(); + protected bool ItemInPreventDupeCategoryList(string tpl) { + // Item type in config list + return itemHelper.IsOfBaseclasses(tpl, traderConfig.Fence.PreventDuplicateOffersOfCategory); } - /// - /// Adjust price of item based on what is left to buy (resource/uses left) - /// - /// All barter scheme for item having price adjusted - /// Root item having price adjusted - /// Db template of item - protected virtual void AdjustItemPriceByQuality( + /** + * Adjust price of item based on what is left to buy (resource/uses left) + * @param barterSchemes All barter scheme for item having price adjusted + * @param itemRoot Root item having price adjusted + * @param itemTemplate Db template of item + */ + protected void AdjustItemPriceByQuality( Dictionary>> barterSchemes, Item itemRoot, - TemplateItem itemTemplate) + TemplateItem itemTemplate + ) { - throw new NotImplementedException(); + // Healing items + if (itemRoot.Upd?.MedKit != null) { + var itemTotalMax = itemTemplate.Properties.MaxHpResource; + var current = itemRoot.Upd.MedKit.HpResource; + + // Current and max match, no adjustment necessary + if (itemTotalMax == current) { + return; + } + + var multipler = current / itemTotalMax; + + // Multiply item cost by desired multiplier + var basePrice = barterSchemes[itemRoot.Id][0][0].Count; + barterSchemes[itemRoot.Id][0][0].Count = Math.Round((double)(basePrice * multipler)); + + return; + } + + // Adjust price based on durability + if (itemRoot.Upd?.Repairable != null || itemHelper.IsOfBaseclass(itemRoot.Template, BaseClasses.KEY_MECHANICAL)) { + var itemQualityModifier = itemHelper.GetItemQualityModifier(itemRoot); + var basePrice = barterSchemes[itemRoot.Id][0][0].Count; + barterSchemes[itemRoot.Id][0][0].Count = Math.Round((double) basePrice * itemQualityModifier); + } } - protected virtual Dictionary GetMatchingItemLimit( - Dictionary itemTypeLimits, - string itemTpl) + protected (int current, int max)? GetMatchingItemLimit(Dictionary itemTypeLimits, string itemTpl) { - throw new NotImplementedException(); + foreach (var baseTypeKey in itemTypeLimits.Keys) { + if (itemHelper.IsOfBaseclass(itemTpl, baseTypeKey)) { + return itemTypeLimits[baseTypeKey]; + } + } + + return null; } - /// - /// Find presets in base fence assort and add desired number to 'assorts' parameter - /// - /// - /// Assorts to add preset to - /// Base data to draw from - /// Which loyalty level is required to see/buy item - protected virtual void AddPresetsToAssort( - int desiredWeaponPresetsCount, - int desiredEquipmentPresetsCount, + /** + * Find presets in base fence assort and add desired number to 'assorts' parameter + * @param desiredWeaponPresetsCount + * @param assorts Assorts to add preset to + * @param baseFenceAssort Base data to draw from + * @param loyaltyLevel Which loyalty level is required to see/buy item + */ + protected void AddPresetsToAssort( + int? desiredWeaponPresetsCount, + int? desiredEquipmentPresetsCount, CreateFenceAssortsResult assorts, TraderAssort baseFenceAssort, - int loyaltyLevel) + int loyaltyLevel + ) { - throw new NotImplementedException(); + var weaponPresetsAddedCount = 0; + if (desiredWeaponPresetsCount > 0) { + var weaponPresetRootItems = baseFenceAssort.Items.Where(item => item.Upd?.SptPresetId != null && itemHelper.IsOfBaseclass(item.Template, BaseClasses.WEAPON)); + while (weaponPresetsAddedCount < desiredWeaponPresetsCount) { + var randomPresetRoot = randomUtil.GetArrayValue(weaponPresetRootItems); + if (traderConfig.Fence.Blacklist.Contains(randomPresetRoot.Template)) { + continue; + } + + var rootItemDb = itemHelper.GetItem(randomPresetRoot.Template).Value; + + var presetWithChildrenClone = cloner.Clone(itemHelper.FindAndReturnChildrenAsItems(baseFenceAssort.Items, randomPresetRoot.Id)); + + RandomiseItemUpdProperties(rootItemDb, presetWithChildrenClone[0]); + + RemoveRandomModsOfItem(presetWithChildrenClone); + + // Check chosen item is below price cap + var priceLimitRouble = traderConfig.Fence.ItemCategoryRoublePriceLimit[rootItemDb.Parent]; + var itemPrice = + handbookHelper.GetTemplatePriceForItems(presetWithChildrenClone) * + itemHelper.GetItemQualityModifierForItems(presetWithChildrenClone); + if (priceLimitRouble != null) { + if (itemPrice > priceLimitRouble) { + // Too expensive, try again + continue; + } + } + + // MUST randomise Ids as its possible to add the same base fence assort twice = duplicate IDs = dead client + itemHelper.ReparentItemAndChildren(presetWithChildrenClone[0], presetWithChildrenClone); + itemHelper.RemapRootItemId(presetWithChildrenClone); + + // Remapping IDs causes parentid to be altered + presetWithChildrenClone[0].ParentId = "hideout"; + + assorts.SptItems.Add(presetWithChildrenClone); + + // Set assort price + // Must be careful to use correct id as the item has had its IDs regenerated + assorts.BarterScheme[presetWithChildrenClone[0].Id] = [ + [ + new BarterScheme() { + Template = Money.ROUBLES, + Count = Math.Round(itemPrice), + } + ] + ]; + assorts.LoyalLevelItems[presetWithChildrenClone[0].Id] = loyaltyLevel; + + weaponPresetsAddedCount++; + } + } + + var equipmentPresetsAddedCount = 0; + if (desiredEquipmentPresetsCount <= 0) { + return; + } + + var equipmentPresetRootItems = baseFenceAssort.Items.Where((item) => item.Upd?.SptPresetId != null && itemHelper.ArmorItemCanHoldMods(item.Template)); + while (equipmentPresetsAddedCount < desiredEquipmentPresetsCount) { + var randomPresetRoot = randomUtil.GetArrayValue(equipmentPresetRootItems); + var rootItemDb = itemHelper.GetItem(randomPresetRoot.Template).Value; + + var presetWithChildrenClone = cloner.Clone(itemHelper.FindAndReturnChildrenAsItems(baseFenceAssort.Items, randomPresetRoot.Id)); + + // Need to add mods to armors so they dont show as red in the trade screen + if (itemHelper.ItemRequiresSoftInserts(randomPresetRoot.Template)) + { + RandomiseArmorModDurability(presetWithChildrenClone, rootItemDb); + } + + RemoveRandomModsOfItem(presetWithChildrenClone); + + // Check chosen item is below price cap + var priceLimitRouble = traderConfig.Fence.ItemCategoryRoublePriceLimit[rootItemDb.Parent]; + var itemPrice = + handbookHelper.GetTemplatePriceForItems(presetWithChildrenClone) * + itemHelper.GetItemQualityModifierForItems(presetWithChildrenClone); + if (priceLimitRouble != null) { + if (itemPrice > priceLimitRouble) { + // Too expensive, try again + continue; + } + } + + // MUST randomise Ids as its possible to add the same base fence assort twice = duplicate IDs = dead client + itemHelper.ReparentItemAndChildren(presetWithChildrenClone[0], presetWithChildrenClone); + itemHelper.RemapRootItemId(presetWithChildrenClone); + + // Remapping IDs causes parentid to be altered + presetWithChildrenClone[0].ParentId = "hideout"; + + assorts.SptItems.Add(presetWithChildrenClone); + + // Set assort price + // Must be careful to use correct id as the item has had its IDs regenerated + assorts.BarterScheme[presetWithChildrenClone[0].Id] = [ + [ + new BarterScheme(){ + Template = Money.ROUBLES, + Count = Math.Round(itemPrice), + } + ] + ]; + assorts.LoyalLevelItems[presetWithChildrenClone[0].Id] = loyaltyLevel; + + equipmentPresetsAddedCount++; + } } - /// - /// Adjust plate / soft insert durability values - /// - /// Armor item array to add mods into - /// Armor items db template - protected virtual void RandomiseArmorModDurability(List armor, TemplateItem itemDbDetails) + /** + * Adjust plate / soft insert durability values + * @param armor Armor item array to add mods into + * @param itemDbDetails Armor items db template + */ + protected void RandomiseArmorModDurability(List armor, TemplateItem itemDbDetails) { - throw new NotImplementedException(); + // Armor has no mods, nothing to randomise + if (itemDbDetails.Properties.Slots == null) { + return; + } + + // Check for and adjust soft insert durability values + var requiredSlots = itemDbDetails.Properties.Slots?.Where(slot => slot.Required ?? false).ToList(); + if ((requiredSlots?.Count ?? 0) > 1) { + RandomiseArmorSoftInsertDurabilities(requiredSlots, armor); + } + + // Check for and adjust plate durability values + var plateSlots = itemDbDetails.Properties.Slots?.Where(slot => itemHelper.IsRemovablePlateSlot(slot.Name)).ToList(); + if ((plateSlots?.Count ?? 0) > 1) { + RandomiseArmorInsertsDurabilities(plateSlots, armor); + } } - /// - /// Randomise the durability values of items on armor with a passed in slot - /// - /// Slots of items to randomise - /// Array of armor + inserts to get items from - protected virtual void RandomiseArmorSoftInsertDurabilities(List softInsertSlots, List armorItemAndMods) + /** + * Randomise the durability values of items on armor with a passed in slot + * @param softInsertSlots Slots of items to randomise + * @param armorItemAndMods Array of armor + inserts to get items from + */ + protected void RandomiseArmorSoftInsertDurabilities(List softInsertSlots, List armorItemAndMods) { - throw new NotImplementedException(); + foreach (var requiredSlot in softInsertSlots) { + var modItemDbDetails = itemHelper.GetItem(requiredSlot.Props.Filters[0].Plate).Value; + var durabilityValues = GetRandomisedArmorDurabilityValues(modItemDbDetails, traderConfig.Fence.ArmorMaxDurabilityPercentMinMax); + var plateTpl = requiredSlot.Props.Filters[0].Plate ?? string.Empty; // "Plate" property appears to be the 'default' item for slot + if (plateTpl == "") { + // Some bsg plate properties are empty, skip mod + continue; + } + + // Find items mod to apply dura changes to + var modItemToAdjust = armorItemAndMods.FirstOrDefault(mod => mod.SlotId.ToLower() == requiredSlot.Name.ToLower()); + + itemHelper.AddUpdObjectToItem(modItemToAdjust); + + if (modItemToAdjust.Upd.Repairable == null) { + modItemToAdjust.Upd.Repairable = new UpdRepairable(){ + Durability = modItemDbDetails.Properties.MaxDurability, + MaxDurability = modItemDbDetails.Properties.MaxDurability + }; + } + modItemToAdjust.Upd.Repairable.Durability = durabilityValues.Durability; + modItemToAdjust.Upd.Repairable.MaxDurability = durabilityValues.MaxDurability; + + // 25% chance to add shots to visor items when its below max durability + if (randomUtil.GetChance100(25) && + modItemToAdjust.ParentId == BaseClasses.ARMORED_EQUIPMENT && + modItemToAdjust.SlotId == "mod_equipment_000" && + modItemToAdjust.Upd.Repairable.Durability < modItemDbDetails.Properties.MaxDurability) + { + // Is damaged + modItemToAdjust.Upd.FaceShield = new UpdFaceShield() { Hits = randomUtil.GetInt(1, 3) }; + } + } } - /// - /// Randomise the durability values of plate items in armor - /// Has chance to remove plate - /// - /// Slots of items to randomise - /// Array of armor + inserts to get items from - protected virtual void RandomiseArmorInsertsDurabilities(List plateSlots, List armorItemAndMods) + /** + * Randomise the durability values of plate items in armor + * Has chance to remove plate + * @param plateSlots Slots of items to randomise + * @param armorItemAndMods Array of armor + inserts to get items from + */ + protected void RandomiseArmorInsertsDurabilities(List plateSlots, List armorItemAndMods) { - throw new NotImplementedException(); + foreach (var plateSlot in plateSlots) { + var plateTpl = plateSlot.Props.Filters[0].Plate; + if (plateTpl == null) { + // Bsg data lacks a default plate, skip randomisng for this mod + continue; + } + + var armorWithMods = armorItemAndMods; + + var modItemDbDetails = itemHelper.GetItem(plateTpl).Value; + + // Chance to remove plate + var plateExistsChance = traderConfig.Fence.ChancePlateExistsInArmorPercent[modItemDbDetails?.Properties?.ArmorClass?.ToString() ?? "3"]; + if (!randomUtil.GetChance100(plateExistsChance)) { + // Remove plate from armor + armorWithMods = armorItemAndMods.Where(item => item.SlotId.ToLower() != plateSlot.Name.ToLower()).ToList(); + + continue; + } + + var durabilityValues = GetRandomisedArmorDurabilityValues( + modItemDbDetails, + traderConfig.Fence.ArmorMaxDurabilityPercentMinMax + ); + + // Find items mod to apply dura changes to + var modItemToAdjust = armorWithMods.FirstOrDefault(mod => mod.SlotId.ToLower() == plateSlot.Name.ToLower()); + + if (modItemToAdjust == null) { + logger.LogWarning($"Unable to randomise armor items {armorWithMods[0].Template} ${plateSlot.Name} slot as it cannot be found, skipping"); + continue; + } + + itemHelper.AddUpdObjectToItem(modItemToAdjust); + + if (modItemToAdjust?.Upd?.Repairable == null) { + modItemToAdjust.Upd.Repairable = new UpdRepairable(){ + Durability = modItemDbDetails.Properties.MaxDurability, + MaxDurability = modItemDbDetails.Properties.MaxDurability + }; + } + + modItemToAdjust.Upd.Repairable.Durability = durabilityValues.Durability; + modItemToAdjust.Upd.Repairable.MaxDurability = durabilityValues.MaxDurability; + } } - /// - /// Get stack size of a singular item (no mods) - /// - /// item being added to fence - /// Stack size - protected virtual int GetSingleItemStackCount(TemplateItem itemDbDetails) + /** + * Get stack size of a singular item (no mods) + * @param itemDbDetails item being added to fence + * @returns Stack size + */ + protected int GetSingleItemStackCount(TemplateItem itemDbDetails) { - throw new NotImplementedException(); + MinMax? overrideValues; + if (itemHelper.IsOfBaseclass(itemDbDetails.Id, BaseClasses.AMMO)) { + overrideValues = traderConfig.Fence.ItemStackSizeOverrideMinMax[itemDbDetails.Parent]; + if (overrideValues != null) { + return randomUtil.GetInt((int) overrideValues.Min, (int) overrideValues.Max); + } + + // No override, use stack max size from item db + return itemDbDetails.Properties.StackMaxSize == 1 + ? 1 + : randomUtil.GetInt((int) itemDbDetails.Properties.StackMinRandom, (int) itemDbDetails.Properties.StackMaxRandom); + } + + // Check for override in config, use values if exists + overrideValues = traderConfig.Fence.ItemStackSizeOverrideMinMax[itemDbDetails.Id]; + if (overrideValues != null) { + return randomUtil.GetInt((int) overrideValues.Min, (int) overrideValues.Max); + } + + // Check for parent override + overrideValues = traderConfig.Fence.ItemStackSizeOverrideMinMax[itemDbDetails.Parent]; + if (overrideValues != null) { + return randomUtil.GetInt((int) overrideValues.Min, (int) overrideValues.Max); + } + + return 1; } - /// - /// Remove parts of a weapon prior to being listed on flea - /// - /// Weapon to remove parts from - protected virtual void RemoveRandomModsOfItem(List itemAndMods) + /** + * Remove parts of a weapon prior to being listed on flea + * @param itemAndMods Weapon to remove parts from + */ + protected void RemoveRandomModsOfItem(List itemAndMods) { - throw new NotImplementedException(); + // Items to be removed from inventory + var toDelete = new List(); + + // Find mods to remove from item that could've been scavenged by other players in-raid + foreach (var itemMod in itemAndMods) { + if (PresetModItemWillBeRemoved(itemMod, toDelete)) { + // Skip if not an item + var itemDbDetails = itemHelper.GetItem(itemMod.Template); + if (!itemDbDetails.Key) { + continue; + } + + // Remove item and its sub-items to prevent orphans + toDelete.AddRange(itemHelper.FindAndReturnChildrenByItems(itemAndMods, itemMod.Id)); + } + } + + // Reverse loop and remove items + for (var index = itemAndMods.Count - 1; index >= 0; --index) { + if (toDelete.Contains(itemAndMods[index].Id)) { + itemAndMods.Splice(index, 1); + } + } } - /// - /// Roll % chance check to see if item should be removed - /// - /// Weapon mod being checked - /// Current list of items on weapon being deleted - /// True if item will be removed - protected virtual bool PresetModItemWillBeRemoved(Item weaponMod, List itemsBeingDeleted) + /** + * Roll % chance check to see if item should be removed + * @param weaponMod Weapon mod being checked + * @param itemsBeingDeleted Current list of items on weapon being deleted + * @returns True if item will be removed + */ + protected bool PresetModItemWillBeRemoved(Item weaponMod, List itemsBeingDeleted) { - throw new NotImplementedException(); + var slotIdsThatCanFail = traderConfig.Fence.PresetSlotsToRemoveChancePercent; + var removalChance = slotIdsThatCanFail[weaponMod.SlotId]; + if (removalChance is null or 0.0) { + return false; + } + + // Roll from 0 to 9999, then divide it by 100: 9999 = 99.99% + var randomChance = randomUtil.GetInt(0, 9999) / 100; + + return removalChance > randomChance && !itemsBeingDeleted.Contains(weaponMod.Id); } - /// - /// Randomise items' upd properties e.g. med packs/weapons/armor - /// - /// Item being randomised - /// Item being edited - protected virtual void RandomiseItemUpdProperties(TemplateItem itemDetails, Item itemToAdjust) + /** + * Randomise items' Upd properties e.g. med packs/weapons/armor + * @param itemDetails Item being randomised + * @param itemToAdjust Item being edited + */ + protected void RandomiseItemUpdProperties(TemplateItem itemDetails, Item itemToAdjust) { - throw new NotImplementedException(); + if (itemDetails.Properties == null) { + logger.LogError($"Item {itemDetails.Name} lacks a _props field, unable to randomise item: {itemToAdjust.Id}"); + return; + } + + // Randomise hp resource of med items + if (itemDetails.Properties.MaxHpResource != null && (itemDetails.Properties.MaxHpResource ?? 0) > 0) { + itemToAdjust.Upd.MedKit = new UpdMedKit() { HpResource = randomUtil.GetInt(1, (int) itemDetails.Properties.MaxHpResource) }; + } + + // Randomise armor durability + if ( + (itemDetails.Parent == BaseClasses.ARMORED_EQUIPMENT || + itemDetails.Parent == BaseClasses.FACECOVER || + itemDetails.Parent == BaseClasses.ARMOR_PLATE) && + (itemDetails.Properties.MaxDurability ?? 0) > 0 + ) { + var values = GetRandomisedArmorDurabilityValues( + itemDetails, + traderConfig.Fence.ArmorMaxDurabilityPercentMinMax + ); + itemToAdjust.Upd.Repairable = new UpdRepairable() { Durability = values.Durability, MaxDurability= values.MaxDurability }; + + return; + } + + // Randomise Weapon durability + if (itemHelper.IsOfBaseclass(itemDetails.Id, BaseClasses.WEAPON)) { + var weaponDurabilityLimits = traderConfig.Fence.WeaponDurabilityPercentMinMax; + var maxDuraMin = (weaponDurabilityLimits.Max.Min / 100) * itemDetails.Properties.MaxDurability; + var maxDuraMax = (weaponDurabilityLimits.Max.Max / 100) * itemDetails.Properties.MaxDurability; + var chosenMaxDurability = randomUtil.GetInt((int) maxDuraMin, (int) maxDuraMax); + + var currentDuraMin = (weaponDurabilityLimits.Current.Min / 100) * itemDetails.Properties.MaxDurability; + var currentDuraMax = (weaponDurabilityLimits.Current.Max / 100) * itemDetails.Properties.MaxDurability; + var currentDurability = Math.Min( + randomUtil.GetInt((int) currentDuraMin, (int) currentDuraMax), + chosenMaxDurability + ); + + itemToAdjust.Upd.Repairable = new UpdRepairable { Durability = currentDurability, MaxDurability = chosenMaxDurability }; + + return; + } + + if (itemHelper.IsOfBaseclass(itemDetails.Id, BaseClasses.REPAIR_KITS)) { + itemToAdjust.Upd.RepairKit = new UpdRepairKit { + Resource = randomUtil.GetInt(1, (int) itemDetails.Properties.MaxRepairResource), + }; + + return; + } + + // Mechanical key + has limited uses + if (itemHelper.IsOfBaseclass(itemDetails.Id, BaseClasses.KEY_MECHANICAL) && + (itemDetails.Properties.MaximumNumberOfUsage ?? 0) > 1) { + itemToAdjust.Upd.Key = new UpdKey { + NumberOfUsages = randomUtil.GetInt(0, (int) itemDetails.Properties.MaximumNumberOfUsage - 1), + }; + + return; + } + + // Randomise items that use resources (e.g. fuel) + if ((itemDetails.Properties.MaxResource ?? 0) > 0) { + var resourceMax = itemDetails.Properties.MaxResource; + var resourceCurrent = randomUtil.GetInt(1, (int) itemDetails.Properties.MaxResource); + + itemToAdjust.Upd.Resource = new UpdResource { Value = resourceMax - resourceCurrent, UnitsConsumed = resourceCurrent }; + } } - /// - /// Generate a randomised current and max durabiltiy value for an armor item - /// - /// Item to create values for - /// Max durabiltiy percent min/max values - /// Durability + MaxDurability values - protected virtual UpdRepairable GetRandomisedArmorDurabilityValues( + /** + * Generate a randomised current and max durabiltiy value for an armor item + * @param itemDetails Item to create values for + * @param equipmentDurabilityLimits Max durabiltiy percent min/max values + * @returns Durability + MaxDurability values + */ + protected UpdRepairable GetRandomisedArmorDurabilityValues( TemplateItem itemDetails, - ItemDurabilityCurrentMax equipmentDurabilityLimits) + ItemDurabilityCurrentMax equipmentDurabilityLimits + ) { - throw new NotImplementedException(); + var maxDuraMin = (equipmentDurabilityLimits.Max.Min / 100) * itemDetails.Properties.MaxDurability; + var maxDuraMax = (equipmentDurabilityLimits.Max.Max / 100) * itemDetails.Properties.MaxDurability; + var chosenMaxDurability = randomUtil.GetInt((int) maxDuraMin, (int) maxDuraMax); + + var currentDuraMin = (equipmentDurabilityLimits.Current.Min / 100) * itemDetails.Properties.MaxDurability; + var currentDuraMax = (equipmentDurabilityLimits.Current.Max / 100) * itemDetails.Properties.MaxDurability; + var chosenCurrentDurability = Math.Min( + randomUtil.GetInt((int) currentDuraMin, (int) currentDuraMax), + chosenMaxDurability + ); + + return new UpdRepairable() { Durability = chosenCurrentDurability, MaxDurability = chosenMaxDurability }; } - /// - /// Construct item limit record to hold max and current item count - /// - /// limits as defined in config - /// record, key: item tplId, value: current/max item count allowed + /** + * Construct item limit record to hold max and current item count + * @param limits limits as defined in config + * @returns record, key: item tplId, value: current/max item count allowed + */ protected Dictionary InitItemLimitCounter(Dictionary limits) { - throw new NotImplementedException(); + var itemTypeCounts = new Dictionary(); + + foreach (var x in limits.Keys) { + itemTypeCounts[x] = new () { current = 0, max = limits[x] }; + } + + return itemTypeCounts; } - /// - /// Get the next update timestamp for fence - /// - /// future timestamp - public int GetNextFenceUpdateTimestamp() + /** + * Get the next Update timestamp for fence + * @returns future timestamp + */ + public long GetNextFenceUpdateTimestamp() { - throw new NotImplementedException(); + var time = timeUtil.GetTimeStamp(); + var UpdateSeconds = GetFenceRefreshTime(); + return time + UpdateSeconds; } - /// - /// Get fence refresh time in seconds - /// - /// Refresh time in seconds - protected int GetFenceRefreshTime() - { - throw new NotImplementedException(); + /** + * Get fence refresh time in seconds + * @returns Refresh time in seconds + */ + protected int GetFenceRefreshTime() { + var fence = traderConfig.UpdateTime.FirstOrDefault((x) => x.TraderId == Traders.FENCE).Seconds; + + return randomUtil.GetInt((int) fence.Min, (int) fence.Max); } - /// - /// Get fence level the passed in profile has - /// - /// Player profile - /// FenceLevel object + /** + * Get fence level the passed in profile has + * @param pmcData Player profile + * @returns FenceLevel object + */ public FenceLevel GetFenceInfo(PmcData pmcData) { - var fenceSettings = _databaseService.GetGlobals().Configuration.FenceSettings; - pmcData.TradersInfo.TryGetValue(fenceSettings.FenceIdentifier, out var pmcFenceInfo); + var fenceSettings = databaseService.GetGlobals().Configuration.FenceSettings; + var pmcFenceInfo = pmcData.TradersInfo[fenceSettings.FenceIdentifier]; - if (pmcFenceInfo is null) - { + if (pmcFenceInfo == null) { return fenceSettings.Levels["0"]; } - var fenceLevels = fenceSettings.Levels.Select(x => x.Key); + var fenceLevels = fenceSettings.Levels.Keys.Select(int.Parse); var minLevel = fenceLevels.Min(); var maxLevel = fenceLevels.Max(); - var pmcFenceLevel = Math.Floor(pmcFenceInfo.Standing ?? 0); + var pmcFenceLevel = Math.Floor((double) pmcFenceInfo.Standing); - if (pmcFenceLevel < int.Parse(minLevel)) - { - return fenceSettings.Levels[minLevel]; + if (pmcFenceLevel < minLevel) { + return fenceSettings.Levels[minLevel.ToString()]; } - if (pmcFenceLevel > int.Parse(maxLevel)) - { - return fenceSettings.Levels[maxLevel]; + if (pmcFenceLevel > maxLevel) { + return fenceSettings.Levels[maxLevel.ToString()]; } return fenceSettings.Levels[pmcFenceLevel.ToString()]; } - /// - /// Remove or lower stack size of an assort from fence by id - /// - /// assort id to adjust - /// Count of items bought - public void AmendOrRemoveFenceOffer(string assortId, double buyCount) + /** + * Remove or lower stack size of an assort from fence by id + * @param assortId assort id to adjust + * @param buyCount Count of items bought + */ + public void AmendOrRemoveFenceOffer(string assortId, int buyCount) { - throw new NotImplementedException(); + var isNormalAssort = true; + var fenceAssortItem = fenceAssort.Items.FirstOrDefault((item) => item.Id == assortId); + if (fenceAssortItem == null) { + // Not in main assorts, check secondary section + fenceAssortItem = fenceDiscountAssort.Items.FirstOrDefault((item) => item.Id == assortId); + if (fenceAssortItem == null) { + logger.LogError(localisationService.GetText("fence-unable_to_find_offer_by_id", assortId)); + + return; + } + isNormalAssort = false; + } + + // Player wants to buy whole stack, delete stack + if ((fenceAssortItem.Upd?.StackObjectsCount ?? 0) == buyCount) { + DeleteOffer(assortId, isNormalAssort ? fenceAssort.Items : fenceDiscountAssort.Items); + return; + } + + // Adjust stack size + fenceAssortItem.Upd.StackObjectsCount -= buyCount; } protected void DeleteOffer(string assortId, List assorts) { - throw new NotImplementedException(); + // Assort could have child items, remove those too + var itemWithChildrenToRemove = itemHelper.FindAndReturnChildrenAsItems(assorts, assortId); + foreach (var itemToRemove in itemWithChildrenToRemove) { + var indexToRemove = assorts.FindIndex((item) => item.Id == itemToRemove.Id); + + // No offer found in main assort, check discount items + if (indexToRemove == -1) { + indexToRemove = fenceDiscountAssort.Items.FindIndex((item) => item.Id == itemToRemove.Id); + fenceDiscountAssort.Items.Splice(indexToRemove, 1); + + if (indexToRemove == -1) { + logger.LogWarning($"unable to remove fence assort item: {itemToRemove.Id} tpl: {itemToRemove.Template}"); + } + + return; + } + + // Remove offer from assort + assorts.Splice(indexToRemove, 1); + } } } diff --git a/Libraries/Core/Utils/RandomUtil.cs b/Libraries/Core/Utils/RandomUtil.cs index 704b84bb..477e9913 100644 --- a/Libraries/Core/Utils/RandomUtil.cs +++ b/Libraries/Core/Utils/RandomUtil.cs @@ -468,9 +468,16 @@ public class RandomUtil(ISptLogger _logger, ICloner _cloner) : 0; } - public T GetArrayValue(IEnumerable list) + public T? GetArrayValue(IEnumerable list) { var rand = new Random(); - return list.ElementAt(rand.Next(0, list.Count())); + try + { + return list.ElementAt(rand.Next(0, list.Count())); + } + catch (Exception) + { + return default; + } } } From 627d5dfe5c080b17fa739f51c0f56d4dfd213c3f Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 24 Jan 2025 12:17:48 +0000 Subject: [PATCH 2/2] more stuff --- Libraries/Core/Helpers/InventoryHelper.cs | 2 +- .../Eft/Inventory/AddItemDirectRequest.cs | 2 +- Libraries/Core/Services/FenceService.cs | 555 ++++++++++++------ 3 files changed, 372 insertions(+), 187 deletions(-) diff --git a/Libraries/Core/Helpers/InventoryHelper.cs b/Libraries/Core/Helpers/InventoryHelper.cs index b6711e6c..04d291e5 100644 --- a/Libraries/Core/Helpers/InventoryHelper.cs +++ b/Libraries/Core/Helpers/InventoryHelper.cs @@ -132,7 +132,7 @@ public class InventoryHelper( try { if (request.Callback is not null) - request.Callback(rootItemToAdd.Upd.StackObjectsCount.Value); + request.Callback((int) (rootItemToAdd.Upd.StackObjectsCount ?? 0)); } catch (Exception ex) { diff --git a/Libraries/Core/Models/Eft/Inventory/AddItemDirectRequest.cs b/Libraries/Core/Models/Eft/Inventory/AddItemDirectRequest.cs index 2804d64d..6f37f98e 100644 --- a/Libraries/Core/Models/Eft/Inventory/AddItemDirectRequest.cs +++ b/Libraries/Core/Models/Eft/Inventory/AddItemDirectRequest.cs @@ -15,7 +15,7 @@ public record AddItemDirectRequest public bool? FoundInRaid { get; set; } [JsonPropertyName("callback")] - public Action? Callback { get; set; } + public Action? Callback { get; set; } [JsonPropertyName("useSortingTable")] public bool? UseSortingTable { get; set; } diff --git a/Libraries/Core/Services/FenceService.cs b/Libraries/Core/Services/FenceService.cs index 70be1b0f..7fbc8ba3 100644 --- a/Libraries/Core/Services/FenceService.cs +++ b/Libraries/Core/Services/FenceService.cs @@ -1,7 +1,5 @@ -using System.Runtime.InteropServices.JavaScript; using Core.Helpers; using Core.Models.Common; -using SptCommon.Annotations; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; using Core.Models.Enums; @@ -10,6 +8,7 @@ using Core.Models.Spt.Fence; using Core.Servers; using Core.Utils; using Core.Utils.Cloners; +using SptCommon.Annotations; using SptCommon.Extensions; namespace Core.Services; @@ -26,7 +25,7 @@ public class FenceService( LocalisationService localisationService, ConfigServer configServer, ICloner cloner - ) +) { protected TraderConfig traderConfig = configServer.GetConfig(); @@ -109,7 +108,8 @@ public class FenceService( */ public TraderAssort GetFenceAssorts(PmcData pmcProfile) { - if (traderConfig.Fence.RegenerateAssortsOnRefresh) { + if (traderConfig.Fence.RegenerateAssortsOnRefresh) + { // Using base assorts made earlier, do some alterations and store in fenceAssort GenerateFenceAssorts(); } @@ -119,7 +119,8 @@ public class FenceService( AdjustAssortItemPricesByConfigMultiplier(assort, 1, traderConfig.Fence.PresetPriceMult); // merge normal fence assorts + discount assorts if player standing is large enough - if (pmcProfile.TradersInfo[Traders.FENCE].Standing >= 6) { + if (pmcProfile.TradersInfo[Traders.FENCE].Standing >= 6) + { var discountAssort = cloner.Clone(fenceDiscountAssort); AdjustAssortItemPricesByConfigMultiplier( discountAssort, @@ -151,7 +152,8 @@ public class FenceService( // Fix IDs clonedItems = itemHelper.ReparentItemAndChildren(root, clonedItems); root.ParentId = "hideout"; - if (root.Upd?.SpawnedInSession != null) { + if (root.Upd?.SpawnedInSession != null) + { root.Upd.SpawnedInSession = false; } @@ -190,8 +192,10 @@ public class FenceService( protected double? GetAmmoBoxPrice(List items) { double? total = 0D; - foreach (var item in items) { - if (itemHelper.IsOfBaseclass(item.Template, BaseClasses.AMMO)) { + foreach (var item in items) + { + if (itemHelper.IsOfBaseclass(item.Template, BaseClasses.AMMO)) + { total += handbookHelper.GetTemplatePrice(item.Template) * (item.Upd?.StackObjectsCount ?? 1); } } @@ -211,9 +215,11 @@ public class FenceService( double presetMultiplier ) { - foreach (var item in assort.Items) { + foreach (var item in assort.Items) + { // Skip sub-items when adjusting prices - if (item.SlotId != "hideout") { + if (item.SlotId != "hideout") + { continue; } @@ -229,15 +235,18 @@ public class FenceService( */ protected TraderAssort MergeAssorts(TraderAssort firstAssort, TraderAssort secondAssort) { - foreach (var itemId in secondAssort.BarterScheme.Keys) { + foreach (var itemId in secondAssort.BarterScheme.Keys) + { firstAssort.BarterScheme[itemId] = secondAssort.BarterScheme[itemId]; } - foreach (var item in secondAssort.Items) { + foreach (var item in secondAssort.Items) + { firstAssort.Items.Add(item); } - foreach (var itemId in secondAssort.LoyalLevelItems.Keys) { + foreach (var itemId in secondAssort.LoyalLevelItems.Keys) + { firstAssort.LoyalLevelItems[itemId] = secondAssort.LoyalLevelItems[itemId]; } @@ -259,13 +268,19 @@ public class FenceService( ) { // Is preset - if (item.Upd?.SptPresetId != null) { - if (assort.BarterScheme?[item.Id] != null) { + if (item.Upd?.SptPresetId != null) + { + if (assort.BarterScheme?[item.Id] != null) + { assort.BarterScheme[item.Id][0][0].Count *= presetModifier; } - } else if (assort.BarterScheme?[item.Id] != null) { + } + else if (assort.BarterScheme?[item.Id] != null) + { assort.BarterScheme[item.Id][0][0].Count *= modifier; - } else { + } + else + { logger.LogWarning($"adjustItemPriceByModifier() - no action taken for item: {item.Template}"); } } @@ -274,7 +289,8 @@ public class FenceService( * Get fence assorts with no price adjustments based on fence rep * @returns ITraderAssort */ - public TraderAssort GetRawFenceAssorts() { + public TraderAssort GetRawFenceAssorts() + { return MergeAssorts(cloner.Clone(fenceAssort), cloner.Clone(fenceDiscountAssort)); } @@ -320,22 +336,26 @@ public class FenceService( UpdateFenceAssorts(newDiscountItems, fenceDiscountAssort); // Add new barter items to fence barter scheme - foreach (var barterItemKey in newItems.BarterScheme.Keys) { + foreach (var barterItemKey in newItems.BarterScheme.Keys) + { fenceAssort.BarterScheme[barterItemKey] = newItems.BarterScheme[barterItemKey]; } // Add loyalty items to fence assorts loyalty object - foreach (var loyaltyItemKey in newItems.LoyalLevelItems.Keys) { + foreach (var loyaltyItemKey in newItems.LoyalLevelItems.Keys) + { fenceAssort.LoyalLevelItems[loyaltyItemKey] = newItems.LoyalLevelItems[loyaltyItemKey]; } // Add new barter items to fence assorts discounted barter scheme - foreach (var barterItemKey in newDiscountItems.BarterScheme.Keys) { + foreach (var barterItemKey in newDiscountItems.BarterScheme.Keys) + { fenceDiscountAssort.BarterScheme[barterItemKey] = newDiscountItems.BarterScheme[barterItemKey]; } // Add loyalty items to fence discount assorts loyalty object - foreach (var loyaltyItemKey in newDiscountItems.LoyalLevelItems.Keys) { + foreach (var loyaltyItemKey in newDiscountItems.LoyalLevelItems.Keys) + { fenceDiscountAssort.LoyalLevelItems[loyaltyItemKey] = newDiscountItems.LoyalLevelItems[loyaltyItemKey]; } @@ -353,21 +373,26 @@ public class FenceService( TraderAssort existingFenceAssorts ) { - foreach (var itemWithChildren in newFenceAssorts.SptItems) + foreach (var itemWithChildren in newFenceAssorts.SptItems) { // Find the root item var newRootItem = itemWithChildren.FirstOrDefault((item) => item.SlotId == "hideout"); - if (newRootItem == null) { + if (newRootItem == null) + { var firstItem = itemWithChildren.FirstOrDefault((x) => x != null); - logger.LogError($"Unable to process fence assort as root item is missing, {firstItem?.Template}, skipping"); + logger.LogError( + $"Unable to process fence assort as root item is missing, {firstItem?.Template}, skipping" + ); continue; } // Find a matching root item with same tpl in existing assort - var existingRootItem = existingFenceAssorts.Items.FirstOrDefault((item) => item.Template == newRootItem.Template && item.SlotId == "hideout"); + var existingRootItem = existingFenceAssorts.Items.FirstOrDefault( + (item) => item.Template == newRootItem.Template && item.SlotId == "hideout" + ); // Check if same type of item exists + its on list of item types to always stack - if (existingRootItem != null && ItemInPreventDupeCategoryList(newRootItem.Template)) + if (existingRootItem != null && ItemInPreventDupeCategoryList(newRootItem.Template)) { var existingFullItemTree = itemHelper.FindAndReturnChildrenAsItems( existingFenceAssorts.Items, @@ -376,7 +401,8 @@ public class FenceService( if (itemHelper.isSameItems(itemWithChildren, existingFullItemTree, fenceItemUpdCompareProperties)) { // Guard against a missing stack count - if (existingRootItem.Upd?.StackObjectsCount == null) { + if (existingRootItem.Upd?.StackObjectsCount == null) + { existingRootItem.Upd.StackObjectsCount = 1; } @@ -388,14 +414,15 @@ public class FenceService( } // if the Upd doesnt exist just initialize it - if (newRootItem.Upd == null) { + if (newRootItem.Upd == null) + { newRootItem.Upd = new(); } + // New assort to be added to existing assorts existingFenceAssorts.Items.AddRange(itemWithChildren); existingFenceAssorts.BarterScheme[newRootItem.Id] = newFenceAssorts.BarterScheme[newRootItem.Id]; existingFenceAssorts.LoyalLevelItems[newRootItem.Id] = newFenceAssorts.LoyalLevelItems[newRootItem.Id]; - } } @@ -422,10 +449,16 @@ public class FenceService( var rootPresetItems = allRootItems.Where((item) => item?.Upd?.SptPresetId != null); // Get count of weapons - var currentWeaponPresetCount = rootPresetItems.Aggregate(0, (count, item) => itemHelper.IsOfBaseclass(item.Template, BaseClasses.WEAPON) ? count + 1 : count); + var currentWeaponPresetCount = rootPresetItems.Aggregate( + 0, + (count, item) => itemHelper.IsOfBaseclass(item.Template, BaseClasses.WEAPON) ? count + 1 : count + ); // Get count of equipment - var currentEquipmentPresetCount = rootPresetItems.Aggregate(0, (count, item) => itemHelper.ArmorItemCanHoldMods(item.Template) ? count + 1 : count); + var currentEquipmentPresetCount = rootPresetItems.Aggregate( + 0, + (count, item) => itemHelper.ArmorItemCanHoldMods(item.Template) ? count + 1 : count + ); // Normal item count is total count minus weapon + armor count var nonPresetItemAssortCount = allRootItems.Count() - (currentWeaponPresetCount + currentEquipmentPresetCount); @@ -433,9 +466,13 @@ public class FenceService( // Get counts of items to generate, never var values fall below 0 var itemCountToGenerate = Math.Max(generationValues.Item.Value - nonPresetItemAssortCount, 0); var weaponCountToGenerate = Math.Max(generationValues.WeaponPreset.Value - currentWeaponPresetCount, 0); - var equipmentCountToGenerate = Math.Max(generationValues.EquipmentPreset.Value - currentEquipmentPresetCount, 0); + var equipmentCountToGenerate = Math.Max( + generationValues.EquipmentPreset.Value - currentEquipmentPresetCount, + 0 + ); - return new GenerationAssortValues { + return new GenerationAssortValues + { Item = itemCountToGenerate, WeaponPreset = weaponCountToGenerate, EquipmentPreset = equipmentCountToGenerate @@ -449,9 +486,11 @@ public class FenceService( */ protected void DeleteRandomAssorts(int itemCountToReplace, TraderAssort assort) { - if (assort?.Items?.Count > 0) { + if (assort?.Items?.Count > 0) + { var rootItems = assort.Items.Where((item) => item.SlotId == "hideout").ToList(); - for (var index = 0; index < itemCountToReplace; index++) { + for (var index = 0; index < itemCountToReplace; index++) + { RemoveRandomItemFromAssorts(assort, rootItems); } } @@ -470,15 +509,17 @@ public class FenceService( var stackSize = rootItemToAdjust.Upd?.StackObjectsCount ?? 1; // Get a random count of the chosen item to remove - var itemCountToRemove = randomUtil.GetInt(1, (int) stackSize); + var itemCountToRemove = randomUtil.GetInt(1, (int)stackSize); var isEntireStackToBeRemoved = Math.Abs(itemCountToRemove - stackSize) < 0.1; // Partial stack reduction - if (!isEntireStackToBeRemoved) { - if (rootItemToAdjust.Upd == null) { + if (!isEntireStackToBeRemoved) + { + if (rootItemToAdjust.Upd == null) + { logger.LogWarning($"Fence Item: {rootItemToAdjust.Template} lacks a Upd object, adding"); - rootItemToAdjust.Upd = new (); + rootItemToAdjust.Upd = new(); } // Reduce stack to at smallest, 1 @@ -489,7 +530,8 @@ public class FenceService( // Remove item + child mods (if any) var itemWithChildren = itemHelper.FindAndReturnChildrenAsItems(assort.Items, rootItemToAdjust.Id); - foreach (var itemToDelete in itemWithChildren) { + foreach (var itemToDelete in itemWithChildren) + { // Delete item from assort items array assort.Items.Splice(assort.Items.IndexOf(itemToDelete), 1); } @@ -506,9 +548,9 @@ public class FenceService( * @param totalItemCount total item count * @returns rounded int of items to replace */ - protected int GetCountOfItemsToReplace(int totalItemCount) + protected int GetCountOfItemsToReplace(int totalItemCount) { - return (int) Math.Round(totalItemCount * (traderConfig.Fence.PartialRefreshChangePercent / 100)); + return (int)Math.Round(totalItemCount * (traderConfig.Fence.PartialRefreshChangePercent / 100)); } /** @@ -517,7 +559,8 @@ public class FenceService( */ public int GetOfferCount() { - if ((fenceAssort?.Items?.Count ?? 0) == 0) { + if ((fenceAssort?.Items?.Count ?? 0) == 0) + { return 0; } @@ -557,7 +600,8 @@ public class FenceService( protected TraderAssort ConvertIntoFenceAssort(CreateFenceAssortsResult intermediaryAssorts) { var result = CreateFenceAssortSkeleton(); - foreach (var itemWithChilden in intermediaryAssorts.SptItems) { + foreach (var itemWithChilden in intermediaryAssorts.SptItems) + { result.Items.AddRange(itemWithChilden); } @@ -573,7 +617,8 @@ public class FenceService( */ protected void CreateInitialFenceAssortGenerationValues() { - var result = new FenceAssortGenerationValues() { + var result = new FenceAssortGenerationValues() + { Normal = new GenerationAssortValues() { Item = 0, WeaponPreset = 0, EquipmentPreset = 0 }, Discount = new GenerationAssortValues() { Item = 0, WeaponPreset = 0, EquipmentPreset = 0 } }; @@ -581,25 +626,25 @@ public class FenceService( result.Normal.Item = traderConfig.Fence.AssortSize; result.Normal.WeaponPreset = randomUtil.GetInt( - (int) traderConfig.Fence.WeaponPresetMinMax.Min, - (int) traderConfig.Fence.WeaponPresetMinMax.Max + (int)traderConfig.Fence.WeaponPresetMinMax.Min, + (int)traderConfig.Fence.WeaponPresetMinMax.Max ); result.Normal.EquipmentPreset = randomUtil.GetInt( - (int) traderConfig.Fence.EquipmentPresetMinMax.Min, - (int) traderConfig.Fence.EquipmentPresetMinMax.Max + (int)traderConfig.Fence.EquipmentPresetMinMax.Min, + (int)traderConfig.Fence.EquipmentPresetMinMax.Max ); result.Discount.Item = traderConfig.Fence.DiscountOptions.AssortSize; result.Discount.WeaponPreset = randomUtil.GetInt( - (int) traderConfig.Fence.DiscountOptions.WeaponPresetMinMax.Min, - (int) traderConfig.Fence.DiscountOptions.WeaponPresetMinMax.Max + (int)traderConfig.Fence.DiscountOptions.WeaponPresetMinMax.Min, + (int)traderConfig.Fence.DiscountOptions.WeaponPresetMinMax.Max ); result.Discount.EquipmentPreset = randomUtil.GetInt( - (int) traderConfig.Fence.DiscountOptions.EquipmentPresetMinMax.Min, - (int) traderConfig.Fence.DiscountOptions.EquipmentPresetMinMax.Max + (int)traderConfig.Fence.DiscountOptions.EquipmentPresetMinMax.Min, + (int)traderConfig.Fence.DiscountOptions.EquipmentPresetMinMax.Max ); desiredAssortCounts = result; @@ -611,7 +656,8 @@ public class FenceService( */ protected TraderAssort CreateFenceAssortSkeleton() { - return new TraderAssort() { + return new TraderAssort() + { Items = [], BarterScheme = new(), LoyalLevelItems = new(), @@ -631,11 +677,13 @@ public class FenceService( var baseFenceAssortClone = cloner.Clone(databaseService.GetTrader(Traders.FENCE).Assort); var itemTypeLimitCounts = InitItemLimitCounter(traderConfig.Fence.ItemTypeLimits); - if (itemCounts.Item > 0) { + if (itemCounts.Item > 0) + { AddItemAssorts(itemCounts.Item, result, baseFenceAssortClone, itemTypeLimitCounts, loyaltyLevel); } - if (itemCounts.WeaponPreset > 0 || itemCounts.EquipmentPreset > 0) { + if (itemCounts.WeaponPreset > 0 || itemCounts.EquipmentPreset > 0) + { // Add presets AddPresetsToAssort( itemCounts.WeaponPreset, @@ -666,23 +714,32 @@ public class FenceService( ) { var priceLimits = traderConfig.Fence.ItemCategoryRoublePriceLimit; - var assortRootItems = baseFenceAssortClone.Items.Where(item => item.ParentId == "hideout" && item.Upd?.SptPresetId == null).ToList(); - if (assortRootItems.Count == 0) { + var assortRootItems = baseFenceAssortClone.Items + .Where(item => item.ParentId == "hideout" && item.Upd?.SptPresetId == null) + .ToList(); + if (assortRootItems.Count == 0) + { logger.LogError("Unable to add assorts to Fence as no root items exist in items being added"); return; } - for (var i = 0; i < assortCount; i++) { + for (var i = 0; i < assortCount; i++) + { var chosenBaseAssortRoot = randomUtil.GetArrayValue(assortRootItems); - if (chosenBaseAssortRoot == null) { + if (chosenBaseAssortRoot == null) + { logger.LogError(localisationService.GetText("fence-unable_to_find_assort_by_id")); continue; } - var desiredAssortItemAndChildrenClone = cloner.Clone(itemHelper.FindAndReturnChildrenAsItems(baseFenceAssortClone.Items, chosenBaseAssortRoot.Id)); + + var desiredAssortItemAndChildrenClone = cloner.Clone( + itemHelper.FindAndReturnChildrenAsItems(baseFenceAssortClone.Items, chosenBaseAssortRoot.Id) + ); var itemDbDetails = itemHelper.GetItem(chosenBaseAssortRoot.Template).Value; var itemLimitCount = GetMatchingItemLimit(itemTypeLimits, itemDbDetails.Id); - if (itemLimitCount?.current >= itemLimitCount?.max) { + if (itemLimitCount?.current >= itemLimitCount?.max) + { // Skip adding item as assort as limit reached, decrement i counter so we still get another item i--; continue; @@ -691,20 +748,23 @@ public class FenceService( var itemIsPreset = presetHelper.IsPreset(chosenBaseAssortRoot.Id); var price = baseFenceAssortClone.BarterScheme?[chosenBaseAssortRoot.Id][0][0].Count; - if (price == 0 || (price == 1 && !itemIsPreset) || price == 100) { + if (price == 0 || (price == 1 && !itemIsPreset) || price == 100) + { // Don't allow "special" items / presets i--; continue; } - if (price > priceLimits[itemDbDetails.Parent]) { + if (price > priceLimits[itemDbDetails.Parent]) + { // Too expensive for fence, try another item i--; continue; } // Increment count as item is being added - if (itemLimitCount.HasValue) { + if (itemLimitCount.HasValue) + { var value = itemLimitCount.Value; value.current += 1; } @@ -720,14 +780,16 @@ public class FenceService( // Only randomise Upd values for single var isSingleStack = Math.Abs((rootItemBeingAdded.Upd?.StackObjectsCount ?? 0) - 1) < 0.1; - if (isSingleStack) { + if (isSingleStack) + { RandomiseItemUpdProperties(itemDbDetails, rootItemBeingAdded); } // Skip items already in the assort if it exists in the prevent duplicate list var existingItemThatMatches = GetMatchingItem(rootItemBeingAdded, itemDbDetails, assorts.SptItems); var shouldBeStacked = ItemShouldBeForceStacked(existingItemThatMatches, itemDbDetails); - if (shouldBeStacked && existingItemThatMatches != null) { + if (shouldBeStacked && existingItemThatMatches != null) + { // Decrement loop counter so another items gets added i--; existingItemThatMatches.Upd.StackObjectsCount++; @@ -736,16 +798,19 @@ public class FenceService( } // Add mods to armors so they dont show as red in the trade screen - if (itemHelper.ItemRequiresSoftInserts(rootItemBeingAdded.Template)) { + if (itemHelper.ItemRequiresSoftInserts(rootItemBeingAdded.Template)) + { RandomiseArmorModDurability(desiredAssortItemAndChildrenClone, itemDbDetails); } assorts.SptItems.Add(desiredAssortItemAndChildrenClone); - assorts.BarterScheme[rootItemBeingAdded.Id] = cloner.Clone(baseFenceAssortClone.BarterScheme[chosenBaseAssortRoot.Id]); + assorts.BarterScheme[rootItemBeingAdded.Id] = + cloner.Clone(baseFenceAssortClone.BarterScheme[chosenBaseAssortRoot.Id]); // Only adjust item price by quality for solo items, never multi-stack - if (isSingleStack) { + if (isSingleStack) + { AdjustItemPriceByQuality(assorts.BarterScheme, rootItemBeingAdded, itemDbDetails); } @@ -769,30 +834,49 @@ public class FenceService( { // Get matching root items var matchingItems = itemsWithChildren - .Where((itemWithChildren) => itemWithChildren.FirstOrDefault((item) => item.Template == rootItemBeingAdded.Template && item.ParentId == "hideout") != null).SelectMany(i => i).ToList(); - if (matchingItems.Count == 0) { + .Where( + (itemWithChildren) => itemWithChildren.FirstOrDefault( + (item) => item.Template == rootItemBeingAdded.Template && + item.ParentId == "hideout" + ) != + null + ) + .SelectMany(i => i) + .ToList(); + if (matchingItems.Count == 0) + { // Nothing matches by tpl and is root item, exit early return null; } - var isMedical = itemHelper.IsOfBaseclasses(rootItemBeingAdded.Template, [ - BaseClasses.MEDICAL, - BaseClasses.MEDKIT - ]); + var isMedical = itemHelper.IsOfBaseclasses( + rootItemBeingAdded.Template, + [ + BaseClasses.MEDICAL, + BaseClasses.MEDKIT + ] + ); var isGearAndHasSlots = - itemHelper.IsOfBaseclasses(rootItemBeingAdded.Template, [ - BaseClasses.ARMORED_EQUIPMENT, - BaseClasses.SEARCHABLE_ITEM - ]) && (itemDbDetails.Properties.Slots?.Count ?? 0) > 0; + itemHelper.IsOfBaseclasses( + rootItemBeingAdded.Template, + [ + BaseClasses.ARMORED_EQUIPMENT, + BaseClasses.SEARCHABLE_ITEM + ] + ) && + (itemDbDetails.Properties.Slots?.Count ?? 0) > 0; // Only one match and its not medical or armored gear - if (matchingItems.Count == 1 && !(isMedical || isGearAndHasSlots)) { + if (matchingItems.Count == 1 && !(isMedical || isGearAndHasSlots)) + { return matchingItems[0]; } // Items have sub properties that need to be checked against - foreach (var item in matchingItems) { - if (isMedical && rootItemBeingAdded.Upd?.MedKit?.HpResource == item.Upd?.MedKit?.HpResource) { + foreach (var item in matchingItems) + { + if (isMedical && rootItemBeingAdded.Upd?.MedKit?.HpResource == item.Upd?.MedKit?.HpResource) + { // e.g. bandages with multiple use // Both undefined === both max resoruce left return item; @@ -803,7 +887,8 @@ public class FenceService( isGearAndHasSlots && rootItemBeingAdded.Upd.Repairable?.Durability == item.Upd.Repairable?.Durability && rootItemBeingAdded.Upd.Repairable?.MaxDurability == item.Upd.Repairable?.MaxDurability - ) { + ) + { return item; } } @@ -820,19 +905,22 @@ public class FenceService( protected bool ItemShouldBeForceStacked(Item? existingItem, TemplateItem itemDbDetails) { // No existing item in assort - if (existingItem == null) { + if (existingItem == null) + { return false; } // Don't stack child items, only root items - if (existingItem.ParentId != "hideout") { + if (existingItem.ParentId != "hideout") + { return false; } return ItemInPreventDupeCategoryList(itemDbDetails.Id); } - protected bool ItemInPreventDupeCategoryList(string tpl) { + protected bool ItemInPreventDupeCategoryList(string tpl) + { // Item type in config list return itemHelper.IsOfBaseclasses(tpl, traderConfig.Fence.PreventDuplicateOffersOfCategory); } @@ -850,12 +938,14 @@ public class FenceService( ) { // Healing items - if (itemRoot.Upd?.MedKit != null) { + if (itemRoot.Upd?.MedKit != null) + { var itemTotalMax = itemTemplate.Properties.MaxHpResource; var current = itemRoot.Upd.MedKit.HpResource; // Current and max match, no adjustment necessary - if (itemTotalMax == current) { + if (itemTotalMax == current) + { return; } @@ -869,17 +959,23 @@ public class FenceService( } // Adjust price based on durability - if (itemRoot.Upd?.Repairable != null || itemHelper.IsOfBaseclass(itemRoot.Template, BaseClasses.KEY_MECHANICAL)) { + if (itemRoot.Upd?.Repairable != null || itemHelper.IsOfBaseclass(itemRoot.Template, BaseClasses.KEY_MECHANICAL)) + { var itemQualityModifier = itemHelper.GetItemQualityModifier(itemRoot); var basePrice = barterSchemes[itemRoot.Id][0][0].Count; - barterSchemes[itemRoot.Id][0][0].Count = Math.Round((double) basePrice * itemQualityModifier); + barterSchemes[itemRoot.Id][0][0].Count = Math.Round((double)basePrice * itemQualityModifier); } } - protected (int current, int max)? GetMatchingItemLimit(Dictionary itemTypeLimits, string itemTpl) + protected (int current, int max)? GetMatchingItemLimit( + Dictionary itemTypeLimits, + string itemTpl + ) { - foreach (var baseTypeKey in itemTypeLimits.Keys) { - if (itemHelper.IsOfBaseclass(itemTpl, baseTypeKey)) { + foreach (var baseTypeKey in itemTypeLimits.Keys) + { + if (itemHelper.IsOfBaseclass(itemTpl, baseTypeKey)) + { return itemTypeLimits[baseTypeKey]; } } @@ -903,17 +999,24 @@ public class FenceService( ) { var weaponPresetsAddedCount = 0; - if (desiredWeaponPresetsCount > 0) { - var weaponPresetRootItems = baseFenceAssort.Items.Where(item => item.Upd?.SptPresetId != null && itemHelper.IsOfBaseclass(item.Template, BaseClasses.WEAPON)); - while (weaponPresetsAddedCount < desiredWeaponPresetsCount) { + if (desiredWeaponPresetsCount > 0) + { + var weaponPresetRootItems = baseFenceAssort.Items.Where( + item => item.Upd?.SptPresetId != null && itemHelper.IsOfBaseclass(item.Template, BaseClasses.WEAPON) + ); + while (weaponPresetsAddedCount < desiredWeaponPresetsCount) + { var randomPresetRoot = randomUtil.GetArrayValue(weaponPresetRootItems); - if (traderConfig.Fence.Blacklist.Contains(randomPresetRoot.Template)) { + if (traderConfig.Fence.Blacklist.Contains(randomPresetRoot.Template)) + { continue; } var rootItemDb = itemHelper.GetItem(randomPresetRoot.Template).Value; - var presetWithChildrenClone = cloner.Clone(itemHelper.FindAndReturnChildrenAsItems(baseFenceAssort.Items, randomPresetRoot.Id)); + var presetWithChildrenClone = cloner.Clone( + itemHelper.FindAndReturnChildrenAsItems(baseFenceAssort.Items, randomPresetRoot.Id) + ); RandomiseItemUpdProperties(rootItemDb, presetWithChildrenClone[0]); @@ -924,8 +1027,10 @@ public class FenceService( var itemPrice = handbookHelper.GetTemplatePriceForItems(presetWithChildrenClone) * itemHelper.GetItemQualityModifierForItems(presetWithChildrenClone); - if (priceLimitRouble != null) { - if (itemPrice > priceLimitRouble) { + if (priceLimitRouble != null) + { + if (itemPrice > priceLimitRouble) + { // Too expensive, try again continue; } @@ -942,9 +1047,11 @@ public class FenceService( // Set assort price // Must be careful to use correct id as the item has had its IDs regenerated - assorts.BarterScheme[presetWithChildrenClone[0].Id] = [ + assorts.BarterScheme[presetWithChildrenClone[0].Id] = + [ [ - new BarterScheme() { + new BarterScheme() + { Template = Money.ROUBLES, Count = Math.Round(itemPrice), } @@ -957,16 +1064,22 @@ public class FenceService( } var equipmentPresetsAddedCount = 0; - if (desiredEquipmentPresetsCount <= 0) { + if (desiredEquipmentPresetsCount <= 0) + { return; } - var equipmentPresetRootItems = baseFenceAssort.Items.Where((item) => item.Upd?.SptPresetId != null && itemHelper.ArmorItemCanHoldMods(item.Template)); - while (equipmentPresetsAddedCount < desiredEquipmentPresetsCount) { + var equipmentPresetRootItems = baseFenceAssort.Items.Where( + (item) => item.Upd?.SptPresetId != null && itemHelper.ArmorItemCanHoldMods(item.Template) + ); + while (equipmentPresetsAddedCount < desiredEquipmentPresetsCount) + { var randomPresetRoot = randomUtil.GetArrayValue(equipmentPresetRootItems); var rootItemDb = itemHelper.GetItem(randomPresetRoot.Template).Value; - var presetWithChildrenClone = cloner.Clone(itemHelper.FindAndReturnChildrenAsItems(baseFenceAssort.Items, randomPresetRoot.Id)); + var presetWithChildrenClone = cloner.Clone( + itemHelper.FindAndReturnChildrenAsItems(baseFenceAssort.Items, randomPresetRoot.Id) + ); // Need to add mods to armors so they dont show as red in the trade screen if (itemHelper.ItemRequiresSoftInserts(randomPresetRoot.Template)) @@ -981,8 +1094,10 @@ public class FenceService( var itemPrice = handbookHelper.GetTemplatePriceForItems(presetWithChildrenClone) * itemHelper.GetItemQualityModifierForItems(presetWithChildrenClone); - if (priceLimitRouble != null) { - if (itemPrice > priceLimitRouble) { + if (priceLimitRouble != null) + { + if (itemPrice > priceLimitRouble) + { // Too expensive, try again continue; } @@ -999,9 +1114,11 @@ public class FenceService( // Set assort price // Must be careful to use correct id as the item has had its IDs regenerated - assorts.BarterScheme[presetWithChildrenClone[0].Id] = [ + assorts.BarterScheme[presetWithChildrenClone[0].Id] = + [ [ - new BarterScheme(){ + new BarterScheme() + { Template = Money.ROUBLES, Count = Math.Round(itemPrice), } @@ -1021,19 +1138,23 @@ public class FenceService( protected void RandomiseArmorModDurability(List armor, TemplateItem itemDbDetails) { // Armor has no mods, nothing to randomise - if (itemDbDetails.Properties.Slots == null) { + if (itemDbDetails.Properties.Slots == null) + { return; } // Check for and adjust soft insert durability values var requiredSlots = itemDbDetails.Properties.Slots?.Where(slot => slot.Required ?? false).ToList(); - if ((requiredSlots?.Count ?? 0) > 1) { + if ((requiredSlots?.Count ?? 0) > 1) + { RandomiseArmorSoftInsertDurabilities(requiredSlots, armor); } // Check for and adjust plate durability values - var plateSlots = itemDbDetails.Properties.Slots?.Where(slot => itemHelper.IsRemovablePlateSlot(slot.Name)).ToList(); - if ((plateSlots?.Count ?? 0) > 1) { + var plateSlots = itemDbDetails.Properties.Slots?.Where(slot => itemHelper.IsRemovablePlateSlot(slot.Name)) + .ToList(); + if ((plateSlots?.Count ?? 0) > 1) + { RandomiseArmorInsertsDurabilities(plateSlots, armor); } } @@ -1045,26 +1166,37 @@ public class FenceService( */ protected void RandomiseArmorSoftInsertDurabilities(List softInsertSlots, List armorItemAndMods) { - foreach (var requiredSlot in softInsertSlots) { + foreach (var requiredSlot in softInsertSlots) + { var modItemDbDetails = itemHelper.GetItem(requiredSlot.Props.Filters[0].Plate).Value; - var durabilityValues = GetRandomisedArmorDurabilityValues(modItemDbDetails, traderConfig.Fence.ArmorMaxDurabilityPercentMinMax); - var plateTpl = requiredSlot.Props.Filters[0].Plate ?? string.Empty; // "Plate" property appears to be the 'default' item for slot - if (plateTpl == "") { + var durabilityValues = GetRandomisedArmorDurabilityValues( + modItemDbDetails, + traderConfig.Fence.ArmorMaxDurabilityPercentMinMax + ); + var plateTpl = + requiredSlot.Props.Filters[0].Plate ?? + string.Empty; // "Plate" property appears to be the 'default' item for slot + if (plateTpl == "") + { // Some bsg plate properties are empty, skip mod continue; } // Find items mod to apply dura changes to - var modItemToAdjust = armorItemAndMods.FirstOrDefault(mod => mod.SlotId.ToLower() == requiredSlot.Name.ToLower()); + var modItemToAdjust = + armorItemAndMods.FirstOrDefault(mod => mod.SlotId.ToLower() == requiredSlot.Name.ToLower()); itemHelper.AddUpdObjectToItem(modItemToAdjust); - if (modItemToAdjust.Upd.Repairable == null) { - modItemToAdjust.Upd.Repairable = new UpdRepairable(){ + if (modItemToAdjust.Upd.Repairable == null) + { + modItemToAdjust.Upd.Repairable = new UpdRepairable() + { Durability = modItemDbDetails.Properties.MaxDurability, MaxDurability = modItemDbDetails.Properties.MaxDurability }; } + modItemToAdjust.Upd.Repairable.Durability = durabilityValues.Durability; modItemToAdjust.Upd.Repairable.MaxDurability = durabilityValues.MaxDurability; @@ -1075,7 +1207,7 @@ public class FenceService( modItemToAdjust.Upd.Repairable.Durability < modItemDbDetails.Properties.MaxDurability) { // Is damaged - modItemToAdjust.Upd.FaceShield = new UpdFaceShield() { Hits = randomUtil.GetInt(1, 3) }; + modItemToAdjust.Upd.FaceShield = new UpdFaceShield() { Hits = randomUtil.GetInt(1, 3) }; } } } @@ -1088,9 +1220,11 @@ public class FenceService( */ protected void RandomiseArmorInsertsDurabilities(List plateSlots, List armorItemAndMods) { - foreach (var plateSlot in plateSlots) { + foreach (var plateSlot in plateSlots) + { var plateTpl = plateSlot.Props.Filters[0].Plate; - if (plateTpl == null) { + if (plateTpl == null) + { // Bsg data lacks a default plate, skip randomisng for this mod continue; } @@ -1100,10 +1234,14 @@ public class FenceService( var modItemDbDetails = itemHelper.GetItem(plateTpl).Value; // Chance to remove plate - var plateExistsChance = traderConfig.Fence.ChancePlateExistsInArmorPercent[modItemDbDetails?.Properties?.ArmorClass?.ToString() ?? "3"]; - if (!randomUtil.GetChance100(plateExistsChance)) { + var plateExistsChance = + traderConfig.Fence.ChancePlateExistsInArmorPercent[ + modItemDbDetails?.Properties?.ArmorClass?.ToString() ?? "3"]; + if (!randomUtil.GetChance100(plateExistsChance)) + { // Remove plate from armor - armorWithMods = armorItemAndMods.Where(item => item.SlotId.ToLower() != plateSlot.Name.ToLower()).ToList(); + armorWithMods = armorItemAndMods.Where(item => item.SlotId.ToLower() != plateSlot.Name.ToLower()) + .ToList(); continue; } @@ -1116,15 +1254,20 @@ public class FenceService( // Find items mod to apply dura changes to var modItemToAdjust = armorWithMods.FirstOrDefault(mod => mod.SlotId.ToLower() == plateSlot.Name.ToLower()); - if (modItemToAdjust == null) { - logger.LogWarning($"Unable to randomise armor items {armorWithMods[0].Template} ${plateSlot.Name} slot as it cannot be found, skipping"); + if (modItemToAdjust == null) + { + logger.LogWarning( + $"Unable to randomise armor items {armorWithMods[0].Template} ${plateSlot.Name} slot as it cannot be found, skipping" + ); continue; } itemHelper.AddUpdObjectToItem(modItemToAdjust); - if (modItemToAdjust?.Upd?.Repairable == null) { - modItemToAdjust.Upd.Repairable = new UpdRepairable(){ + if (modItemToAdjust?.Upd?.Repairable == null) + { + modItemToAdjust.Upd.Repairable = new UpdRepairable() + { Durability = modItemDbDetails.Properties.MaxDurability, MaxDurability = modItemDbDetails.Properties.MaxDurability }; @@ -1143,28 +1286,35 @@ public class FenceService( protected int GetSingleItemStackCount(TemplateItem itemDbDetails) { MinMax? overrideValues; - if (itemHelper.IsOfBaseclass(itemDbDetails.Id, BaseClasses.AMMO)) { + if (itemHelper.IsOfBaseclass(itemDbDetails.Id, BaseClasses.AMMO)) + { overrideValues = traderConfig.Fence.ItemStackSizeOverrideMinMax[itemDbDetails.Parent]; - if (overrideValues != null) { - return randomUtil.GetInt((int) overrideValues.Min, (int) overrideValues.Max); + if (overrideValues != null) + { + return randomUtil.GetInt((int)overrideValues.Min, (int)overrideValues.Max); } // No override, use stack max size from item db return itemDbDetails.Properties.StackMaxSize == 1 ? 1 - : randomUtil.GetInt((int) itemDbDetails.Properties.StackMinRandom, (int) itemDbDetails.Properties.StackMaxRandom); + : randomUtil.GetInt( + (int)itemDbDetails.Properties.StackMinRandom, + (int)itemDbDetails.Properties.StackMaxRandom + ); } // Check for override in config, use values if exists overrideValues = traderConfig.Fence.ItemStackSizeOverrideMinMax[itemDbDetails.Id]; - if (overrideValues != null) { - return randomUtil.GetInt((int) overrideValues.Min, (int) overrideValues.Max); + if (overrideValues != null) + { + return randomUtil.GetInt((int)overrideValues.Min, (int)overrideValues.Max); } // Check for parent override overrideValues = traderConfig.Fence.ItemStackSizeOverrideMinMax[itemDbDetails.Parent]; - if (overrideValues != null) { - return randomUtil.GetInt((int) overrideValues.Min, (int) overrideValues.Max); + if (overrideValues != null) + { + return randomUtil.GetInt((int)overrideValues.Min, (int)overrideValues.Max); } return 1; @@ -1180,11 +1330,14 @@ public class FenceService( var toDelete = new List(); // Find mods to remove from item that could've been scavenged by other players in-raid - foreach (var itemMod in itemAndMods) { - if (PresetModItemWillBeRemoved(itemMod, toDelete)) { + foreach (var itemMod in itemAndMods) + { + if (PresetModItemWillBeRemoved(itemMod, toDelete)) + { // Skip if not an item var itemDbDetails = itemHelper.GetItem(itemMod.Template); - if (!itemDbDetails.Key) { + if (!itemDbDetails.Key) + { continue; } @@ -1194,8 +1347,10 @@ public class FenceService( } // Reverse loop and remove items - for (var index = itemAndMods.Count - 1; index >= 0; --index) { - if (toDelete.Contains(itemAndMods[index].Id)) { + for (var index = itemAndMods.Count - 1; index >= 0; --index) + { + if (toDelete.Contains(itemAndMods[index].Id)) + { itemAndMods.Splice(index, 1); } } @@ -1211,7 +1366,8 @@ public class FenceService( { var slotIdsThatCanFail = traderConfig.Fence.PresetSlotsToRemoveChancePercent; var removalChance = slotIdsThatCanFail[weaponMod.SlotId]; - if (removalChance is null or 0.0) { + if (removalChance is null or 0.0) + { return false; } @@ -1228,54 +1384,65 @@ public class FenceService( */ protected void RandomiseItemUpdProperties(TemplateItem itemDetails, Item itemToAdjust) { - if (itemDetails.Properties == null) { - logger.LogError($"Item {itemDetails.Name} lacks a _props field, unable to randomise item: {itemToAdjust.Id}"); + if (itemDetails.Properties == null) + { + logger.LogError( + $"Item {itemDetails.Name} lacks a _props field, unable to randomise item: {itemToAdjust.Id}" + ); return; } // Randomise hp resource of med items - if (itemDetails.Properties.MaxHpResource != null && (itemDetails.Properties.MaxHpResource ?? 0) > 0) { - itemToAdjust.Upd.MedKit = new UpdMedKit() { HpResource = randomUtil.GetInt(1, (int) itemDetails.Properties.MaxHpResource) }; + if (itemDetails.Properties.MaxHpResource != null && (itemDetails.Properties.MaxHpResource ?? 0) > 0) + { + itemToAdjust.Upd.MedKit = new UpdMedKit() + { HpResource = randomUtil.GetInt(1, (int)itemDetails.Properties.MaxHpResource) }; } // Randomise armor durability if ( (itemDetails.Parent == BaseClasses.ARMORED_EQUIPMENT || - itemDetails.Parent == BaseClasses.FACECOVER || - itemDetails.Parent == BaseClasses.ARMOR_PLATE) && + itemDetails.Parent == BaseClasses.FACECOVER || + itemDetails.Parent == BaseClasses.ARMOR_PLATE) && (itemDetails.Properties.MaxDurability ?? 0) > 0 - ) { + ) + { var values = GetRandomisedArmorDurabilityValues( itemDetails, traderConfig.Fence.ArmorMaxDurabilityPercentMinMax ); - itemToAdjust.Upd.Repairable = new UpdRepairable() { Durability = values.Durability, MaxDurability= values.MaxDurability }; + itemToAdjust.Upd.Repairable = new UpdRepairable() + { Durability = values.Durability, MaxDurability = values.MaxDurability }; return; } // Randomise Weapon durability - if (itemHelper.IsOfBaseclass(itemDetails.Id, BaseClasses.WEAPON)) { + if (itemHelper.IsOfBaseclass(itemDetails.Id, BaseClasses.WEAPON)) + { var weaponDurabilityLimits = traderConfig.Fence.WeaponDurabilityPercentMinMax; var maxDuraMin = (weaponDurabilityLimits.Max.Min / 100) * itemDetails.Properties.MaxDurability; var maxDuraMax = (weaponDurabilityLimits.Max.Max / 100) * itemDetails.Properties.MaxDurability; - var chosenMaxDurability = randomUtil.GetInt((int) maxDuraMin, (int) maxDuraMax); + var chosenMaxDurability = randomUtil.GetInt((int)maxDuraMin, (int)maxDuraMax); var currentDuraMin = (weaponDurabilityLimits.Current.Min / 100) * itemDetails.Properties.MaxDurability; var currentDuraMax = (weaponDurabilityLimits.Current.Max / 100) * itemDetails.Properties.MaxDurability; var currentDurability = Math.Min( - randomUtil.GetInt((int) currentDuraMin, (int) currentDuraMax), + randomUtil.GetInt((int)currentDuraMin, (int)currentDuraMax), chosenMaxDurability ); - itemToAdjust.Upd.Repairable = new UpdRepairable { Durability = currentDurability, MaxDurability = chosenMaxDurability }; + itemToAdjust.Upd.Repairable = new UpdRepairable + { Durability = currentDurability, MaxDurability = chosenMaxDurability }; return; } - if (itemHelper.IsOfBaseclass(itemDetails.Id, BaseClasses.REPAIR_KITS)) { - itemToAdjust.Upd.RepairKit = new UpdRepairKit { - Resource = randomUtil.GetInt(1, (int) itemDetails.Properties.MaxRepairResource), + if (itemHelper.IsOfBaseclass(itemDetails.Id, BaseClasses.REPAIR_KITS)) + { + itemToAdjust.Upd.RepairKit = new UpdRepairKit + { + Resource = randomUtil.GetInt(1, (int)itemDetails.Properties.MaxRepairResource), }; return; @@ -1283,20 +1450,24 @@ public class FenceService( // Mechanical key + has limited uses if (itemHelper.IsOfBaseclass(itemDetails.Id, BaseClasses.KEY_MECHANICAL) && - (itemDetails.Properties.MaximumNumberOfUsage ?? 0) > 1) { - itemToAdjust.Upd.Key = new UpdKey { - NumberOfUsages = randomUtil.GetInt(0, (int) itemDetails.Properties.MaximumNumberOfUsage - 1), + (itemDetails.Properties.MaximumNumberOfUsage ?? 0) > 1) + { + itemToAdjust.Upd.Key = new UpdKey + { + NumberOfUsages = randomUtil.GetInt(0, (int)itemDetails.Properties.MaximumNumberOfUsage - 1), }; return; } // Randomise items that use resources (e.g. fuel) - if ((itemDetails.Properties.MaxResource ?? 0) > 0) { + if ((itemDetails.Properties.MaxResource ?? 0) > 0) + { var resourceMax = itemDetails.Properties.MaxResource; - var resourceCurrent = randomUtil.GetInt(1, (int) itemDetails.Properties.MaxResource); + var resourceCurrent = randomUtil.GetInt(1, (int)itemDetails.Properties.MaxResource); - itemToAdjust.Upd.Resource = new UpdResource { Value = resourceMax - resourceCurrent, UnitsConsumed = resourceCurrent }; + itemToAdjust.Upd.Resource = new UpdResource + { Value = resourceMax - resourceCurrent, UnitsConsumed = resourceCurrent }; } } @@ -1313,12 +1484,12 @@ public class FenceService( { var maxDuraMin = (equipmentDurabilityLimits.Max.Min / 100) * itemDetails.Properties.MaxDurability; var maxDuraMax = (equipmentDurabilityLimits.Max.Max / 100) * itemDetails.Properties.MaxDurability; - var chosenMaxDurability = randomUtil.GetInt((int) maxDuraMin, (int) maxDuraMax); + var chosenMaxDurability = randomUtil.GetInt((int)maxDuraMin, (int)maxDuraMax); var currentDuraMin = (equipmentDurabilityLimits.Current.Min / 100) * itemDetails.Properties.MaxDurability; var currentDuraMax = (equipmentDurabilityLimits.Current.Max / 100) * itemDetails.Properties.MaxDurability; var chosenCurrentDurability = Math.Min( - randomUtil.GetInt((int) currentDuraMin, (int) currentDuraMax), + randomUtil.GetInt((int)currentDuraMin, (int)currentDuraMax), chosenMaxDurability ); @@ -1334,8 +1505,9 @@ public class FenceService( { var itemTypeCounts = new Dictionary(); - foreach (var x in limits.Keys) { - itemTypeCounts[x] = new () { current = 0, max = limits[x] }; + foreach (var x in limits.Keys) + { + itemTypeCounts[x] = new() { current = 0, max = limits[x] }; } return itemTypeCounts; @@ -1356,10 +1528,11 @@ public class FenceService( * Get fence refresh time in seconds * @returns Refresh time in seconds */ - protected int GetFenceRefreshTime() { + protected int GetFenceRefreshTime() + { var fence = traderConfig.UpdateTime.FirstOrDefault((x) => x.TraderId == Traders.FENCE).Seconds; - return randomUtil.GetInt((int) fence.Min, (int) fence.Max); + return randomUtil.GetInt((int)fence.Min, (int)fence.Max); } /** @@ -1372,20 +1545,23 @@ public class FenceService( var fenceSettings = databaseService.GetGlobals().Configuration.FenceSettings; var pmcFenceInfo = pmcData.TradersInfo[fenceSettings.FenceIdentifier]; - if (pmcFenceInfo == null) { + if (pmcFenceInfo == null) + { return fenceSettings.Levels["0"]; } var fenceLevels = fenceSettings.Levels.Keys.Select(int.Parse); var minLevel = fenceLevels.Min(); var maxLevel = fenceLevels.Max(); - var pmcFenceLevel = Math.Floor((double) pmcFenceInfo.Standing); + var pmcFenceLevel = Math.Floor((double)pmcFenceInfo.Standing); - if (pmcFenceLevel < minLevel) { + if (pmcFenceLevel < minLevel) + { return fenceSettings.Levels[minLevel.ToString()]; } - if (pmcFenceLevel > maxLevel) { + if (pmcFenceLevel > maxLevel) + { return fenceSettings.Levels[maxLevel.ToString()]; } @@ -1401,19 +1577,23 @@ public class FenceService( { var isNormalAssort = true; var fenceAssortItem = fenceAssort.Items.FirstOrDefault((item) => item.Id == assortId); - if (fenceAssortItem == null) { + if (fenceAssortItem == null) + { // Not in main assorts, check secondary section fenceAssortItem = fenceDiscountAssort.Items.FirstOrDefault((item) => item.Id == assortId); - if (fenceAssortItem == null) { + if (fenceAssortItem == null) + { logger.LogError(localisationService.GetText("fence-unable_to_find_offer_by_id", assortId)); return; } + isNormalAssort = false; } // Player wants to buy whole stack, delete stack - if ((fenceAssortItem.Upd?.StackObjectsCount ?? 0) == buyCount) { + if ((fenceAssortItem.Upd?.StackObjectsCount ?? 0) == buyCount) + { DeleteOffer(assortId, isNormalAssort ? fenceAssort.Items : fenceDiscountAssort.Items); return; } @@ -1426,16 +1606,21 @@ public class FenceService( { // Assort could have child items, remove those too var itemWithChildrenToRemove = itemHelper.FindAndReturnChildrenAsItems(assorts, assortId); - foreach (var itemToRemove in itemWithChildrenToRemove) { + foreach (var itemToRemove in itemWithChildrenToRemove) + { var indexToRemove = assorts.FindIndex((item) => item.Id == itemToRemove.Id); // No offer found in main assort, check discount items - if (indexToRemove == -1) { + if (indexToRemove == -1) + { indexToRemove = fenceDiscountAssort.Items.FindIndex((item) => item.Id == itemToRemove.Id); fenceDiscountAssort.Items.Splice(indexToRemove, 1); - if (indexToRemove == -1) { - logger.LogWarning($"unable to remove fence assort item: {itemToRemove.Id} tpl: {itemToRemove.Template}"); + if (indexToRemove == -1) + { + logger.LogWarning( + $"unable to remove fence assort item: {itemToRemove.Id} tpl: {itemToRemove.Template}" + ); } return;