diff --git a/Libraries/SPTarkov.Server.Core/Extensions/ContainerExtensions.cs b/Libraries/SPTarkov.Server.Core/Extensions/ContainerExtensions.cs
index da2d70ff..c5660cad 100644
--- a/Libraries/SPTarkov.Server.Core/Extensions/ContainerExtensions.cs
+++ b/Libraries/SPTarkov.Server.Core/Extensions/ContainerExtensions.cs
@@ -24,7 +24,7 @@ public static class ContainerExtensions
var limitX = containerX - minVolume;
// Every x+y slot taken up in container, exit
- if (ContainerIsFull(container2D))
+ if (container2D.ContainerIsFull())
{
return new FindSlotResult(false);
}
@@ -84,7 +84,8 @@ public static class ContainerExtensions
/// Items width
/// Items height
/// is item rotated
- public static void FillContainerMapWithItem(
+ /// bool = true when successful, string = error message if failed
+ public static (bool, string) FillContainerMapWithItem(
this int[,] container2D,
int columnStartPositionX,
int rowStartPositionY,
@@ -108,7 +109,7 @@ public static class ContainerExtensions
{
container2D[rowStartPositionY, columnStartPositionX] = 1;
- return;
+ return (true, string.Empty);
}
// Loop over rows and columns and flag each as taken by item
@@ -123,12 +124,15 @@ public static class ContainerExtensions
}
else
{
- throw new Exception(
+ return (
+ false,
$"Slot at: ({containerX}, {containerY}) is already filled. Cannot fit: {itemXWidth} by {itemYHeight} item"
);
}
}
}
+
+ return (true, string.Empty);
}
///
@@ -158,7 +162,7 @@ public static class ContainerExtensions
///
/// Container to check
/// True = full
- private static bool ContainerIsFull(int[,] container2D)
+ public static bool ContainerIsFull(this int[,] container2D)
{
var containerY = container2D.GetLength(0); // rows
var containerX = container2D.GetLength(1); // columns
diff --git a/Libraries/SPTarkov.Server.Core/Generators/BotGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/BotGenerator.cs
index 9221c22e..140eea56 100644
--- a/Libraries/SPTarkov.Server.Core/Generators/BotGenerator.cs
+++ b/Libraries/SPTarkov.Server.Core/Generators/BotGenerator.cs
@@ -173,6 +173,9 @@ public class BotGenerator(
var botRoleLowercase = botGenerationDetails.Role.ToLowerInvariant();
var botLevel = botLevelGenerator.GenerateBotLevel(botJsonTemplate.BotExperience.Level, botGenerationDetails, bot);
+ // Generate new bot ID
+ AddIdsToBot(bot, botGenerationDetails);
+
// Only filter bot equipment, never players
if (!botGenerationDetails.IsPlayerScav)
{
@@ -254,6 +257,7 @@ public class BotGenerator(
SetBotAppearance(bot, botJsonTemplate.BotAppearance, botGenerationDetails);
bot.Inventory = botInventoryGenerator.GenerateInventory(
+ bot.Id.Value,
sessionId,
botJsonTemplate,
botRoleLowercase,
@@ -267,9 +271,6 @@ public class BotGenerator(
AddDogtagToBot(bot);
}
- // Generate new bot ID
- AddIdsToBot(bot, botGenerationDetails);
-
// Generate new inventory ID
GenerateInventoryId(bot);
diff --git a/Libraries/SPTarkov.Server.Core/Generators/BotInventoryGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/BotInventoryGenerator.cs
index c4c6b923..7b4fb82c 100644
--- a/Libraries/SPTarkov.Server.Core/Generators/BotInventoryGenerator.cs
+++ b/Libraries/SPTarkov.Server.Core/Generators/BotInventoryGenerator.cs
@@ -32,9 +32,19 @@ public class BotInventoryGenerator(
BotEquipmentFilterService botEquipmentFilterService,
BotEquipmentModPoolService botEquipmentModPoolService,
BotEquipmentModGenerator botEquipmentModGenerator,
+ BotInventoryContainerService botInventoryContainerService,
ConfigServer configServer
)
{
+ // Slots handled individually inside `GenerateAndAddEquipmentToBot`
+ private static readonly FrozenSet _equipmentSlotsWithInventory =
+ [
+ EquipmentSlots.Pockets,
+ EquipmentSlots.TacticalVest,
+ EquipmentSlots.Backpack,
+ EquipmentSlots.SecuredContainer,
+ ];
+
// Slots handled individually inside `GenerateAndAddEquipmentToBot`
private static readonly FrozenSet _excludedEquipmentSlots =
[
@@ -56,6 +66,7 @@ public class BotInventoryGenerator(
///
/// Add equipment/weapons/loot to bot
///
+ /// Bots unique identifier
/// Session id
/// Base json db file for the bot having its loot generated
/// Role bot has (assault/pmcBot)
@@ -64,6 +75,7 @@ public class BotInventoryGenerator(
/// Game version for bot, only really applies for PMCs
/// PmcInventory object with equipment/weapons/loot
public BotBaseInventory GenerateInventory(
+ MongoId botId,
MongoId sessionId,
BotType botJsonTemplate,
string botRole,
@@ -85,6 +97,7 @@ public class BotInventoryGenerator(
var raidConfig = profileActivityService.GetProfileActivityRaidData(sessionId)?.RaidConfiguration;
GenerateAndAddEquipmentToBot(
+ botId,
sessionId,
templateInventory,
wornItemChances,
@@ -98,6 +111,7 @@ public class BotInventoryGenerator(
// Roll weapon spawns (primary/secondary/holster) and generate a weapon for each roll that passed
GenerateAndAddWeaponsToBot(
+ botId,
templateInventory,
wornItemChances,
sessionId,
@@ -109,7 +123,10 @@ public class BotInventoryGenerator(
);
// Pick loot and add to bots containers (rig/backpack/pockets/secure)
- botLootGenerator.GenerateLoot(sessionId, botJsonTemplate, botGenerationDetails, isPmc, botRole, botInventory, botLevel);
+ botLootGenerator.GenerateLoot(botId, sessionId, botJsonTemplate, botGenerationDetails, isPmc, botRole, botInventory, botLevel);
+
+ // Inventory cache isn't needed, clear to save memory
+ botInventoryContainerService.ClearCache(botId);
return botInventory;
}
@@ -153,6 +170,7 @@ public class BotInventoryGenerator(
///
/// Add equipment to a bot
///
+ /// Bots unique identifier
/// Session id
/// bot/x.json data from db
/// Chances items will be added to bot
@@ -163,6 +181,7 @@ public class BotInventoryGenerator(
/// Is the generated bot a PMC
/// RadiConfig
public void GenerateAndAddEquipmentToBot(
+ MongoId botId,
MongoId sessionId,
BotTypeInventory templateInventory,
Chances wornItemChances,
@@ -211,6 +230,7 @@ public class BotInventoryGenerator(
GenerateEquipment(
new GenerateEquipmentProperties
{
+ BotId = botId,
RootEquipmentSlot = equipmentSlot,
RootEquipmentPool = value,
ModPool = templateInventory.Mods,
@@ -233,6 +253,7 @@ public class BotInventoryGenerator(
GenerateEquipment(
new GenerateEquipmentProperties
{
+ BotId = botId,
RootEquipmentSlot = EquipmentSlots.Pockets,
// Unheard profiles have unique sized pockets
RootEquipmentPool = GetPocketPoolByGameEdition(chosenGameVersion, templateInventory, isPmc),
@@ -255,6 +276,7 @@ public class BotInventoryGenerator(
GenerateEquipment(
new GenerateEquipmentProperties
{
+ BotId = botId,
RootEquipmentSlot = EquipmentSlots.FaceCover,
RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.FaceCover],
ModPool = templateInventory.Mods,
@@ -275,6 +297,7 @@ public class BotInventoryGenerator(
GenerateEquipment(
new GenerateEquipmentProperties
{
+ BotId = botId,
RootEquipmentSlot = EquipmentSlots.Headwear,
RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.Headwear],
ModPool = templateInventory.Mods,
@@ -295,6 +318,7 @@ public class BotInventoryGenerator(
GenerateEquipment(
new GenerateEquipmentProperties
{
+ BotId = botId,
RootEquipmentSlot = EquipmentSlots.Earpiece,
RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.Earpiece],
ModPool = templateInventory.Mods,
@@ -315,6 +339,7 @@ public class BotInventoryGenerator(
var hasArmorVest = GenerateEquipment(
new GenerateEquipmentProperties
{
+ BotId = botId,
RootEquipmentSlot = EquipmentSlots.ArmorVest,
RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.ArmorVest],
ModPool = templateInventory.Mods,
@@ -355,6 +380,7 @@ public class BotInventoryGenerator(
GenerateEquipment(
new GenerateEquipmentProperties
{
+ BotId = botId,
RootEquipmentSlot = EquipmentSlots.TacticalVest,
RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.TacticalVest],
ModPool = templateInventory.Mods,
@@ -573,6 +599,12 @@ public class BotInventoryGenerator(
settings.Inventory.Items.Add(item);
}
+ // Cache container ready for items to be added in
+ if (_equipmentSlotsWithInventory.Contains(settings.RootEquipmentSlot))
+ {
+ botInventoryContainerService.AddEmptyContainerToBot(settings.BotId, settings.RootEquipmentSlot, item);
+ }
+
return true;
}
@@ -622,6 +654,7 @@ public class BotInventoryGenerator(
///
/// Work out what weapons bot should have equipped and add them to bot inventory
///
+ /// Bots unique identifier
/// bot/x.json data from db
/// Chances bot can have equipment equipped
/// Session id
@@ -631,6 +664,7 @@ public class BotInventoryGenerator(
/// Limits for items the bot can have
/// level of bot having weapon generated
public void GenerateAndAddWeaponsToBot(
+ MongoId botId,
BotTypeInventory templateInventory,
Chances equipmentChances,
MongoId sessionId,
@@ -648,6 +682,7 @@ public class BotInventoryGenerator(
if (desiredWeapons.ShouldSpawn && templateInventory.Equipment[desiredWeapons.Slot].Any())
{
AddWeaponAndMagazinesToInventory(
+ botId,
sessionId,
desiredWeapons,
templateInventory,
@@ -689,6 +724,7 @@ public class BotInventoryGenerator(
///
/// Add weapon + spare mags/ammo to bots inventory
///
+ /// Bots unique identifier
/// Session id
/// Weapon slot being generated
/// bot/x.json data from db
@@ -699,6 +735,7 @@ public class BotInventoryGenerator(
///
///
public void AddWeaponAndMagazinesToInventory(
+ MongoId botId,
MongoId sessionId,
DesiredWeapons weaponSlot,
BotTypeInventory templateInventory,
@@ -723,7 +760,13 @@ public class BotInventoryGenerator(
botInventory.Items.AddRange(generatedWeapon.Weapon);
- botWeaponGenerator.AddExtraMagazinesToInventory(generatedWeapon, itemGenerationWeights.Items.Magazines, botInventory, botRole);
+ botWeaponGenerator.AddExtraMagazinesToInventory(
+ botId,
+ generatedWeapon,
+ itemGenerationWeights.Items.Magazines,
+ botInventory,
+ botRole
+ );
}
}
diff --git a/Libraries/SPTarkov.Server.Core/Generators/BotLootGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/BotLootGenerator.cs
index 5afc2e1d..db2bf6b1 100644
--- a/Libraries/SPTarkov.Server.Core/Generators/BotLootGenerator.cs
+++ b/Libraries/SPTarkov.Server.Core/Generators/BotLootGenerator.cs
@@ -57,6 +57,7 @@ public class BotLootGenerator(
///
/// Add loot to bots containers
///
+ /// Bots unique identifier
/// Session id
/// Clone of Base JSON db file for the bot having its loot generated
/// Details relating to generating a bot
@@ -65,6 +66,7 @@ public class BotLootGenerator(
/// Inventory to add loot to
/// Level of bot
public void GenerateLoot(
+ MongoId botId,
MongoId sessionId,
BotType botJsonTemplate,
BotGenerationDetails botGenerationDetails,
@@ -119,7 +121,7 @@ public class BotLootGenerator(
// Forced pmc healing loot into secure container
if (isPmc && _pmcConfig.ForceHealingItemsIntoSecure)
{
- AddForcedMedicalItemsToPmcSecure(botInventory, botRole);
+ AddForcedMedicalItemsToPmcSecure(botInventory, botRole, botId);
}
var botItemLimits = GetItemSpawnLimitsForBot(botRole);
@@ -132,6 +134,7 @@ public class BotLootGenerator(
// Special items
AddLootFromPool(
+ botId,
botLootCacheService.GetLootFromCache(botRole, isPmc, LootCacheType.Special, botJsonTemplate),
containersBotHasAvailable,
specialLootItemCount,
@@ -143,6 +146,7 @@ public class BotLootGenerator(
// Healing items / Meds
AddLootFromPool(
+ botId,
botLootCacheService.GetLootFromCache(botRole, isPmc, LootCacheType.HealingItems, botJsonTemplate),
containersBotHasAvailable,
healingItemCount,
@@ -156,6 +160,7 @@ public class BotLootGenerator(
// Drugs
AddLootFromPool(
+ botId,
botLootCacheService.GetLootFromCache(botRole, isPmc, LootCacheType.DrugItems, botJsonTemplate),
containersBotHasAvailable,
drugItemCount,
@@ -169,6 +174,7 @@ public class BotLootGenerator(
// Food
AddLootFromPool(
+ botId,
botLootCacheService.GetLootFromCache(botRole, isPmc, LootCacheType.FoodItems, botJsonTemplate),
containersBotHasAvailable,
foodItemCount,
@@ -182,6 +188,7 @@ public class BotLootGenerator(
// Drink
AddLootFromPool(
+ botId,
botLootCacheService.GetLootFromCache(botRole, isPmc, LootCacheType.DrinkItems, botJsonTemplate),
containersBotHasAvailable,
drinkItemCount,
@@ -195,6 +202,7 @@ public class BotLootGenerator(
// Currency
AddLootFromPool(
+ botId,
botLootCacheService.GetLootFromCache(botRole, isPmc, LootCacheType.CurrencyItems, botJsonTemplate),
containersBotHasAvailable,
currencyItemCount,
@@ -208,6 +216,7 @@ public class BotLootGenerator(
// Stims
AddLootFromPool(
+ botId,
botLootCacheService.GetLootFromCache(botRole, isPmc, LootCacheType.StimItems, botJsonTemplate),
containersBotHasAvailable,
stimItemCount,
@@ -221,6 +230,7 @@ public class BotLootGenerator(
// Grenades
AddLootFromPool(
+ botId,
botLootCacheService.GetLootFromCache(botRole, isPmc, LootCacheType.GrenadeItems, botJsonTemplate),
[EquipmentSlots.Pockets, EquipmentSlots.TacticalVest], // Can't use containersBotHasEquipped as we don't want grenades added to backpack
grenadeCount,
@@ -241,6 +251,7 @@ public class BotLootGenerator(
if (isPmc && randomUtil.GetChance100(_pmcConfig.LooseWeaponInBackpackChancePercent))
{
AddLooseWeaponsToInventorySlot(
+ botId,
sessionId,
botInventory,
EquipmentSlots.Backpack,
@@ -258,6 +269,7 @@ public class BotLootGenerator(
: 0;
AddLootFromPool(
+ botId,
botLootCacheService.GetLootFromCache(botRole, isPmc, LootCacheType.Backpack, botJsonTemplate, itemPriceLimits?.Backpack),
[EquipmentSlots.Backpack],
backpackLootCount,
@@ -277,6 +289,7 @@ public class BotLootGenerator(
// Vest
{
AddLootFromPool(
+ botId,
botLootCacheService.GetLootFromCache(botRole, isPmc, LootCacheType.Vest, botJsonTemplate, itemPriceLimits?.Vest),
[EquipmentSlots.TacticalVest],
vestLootCount,
@@ -293,6 +306,7 @@ public class BotLootGenerator(
// Pockets
AddLootFromPool(
+ botId,
botLootCacheService.GetLootFromCache(botRole, isPmc, LootCacheType.Pocket, botJsonTemplate, itemPriceLimits?.Pocket),
[EquipmentSlots.Pockets],
pocketLootCount,
@@ -310,6 +324,7 @@ public class BotLootGenerator(
if (!isPmc || (isPmc && _pmcConfig.AddSecureContainerLootFromBotConfig))
{
AddLootFromPool(
+ botId,
botLootCacheService.GetLootFromCache(botRole, isPmc, LootCacheType.Secure, botJsonTemplate),
[EquipmentSlots.SecuredContainer],
50,
@@ -365,10 +380,12 @@ public class BotLootGenerator(
///
/// Inventory to add items to
/// Role of bot (pmcBEAR/pmcUSEC)
- protected void AddForcedMedicalItemsToPmcSecure(BotBaseInventory botInventory, string botRole)
+ /// Bots unique identifier
+ protected void AddForcedMedicalItemsToPmcSecure(BotBaseInventory botInventory, string botRole, MongoId botId)
{
// surv12
AddLootFromPool(
+ botId,
new Dictionary { { ItemTpl.MEDICAL_SURV12_FIELD_SURGICAL_KIT, 1 } },
[EquipmentSlots.SecuredContainer],
1,
@@ -380,21 +397,14 @@ public class BotLootGenerator(
);
// AFAK
- AddLootFromPool(
- new Dictionary { { ItemTpl.MEDKIT_AFAK_TACTICAL_INDIVIDUAL_FIRST_AID_KIT, 1 } },
- [EquipmentSlots.SecuredContainer],
- 10,
- botInventory,
- botRole,
- null,
- 0,
- true
- );
+ var afaks = new Dictionary { { ItemTpl.MEDKIT_AFAK_TACTICAL_INDIVIDUAL_FIRST_AID_KIT, 1 } };
+ AddLootFromPool(botId, afaks, [EquipmentSlots.SecuredContainer], 10, botInventory, botRole, null, 0, true);
}
///
/// Take random items from a pool and add to an inventory until totalItemCount or totalValueLimit or space limit is reached
///
+ /// Bots unique identifier
/// Pool of items to pick from with weight
/// What equipment slot will the loot items be added to
/// Max count of items to add
@@ -405,6 +415,7 @@ public class BotLootGenerator(
/// Total value of loot allowed in roubles
/// Is bot being generated for a pmc
protected internal void AddLootFromPool(
+ MongoId botId,
Dictionary pool,
HashSet equipmentSlots,
double totalItemCount,
@@ -498,6 +509,7 @@ public class BotLootGenerator(
// Attempt to add item to container(s)
var itemAddedResult = botGeneratorHelper.AddItemWithChildrenToEquipmentSlot(
+ botId,
equipmentSlots,
newRootItemId,
itemToAddTemplate.Id,
@@ -619,6 +631,7 @@ public class BotLootGenerator(
///
/// Add generated weapons to inventory as loot
///
+ /// Bots unique identifier
/// Session/Player id
/// Inventory to add preset to
/// Slot to place the preset in (backpack)
@@ -629,6 +642,7 @@ public class BotLootGenerator(
///
///
public void AddLooseWeaponsToInventorySlot(
+ MongoId botId,
MongoId sessionId,
BotBaseInventory botInventory,
EquipmentSlots equipmentSlot,
@@ -679,6 +693,7 @@ public class BotLootGenerator(
continue;
}
var result = botGeneratorHelper.AddItemWithChildrenToEquipmentSlot(
+ botId,
[equipmentSlot],
weaponRootItem.Id,
weaponRootItem.Template,
diff --git a/Libraries/SPTarkov.Server.Core/Generators/BotWeaponGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/BotWeaponGenerator.cs
index 13700959..1f6f0adf 100644
--- a/Libraries/SPTarkov.Server.Core/Generators/BotWeaponGenerator.cs
+++ b/Libraries/SPTarkov.Server.Core/Generators/BotWeaponGenerator.cs
@@ -413,11 +413,13 @@ public class BotWeaponGenerator(
/// Generates extra magazines or bullets (if magazine is internal) and adds them to TacticalVest and Pockets.
/// Additionally, adds extra bullets to SecuredContainer
///
+ /// Bots unique identifier
/// Object with properties for generated weapon (weapon mods pool / weapon template / ammo tpl)
/// Magazine weights for count to add to inventory
/// Inventory to add magazines to
/// The bot type we're generating extra mags for
public void AddExtraMagazinesToInventory(
+ MongoId botId,
GenerateWeaponResult generatedWeaponResult,
GenerationData magWeights,
BotBaseInventory inventory,
@@ -448,15 +450,16 @@ public class BotWeaponGenerator(
// Has an UBGL
if (generatedWeaponResult.ChosenUbglAmmoTemplate is not null && !generatedWeaponResult.ChosenUbglAmmoTemplate.Value.IsEmpty)
{
- AddUbglGrenadesToBotInventory(weaponAndMods, generatedWeaponResult, inventory);
+ AddUbglGrenadesToBotInventory(botId, weaponAndMods, generatedWeaponResult, inventory);
}
- var inventoryMagGenModel = new InventoryMagGen(magWeights, magTemplate, weaponTemplate, ammoTemplate.Value, inventory);
+ var inventoryMagGenModel = new InventoryMagGen(magWeights, magTemplate, weaponTemplate, ammoTemplate.Value, inventory, botId);
_inventoryMagGenComponents.FirstOrDefault(v => v.CanHandleInventoryMagGen(inventoryMagGenModel)).Process(inventoryMagGenModel);
// Add x stacks of bullets to SecuredContainer (bots use a magic mag packing skill to reload instantly)
AddAmmoToSecureContainer(
+ botId,
_botConfig.SecureContainerAmmoStackCount,
generatedWeaponResult.ChosenAmmoTemplate,
ammoTemplate.Value.Properties.StackMaxSize ?? 0,
@@ -467,10 +470,12 @@ public class BotWeaponGenerator(
///
/// Add Grenades for UBGL to bot's vest and secure container
///
+ /// Bots unique identifier
/// Weapon list with mods
/// Result of weapon generation
/// Bot inventory to add grenades to
protected void AddUbglGrenadesToBotInventory(
+ MongoId botId,
List- weaponMods,
GenerateWeaponResult generatedWeaponResult,
BotBaseInventory inventory
@@ -491,11 +496,11 @@ public class BotWeaponGenerator(
var ubglAmmoDbTemplate = itemHelper.GetItem(generatedWeaponResult.ChosenUbglAmmoTemplate.Value).Value;
// Add greandes to bot inventory
- var ubglAmmoGenModel = new InventoryMagGen(ubglMinMax, ubglDbTemplate, ubglDbTemplate, ubglAmmoDbTemplate, inventory);
+ var ubglAmmoGenModel = new InventoryMagGen(ubglMinMax, ubglDbTemplate, ubglDbTemplate, ubglAmmoDbTemplate, inventory, botId);
_inventoryMagGenComponents.FirstOrDefault(v => v.CanHandleInventoryMagGen(ubglAmmoGenModel)).Process(ubglAmmoGenModel);
// Store extra grenades in secure container
- AddAmmoToSecureContainer(5, generatedWeaponResult.ChosenUbglAmmoTemplate.Value, 20, inventory);
+ AddAmmoToSecureContainer(botId, 5, generatedWeaponResult.ChosenUbglAmmoTemplate.Value, 20, inventory);
}
///
@@ -505,13 +510,14 @@ public class BotWeaponGenerator(
/// Ammo type to add.
/// Size of the ammo stack to add.
/// Player inventory.
- protected void AddAmmoToSecureContainer(int stackCount, MongoId ammoTpl, int stackSize, BotBaseInventory inventory)
+ protected void AddAmmoToSecureContainer(MongoId botId, int stackCount, MongoId ammoTpl, int stackSize, BotBaseInventory inventory)
{
var container = new HashSet { EquipmentSlots.SecuredContainer };
for (var i = 0; i < stackCount; i++)
{
var id = new MongoId();
botGeneratorHelper.AddItemWithChildrenToEquipmentSlot(
+ botId,
container,
id,
ammoTpl,
diff --git a/Libraries/SPTarkov.Server.Core/Generators/PlayerScavGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/PlayerScavGenerator.cs
index 2cf6dd60..ecbb0bcd 100644
--- a/Libraries/SPTarkov.Server.Core/Generators/PlayerScavGenerator.cs
+++ b/Libraries/SPTarkov.Server.Core/Generators/PlayerScavGenerator.cs
@@ -113,6 +113,7 @@ public class PlayerScavGenerator(
// Add additional items to player scav as loot
AddAdditionalLootToPlayerScavContainers(
+ scavData.Id.Value,
playerScavKarmaSettings.LootItemsToAddChancePercent,
scavData,
[EquipmentSlots.TacticalVest, EquipmentSlots.Pockets, EquipmentSlots.Backpack]
@@ -133,10 +134,12 @@ public class PlayerScavGenerator(
///
/// Add items picked from `playerscav.lootItemsToAddChancePercent`
///
+ /// Bots unique identifier
/// dict of tpl + % chance to be added
///
/// Possible slotIds to add loot to
protected void AddAdditionalLootToPlayerScavContainers(
+ MongoId botId,
Dictionary possibleItemsToAdd,
BotBase scavData,
HashSet containersToAddTo
@@ -169,6 +172,7 @@ public class PlayerScavGenerator(
};
var result = botGeneratorHelper.AddItemWithChildrenToEquipmentSlot(
+ botId,
containersToAddTo,
itemsToAdd[0].Id,
itemTemplate.Id,
diff --git a/Libraries/SPTarkov.Server.Core/Generators/WeaponGen/Implementations/BarrelInventoryMagGen.cs b/Libraries/SPTarkov.Server.Core/Generators/WeaponGen/Implementations/BarrelInventoryMagGen.cs
index 85362fe4..641f06b4 100644
--- a/Libraries/SPTarkov.Server.Core/Generators/WeaponGen/Implementations/BarrelInventoryMagGen.cs
+++ b/Libraries/SPTarkov.Server.Core/Generators/WeaponGen/Implementations/BarrelInventoryMagGen.cs
@@ -38,6 +38,7 @@ public class BarrelInventoryMagGen(RandomUtil randomUtil, BotWeaponGeneratorHelp
}
botWeaponGeneratorHelper.AddAmmoIntoEquipmentSlots(
+ inventoryMagGen.GetBotId(),
inventoryMagGen.GetAmmoTemplate().Id,
(int)randomisedAmmoStackSize,
inventoryMagGen.GetPmcInventory(),
diff --git a/Libraries/SPTarkov.Server.Core/Generators/WeaponGen/Implementations/ExternalInventoryMagGen.cs b/Libraries/SPTarkov.Server.Core/Generators/WeaponGen/Implementations/ExternalInventoryMagGen.cs
index d723b04c..566ae17a 100644
--- a/Libraries/SPTarkov.Server.Core/Generators/WeaponGen/Implementations/ExternalInventoryMagGen.cs
+++ b/Libraries/SPTarkov.Server.Core/Generators/WeaponGen/Implementations/ExternalInventoryMagGen.cs
@@ -54,6 +54,7 @@ public class ExternalInventoryMagGen(
);
var fitsIntoInventory = botGeneratorHelper.AddItemWithChildrenToEquipmentSlot(
+ inventoryMagGen.GetBotId(),
[EquipmentSlots.TacticalVest, EquipmentSlots.Pockets],
magazineWithAmmo[0].Id,
magazineTpl,
diff --git a/Libraries/SPTarkov.Server.Core/Generators/WeaponGen/Implementations/InternalMagazineInventoryMagGen.cs b/Libraries/SPTarkov.Server.Core/Generators/WeaponGen/Implementations/InternalMagazineInventoryMagGen.cs
index c49003bf..2a9375ea 100644
--- a/Libraries/SPTarkov.Server.Core/Generators/WeaponGen/Implementations/InternalMagazineInventoryMagGen.cs
+++ b/Libraries/SPTarkov.Server.Core/Generators/WeaponGen/Implementations/InternalMagazineInventoryMagGen.cs
@@ -24,6 +24,7 @@ public class InternalMagazineInventoryMagGen(BotWeaponGeneratorHelper botWeaponG
inventoryMagGen.GetMagazineTemplate()
);
botWeaponGeneratorHelper.AddAmmoIntoEquipmentSlots(
+ inventoryMagGen.GetBotId(),
inventoryMagGen.GetAmmoTemplate().Id,
(int)bulletCount,
inventoryMagGen.GetPmcInventory(),
diff --git a/Libraries/SPTarkov.Server.Core/Generators/WeaponGen/Implementations/UbglExternalMagGen.cs b/Libraries/SPTarkov.Server.Core/Generators/WeaponGen/Implementations/UbglExternalMagGen.cs
index 5d196db6..962b1652 100644
--- a/Libraries/SPTarkov.Server.Core/Generators/WeaponGen/Implementations/UbglExternalMagGen.cs
+++ b/Libraries/SPTarkov.Server.Core/Generators/WeaponGen/Implementations/UbglExternalMagGen.cs
@@ -24,6 +24,7 @@ public class UbglExternalMagGen(BotWeaponGeneratorHelper botWeaponGeneratorHelpe
inventoryMagGen.GetMagazineTemplate()
);
botWeaponGeneratorHelper.AddAmmoIntoEquipmentSlots(
+ inventoryMagGen.GetBotId(),
inventoryMagGen.GetAmmoTemplate().Id,
(int)bulletCount,
inventoryMagGen.GetPmcInventory(),
diff --git a/Libraries/SPTarkov.Server.Core/Generators/WeaponGen/InventoryMagGen.cs b/Libraries/SPTarkov.Server.Core/Generators/WeaponGen/InventoryMagGen.cs
index 48f2f184..c5c899e7 100644
--- a/Libraries/SPTarkov.Server.Core/Generators/WeaponGen/InventoryMagGen.cs
+++ b/Libraries/SPTarkov.Server.Core/Generators/WeaponGen/InventoryMagGen.cs
@@ -1,4 +1,5 @@
using SPTarkov.DI.Annotations;
+using SPTarkov.Server.Core.Models.Common;
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
namespace SPTarkov.Server.Core.Generators.WeaponGen;
@@ -10,6 +11,7 @@ public class InventoryMagGen()
private readonly TemplateItem? _magazineTemplate;
private readonly GenerationData? _magCounts;
private readonly BotBaseInventory? _pmcInventory;
+ private readonly MongoId _botId;
private readonly TemplateItem? _weaponTemplate;
public InventoryMagGen(
@@ -17,7 +19,8 @@ public class InventoryMagGen()
TemplateItem magazineTemplate,
TemplateItem weaponTemplate,
TemplateItem ammoTemplate,
- BotBaseInventory pmcInventory
+ BotBaseInventory pmcInventory,
+ MongoId botId
)
: this()
{
@@ -26,6 +29,7 @@ public class InventoryMagGen()
_weaponTemplate = weaponTemplate;
_ammoTemplate = ammoTemplate;
_pmcInventory = pmcInventory;
+ _botId = botId;
}
public GenerationData GetMagCount()
@@ -52,4 +56,9 @@ public class InventoryMagGen()
{
return _pmcInventory!;
}
+
+ public MongoId GetBotId()
+ {
+ return _botId;
+ }
}
diff --git a/Libraries/SPTarkov.Server.Core/Helpers/BotGeneratorHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/BotGeneratorHelper.cs
index 9245cf8a..7d9219e8 100644
--- a/Libraries/SPTarkov.Server.Core/Helpers/BotGeneratorHelper.cs
+++ b/Libraries/SPTarkov.Server.Core/Helpers/BotGeneratorHelper.cs
@@ -1,7 +1,6 @@
using System.Collections.Frozen;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Constants;
-using SPTarkov.Server.Core.Extensions;
using SPTarkov.Server.Core.Models.Common;
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
using SPTarkov.Server.Core.Models.Enums;
@@ -24,6 +23,7 @@ public class BotGeneratorHelper(
InventoryHelper inventoryHelper,
ProfileActivityService profileActivityService,
ServerLocalisationService serverLocalisationService,
+ BotInventoryContainerService botInventoryContainerService,
ConfigServer configServer
)
{
@@ -428,9 +428,10 @@ public class BotGeneratorHelper(
}
///
- /// Adds an item with all its children into specified equipmentSlots, wherever it fits.
+ /// Adds an item with all its children into specified equipmentSlots, wherever it fits
///
- /// Slot to add item+children into
+ /// Bots unique identifier
+ /// Slot to try and add item+children into
/// Root item id to use as mod items parentId
/// Root items tpl id
/// Item to add
@@ -438,6 +439,7 @@ public class BotGeneratorHelper(
/// Container Ids with no space for more items
/// ItemAddedResult result object
public ItemAddedResult AddItemWithChildrenToEquipmentSlot(
+ MongoId botId,
HashSet equipmentSlots,
MongoId rootItemId,
MongoId rootItemTplId,
@@ -481,7 +483,7 @@ public class BotGeneratorHelper(
}
// Get container details from db
- var (isValidItem, itemDbDetails) = itemHelper.GetItem(container.Template);
+ var (isValidItem, containerDbDetails) = itemHelper.GetItem(container.Template);
if (!isValidItem)
{
logger.Warning(serverLocalisationService.GetText("bot-missing_container_with_tpl", container.Template));
@@ -490,7 +492,7 @@ public class BotGeneratorHelper(
continue;
}
- if (itemDbDetails?.Properties?.Grids is null || !itemDbDetails.Properties.Grids.Any())
+ if (containerDbDetails?.Properties?.Grids is null || !containerDbDetails.Properties.Grids.Any())
{
// Container has no slots to hold items, skip to next container
continue;
@@ -499,171 +501,23 @@ public class BotGeneratorHelper(
// Get x/y grid size of item
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;
- var totalSlotGridCount = itemDbDetails?.Properties?.Grids?.Count();
- foreach (var slotGrid in itemDbDetails?.Properties?.Grids ?? [])
- {
- // Grid is empty, skip or item size is bigger than grid
- if (IsGridSmallerThanItem(slotGrid, itemWidth, itemHeight))
- {
- continue;
- }
-
- // Can't put item type in grid, skip all grids as we're assuming they have the same rules
- if (!ItemAllowedInContainer(slotGrid, rootItemTplId))
- // Multiple containers, maybe next one allows item, only break out of loop for the containers grids
- {
- break;
- }
-
- // 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 each root item + children
- var containerItemsWithChildren = GetContainerItemsWithChildren(rootItemsInContainer, inventory.Items);
-
- if (slotGrid.Props is not null)
- {
- // Get rid of an items free/used spots in current grid
- var slotGridMap = inventoryHelper.GetContainerMap(
- slotGrid.Props.CellsH.GetValueOrDefault(),
- slotGrid.Props.CellsV.GetValueOrDefault(),
- containerItemsWithChildren,
- container.Id
- );
-
- // Try to fit item into grid
- var findSlotResult = slotGridMap.FindSlotForItem(itemWidth, itemHeight);
-
- // Free slot found, add item
- if (findSlotResult.Success ?? false)
- {
- var parentItem = itemWithChildrenList.FirstOrDefault(i => i.Id == rootItemId);
-
- // Set items parent to container id
- if (parentItem is not null)
- {
- parentItem.ParentId = container.Id;
- parentItem.SlotId = slotGrid.Name;
- parentItem.Location = new ItemLocation
- {
- X = findSlotResult.X,
- Y = findSlotResult.Y,
- R = findSlotResult.Rotation ?? false ? ItemRotation.Vertical : ItemRotation.Horizontal,
- };
- }
-
- (inventory.Items ?? []).AddRange(itemWithChildrenList);
-
- return ItemAddedResult.SUCCESS;
- }
- }
-
- // If we've checked all grids in container and reached this point, there's no space for item
- if (currentGridCount >= totalSlotGridCount)
- {
- break;
- }
-
- currentGridCount++;
- // 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 (containersIdFull is null)
+ var result = botInventoryContainerService.AddItemToBotContainer(
+ botId,
+ equipmentSlotId,
+ itemWithChildrenList,
+ inventory,
+ itemWidth,
+ itemHeight
+ );
+ if (result != ItemAddedResult.SUCCESS)
{
+ // Failed to add to container, try next
continue;
}
- // if the item was a one by one, we know it must be full. Or if the maps cant find a slot for a one by one
- if (itemWidth == 1 && itemHeight == 1)
- {
- containersIdFull.Add(equipmentSlotId.ToString());
- }
+ return result;
}
return ItemAddedResult.NO_SPACE;
}
-
- protected static bool IsGridSmallerThanItem(Grid slotGrid, int itemWidth, int itemHeight)
- {
- return slotGrid.Props?.CellsH == 0
- || slotGrid.Props?.CellsV == 0
- || itemWidth * itemHeight > slotGrid.Props?.CellsV * slotGrid.Props?.CellsH;
- }
-
- ///
- /// Take a list of items and check if they need children + add them
- ///
- ///
- ///
- ///
- protected List
- GetContainerItemsWithChildren(IEnumerable
- containerRootItems, IEnumerable
- inventoryItems)
- {
- var result = new List
- ();
- if (!containerRootItems.Any())
- {
- // Container has no root items
- return result;
- }
-
- // 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)
- itemsWithoutLocation.Insert(0, rootItem);
- var itemWithChildItems = itemsWithoutLocation.GetItemWithChildren(rootItem.Id);
-
- // Item had children, replace existing data with item + its children
- result.AddRange(itemWithChildItems);
- }
-
- return result;
- }
-
- ///
- /// Is the provided item allowed inside a container
- ///
- /// Items sub-grid we want to place item inside
- /// Item tpl being placed
- /// True if allowed
- protected bool ItemAllowedInContainer(Grid? slotGrid, MongoId itemTpl)
- {
- var propFilters = slotGrid?.Props?.Filters;
- if (propFilters is null || !propFilters.Any())
- // no filters, item is fine to add
- {
- return true;
- }
-
- // Check if item base type is excluded
- var itemDetails = itemHelper.GetItem(itemTpl).Value;
-
- // if item to add is found in exclude filter, not allowed
- var excludedFilter = propFilters.FirstOrDefault()?.ExcludedFilter ?? [];
- if (excludedFilter.Contains(itemDetails?.Parent ?? string.Empty))
- {
- return false;
- }
-
- // If Filter array only contains 1 filter and it is for basetype 'item', allow it
- var filter = propFilters.FirstOrDefault()?.Filter ?? [];
- if (filter.Count == 1 && filter.Contains(BaseClasses.ITEM))
- {
- return true;
- }
-
- // If allowed filter has something in it + filter doesn't have basetype 'item', not allowed
- if (filter.Count > 0 && !filter.Contains(itemDetails?.Parent ?? string.Empty))
- {
- return false;
- }
-
- return true;
- }
}
diff --git a/Libraries/SPTarkov.Server.Core/Helpers/BotWeaponGeneratorHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/BotWeaponGeneratorHelper.cs
index a90a4e36..04727e88 100644
--- a/Libraries/SPTarkov.Server.Core/Helpers/BotWeaponGeneratorHelper.cs
+++ b/Libraries/SPTarkov.Server.Core/Helpers/BotWeaponGeneratorHelper.cs
@@ -99,11 +99,13 @@ public class BotWeaponGeneratorHelper(
///
/// Add a specific number of cartridges to a bots inventory (defaults to vest and pockets)
///
+ /// Bots unique identifier
/// Ammo tpl to add to vest/pockets
/// Number of cartridges to add to vest/pockets
/// Bot inventory to add cartridges to
/// What equipment slots should bullets be added into
public void AddAmmoIntoEquipmentSlots(
+ MongoId botId,
MongoId ammoTpl,
int cartridgeCount,
BotBaseInventory inventory,
@@ -125,6 +127,7 @@ public class BotWeaponGeneratorHelper(
foreach (var ammoItem in ammoItems)
{
var result = botGeneratorHelper.AddItemWithChildrenToEquipmentSlot(
+ botId,
equipmentSlotsToAddTo,
ammoItem.Id,
ammoItem.Template,
diff --git a/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/TemplateItem.cs b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/TemplateItem.cs
index 8041d681..b7ab5de3 100644
--- a/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/TemplateItem.cs
+++ b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/TemplateItem.cs
@@ -1720,10 +1720,10 @@ public record GridFilter
public Dictionary? ExtensionData { get; set; }
[JsonPropertyName("Filter")]
- public HashSet? Filter { get; set; }
+ public HashSet? Filter { get; set; }
[JsonPropertyName("ExcludedFilter")]
- public HashSet? ExcludedFilter { get; set; }
+ public HashSet? ExcludedFilter { get; set; }
[JsonPropertyName("locked")]
public bool? Locked { get; set; }
diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Bots/GenerateEquipmentProperties.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Bots/GenerateEquipmentProperties.cs
index de0e8b08..afbe26e0 100644
--- a/Libraries/SPTarkov.Server.Core/Models/Spt/Bots/GenerateEquipmentProperties.cs
+++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Bots/GenerateEquipmentProperties.cs
@@ -11,6 +11,8 @@ public record GenerateEquipmentProperties
[JsonExtensionData]
public Dictionary? ExtensionData { get; set; }
+ public MongoId BotId { get; set; }
+
///
/// Root Slot being generated
///
diff --git a/Libraries/SPTarkov.Server.Core/Services/BotInventoryContainerService.cs b/Libraries/SPTarkov.Server.Core/Services/BotInventoryContainerService.cs
new file mode 100644
index 00000000..889be21e
--- /dev/null
+++ b/Libraries/SPTarkov.Server.Core/Services/BotInventoryContainerService.cs
@@ -0,0 +1,453 @@
+using System.Collections.Concurrent;
+using SPTarkov.DI.Annotations;
+using SPTarkov.Server.Core.Extensions;
+using SPTarkov.Server.Core.Helpers;
+using SPTarkov.Server.Core.Models.Common;
+using SPTarkov.Server.Core.Models.Eft.Common.Tables;
+using SPTarkov.Server.Core.Models.Enums;
+using SPTarkov.Server.Core.Models.Utils;
+
+namespace SPTarkov.Server.Core.Services;
+
+///
+/// Service for keeping track of items and their exact position inside a bots container
+///
+[Injectable]
+public class BotInventoryContainerService(ISptLogger logger, ItemHelper itemHelper)
+{
+ // botId/containerName
+ private readonly ConcurrentDictionary> _botContainers = new();
+
+ ///
+ /// Add a container + details to a bots cache ready to accept loot
+ ///
+ /// Unique identifier of bot
+ /// name of container e.g. "Backpack"
+ /// Inventory item loot will be linked to in bots inventory
+ public void AddEmptyContainerToBot(MongoId botId, EquipmentSlots containerName, Item containerInventoryItem)
+ {
+ // Add bot to dict if it doesn't exist
+ _botContainers.TryAdd(botId, new());
+
+ // Get the bots' currently cached containers
+ var containers = GetOrCreateBotContainerDictionary(botId);
+
+ // Add container to bot
+ if (!containers.ContainsKey(containerName))
+ {
+ var containerDbItem = itemHelper.GetItem(containerInventoryItem.Template);
+ containers.Add(containerName, new ContainerDetails(containerDbItem.Value, containerInventoryItem));
+ }
+ }
+
+ ///
+ /// Attempt to add an item + children to a container
+ ///
+ /// Bots unique id
+ /// Name of container to add to e.g. "Backpack"
+ /// Item and its children to add to container
+ /// Inventory to add Item+children to
+ /// Width of item with its children
+ /// Height of item with its children
+ /// ItemAddedResult
+ public ItemAddedResult AddItemToBotContainer(
+ MongoId botId,
+ EquipmentSlots containerName,
+ List
- itemAndChildren,
+ BotBaseInventory botInventory,
+ int itemWidth,
+ int itemHeight
+ )
+ {
+ var addResult = ItemAddedResult.UNKNOWN;
+
+ // Find bot and the container we will attempt to add into
+ var botContainers = GetOrCreateBotContainerDictionary(botId);
+
+ botContainers.TryGetValue(containerName, out var containerDetails);
+
+ if (containerDetails.ContainerGridDetails.Count == 0)
+ {
+ // No grids, cannot add item
+ return ItemAddedResult.NO_CONTAINERS;
+ }
+
+ if (!ItemAllowedInContainer(containerDetails, itemAndChildren))
+ // Multiple containers, maybe next one allows item, only break out of loop for the containers grids
+ {
+ return ItemAddedResult.INCOMPATIBLE_ITEM;
+ }
+
+ // Try to fit item into one of the containers' grids
+ var rootItem = itemAndChildren.FirstOrDefault();
+ var gridIndex = 0;
+ foreach (var gridDb in containerDetails.ContainerDbItem.Properties.Grids)
+ {
+ var gridDetails = containerDetails.ContainerGridDetails[gridIndex];
+ if (gridDetails.GridFull)
+ {
+ continue;
+ }
+
+ if (IsItemBiggerThanGrid(gridDetails.GridMap, itemWidth, itemHeight))
+ {
+ // Skip to next grid
+ continue;
+ }
+
+ // Look for a slot in the grid to place item
+ var findSlotResult = gridDetails.GridMap.FindSlotForItem(itemWidth, itemHeight);
+ if (findSlotResult.Success.GetValueOrDefault(false))
+ {
+ // It Fits!
+
+ // Set items parent to Id of container
+ if (rootItem is not null)
+ {
+ rootItem.ParentId = containerDetails.ContainerInventoryItem.Id;
+ rootItem.SlotId = gridDb.Name; // Can be name of container e.g. "Backpack" OR "2/3/4/5" depending on which grid of a container item is added to
+ rootItem.Location = new ItemLocation
+ {
+ X = findSlotResult.X,
+ Y = findSlotResult.Y,
+ R = findSlotResult.Rotation ?? false ? ItemRotation.Vertical : ItemRotation.Horizontal,
+ };
+ }
+
+ // Flag result as success to report to caller
+ addResult = ItemAddedResult.SUCCESS;
+
+ // Update grid with slots taken up by above item
+ FillGridRegion(
+ gridDetails.GridMap,
+ findSlotResult.X.Value,
+ findSlotResult.Y.Value,
+ findSlotResult.Rotation.GetValueOrDefault() ? itemHeight : itemWidth,
+ findSlotResult.Rotation.GetValueOrDefault() ? itemWidth : itemHeight
+ );
+
+ // Add item into bots inventory
+ botInventory.Items.AddRange(itemAndChildren);
+
+ // Exit loop, we've found a slot for item
+ break;
+ }
+
+ gridIndex++;
+
+ // Didn't fit, flag as no space, hopefully next grid has space
+ addResult = ItemAddedResult.NO_SPACE;
+
+ // If the item is 1x1 and it failed to fit, grid must be full
+ if (itemHeight == 1 && itemWidth == 1)
+ {
+ gridDetails.GridFull = true;
+ continue;
+ }
+
+ // Check if grid is full and flag
+ if (gridDetails.GridMap.ContainerIsFull())
+ {
+ gridDetails.GridFull = true;
+ }
+ }
+
+ return addResult;
+ }
+
+ ///
+ /// Attempt to add an item + children to a container at a specific x/y grid position
+ ///
+ /// Bots unique id
+ /// Name of container to add to e.g. "Backpack"
+ /// Item and its children to add to container
+ /// Inventory to add Item+children to
+ /// Width of item with its children
+ /// Height of item with its children
+ /// Details for where to place item in container grid
+ /// ItemAddedResult
+ public ItemAddedResult AddItemToBotContainerFixedPosition(
+ MongoId botId,
+ EquipmentSlots containerName,
+ List
- itemAndChildren,
+ BotBaseInventory botInventory,
+ int itemWidth,
+ int itemHeight,
+ ItemLocation fixedLocation
+ )
+ {
+ // Default result
+ var addResult = ItemAddedResult.UNKNOWN;
+
+ // Find bot and the container we are attempting to store item in
+ var botContainers = GetOrCreateBotContainerDictionary(botId);
+
+ botContainers.TryGetValue(containerName, out var containerDetails);
+
+ if (containerDetails.ContainerGridDetails.Count == 0)
+ {
+ // No grids, cannot add item
+ return ItemAddedResult.NO_CONTAINERS;
+ }
+
+ if (!ItemAllowedInContainer(containerDetails, itemAndChildren))
+ // Multiple containers, maybe next one allows item, only break out of loop for the containers grids
+ {
+ return ItemAddedResult.INCOMPATIBLE_ITEM;
+ }
+
+ // Try to fit item into one of the containers' grids
+ var rootItem = itemAndChildren.FirstOrDefault();
+ if (rootItem is null)
+ {
+ return ItemAddedResult.UNKNOWN;
+ }
+ foreach (var gridDetails in containerDetails.ContainerGridDetails)
+ {
+ if (gridDetails.GridFull)
+ {
+ // No space, skip early
+ continue;
+ }
+
+ if (IsItemBiggerThanGrid(gridDetails.GridMap, itemWidth, itemHeight))
+ {
+ // Skip early
+ continue;
+ }
+
+ // Look for a slot in the grid to place item
+ var result = gridDetails.GridMap.FillContainerMapWithItem(
+ fixedLocation.X.Value,
+ fixedLocation.Y.Value,
+ itemWidth,
+ itemHeight,
+ fixedLocation.R == ItemRotation.Vertical
+ );
+ if (result.Item1)
+ {
+ // It Fits!
+
+ // Parent root item to container
+ rootItem.ParentId = containerDetails.ContainerInventoryItem.Id;
+ rootItem.SlotId = containerName.ToString();
+ rootItem.Location = new ItemLocation
+ {
+ X = fixedLocation.X.Value,
+ Y = fixedLocation.Y.Value,
+ R = fixedLocation.R,
+ };
+
+ // Flag result as success to report to caller
+ addResult = ItemAddedResult.SUCCESS;
+
+ // Update internal grid with slots taken up by above item
+ FillGridRegion(
+ gridDetails.GridMap,
+ fixedLocation.X.Value,
+ fixedLocation.Y.Value,
+ fixedLocation.R == ItemRotation.Vertical ? itemHeight : itemWidth,
+ fixedLocation.R == ItemRotation.Vertical ? itemWidth : itemHeight
+ );
+
+ // Item fits + Added to layout grid, add item and children
+ //containerDetails.ItemsAndChildrenInContainer.AddRange(itemAndChildren);
+
+ // Add item into bots inventory
+ botInventory.Items.AddRange(itemAndChildren);
+
+ // Exit loop, we've found a position for item and can stop
+ break;
+ }
+
+ // Didn't fit, flag as no space, hopefully next grid has space
+ addResult = ItemAddedResult.NO_SPACE;
+
+ // If the item is 1x1 and it failed to fit, grid must be full
+ if (itemHeight == 1 && itemWidth == 1)
+ {
+ gridDetails.GridFull = true;
+ continue;
+ }
+
+ // Check if grid is full and flag
+ if (gridDetails.GridMap.ContainerIsFull())
+ {
+ gridDetails.GridFull = true;
+ }
+ }
+
+ return addResult;
+ }
+
+ ///
+ /// Helper - Get the bot-specific container details, create if data doesn't exist
+ ///
+ /// Bot unique identifier
+ /// Dictionary
+ protected Dictionary GetOrCreateBotContainerDictionary(MongoId botId)
+ {
+ if (!_botContainers.TryGetValue(botId, out var botContainers))
+ {
+ // Create blank dict ready for containers to be added
+ botContainers = new();
+ }
+
+ return botContainers;
+ }
+
+ ///
+ /// Fill region of a 2D array
+ ///
+ /// The 2D grid array to modify
+ /// The starting column index (left)
+ /// The starting row index (top)
+ /// The number of cells to update horizontally
+ /// The number of cells to update vertically
+ private void FillGridRegion(int[,] grid, int x, int y, int itemWidth, int itemHeight)
+ {
+ // Outer loop iterates through rows (from starting y position)
+ for (var row = y; row < y + itemHeight; row++)
+ {
+ // Inner loop iterates through columns (from starting x position)
+ for (var col = x; col < x + itemWidth; col++)
+ {
+ grid[row, col] = 1;
+ }
+ }
+ }
+
+ ///
+ /// Is the items subtype allowed inside this container / is it excluded from this container
+ ///
+ /// Details on the container we want to add item into
+ /// Item+children we want to add into container
+ /// true = item is allowed
+ private bool ItemAllowedInContainer(ContainerDetails containerDetails, List
- ? itemAndChildren)
+ {
+ // Assume all grids have same limitations
+ var firstSlotGrid = containerDetails.ContainerDbItem.Properties.Grids.FirstOrDefault();
+ var propFilters = firstSlotGrid?.Props?.Filters;
+ if (propFilters is null || !propFilters.Any())
+ // No filters, item is fine to add
+ {
+ return true;
+ }
+
+ // Check if item base type is excluded
+ var itemDetails = itemHelper.GetItem(itemAndChildren.FirstOrDefault().Template).Value;
+
+ // if item to add is found in exclude filter, not allowed
+ var excludedFilter = propFilters.FirstOrDefault()?.ExcludedFilter ?? [];
+ if (excludedFilter.Contains(itemDetails?.Parent ?? string.Empty))
+ {
+ return false;
+ }
+
+ // If Filter array only contains 1 filter and it is for basetype 'item', allow it
+ var filter = propFilters.FirstOrDefault()?.Filter ?? [];
+ if (filter.Count == 1 && filter.Contains(BaseClasses.ITEM))
+ {
+ return true;
+ }
+
+ // If allowed filter has something in it + filter doesn't have basetype 'item', not allowed
+ if (filter.Count > 0 && !filter.Contains(itemDetails?.Parent ?? string.Empty))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Is the items edge length bigger than the grid trying to hold it
+ ///
+ /// Container grid
+ /// Width of item
+ /// Height of item
+ /// true = item bigger than grid
+ private bool IsItemBiggerThanGrid(int[,] grid, int itemWidth, int itemHeight)
+ {
+ var gridHeight = grid.GetLength(0);
+ var gridWidth = grid.GetLength(1);
+
+ // Check if it can fit in either orientation
+ var fitsNormally = itemWidth <= gridWidth && itemHeight <= gridHeight;
+ var fitsRotated = itemHeight <= gridWidth && itemWidth <= gridHeight;
+
+ // Fails both checks
+ return !fitsNormally && !fitsRotated;
+ }
+
+ ///
+ /// Get a bots container details from cache by its id
+ ///
+ /// Identifier of bot to get details of
+ /// Dictionary of containers and their details
+ public Dictionary? GetBotContainer(MongoId botId)
+ {
+ return GetOrCreateBotContainerDictionary(botId);
+ }
+
+ ///
+ /// Clear the cache of all bot containers
+ ///
+ public void ClearCache()
+ {
+ _botContainers.Clear();
+ }
+
+ ///
+ /// Clear specific bot container details from cache
+ ///
+ /// Bot identifier
+ public void ClearCache(MongoId botId)
+ {
+ _botContainers.Remove(botId, out _);
+ }
+
+ public record ContainerDetails
+ {
+ public ContainerDetails(TemplateItem containerDbItem, Item containerInventoryItem)
+ {
+ ContainerDbItem = containerDbItem;
+ ContainerInventoryItem = containerInventoryItem;
+ // Add all grids for this container
+ foreach (var grid in containerDbItem.Properties.Grids)
+ {
+ ContainerGridDetails.Add(
+ new ContainerMapDetails
+ {
+ GridMap = new int[grid.Props.CellsV.GetValueOrDefault(), grid.Props.CellsH.GetValueOrDefault()],
+ GridFull = false,
+ }
+ );
+ }
+ }
+
+ ///
+ /// Grid layout and flag if grid is full
+ ///
+ public List ContainerGridDetails { get; } = [];
+
+ ///
+ /// Db record for the container holding items
+ ///
+ public TemplateItem ContainerDbItem { get; set; }
+
+ ///
+ /// Inventory item representing the container
+ ///
+ public Item ContainerInventoryItem { get; set; }
+
+ // TODO: implement this + add checks inside AddItemToBotContainer for perf improvement
+ public bool ContainerFull { get; set; } = false;
+ }
+
+ public record ContainerMapDetails
+ {
+ public int[,] GridMap { get; init; }
+ public bool GridFull { get; set; }
+ }
+}
diff --git a/Testing/UnitTests/Tests/Helpers/BotGeneratorHelperTests.cs b/Testing/UnitTests/Tests/Helpers/BotGeneratorHelperTests.cs
index 1cf487e6..f3a0a2c7 100644
--- a/Testing/UnitTests/Tests/Helpers/BotGeneratorHelperTests.cs
+++ b/Testing/UnitTests/Tests/Helpers/BotGeneratorHelperTests.cs
@@ -5,6 +5,7 @@ using SPTarkov.Server.Core.Models.Common;
using SPTarkov.Server.Core.Models.Eft.Common;
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
using SPTarkov.Server.Core.Models.Enums;
+using SPTarkov.Server.Core.Services;
namespace UnitTests.Tests.Helpers;
@@ -13,12 +14,16 @@ public class BotGeneratorHelperTests
{
private BotGeneratorHelper _botGeneratorHelper;
private BotLootGenerator _botLootGenerator;
+ private BotInventoryContainerService _botInventoryContainerService;
+ private ItemHelper _itemHelper;
[OneTimeSetUp]
public void Initialize()
{
_botGeneratorHelper = DI.GetInstance().GetService();
+ _itemHelper = DI.GetInstance().GetService();
_botLootGenerator = DI.GetInstance().GetService();
+ _botInventoryContainerService = DI.GetInstance().GetService();
}
#region AddItemWithChildrenToEquipmentSlot
@@ -26,6 +31,7 @@ public class BotGeneratorHelperTests
[Test]
public void AddItemWithChildrenToEquipmentSlot_fit_vertical()
{
+ var botId = new MongoId();
var stashId = new MongoId();
var equipmentId = new MongoId();
var botInventory = new BotBaseInventory
@@ -42,14 +48,16 @@ public class BotGeneratorHelperTests
// Has a 3grids, first is a 3hx5v grid
Template = ItemTpl.BACKPACK_EBERLESTOCK_G2_GUNSLINGER_II_BACKPACK_DRY_EARTH,
ParentId = equipmentId,
- SlotId = "Backpack",
+ SlotId = nameof(EquipmentSlots.Backpack),
};
botInventory.Items.Add(backpack);
+ _botInventoryContainerService.AddEmptyContainerToBot(botId, EquipmentSlots.Backpack, backpack);
var rootWeaponId = new MongoId();
var weaponWithChildren = CreateMp18(rootWeaponId);
var result = _botGeneratorHelper.AddItemWithChildrenToEquipmentSlot(
+ botId,
[EquipmentSlots.Backpack],
rootWeaponId,
ItemTpl.SHOTGUN_MP18_762X54R_SINGLESHOT_RIFLE,
@@ -93,6 +101,7 @@ public class BotGeneratorHelperTests
[Test]
public void AddItemWithChildrenToEquipmentSlot_fit_horizontal()
{
+ var botId = new MongoId();
var stashId = new MongoId();
var equipmentId = new MongoId();
var botInventory = new BotBaseInventory
@@ -108,14 +117,16 @@ public class BotGeneratorHelperTests
Id = new MongoId(),
Template = ItemTpl.BACKPACK_ANA_TACTICAL_BETA_2_BATTLE_BACKPACK_OLIVE_DRAB,
ParentId = equipmentId,
- SlotId = "Backpack",
+ SlotId = nameof(EquipmentSlots.Backpack),
};
botInventory.Items.Add(backpack);
+ _botInventoryContainerService.AddEmptyContainerToBot(botId, EquipmentSlots.Backpack, backpack);
var rootWeaponId = new MongoId();
var weaponWithChildren = CreateMp18(rootWeaponId);
var result = _botGeneratorHelper.AddItemWithChildrenToEquipmentSlot(
+ botId,
[EquipmentSlots.Backpack],
rootWeaponId,
ItemTpl.SHOTGUN_MP18_762X54R_SINGLESHOT_RIFLE,
@@ -130,7 +141,7 @@ public class BotGeneratorHelperTests
{ ItemTpl.BARTER_GOLD_SKULL_RING, 1 },
{ ItemTpl.BARTER_PACK_OF_NAILS, 1 },
};
- _botLootGenerator.AddLootFromPool(tplsToAdd, [EquipmentSlots.Backpack], 4, botInventory, "assault", null);
+ _botLootGenerator.AddLootFromPool(botId, tplsToAdd, [EquipmentSlots.Backpack], 4, botInventory, "assault", null);
Assert.AreEqual(ItemAddedResult.SUCCESS, result);
@@ -152,37 +163,40 @@ public class BotGeneratorHelperTests
[Test]
public void AddItemWithChildrenToEquipmentSlot_fit_vertical_with_items_in_backpack()
{
+ var botId = new MongoId();
var botInventory = new BotBaseInventory { Items = [] };
var backpack = new Item
{
Id = new MongoId(),
// Has a 3hx5v grid first
Template = ItemTpl.BACKPACK_EBERLESTOCK_G2_GUNSLINGER_II_BACKPACK_DRY_EARTH,
- SlotId = "Backpack",
+ SlotId = nameof(EquipmentSlots.Backpack),
};
botInventory.Items.Add(backpack);
- botInventory.Items.Add(
- new Item
+ _botInventoryContainerService.AddEmptyContainerToBot(botId, EquipmentSlots.Backpack, backpack);
+
+ var akbsCartridge = new Item
+ {
+ Id = new MongoId(),
+ Template = ItemTpl.AMMO_762X25TT_AKBS,
+ ParentId = backpack.Id,
+ SlotId = "main",
+ Location = new ItemLocation
{
- Id = new MongoId(),
- Template = ItemTpl.AMMO_762X25TT_AKBS,
- ParentId = backpack.Id,
- SlotId = "main",
- Location = new ItemLocation
- {
- X = 0,
- Y = 0,
- R = ItemRotation.Horizontal,
- },
- Upd = new Upd { StackObjectsCount = 1 },
- }
- );
+ X = 0,
+ Y = 0,
+ R = ItemRotation.Horizontal,
+ },
+ Upd = new Upd { StackObjectsCount = 1 },
+ };
+ _botInventoryContainerService.AddItemToBotContainer(botId, EquipmentSlots.Backpack, [akbsCartridge], botInventory, 1, 1);
var rootWeaponId = new MongoId();
var weaponWithChildren = CreateMp18(rootWeaponId);
var result = _botGeneratorHelper.AddItemWithChildrenToEquipmentSlot(
+ botId,
[EquipmentSlots.Backpack],
rootWeaponId,
ItemTpl.SHOTGUN_MP18_762X54R_SINGLESHOT_RIFLE,
@@ -204,18 +218,28 @@ public class BotGeneratorHelperTests
[Test]
public void AddItemWithChildrenToEquipmentSlot_no_space_in_first_grid_choose_second_grid()
{
+ var botId = new MongoId();
var botInventory = new BotBaseInventory { Items = [] };
var backpack = new Item
{
Id = new MongoId(),
// Has a 3hx5v grid first
Template = ItemTpl.BACKPACK_EBERLESTOCK_G2_GUNSLINGER_II_BACKPACK_DRY_EARTH,
- SlotId = "Backpack",
+ SlotId = nameof(EquipmentSlots.Backpack),
};
botInventory.Items.Add(backpack);
+ _botInventoryContainerService.AddEmptyContainerToBot(botId, EquipmentSlots.Backpack, backpack);
- botInventory.Items.AddRange(
- new Item
+ // Insert items at specific locations
+ var takenSlots = new List
+ {
+ new() { X = 0, Y = 0 },
+ new() { X = 1, Y = 0 },
+ new() { X = 2, Y = 0 },
+ };
+ foreach (var takenSlot in takenSlots)
+ {
+ var itemToAdd = new Item
{
Id = new MongoId(),
Template = ItemTpl.AMMO_762X25TT_AKBS,
@@ -223,46 +247,29 @@ public class BotGeneratorHelperTests
SlotId = "main",
Location = new ItemLocation
{
- X = 0,
- Y = 0,
+ X = (int)takenSlot.X.Value,
+ Y = (int)takenSlot.Y.Value,
R = ItemRotation.Horizontal,
},
Upd = new Upd { StackObjectsCount = 1 },
- },
- new Item
- {
- Id = new MongoId(),
- Template = ItemTpl.AMMO_762X25TT_AKBS,
- ParentId = backpack.Id,
- SlotId = "main",
- Location = new ItemLocation
- {
- X = 1,
- Y = 0,
- R = ItemRotation.Horizontal,
- },
- Upd = new Upd { StackObjectsCount = 1 },
- },
- new Item
- {
- Id = new MongoId(),
- Template = ItemTpl.AMMO_762X25TT_AKBS,
- ParentId = backpack.Id,
- SlotId = "main",
- Location = new ItemLocation
- {
- X = 2,
- Y = 0,
- R = ItemRotation.Horizontal,
- },
- Upd = new Upd { StackObjectsCount = 1 },
- }
- );
+ };
+
+ _botInventoryContainerService.AddItemToBotContainerFixedPosition(
+ botId,
+ EquipmentSlots.Backpack,
+ [itemToAdd],
+ botInventory,
+ 1,
+ 1,
+ (ItemLocation)itemToAdd.Location
+ );
+ }
var rootWeaponId = new MongoId();
var weaponWithChildren = CreateMp18(rootWeaponId);
var result = _botGeneratorHelper.AddItemWithChildrenToEquipmentSlot(
+ botId,
[EquipmentSlots.Backpack],
rootWeaponId,
ItemTpl.SHOTGUN_MP18_762X54R_SINGLESHOT_RIFLE,
@@ -271,8 +278,10 @@ public class BotGeneratorHelperTests
);
Assert.AreEqual(ItemAddedResult.SUCCESS, result);
+
var weaponRoot = weaponWithChildren.FirstOrDefault(item => item.Id == rootWeaponId);
Assert.AreEqual("1", weaponRoot.SlotId);
+
Assert.AreEqual((weaponRoot.Location as ItemLocation).X, 0);
Assert.AreEqual((weaponRoot.Location as ItemLocation).Y, 0);
Assert.AreEqual((weaponRoot.Location as ItemLocation).R, ItemRotation.Vertical);
@@ -284,18 +293,29 @@ public class BotGeneratorHelperTests
[Test]
public void AddItemWithChildrenToEquipmentSlot_no_space()
{
+ var botId = new MongoId();
var botInventory = new BotBaseInventory { Items = [] };
var backpack = new Item
{
Id = new MongoId(),
// Has a 4hx5v grid first
Template = ItemTpl.BACKPACK_WARTECH_BERKUT_BB102_BACKPACK_ATACS_FG,
- SlotId = "Backpack",
+ SlotId = nameof(EquipmentSlots.Backpack),
};
botInventory.Items.Add(backpack);
+ _botInventoryContainerService.AddEmptyContainerToBot(botId, EquipmentSlots.Backpack, backpack);
- botInventory.Items.AddRange(
- new Item
+ // Insert items at specific locations
+ var takenSlots = new List
+ {
+ new() { X = 0, Y = 0 },
+ new() { X = 1, Y = 0 },
+ new() { X = 2, Y = 0 },
+ new() { X = 3, Y = 0 },
+ };
+ foreach (var takenSlot in takenSlots)
+ {
+ var itemToAdd = new Item
{
Id = new MongoId(),
Template = ItemTpl.AMMO_762X25TT_AKBS,
@@ -303,60 +323,29 @@ public class BotGeneratorHelperTests
SlotId = "main",
Location = new ItemLocation
{
- X = 0,
- Y = 0,
+ X = (int)takenSlot.X.Value,
+ Y = (int)takenSlot.Y.Value,
R = ItemRotation.Horizontal,
},
Upd = new Upd { StackObjectsCount = 1 },
- },
- new Item
- {
- Id = new MongoId(),
- Template = ItemTpl.AMMO_762X25TT_AKBS,
- ParentId = backpack.Id,
- SlotId = "main",
- Location = new ItemLocation
- {
- X = 1,
- Y = 0,
- R = ItemRotation.Horizontal,
- },
- Upd = new Upd { StackObjectsCount = 1 },
- },
- new Item
- {
- Id = new MongoId(),
- Template = ItemTpl.AMMO_762X25TT_AKBS,
- ParentId = backpack.Id,
- SlotId = "main",
- Location = new ItemLocation
- {
- X = 2,
- Y = 0,
- R = ItemRotation.Horizontal,
- },
- Upd = new Upd { StackObjectsCount = 1 },
- },
- new Item
- {
- Id = new MongoId(),
- Template = ItemTpl.AMMO_762X25TT_AKBS,
- ParentId = backpack.Id,
- SlotId = "main",
- Location = new ItemLocation
- {
- X = 3,
- Y = 0,
- R = ItemRotation.Horizontal,
- },
- Upd = new Upd { StackObjectsCount = 1 },
- }
- );
+ };
+
+ _botInventoryContainerService.AddItemToBotContainerFixedPosition(
+ botId,
+ EquipmentSlots.Backpack,
+ [itemToAdd],
+ botInventory,
+ 1,
+ 1,
+ (ItemLocation)itemToAdd.Location
+ );
+ }
var rootWeaponId = new MongoId();
var weaponWithChildren = CreateMp18(rootWeaponId);
var result = _botGeneratorHelper.AddItemWithChildrenToEquipmentSlot(
+ botId,
[EquipmentSlots.Backpack],
rootWeaponId,
ItemTpl.SHOTGUN_MP18_762X54R_SINGLESHOT_RIFLE,
@@ -373,16 +362,20 @@ public class BotGeneratorHelperTests
[Test]
public void AddItemWithChildrenToEquipmentSlot_custom_gun_no_space()
{
+ var botId = new MongoId();
var botInventory = new BotBaseInventory { Items = [] };
var backpack = new Item
{
Id = new MongoId(),
// Has a 4hx5v grid first
Template = ItemTpl.BACKPACK_GRUPPA_99_T30_BACKPACK_BLACK,
- SlotId = "Backpack",
+ SlotId = nameof(EquipmentSlots.Backpack),
};
- botInventory.Items.Add(backpack);
+ botInventory.Items.Add(backpack);
+ _botInventoryContainerService.AddEmptyContainerToBot(botId, EquipmentSlots.Backpack, backpack);
+
+ // Insert items at specific locations to ensure there's no space for adding the weapon
var takenSlots = new List
{
new() { X = 1, Y = 0 },
@@ -407,27 +400,40 @@ public class BotGeneratorHelperTests
};
foreach (var takenSlot in takenSlots)
{
- botInventory.Items.Add(
- new Item
+ var itemToAdd = new Item
+ {
+ Id = new MongoId(),
+ Template = ItemTpl.AMMO_762X25TT_AKBS,
+ ParentId = backpack.Id,
+ SlotId = "main",
+ Location = new ItemLocation
{
- Id = new MongoId(),
- Template = ItemTpl.AMMO_762X25TT_AKBS,
- ParentId = backpack.Id,
- SlotId = "main",
- Location = new ItemLocation
- {
- X = (int)takenSlot.X.Value,
- Y = (int)takenSlot.Y.Value,
- R = ItemRotation.Horizontal,
- },
- Upd = new Upd { StackObjectsCount = 1 },
- }
+ X = (int)takenSlot.X.Value,
+ Y = (int)takenSlot.Y.Value,
+ R = ItemRotation.Horizontal,
+ },
+ Upd = new Upd { StackObjectsCount = 1 },
+ };
+
+ _botInventoryContainerService.AddItemToBotContainerFixedPosition(
+ botId,
+ EquipmentSlots.Backpack,
+ [itemToAdd],
+ botInventory,
+ 1,
+ 1,
+ (ItemLocation)itemToAdd.Location
);
}
var rootWeaponId = new MongoId();
var weaponWithChildren = new List
- ();
- var root = new Item { Id = rootWeaponId, Template = ItemTpl.ASSAULTRIFLE_MOLOT_ARMS_VPO136_VEPRKM_762X39_CARBINE };
+ var root = new Item
+ {
+ Id = rootWeaponId,
+ Template = ItemTpl.ASSAULTRIFLE_MOLOT_ARMS_VPO136_VEPRKM_762X39_CARBINE,
+ ParentId = backpack.Id,
+ };
weaponWithChildren.Add(root);
var stock = new Item
@@ -458,6 +464,7 @@ public class BotGeneratorHelperTests
weaponWithChildren.Add(muzzle);
var result = _botGeneratorHelper.AddItemWithChildrenToEquipmentSlot(
+ botId,
[EquipmentSlots.Backpack],
rootWeaponId,
root.Template,