Bot generation performance improvements

Trader assort generation performance improvements

Removed use of recursion inside `FindAndReturnChildrenByAssort`
Created extension method `CreateParentIdLookupCache`
This commit is contained in:
Chomp
2025-08-08 12:33:31 +01:00
parent 5e05049669
commit dea94255e4
4 changed files with 121 additions and 56 deletions
@@ -238,43 +238,20 @@ public static class ItemExtensions
/// <returns>list of Item objects</returns>
public static List<Item> GetItemWithChildren(this IEnumerable<Item> items, MongoId baseItemId, bool excludeStoredItems = false)
{
// Convert to list if not already
var itemList = items.ToList();
// Create dict of items by parentId
var childrenByParent = new Dictionary<string, List<Item>>(itemList.Count);
foreach (var child in itemList)
{
var key = child.ParentId;
if (key is null)
{
continue;
}
if (childrenByParent.TryGetValue(key, out var list))
{
list.Add(child);
}
else
{
childrenByParent[key] = [child];
}
}
// Find root item
var root = itemList.FirstOrDefault(i => i.Id == baseItemId);
if (root is null)
var childrenByParent = items.CreateParentIdLookupCache(out var rootItem, baseItemId);
if (rootItem is null)
{
// Root not found, nothing to return, exit
return [];
}
var result = new List<Item>();
var stack = new Stack<Item>();
stack.Push(root);
var processingStack = new Stack<Item>();
processingStack.Push(rootItem);
while (stack.Count > 0)
while (processingStack.Count > 0)
{
var current = stack.Pop();
var current = processingStack.Pop();
result.Add(current);
if (!childrenByParent.TryGetValue(current.Id.ToString(), out var children))
@@ -285,18 +262,67 @@ public static class ItemExtensions
foreach (var child in children)
{
// child item has a location property = is stored inside parent
// Child item has a location property = is stored inside parent and not a mod, skip
if (excludeStoredItems && child.Location is not null)
{
continue;
}
stack.Push(child);
// Add item to stack to check if it has children we need to add to result
processingStack.Push(child);
}
}
return result;
}
/// <summary>
/// Cache items by their parentId
/// </summary>
/// <param name="items">items to process</param>
/// <param name="baseItemId">Id of root item</param>
/// <param name="rootItem">Root item from inputted data</param>
/// <returns>Dictionary of items keyed by their parentId</returns>
public static Dictionary<string, List<Item>> CreateParentIdLookupCache(
this IEnumerable<Item> items,
out Item? rootItem,
MongoId? baseItemId = null
)
{
rootItem = null;
// If passed in items implements ICollection, we can determine size and pre-allocate to avoid re-allocations
var capacity = items is ICollection<Item> collection ? collection.Count : 0;
// Create lookup of items keyed by parentId
var childrenByParent = new Dictionary<string, List<Item>>(capacity);
foreach (var item in items)
{
if (baseItemId is not null && item.Id == baseItemId)
{
// Root item found, store in out param
rootItem = item;
}
if (item.ParentId is null)
{
// no parent, nothing to key item against
continue;
}
if (!childrenByParent.TryGetValue(item.ParentId, out var children))
{
// No collection for this parentId, create
children = [];
childrenByParent[item.ParentId] = children;
}
children.Add(item);
}
return childrenByParent;
}
/// <summary>
/// Convert an Item to SptLootItem
/// </summary>
@@ -417,19 +443,38 @@ public static class ItemExtensions
}
/// <summary>
/// Create hashsets for passed in items, keyed by the items ID and by the items parentId
/// Create 2 hashsets for passed in items, keyed by the items ID and by the items parentId
/// </summary>
/// <param name="inventoryItems">Items to hash</param>
/// <returns>InventoryItemHash</returns>
public static InventoryItemHash GetInventoryItemHash(this IEnumerable<Item> inventoryItems)
{
// Group by parentId + turn value into mongoId as we've filtered out non-mongoId values
var byParentId = inventoryItems
.Where(item => !string.IsNullOrEmpty(item.ParentId) && item.ParentId != "hideout")
.GroupBy(item => new MongoId(item.ParentId))
.ToDictionary(kvp => kvp.Key, group => group.ToHashSet());
Dictionary<MongoId, Item> byItemId = new();
Dictionary<MongoId, HashSet<Item>> byParentId = new();
return new InventoryItemHash { ByItemId = inventoryItems.ToDictionary(item => item.Id), ByParentId = byParentId };
foreach (var item in inventoryItems)
{
// Add every item to 'byItemId'
byItemId[item.Id] = item;
if (string.IsNullOrEmpty(item.ParentId) || item.ParentId == "hideout")
{
// Inventory non-items, skip
continue;
}
var parentId = new MongoId(item.ParentId);
if (!byParentId.TryGetValue(parentId, out var childItems))
{
// Hashset doesn't exist for this parentId, create and add blank set
childItems = [];
byParentId[parentId] = childItems;
}
childItems.Add(item);
}
return new InventoryItemHash { ByItemId = byItemId, ByParentId = byParentId };
}
/// <summary>
@@ -392,11 +392,11 @@ public class BotGenerator(
continue;
}
// Create a set of tpls to remove
// Create list of blacklisted keys to remove
var keysToRemove = container
.Where(item => itemFilterService.IsLootableItemBlacklisted(item.Key))
.Select(item => item.Key)
.ToHashSet();
.ToList();
// Remove from container by key
foreach (var key in keysToRemove)
@@ -452,8 +452,9 @@ public class BotGeneratorHelper(
var missingContainerCount = 0;
foreach (var equipmentSlotId in equipmentSlots)
{
if (containersIdFull?.Contains(equipmentSlotId.ToString()) ?? false)
if (containersIdFull is not null && containersIdFull.Contains(equipmentSlotId.ToString()))
{
// Container has been flagged as full already, skip trying to add item into it
continue;
}
@@ -490,8 +491,8 @@ public class BotGeneratorHelper(
}
if (itemDbDetails?.Properties?.Grids is null || !itemDbDetails.Properties.Grids.Any())
// Container has no slots to hold items
{
// Container has no slots to hold items, skip to next container
continue;
}
@@ -516,14 +517,13 @@ public class BotGeneratorHelper(
break;
}
// Get all root items in found container
var existingContainerItems = (inventory.Items ?? []).Where(item =>
item.ParentId == container.Id && item.SlotId == slotGrid.Name
);
// Get all root items in container
var rootItemsInContainer = inventory.Items is null
? []
: inventory.Items.Where(item => item.SlotId == slotGrid.Name && item.ParentId == container.Id);
// Get root items in container we can iterate over to find out what space is free
var containerItemsToCheck = existingContainerItems.Where(x => x.SlotId == slotGrid.Name);
var containerItemsWithChildren = GetContainerItemsWithChildren(containerItemsToCheck, inventory.Items);
var containerItemsWithChildren = GetContainerItemsWithChildren(rootItemsInContainer, inventory.Items);
if (slotGrid.Props is not null)
{
@@ -675,19 +675,39 @@ public class ItemHelper(
/// <returns>List of children of requested item</returns>
public List<Item> FindAndReturnChildrenByAssort(MongoId itemIdToFind, IEnumerable<Item> assort)
{
List<Item> list = [];
var itemIdToFindString = itemIdToFind.ToString();
foreach (var itemFromAssort in assort)
// Group items by ParentId
var lookup = assort.CreateParentIdLookupCache(out _);
var results = new List<Item>();
var visitedCache = new HashSet<string>();
var explorationStack = new Stack<string>();
explorationStack.Push(itemIdToFind.ToString());
while (explorationStack.Count > 0)
{
// Parent matches desired item + all items in list do not match
if (itemFromAssort.ParentId == itemIdToFindString && list.All(item => itemFromAssort.Id != item.Id))
var currentId = explorationStack.Pop();
if (!lookup.TryGetValue(currentId, out var childItems))
{
list.Add(itemFromAssort);
list = list.Concat(FindAndReturnChildrenByAssort(itemFromAssort.Id, assort)).ToList();
continue;
}
foreach (var childItem in childItems)
{
// Store item in visited cache so it's not added to results more than once
if (visitedCache.Add(childItem.Id))
{
// Item not in visited cache, take it
results.Add(childItem);
// Add item to stack so it gets processed
explorationStack.Push(childItem.Id);
}
}
}
return list;
return results;
}
/// <summary>