using Core.Annotations; using Core.Helpers; using Core.Models.Common; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; using Core.Models.Enums; using Core.Models.Enums.RaidSettings; using Core.Models.Spt.Bots; using Core.Models.Spt.Config; using Core.Servers; using Core.Services; using Core.Utils; using Core.Utils.Cloners; using BodyPart = Core.Models.Eft.Common.Tables.BodyPart; using ILogger = Core.Models.Utils.ILogger; namespace Core.Generators; [Injectable] public class BotGenerator { private readonly ILogger _logger; private readonly HashUtil _hashUtil; private readonly RandomUtil _randomUtil; private readonly TimeUtil _timeUtil; private readonly ProfileHelper _profileHelper; private readonly DatabaseService _databaseService; private readonly BotInventoryGenerator _botInventoryGenerator; private readonly BotLevelGenerator _botLevelGenerator; private readonly BotEquipmentFilterService _botEquipmentFilterService; private readonly WeightedRandomHelper _weightedRandomHelper; private readonly BotHelper _botHelper; private readonly BotGeneratorHelper _botGeneratorHelper; private readonly SeasonalEventService _seasonalEventService; private readonly ItemFilterService _itemFilterService; private readonly BotNameService _botNameService; private readonly ConfigServer _configServer; private readonly ICloner _cloner; private BotConfig _botConfig; private PmcConfig _pmcConfig; public BotGenerator( ILogger logger, HashUtil hashUtil, RandomUtil randomUtil, TimeUtil timeUtil, ProfileHelper profileHelper, DatabaseService databaseService, BotInventoryGenerator botInventoryGenerator, BotLevelGenerator botLevelGenerator, BotEquipmentFilterService botEquipmentFilterService, WeightedRandomHelper weightedRandomHelper, BotHelper botHelper, BotGeneratorHelper botGeneratorHelper, SeasonalEventService seasonalEventService, ItemFilterService itemFilterService, BotNameService botNameService, ConfigServer configServer, ICloner cloner ) { _logger = logger; _hashUtil = hashUtil; _randomUtil = randomUtil; _timeUtil = timeUtil; _profileHelper = profileHelper; _databaseService = databaseService; _botInventoryGenerator = botInventoryGenerator; _botLevelGenerator = botLevelGenerator; _botEquipmentFilterService = botEquipmentFilterService; _weightedRandomHelper = weightedRandomHelper; _botHelper = botHelper; _botGeneratorHelper = botGeneratorHelper; _seasonalEventService = seasonalEventService; _itemFilterService = itemFilterService; _botNameService = botNameService; _configServer = configServer; _cloner = cloner; _botConfig = _configServer.GetConfig(ConfigTypes.BOT); _pmcConfig = _configServer.GetConfig(ConfigTypes.PMC); } /// /// Generate a player scav bot object /// /// Session id /// e.g. assault / pmcbot /// easy/normal/hard/impossible /// base bot template to use (e.g. assault/pmcbot) /// profile of player generating pscav /// BotBase public PmcData GeneratePlayerScav(string sessionId, string role, string difficulty, BotType botTemplate, PmcData profile) { var bot = GetCloneOfBotBase(); bot.Info.Settings.BotDifficulty = difficulty; bot.Info.Settings.Role = role; bot.Info.Side = "Savage"; var botGenDetails = new BotGenerationDetails{ IsPmc = false, Side = "Savage", Role = role, BotRelativeLevelDeltaMax = 0, BotRelativeLevelDeltaMin = 0, BotCountToGenerate = 1, BotDifficulty = difficulty, IsPlayerScav = true, }; bot = GenerateBot(sessionId, bot, botTemplate, botGenDetails); // Sets the name after scav name shown in parentheses bot.Info.MainProfileNickname = profile.Info.Nickname; return new PmcData { Id = bot.Id, Aid = bot.Aid, SessionId = bot.SessionId, Savage = bot.Savage, KarmaValue = bot.KarmaValue, Info = bot.Info, Customization = bot.Customization, Health = bot.Health, Inventory = bot.Inventory, Skills = bot.Skills, Stats = bot.Stats, Encyclopedia = bot.Encyclopedia, TaskConditionCounters = bot.TaskConditionCounters, InsuredItems = bot.InsuredItems, Hideout = bot.Hideout, Quests = bot.Quests, TradersInfo = bot.TradersInfo, UnlockedInfo = bot.UnlockedInfo, RagfairInfo = bot.RagfairInfo, Achievements = bot.Achievements, RepeatableQuests = bot.RepeatableQuests, Bonuses = bot.Bonuses, Notes = bot.Notes, CarExtractCounts = bot.CarExtractCounts, CoopExtractCounts = bot.CoopExtractCounts, SurvivorClass = bot.SurvivorClass, WishList = bot.WishList, MoneyTransferLimitData = bot.MoneyTransferLimitData, IsPmc = bot.IsPmc, Prestige = new Prestige() }; } /// /// Create 1 bot of the type/side/difficulty defined in botGenerationDetails /// /// Session id /// details on how to generate bots /// constructed bot public BotBase PrepareAndGenerateBot(string sessionId, BotGenerationDetails botGenerationDetails) { throw new NotImplementedException(); } /// /// Get a clone of the default bot base object and adjust its role/side/difficulty values /// /// Role bot should have /// Side bot should have /// Difficult bot should have /// Cloned bot base public BotBase GetPreparedBotBase(string botRole, string botSide, string difficulty) { throw new NotImplementedException(); } /// /// Get a clone of the database\bots\base.json file /// /// BotBase object public BotBase GetCloneOfBotBase() { return _cloner.Clone(_databaseService.GetBots().Base); } /// /// Create a IBotBase object with equipment/loot/exp etc /// /// Session id /// Bots base file /// Bot template from db/bots/x.json /// details on how to generate the bot /// BotBase object public BotBase GenerateBot( string sessionId, BotBase bot, BotType botJsonTemplate, BotGenerationDetails botGenerationDetails) { _logger.Error("NOT IMPLEMENTED BotGenerator.GenerateBot"); var botRoleLowercase = botGenerationDetails.Role.ToLower(); var botLevel = _botLevelGenerator.GenerateBotLevel( botJsonTemplate.BotExperience.Level, botGenerationDetails, bot); // Only filter bot equipment, never players if (!botGenerationDetails.IsPlayerScav.GetValueOrDefault(false)) { _botEquipmentFilterService.FilterBotEquipment( sessionId, botJsonTemplate, botLevel.Level.Value, botGenerationDetails); } bot.Info.Nickname = _botNameService.GenerateUniqueBotNickname( botJsonTemplate, botGenerationDetails, botRoleLowercase, _botConfig.BotRolesThatMustHaveUniqueName); bot.Info.LowerNickname = bot.Info.Nickname.ToLower(); // Only run when generating a 'fake' playerscav, not actual player scav if (!botGenerationDetails.IsPlayerScav.GetValueOrDefault(false) && ShouldSimulatePlayerScav(botRoleLowercase)) { _botNameService.AddRandomPmcNameToBotMainProfileNicknameProperty(bot); SetRandomisedGameVersionAndCategory(bot.Info); } if (!_seasonalEventService.ChristmasEventEnabled()) { // Process all bots EXCEPT gifter, he needs christmas items if (botGenerationDetails.Role != "gifter") { _seasonalEventService.RemoveChristmasItemsFromBotInventory( botJsonTemplate.BotInventory, botGenerationDetails.Role); } } RemoveBlacklistedLootFromBotTemplate(botJsonTemplate.BotInventory); // Remove hideout data if bot is not a PMC or pscav - match what live sends if (!(botGenerationDetails.IsPmc.GetValueOrDefault(false) || botGenerationDetails.IsPlayerScav.GetValueOrDefault(false))) { bot.Hideout = null; } bot.Info.Experience = botLevel.Exp; bot.Info.Level = botLevel.Level; bot.Info.Settings.Experience = GetExperienceRewardForKillByDifficulty( botJsonTemplate.BotExperience.Reward, botGenerationDetails.BotDifficulty, botGenerationDetails.Role); bot.Info.Settings.StandingForKill = GetStandingChangeForKillByDifficulty( botJsonTemplate.BotExperience.StandingForKill, botGenerationDetails.BotDifficulty, botGenerationDetails.Role); bot.Info.Settings.AggressorBonus = GetAgressorBonusByDifficulty( botJsonTemplate.BotExperience.StandingForKill, botGenerationDetails.BotDifficulty, botGenerationDetails.Role); bot.Info.Settings.UseSimpleAnimator = botJsonTemplate.BotExperience.UseSimpleAnimator ?? false; bot.Info.Voice = _weightedRandomHelper.GetWeightedValue(botJsonTemplate.BotAppearance.Voice); bot.Health = GenerateHealth(botJsonTemplate.BotHealth, botGenerationDetails.IsPlayerScav.GetValueOrDefault(false)); bot.Skills = GenerateSkills(botJsonTemplate.BotSkills); // TODO: fix bad type, bot jsons store skills in dict, output needs to be array if (botGenerationDetails.IsPmc.GetValueOrDefault(false)) { bot.Info.IsStreamerModeAvailable = true; // Set to true so client patches can pick it up later - client sometimes alters botrole to assaultGroup SetRandomisedGameVersionAndCategory(bot.Info); if (bot.Info.GameVersion == GameEditions.UNHEARD) { AddAdditionalPocketLootWeightsForUnheardBot(botJsonTemplate); } } // Add drip SetBotAppearance(bot, botJsonTemplate.BotAppearance, botGenerationDetails); // Filter out blacklisted gear from the base template FilterBlacklistedGear(botJsonTemplate, botGenerationDetails); bot.Inventory = _botInventoryGenerator.generateInventory( sessionId, botJsonTemplate, botRoleLowercase, botGenerationDetails.IsPmc.GetValueOrDefault(false), botLevel.Level.Value, bot.Info.GameVersion); if (_botConfig.BotRolesWithDogTags.Contains(botRoleLowercase)) { AddDogtagToBot(bot); } // Generate new bot ID AddIdsToBot(bot); // Generate new inventory ID GenerateInventoryId(bot); // Set role back to originally requested now its been generated if (botGenerationDetails.EventRole is not null) { bot.Info.Settings.Role = botGenerationDetails.EventRole; } return bot; } /// /// Should this bot have a name like "name (Pmc Name)" and be altered by client patch to be hostile to player /// /// Role bot has /// True if name should be simulated pscav public bool ShouldSimulatePlayerScav(string botRole) { return botRole == "assault" && _randomUtil.GetChance100(_botConfig.ChanceAssaultScavHasPlayerScavName); } /// /// Get exp for kill by bot difficulty /// /// Dict of difficulties and experience /// the killed bots difficulty /// Role of bot (optional, used for error logging) /// Experience for kill public double GetExperienceRewardForKillByDifficulty(Dictionary experiences, string botDifficulty, string role) { var result = experiences[botDifficulty.ToLower()]; if (result is null) { _logger.Debug("Unable to find experience for kill value for: ${ role} ${ botDifficulty}, falling back to `normal`"); return _randomUtil.GetDouble(experiences["normal"].Min.Value, experiences["normal"].Max.Value); } return _randomUtil.GetDouble(result.Min.Value, result.Max.Value); } /// /// Get the standing value change when player kills a bot /// /// Dictionary of standing values keyed by bot difficulty /// Difficulty of bot to look up /// Role of bot (optional, used for error logging) /// Standing change value public double GetStandingChangeForKillByDifficulty(Dictionary standingsForKill, string botDifficulty, string role) { if (!standingsForKill.TryGetValue(botDifficulty.ToLower(), out var result)) { _logger.Warning($"Unable to find standing for kill value for: {role} {botDifficulty}, falling back to `normal`"); return standingsForKill["normal"]; } return result; } /// /// Get the agressor bonus value when player kills a bot /// /// Dictionary of standing values keyed by bot difficulty /// Difficulty of bot to look up /// Role of bot (optional, used for error logging) /// Standing change value public double GetAgressorBonusByDifficulty(Dictionary aggressorBonuses, string botDifficulty, string role) { if (!aggressorBonuses.TryGetValue(botDifficulty.ToLower(), out var result)) { _logger.Warning($"Unable to find aggressor bonus for kill value for: {role} {botDifficulty}, falling back to `normal`"); return aggressorBonuses["normal"]; } return result; } /// /// Set weighting of flagged equipment to 0 /// /// Bot data to adjust /// Generation details of bot public void FilterBlacklistedGear(BotType botJsonTemplate, BotGenerationDetails botGenerationDetails) { var blacklist = _botEquipmentFilterService.GetBotEquipmentBlacklist( _botGeneratorHelper.GetBotEquipmentRole(botGenerationDetails.Role), botGenerationDetails.PlayerLevel.Value); if (blacklist?.Gear is null) { // Nothing to filter by return; } foreach (var equipmentKvP in blacklist.Gear) { var equipmentDict = botJsonTemplate.BotInventory.Equipment[equipmentKvP.Key]; foreach (var blacklistedTpl in equipmentKvP.Value) { // Set weighting to 0, will never be picked equipmentDict[blacklistedTpl] = 0; } } } /// /// TODO: Complete Summary /// /// Bot data to adjust public void AddAdditionalPocketLootWeightsForUnheardBot(BotType botJsonTemplate) { // Adjust pocket loot weights to allow for 5 or 6 items var pocketWeights = botJsonTemplate.BotGeneration.Items["pocketLoot"].Weights; pocketWeights["5"] = 1; pocketWeights["6"] = 1; } /// /// Remove items from item.json/lootableItemBlacklist from bots inventory /// /// Bot to filter public void RemoveBlacklistedLootFromBotTemplate(BotTypeInventory botInventory) { throw new NotImplementedException(); } /// /// Choose various appearance settings for a bot using weights: head/body/feet/hands /// /// Bot to adjust /// Appearance settings to choose from /// Generation details public void SetBotAppearance(BotBase bot, Appearance appearance, BotGenerationDetails botGenerationDetails) { throw new NotImplementedException(); } /// /// Log the number of PMCs generated to the debug console /// /// Generated bot array, ready to send to client public void LogPmcGeneratedCount(List output) { throw new NotImplementedException(); } /// /// Converts health object to the required format /// /// health object from bot json /// Is a pscav bot being generated /// Health object public BotBaseHealth GenerateHealth(BotTypeHealth healthObj, bool playerScav = false) { throw new NotImplementedException(); } /// /// Sum up body parts max hp values, return the bodypart collection with lowest value /// /// Body parts to sum up /// Lowest hp collection public BodyPart? GetLowestHpBody(List bodies) // TODO: there are two types of body parts { throw new NotImplementedException(); } /// /// Get a bots skills with randomsied progress value between the min and max values /// /// Skills that should have their progress value randomised /// Skills public Skills GenerateSkills(BotDbSkills botSkills) { throw new NotImplementedException(); } /// /// Randomise the progress value of passed in skills based on the min/max value /// /// Skills to randomise /// Are the skills 'common' skills /// Skills with randomised progress values as an array public List GetSkillsWithRandomisedProgressValue(Dictionary skills, bool isCommonSkills) { throw new NotImplementedException(); } /// /// Generate an id+aid for a bot and apply /// /// bot to update /// updated IBotBase object // TODO: Node server claims this in summary but is void public void AddIdsToBot(BotBase bot) { throw new NotImplementedException(); } /// /// Update a profiles profile.Inventory.equipment value with a freshly generated one. /// Update all inventory items that make use of this value too. /// /// Profile to update public void GenerateInventoryId(BotBase profile) { throw new NotImplementedException(); } /// /// Randomise a bots game version and account category. /// Chooses from all the game versions (standard, eod etc). /// Chooses account type (default, Sherpa, etc). /// /// bot info object to update /// Chosen game version public string SetRandomisedGameVersionAndCategory(Info botInfo) // TODO: there are two types of Info { throw new NotImplementedException(); } /// /// Add a side-specific (usec/bear) dogtag item to a bots inventory /// /// bot to add dogtag to /// Bot with dogtag added // TODO: Node server claims this in summary but is void public void AddDogtagToBot(BotBase bot) { throw new NotImplementedException(); } /// /// Get a dogtag tpl that matches the bots game version and side /// /// Usec/Bear /// edge_of_darkness / standard /// item tpl public string GetDogtagTplByGameVersionAndSide(string side, string gameVersion) { throw new NotImplementedException(); } /// /// Adjust a PMCs pocket tpl to UHD if necessary, otherwise do nothing /// /// Pmc object to adjust public void SetPmcPocketsByGameVersion(BotBase bot) { throw new NotImplementedException(); } }