From 6b297adf6833894daf54488bbaa85ae630a5e355 Mon Sep 17 00:00:00 2001 From: Chomp Date: Wed, 6 Aug 2025 17:52:49 +0100 Subject: [PATCH] Replaced `ProbabilityObjectArray.Draw()` with `DrawAndRemove` `Draw` Reduced overhead when drawing a large number of elements during loot generation --- .../Controllers/InsuranceController.cs | 2 +- .../Generators/LocationLootGenerator.cs | 13 +- .../EliminationQuestGenerator.cs | 6 +- .../Collections/ProbabilityObjectArray.cs | 113 ++++++++++++------ 4 files changed, 86 insertions(+), 48 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Controllers/InsuranceController.cs b/Libraries/SPTarkov.Server.Core/Controllers/InsuranceController.cs index a7a520d9..eeddb872 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/InsuranceController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/InsuranceController.cs @@ -482,7 +482,7 @@ public class InsuranceController( } // Draw x attachments from weighted array to remove from parent, remove from pool after being picked - var attachmentIdsToRemove = attachmentsProbabilityArray.Draw((int)countOfAttachmentsToRemove, false); + var attachmentIdsToRemove = attachmentsProbabilityArray.DrawAndRemove((int)countOfAttachmentsToRemove); foreach (var attachmentId in attachmentIdsToRemove) { toDelete.Add(attachmentId); diff --git a/Libraries/SPTarkov.Server.Core/Generators/LocationLootGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/LocationLootGenerator.cs index 187ddd2f..0f9539de 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/LocationLootGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/LocationLootGenerator.cs @@ -460,10 +460,11 @@ public class LocationLootGenerator( // Choose items to add to container, factor in weighting + lock money down // Filter out items picked that are already in the above `tplsForced` array - var chosenTpls = containerLootPool - .Draw(itemCountToAdd, _locationConfig.AllowDuplicateItemsInStaticContainers, lockList) - .Where(tpl => !tplsForced.Contains(tpl)) - .Where(tpl => !counterTrackerHelper.IncrementCount(tpl)); + var chosenTpls = _locationConfig.AllowDuplicateItemsInStaticContainers + ? containerLootPool.Draw(itemCountToAdd).Where(tpl => !tplsForced.Contains(tpl) && !counterTrackerHelper.IncrementCount(tpl)) + : containerLootPool + .DrawAndRemove(itemCountToAdd, lockList) + .Where(tpl => !tplsForced.Contains(tpl) && !counterTrackerHelper.IncrementCount(tpl)); // Add forced loot to chosen item pool var tplsToAddToContainer = tplsForced.Concat(chosenTpls); @@ -710,9 +711,9 @@ public class LocationLootGenerator( var randomSpawnPointCount = desiredSpawnPointCount - chosenSpawnPoints.Count; // Only draw random spawn points if needed if (randomSpawnPointCount > 0 && spawnPointArray.Count > 0) - // Add randomly chosen spawn points + // Add randomly chosen spawn points, remove from pool after being picked { - foreach (var si in spawnPointArray.Draw((int)randomSpawnPointCount, false)) + foreach (var si in spawnPointArray.DrawAndRemove((int)randomSpawnPointCount)) { chosenSpawnPoints.Add(spawnPointArray.Data(si)); } diff --git a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/EliminationQuestGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/EliminationQuestGenerator.cs index ae1da806..348017b7 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/EliminationQuestGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/EliminationQuestGenerator.cs @@ -415,7 +415,7 @@ public class EliminationQuestGenerator( // e.g. we draw "Arms" from the probability array but must present ["LeftArm", "RightArm"] to the client var bodyPartsToClient = new List(); - var bodyParts = generationData.BodyPartsConfig.Draw(randomUtil.RandInt(1, 3), false); + var bodyParts = generationData.BodyPartsConfig.DrawAndRemove(randomUtil.RandInt(1, 3)); var probability = 0d; @@ -555,7 +555,7 @@ public class EliminationQuestGenerator( } // Pick a weighted weapon category - var weaponRequirement = generationData.WeaponCategoryRequirementConfig.Draw(1, false); + var weaponRequirement = generationData.WeaponCategoryRequirementConfig.DrawAndRemove(); // Get the hideout id value stored in the .data array return generationData.WeaponCategoryRequirementConfig.Data(weaponRequirement[0])?[0]; @@ -568,7 +568,7 @@ public class EliminationQuestGenerator( /// Weapon to use protected MongoId GenerateSpecificWeaponRequirement(EliminationQuestGenerationData generationData) { - var weaponRequirement = generationData.WeaponRequirementConfig.Draw(1, false); + var weaponRequirement = generationData.WeaponRequirementConfig.DrawAndRemove(); var specificAllowedWeaponCategory = generationData.WeaponRequirementConfig.Data(weaponRequirement[0]); if (specificAllowedWeaponCategory?[0] is null) diff --git a/Libraries/SPTarkov.Server.Core/Utils/Collections/ProbabilityObjectArray.cs b/Libraries/SPTarkov.Server.Core/Utils/Collections/ProbabilityObjectArray.cs index 2bb7d785..670de92c 100644 --- a/Libraries/SPTarkov.Server.Core/Utils/Collections/ProbabilityObjectArray.cs +++ b/Libraries/SPTarkov.Server.Core/Utils/Collections/ProbabilityObjectArray.cs @@ -35,13 +35,13 @@ public class ProbabilityObjectArray : List> /// /// The relative probability values of which to calculate the normalized cumulative sum /// Cumulative Sum normalized to 1 - public List CumulativeProbability(IEnumerable probValues) + public IEnumerable CumulativeProbability(IEnumerable probValues) { var sum = probValues.Sum(); var probCumsum = probValues.CumulativeSum(); probCumsum = probCumsum.Product(1D / sum); - return probCumsum.ToList(); + return probCumsum; } /// @@ -136,59 +136,96 @@ public class ProbabilityObjectArray : List> } /// - /// Draw random element of the ProbabilityObject N times to return an array of N keys. - /// Drawing can be with or without replacement + ///Draw random element of the ProbabilityObject N times to return an array of N keys + /// Keeps chosen element in place + /// Chosen items can be duplicates /// - /// The number of times we want to draw - /// Draw with or without replacement from the input dict (true = don't remove after drawing) - /// List of keys which shall be replaced even if drawing without replacement + /// The number of times we want to draw /// Collection consisting of N random keys for this ProbabilityObjectArray - public List Draw(int drawCount = 1, bool removeAfterDraw = true, List? neverRemoveWhitelist = null) + public List Draw(int itemCountToDraw = 1) { - neverRemoveWhitelist ??= []; if (Count == 0) { + // Nothing in pool return []; } - var totals = this.Aggregate( - new { probArray = new List(), keyArray = new List() }, - (acc, x) => - { - acc.probArray.Add(x.RelativeProbability.Value); - acc.keyArray.Add(x.Key); - return acc; - } - ); + var cumulativeProbabilities = CumulativeProbability(this.Select(x => x.RelativeProbability.Value)).ToList(); - var probCumsum = CumulativeProbability(totals.probArray); + // Init results collection + var results = new List(itemCountToDraw); - var drawnKeys = new List(); - for (var i = 0; i < drawCount; i++) + // Loop until we've picked to desired item count + for (var i = 0; i < itemCountToDraw; i++) { var rand = Random.Shared.NextDouble(); - var randomIndex = probCumsum.FindIndex(x => x > rand); - // We cannot put Math.random() directly in the findIndex because then it draws anew for each of its iteration - if (removeAfterDraw || neverRemoveWhitelist.Contains(totals.keyArray[randomIndex])) + var randomIndex = cumulativeProbabilities.FindIndex(probability => probability > rand); + results.Add(this[randomIndex].Key); + } + + return results; + } + + /// + ///Draw random element of the ProbabilityObject N times to return an array of N keys + /// Removes drawn elements + /// + /// The number of times we want to draw + /// List of keys which shall be replaced even if drawing without replacement + /// Collection consisting of N random keys for this ProbabilityObjectArray + public List DrawAndRemove(int itemCountToDraw = 1, List? neverRemoveWhitelist = null) + { + if (Count == 0) + { + // Nothing in pool + return []; + } + + var availableItems = this.Select(x => (x.Key, Weight: x.RelativeProbability.Value)).ToList(); + + // Calculate total weighting of all items combined + var totalWeight = availableItems.Sum(x => x.Weight); + + // Init results collection + var drawnKeys = new List(itemCountToDraw); + + // Loop until we have drawn to desired count or pool is empty + for (var i = 0; i < itemCountToDraw && availableItems.Any(); i++) + { + // Get value between 0 and 1 to act as a target to aim for + var randomTarget = Random.Shared.NextDouble() * totalWeight; + + // Set default index to start + var chosenIndex = -1; + + // Find element related to random target (greedy) + for (var j = 0; j < availableItems.Count; j++) { - // Add random item from possible value into return array - drawnKeys.Add(totals.keyArray[randomIndex]); - } - else - { - // We draw without replacement -> remove the key and its probability from array - var key = totals.keyArray[randomIndex]; - totals.keyArray.RemoveAt(randomIndex); - _ = totals.probArray[randomIndex]; - totals.probArray.RemoveAt(randomIndex); - drawnKeys.Add(key); - probCumsum = CumulativeProbability(totals.probArray); - // If we draw without replacement and the ProbabilityObjectArray is exhausted we need to break - if (totals.keyArray.Count < 1) + // Subtract weight of item from above chosen value + randomTarget -= availableItems[j].Weight; + if (randomTarget <= 0) { + // Item falls within 'slice' of desired target, + // item has weight that eclipses accumulated weight of randomTarget + chosenIndex = j; break; } } + + // If index not found choose the last element + chosenIndex = (chosenIndex == -1) ? availableItems.Count - 1 : chosenIndex; + + // Get chosen item via index and add to results + var chosenItem = availableItems[chosenIndex]; + drawnKeys.Add(chosenItem.Key); + + // Only remove item if it's not in whitelist + if (neverRemoveWhitelist is not null && !neverRemoveWhitelist.Contains(chosenItem.Key)) + { + // Reduce total weight value by items weight + Remove item from pool + totalWeight -= chosenItem.Weight; + availableItems.RemoveAt(chosenIndex); + } } return drawnKeys;