using System.Collections.Concurrent; using SPTarkov.DI.Annotations; 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; [Injectable(InjectionType.Singleton)] public class BotEquipmentModPoolService( ISptLogger logger, ItemHelper itemHelper, DatabaseService databaseService, ServerLocalisationService localisationService ) { private readonly Lock _lockObject = new(); private ConcurrentDictionary< MongoId, ConcurrentDictionary> >? _gearModPool; protected ConcurrentDictionary< MongoId, ConcurrentDictionary> > GearModPool { get { lock (_lockObject) { return _gearModPool ??= GenerateGearPool(); } } } private ConcurrentDictionary< MongoId, ConcurrentDictionary> >? _weaponModPool; protected ConcurrentDictionary< MongoId, ConcurrentDictionary> > WeaponModPool { get { lock (_lockObject) { return _weaponModPool ??= GenerateWeaponPool(); } } } /// /// Get 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 ConcurrentDictionary< MongoId, ConcurrentDictionary> > GeneratePool(IEnumerable? inputItems, string poolType) { if (inputItems is null || !inputItems.Any()) { logger.Error( localisationService.GetText("bot-unable_to_generate_item_pool_no_items", poolType) ); return []; } var pool = new ConcurrentDictionary>>(); foreach (var item in inputItems) { 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; // Item has Slots + pool doesn't have value if (hasSubItemsToAdd && !pool.ContainsKey(subItemDetails.Id)) // Recursive call { GeneratePool([subItemDetails], poolType); } } } } return pool; } private bool SetContainsTpl(HashSet itemSet, MongoId tpl) { lock (_lockObject) { return itemSet.Contains(tpl); } } private bool AddTplToSet(HashSet itemSet, MongoId 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 ResetWeaponPool() { 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(MongoId itemTpl, string slotName) { if (WeaponModPool.TryGetValue(itemTpl, out var value)) { if (value.TryGetValue(slotName, out var tplsForSlotHashSet)) { return tplsForSlotHashSet; } } logger.Warning($"Slot: {slotName} not found for item: {itemTpl} in cache"); return []; } /// /// 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(MongoId itemTpl) { 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(MongoId itemTpl) { return WeaponModPool.TryGetValue(itemTpl, out var value) ? value : []; } /// /// 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(MongoId 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.TryAdd(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 ConcurrentDictionary< MongoId, ConcurrentDictionary> > GenerateWeaponPool() { var weaponsAndMods = databaseService .GetItems() .Values.Where(item => string.Equals(item.Type, "Item", StringComparison.OrdinalIgnoreCase) && itemHelper.IsOfBaseclasses(item.Id, [BaseClasses.WEAPON, BaseClasses.MOD]) ); return GeneratePool(weaponsAndMods, "weapon"); } /// /// Create gear mod pool and set generated flag to true /// protected ConcurrentDictionary< MongoId, ConcurrentDictionary> > GenerateGearPool() { var gearAndMods = 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, BaseClasses.MOD, ] ) ); return GeneratePool(gearAndMods, "gear"); } }