Replaced ProbabilityObjectArray.Draw() with DrawAndRemove Draw

Reduced overhead when drawing a large number of elements during loot generation
This commit is contained in:
Chomp
2025-08-06 17:52:49 +01:00
parent 3f405fc67e
commit 6b297adf68
4 changed files with 86 additions and 48 deletions
@@ -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);
@@ -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));
}
@@ -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<string>();
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(
/// <returns>Weapon to use</returns>
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)
@@ -35,13 +35,13 @@ public class ProbabilityObjectArray<K, V> : List<ProbabilityObject<K, V>>
/// </summary>
/// <param name="probValues">The relative probability values of which to calculate the normalized cumulative sum</param>
/// <returns>Cumulative Sum normalized to 1</returns>
public List<double> CumulativeProbability(IEnumerable<double> probValues)
public IEnumerable<double> CumulativeProbability(IEnumerable<double> probValues)
{
var sum = probValues.Sum();
var probCumsum = probValues.CumulativeSum();
probCumsum = probCumsum.Product(1D / sum);
return probCumsum.ToList();
return probCumsum;
}
/// <summary>
@@ -136,59 +136,96 @@ public class ProbabilityObjectArray<K, V> : List<ProbabilityObject<K, V>>
}
/// <summary>
/// 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
/// </summary>
/// <param name="drawCount">The number of times we want to draw</param>
/// <param name="removeAfterDraw">Draw with or without replacement from the input dict (true = don't remove after drawing)</param>
/// <param name="neverRemoveWhitelist">List of keys which shall be replaced even if drawing without replacement</param>
/// <param name="itemCountToDraw">The number of times we want to draw</param>
/// <returns>Collection consisting of N random keys for this ProbabilityObjectArray</returns>
public List<K> Draw(int drawCount = 1, bool removeAfterDraw = true, List<K>? neverRemoveWhitelist = null)
public List<K> Draw(int itemCountToDraw = 1)
{
neverRemoveWhitelist ??= [];
if (Count == 0)
{
// Nothing in pool
return [];
}
var totals = this.Aggregate(
new { probArray = new List<double>(), keyArray = new List<K>() },
(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<K>(itemCountToDraw);
var drawnKeys = new List<K>();
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;
}
/// <summary>
///Draw random element of the ProbabilityObject N times to return an array of N keys
/// Removes drawn elements
/// </summary>
/// <param name="itemCountToDraw">The number of times we want to draw</param>
/// <param name="neverRemoveWhitelist">List of keys which shall be replaced even if drawing without replacement</param>
/// <returns>Collection consisting of N random keys for this ProbabilityObjectArray</returns>
public List<K> DrawAndRemove(int itemCountToDraw = 1, List<K>? 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<K>(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;