From e14f216209fd459356a5330abfd8187567b4616f Mon Sep 17 00:00:00 2001 From: Chris Adamson Date: Tue, 20 May 2025 17:02:55 -0500 Subject: [PATCH] skip btr driver and lightkeeper quest rewards (#262) * skip btr driver and lightkeeper quest rewards * remove new line * fixed circular dep * fixes based on feedback * more feedback fixes --- .../Helpers/QuestRewardHelper.cs | 3 +- .../Helpers/RewardHelper.cs | 262 +++++++++++------- 2 files changed, 160 insertions(+), 105 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Helpers/QuestRewardHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/QuestRewardHelper.cs index a4435f34..6ae48945 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/QuestRewardHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/QuestRewardHelper.cs @@ -76,7 +76,8 @@ public class QuestRewardHelper( fullProfile, profileData, questId, - questResponse + questResponse, + questDetails ); } diff --git a/Libraries/SPTarkov.Server.Core/Helpers/RewardHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/RewardHelper.cs index b4681db1..e298e46a 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/RewardHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/RewardHelper.cs @@ -28,22 +28,25 @@ public class RewardHelper( PlayerService _playerService ) { - /** - * Apply the given rewards to the passed in profile - * @param rewards List of rewards to apply - * @param source The source of the rewards (Achievement, quest) - * @param fullProfile The full profile to apply the rewards to - * @param questId The quest or achievement ID, used for finding production unlocks - * @param questResponse Response to quest completion when a production is unlocked - * @returns List of items that were rewarded - */ + /// + /// Apply the given rewards to the passed in profile. + /// + /// List of rewards to apply. + /// The source of the rewards (Achievement, quest). + /// The full profile to apply the rewards to. + /// The profile data (could be the scav profile). + /// The quest or achievement ID, used for finding production unlocks. + /// Response to quest completion when a production is unlocked. + /// The quest that the reward is for. + /// List of items that is the reward. public List ApplyRewards( List rewards, string source, SptProfile fullProfile, PmcData profileData, - string questId, - ItemEventRouterResponse questResponse = null + string rewardSourceId, + ItemEventRouterResponse? questResponse = null, + Quest? quest = null ) { var sessionId = fullProfile?.ProfileInfo?.ProfileId; @@ -56,6 +59,15 @@ public class RewardHelper( var gameVersion = pmcProfile.Info.GameVersion; + var isInGameRewardTrader = quest != null && IsInGameRewardTrader(quest); + if (isInGameRewardTrader) + { + _logger.Debug( + $"Skipping quest rewards for quest {quest.Id} as it is in the InGameRewardrTader list" + ); + return []; + } + foreach (var reward in rewards) { // Handle reward availability for different game versions, notAvailableInGameEditions currently not used @@ -102,13 +114,16 @@ public class RewardHelper( AddAchievementToProfile(fullProfile, reward.Target); break; case RewardType.StashRows: - _profileHelper.AddStashRowsBonusToProfile( - sessionId, - (int) reward.Value - ); // Add specified stash rows from reward - requires client restart + _profileHelper.AddStashRowsBonusToProfile(sessionId, (int)reward.Value); // Add specified stash rows from reward - requires client restart break; case RewardType.ProductionScheme: - FindAndAddHideoutProductionIdToProfile(pmcProfile, reward, questId, sessionId, questResponse); + FindAndAddHideoutProductionIdToProfile( + pmcProfile, + reward, + rewardSourceId, + sessionId, + questResponse + ); break; case RewardType.Pockets: _profileHelper.ReplaceProfilePocketTpl(pmcProfile, reward.Target); @@ -120,37 +135,61 @@ public class RewardHelper( _logger.Error( _localisationService.GetText( "reward-type_not_handled", - new - { - rewardType = reward.Type, - questId - } + new { rewardType = reward.Type, rewardSourceId } ) ); break; } } - return GetRewardItems(rewards, gameVersion); + return GetRewardItems(rewards, gameVersion, quest); } - /** - * Does the provided reward have a game version requirement to be given and does it match - * @param reward Reward to check - * @param gameVersion Version of game to check reward against - * @returns True if it has requirement, false if it doesnt pass check - */ + /// + /// Value for in game reward traders to not duplicate quest rewards. + /// Value can be modified by modders by overriding this value with new traders. + /// Ensure to add Lightkeeper's ID (638f541a29ffd1183d187f57) and BTR Driver's ID (656f0f98d80a697f855d34b1) + /// + protected string[] noRewardTraders = + [ + // LightKeeper + "638f541a29ffd1183d187f57", + // BTR Driver + "656f0f98d80a697f855d34b1", + ]; + + /// + /// Determines if quest rewards are given in raid by the trader instead of through messaging system. + /// + /// The quest to check. + /// True if the quest's trader is in the in-game reward trader list; otherwise, false. + protected bool IsInGameRewardTrader(Quest quest) + { + return noRewardTraders.Contains(quest.TraderId); + } + + /// + /// Does the provided reward have a game version requirement to be given and does it match. + /// + /// Reward to check. + /// Version of game to check reward against. + /// True if it has requirement, false if it doesn't pass check. public bool RewardIsForGameEdition(Reward reward, string gameVersion) { - if (reward.AvailableInGameEditions?.Count > 0 && !reward.AvailableInGameEditions.Contains(gameVersion)) - // Reward has edition whitelist and game version isn't in it + if ( + reward.AvailableInGameEditions?.Count > 0 + && !reward.AvailableInGameEditions.Contains(gameVersion) + ) + // Reward has edition whitelist and game version isn't in it { return false; } - if (reward.NotAvailableInGameEditions?.Count > 0 && - reward.NotAvailableInGameEditions.Contains(gameVersion)) - // Reward has edition blacklist and game version is in it + if ( + reward.NotAvailableInGameEditions?.Count > 0 + && reward.NotAvailableInGameEditions.Contains(gameVersion) + ) + // Reward has edition blacklist and game version is in it { return false; } @@ -159,21 +198,22 @@ public class RewardHelper( return true; } - /** - * WIP - Find hideout craft id and add to unlockedProductionRecipe array in player profile - * also update client response recipeUnlocked array with craft id - * @param pmcData Player profile - * @param craftUnlockReward Reward with craft unlock details - * @param questId Quest or achievement ID with craft unlock reward - * @param sessionID Session id - * @param response Response to send back to client - */ + /// + /// WIP - Find hideout craft id and add to unlockedProductionRecipe array in player profile + /// also update client response recipeUnlocked array with craft id + /// + /// Player profile. + /// Reward with craft unlock details. + /// Quest or achievement ID with craft unlock reward. + /// Session id. + /// Response to send back to client. protected void FindAndAddHideoutProductionIdToProfile( PmcData pmcData, Reward craftUnlockReward, string questId, string sessionID, - ItemEventRouterResponse response) + ItemEventRouterResponse response + ) { var matchingProductions = GetRewardProductionMatch(craftUnlockReward, questId); if (matchingProductions.Count != 1) @@ -181,11 +221,7 @@ public class RewardHelper( _logger.Error( _localisationService.GetText( "reward-unable_to_find_matching_hideout_production", - new - { - questId, - matchCount = matchingProductions.Count - } + new { questId, matchCount = matchingProductions.Count } ) ); @@ -202,50 +238,60 @@ public class RewardHelper( } } - /** - * Find hideout craft for the specified reward - * @param craftUnlockReward Reward with craft unlock details - * @param questId Quest or achievement ID with craft unlock reward - * @returns Hideout craft - */ - public List GetRewardProductionMatch(Reward craftUnlockReward, string questId) + /// + /// Find hideout craft for the specified reward. + /// + /// Reward with craft unlock details. + /// Quest or achievement ID with craft unlock reward. + /// List of matching HideoutProduction objects. + public List GetRewardProductionMatch( + Reward craftUnlockReward, + string questId + ) { // Get hideout crafts and find those that match by areatype/required level/end product tpl - hope for just one match var craftingRecipes = _databaseService.GetHideout().Production.Recipes; // Area that will be used to craft unlocked item - var desiredHideoutAreaType = (HideoutAreas) int.Parse(craftUnlockReward.TraderId.ToString()); + var desiredHideoutAreaType = (HideoutAreas)int.Parse(craftUnlockReward.TraderId.ToString()); - var matchingProductions = craftingRecipes.Where(prod => - prod.AreaType == desiredHideoutAreaType && + var matchingProductions = craftingRecipes + .Where(prod => + prod.AreaType == desiredHideoutAreaType + && //prod.requirements.some((requirement) => requirement.questId == questId) && // BSG don't store the quest id in requirement any more! - prod.Requirements.Any(requirement => requirement.Type == "QuestComplete") && - prod.Requirements.Any(requirement => requirement.RequiredLevel == craftUnlockReward.LoyaltyLevel - ) && - prod.EndProduct == craftUnlockReward.Items.FirstOrDefault().Template + prod.Requirements.Any(requirement => requirement.Type == "QuestComplete") + && prod.Requirements.Any(requirement => + requirement.RequiredLevel == craftUnlockReward.LoyaltyLevel + ) + && prod.EndProduct == craftUnlockReward.Items.FirstOrDefault().Template ) .ToList(); // More/less than single match, above filtering wasn't strict enough if (matchingProductions.Count != 1) - // Multiple matches were found, last ditch attempt to match by questid (value we add manually to production.json via `gen:productionquests` command) + // Multiple matches were found, last ditch attempt to match by questid (value we add manually to production.json via `gen:productionquests` command) { - matchingProductions = matchingProductions.Where(prod => - prod.Requirements.Any(requirement => requirement.QuestId == questId) - ) + matchingProductions = matchingProductions + .Where(prod => prod.Requirements.Any(requirement => requirement.QuestId == questId)) .ToList(); } return matchingProductions; } - /** - * Gets a flat list of reward items from the given rewards for the specified game version - * @param rewards Array of rewards to get the items from - * @param gameVersion The game version of the profile - * @returns array of items with the correct maxStack - */ - protected List GetRewardItems(List rewards, string gameVersion) + /// + /// Gets a flat list of reward items from the given rewards for the specified game version. + /// + /// Array of rewards to get the items from. + /// The game version of the profile. + /// The quest (optional). + /// Array of items with the correct maxStack. + protected List GetRewardItems( + List rewards, + string gameVersion, + Quest? quest = null + ) { // Iterate over all rewards with the desired status, flatten out items that have a type of Item var rewardItems = rewards.SelectMany(reward => @@ -257,11 +303,11 @@ public class RewardHelper( return rewardItems.ToList(); } - /** - * Take reward item and set FiR status + fix stack sizes + fix mod Ids - * @param reward Reward item to fix - * @returns Fixed rewards - */ + /// + /// Take reward item and set FiR status, fix stack sizes, and fix mod Ids. + /// + /// Reward item to fix. + /// Fixed rewards. protected List ProcessReward(Reward reward) { /** item with mods to return */ @@ -271,10 +317,10 @@ public class RewardHelper( // Is armor item that may need inserts / plates if (reward.Items.Count == 1 && _itemHelper.ArmorItemCanHoldMods(reward.Items[0].Template)) - // Only process items with slots + // Only process items with slots { if (_itemHelper.ItemHasSlots(reward.Items.FirstOrDefault().Template)) - // Attempt to pull default preset from globals and add child items to reward (clones reward.items) + // Attempt to pull default preset from globals and add child items to reward (clones reward.items) { GenerateArmorRewardChildSlots(reward.Items.FirstOrDefault(), reward); } @@ -292,10 +338,12 @@ public class RewardHelper( { // Is base reward item if ( - rewardItem.ParentId != null && - rewardItem.ParentId == "hideout" && // Has parentId of hideout - rewardItem.Upd != null && - rewardItem.Upd.StackObjectsCount != null && // Has upd with stackobject count + rewardItem.ParentId != null + && rewardItem.ParentId == "hideout" + && // Has parentId of hideout + rewardItem.Upd != null + && rewardItem.Upd.StackObjectsCount != null + && // Has upd with stackobject count rewardItem.Upd.StackObjectsCount > 1 // More than 1 item in stack ) { @@ -314,11 +362,18 @@ public class RewardHelper( { // Is child mod if (reward.Items.FirstOrDefault().Upd.SpawnedInSession.GetValueOrDefault(false)) - // Propagate FiR status into child items + // Propagate FiR status into child items { - if (!_itemHelper.IsOfBaseclasses(rewardItem.Template, [BaseClasses.AMMO, BaseClasses.MONEY])) + if ( + !_itemHelper.IsOfBaseclasses( + rewardItem.Template, + [BaseClasses.AMMO, BaseClasses.MONEY] + ) + ) { - rewardItem.Upd.SpawnedInSession = reward.Items.FirstOrDefault()?.Upd.SpawnedInSession; + rewardItem.Upd.SpawnedInSession = reward + .Items.FirstOrDefault() + ?.Upd.SpawnedInSession; } } @@ -330,10 +385,7 @@ public class RewardHelper( foreach (var target in targets) { // This has all the original id relations since we reset the id to the original after the splitStack - var itemsClone = new List - { - _cloner.Clone(target) - }; + var itemsClone = new List { _cloner.Clone(target) }; // Here we generate a new id for the root item target.Id = _hashUtil.Generate(); @@ -352,11 +404,11 @@ public class RewardHelper( return rewardItems; } - /** - * Add missing mod items to an armor reward - * @param originalRewardRootItem Original armor reward item from IReward.items object - * @param reward Armor reward - */ + /// + /// Add missing mod items to an armor reward. + /// + /// Original armor reward item from IReward.items object. + /// Armor reward. protected void GenerateArmorRewardChildSlots(Item originalRewardRootItem, Reward reward) { // Look for a default preset from globals for armor @@ -390,17 +442,19 @@ public class RewardHelper( reward.Items = _itemHelper.AddChildSlotItems(reward.Items, itemDbData, null, true); } - /** - * Add an achievement to player profile and handle any rewards for the achievement - * Triggered from a quest, or another achievement - * @param fullProfile Profile to add achievement to - * @param achievementId Id of achievement to add - */ + /// + /// Add an achievement to player profile and handle any rewards for the achievement. + /// Triggered from a quest, or another achievement. + /// + /// Profile to add achievement to. + /// Id of achievement to add. public void AddAchievementToProfile(SptProfile fullProfile, string achievementId) { // Add achievement id to profile with timestamp it was unlocked - fullProfile.CharacterData.PmcData.Achievements.TryAdd(achievementId, _timeUtil.GetTimeStamp()); - + fullProfile.CharacterData.PmcData.Achievements.TryAdd( + achievementId, + _timeUtil.GetTimeStamp() + ); // Check for any customisation unlocks var achievementDataDb = _databaseService