using Core.Annotations; using Core.Helpers; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; using Core.Models.Enums; using Core.Models.Spt.Config; using Core.Servers; using Core.Services; using Core.Utils; using Core.Utils.Cloners; using ILogger = Core.Models.Utils.ILogger; namespace Core.Generators; [Injectable] public class PlayerScavGenerator { private readonly ILogger _logger; private readonly RandomUtil _randomUtil; private readonly DatabaseService _databaseService; private readonly HashUtil _hashUtil; private readonly ItemHelper _itemHelper; private readonly BotGeneratorHelper _botGeneratorHelper; private readonly SaveServer _saveServer; private readonly ProfileHelper _profileHelper; private readonly BotHelper _botHelper; private readonly FenceService _fenceService; private readonly BotLootCacheService _botLootCacheService; private readonly LocalisationService _localisationService; private readonly BotGenerator _botGenerator; private readonly ConfigServer _configServer; private readonly ICloner _cloner; private readonly TimeUtil _timeUtil; private PlayerScavConfig _playerScavConfig; public PlayerScavGenerator ( ILogger logger, RandomUtil randomUtil, DatabaseService databaseService, HashUtil hashUtil, ItemHelper itemHelper, BotGeneratorHelper botGeneratorHelper, SaveServer saveServer, ProfileHelper profileHelper, BotHelper botHelper, FenceService fenceService, BotLootCacheService botLootCacheService, LocalisationService localisationService, BotGenerator botGenerator, ConfigServer configServer, ICloner cloner, TimeUtil timeUtil ) { _logger = logger; _randomUtil = randomUtil; _databaseService = databaseService; _hashUtil = hashUtil; _itemHelper = itemHelper; _botGeneratorHelper = botGeneratorHelper; _saveServer = saveServer; _profileHelper = profileHelper; _botHelper = botHelper; _fenceService = fenceService; _botLootCacheService = botLootCacheService; _localisationService = localisationService; _botGenerator = botGenerator; _configServer = configServer; _cloner = cloner; _timeUtil = timeUtil; _playerScavConfig = configServer.GetConfig(ConfigTypes.PLAYERSCAV); } /// /// Update a player profile to include a new player scav profile /// /// session id to specify what profile is updated /// profile object public PmcData Generate(string sessionID) { // get karma level from profile var profile = _saveServer.GetProfile(sessionID); var profileCharactersClone = _cloner.Clone(profile.CharacterData); var pmcDataClone = _cloner.Clone(profileCharactersClone.PmcData); var existingScavDataClone = _cloner.Clone(profileCharactersClone.ScavData); var scavKarmaLevel = GetScavKarmaLevel(pmcDataClone); // use karma level to get correct karmaSettings var playerScavKarmaSettings = _playerScavConfig.KarmaLevel[scavKarmaLevel.ToString()]; if (playerScavKarmaSettings == null) _logger.Error(_localisationService.GetText("scav-missing_karma_settings", scavKarmaLevel)); _logger.Debug($"generated player scav loadout with karma level {scavKarmaLevel}"); // Edit baseBotNode values var baseBotNode = ConstructBotBaseTemplate(playerScavKarmaSettings.BotTypeForLoot); AdjustBotTemplateWithKarmaSpecificSettings(playerScavKarmaSettings, baseBotNode); var scavData = (PmcData)_botGenerator.GeneratePlayerScav( sessionID, playerScavKarmaSettings.BotTypeForLoot.ToLower(), "easy", baseBotNode, pmcDataClone); // Remove cached bot data after scav was generated _botLootCacheService.ClearCache(); // Add scav metadata scavData.Savage = null; scavData.Aid = pmcDataClone.Aid; scavData.TradersInfo = pmcDataClone.TradersInfo; scavData.Info.Settings = new(); scavData.Info.Bans = []; scavData.Info.RegistrationDate = pmcDataClone.Info.RegistrationDate; scavData.Info.GameVersion = pmcDataClone.Info.GameVersion; scavData.Info.MemberCategory = MemberCategory.UNIQUE_ID; scavData.Info.LockedMoveCommands = true; scavData.RagfairInfo = pmcDataClone.RagfairInfo; scavData.UnlockedInfo = pmcDataClone.UnlockedInfo; // Persist previous scav data into new scav scavData.Id = existingScavDataClone.Id ?? pmcDataClone.Savage; scavData.SessionId = existingScavDataClone.SessionId ?? pmcDataClone.SessionId; scavData.Skills = GetScavSkills(existingScavDataClone); scavData.Stats = GetScavStats(existingScavDataClone); scavData.Info.Level = GetScavLevel(existingScavDataClone); scavData.Info.Experience = GetScavExperience(existingScavDataClone); scavData.Quests = existingScavDataClone.Quests ?? []; scavData.TaskConditionCounters = existingScavDataClone.TaskConditionCounters ?? new(); scavData.Notes = existingScavDataClone.Notes ?? new() { DataNotes = new() }; scavData.WishList = existingScavDataClone.WishList ?? new(); scavData.Encyclopedia = pmcDataClone.Encyclopedia ?? new(); // Add additional items to player scav as loot AddAdditionalLootToPlayerScavContainers(playerScavKarmaSettings.LootItemsToAddChancePercent, scavData, [ "TacticalVest", "Pockets", "Backpack" ]); // Remove secure container scavData = _profileHelper.RemoveSecureContainer(scavData); // set cooldown timer scavData = SetScavCooldownTimer(scavData, pmcDataClone); // add scav to profile _saveServer.GetProfile(sessionID).CharacterData.ScavData = scavData; return scavData; } /// /// Add items picked from `playerscav.lootItemsToAddChancePercent` /// /// dict of tpl + % chance to be added /// /// Possible slotIds to add loot to protected void AddAdditionalLootToPlayerScavContainers(Dictionary possibleItemsToAdd, BotBase scavData, List containersToAddTo) { foreach (var tpl in possibleItemsToAdd) { var shouldAdd = _randomUtil.GetChance100(tpl.Value); if (!shouldAdd) continue; var itemResult = _itemHelper.GetItem(tpl.Key); if (!itemResult.Key) { _logger.Warning(_localisationService.GetText("scav-unable_to_add_item_to_player_scav", tpl)); continue; } var itemTemplate = itemResult.Value; var itemsToAdd = new List() { new Item() { Id = _hashUtil.Generate(), Template = itemTemplate.Id, Upd = _botGeneratorHelper.GenerateExtraPropertiesForItem(itemTemplate) } }; var result = _botGeneratorHelper.AddItemWithChildrenToEquipmentSlot( containersToAddTo, itemsToAdd[0].Id, itemTemplate.Id, itemsToAdd, scavData.Inventory); if (result != ItemAddedResult.SUCCESS) _logger.Debug($"Unable to add keycard to bot. Reason: {result.ToString()}"); } } /// /// Get the scav karama level for a profile /// Is also the fence trader rep level /// /// pmc profile /// karma level protected double GetScavKarmaLevel(PmcData pmcData) { // can be empty during profile creation if (!pmcData.TradersInfo.TryGetValue(Traders.FENCE, out var fenceInfo)) { _logger.Warning(_localisationService.GetText("scav-missing_karma_level_getting_default")); return 0; } if (fenceInfo.Standing > 6) return 6; return Math.Floor(fenceInfo.Standing ?? 0); } /// /// Get a baseBot template /// If the parameter doesnt match "assault", take parts from the loot type and apply to the return bot template /// /// bot type to use for inventory/chances /// IBotType object protected BotType ConstructBotBaseTemplate(string botTypeForLoot) { var baseScavType = "assault"; var asssaultBase = _cloner.Clone(_botHelper.GetBotTemplate(baseScavType)); // Loot bot is same as base bot, return base with no modification if (botTypeForLoot == baseScavType) return asssaultBase; var lootBase = _cloner.Clone(_botHelper.GetBotTemplate(botTypeForLoot)); asssaultBase.BotInventory = lootBase.BotInventory; asssaultBase.BotChances = lootBase.BotChances; asssaultBase.BotGeneration = lootBase.BotGeneration; return asssaultBase; } /// /// Adjust equipment/mod/item generation values based on scav karma levels /// /// Values to modify the bot template with /// bot template to modify according to karama level settings protected void AdjustBotTemplateWithKarmaSpecificSettings(KarmaLevel karmaSettings, BotType baseBotNode) { // Adjust equipment chance values foreach (var equipmentKvP in karmaSettings.Modifiers.Equipment) { // Adjustment value zero, nothing to do if (equipmentKvP.Value == 0) { continue; } // Try add new key with value if (!baseBotNode.BotChances.EquipmentChances.TryAdd(equipmentKvP.Key, equipmentKvP.Value)) { // Unable to add new, update existing baseBotNode.BotChances.EquipmentChances[equipmentKvP.Key] += equipmentKvP.Value; } } // Adjust mod chance values foreach (var modKvP in karmaSettings.Modifiers.Mod) { // Adjustment value zero, nothing to do if (modKvP.Value == 0) continue; baseBotNode.BotChances.WeaponModsChances[modKvP.Key] += karmaSettings.Modifiers.Mod[modKvP.Key]; } // Adjust item spawn quantity values foreach (var itemLimitKvP in karmaSettings.ItemLimits) { baseBotNode.BotGeneration.Items[itemLimitKvP.Key] = itemLimitKvP.Value; } // Blacklist equipment, keyed by equipment slot foreach (var equipmentBlacklistKvP in karmaSettings.EquipmentBlacklist) { baseBotNode.BotInventory.Equipment.TryGetValue(equipmentBlacklistKvP.Key, out var equipmentDict); foreach (var itemToRemove in equipmentBlacklistKvP.Value) { equipmentDict.Remove(itemToRemove); } } } protected Skills GetScavSkills(PmcData scavProfile) { if (scavProfile?.Skills != null) return scavProfile.Skills; return GetDefaultScavSkills(); } protected Skills GetDefaultScavSkills() { return new() { Common = new(new(), new()), Mastering = new(), Points = 0 }; } protected Stats GetScavStats(PmcData scavProfile) { if (scavProfile?.Stats != null) return scavProfile.Stats; return _profileHelper.GetDefaultCounters(); } protected int GetScavLevel(PmcData scavProfile) { // Info can be null on initial account creation if (scavProfile?.Info?.Level == null) return 1; return scavProfile?.Info?.Level ?? 1; } protected double GetScavExperience(PmcData scavProfile) { // Info can be null on initial account creation if (scavProfile?.Info?.Experience == null) return 0; return scavProfile?.Info?.Experience ?? 0; } /// /// Set cooldown till scav is playable /// take into account scav cooldown bonus /// /// scav profile /// pmc profile /// protected PmcData SetScavCooldownTimer(PmcData scavData, PmcData pmcData) { // Set cooldown time. // Make sure to apply ScavCooldownTimer bonus from Hideout if the player has it. var scavLockDuration = _databaseService.GetGlobals().Configuration.SavagePlayCooldown; var modifier = 1D; foreach (var bonus in pmcData.Bonuses) { if (bonus.Type == BonusType.ScavCooldownTimer) { // Value is negative, so add. // Also note that for scav cooldown, multiple bonuses stack additively. modifier += (bonus?.Value ?? 1) / 100; } } var fenceInfo = _fenceService.GetFenceInfo(pmcData); modifier *= fenceInfo.SavageCooldownModifier ?? 1; scavLockDuration *= modifier; var fullProfile = _profileHelper.GetFullProfile(pmcData?.SessionId); if (fullProfile?.ProfileInfo?.Edition.ToLower().StartsWith(AccountTypes.SPT_DEVELOPER) ?? false) scavLockDuration = 10; if (scavData?.Info != null) scavData.Info.SavageLockTime = _timeUtil.GetTimeStamp() / 1000 + (long)scavLockDuration; return scavData; } }