From dea94255e410387bb857619bacf6384ebc4025e0 Mon Sep 17 00:00:00 2001 From: Chomp Date: Fri, 8 Aug 2025 12:33:31 +0100 Subject: [PATCH] Bot generation performance improvements Trader assort generation performance improvements Removed use of recursion inside `FindAndReturnChildrenByAssort` Created extension method `CreateParentIdLookupCache` --- .../Extensions/ItemExtensions.cs | 121 ++++++++++++------ .../Generators/BotGenerator.cs | 4 +- .../Helpers/BotGeneratorHelper.cs | 16 +-- .../Helpers/ItemHelper.cs | 36 ++++-- 4 files changed, 121 insertions(+), 56 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Extensions/ItemExtensions.cs b/Libraries/SPTarkov.Server.Core/Extensions/ItemExtensions.cs index d0b6b370..61c66b54 100644 --- a/Libraries/SPTarkov.Server.Core/Extensions/ItemExtensions.cs +++ b/Libraries/SPTarkov.Server.Core/Extensions/ItemExtensions.cs @@ -238,43 +238,20 @@ public static class ItemExtensions /// list of Item objects public static List GetItemWithChildren(this IEnumerable 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>(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(); - var stack = new Stack(); - stack.Push(root); + var processingStack = new Stack(); + 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; } + /// + /// Cache items by their parentId + /// + /// items to process + /// Id of root item + /// Root item from inputted data + /// Dictionary of items keyed by their parentId + public static Dictionary> CreateParentIdLookupCache( + this IEnumerable 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 collection ? collection.Count : 0; + + // Create lookup of items keyed by parentId + var childrenByParent = new Dictionary>(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; + } + /// /// Convert an Item to SptLootItem /// @@ -417,19 +443,38 @@ public static class ItemExtensions } /// - /// 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 /// /// Items to hash /// InventoryItemHash public static InventoryItemHash GetInventoryItemHash(this IEnumerable 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 byItemId = new(); + Dictionary> 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 }; } /// diff --git a/Libraries/SPTarkov.Server.Core/Generators/BotGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/BotGenerator.cs index 2d128b52..9221c22e 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/BotGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/BotGenerator.cs @@ -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) diff --git a/Libraries/SPTarkov.Server.Core/Helpers/BotGeneratorHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/BotGeneratorHelper.cs index 9985f938..7d8ea8ff 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/BotGeneratorHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/BotGeneratorHelper.cs @@ -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) { diff --git a/Libraries/SPTarkov.Server.Core/Helpers/ItemHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/ItemHelper.cs index d8db12d9..d1ec2242 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/ItemHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/ItemHelper.cs @@ -675,19 +675,39 @@ public class ItemHelper( /// List of children of requested item public List FindAndReturnChildrenByAssort(MongoId itemIdToFind, IEnumerable assort) { - List list = []; - var itemIdToFindString = itemIdToFind.ToString(); - foreach (var itemFromAssort in assort) + // Group items by ParentId + var lookup = assort.CreateParentIdLookupCache(out _); + + var results = new List(); + var visitedCache = new HashSet(); + + var explorationStack = new Stack(); + 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; } ///