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;
}
///