using Core.Annotations; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; using Core.Models.Enums; using Core.Models.Spt.Bots; using Core.Models.Spt.Config; using Core.Models.Utils; using Core.Helpers; using Core.Models.Spt.Server; using Core.Servers; using Core.Services; using Core.Utils; using Core.Utils.Cloners; using System.Collections.Generic; namespace Core.Generators; [Injectable] public class BotEquipmentModGenerator { private readonly ISptLogger _logger; private readonly HashUtil _hashUtil; private readonly RandomUtil _randomUtil; private readonly ItemHelper _itemHelper; private readonly BotGeneratorHelper _botGeneratorHelper; private readonly BotEquipmentModPoolService _botEquipmentModPoolService; private readonly PresetHelper _presetHelper; private readonly ProbabilityHelper _probabilityHelper; private readonly LocalisationService _localisationService; private readonly ItemFilterService _itemFilterService; private readonly ConfigServer _configServer; private readonly ICloner _cloner; private BotConfig _botConfig; public BotEquipmentModGenerator( ISptLogger logger, HashUtil hashUtil, RandomUtil randomUtil, ItemHelper itemHelper, BotGeneratorHelper botGeneratorHelper, BotEquipmentModPoolService botEquipmentModPoolService, PresetHelper presetHelper, ProbabilityHelper probabilityHelper, LocalisationService localisationService, ItemFilterService itemFilterService, ConfigServer configServer, ICloner cloner) { _logger = logger; _hashUtil = hashUtil; _randomUtil = randomUtil; _itemHelper = itemHelper; _botGeneratorHelper = botGeneratorHelper; _botEquipmentModPoolService = botEquipmentModPoolService; _presetHelper = presetHelper; _probabilityHelper = probabilityHelper; _localisationService = localisationService; _itemFilterService = itemFilterService; _configServer = configServer; _cloner = cloner; _botConfig = _configServer.GetConfig(); } /// /// Check mods are compatible and add to array /// /// Equipment item to add mods to /// Mod list to choose from /// parentid of item to add mod to /// Template object of item to add mods to /// The relevant blacklist from bot.json equipment dictionary /// should this mod be forced to spawn /// Item + compatible mods as an array public List GenerateModsForEquipment(List equipment, string parentId, TemplateItem parentTemplate, GenerateEquipmentProperties settings, EquipmentFilterDetails specificBlacklist, bool shouldForceSpawn = false) { var forceSpawn = shouldForceSpawn; // Get mod pool for the desired item var compatibleModsPool = settings.ModPool[parentTemplate.Id]; if (compatibleModsPool is null) { _logger.Warning($"bot: { settings.BotData.Role} lacks a mod slot pool for item: { parentTemplate.Id} { parentTemplate.Name}"); } // Iterate over mod pool and choose mods to add to item foreach (var modSlotKvP in compatibleModsPool) { var modSlotName = modSlotKvP.Key; // Get the templates slot object from db var itemSlotTemplate = GetModItemSlotFromDb(modSlotName, parentTemplate); if (itemSlotTemplate is null) { _logger.Error(_localisationService.GetText("bot-mod_slot_missing_from_item", new { modSlot = modSlotName, parentId= parentTemplate.Id, parentName= parentTemplate.Name, botRole= settings.BotData.Role })); continue; } var modSpawnResult = ShouldModBeSpawned( itemSlotTemplate, modSlotName, settings.SpawnChances.EquipmentModsChances, settings.BotEquipmentConfig); // Rolled to skip mod and it shouldnt be force-spawned if (modSpawnResult == ModSpawn.SKIP && !forceSpawn) { continue; } // Ensure submods for nvgs all spawn together if (modSlotName == "mod_nvg") { forceSpawn = true; } // Get pool of items we can add for this slot var modPoolToChooseFrom = modSlotKvP.Value; // Filter the pool of items in blacklist var filteredModPool = FilterModsByBlacklist(modPoolToChooseFrom, specificBlacklist, modSlotName); if (filteredModPool.Count > 0) { // use filtered pool as it has items in it modPoolToChooseFrom = filteredModPool; } // Slot can hold armor plates + we are filtering possible items by bot level, handle if ( settings.BotEquipmentConfig.FilterPlatesByLevel.GetValueOrDefault(false) && _itemHelper.IsRemovablePlateSlot(modSlotName.ToLower()) ) { var plateSlotFilteringOutcome = FilterPlateModsForSlotByLevel( settings, modSlotName.ToLower(), compatibleModsPool[modSlotName], parentTemplate); if (plateSlotFilteringOutcome.Result is Result.UNKNOWN_FAILURE or Result.NO_DEFAULT_FILTER) { _logger.Debug($"Plate slot: {modSlotName} selection for armor: {parentTemplate.Id} failed: {plateSlotFilteringOutcome.Result}, skipping"); continue; } if (plateSlotFilteringOutcome.Result == Result.LACKS_PLATE_WEIGHTS) { _logger.Warning($"Plate slot: {modSlotName} lacks weights for armor: { parentTemplate.Id}, unable to adjust plate choice, using existing data"); } // Replace mod pool with pool of chosen plate items modPoolToChooseFrom = plateSlotFilteringOutcome.PlateModTemplates; } // Choose random mod from pool and check its compatibility string modTpl = null; var found = false; var exhaustableModPool = CreateExhaustableArray(modPoolToChooseFrom); while (exhaustableModPool.HasValues()) { modTpl = exhaustableModPool.GetRandomValue(); if (modTpl is not null && !_botGeneratorHelper.IsItemIncompatibleWithCurrentItems(equipment, modTpl, modSlotName).Incompatible.GetValueOrDefault(false)) { found = true; break; } } // Compatible item not found but slot REQUIRES item, get random item from db if (!found && itemSlotTemplate.Required.GetValueOrDefault(false)) { modTpl = GetRandomModTplFromItemDb(modTpl, itemSlotTemplate, modSlotName, equipment); found = modTpl is not null; } // Compatible item not found + not required - skip if (!(found || itemSlotTemplate.Required.GetValueOrDefault(false))) { continue; } // Get chosen mods db template and check it fits into slot var modTemplate = _itemHelper.GetItem(modTpl); if ( !IsModValidForSlot( modTemplate, itemSlotTemplate, modSlotName, parentTemplate, settings.BotData.Role) ) { continue; } // Generate new id to ensure all items are unique on bot var modId = _hashUtil.Generate(); equipment.Add( CreateModItem(modId, modTpl, parentId, modSlotName, modTemplate.Value, settings.BotData.Role)); // Does item being added exist in mod pool - has its own mod pool if (settings.ModPool.ContainsKey(modTpl)) { // Call self again with mod being added as item to add child mods to GenerateModsForEquipment( equipment, modId, modTemplate.Value, settings, specificBlacklist, forceSpawn); } } return equipment; } /// /// Filter a bots plate pool based on its current level /// /// Bot equipment generation settings /// Armor slot being filtered /// Plates tpls to choose from /// The armor items db template /// Array of plate tpls to choose from public FilterPlateModsForSlotByLevelResult FilterPlateModsForSlotByLevel(GenerateEquipmentProperties settings, string modSlot, List existingPlateTplPool, TemplateItem armorItem) { throw new NotImplementedException(); } /// /// Add mods to a weapon using the provided mod pool /// /// Session id /// Data used to generate the weapon /// Weapon + mods array public List GenerateModsForWeapon(string sessionId, GenerateWeaponRequest request) { throw new NotImplementedException(); } /// /// Should the provided bot have its stock chance values altered to 100% /// /// Slot to check /// Bots equipment config/chance values /// Mod being added to bots weapon /// True if it should public bool ShouldForceSubStockSlots(string modSlot, EquipmentFilters botEquipConfig, TemplateItem modToAddTemplate) { // Slots a weapon can store its stock in string[] stockSlots = ["mod_stock", "mod_stock_000", "mod_stock_001", "mod_stock_akms"]; // Can the stock hold child items var hasSubSlots = modToAddTemplate.Properties.Slots?.Count > 0; return (stockSlots.Contains(modSlot) && hasSubSlots) || botEquipConfig.ForceStock.GetValueOrDefault(false); } /// /// Is this modslot a front or rear sight /// /// Slot to check /// /// true if it's a front/rear sight public bool ModIsFrontOrRearSight(string modSlot, string tpl) { // Gas block /w front sight is special case, deem it a 'front sight' too if (modSlot == "mod_gas_block" && tpl == "5ae30e795acfc408fb139a0b") { // M4A1 front sight with gas block return true; } return ((string[])["mod_sight_front", "mod_sight_rear"]).Contains(modSlot); } /// /// Does the provided mod details show the mod can hold a scope /// /// e.g. mod_scope, mod_mount /// Parent id of mod item /// true if it can hold a scope public bool ModSlotCanHoldScope(string modSlot, string modsParentId) { return ( ((string[])[ "mod_scope", "mod_mount", "mod_mount_000", "mod_scope_000", "mod_scope_001", "mod_scope_002", "mod_scope_003", ]).Contains(modSlot.ToLower()) && modsParentId == BaseClasses.MOUNT ); } /// /// Set mod spawn chances to defined amount /// /// Chance dictionary to update /// /// public void AdjustSlotSpawnChances(Dictionary? modSpawnChances, List? modSlotsToAdjust, double newChancePercent) { if (modSpawnChances is null) { _logger.Warning("AdjustSlotSpawnChances() modSpawnChances missing"); return; } if (modSlotsToAdjust is null) { _logger.Warning("AdjustSlotSpawnChances() modSlotsToAdjust missing"); return; } foreach (var modName in modSlotsToAdjust) { modSpawnChances[modName] = newChancePercent; } } /// /// Does the provided modSlot allow muzzle-related items /// /// Slot id to check /// OPTIONAL: parent id of modslot being checked /// True if modSlot can have muzzle-related items public bool ModSlotCanHoldMuzzleDevices(string modSlot, string? modsParentId) { return ((string[])["mod_muzzle", "mod_muzzle_000", "mod_muzzle_001"]).Contains(modSlot.ToLower()); } /// /// Sort mod slots into an ordering that maximises chance of a successful weapon generation /// /// Array of mod slot strings to sort /// The Tpl of the item with mod keys being sorted /// Sorted array public List SortModKeys(List unsortedSlotKeys, string itemTplWithKeysToSort) { throw new NotImplementedException(); } /// /// Get a Slot property for an item (chamber/cartridge/slot) /// /// e.g patron_in_weapon /// item template /// Slot item public Slot? GetModItemSlotFromDb(string modSlot, TemplateItem parentTemplate) { var modSlotLower = modSlot.ToLower(); switch (modSlotLower) { case "patron_in_weapon": case "patron_in_weapon_000": case "patron_in_weapon_001": return parentTemplate.Properties.Chambers.FirstOrDefault((chamber) => chamber.Name.Contains(modSlotLower)); case "cartridges": return parentTemplate.Properties.Cartridges.FirstOrDefault((c) => c.Name.ToLower() == modSlotLower); default: return parentTemplate.Properties.Slots.FirstOrDefault((s) => s.Name.ToLower() == modSlotLower); } } /// /// Randomly choose if a mod should be spawned, 100% for required mods OR mod is ammo slot /// /// slot the item sits in from db /// Name of slot the mod sits in /// Chances for various mod spawns /// Various config settings for generating this type of bot /// ModSpawn.SPAWN when mod should be spawned, ModSpawn.DEFAULT_MOD when default mod should spawn, ModSpawn.SKIP when mod is skipped public ModSpawn ShouldModBeSpawned(Slot itemSlot, string modSlotName, Dictionary modSpawnChances, EquipmentFilters botEquipConfig) { var slotRequired = itemSlot.Required; if (GetAmmoContainers().Contains(modSlotName)) { // Always force mags/cartridges in weapon to spawn return ModSpawn.SPAWN; } var spawnMod = _probabilityHelper.RollChance(modSpawnChances[modSlotName]); if (!spawnMod && (slotRequired.GetValueOrDefault(false) || botEquipConfig.WeaponSlotIdsToMakeRequired.Contains(modSlotName))) { // Edge case: Mod is required but spawn chance roll failed, choose default mod spawn for slot return ModSpawn.DEFAULT_MOD; } return spawnMod ? ModSpawn.SPAWN : ModSpawn.SKIP; } /// /// Choose a mod to fit into the desired slot /// /// Data used to choose an appropriate mod with /// itemHelper.getItem() result public object? ChooseModToPutIntoSlot(ModToSpawnRequest request) // TODO: type fuckery: [boolean, ITemplateItem] | undefined { throw new NotImplementedException(); } /// /// Given the passed in array of magaizne tpls, look up the min size set in config and return only those that have that size or larger /// /// Request data /// Pool of magazine tpls to filter /// Filtered pool of magazine tpls /// public List GetFilterdMagazinePoolByCapacity(ModToSpawnRequest modSpawnRequest, List modPool) { throw new NotImplementedException(); } /// /// Choose a weapon mod tpl for a given slot from a pool of choices /// Checks chosen tpl is compatible with all existing weapon items /// /// /// Pool of mods that can be picked from /// Slot the picked mod will have as a parent /// How should chosen tpl be treated: DEFAULT_MOD/SPAWN/SKIP /// Array of weapon items chosen item will be added to /// Name of slot picked mod will be placed into /// Chosen weapon details public ChooseRandomCompatibleModResult GetCompatibleWeaponModTplForSlotFromPool(ModToSpawnRequest request, List modPool, Slot parentSlot, ModSpawn choiceTypeEnum, List weapon, string modSlotName) { throw new NotImplementedException(); } /// /// /// /// Pool of item Tpls to choose from /// How should the slot choice be handled - forced/normal etc /// Weapon mods at current time /// IChooseRandomCompatibleModResult public ChooseRandomCompatibleModResult GetCompatibleModFromPool(List modPool, ModSpawn modSpawnType, List weapon) { throw new NotImplementedException(); } public ExhaustableArray CreateExhaustableArray(List itemsToAddToArray) // TODO: this wont likely be needed, reimplement for C# { return new ExhaustableArray(itemsToAddToArray, _randomUtil, _cloner); } /// /// Get a list of mod tpls that are compatible with the current weapon /// /// /// Tpls that are incompatible and should not be used /// string array of compatible mod tpls with weapon public List GetFilteredModPool(List modPool, List tplBlacklist) { return modPool.Where((tpl) => !tplBlacklist.Contains(tpl)).ToList(); } /// /// Filter mod pool down based on various criteria: /// Is slot flagged as randomisable /// Is slot required /// Is slot flagged as default mod only /// /// /// Mods root parent (weapon/equipment) /// Array of mod tpls public List GetModPoolForSlot(ModToSpawnRequest request, TemplateItem weaponTemplate) { // Mod is flagged as being default only, try and find it in globals if (request.ModSpawnResult == ModSpawn.DEFAULT_MOD) { return GetModPoolForDefaultSlot(request, weaponTemplate); } if (request.IsRandomisableSlot.GetValueOrDefault(false)) { return GetDynamicModPool(request.ParentTemplate.Id, request.ModSlot, request.BotEquipBlacklist); } // Required mod is not default or randomisable, use existing pool return request.ItemModPool[request.ModSlot]; } public List GetModPoolForDefaultSlot(ModToSpawnRequest request, TemplateItem weaponTemplate) { throw new NotImplementedException(); } public Item GetMatchingModFromPreset(ModToSpawnRequest request, TemplateItem weaponTemplate) { var matchingPreset = GetMatchingPreset(weaponTemplate, request.ParentTemplate.Id); return matchingPreset?.Items.FirstOrDefault((item) => item?.SlotId?.ToLower() == request.ModSlot.ToLower()); } /// /// Get default preset for weapon OR get specific weapon presets for edge cases (mp5/silenced dvl) /// /// Weapons db template /// Tpl of the parent item /// Default preset found public Preset? GetMatchingPreset(TemplateItem weaponTemplate, string parentItemTpl) { // Edge case - using mp5sd reciever means default mp5 handguard doesn't fit var isMp5sd = parentItemTpl == "5926f2e086f7745aae644231"; if (isMp5sd) { return _presetHelper.GetPreset("59411abb86f77478f702b5d2"); } // Edge case - dvl 500mm is the silenced barrel and has specific muzzle mods var isDvl500mmSilencedBarrel = parentItemTpl == "5888945a2459774bf43ba385"; if (isDvl500mmSilencedBarrel) { return _presetHelper.GetPreset("59e8d2b386f77445830dd299"); } return _presetHelper.GetDefaultPreset(weaponTemplate.Id); } /// /// Temp fix to prevent certain combinations of weapons with mods that are known to be incompatible /// /// Array of items that make up a weapon /// Mod to check compatibility with weapon /// True if incompatible public bool WeaponModComboIsIncompatible(List weapon, string modTpl) { // STM-9 + AR-15 Lone Star Ion Lite handguard if (weapon[0].Template == "60339954d62c9b14ed777c06" && modTpl == "5d4405f0a4b9361e6a4e6bd9") { return true; } return false; } /// /// Create a mod item with provided parameters as properties + add upd property /// /// _id /// _tpl /// parentId /// slotId /// Used to add additional properties in the upd object /// The bots role mod is being created for /// Item object public Item CreateModItem(string modId, string modTpl, string parentId, string modSlot, TemplateItem modTemplate, string botRole) { return new Item { Id = modId, Template = modTpl, ParentId = parentId, SlotId = modSlot, Upd = _botGeneratorHelper.GenerateExtraPropertiesForItem(modTemplate, botRole)}; } /// /// Get a list of containers that hold ammo /// e.g. mod_magazine / patron_in_weapon_000 /// /// string array public List GetAmmoContainers() { return ["mod_magazine", "patron_in_weapon", "patron_in_weapon_000", "patron_in_weapon_001", "cartridges"]; } /// /// Get a random mod from an items compatible mods Filter array /// /// Default value to return if parentSlot Filter is empty /// Item mod will go into, used to get compatible items /// Slot to get mod to fill /// Items to ensure picked mod is compatible with /// Item tpl public string? GetRandomModTplFromItemDb(string fallbackModTpl, Slot parentSlot, string modSlot, List items) { // Find compatible mods and make an array of them var allowedItems = parentSlot.Props.Filters[0].Filter; // Find mod item that fits slot from sorted mod array var exhaustableModPool = CreateExhaustableArray(allowedItems); var tmpModTpl = fallbackModTpl; while (exhaustableModPool.HasValues()) { tmpModTpl = exhaustableModPool.GetRandomValue(); if (!_botGeneratorHelper.IsItemIncompatibleWithCurrentItems(items, tmpModTpl, modSlot).Incompatible.GetValueOrDefault(false)) { return tmpModTpl; } } // No mod found return null; } /// /// Check if mod exists in db + is for a required slot /// /// Db template of mod to check /// Slot object the item will be placed as child into /// Slot the mod will fill /// Db template of the mods being added /// Bots wildspawntype (assault/pmcBot/exUsec etc) /// True if valid for slot public bool IsModValidForSlot(KeyValuePair modToAdd, Slot slotAddedToTemplate, string modSlot, TemplateItem parentTemplate, string botRole) { throw new NotImplementedException(); } /// /// Find mod tpls of a provided type and add to modPool /// /// Slot to look up and add we are adding tpls for (e.g mod_scope) /// db object for modItem we get compatible mods from /// Pool of mods we are adding to /// A blacklist of items that cannot be picked public void AddCompatibleModsForProvidedMod(string desiredSlotName, TemplateItem modTemplate, Dictionary>> modPool, EquipmentFilterDetails botEquipBlacklist) { throw new NotImplementedException(); } /// /// Get the possible items that fit a slot /// /// item tpl to get compatible items for /// Slot item should fit in /// Equipment that should not be picked /// Array of compatible items for that slot public List GetDynamicModPool(string parentItemId, string modSlot, EquipmentFilterDetails botEquipBlacklist) { var modsFromDynamicPool = _cloner.Clone( _botEquipmentModPoolService.GetCompatibleModsForWeaponSlot(parentItemId, modSlot) ); var filteredMods = FilterModsByBlacklist(modsFromDynamicPool, botEquipBlacklist, modSlot); if (!filteredMods.Any()) { _logger.Warning(_localisationService.GetText("bot-unable_to_filter_mod_slot_all_blacklisted", modSlot)); return modsFromDynamicPool; } return filteredMods; } /// /// Take a list of tpls and filter out blacklisted values using itemFilterService + botEquipmentBlacklist /// /// Base mods to filter /// Equipment blacklist /// Slot mods belong to /// Filtered array of mod tpls public List FilterModsByBlacklist(List allowedMods, EquipmentFilterDetails? botEquipBlacklist, string modSlot) { // No blacklist, nothing to filter out if (botEquipBlacklist is null) { return allowedMods; } var result = new List(); // Get item blacklist and mod equipment blacklist as one array var blacklist = _itemFilterService.GetBlacklistedItems().Concat(botEquipBlacklist.Equipment[modSlot]); result = allowedMods.Where((tpl) => !blacklist.Contains(tpl)).ToList(); return result; } /// /// With the shotgun revolver (60db29ce99594040e04c4a27) 12.12 introduced CylinderMagazines. /// Those magazines (e.g. 60dc519adf4c47305f6d410d) have a "Cartridges" entry with a _max_count=0. /// Ammo is not put into the magazine directly but assigned to the magazine's slots: The "camora_xxx" slots. /// This function is a helper called by generateModsForItem for mods with parent type "CylinderMagazine" /// /// The items where the CylinderMagazine's camora are appended to /// ModPool which should include available cartridges /// The CylinderMagazine's UID /// The CylinderMagazine's template public void FillCamora(List items, Dictionary>> modPool, string cylinderMagParentId, TemplateItem cylinderMagTemplate) { throw new NotImplementedException(); } /// /// Take a record of camoras and merge the compatible shells into one array /// /// Dictionary of camoras we want to merge into one array /// String array of shells for multiple camora sources public List MergeCamoraPools(Dictionary> camorasWithShells) { var uniqueShells = new HashSet(); foreach (var shell in camorasWithShells .SelectMany(shellKvP => shellKvP.Value)) { uniqueShells.Add(shell); } return uniqueShells.ToList(); } /// /// Filter out non-whitelisted weapon scopes /// Controlled by bot.json weaponSightWhitelist /// e.g. filter out rifle scopes from SMGs /// /// Weapon scopes will be added to /// Full scope pool /// Whitelist of scope types by weapon base type /// Array of scope tpls that have been filtered to just ones allowed for that weapon type public List FilterSightsByWeaponType(Item weapon, List scopes, Dictionary> botWeaponSightWhitelist) { throw new NotImplementedException(); } }