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,