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.Common; 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, 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 ) { private 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( MongoId sessionId, string equipmentSlot, BotTypeInventory botTemplateInventory, MongoId 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 MongoId 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( MongoId sessionId, MongoId weaponTpl, string slotName, BotTypeInventory botTemplateInventory, MongoId 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).ToList(); // 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 = [], }; 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).Any() && weaponItemTemplate.Properties.Chambers.FirstOrDefault().Properties.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"); MongoId? 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.Value); } } 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, MongoId ammoTemplate, IEnumerable 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 = new MongoId(), 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 IEnumerable ConstructWeaponBaseList( MongoId weaponTemplate, string weaponParentId, string equipmentSlot, TemplateItem weaponItemTemplate, string botRole ) { return [ new Item { Id = new MongoId(), 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( MongoId 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 weaponAndChildren, string botRole) { // Key weapon + children by parentId + slot name, ignore items without parentId or slotId var slotItemLookup = weaponAndChildren.ToLookup(item => (item.ParentId, item.SlotId)); foreach (var item in weaponAndChildren) { var modTemplate = itemHelper.GetItem(item.Template).Value; if (!modTemplate?.Properties?.Slots?.Any() ?? false) { continue; } var requiredSlots = modTemplate?.Properties?.Slots?.Where(slot => slot.Required.GetValueOrDefault(false)) ?? []; if (!requiredSlots.Any()) { // No required slots, skip to next item in weapon continue; } foreach (var requiredSlot in requiredSlots.ToList()) { // Check if slot exists in cache if (!slotItemLookup[(item.Id, requiredSlot.Name)].Any()) { logger.Warning( serverLocalisationService.GetText( "bot-weapons_required_slot_missing_item", new { modSlot = requiredSlot.Name, modName = modTemplate.Name, slotId = item.SlotId, botRole, } ) ); return false; } } return true; } return true; } /// /// Generates extra magazines or bullets (if magazine is internal) and adds them to TacticalVest and Pockets. /// Additionally, adds extra bullets to SecuredContainer /// /// Bots unique identifier /// 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( MongoId botId, 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).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 && !generatedWeaponResult.ChosenUbglAmmoTemplate.Value.IsEmpty) { AddUbglGrenadesToBotInventory(botId, weaponAndMods, generatedWeaponResult, inventory); } var inventoryMagGenModel = new InventoryMagGen(magWeights, magTemplate, weaponTemplate, ammoTemplate.Value, inventory, botId); 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( botId, BotConfig.SecureContainerAmmoStackCount, generatedWeaponResult.ChosenAmmoTemplate, ammoTemplate.Value.Properties.StackMaxSize ?? 0, inventory ); } /// /// Add Grenades for UBGL to bot's vest and secure container /// /// Bots unique identifier /// Weapon list with mods /// Result of weapon generation /// Bot inventory to add grenades to protected void AddUbglGrenadesToBotInventory( MongoId botId, 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).Value; // Add grenades to bot inventory var ubglAmmoGenModel = new InventoryMagGen(ubglMinMax, ubglDbTemplate, ubglDbTemplate, ubglAmmoDbTemplate, inventory, botId); InventoryMagGenComponents.FirstOrDefault(v => v.CanHandleInventoryMagGen(ubglAmmoGenModel)).Process(ubglAmmoGenModel); // Store extra grenades in secure container AddAmmoToSecureContainer(botId, 5, generatedWeaponResult.ChosenUbglAmmoTemplate.Value, 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(MongoId botId, int stackCount, MongoId ammoTpl, int stackSize, BotBaseInventory inventory) { var container = new HashSet { EquipmentSlots.SecuredContainer }; for (var i = 0; i < stackCount; i++) { var id = new MongoId(); botGeneratorHelper.AddItemWithChildrenToEquipmentSlot( botId, container, id, ammoTpl, [ new Item { 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 MongoId? GetMagazineTemplateFromWeaponTemplate(IEnumerable weaponMods, TemplateItem weaponTemplate, string botRole) { var magazine = weaponMods.FirstOrDefault(m => m.SlotId == ModMagazineSlotId); if (magazine is null) { // Edge case - magazineless chamber loaded weapons don't 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 MongoId 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.Value; } // 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.Value; } // 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.Value; } // 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.Value; } } 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()?.Properties?.Filters?.First().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.Properties?.Filters.FirstOrDefault()?.Filter?.FirstOrDefault() ?? new MongoId(null) ); 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()?.Properties?.Filters.FirstOrDefault()?.Filter ?? magazineTemplate.Value.Properties.Cartridges.FirstOrDefault()?.Properties?.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.First().Properties.Filters.First().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, MongoId 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 parentDbItem = 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(parentDbItem.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. /// Underbarrrel grenade launcher item. /// Grenade ammo template. protected void FillUbgl(List weaponMods, Item ubglMod, MongoId ubglAmmoTpl) { weaponMods.Add( new Item { Id = new MongoId(), 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, MongoId 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(IEnumerable weaponMods, MongoId magazineId, MongoId 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 }; } } } }