using System.Globalization; using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Extensions; using SPTarkov.Server.Core.Helpers; using SPTarkov.Server.Core.Models.Eft.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Eft.Notes; using SPTarkov.Server.Core.Models.Enums; 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; using SPTarkov.Server.Core.Utils.Cloners; using SPTarkov.Server.Core.Utils.Json; using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel; namespace SPTarkov.Server.Core.Generators; [Injectable] public class PlayerScavGenerator( ISptLogger _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 ) { protected PlayerScavConfig _playerScavConfig = _configServer.GetConfig(); /// /// 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 = pmcDataClone.GetScavKarmaLevel(); // use karma level to get correct karmaSettings if ( !_playerScavConfig.KarmaLevel.TryGetValue( scavKarmaLevel.ToString(CultureInfo.InvariantCulture), out var playerScavKarmaSettings ) ) { _logger.Error( _localisationService.GetText("scav-missing_karma_settings", scavKarmaLevel) ); } if (_logger.IsLogEnabled(LogLevel.Debug)) { _logger.Debug($"Generated player scav loadout with karma level {scavKarmaLevel}"); } // Edit baseBotNode values var baseBotNode = ConstructBotBaseTemplate(playerScavKarmaSettings.BotTypeForLoot); AdjustBotTemplateWithKarmaSpecificSettings(playerScavKarmaSettings, baseBotNode); var scavData = _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 BotInfoSettings(); scavData.Info.Bans = []; scavData.Info.RegistrationDate = pmcDataClone.Info.RegistrationDate; scavData.Info.GameVersion = pmcDataClone.Info.GameVersion; scavData.Info.MemberCategory = MemberCategory.UniqueId; scavData.Info.LockedMoveCommands = true; scavData.Info.MainProfileNickname = pmcDataClone.Info.Nickname; 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 Dictionary(); scavData.Notes = existingScavDataClone.Notes ?? new Notes { DataNotes = new List() }; scavData.WishList = existingScavDataClone.WishList ?? new DictionaryOrList(new Dictionary(), new List()); scavData.Encyclopedia = pmcDataClone.Encyclopedia ?? new Dictionary(); // Add additional items to player scav as loot AddAdditionalLootToPlayerScavContainers( playerScavKarmaSettings.LootItemsToAddChancePercent, scavData, [EquipmentSlots.TacticalVest, EquipmentSlots.Pockets, EquipmentSlots.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, HashSet 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() { 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) { if (_logger.IsLogEnabled(LogLevel.Debug)) { _logger.Debug($"Unable to add keycard to bot. Reason: {result.ToString()}"); } } } } /// /// 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) { const string 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 karma level settings protected void AdjustBotTemplateWithKarmaSpecificSettings( KarmaLevel karmaSettings, BotType baseBotNode ) { // Adjust equipment chance values AdjustEquipmentWeights( karmaSettings.Modifiers.Equipment, baseBotNode.BotChances.EquipmentChances ); // Adjust mod chance values AdjustWeaponModWeights( karmaSettings.Modifiers.Mod, baseBotNode.BotChances.WeaponModsChances ); // Adjust item spawn quantity values AdjustItemWeights(karmaSettings.ItemLimits, baseBotNode.BotGeneration.Items); // Blacklist equipment, keyed by equipment slot BlacklistEquipment(karmaSettings, baseBotNode); } protected static void AdjustEquipmentWeights( Dictionary equipmentChangesToApply, Dictionary botEquipmentChances ) { foreach (var (equipmentSlot, chanceToAdd) in equipmentChangesToApply) { // Adjustment value zero, nothing to do if (chanceToAdd == 0) { continue; } // Try and add new key with value if (!botEquipmentChances.TryAdd(equipmentSlot, chanceToAdd)) { // Unable to add new, update existing botEquipmentChances[equipmentSlot] += chanceToAdd; } } } /// /// Get a bots item type weightings based on the desired key /// /// e.g. "healing" / "looseLoot" /// /// GenerationData protected GenerationData? GetKarmaLimitValuesByKey( string key, GenerationWeightingItems botItemWeights ) { switch (key) { case "healing": return botItemWeights.Healing; case "drugs": return botItemWeights.Drugs; case "stims": return botItemWeights.Stims; case "looseLoot": return botItemWeights.LooseLoot; case "magazines": return botItemWeights.Magazines; case "grenades": return botItemWeights.Grenades; case "backpackLoot": return botItemWeights.BackpackLoot; case "drink": return botItemWeights.Drink; case "currency": return botItemWeights.Currency; case "pocketLoot": return botItemWeights.PocketLoot; case "vestLoot": return botItemWeights.VestLoot; case "specialItems": return botItemWeights.SpecialItems; default: _logger.Error($"Subtype: {key} not found"); return null; } } protected static void AdjustWeaponModWeights( Dictionary modChangesToApply, Dictionary weaponModChances ) { foreach (var (modSlot, weight) in modChangesToApply) { // Adjustment value zero, nothing to do if (weight == 0) { continue; } if (modChangesToApply.TryGetValue(modSlot, out var value)) { weaponModChances.TryAdd(modSlot, 0); weaponModChances[modSlot] += value; } } } protected void AdjustItemWeights( Dictionary karmaSettingsItemLimits, GenerationWeightingItems? botGenerationItems ) { foreach (var (subType, limitData) in karmaSettingsItemLimits) { var playerValues = GetKarmaLimitValuesByKey(subType, botGenerationItems); if (playerValues is null) { continue; } if (limitData.Weights is not null) { playerValues.Weights = limitData.Weights; } if (limitData.Whitelist is not null) { playerValues.Whitelist = limitData.Whitelist; } } } protected static void BlacklistEquipment(KarmaLevel karmaSettings, BotType baseBotNode) { foreach (var (slot, blacklist) in karmaSettings.EquipmentBlacklist) { if (!baseBotNode.BotInventory.Equipment.TryGetValue(slot, out var equipmentDict)) { continue; } foreach (var itemToRemove in blacklist) { equipmentDict.Remove(itemToRemove); } } } protected Skills GetScavSkills(PmcData scavProfile) { if (scavProfile?.Skills != null) { return scavProfile.Skills; } return GetDefaultScavSkills(); } protected Skills GetDefaultScavSkills() { return new Skills { Common = [], Mastering = [], 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 int 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 /// PmcData protected PmcData SetScavCooldownTimer(PmcData scavData, PmcData pmcData) { // Get sum of all scav cooldown reduction timer bonuses var modifier = 1d + pmcData .Bonuses.Where(x => x.Type == BonusType.ScavCooldownTimer) .Sum(bonus => (bonus?.Value ?? 1) / 100); var fenceInfo = _fenceService.GetFenceInfo(pmcData); modifier *= fenceInfo.SavageCooldownModifier ?? 1d; // Make sure to apply ScavCooldownTimer bonus from Hideout if the player has it. var scavLockDuration = _databaseService.GetGlobals().Configuration.SavagePlayCooldown * modifier; var fullProfile = _profileHelper.GetFullProfile(pmcData?.SessionId); if ( fullProfile?.ProfileInfo?.Edition?.StartsWith( AccountTypes.SPT_DEVELOPER, StringComparison.OrdinalIgnoreCase ) ?? false ) { // Force lock duration to 10seconds for dev profiles scavLockDuration = 10; } if (scavData?.Info != null) { scavData.Info.SavageLockTime = Math.Round( _timeUtil.GetTimeStamp() + (scavLockDuration ?? 0) ); } return scavData; } }