using Core.Annotations; using Core.Context; using Core.Helpers; using Core.Models.Eft.Common.Tables; using Core.Models.Eft.Match; using Core.Models.Enums; using Core.Models.Spt.Bots; using Core.Models.Spt.Config; using Core.Models.Utils; using Core.Servers; using Core.Services; using Core.Utils; namespace Core.Generators; [Injectable] public class BotInventoryGenerator( ISptLogger _logger, HashUtil _hashUtil, RandomUtil _randomUtil, DatabaseService _databaseService, ApplicationContext _applicationContext, BotWeaponGenerator _botWeaponGenerator, BotLootGenerator _botLootGenerator, BotGeneratorHelper _botGeneratorHelper, ProfileHelper _profileHelper, BotHelper _botHelper, WeightedRandomHelper _weightedRandomHelper, ItemHelper _itemHelper, WeatherHelper _weatherHelper, LocalisationService _localisationService, BotEquipmentFilterService _botEquipmentFilterService, BotEquipmentModPoolService _botEquipmentModPoolService, BotEquipmentModGenerator _botEquipmentModGenerator, ConfigServer _configServer ) { private BotConfig _botConfig = _configServer.GetConfig(); /// /// Add equipment/weapons/loot to bot /// /// Session id /// Base json db file for the bot having its loot generated /// Role bot has (assault/pmcBot) /// Is bot being converted into a pmc /// Level of bot being generated /// Game version for bot, only really applies for PMCs /// PmcInventory object with equipment/weapons/loot public BotBaseInventory GenerateInventory(string sessionId, BotType botJsonTemplate, string botRole, bool isPmc, int botLevel, string chosenGameVersion) { var templateInventory = botJsonTemplate.BotInventory; var wornItemChances = botJsonTemplate.BotChances; var itemGenerationLimitsMinMax = botJsonTemplate.BotGeneration; // Generate base inventory with no items var botInventory = GenerateInventoryBase(); // Get generated raid details bot will be spawned in var raidConfig = _applicationContext .GetLatestValue(ContextVariableType.RAID_CONFIGURATION) ?.GetValue(); GenerateAndAddEquipmentToBot( sessionId, templateInventory, wornItemChances, botRole, botInventory, botLevel, chosenGameVersion, raidConfig ); // Roll weapon spawns (primary/secondary/holster) and generate a weapon for each roll that passed GenerateAndAddWeaponsToBot( templateInventory, wornItemChances, sessionId, botInventory, botRole, isPmc, itemGenerationLimitsMinMax, botLevel ); // Pick loot and add to bots containers (rig/backpack/pockets/secure) _botLootGenerator.GenerateLoot(sessionId, botJsonTemplate, isPmc, botRole, botInventory, botLevel); return botInventory; } /// /// Create a pmcInventory object with all the base/generic items needed /// /// PmcInventory object public BotBaseInventory GenerateInventoryBase() { var equipmentId = _hashUtil.Generate(); var stashId = _hashUtil.Generate(); var questRaidItemsId = _hashUtil.Generate(); var questStashItemsId = _hashUtil.Generate(); var sortingTableId = _hashUtil.Generate(); return new BotBaseInventory { Items = [ new() { Id = equipmentId, Template = ItemTpl.INVENTORY_DEFAULT }, new() { Id = stashId, Template = ItemTpl.STASH_STANDARD_STASH_10X30 }, new() { Id = questRaidItemsId, Template = ItemTpl.STASH_QUESTRAID }, new() { Id = questStashItemsId, Template = ItemTpl.STASH_QUESTOFFLINE }, new() { Id = sortingTableId, Template = ItemTpl.SORTINGTABLE_SORTING_TABLE } ], Equipment = equipmentId, Stash = stashId, QuestRaidItems = questRaidItemsId, QuestStashItems = questStashItemsId, SortingTable = sortingTableId, HideoutAreaStashes = { }, FastPanel = { }, FavoriteItems = [], HideoutCustomizationStashId = "", }; } /// /// Add equipment to a bot /// /// Session id /// bot/x.json data from db /// Chances items will be added to bot /// Role bot has (assault/pmcBot) /// Inventory to add equipment to /// Level of bot /// Game version for bot, only really applies for PMCs /// RadiConfig public void GenerateAndAddEquipmentToBot(string sessionId, BotTypeInventory templateInventory, Chances wornItemChances, string botRole, BotBaseInventory botInventory, int botLevel, string chosenGameVersion, GetRaidConfigurationRequestData raidConfig) { // These will be handled later var excludedSlots = new List() { EquipmentSlots.Pockets, EquipmentSlots.FirstPrimaryWeapon, EquipmentSlots.SecondPrimaryWeapon, EquipmentSlots.Holster, EquipmentSlots.ArmorVest, EquipmentSlots.TacticalVest, EquipmentSlots.FaceCover, EquipmentSlots.Headwear, EquipmentSlots.Earpiece }; _botConfig.Equipment.TryGetValue(_botGeneratorHelper.GetBotEquipmentRole(botRole), out var botEquipConfig); var randomistionDetails = _botHelper.GetBotRandomizationDetails(botLevel, botEquipConfig); // Apply nighttime changes if its nighttime + there's changes to make if ( randomistionDetails?.NighttimeChanges is not null && raidConfig is not null && _weatherHelper.IsNightTime(raidConfig.TimeVariant) ) { foreach (var equipmentSlotKvP in (randomistionDetails.NighttimeChanges.EquipmentModsModifiers)) { // Never let mod chance go outside of 0 - 100 randomistionDetails.EquipmentMods[equipmentSlotKvP.Key] = Math.Min( Math.Max(randomistionDetails.EquipmentMods[equipmentSlotKvP.Key] + equipmentSlotKvP.Value, 0), 100 ); } } // Get profile of player generating bots, we use their level later on var pmcProfile = _profileHelper.GetPmcProfile(sessionId); var botEquipmentRole = _botGeneratorHelper.GetBotEquipmentRole(botRole); // Iterate over all equipment slots of bot, do it in specifc order to reduce conflicts // e.g. ArmorVest should be generated after TactivalVest // or FACE_COVER before HEADWEAR foreach (var (equipmentSlot, value) in templateInventory.Equipment) { // Skip some slots as they need to be done in a specific order + with specific parameter values // e.g. Weapons if (excludedSlots.Contains(equipmentSlot)) { continue; } GenerateEquipment( new GenerateEquipmentProperties { RootEquipmentSlot = equipmentSlot, RootEquipmentPool = value, ModPool = templateInventory.Mods, SpawnChances = wornItemChances, BotData = new BotData { Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole }, Inventory = botInventory, BotEquipmentConfig = botEquipConfig, RandomisationDetails = randomistionDetails, GeneratingPlayerLevel = pmcProfile.Info.Level, } ); } // Generate below in specific order GenerateEquipment( new GenerateEquipmentProperties { RootEquipmentSlot = EquipmentSlots.Pockets, // Unheard profiles have unique sized pockets, TODO - handle this somewhere else in a better way RootEquipmentPool = chosenGameVersion == GameEditions.UNHEARD ? new Dictionary { [ItemTpl.POCKETS_1X4_TUE] = 1 } : templateInventory.Equipment[EquipmentSlots.Pockets], ModPool = templateInventory.Mods, SpawnChances = wornItemChances, BotData = new BotData { Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole }, Inventory = botInventory, BotEquipmentConfig = botEquipConfig, RandomisationDetails = randomistionDetails, GenerateModsBlacklist = [ItemTpl.POCKETS_1X4_TUE], GeneratingPlayerLevel = pmcProfile.Info.Level, } ); GenerateEquipment( new GenerateEquipmentProperties { RootEquipmentSlot = EquipmentSlots.FaceCover, RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.FaceCover], ModPool = templateInventory.Mods, SpawnChances = wornItemChances, BotData = new BotData { Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole }, Inventory = botInventory, BotEquipmentConfig = botEquipConfig, RandomisationDetails = randomistionDetails, GeneratingPlayerLevel = pmcProfile.Info.Level, } ); GenerateEquipment( new GenerateEquipmentProperties { RootEquipmentSlot = EquipmentSlots.Headwear, RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.Headwear], ModPool = templateInventory.Mods, SpawnChances = wornItemChances, BotData = new BotData { Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole }, Inventory = botInventory, BotEquipmentConfig = botEquipConfig, RandomisationDetails = randomistionDetails, GeneratingPlayerLevel = pmcProfile.Info.Level, } ); GenerateEquipment( new GenerateEquipmentProperties { RootEquipmentSlot = EquipmentSlots.Earpiece, RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.Earpiece], ModPool = templateInventory.Mods, SpawnChances = wornItemChances, BotData = new BotData { Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole }, Inventory = botInventory, BotEquipmentConfig = botEquipConfig, RandomisationDetails = randomistionDetails, GeneratingPlayerLevel = pmcProfile.Info.Level, } ); var hasArmorVest = GenerateEquipment( new GenerateEquipmentProperties { RootEquipmentSlot = EquipmentSlots.ArmorVest, RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.ArmorVest], ModPool = templateInventory.Mods, SpawnChances = wornItemChances, BotData = new BotData { Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole }, Inventory = botInventory, BotEquipmentConfig = botEquipConfig, RandomisationDetails = randomistionDetails, GeneratingPlayerLevel = pmcProfile.Info.Level, } ); // Bot has no armor vest and flagged to be forced to wear armored rig in this event if (botEquipConfig.ForceOnlyArmoredRigWhenNoArmor.GetValueOrDefault(false) && !hasArmorVest) { // Filter rigs down to only those with armor FilterRigsToThoseWithProtection(templateInventory.Equipment, botRole); } // Optimisation - Remove armored rigs from pool if (hasArmorVest) { // Filter rigs down to only those with armor FilterRigsToThoseWithoutProtection(templateInventory.Equipment, botRole); } // Bot is flagged as always needing a vest if (botEquipConfig.ForceRigWhenNoVest.GetValueOrDefault(false) && !hasArmorVest) { wornItemChances.EquipmentChances["TacticalVest"] = 100; } GenerateEquipment( new GenerateEquipmentProperties { RootEquipmentSlot = EquipmentSlots.Earpiece, RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.Earpiece], ModPool = templateInventory.Mods, SpawnChances = wornItemChances, BotData = new BotData { Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole }, Inventory = botInventory, BotEquipmentConfig = botEquipConfig, RandomisationDetails = randomistionDetails, GeneratingPlayerLevel = pmcProfile.Info.Level, } ); } /// /// Remove non-armored rigs from parameter data /// /// Equpiment to filter TacticalVest of /// Role of bot vests are being filtered for public void FilterRigsToThoseWithProtection(Dictionary> templateEquipment, string botRole) { var tacVestsWithArmor = templateEquipment[EquipmentSlots.TacticalVest] .Where(kvp => _itemHelper.ItemHasSlots(kvp.Key)) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); if (!tacVestsWithArmor.Any()) { _logger.Debug($"Unable to filter to only armored rigs as bot: {botRole} has none in pool"); return; } templateEquipment[EquipmentSlots.TacticalVest] = tacVestsWithArmor; } /// /// Remove armored rigs from parameter data /// /// Equipment to filter TacticalVest by /// Role of bot vests are being filtered for /// Should the function return all rigs when 0 unarmored are found public void FilterRigsToThoseWithoutProtection(Dictionary> templateEquipment, string botRole, bool allowEmptyResult = true) { var tacVestsWithoutArmor = templateEquipment[EquipmentSlots.TacticalVest] .Where(kvp => !_itemHelper.ItemHasSlots(kvp.Key)) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); if (!allowEmptyResult && !tacVestsWithoutArmor.Any()) { _logger.Debug($"Unable to filter to only unarmored rigs as bot: {botRole} has none in pool"); return; } templateEquipment[EquipmentSlots.TacticalVest] = tacVestsWithoutArmor; } /// /// Add a piece of equipment with mods to inventory from the provided pools /// /// Values to adjust how item is chosen and added to bot /// true when item added public bool GenerateEquipment(GenerateEquipmentProperties settings) { List slotsToCheck = [EquipmentSlots.Pockets.ToString(), EquipmentSlots.SecuredContainer.ToString()]; double? spawnChance = slotsToCheck.Contains(settings.RootEquipmentSlot.ToString()) ? 100 : settings.SpawnChances.EquipmentChances.GetValueOrDefault(settings.RootEquipmentSlot.ToString()); if (!spawnChance.HasValue) { _logger.Warning( _localisationService.GetText( "bot-no_spawn_chance_defined_for_equipment_slot", settings.RootEquipmentSlot ) ); return false; } // Roll dice on equipment item var shouldSpawn = _randomUtil.GetChance100(spawnChance ?? 0); if (shouldSpawn && !settings.RootEquipmentPool.Any()) { var pickedItemDb = new TemplateItem(); var found = false; // Limit attempts to find a compatible item as it's expensive to check them all var maxAttempts = Math.Round(settings.RootEquipmentPool.Count() * 0.75); // Roughly 75% of pool size var attempts = 0; while (!found) { if (!settings.RootEquipmentPool.Any()) { return false; } var chosenItemTpl = _weightedRandomHelper.GetWeightedValue(settings.RootEquipmentPool); var dbResult = _itemHelper.GetItem(chosenItemTpl); if (!dbResult.Key) { _logger.Error(_localisationService.GetText("bot-missing_item_template", chosenItemTpl)); _logger.Debug($"EquipmentSlot-> {settings.RootEquipmentSlot}"); // Remove picked item settings.RootEquipmentPool.Remove(chosenItemTpl); attempts++; continue; } // Is the chosen item compatible with other items equipped var compatibilityResult = _botGeneratorHelper.IsItemIncompatibleWithCurrentItems( settings.Inventory.Items, chosenItemTpl, settings.RootEquipmentSlot.ToString() ); if (compatibilityResult.Incompatible ?? false) { // Tried x different items that failed, stop if (attempts > maxAttempts) { return false; } // Remove picked item from pool settings.RootEquipmentPool.Remove(chosenItemTpl); // Increment times tried attempts++; } else { // Success found = true; pickedItemDb = dbResult.Value; } } // Create root item var id = _hashUtil.Generate(); Item item = new() { Id = id, Template = pickedItemDb.Id, ParentId = settings.Inventory.Equipment, SlotId = settings.RootEquipmentSlot.ToString(), Upd = _botGeneratorHelper.GenerateExtraPropertiesForItem(pickedItemDb, settings.BotData.Role) }; var botEquipBlacklist = _botEquipmentFilterService.GetBotEquipmentBlacklist( settings.BotData.EquipmentRole, (double)settings.GeneratingPlayerLevel ); // Edge case: Filter the armor items mod pool if bot exists in config dict + config has armor slot if (_botConfig.Equipment[settings.BotData.EquipmentRole] is not null && settings.RandomisationDetails.RandomisedArmorSlots.Contains(settings.RootEquipmentSlot.ToString())) { // Filter out mods from relevant blacklist settings.ModPool[pickedItemDb.Id] = GetFilteredDynamicModsForItem( pickedItemDb.Id, botEquipBlacklist.Equipment ); } // Does item have slots for sub-mods to be inserted into if (pickedItemDb.Properties.Slots.Any() && settings.GenerateModsBlacklist.Contains(pickedItemDb.Id)) { var childItemsToAdd = _botEquipmentModGenerator.GenerateModsForEquipment( [item], id, pickedItemDb, settings, botEquipBlacklist ); settings.Inventory.Items.AddRange(childItemsToAdd); } else { // No slots, add root item only settings.Inventory.Items.Add(item); } return true; } return false; } /// /// Get all possible mods for item and filter down based on equipment blacklist from bot.json config /// /// Item mod pool is being retrieved and filtered /// Blacklist to filter mod pool with /// Filtered pool of mods public Dictionary> GetFilteredDynamicModsForItem(string itemTpl, Dictionary> equipmentBlacklist) { var modPool = _botEquipmentModPoolService.GetModsForGearSlot(itemTpl); foreach (var modSlot in modPool.Keys ?? Enumerable.Empty()) { var blacklistedMods = equipmentBlacklist[modSlot] ?? []; var filteredMods = modPool[modSlot].Where((slotName) => !blacklistedMods.Contains(slotName)); if (filteredMods.Any()) { modPool[modSlot] = filteredMods.ToList(); } } return modPool; } /// /// Work out what weapons bot should have equipped and add them to bot inventory /// /// bot/x.json data from db /// Chances bot can have equipment equipped /// Session id /// Inventory to add weapons to /// assault/pmcBot/bossTagilla etc /// Is the bot being generated as a pmc /// Limits for items the bot can have /// level of bot having weapon generated public void GenerateAndAddWeaponsToBot(BotTypeInventory templateInventory, Chances equipmentChances, string sessionId, BotBaseInventory botInventory, string botRole, bool isPmc, Generation itemGenerationLimitsMinMax, int botLevel) { var weaponSlotsToFill = GetDesiredWeaponsForBot(equipmentChances); foreach (var desiredWeapons in weaponSlotsToFill) { // Add weapon to bot if true and bot json has something to put into the slot if (desiredWeapons.ShouldSpawn && templateInventory.Equipment[desiredWeapons.Slot].Any()) { AddWeaponAndMagazinesToInventory( sessionId, desiredWeapons, templateInventory, botInventory, equipmentChances, botRole, isPmc, itemGenerationLimitsMinMax, botLevel ); } } } /// /// Calculate if the bot should have weapons in Primary/Secondary/Holster slots /// /// Chances bot has certain equipment /// What slots bot should have weapons generated for public List GetDesiredWeaponsForBot(Chances equipmentChances) { var shouldSpawnPrimary = _randomUtil.GetChance100(equipmentChances.EquipmentChances["FirstPrimaryWeapon"]); return [ new() { Slot = EquipmentSlots.FirstPrimaryWeapon, ShouldSpawn = shouldSpawnPrimary }, new() { Slot = EquipmentSlots.SecondPrimaryWeapon, ShouldSpawn = shouldSpawnPrimary ? _randomUtil.GetChance100(equipmentChances.EquipmentChances["SecondPrimaryWeapon"]) : false }, new() { Slot = EquipmentSlots.Holster, ShouldSpawn = shouldSpawnPrimary ? _randomUtil.GetChance100(equipmentChances.EquipmentChances["Holster"]) // Primary weapon = roll for chance at pistol : true // No primary = force pistol } ]; } /// /// Add weapon + spare mags/ammo to bots inventory /// /// Session id /// Weapon slot being generated /// bot/x.json data from db /// Inventory to add weapon+mags/ammo to /// Chances bot can have equipment equipped /// assault/pmcBot/bossTagilla etc /// Is the bot being generated as a pmc /// /// public void AddWeaponAndMagazinesToInventory(string sessionId, DesiredWeapons weaponSlot, BotTypeInventory templateInventory, BotBaseInventory botInventory, Chances equipmentChances, string botRole, bool isPmc, Generation itemGenerationWeights, int botLevel) { var generatedWeapon = _botWeaponGenerator.GenerateRandomWeapon( sessionId, weaponSlot.Slot.ToString(), templateInventory, botInventory.Equipment, equipmentChances.WeaponModsChances, botRole, isPmc, botLevel ); botInventory.Items.AddRange(generatedWeapon.Weapon); _botWeaponGenerator.AddExtraMagazinesToInventory( generatedWeapon, itemGenerationWeights.Items.Magazines, botInventory, botRole ); } } public class DesiredWeapons { public EquipmentSlots Slot { get; set; } public bool ShouldSpawn { get; set; } }