using System.Collections.Concurrent; using System.Collections.Frozen; using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Constants; using SPTarkov.Server.Core.Models.Eft.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; 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; namespace SPTarkov.Server.Core.Helpers; [Injectable] public class BotHelper( ISptLogger logger, DatabaseService databaseService, RandomUtil randomUtil, ConfigServer configServer ) { private static readonly FrozenSet _pmcTypeIds = [ Sides.Usec.ToLowerInvariant(), Sides.Bear.ToLowerInvariant(), Sides.PmcBear.ToLowerInvariant(), Sides.PmcUsec.ToLowerInvariant(), ]; private readonly BotConfig _botConfig = configServer.GetConfig(); private readonly PmcConfig _pmcConfig = configServer.GetConfig(); private readonly ConcurrentDictionary> _pmcNameCache = new(); /// /// Get a template object for the specified botRole from bots.types db /// /// botRole to get template for /// BotType object public BotType? GetBotTemplate(string role) { if (!databaseService.GetBots().Types.TryGetValue(role.ToLowerInvariant(), out var bot)) { logger.Error($"Unable to get bot of type: {role} from DB"); return null; } return bot; } /// /// Is the passed in bot role a PMC (USEC/Bear/PMC) /// /// bot role to check /// true if is pmc public bool IsBotPmc(string? botRole) { return _pmcTypeIds.Contains(botRole?.ToLowerInvariant()); } public bool IsBotBoss(string botRole) { return !IsBotFollower(botRole) && _botConfig.Bosses.Any(x => string.Equals(x, botRole, StringComparison.CurrentCultureIgnoreCase) ); } public bool IsBotFollower(string botRole) { return botRole.StartsWith("follower", StringComparison.CurrentCultureIgnoreCase); } public bool IsBotZombie(string botRole) { return botRole.StartsWith("infected", StringComparison.CurrentCultureIgnoreCase); } /// /// Add a bot to the FRIENDLY_BOT_TYPES list /// /// bot settings to alter /// bot type to add to friendly list public void AddBotToFriendlyList(DifficultyCategories difficultySettings, string typeToAdd) { const string friendlyBotTypesKey = "FRIENDLY_BOT_TYPES"; // Null guard if (!difficultySettings.Mind.ContainsKey(friendlyBotTypesKey)) { difficultySettings.Mind[friendlyBotTypesKey] = new List(); } ((List)difficultySettings.Mind[friendlyBotTypesKey]).Add(typeToAdd); } /// /// Add a bot to the REVENGE_BOT_TYPES list /// /// bot settings to alter /// bot type to add to revenge list public void AddBotToRevengeList(DifficultyCategories difficultySettings, string[] typesToAdd) { const string revengePropKey = "REVENGE_BOT_TYPES"; // Nothing to add if (typesToAdd.Length == 0) { return; } // Null guard if (!difficultySettings.Mind.ContainsKey(revengePropKey)) { difficultySettings.Mind[revengePropKey] = new List(); } var revengeArray = (List)difficultySettings.Mind[revengePropKey]; foreach (var botTypeToAdd in typesToAdd) { if (!revengeArray.Contains(botTypeToAdd)) { revengeArray.Add(botTypeToAdd); } } } /// /// Get randomization settings for bot from config/bot.json /// /// level of bot /// bot equipment json /// RandomisationDetails public RandomisationDetails? GetBotRandomizationDetails( int botLevel, EquipmentFilters botEquipConfig ) { // No randomisation details found, skip return botEquipConfig.Randomisation?.FirstOrDefault(randDetails => botLevel >= randDetails.LevelRange.Min && botLevel <= randDetails.LevelRange.Max ); } /// /// Get the corresponding side when pmcBEAR or pmcUSEC is passed in /// /// role to get side for /// side (usec/bear) public string GetPmcSideByRole(string botRole) { if (string.Equals(_pmcConfig.BearType, botRole, StringComparison.OrdinalIgnoreCase)) { return Sides.Bear; } if (string.Equals(_pmcConfig.UsecType, botRole, StringComparison.OrdinalIgnoreCase)) { return Sides.Usec; } return GetRandomizedPmcSide(); } /// /// Get the corresponding side when pmcBEAR or pmcUSEC is passed in /// /// role to get side for /// side (usec/bear) public string GetPmcSideByRole(WildSpawnType botRole) { switch (botRole) { case WildSpawnType.pmcBEAR: return Sides.Bear; case WildSpawnType.pmcUSEC: return Sides.Usec; default: return GetRandomizedPmcSide(); } } /// /// Get a randomized PMC side based on bot config value 'isUsec' /// /// pmc side as string protected string GetRandomizedPmcSide() { return randomUtil.GetChance100(_pmcConfig.IsUsec) ? Sides.Usec : Sides.Bear; } /// /// Get a name from a PMC that fits the desired length /// /// Max length of name, inclusive /// OPTIONAL - what side PMC to get name from (usec/bear) /// name of PMC public string GetPmcNicknameOfMaxLength(int maxLength, string? side = null) { var chosenFaction = ( side ?? (randomUtil.GetInt(0, 1) == 0 ? Sides.Usec : Sides.Bear) ).ToLowerInvariant(); var cacheKey = $"{chosenFaction}{maxLength}"; if (!_pmcNameCache.TryGetValue(cacheKey, out var eligibleNames)) { if ( !databaseService .GetBots() .Types.TryGetValue(chosenFaction, out var chosenFactionDetails) ) { logger.Error($"Unknown faction: {chosenFaction} Defaulting to: {Sides.Usec}"); chosenFaction = Sides.Usec.ToLowerInvariant(); chosenFactionDetails = databaseService.GetBots().Types[chosenFaction]; } var matchingNames = chosenFactionDetails .FirstNames.Where(name => name.Length <= maxLength) .ToList(); if (!matchingNames.Any()) { logger.Warning( $"Unable to filter: {chosenFaction} PMC names to only those under: {maxLength}, none found that match that criteria, selecting from entire name pool instead" ); // Return a random string from names return randomUtil.GetRandomElement(chosenFactionDetails.FirstNames); } _pmcNameCache.TryAdd(cacheKey, matchingNames); eligibleNames = matchingNames; } return randomUtil.GetRandomElement(eligibleNames); } }