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