Bot generation performance improvements
Trader assort generation performance improvements Removed use of recursion inside `FindAndReturnChildrenByAssort` Created extension method `CreateParentIdLookupCache`
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user