using System.Collections.Concurrent; using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Helpers; 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; [Injectable(InjectionType.Singleton)] public class BotEquipmentModPoolService { private readonly Lock _lockObject = new(); protected DatabaseService _databaseService; protected ConcurrentDictionary>> _gearModPool; protected ItemHelper _itemHelper; protected LocalisationService _localisationService; protected ISptLogger _logger; protected ConcurrentDictionary>> _weaponModPool; protected bool _weaponPoolGenerated; protected bool _armorPoolGenerated; public BotEquipmentModPoolService( ISptLogger logger, ItemHelper itemHelper, DatabaseService databaseService, LocalisationService localisationService ) { _logger = logger; _itemHelper = itemHelper; _databaseService = databaseService; _localisationService = localisationService; _weaponModPool = new ConcurrentDictionary>>(); _gearModPool = new ConcurrentDictionary>>(); } /// /// Store dictionary of mods for each item passed in /// /// Items to find related mods and store in modPool /// Mod pool to choose from e.g. "weapon" for weaponModPool protected void GeneratePool(IEnumerable? items, string poolType) { if (items is null) { _logger.Error(_localisationService.GetText("bot-unable_to_generate_item_pool_no_items", poolType)); return; } // Get weapon or gear pool var pool = poolType == "weapon" ? _weaponModPool : _gearModPool; foreach (var item in items) { if (item.Properties is null) { _logger.Error( _localisationService.GetText( "bot-item_missing_props_property", new { itemTpl = item.Id, name = item.Name } ) ); continue; } // Skip item without slots if (item.Properties.Slots is null || item.Properties.Slots.Count == 0) { continue; } // Add base item (weapon/armor) to pool pool.TryAdd(item.Id, new ConcurrentDictionary>()); // iterate over each items mod slots e.g. mod_muzzle foreach (var slot in item.Properties.Slots) { // Get mods that fit into the current mod slot var itemsThatFit = slot.Props.Filters.FirstOrDefault().Filter; // Get weapon/armor pool to add mod slots + mod tpls to var itemModPool = pool[item.Id]; foreach (var itemToAddTpl in itemsThatFit) { // Ensure Mod slot key + blank dict value exist InitSetInDict(itemModPool, slot.Name); // Does tpl exist inside mod_slots hashset if (!SetContainsTpl(itemModPool[slot.Name], itemToAddTpl)) // Keyed by mod slot { AddTplToSet(itemModPool[slot.Name], itemToAddTpl); } var subItemDetails = _itemHelper.GetItem(itemToAddTpl).Value; var hasSubItemsToAdd = (subItemDetails?.Properties?.Slots?.Count ?? 0) > 0; if (hasSubItemsToAdd && !pool.ContainsKey(subItemDetails.Id)) // Recursive call { GeneratePool([subItemDetails], poolType); } } } } } private bool SetContainsTpl(HashSet itemSet, string tpl) { lock (_lockObject) { return itemSet.Contains(tpl); } } private bool AddTplToSet(HashSet itemSet, string itemToAddTpl) { lock (_lockObject) { return itemSet.Add(itemToAddTpl); } } private bool InitSetInDict(ConcurrentDictionary> dictionary, string slotName) { lock (_lockObject) { return dictionary.TryAdd(slotName, []); } } /// /// Empty the mod pool /// public void ResetPool() { _weaponModPool.Clear(); } /// /// Get array of compatible mods for an items mod slot (generate pool if it doesn't exist already) /// /// Item to look up /// Slot to get compatible mods for /// Hashset of tpls that fit the slot public HashSet GetCompatibleModsForWeaponSlot(string itemTpl, string slotName) { if (!_weaponPoolGenerated) // Get every weapon in db and generate mod pool { GenerateWeaponPool(); } return _weaponModPool[itemTpl][slotName]; } /// /// Get mods for a piece of gear by its tpl /// /// Items tpl to look up mods for /// Dictionary of mods (keys are mod slot names) with array of compatible mod tpls as value public ConcurrentDictionary> GetModsForGearSlot(string itemTpl) { if (!_armorPoolGenerated) { GenerateGearPool(); } return _gearModPool.TryGetValue(itemTpl, out var value) ? value : []; } /// /// Get mods for a weapon by its tpl /// /// Weapons tpl to look up mods for /// Dictionary of mods (keys are mod slot names) with array of compatible mod tpls as value public ConcurrentDictionary> GetModsForWeaponSlot(string itemTpl) { if (!_weaponPoolGenerated) { GenerateWeaponPool(); } return _weaponModPool[itemTpl]; } /// /// Get required mods for a weapon by its tpl /// /// Weapons tpl to look up mods for /// Dictionary of mods (keys are mod slot names) with array of compatible mod tpls as value public Dictionary>? GetRequiredModsForWeaponSlot(string itemTpl) { var result = new Dictionary>(); // Get item from db var itemDb = _itemHelper.GetItem(itemTpl).Value; if (itemDb.Properties.Slots is not null) // Loop over slots flagged as 'required' { foreach (var slot in itemDb.Properties.Slots.Where(slot => slot.Required.GetValueOrDefault(false))) { // Create dict entry for mod slot result.Add(slot.Name, []); // Add compatible tpls to dicts hashset foreach (var compatibleItemTpl in slot.Props.Filters.FirstOrDefault().Filter) { result[slot.Name].Add(compatibleItemTpl); } } } return result; } /// /// Create weapon mod pool and set generated flag to true /// protected void GenerateWeaponPool() { var weapons = _databaseService.GetItems() .Values.Where(item => string.Equals(item.Type, "Item", StringComparison.OrdinalIgnoreCase) && _itemHelper.IsOfBaseclass(item.Id, BaseClasses.WEAPON) ); GeneratePool(weapons, "weapon"); // Flag pool as being complete _weaponPoolGenerated = true; } /// /// Create gear mod pool and set generated flag to true /// protected void GenerateGearPool() { var gear = _databaseService.GetItems() .Values.Where(item => string.Equals(item.Type, "Item", StringComparison.OrdinalIgnoreCase) && _itemHelper.IsOfBaseclasses( item.Id, [ BaseClasses.ARMORED_EQUIPMENT, BaseClasses.VEST, BaseClasses.ARMOR, BaseClasses.HEADWEAR ] ) ); GeneratePool(gear, "gear"); // Flag pool as being complete _armorPoolGenerated = true; } }