diff --git a/Core/Generators/BotEquipmentModGenerator.cs b/Core/Generators/BotEquipmentModGenerator.cs index ffc8a4b3..895e84f9 100644 --- a/Core/Generators/BotEquipmentModGenerator.cs +++ b/Core/Generators/BotEquipmentModGenerator.cs @@ -21,41 +21,65 @@ 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 DatabaseService _databaseService; + private readonly ItemHelper _itemHelper; + private readonly BotEquipmentFilterService _botEquipmentFilterService; private readonly ItemFilterService _itemFilterService; + private readonly ProfileHelper _profileHelper; + private readonly BotWeaponModLimitService _botWeaponModLimitService; + private readonly BotHelper _botHelper; + private readonly BotGeneratorHelper _botGeneratorHelper; + private readonly BotWeaponGeneratorHelper _botWeaponGeneratorHelper; + private readonly WeightedRandomHelper _weightedRandomHelper; + private readonly PresetHelper _presetHelper; + private readonly LocalisationService _localisationService; + private readonly BotEquipmentModPoolService _botEquipmentModPoolService; private readonly ConfigServer _configServer; private readonly ICloner _cloner; + private BotConfig _botConfig; - public BotEquipmentModGenerator( + public BotEquipmentModGenerator + ( ISptLogger logger, HashUtil hashUtil, RandomUtil randomUtil, - ItemHelper itemHelper, - BotGeneratorHelper botGeneratorHelper, - BotEquipmentModPoolService botEquipmentModPoolService, - PresetHelper presetHelper, ProbabilityHelper probabilityHelper, - LocalisationService localisationService, + DatabaseService databaseService, + ItemHelper itemHelper, + BotEquipmentFilterService botEquipmentFilterService, ItemFilterService itemFilterService, + ProfileHelper profileHelper, + BotWeaponModLimitService botWeaponModLimitService, + BotHelper botHelper, + BotGeneratorHelper botGeneratorHelper, + BotWeaponGeneratorHelper botWeaponGeneratorHelper, + WeightedRandomHelper weightedRandomHelper, + PresetHelper presetHelper, + LocalisationService localisationService, + BotEquipmentModPoolService botEquipmentModPoolService, ConfigServer configServer, - ICloner cloner) + ICloner cloner + ) { _logger = logger; _hashUtil = hashUtil; _randomUtil = randomUtil; - _itemHelper = itemHelper; - _botGeneratorHelper = botGeneratorHelper; - _botEquipmentModPoolService = botEquipmentModPoolService; - _presetHelper = presetHelper; _probabilityHelper = probabilityHelper; - _localisationService = localisationService; + _databaseService = databaseService; + _itemHelper = itemHelper; + _botEquipmentFilterService = botEquipmentFilterService; _itemFilterService = itemFilterService; + _profileHelper = profileHelper; + _botWeaponModLimitService = botWeaponModLimitService; + _botHelper = botHelper; + _botGeneratorHelper = botGeneratorHelper; + _botWeaponGeneratorHelper = botWeaponGeneratorHelper; + _weightedRandomHelper = weightedRandomHelper; + _presetHelper = presetHelper; + _localisationService = localisationService; + _botEquipmentModPoolService = botEquipmentModPoolService; _configServer = configServer; _cloner = cloner; @@ -76,12 +100,12 @@ public class BotEquipmentModGenerator 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}"); + _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 @@ -92,12 +116,18 @@ public class BotEquipmentModGenerator 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 - })); + _logger.Error( + _localisationService.GetText( + "bot-mod_slot_missing_from_item", + new + { + modSlot = modSlotName, + parentId = parentTemplate.Id, + parentName = parentTemplate.Name, + botRole = settings.BotData.Role + } + ) + ); continue; } @@ -106,7 +136,8 @@ public class BotEquipmentModGenerator itemSlotTemplate, modSlotName, settings.SpawnChances.EquipmentModsChances, - settings.BotEquipmentConfig); + settings.BotEquipmentConfig + ); // Rolled to skip mod and it shouldnt be force-spawned if (modSpawnResult == ModSpawn.SKIP && !forceSpawn) @@ -141,7 +172,8 @@ public class BotEquipmentModGenerator settings, modSlotName.ToLower(), compatibleModsPool[modSlotName], - parentTemplate); + 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"); @@ -151,7 +183,9 @@ public class BotEquipmentModGenerator 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"); + _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 @@ -165,7 +199,8 @@ public class BotEquipmentModGenerator while (exhaustableModPool.HasValues()) { modTpl = exhaustableModPool.GetRandomValue(); - if (modTpl is not null && !_botGeneratorHelper.IsItemIncompatibleWithCurrentItems(equipment, modTpl, modSlotName).Incompatible.GetValueOrDefault(false)) + if (modTpl is not null && + !_botGeneratorHelper.IsItemIncompatibleWithCurrentItems(equipment, modTpl, modSlotName).Incompatible.GetValueOrDefault(false)) { found = true; break; @@ -193,7 +228,8 @@ public class BotEquipmentModGenerator itemSlotTemplate, modSlotName, parentTemplate, - settings.BotData.Role) + settings.BotData.Role + ) ) { continue; @@ -202,7 +238,8 @@ public class BotEquipmentModGenerator // 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)); + 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)) @@ -214,7 +251,8 @@ public class BotEquipmentModGenerator modTemplate.Value, settings, specificBlacklist, - forceSpawn); + forceSpawn + ); } } @@ -243,7 +281,275 @@ public class BotEquipmentModGenerator /// Weapon + mods array public List GenerateModsForWeapon(string sessionId, GenerateWeaponRequest request) { - throw new NotImplementedException(); + var pmcProfile = _profileHelper.GetPmcProfile(sessionId); + + // Get pool of mods that fit weapon + var compatibleModsPool = request.ModPool[request.ParentTemplate.Id]; + + if ( + !( + request.ParentTemplate.Properties.Slots.Any() || + request.ParentTemplate.Properties.Cartridges.Any() || + request.ParentTemplate.Properties.Chambers.Any() + ) + ) + { + _logger.Error( + _localisationService.GetText( + "bot-unable_to_add_mods_to_weapon_missing_ammo_slot", + new + { + WeaponName = request.ParentTemplate.Name, + WeaponId = request.ParentTemplate.Id, + BotRole = request.BotData.Role, + } + ) + ); + + return request.Weapon; + } + + var botEquipConfig = _botConfig.Equipment[request.BotData.EquipmentRole]; + var botEquipBlacklist = _botEquipmentFilterService.GetBotEquipmentBlacklist( + request.BotData.EquipmentRole, + pmcProfile.Info.Level ?? 0 + ); + var botWeaponSightWhitelist = _botEquipmentFilterService.GetBotWeaponSightWhitelist( + request.BotData.EquipmentRole + ); + var randomisationSettings = _botHelper.GetBotRandomizationDetails(request.BotData.Level ?? 0, botEquipConfig); + + // Iterate over mod pool and choose mods to attach + var sortedModKeys = SortModKeys(compatibleModsPool.Keys.ToList(), request.ParentTemplate.Id); + foreach (var modSlot in sortedModKeys) + { + // Check weapon has slot for mod to fit in + var modsParentSlot = GetModItemSlotFromDb(modSlot, request.ParentTemplate); + if (modsParentSlot is null) + { + _logger.Error( + _localisationService.GetText( + "bot-weapon_missing_mod_slot", + new + { + ModSlot = modSlot, + WeaponId = request.ParentTemplate.Id, + WeaponName = request.ParentTemplate.Name, + BotRole = request.BotData.Role, + } + ) + ); + + continue; + } + + // Check spawn chance of mod + var modSpawnResult = ShouldModBeSpawned( + modsParentSlot, + modSlot, + request.ModSpawnChances, + botEquipConfig + ); + if (modSpawnResult == ModSpawn.SKIP) + { + continue; + } + + var isRandomisableSlot = randomisationSettings?.RandomisedWeaponModSlots?.Contains(modSlot) ?? false; + ModToSpawnRequest modToSpawnRequest = new() + { + ModSlot = modSlot, + IsRandomisableSlot = isRandomisableSlot, + RandomisationSettings = randomisationSettings, + BotWeaponSightWhitelist = botWeaponSightWhitelist, + BotEquipBlacklist = botEquipBlacklist, + ItemModPool = compatibleModsPool, + Weapon = request.Weapon, + AmmoTpl = request.AmmoTpl, + ParentTemplate = request.ParentTemplate, + ModSpawnResult = modSpawnResult, + WeaponStats = request.WeaponStats, + ConflictingItemTpls = request.ConflictingItemTpls, + BotData = request.BotData + }; + var modToAdd = ChooseModToPutIntoSlot(modToSpawnRequest); + + // Compatible mod not found + if (modToAdd is null) + { + continue; + } + + if ( + IsModValidForSlot(modToAdd, modsParentSlot, modSlot, request.ParentTemplate, request.BotData.Role) + ) + { + continue; + } + + var modToAddTemplate = modToAdd.Value; + // Skip adding mod to weapon if type limit reached + if ( + _botWeaponModLimitService.WeaponModHasReachedLimit( + request.BotData.EquipmentRole, + modToAddTemplate.Value, + request.ModLimits, + request.ParentTemplate, + request.Weapon + ) + ) + { + continue; + } + + // If item is a mount for scopes, set scope chance to 100%, this helps fix empty mounts appearing on weapons + if (ModSlotCanHoldScope(modSlot, modToAddTemplate.Value.Parent)) + { + // mod_mount was picked to be added to weapon, force scope chance to ensure its filled + List scopeSlots = ["mod_scope", "mod_scope_000", "mod_scope_001", "mod_scope_002", "mod_scope_003"]; + AdjustSlotSpawnChances(request.ModSpawnChances, scopeSlots, 100); + + // Hydrate pool of mods that fit into mount as its a randomisable slot + if (isRandomisableSlot) + { + // Add scope mods to modPool dictionary to ensure the mount has a scope in the pool to pick + AddCompatibleModsForProvidedMod( + "mod_scope", + modToAddTemplate.Value, + request.ModPool, + botEquipBlacklist + ); + } + } + + // If picked item is muzzle adapter that can hold a child, adjust spawn chance + if (ModSlotCanHoldMuzzleDevices(modSlot, modToAddTemplate.Value.Parent)) + { + List muzzleSlots = ["mod_muzzle", "mod_muzzle_000", "mod_muzzle_001"]; + // Make chance of muzzle devices 95%, nearly certain but not guaranteed + AdjustSlotSpawnChances(request.ModSpawnChances, muzzleSlots, 95); + } + + // If front/rear sight are to be added, set opposite to 100% chance + if (ModIsFrontOrRearSight(modSlot, modToAddTemplate.Value.Id)) + { + request.ModSpawnChances["mod_sight_front"] = 100; + request.ModSpawnChances["mod_sight_rear"] = 100; + } + + // Handguard mod can take a sub handguard mod + weapon has no UBGL (takes same slot) + // Force spawn chance to be 100% to ensure it gets added + if ( + modSlot == "mod_handguard" && + modToAddTemplate.Value.Properties.Slots.Any((slot) => slot.Name == "mod_handguard") && + !request.Weapon.Any((item) => item.SlotId == "mod_launcher") + ) + { + // Needed for handguards with lower + request.ModSpawnChances["mod_handguard"] = 100; + } + + // If stock mod can take a sub stock mod, force spawn chance to be 100% to ensure sub-stock gets added + // Or if bot has stock force enabled + if (ShouldForceSubStockSlots(modSlot, botEquipConfig, modToAddTemplate.Value)) + { + // Stock mod can take additional stocks, could be a locking device, force 100% chance + List subStockSlots = ["mod_stock", "mod_stock_000", "mod_stock_001", "mod_stock_akms"]; + AdjustSlotSpawnChances(request.ModSpawnChances, subStockSlots, 100); + } + + // Gather stats on mods being added to weapon + if (_itemHelper.IsOfBaseclass(modToAddTemplate.Value.Id, BaseClasses.IRON_SIGHT)) + { + if (modSlot == "mod_sight_front") + { + request.WeaponStats.HasFrontIronSight = true; + } + else if (modSlot == "mod_sight_rear") + { + request.WeaponStats.HasRearIronSight = true; + } + } + else if ( + !request.WeaponStats.HasOptic ?? + false && + _itemHelper.IsOfBaseclass(modToAddTemplate.Value.Id, BaseClasses.SIGHTS) + ) + { + request.WeaponStats.HasOptic = true; + } + + var modId = _hashUtil.Generate(); + request.Weapon.Add( + CreateModItem( + modId, + modToAddTemplate.Value.Id, + request.WeaponId, + modSlot, + modToAddTemplate.Value, + request.BotData.Role + ) + ); + + // Update conflicting item list now item has been chosen + foreach (var conflictingItem in modToAddTemplate.Value.Properties.ConflictingItems) + { + request.ConflictingItemTpls.Add(conflictingItem); + } + + // I first thought we could use the recursive generateModsForItems as previously for cylinder magazines. + // However, the recursion doesn't go over the slots of the parent mod but over the modPool which is given by the bot config + // where we decided to keep cartridges instead of camoras. And since a CylinderMagazine only has one cartridge entry and + // this entry is not to be filled, we need a special handling for the CylinderMagazine + var modParentItem = _itemHelper.GetItem(modToAddTemplate.Value.Parent).Value; + if (_botWeaponGeneratorHelper.MagazineIsCylinderRelated(modParentItem.Name)) + { + // We don't have child mods, we need to create the camoras for the magazines instead + FillCamora(request.Weapon, request.ModPool, modId, modToAddTemplate.Value); + } + else + { + var containsModInPool = request.ModPool.Keys.Contains(modToAddTemplate.Value.Id); + + // Sometimes randomised slots are missing sub-mods, if so, get values from mod pool service + // Check for a randomisable slot + without data in modPool + item being added as additional slots + if (isRandomisableSlot && !containsModInPool && modToAddTemplate.Value.Properties.Slots.Any()) + { + var modFromService = _botEquipmentModPoolService.GetModsForWeaponSlot(modToAddTemplate.Value.Id); + if (modFromService.Keys.Any()) + { + request.ModPool[modToAddTemplate.Value.Id] = modFromService; + containsModInPool = true; + } + } + + if (containsModInPool) + { + GenerateWeaponRequest recursiveRequestData = new() + { + Weapon = request.Weapon, + ModPool = request.ModPool, + WeaponId = modId, + ParentTemplate = modToAddTemplate.Value, + ModSpawnChances = request.ModSpawnChances, + AmmoTpl = request.AmmoTpl, + BotData = new() + { + Role = request.BotData.Role, + Level = request.BotData.Level, + EquipmentRole = request.BotData.EquipmentRole + }, + ModLimits = request.ModLimits, + WeaponStats = request.WeaponStats, + ConflictingItemTpls = request.ConflictingItemTpls + }; + // Call self recursively to add mods to this mod + GenerateModsForWeapon(sessionId, recursiveRequestData); + } + } + } + + return request.Weapon; } /// @@ -279,7 +585,7 @@ public class BotEquipmentModGenerator return true; } - return ((string[])["mod_sight_front", "mod_sight_rear"]).Contains(modSlot); + return ((string[]) ["mod_sight_front", "mod_sight_rear"]).Contains(modSlot); } /// @@ -291,7 +597,8 @@ public class BotEquipmentModGenerator public bool ModSlotCanHoldScope(string modSlot, string modsParentId) { return ( - ((string[])[ + ((string[]) + [ "mod_scope", "mod_mount", "mod_mount_000", @@ -299,7 +606,8 @@ public class BotEquipmentModGenerator "mod_scope_001", "mod_scope_002", "mod_scope_003", - ]).Contains(modSlot.ToLower()) && modsParentId == BaseClasses.MOUNT + ]).Contains(modSlot.ToLower()) && + modsParentId == BaseClasses.MOUNT ); } @@ -325,7 +633,8 @@ public class BotEquipmentModGenerator return; } - foreach (var modName in modSlotsToAdjust) { + foreach (var modName in modSlotsToAdjust) + { modSpawnChances[modName] = newChancePercent; } } @@ -338,7 +647,7 @@ public class BotEquipmentModGenerator /// 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()); + return ((string[]) ["mod_muzzle", "mod_muzzle_000", "mod_muzzle_001"]).Contains(modSlot.ToLower()); } /// @@ -390,6 +699,7 @@ public class BotEquipmentModGenerator // 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))) { @@ -405,7 +715,7 @@ public class BotEquipmentModGenerator /// /// Data used to choose an appropriate mod with /// itemHelper.getItem() result - public object? ChooseModToPutIntoSlot(ModToSpawnRequest request) // TODO: type fuckery: [boolean, ITemplateItem] | undefined + public KeyValuePair? ChooseModToPutIntoSlot(ModToSpawnRequest request) // TODO: type fuckery: [boolean, ITemplateItem] | undefined { throw new NotImplementedException(); } @@ -558,12 +868,14 @@ public class BotEquipmentModGenerator /// Item object public Item CreateModItem(string modId, string modTpl, string parentId, string modSlot, TemplateItem modTemplate, string botRole) { - return new Item { + return new Item + { Id = modId, Template = modTpl, ParentId = parentId, SlotId = modSlot, - Upd = _botGeneratorHelper.GenerateExtraPropertiesForItem(modTemplate, botRole)}; + Upd = _botGeneratorHelper.GenerateExtraPropertiesForItem(modTemplate, botRole) + }; } /// @@ -614,7 +926,8 @@ public class BotEquipmentModGenerator /// 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) + public bool IsModValidForSlot(KeyValuePair? modToAdd, Slot slotAddedToTemplate, string modSlot, TemplateItem parentTemplate, + string botRole) { throw new NotImplementedException(); } diff --git a/Core/Models/Eft/Common/Tables/BotBase.cs b/Core/Models/Eft/Common/Tables/BotBase.cs index b61ed6e9..d5c2429e 100644 --- a/Core/Models/Eft/Common/Tables/BotBase.cs +++ b/Core/Models/Eft/Common/Tables/BotBase.cs @@ -153,7 +153,7 @@ public class Info public bool? HasCoopExtension { get; set; } public bool? HasPveGame { get; set; } public string? Voice { get; set; } - public int? Level { get; set; } + public double? Level { get; set; } public double? Experience { get; set; } [JsonConverter(typeof(StringToNumberFactoryConverter))] public long? RegistrationDate { get; set; }