Refactor of how bot loot has its position picked to improve performance (#548)

* Initial work on moving container space checks into new class

* Updated tests

Wired up service to save item into inventory when space is found

Updated `FillContainerMapWithItem` to return outcome and not throw exception on failure

Add containers to bot when generating bot equipment

Clean bot cache after completion of loot generation

Removed redundant code from `AddItemWithChildrenToEquipmentSlot`

Removed unnecessary Singleton status from `BotInventoryContainerService`

* Clean-up of service

* Add botId xml docs

* Updated documentation for `FillContainerMapWithItem`

* Code review fixes and improvements

* Remove TODO

---------

Co-authored-by: Chomp <dev@dev.sp-tarkov.com>
This commit is contained in:
Chomp
2025-08-13 15:35:57 +00:00
committed by GitHub
parent 965d503021
commit b061200803
17 changed files with 719 additions and 314 deletions
@@ -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
/// <param name="itemXWidth">Items width</param>
/// <param name="itemYHeight">Items height</param>
/// <param name="isRotated">is item rotated</param>
public static void FillContainerMapWithItem(
/// <returns>bool = true when successful, string = error message if failed</returns>
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);
}
/// <summary>
@@ -158,7 +162,7 @@ public static class ContainerExtensions
/// </summary>
/// <param name="container2D">Container to check</param>
/// <returns>True = full</returns>
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
@@ -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);
@@ -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<EquipmentSlots> _equipmentSlotsWithInventory =
[
EquipmentSlots.Pockets,
EquipmentSlots.TacticalVest,
EquipmentSlots.Backpack,
EquipmentSlots.SecuredContainer,
];
// Slots handled individually inside `GenerateAndAddEquipmentToBot`
private static readonly FrozenSet<EquipmentSlots> _excludedEquipmentSlots =
[
@@ -56,6 +66,7 @@ public class BotInventoryGenerator(
/// <summary>
/// Add equipment/weapons/loot to bot
/// </summary>
/// <param name="botId">Bots unique identifier</param>
/// <param name="sessionId">Session id</param>
/// <param name="botJsonTemplate">Base json db file for the bot having its loot generated</param>
/// <param name="botRole">Role bot has (assault/pmcBot)</param>
@@ -64,6 +75,7 @@ public class BotInventoryGenerator(
/// <param name="chosenGameVersion">Game version for bot, only really applies for PMCs</param>
/// <returns>PmcInventory object with equipment/weapons/loot</returns>
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(
/// <summary>
/// Add equipment to a bot
/// </summary>
/// <param name="botId">Bots unique identifier</param>
/// <param name="sessionId">Session id</param>
/// <param name="templateInventory">bot/x.json data from db</param>
/// <param name="wornItemChances">Chances items will be added to bot</param>
@@ -163,6 +181,7 @@ public class BotInventoryGenerator(
/// <param name="isPmc">Is the generated bot a PMC</param>
/// <param name="raidConfig">RadiConfig</param>
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(
/// <summary>
/// Work out what weapons bot should have equipped and add them to bot inventory
/// </summary>
/// <param name="botId">Bots unique identifier</param>
/// <param name="templateInventory">bot/x.json data from db</param>
/// <param name="equipmentChances">Chances bot can have equipment equipped</param>
/// <param name="sessionId">Session id</param>
@@ -631,6 +664,7 @@ public class BotInventoryGenerator(
/// <param name="itemGenerationLimitsMinMax">Limits for items the bot can have</param>
/// <param name="botLevel">level of bot having weapon generated</param>
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(
/// <summary>
/// Add weapon + spare mags/ammo to bots inventory
/// </summary>
/// <param name="botId">Bots unique identifier</param>
/// <param name="sessionId">Session id</param>
/// <param name="weaponSlot">Weapon slot being generated</param>
/// <param name="templateInventory">bot/x.json data from db</param>
@@ -699,6 +735,7 @@ public class BotInventoryGenerator(
/// <param name="itemGenerationWeights"></param>
/// <param name="botLevel"></param>
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
);
}
}
@@ -57,6 +57,7 @@ public class BotLootGenerator(
/// <summary>
/// Add loot to bots containers
/// </summary>
/// <param name="botId">Bots unique identifier</param>
/// <param name="sessionId">Session id</param>
/// <param name="botJsonTemplate">Clone of Base JSON db file for the bot having its loot generated</param>
/// <param name="botGenerationDetails">Details relating to generating a bot</param>
@@ -65,6 +66,7 @@ public class BotLootGenerator(
/// <param name="botInventory">Inventory to add loot to</param>
/// <param name="botLevel">Level of bot</param>
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(
/// </summary>
/// <param name="botInventory">Inventory to add items to</param>
/// <param name="botRole">Role of bot (pmcBEAR/pmcUSEC)</param>
protected void AddForcedMedicalItemsToPmcSecure(BotBaseInventory botInventory, string botRole)
/// <param name="botId">Bots unique identifier</param>
protected void AddForcedMedicalItemsToPmcSecure(BotBaseInventory botInventory, string botRole, MongoId botId)
{
// surv12
AddLootFromPool(
botId,
new Dictionary<MongoId, double> { { ItemTpl.MEDICAL_SURV12_FIELD_SURGICAL_KIT, 1 } },
[EquipmentSlots.SecuredContainer],
1,
@@ -380,21 +397,14 @@ public class BotLootGenerator(
);
// AFAK
AddLootFromPool(
new Dictionary<MongoId, double> { { ItemTpl.MEDKIT_AFAK_TACTICAL_INDIVIDUAL_FIRST_AID_KIT, 1 } },
[EquipmentSlots.SecuredContainer],
10,
botInventory,
botRole,
null,
0,
true
);
var afaks = new Dictionary<MongoId, double> { { ItemTpl.MEDKIT_AFAK_TACTICAL_INDIVIDUAL_FIRST_AID_KIT, 1 } };
AddLootFromPool(botId, afaks, [EquipmentSlots.SecuredContainer], 10, botInventory, botRole, null, 0, true);
}
/// <summary>
/// Take random items from a pool and add to an inventory until totalItemCount or totalValueLimit or space limit is reached
/// </summary>
/// <param name="botId">Bots unique identifier</param>
/// <param name="pool">Pool of items to pick from with weight</param>
/// <param name="equipmentSlots">What equipment slot will the loot items be added to</param>
/// <param name="totalItemCount">Max count of items to add</param>
@@ -405,6 +415,7 @@ public class BotLootGenerator(
/// <param name="totalValueLimitRub">Total value of loot allowed in roubles</param>
/// <param name="isPmc">Is bot being generated for a pmc</param>
protected internal void AddLootFromPool(
MongoId botId,
Dictionary<MongoId, double> pool,
HashSet<EquipmentSlots> 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(
/// <summary>
/// Add generated weapons to inventory as loot
/// </summary>
/// <param name="botId">Bots unique identifier</param>
/// <param name="sessionId">Session/Player id</param>
/// <param name="botInventory">Inventory to add preset to</param>
/// <param name="equipmentSlot">Slot to place the preset in (backpack)</param>
@@ -629,6 +642,7 @@ public class BotLootGenerator(
/// <param name="botLevel"></param>
/// <param name="containersIdFull"></param>
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,
@@ -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
/// </summary>
/// <param name="botId">Bots unique identifier</param>
/// <param name="generatedWeaponResult">Object with properties for generated weapon (weapon mods pool / weapon template / ammo tpl)</param>
/// <param name="magWeights">Magazine weights for count to add to inventory</param>
/// <param name="inventory">Inventory to add magazines to</param>
/// <param name="botRole">The bot type we're generating extra mags for</param>
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(
/// <summary>
/// Add Grenades for UBGL to bot's vest and secure container
/// </summary>
/// <param name="botId">Bots unique identifier</param>
/// <param name="weaponMods">Weapon list with mods</param>
/// <param name="generatedWeaponResult">Result of weapon generation</param>
/// <param name="inventory">Bot inventory to add grenades to</param>
protected void AddUbglGrenadesToBotInventory(
MongoId botId,
List<Item> 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);
}
/// <summary>
@@ -505,13 +510,14 @@ public class BotWeaponGenerator(
/// <param name="ammoTpl">Ammo type to add.</param>
/// <param name="stackSize">Size of the ammo stack to add.</param>
/// <param name="inventory">Player inventory.</param>
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> { EquipmentSlots.SecuredContainer };
for (var i = 0; i < stackCount; i++)
{
var id = new MongoId();
botGeneratorHelper.AddItemWithChildrenToEquipmentSlot(
botId,
container,
id,
ammoTpl,
@@ -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(
/// <summary>
/// Add items picked from `playerscav.lootItemsToAddChancePercent`
/// </summary>
/// <param name="botId">Bots unique identifier</param>
/// <param name="possibleItemsToAdd">dict of tpl + % chance to be added</param>
/// <param name="scavData"></param>
/// <param name="containersToAddTo">Possible slotIds to add loot to</param>
protected void AddAdditionalLootToPlayerScavContainers(
MongoId botId,
Dictionary<MongoId, double> possibleItemsToAdd,
BotBase scavData,
HashSet<EquipmentSlots> containersToAddTo
@@ -169,6 +172,7 @@ public class PlayerScavGenerator(
};
var result = botGeneratorHelper.AddItemWithChildrenToEquipmentSlot(
botId,
containersToAddTo,
itemsToAdd[0].Id,
itemTemplate.Id,
@@ -38,6 +38,7 @@ public class BarrelInventoryMagGen(RandomUtil randomUtil, BotWeaponGeneratorHelp
}
botWeaponGeneratorHelper.AddAmmoIntoEquipmentSlots(
inventoryMagGen.GetBotId(),
inventoryMagGen.GetAmmoTemplate().Id,
(int)randomisedAmmoStackSize,
inventoryMagGen.GetPmcInventory(),
@@ -54,6 +54,7 @@ public class ExternalInventoryMagGen(
);
var fitsIntoInventory = botGeneratorHelper.AddItemWithChildrenToEquipmentSlot(
inventoryMagGen.GetBotId(),
[EquipmentSlots.TacticalVest, EquipmentSlots.Pockets],
magazineWithAmmo[0].Id,
magazineTpl,
@@ -24,6 +24,7 @@ public class InternalMagazineInventoryMagGen(BotWeaponGeneratorHelper botWeaponG
inventoryMagGen.GetMagazineTemplate()
);
botWeaponGeneratorHelper.AddAmmoIntoEquipmentSlots(
inventoryMagGen.GetBotId(),
inventoryMagGen.GetAmmoTemplate().Id,
(int)bulletCount,
inventoryMagGen.GetPmcInventory(),
@@ -24,6 +24,7 @@ public class UbglExternalMagGen(BotWeaponGeneratorHelper botWeaponGeneratorHelpe
inventoryMagGen.GetMagazineTemplate()
);
botWeaponGeneratorHelper.AddAmmoIntoEquipmentSlots(
inventoryMagGen.GetBotId(),
inventoryMagGen.GetAmmoTemplate().Id,
(int)bulletCount,
inventoryMagGen.GetPmcInventory(),
@@ -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;
}
}
@@ -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(
}
/// <summary>
/// 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
/// </summary>
/// <param name="equipmentSlots">Slot to add item+children into</param>
/// <param name="botId">Bots unique identifier</param>
/// <param name="equipmentSlots">Slot to try and add item+children into</param>
/// <param name="rootItemId">Root item id to use as mod items parentId</param>
/// <param name="rootItemTplId">Root items tpl id</param>
/// <param name="itemWithChildren">Item to add</param>
@@ -438,6 +439,7 @@ public class BotGeneratorHelper(
/// <param name="containersIdFull">Container Ids with no space for more items</param>
/// <returns>ItemAddedResult result object</returns>
public ItemAddedResult AddItemWithChildrenToEquipmentSlot(
MongoId botId,
HashSet<EquipmentSlots> 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;
}
/// <summary>
/// Take a list of items and check if they need children + add them
/// </summary>
/// <param name="containerRootItems"></param>
/// <param name="inventoryItems"></param>
/// <returns></returns>
protected List<Item> GetContainerItemsWithChildren(IEnumerable<Item> containerRootItems, IEnumerable<Item> inventoryItems)
{
var result = new List<Item>();
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;
}
/// <summary>
/// Is the provided item allowed inside a container
/// </summary>
/// <param name="slotGrid">Items sub-grid we want to place item inside</param>
/// <param name="itemTpl">Item tpl being placed</param>
/// <returns>True if allowed</returns>
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;
}
}
@@ -99,11 +99,13 @@ public class BotWeaponGeneratorHelper(
/// <summary>
/// Add a specific number of cartridges to a bots inventory (defaults to vest and pockets)
/// </summary>
/// <param name="botId">Bots unique identifier</param>
/// <param name="ammoTpl">Ammo tpl to add to vest/pockets</param>
/// <param name="cartridgeCount">Number of cartridges to add to vest/pockets</param>
/// <param name="inventory">Bot inventory to add cartridges to</param>
/// <param name="equipmentSlotsToAddTo">What equipment slots should bullets be added into</param>
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,
@@ -1720,10 +1720,10 @@ public record GridFilter
public Dictionary<string, object>? ExtensionData { get; set; }
[JsonPropertyName("Filter")]
public HashSet<string>? Filter { get; set; }
public HashSet<MongoId>? Filter { get; set; }
[JsonPropertyName("ExcludedFilter")]
public HashSet<string>? ExcludedFilter { get; set; }
public HashSet<MongoId>? ExcludedFilter { get; set; }
[JsonPropertyName("locked")]
public bool? Locked { get; set; }
@@ -11,6 +11,8 @@ public record GenerateEquipmentProperties
[JsonExtensionData]
public Dictionary<string, object>? ExtensionData { get; set; }
public MongoId BotId { get; set; }
/// <summary>
/// Root Slot being generated
/// </summary>
@@ -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;
/// <summary>
/// Service for keeping track of items and their exact position inside a bots container
/// </summary>
[Injectable]
public class BotInventoryContainerService(ISptLogger<BotGeneratorHelper> logger, ItemHelper itemHelper)
{
// botId/containerName
private readonly ConcurrentDictionary<MongoId, Dictionary<EquipmentSlots, ContainerDetails>> _botContainers = new();
/// <summary>
/// Add a container + details to a bots cache ready to accept loot
/// </summary>
/// <param name="botId">Unique identifier of bot</param>
/// <param name="containerName">name of container e.g. "Backpack"</param>
/// <param name="containerInventoryItem">Inventory item loot will be linked to in bots inventory</param>
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));
}
}
/// <summary>
/// Attempt to add an item + children to a container
/// </summary>
/// <param name="botId">Bots unique id</param>
/// <param name="containerName">Name of container to add to e.g. "Backpack"</param>
/// <param name="itemAndChildren">Item and its children to add to container</param>
/// <param name="botInventory">Inventory to add Item+children to</param>
/// <param name="itemWidth">Width of item with its children</param>
/// <param name="itemHeight">Height of item with its children</param>
/// <returns>ItemAddedResult</returns>
public ItemAddedResult AddItemToBotContainer(
MongoId botId,
EquipmentSlots containerName,
List<Item> 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;
}
/// <summary>
/// Attempt to add an item + children to a container at a specific x/y grid position
/// </summary>
/// <param name="botId">Bots unique id</param>
/// <param name="containerName">Name of container to add to e.g. "Backpack"</param>
/// <param name="itemAndChildren">Item and its children to add to container</param>
/// <param name="botInventory">Inventory to add Item+children to</param>
/// <param name="itemWidth">Width of item with its children</param>
/// <param name="itemHeight">Height of item with its children</param>
/// <param name="fixedLocation">Details for where to place item in container grid</param>
/// <returns>ItemAddedResult</returns>
public ItemAddedResult AddItemToBotContainerFixedPosition(
MongoId botId,
EquipmentSlots containerName,
List<Item> 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;
}
/// <summary>
/// Helper - Get the bot-specific container details, create if data doesn't exist
/// </summary>
/// <param name="botId">Bot unique identifier</param>
/// <returns>Dictionary</returns>
protected Dictionary<EquipmentSlots, ContainerDetails> GetOrCreateBotContainerDictionary(MongoId botId)
{
if (!_botContainers.TryGetValue(botId, out var botContainers))
{
// Create blank dict ready for containers to be added
botContainers = new();
}
return botContainers;
}
/// <summary>
/// Fill region of a 2D array
/// </summary>
/// <param name="grid">The 2D grid array to modify</param>
/// <param name="x">The starting column index (left)</param>
/// <param name="y">The starting row index (top)</param>
/// <param name="itemWidth">The number of cells to update horizontally</param>
/// <param name="itemHeight">The number of cells to update vertically</param>
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;
}
}
}
/// <summary>
/// Is the items subtype allowed inside this container / is it excluded from this container
/// </summary>
/// <param name="containerDetails">Details on the container we want to add item into</param>
/// <param name="itemAndChildren">Item+children we want to add into container</param>
/// <returns>true = item is allowed</returns>
private bool ItemAllowedInContainer(ContainerDetails containerDetails, List<Item>? 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;
}
/// <summary>
/// Is the items edge length bigger than the grid trying to hold it
/// </summary>
/// <param name="grid">Container grid</param>
/// <param name="itemWidth">Width of item</param>
/// <param name="itemHeight">Height of item</param>
/// <returns>true = item bigger than grid</returns>
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;
}
/// <summary>
/// Get a bots container details from cache by its id
/// </summary>
/// <param name="botId">Identifier of bot to get details of</param>
/// <returns>Dictionary of containers and their details</returns>
public Dictionary<EquipmentSlots, ContainerDetails>? GetBotContainer(MongoId botId)
{
return GetOrCreateBotContainerDictionary(botId);
}
/// <summary>
/// Clear the cache of all bot containers
/// </summary>
public void ClearCache()
{
_botContainers.Clear();
}
/// <summary>
/// Clear specific bot container details from cache
/// </summary>
/// <param name="botId">Bot identifier</param>
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,
}
);
}
}
/// <summary>
/// Grid layout and flag if grid is full
/// </summary>
public List<ContainerMapDetails> ContainerGridDetails { get; } = [];
/// <summary>
/// Db record for the container holding items
/// </summary>
public TemplateItem ContainerDbItem { get; set; }
/// <summary>
/// Inventory item representing the container
/// </summary>
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; }
}
}
@@ -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<BotGeneratorHelper>();
_itemHelper = DI.GetInstance().GetService<ItemHelper>();
_botLootGenerator = DI.GetInstance().GetService<BotLootGenerator>();
_botInventoryContainerService = DI.GetInstance().GetService<BotInventoryContainerService>();
}
#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<XY>
{
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<XY>
{
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<XY>
{
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<Item>();
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,