From ccfac42814140efce6cfdd6a68ad58b1c6293026 Mon Sep 17 00:00:00 2001 From: Chomp Date: Wed, 6 Aug 2025 23:14:32 +0100 Subject: [PATCH] Improved performance of `GetItemWithChildren()` Reduced number of enumerations of `itemWithChildren` inside AddItemWithChildrenToEquipmentSlot()` by converting children to list at start of method Applied additional filtering to child items collection inside `GetContainerItemsWithChildren()` --- .../Extensions/ItemExtensions.cs | 68 +++++++++++-------- .../Helpers/BotGeneratorHelper.cs | 28 ++++---- UnitTests/Tests/Extensions/ItemTests.cs | 36 +++++++++- 3 files changed, 90 insertions(+), 42 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Extensions/ItemExtensions.cs b/Libraries/SPTarkov.Server.Core/Extensions/ItemExtensions.cs index 317618f2..d0b6b370 100644 --- a/Libraries/SPTarkov.Server.Core/Extensions/ItemExtensions.cs +++ b/Libraries/SPTarkov.Server.Core/Extensions/ItemExtensions.cs @@ -230,7 +230,6 @@ public static class ItemExtensions } /// - /// TODO: return IEnumerable and update all calling code /// Get an item with its attachments (children) /// /// List of items (item + possible children) @@ -239,48 +238,63 @@ public static class ItemExtensions /// list of Item objects public static List GetItemWithChildren(this IEnumerable items, MongoId baseItemId, bool excludeStoredItems = false) { - // Use dictionary to make key lookup faster, convert to list before being returned + // Convert to list if not already var itemList = items.ToList(); - OrderedDictionary result = []; - // Find desired root item - var desiredRootItem = itemList.FirstOrDefault(item => item.Id == baseItemId); - if (desiredRootItem is null) + // 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) { // Root not found, nothing to return, exit return []; } - result.Add(desiredRootItem.Id, desiredRootItem); - var rootItemIdString = desiredRootItem.Id.ToString(); - foreach (var item in itemList) + var result = new List(); + var stack = new Stack(); + stack.Push(root); + + while (stack.Count > 0) { - if (result.ContainsKey(item.Id)) + var current = stack.Pop(); + result.Add(current); + + if (!childrenByParent.TryGetValue(current.Id.ToString(), out var children)) { - // Already processed, skip + // No children, skip to next continue; } - // Skip items with different parentId - if (item.ParentId != rootItemIdString) + foreach (var child in children) { - continue; - } - - // Is stored in parent and disallowed - if (excludeStoredItems && item.Location is not null) - { - continue; - } - - // Item may have children, check - foreach (var subItem in GetItemWithChildren(itemList, item.Id)) - { - result.Add(subItem.Id, subItem); + // child item has a location property = is stored inside parent + if (excludeStoredItems && child.Location is not null) + { + continue; + } + stack.Push(child); } } - return result.Values.ToList(); + return result; } /// diff --git a/Libraries/SPTarkov.Server.Core/Helpers/BotGeneratorHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/BotGeneratorHelper.cs index 64a99867..8e6bec40 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/BotGeneratorHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/BotGeneratorHelper.cs @@ -435,7 +435,7 @@ public class BotGeneratorHelper( /// Root items tpl id /// Item to add /// Inventory to add item+children into - /// + /// Container Ids with no space for more items /// ItemAddedResult result object public ItemAddedResult AddItemWithChildrenToEquipmentSlot( HashSet equipmentSlots, @@ -446,6 +446,8 @@ public class BotGeneratorHelper( HashSet? containersIdFull = null ) { + var itemWithChildrenList = itemWithChildren.ToList(); + // Track how many containers are unable to be found var missingContainerCount = 0; foreach (var equipmentSlotId in equipmentSlots) @@ -455,8 +457,8 @@ public class BotGeneratorHelper( continue; } - // Get container to put item into - var container = inventory.Items.FirstOrDefault(item => item.SlotId == equipmentSlotId.ToString()); + // Get container from inventory to put item into + var container = inventory.Items?.FirstOrDefault(item => item.SlotId == equipmentSlotId.ToString()); if (container is null) { missingContainerCount++; @@ -466,7 +468,7 @@ public class BotGeneratorHelper( if (logger.IsLogEnabled(LogLevel.Debug)) { logger.Debug( - $"Unable to add item: {itemWithChildren.FirstOrDefault()?.Template} to bot as it lacks the following containers: {string.Join(",", equipmentSlots)}" + $"Unable to add item: {itemWithChildrenList.FirstOrDefault()?.Template} to bot as it lacks the following containers: {string.Join(",", equipmentSlots)}" ); } @@ -494,7 +496,7 @@ public class BotGeneratorHelper( } // Get x/y grid size of item - var (itemWidth, itemHeight) = inventoryHelper.GetItemSize(rootItemTplId, rootItemId, itemWithChildren); + var (itemWidth, itemHeight) = inventoryHelper.GetItemSize(rootItemTplId, rootItemId, itemWithChildrenList); // Iterate over each grid in the container and look for a big enough space for the item to be placed in var currentGridCount = 1; @@ -520,7 +522,7 @@ public class BotGeneratorHelper( ); // 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).ToList(); + var containerItemsToCheck = existingContainerItems.Where(x => x.SlotId == slotGrid.Name); var containerItemsWithChildren = GetContainerItemsWithChildren(containerItemsToCheck, inventory.Items); if (slotGrid.Props is not null) @@ -539,7 +541,7 @@ public class BotGeneratorHelper( // Free slot found, add item if (findSlotResult.Success ?? false) { - var parentItem = itemWithChildren.FirstOrDefault(i => i.Id == rootItemId); + var parentItem = itemWithChildrenList.FirstOrDefault(i => i.Id == rootItemId); // Set items parent to container id if (parentItem is not null) @@ -554,7 +556,7 @@ public class BotGeneratorHelper( }; } - (inventory.Items ?? []).AddRange(itemWithChildren); + (inventory.Items ?? []).AddRange(itemWithChildrenList); return ItemAddedResult.SUCCESS; } @@ -570,7 +572,7 @@ public class BotGeneratorHelper( // No space in this grid, move to next container grid and try again } - // if we got to this point, the item couldn't be placed on the container + // If we got to this point, the item couldn't be placed on the container if (containersIdFull is null) { continue; @@ -608,14 +610,14 @@ public class BotGeneratorHelper( return result; } - // Filter out all items without location prop, (child items) - var itemsWithoutLocation = inventoryItems.Where(item => item.Location is null); + // Get collection of items likely to be children of root items + var itemsWithoutLocation = inventoryItems.Where(item => item.Location is null && item.ParentId is not null).ToList(); foreach (var rootItem in containerRootItems) { // Check item in container for children, store for later insertion into `containerItemsToCheck` // (used later when figuring out how much space weapon takes up) - List itemsToFilter = [.. itemsWithoutLocation, rootItem]; - var itemWithChildItems = itemsToFilter.GetItemWithChildren(rootItem.Id); + itemsWithoutLocation.Insert(0, rootItem); + var itemWithChildItems = itemsWithoutLocation.GetItemWithChildren(rootItem.Id); // Item had children, replace existing data with item + its children result.AddRange(itemWithChildItems); diff --git a/UnitTests/Tests/Extensions/ItemTests.cs b/UnitTests/Tests/Extensions/ItemTests.cs index 20dcd819..71a75e02 100644 --- a/UnitTests/Tests/Extensions/ItemTests.cs +++ b/UnitTests/Tests/Extensions/ItemTests.cs @@ -63,7 +63,12 @@ public class ItemTests public void GetItemWithChildren_mods_and_inventory_item() { var testData = new List(); - var rootItem = new Item { Id = new MongoId(), Template = ItemTpl.AMMOBOX_127X33_COPPER_20RND }; + var rootItem = new Item + { + Id = new MongoId(), + Template = ItemTpl.AMMOBOX_127X33_COPPER_20RND, + ParentId = new MongoId(), + }; var childItem = new Item { Id = new MongoId(), @@ -83,7 +88,8 @@ public class ItemTests var result = testData.GetItemWithChildren(rootItem.Id, false); - Assert.AreEqual(result[1].Id, childItem.Id); + Assert.Contains(childItem, result); + Assert.Contains(childItem2, result); Assert.AreEqual(result.Count, 3); } @@ -292,4 +298,30 @@ public class ItemTests Assert.AreEqual(false, profile.Inventory.Items.FirstOrDefault(item => item.Id == item2Id).Upd.SpawnedInSession); Assert.AreEqual(true, profile.Inventory.Items.FirstOrDefault(item => item.Id == item3Id).Upd.SpawnedInSession); } + + [Test] + public void GetItemWithChildren_rootIdNotFound() + { + var testData = new List(); + var rootItem = new Item { Id = new MongoId(), Template = ItemTpl.AMMOBOX_127X33_COPPER_20RND }; + var childItem = new Item + { + Id = new MongoId(), + Template = ItemTpl.AMMO_127X33_COPPER, + ParentId = rootItem.Id, + }; + var childOfChild = new Item + { + Id = new MongoId(), + Template = ItemTpl.AMMO_26X75_GREEN, + ParentId = childItem.Id, + }; + testData.Add(rootItem); + testData.Add(childItem); + testData.Add(childOfChild); + + var result = testData.GetItemWithChildren(new MongoId(), true); + + Assert.AreEqual(result.Count, 0); + } }