diff --git a/Libraries/Core/Generators/BotEquipmentModGenerator.cs b/Libraries/Core/Generators/BotEquipmentModGenerator.cs index 1405f2fe..d70472d0 100644 --- a/Libraries/Core/Generators/BotEquipmentModGenerator.cs +++ b/Libraries/Core/Generators/BotEquipmentModGenerator.cs @@ -244,11 +244,11 @@ public class BotEquipmentModGenerator( * @param modSlot front/back * @returns Armor IItem */ - protected Item GetDefaultPresetArmorSlot(string armorItemTpl, string modSlot) + protected Item? GetDefaultPresetArmorSlot(string armorItemTpl, string modSlot) { var defaultPreset = _presetHelper.GetDefaultPreset(armorItemTpl); - return defaultPreset?.Items.FirstOrDefault((item) => item.SlotId?.ToLower() == modSlot); + return defaultPreset?.Items?.FirstOrDefault((item) => item.SlotId?.ToLower() == modSlot); } @@ -260,11 +260,6 @@ public class BotEquipmentModGenerator( /// Weapon + mods array public List GenerateModsForWeapon(string sessionId, GenerateWeaponRequest request) { - 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() || @@ -288,7 +283,12 @@ public class BotEquipmentModGenerator( return request.Weapon; } - var botEquipConfig = _botConfig.Equipment[request.BotData.EquipmentRole]; + var pmcProfile = _profileHelper.GetPmcProfile(sessionId); + + // Get pool of mods that fit weapon + request.ModPool.TryGetValue(request.ParentTemplate.Id, out var compatibleModsPool); + + _botConfig.Equipment.TryGetValue(request.BotData.EquipmentRole, out var botEquipConfig); var botEquipBlacklist = _botEquipmentFilterService.GetBotEquipmentBlacklist( request.BotData.EquipmentRole, pmcProfile.Info.Level ?? 0 @@ -494,13 +494,26 @@ public class BotEquipmentModGenerator( if (isRandomisableSlot && !containsModInPool && modToAddTemplate.Value.Properties.Slots.Any()) { var modFromService = _botEquipmentModPoolService.GetModsForWeaponSlot(modToAddTemplate.Value.Id); - if (modFromService.Keys.Any()) + if (modFromService?.Keys.Count > 0) { request.ModPool[modToAddTemplate.Value.Id] = modFromService; containsModInPool = true; } } + // Fallback when mods with REQUIRED children are not in the pool, add them and process + if (!containsModInPool && !isRandomisableSlot) + { + // Check for required mods the item we've added needs to be classified as 'valid' + var modFromService = _botEquipmentModPoolService.GetRequiredModsForWeaponSlot(modToAddTemplate.Value.Id); + if (modFromService?.Keys.Count > 0) + { + request.ModPool[modToAddTemplate.Value.Id] = modFromService; + containsModInPool = true; + } + + } + if (containsModInPool) { GenerateWeaponRequest recursiveRequestData = new() @@ -871,16 +884,16 @@ public class BotEquipmentModGenerator( request.Weapon, request.ModSlot ); - if (chosenModResult.SlotBlocked ?? false && !(parentSlot.Required ?? false)) + if (chosenModResult.SlotBlocked.GetValueOrDefault(false) && !parentSlot.Required.GetValueOrDefault(false)) { // Don't bother trying to fit mod, slot is completely blocked return null; } // Log if mod chosen was incompatible - if (chosenModResult.Incompatible ?? false && !(parentSlot.Required ?? false)) + if (chosenModResult.Incompatible.GetValueOrDefault(false) && !(parentSlot.Required.GetValueOrDefault(false))) { - _logger.Debug(chosenModResult.Reason); + _logger.Debug($"Unable to find compatible mod of type: {parentSlot.Name}, in slot: {request.ModSlot} reason: {chosenModResult.Reason}"); } // Get random mod to attach from items db for required slots if none found above @@ -891,14 +904,14 @@ public class BotEquipmentModGenerator( } // Compatible item not found + not required - if (!(chosenModResult.Found ?? false) && parentSlot != null && (!parentSlot.Required ?? false)) + if (!(chosenModResult.Found.GetValueOrDefault(false)) && parentSlot is not null && (!parentSlot.Required.GetValueOrDefault(false))) { return null; } - if (!(chosenModResult.Found ?? false) && parentSlot != null) + if (!(chosenModResult.Found ?? false) && parentSlot is not null) { - if (parentSlot.Required ?? false) + if (parentSlot.Required.GetValueOrDefault(false)) { _logger.Warning( $"Required slot unable to be filled, {request.ModSlot} on {request.ParentTemplate.Name} {request.ParentTemplate.Id} for weapon: {request.Weapon[0].Template}" @@ -966,7 +979,7 @@ public class BotEquipmentModGenerator( // Filter mod pool to only items that appear in parents allowed list preFilteredModPool = preFilteredModPool.Where((tpl) => parentSlot.Props.Filters[0].Filter.Contains(tpl)).ToList(); - if (preFilteredModPool.Count() == 0) + if (preFilteredModPool.Count == 0) { return new() { Incompatible = true, Found = false, Reason = "No mods found in parents allowed list" }; } @@ -995,9 +1008,9 @@ public class BotEquipmentModGenerator( }; // Limit how many attempts to find a compatible mod can occur before giving up - var maxBlockedAttempts = Math.Round(modPool.Count() * 0.75); // 75% of pool size + var maxBlockedAttempts = Math.Round(modPool.Count * 0.75); // 75% of pool size var blockedAttemptCount = 0; - string chosenTpl = null; + string chosenTpl; while (exhaustableModPool.HasValues()) { chosenTpl = exhaustableModPool.GetRandomValue(); @@ -1015,7 +1028,7 @@ public class BotEquipmentModGenerator( } // Success - Default wanted + only 1 item in pool - if (modSpawnType == ModSpawn.DEFAULT_MOD && modPool.Count() == 1) + if (modSpawnType == ModSpawn.DEFAULT_MOD && modPool.Count == 1) { chosenModResult.Found = true; chosenModResult.Incompatible = false; @@ -1032,15 +1045,20 @@ public class BotEquipmentModGenerator( if (existingItemBlockingChoice is not null) { // Give max of x attempts of picking a mod if blocked by another - if (blockedAttemptCount > maxBlockedAttempts) + // OR Blocked and modpool only had 1 item + if (blockedAttemptCount > maxBlockedAttempts || modPool.Count == 1) { blockedAttemptCount = 0; // reset + //chosenModResult.SlotBlocked = true; // Later in code we try to find replacement, but only when "slotBlocked" is not true + chosenModResult.Reason = "Blocked"; + break; } blockedAttemptCount++; // Not compatible - Try again + ; continue; } @@ -1073,7 +1091,7 @@ public class BotEquipmentModGenerator( /// /// Tpls that are incompatible and should not be used /// string array of compatible mod tpls with weapon - public List GetFilteredModPool(HashSet modPool, List tplBlacklist) + public List GetFilteredModPool(HashSet modPool, HashSet tplBlacklist) { return modPool.Where((tpl) => !tplBlacklist.Contains(tpl)).ToList(); } diff --git a/Libraries/Core/Generators/BotWeaponGenerator.cs b/Libraries/Core/Generators/BotWeaponGenerator.cs index 6799090c..7a625bda 100644 --- a/Libraries/Core/Generators/BotWeaponGenerator.cs +++ b/Libraries/Core/Generators/BotWeaponGenerator.cs @@ -176,7 +176,7 @@ public class BotWeaponGenerator( ); } - // Use weapon preset from globals.json if weapon isnt valid + // Use weapon preset from globals.json if weapon isn't valid if (!IsWeaponValid(weaponWithModsArray, botRole)) { // Weapon is bad, fall back to weapons preset diff --git a/Libraries/Core/Models/Spt/Bots/GenerateWeaponRequest.cs b/Libraries/Core/Models/Spt/Bots/GenerateWeaponRequest.cs index e1ec77d4..a1fef553 100644 --- a/Libraries/Core/Models/Spt/Bots/GenerateWeaponRequest.cs +++ b/Libraries/Core/Models/Spt/Bots/GenerateWeaponRequest.cs @@ -43,7 +43,7 @@ public record GenerateWeaponRequest /** Array of item tpls the weapon does not support */ [JsonPropertyName("conflictingItemTpls")] - public List? ConflictingItemTpls { get; set; } + public HashSet? ConflictingItemTpls { get; set; } } public record BotData diff --git a/Libraries/Core/Models/Spt/Bots/ModToSpawnRequest.cs b/Libraries/Core/Models/Spt/Bots/ModToSpawnRequest.cs index a5a8edad..96d0d652 100644 --- a/Libraries/Core/Models/Spt/Bots/ModToSpawnRequest.cs +++ b/Libraries/Core/Models/Spt/Bots/ModToSpawnRequest.cs @@ -74,7 +74,7 @@ public record ModToSpawnRequest /// List of item tpls the weapon does not support /// [JsonPropertyName("conflictingItemTpls")] - public List? ConflictingItemTpls { get; set; } + public HashSet? ConflictingItemTpls { get; set; } [JsonPropertyName("botData")] public BotData? BotData { get; set; } diff --git a/Libraries/Core/Services/BotEquipmentModPoolService.cs b/Libraries/Core/Services/BotEquipmentModPoolService.cs index f2ba4968..5473178d 100644 --- a/Libraries/Core/Services/BotEquipmentModPoolService.cs +++ b/Libraries/Core/Services/BotEquipmentModPoolService.cs @@ -170,6 +170,31 @@ public class BotEquipmentModPoolService return _weaponModPool[itemTpl]; } + 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 */