using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Extensions; using SPTarkov.Server.Core.Generators.WeaponGen; using SPTarkov.Server.Core.Helpers; using SPTarkov.Server.Core.Models.Eft.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Spt.Bots; using SPTarkov.Server.Core.Models.Spt.Config; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Services; using SPTarkov.Server.Core.Utils; using SPTarkov.Server.Core.Utils.Cloners; using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel; namespace SPTarkov.Server.Core.Generators; [Injectable(InjectionType.Singleton)] public class BotWeaponGenerator( ISptLogger _logger, HashUtil _hashUtil, DatabaseService _databaseService, ItemHelper _itemHelper, WeightedRandomHelper _weightedRandomHelper, BotGeneratorHelper _botGeneratorHelper, RandomUtil _randomUtil, BotWeaponGeneratorHelper _botWeaponGeneratorHelper, BotWeaponModLimitService _botWeaponModLimitService, BotEquipmentModGenerator _botEquipmentModGenerator, ServerLocalisationService _serverLocalisationService, RepairService _repairService, ICloner _cloner, ConfigServer _configServer, IEnumerable inventoryMagGenComponents ) { protected const string _modMagazineSlotId = "mod_magazine"; protected readonly BotConfig _botConfig = _configServer.GetConfig(); protected readonly IEnumerable _inventoryMagGenComponents = MagGenSetUp( inventoryMagGenComponents ); protected readonly PmcConfig _pmcConfig = _configServer.GetConfig(); protected readonly RepairConfig _repairConfig = _configServer.GetConfig(); protected static List MagGenSetUp(IEnumerable components) { var inventoryMagGens = components.ToList(); inventoryMagGens.Sort((a, b) => a.GetPriority() - b.GetPriority()); return inventoryMagGens; } /// /// Pick a random weapon based on weightings and generate a functional weapon /// /// Session identifier /// Primary/secondary/holster /// e.g. assault.json /// /// /// Role of bot, e.g. assault/followerBully /// Is weapon generated for a pmc /// /// GenerateWeaponResult object public GenerateWeaponResult GenerateRandomWeapon( string sessionId, string equipmentSlot, BotTypeInventory botTemplateInventory, string weaponParentId, Dictionary modChances, string botRole, bool isPmc, int botLevel ) { var weaponTpl = PickWeightedWeaponTemplateFromPool(equipmentSlot, botTemplateInventory); return GenerateWeaponByTpl( sessionId, weaponTpl, equipmentSlot, botTemplateInventory, weaponParentId, modChances, botRole, isPmc, botLevel ); } /// /// Gets a random weighted weapon from a bot's pool of weapons. /// /// Primary/secondary/holster /// e.g. assault.json /// Weapon template public string PickWeightedWeaponTemplateFromPool( string equipmentSlot, BotTypeInventory botTemplateInventory ) { if (!Enum.TryParse(equipmentSlot, out EquipmentSlots key)) { _logger.Error($"Unable to parse equipment slot: {equipmentSlot}"); } var weaponPool = botTemplateInventory.Equipment[key]; return _weightedRandomHelper.GetWeightedValue(weaponPool); } /// /// Generates a weapon based on the supplied weapon template. /// /// The session identifier. /// Weapon template to generate (use pickWeightedWeaponTplFromPool()). /// Slot to fit into, primary/secondary/holster. /// e.g. assault.json. /// Parent ID of the weapon being generated. /// Dictionary of item types and % chance weapon will have that mod. /// e.g. assault/exusec. /// Is weapon being generated for a PMC. /// The level of the bot. /// GenerateWeaponResult object. public GenerateWeaponResult? GenerateWeaponByTpl( string sessionId, string weaponTpl, string slotName, BotTypeInventory botTemplateInventory, string weaponParentId, Dictionary modChances, string botRole, bool isPmc, int botLevel ) { var modPool = botTemplateInventory.Mods; var weaponItemTemplate = _itemHelper.GetItem(weaponTpl).Value; if (weaponItemTemplate is null) { _logger.Error( _serverLocalisationService.GetText("bot-missing_item_template", weaponTpl) ); _logger.Error($"WeaponSlot -> {slotName}"); return null; } // Find ammo to use when filling magazines/chamber if (botTemplateInventory.Ammo is null) { _logger.Error( _serverLocalisationService.GetText("bot-no_ammo_found_in_bot_json", botRole) ); _logger.Error(_serverLocalisationService.GetText("bot-generation_failed")); } var ammoTpl = GetWeightedCompatibleAmmo(botTemplateInventory.Ammo, weaponItemTemplate); // Create with just base weapon item var weaponWithModsArray = ConstructWeaponBaseList( weaponTpl, weaponParentId, slotName, weaponItemTemplate, botRole ); // Chance to add randomised weapon enhancement if (isPmc && _randomUtil.GetChance100(_pmcConfig.WeaponHasEnhancementChancePercent)) // Add buff to weapon root { _repairService.AddBuff(_repairConfig.RepairKit.Weapon, weaponWithModsArray[0]); } // Add mods to weapon base if (modPool.ContainsKey(weaponTpl)) { // Role to treat bot as e.g. pmc/scav/boss var botEquipmentRole = _botGeneratorHelper.GetBotEquipmentRole(botRole); // Different limits if bot is boss vs scav var modLimits = _botWeaponModLimitService.GetWeaponModLimits(botEquipmentRole); GenerateWeaponRequest generateWeaponModsRequest = new() { Weapon = weaponWithModsArray, // Will become hydrated array of weapon + mods ModPool = modPool, WeaponId = weaponWithModsArray[0].Id, // Weapon root id ParentTemplate = weaponItemTemplate, ModSpawnChances = modChances, AmmoTpl = ammoTpl, BotData = new BotData { Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole, }, ModLimits = modLimits, WeaponStats = new WeaponStats(), ConflictingItemTpls = new HashSet(), }; weaponWithModsArray = _botEquipmentModGenerator.GenerateModsForWeapon( sessionId, generateWeaponModsRequest ); } // Use weapon preset from globals.json if weapon isn't valid if (!IsWeaponValid(weaponWithModsArray, botRole)) // Weapon is bad, fall back to weapons preset { weaponWithModsArray = GetPresetWeaponMods( weaponTpl, slotName, weaponParentId, weaponItemTemplate, botRole ); } var tempList = _cloner.Clone( weaponWithModsArray.Where(item => item.SlotId == _modMagazineSlotId) ); // Fill existing magazines to full and sync ammo type foreach (var magazine in tempList) { FillExistingMagazines(weaponWithModsArray, magazine, ammoTpl); } // Add cartridge(s) to gun chamber(s) if ( weaponItemTemplate.Properties?.Chambers?.Count > 0 && weaponItemTemplate .Properties.Chambers.FirstOrDefault() .Props.Filters.FirstOrDefault() .Filter.Contains(ammoTpl) ) { // Guns have variety of possible Chamber ids, patron_in_weapon/patron_in_weapon_000/patron_in_weapon_001 var chamberSlotNames = weaponItemTemplate.Properties.Chambers.Select(chamberSlot => chamberSlot.Name ); AddCartridgeToChamber(weaponWithModsArray, ammoTpl, chamberSlotNames.ToList()); } // Fill UBGL if found var ubglMod = weaponWithModsArray.FirstOrDefault(x => x.SlotId == "mod_launcher"); string? ubglAmmoTpl = null; if (ubglMod is not null) { var ubglTemplate = _itemHelper.GetItem(ubglMod.Template).Value; ubglAmmoTpl = GetWeightedCompatibleAmmo(botTemplateInventory.Ammo, ubglTemplate); // this can be null - example - FollowerBoarClose2 can have an UBGL but doesn't have the ammo caliber defined in its json // the default ammo passed from GetWeightCompatibleAmmo can be null if (ubglAmmoTpl is not null) { FillUbgl(weaponWithModsArray, ubglMod, ubglAmmoTpl); } } return new GenerateWeaponResult { Weapon = weaponWithModsArray, ChosenAmmoTemplate = ammoTpl, ChosenUbglAmmoTemplate = ubglAmmoTpl, WeaponMods = modPool, WeaponTemplate = weaponItemTemplate, }; } /// /// Insert cartridge(s) into a weapon /// Handles all chambers - patron_in_weapon, patron_in_weapon_000 etc /// /// Weapon and mods /// Cartridge to add to weapon /// Name of slots to create or add ammo to protected void AddCartridgeToChamber( List weaponWithModsList, string ammoTemplate, List chamberSlotIds ) { foreach (var slotId in chamberSlotIds) { var existingItemWithSlot = weaponWithModsList.FirstOrDefault(x => x.SlotId == slotId); if (existingItemWithSlot is null) { // Not found, add new slot to weapon weaponWithModsList.Add( new Item { Id = _hashUtil.Generate(), Template = ammoTemplate, ParentId = weaponWithModsList[0].Id, SlotId = slotId, Upd = new Upd { StackObjectsCount = 1 }, } ); } else { // Already exists, update values existingItemWithSlot.Template = ammoTemplate; existingItemWithSlot.Upd = new Upd { StackObjectsCount = 1 }; } } } /// /// Create a list with weapon base as the only element and /// add additional properties based on weapon type /// /// Weapon template to create item with /// Weapons parent id /// e.g. primary/secondary/holster /// Database template for weapon /// For durability values /// Base weapon item in a list protected List ConstructWeaponBaseList( string weaponTemplate, string weaponParentId, string equipmentSlot, TemplateItem weaponItemTemplate, string botRole ) { return [ new Item { Id = _hashUtil.Generate(), Template = weaponTemplate, ParentId = weaponParentId, SlotId = equipmentSlot, Upd = _botGeneratorHelper.GenerateExtraPropertiesForItem( weaponItemTemplate, botRole ), }, ]; } /// /// Get the mods necessary to kit out a weapon to its preset level /// /// Weapon to find preset for /// The slot the weapon will be placed in /// Value used for the parent id /// Item template /// Bot role /// List of weapon mods protected List GetPresetWeaponMods( string weaponTemplate, string equipmentSlot, string weaponParentId, TemplateItem itemTemplate, string botRole ) { // Invalid weapon generated, fallback to preset _logger.Warning( _serverLocalisationService.GetText( "bot-weapon_generated_incorrect_using_default", $"{weaponTemplate} - {itemTemplate.Name}" ) ); List weaponMods = []; // TODO: Preset weapons trigger a lot of warnings regarding missing ammo in magazines & such Preset? preset = null; foreach (var (_, itemPreset) in _databaseService.GetGlobals().ItemPresets) { if (itemPreset.Items[0].Template == weaponTemplate) { preset = _cloner.Clone(itemPreset); break; } } if (preset is not null) { var parentItem = preset.Items[0]; parentItem.ParentId = weaponParentId; parentItem.SlotId = equipmentSlot; parentItem.Upd = _botGeneratorHelper.GenerateExtraPropertiesForItem( itemTemplate, botRole ); preset.Items[0] = parentItem; weaponMods.AddRange(preset.Items); } else { _logger.Error( _serverLocalisationService.GetText("bot-missing_weapon_preset", weaponTemplate) ); } return weaponMods; } /// /// Checks if all required slots are occupied on a weapon and all its mods. /// /// Weapon + mods /// Role of bot weapon is for /// True if valid protected bool IsWeaponValid(List weaponItemList, string botRole) { foreach (var mod in weaponItemList) { var modTemplate = _itemHelper.GetItem(mod.Template).Value; if (!modTemplate.Properties.Slots?.Any() ?? false) { continue; } // Iterate over required slots in db item, check mod exists for that slot foreach ( var modSlotTemplate in modTemplate.Properties.Slots?.Where(slot => slot.Required.GetValueOrDefault(false) ) ?? [] ) { var slotName = modSlotTemplate.Name; var hasWeaponSlotItem = weaponItemList.Any(weaponItem => weaponItem.ParentId == mod.Id && weaponItem.SlotId == slotName ); if (!hasWeaponSlotItem) { _logger.Warning( _serverLocalisationService.GetText( "bot-weapons_required_slot_missing_item", new { modSlot = modSlotTemplate.Name, modName = modTemplate.Name, slotId = mod.SlotId, botRole, } ) ); return false; } } } return true; } /// /// Generates extra magazines or bullets (if magazine is internal) and adds them to TacticalVest and Pockets. /// Additionally, adds extra bullets to SecuredContainer /// /// Object with properties for generated weapon (weapon mods pool / weapon template / ammo tpl) /// Magazine weights for count to add to inventory /// Inventory to add magazines to /// The bot type we're generating extra mags for public void AddExtraMagazinesToInventory( GenerateWeaponResult generatedWeaponResult, GenerationData magWeights, BotBaseInventory inventory, string botRole ) { var weaponAndMods = generatedWeaponResult.Weapon; var weaponTemplate = generatedWeaponResult.WeaponTemplate; var magazineTpl = GetMagazineTemplateFromWeaponTemplate( weaponAndMods, weaponTemplate, botRole ); var magTemplate = _itemHelper.GetItem(magazineTpl).Value; if (magTemplate is null) { _logger.Error( _serverLocalisationService.GetText("bot-unable_to_find_magazine_item", magazineTpl) ); return; } //var isInternalMag = magTemplate.Properties.ReloadMagType == ReloadMode.InternalMagazine; var ammoTemplate = _itemHelper.GetItem(generatedWeaponResult.ChosenAmmoTemplate); if (!ammoTemplate.Key) { _logger.Error( _serverLocalisationService.GetText( "bot-unable_to_find_ammo_item", generatedWeaponResult.ChosenAmmoTemplate ) ); return; } // Has an UBGL if (generatedWeaponResult.ChosenUbglAmmoTemplate is not null) { AddUbglGrenadesToBotInventory(weaponAndMods, generatedWeaponResult, inventory); } var inventoryMagGenModel = new InventoryMagGen( magWeights, magTemplate, weaponTemplate, ammoTemplate.Value, inventory ); _inventoryMagGenComponents .FirstOrDefault(v => v.CanHandleInventoryMagGen(inventoryMagGenModel)) .Process(inventoryMagGenModel); // Add x stacks of bullets to SecuredContainer (bots use a magic mag packing skill to reload instantly) AddAmmoToSecureContainer( _botConfig.SecureContainerAmmoStackCount, generatedWeaponResult.ChosenAmmoTemplate, ammoTemplate.Value.Properties.StackMaxSize ?? 0, inventory ); } /// /// Add Grenades for UBGL to bot's vest and secure container /// /// Weapon list with mods /// Result of weapon generation /// Bot inventory to add grenades to protected void AddUbglGrenadesToBotInventory( List weaponMods, GenerateWeaponResult generatedWeaponResult, BotBaseInventory inventory ) { // Find ubgl mod item + get details of it from db var ubglMod = weaponMods.FirstOrDefault(x => x.SlotId == "mod_launcher"); var ubglDbTemplate = _itemHelper.GetItem(ubglMod.Template).Value; // Define min/max of how many grenades bot will have GenerationData ubglMinMax = new() { Weights = new Dictionary { { 1, 1 }, { 2, 1 } }, Whitelist = new Dictionary(), }; // get ammo template from db var ubglAmmoDbTemplate = _itemHelper .GetItem(generatedWeaponResult.ChosenUbglAmmoTemplate) .Value; // Add greandes to bot inventory var ubglAmmoGenModel = new InventoryMagGen( ubglMinMax, ubglDbTemplate, ubglDbTemplate, ubglAmmoDbTemplate, inventory ); _inventoryMagGenComponents .FirstOrDefault(v => v.CanHandleInventoryMagGen(ubglAmmoGenModel)) .Process(ubglAmmoGenModel); // Store extra grenades in secure container AddAmmoToSecureContainer(5, generatedWeaponResult.ChosenUbglAmmoTemplate, 20, inventory); } /// /// Add ammo to the secure container. /// /// How many stacks of ammo to add. /// Ammo type to add. /// Size of the ammo stack to add. /// Player inventory. protected void AddAmmoToSecureContainer( int stackCount, string ammoTpl, int stackSize, BotBaseInventory inventory ) { for (var i = 0; i < stackCount; i++) { var id = _hashUtil.Generate(); _botGeneratorHelper.AddItemWithChildrenToEquipmentSlot( new HashSet { EquipmentSlots.SecuredContainer }, id, ammoTpl, new List { new() { Id = id, Template = ammoTpl, Upd = new Upd { StackObjectsCount = stackSize }, }, }, inventory ); } } /// /// Get a weapons magazine template from a weapon template. /// /// Mods from a weapon template. /// Weapon to get magazine template for. /// The bot type we are getting the magazine for. /// Magazine template string. protected string GetMagazineTemplateFromWeaponTemplate( List weaponMods, TemplateItem weaponTemplate, string botRole ) { var magazine = weaponMods.FirstOrDefault(m => m.SlotId == _modMagazineSlotId); if (magazine is null) { // Edge case - magazineless chamber loaded weapons dont have magazines, e.g. mp18 // return default mag tpl if (weaponTemplate.Properties.ReloadMode == ReloadMode.OnlyBarrel) { return weaponTemplate.GetWeaponsDefaultMagazineTpl(); } // log error if no magazine AND not a chamber loaded weapon (e.g. shotgun revolver) if (!weaponTemplate.Properties.IsChamberLoad ?? false) // Shouldn't happen { _logger.Warning( _serverLocalisationService.GetText( "bot-weapon_missing_magazine_or_chamber", new { weaponId = weaponTemplate.Id, botRole } ) ); } var defaultMagTplId = weaponTemplate.GetWeaponsDefaultMagazineTpl(); if (_logger.IsLogEnabled(LogLevel.Debug)) { _logger.Debug( $"[{botRole}] Unable to find magazine for weapon: {weaponTemplate.Id} {weaponTemplate.Name}, using mag template default: {defaultMagTplId}." ); } return defaultMagTplId; } return magazine.Template; } /// /// Finds and returns a compatible ammo template based on the bots ammo weightings (x.json/inventory/equipment/ammo) /// /// Dictionary of all cartridges keyed by type e.g. Caliber556x45NATO /// Weapon details from database we want to pick ammo for /// Ammo template that works with the desired gun protected string? GetWeightedCompatibleAmmo( Dictionary> cartridgePool, TemplateItem weaponTemplate ) { var desiredCaliber = GetWeaponCaliber(weaponTemplate); if ( !cartridgePool.TryGetValue(desiredCaliber, out var cartridgePoolForWeapon) || cartridgePoolForWeapon?.Count == 0 ) { if (_logger.IsLogEnabled(LogLevel.Debug)) { _logger.Debug( _serverLocalisationService.GetText( "bot-no_caliber_data_for_weapon_falling_back_to_default", new { weaponId = weaponTemplate.Id, weaponName = weaponTemplate.Name, defaultAmmo = weaponTemplate.Properties.DefAmmo, } ) ); } // Immediately returns, default ammo is guaranteed to be compatible // it is not guaranteed to even have a default ammo return weaponTemplate.Properties.DefAmmo; } // Get cartridges the weapons first chamber allow var compatibleCartridgesInTemplate = GetCompatibleCartridgesFromWeaponTemplate( weaponTemplate ); if (compatibleCartridgesInTemplate.Count == 0) // No chamber data found in weapon, send default { return weaponTemplate.Properties.DefAmmo; } // Inner join the weapons allowed + passed in cartridge pool to get compatible cartridges Dictionary compatibleCartridges = new(); foreach (var cartridge in cartridgePoolForWeapon) { if (compatibleCartridgesInTemplate.Contains(cartridge.Key)) { compatibleCartridges[cartridge.Key] = cartridge.Value; } } // No cartridges found, try and get something that's compatible with the gun if (!compatibleCartridges.Any()) { // Get cartridges from the weapons first magazine in filters var compatibleCartridgesInMagazine = GetCompatibleCartridgesFromMagazineTemplate( weaponTemplate ); if (compatibleCartridgesInMagazine.Count == 0) { // No compatible cartridges found in magazine, use default return weaponTemplate.Properties.DefAmmo; } // Get the caliber data from the first compatible round in the magazine var magazineCaliberData = _itemHelper .GetItem(compatibleCartridgesInMagazine.FirstOrDefault()) .Value.Properties.Caliber; cartridgePoolForWeapon = cartridgePool[magazineCaliberData]; foreach (var cartridgeKvP in cartridgePoolForWeapon) { if (compatibleCartridgesInMagazine.Contains(cartridgeKvP.Key)) { compatibleCartridges[cartridgeKvP.Key] = cartridgeKvP.Value; } } // Nothing found after also checking magazines, return default ammo if (compatibleCartridges.Count == 0) { return weaponTemplate.Properties.DefAmmo; } } return _weightedRandomHelper.GetWeightedValue(compatibleCartridges); } /// /// Get the cartridge ids from a weapon template that work with the weapon /// /// Weapon db template to get cartridges for /// List of cartridge tpls protected HashSet GetCompatibleCartridgesFromWeaponTemplate(TemplateItem weaponTemplate) { ArgumentNullException.ThrowIfNull(weaponTemplate); var cartridges = weaponTemplate .Properties?.Chambers?.FirstOrDefault() ?.Props?.Filters?[0].Filter; if (cartridges is not null) { return cartridges; } // Fallback to the magazine if possible, e.g. for revolvers return GetCompatibleCartridgesFromMagazineTemplate(weaponTemplate); } /// /// Get the cartridge ids from a weapon's magazine template that work with the weapon /// /// Weapon db template to get magazine cartridges for /// Hashset of cartridge tpls /// Thrown when weaponTemplate is null. protected HashSet GetCompatibleCartridgesFromMagazineTemplate( TemplateItem weaponTemplate ) { ArgumentNullException.ThrowIfNull(weaponTemplate); // Get the first magazine's template from the weapon var magazineSlot = weaponTemplate.Properties.Slots?.FirstOrDefault(slot => slot.Name == "mod_magazine" ); if (magazineSlot is null) { return []; } var magazineTemplate = _itemHelper.GetItem( magazineSlot.Props?.Filters.FirstOrDefault()?.Filter?.FirstOrDefault() ); if (!magazineTemplate.Key) { return []; } // Try to get cartridges from slots array first, if none found, try Cartridges array var cartridges = magazineTemplate .Value.Properties.Slots.FirstOrDefault() ?.Props?.Filters.FirstOrDefault() ?.Filter ?? magazineTemplate .Value.Properties.Cartridges.FirstOrDefault() ?.Props?.Filters.FirstOrDefault() ?.Filter; return cartridges ?? []; } /// /// Get a weapons compatible cartridge caliber /// /// Weapon to look up caliber of /// Caliber as string protected string? GetWeaponCaliber(TemplateItem weaponTemplate) { if (!string.IsNullOrEmpty(weaponTemplate.Properties.Caliber)) { return weaponTemplate.Properties.Caliber; } if (!string.IsNullOrEmpty(weaponTemplate.Properties.AmmoCaliber)) // 9x18pmm has a typo, should be Caliber9x18PM { return weaponTemplate.Properties.AmmoCaliber == "Caliber9x18PMM" ? "Caliber9x18PM" : weaponTemplate.Properties.AmmoCaliber; } if (!string.IsNullOrEmpty(weaponTemplate.Properties.LinkedWeapon)) { var ammoInChamber = _itemHelper.GetItem( weaponTemplate.Properties.Chambers[0].Props.Filters[0].Filter.FirstOrDefault() ); return !ammoInChamber.Key ? null : ammoInChamber.Value.Properties.Caliber; } return null; } /// /// Fill existing magazines to full, while replacing their contents with specified ammo /// /// Weapon with children /// Magazine item /// Cartridge to insert into magazine protected void FillExistingMagazines( List weaponMods, Item magazine, string cartridgeTemplate ) { var magazineTemplate = _itemHelper.GetItem(magazine.Template).Value; if (magazineTemplate is null) { _logger.Error( _serverLocalisationService.GetText( "bot-unable_to_find_magazine_item", magazine.Template ) ); return; } // Magazine, usually var parentItem = _itemHelper.GetItem(magazineTemplate.Parent).Value; // Revolver shotgun (MTs-255-12) uses a magazine with chambers, not cartridges ("camora_xxx") // Exchange of the camora ammo is not necessary we could also just check for stackSize > 0 here // and remove the else if (_botWeaponGeneratorHelper.MagazineIsCylinderRelated(parentItem.Name)) { FillCamorasWithAmmo(weaponMods, magazine.Id, cartridgeTemplate); } else { AddOrUpdateMagazinesChildWithAmmo( weaponMods, magazine, cartridgeTemplate, magazineTemplate ); } } /// /// Add desired ammo template as item to weapon modifications list, placed as child to UBGL. /// /// Weapon with children. /// UBGL item. /// Grenade ammo template. protected void FillUbgl(List weaponMods, Item ubglMod, string ubglAmmoTpl) { weaponMods.Add( new Item { Id = _hashUtil.Generate(), Template = ubglAmmoTpl, ParentId = ubglMod.Id, SlotId = "patron_in_weapon", Upd = new Upd { StackObjectsCount = 1 }, } ); } /// /// Add cartridges to a weapons magazine /// /// Weapon with magazine to amend /// Magazine item details we're adding cartridges to /// Cartridge to put into the magazine /// Magazines db template protected void AddOrUpdateMagazinesChildWithAmmo( List weaponWithMods, Item magazine, string chosenAmmoTpl, TemplateItem magazineTemplate ) { var magazineCartridgeChildItem = weaponWithMods.FirstOrDefault(m => m.ParentId == magazine.Id && m.SlotId == "cartridges" ); if (magazineCartridgeChildItem is not null) { // Delete the existing cartridge object and create fresh below weaponWithMods.Remove(magazineCartridgeChildItem); } // Create array with just magazine List magazineWithCartridges = [magazine]; // Add cartridges as children to above mag array _itemHelper.FillMagazineWithCartridge( magazineWithCartridges, magazineTemplate, chosenAmmoTpl, 1 ); // Replace existing magazine with above array of mag + cartridge stacks var magazineIndex = weaponWithMods.FindIndex(i => i.Id == magazine.Id); // magazineWithCartridges if (magazineIndex == -1) { _logger.Error( $"Unable to add cartridges: {chosenAmmoTpl} to magazine: {magazine.Id} as none found" ); return; } weaponWithMods.RemoveAt(magazineIndex); // Insert new mag at same index position original was weaponWithMods.InsertRange(magazineIndex, magazineWithCartridges); } /// /// Fill each Camora with a bullet /// /// Weapon mods to find and update camora mod(s) from /// Magazine id to find and add to /// Ammo template id to hydrate with protected void FillCamorasWithAmmo(List weaponMods, string magazineId, string ammoTpl) { // for CylinderMagazine we exchange the ammo in the "camoras". // This might not be necessary since we already filled the camoras with a random whitelisted and compatible ammo type, // but I'm not sure whether this is also used elsewhere var camoras = weaponMods .Where(x => x.ParentId == magazineId && x.SlotId.StartsWith("camora", StringComparison.Ordinal) ) .ToList(); if (camoras.Count == 0) { return; } foreach (var camora in camoras) { camora.Template = ammoTpl; if (camora.Upd is not null) { camora.Upd.StackObjectsCount = 1; } else { camora.Upd = new Upd { StackObjectsCount = 1 }; } } } }