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; using Equipment = Core.Models.Eft.Common.Tables.Equipment; namespace Core.Generators; [Injectable] public class BotInventoryGenerator { protected ISptLogger _logger; protected HashUtil _hashUtil; protected RandomUtil _randomUtil; protected DatabaseService _databaseService; protected ApplicationContext _applicationContext; protected BotWeaponGenerator _botWeaponGenerator; protected BotLootGenerator _botLootGenerator; protected BotGeneratorHelper _botGeneratorHelper; protected ProfileHelper _profileHelper; protected BotHelper _botHelper; protected WeightedRandomHelper _weightedRandomHelper; protected ItemHelper _itemHelper; protected WeatherHelper _weatherHelper; protected LocalisationService _localisationService; protected BotEquipmentFilterService _botEquipmentFilterService; protected BotEquipmentModPoolService _botEquipmentModPoolService; protected BotEquipmentModGenerator _botEquipmentModGenerator; protected ConfigServer _configServer; private BotConfig _botConfig; public 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 ) { _logger = 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; _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 equipmentSlotKvP 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(equipmentSlotKvP.Key)) { continue; } GenerateEquipment(new GenerateEquipmentProperties { RootEquipmentSlot = equipmentSlotKvP.Key, RootEquipmentPool = equipmentSlotKvP.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 /// /// Equpiment to filter TacticalVest of /// 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.Count() > 0) { 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 weaponSlot in weaponSlotsToFill) { // Add weapon to bot if true and bot json has something to put into the slot if (weaponSlot.ShouldSpawn && templateInventory.Equipment[weaponSlot.Slot].Any()) { AddWeaponAndMagazinesToInventory( sessionId, weaponSlot, 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) // TODO: Type fuckery { slot: EquipmentSlots; shouldSpawn: boolean }[] { 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; } }