Updated Reward system
This commit is contained in:
@@ -559,17 +559,6 @@ public class ProfileHelper(
|
||||
return pmcProfile?.Info?.Bans?.Any(b => b.BanType == BanType.RAGFAIR && currentTimestamp < b.DateTime) ?? false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an achievement to player profile
|
||||
/// </summary>
|
||||
/// <param name="pmcProfile">Profile to add achievement to</param>
|
||||
/// <param name="achievementId">Id of achievement to add</param>
|
||||
public void AddAchievementToProfile(SptProfile pmcProfile, string achievementId)
|
||||
{
|
||||
pmcProfile.CharacterData.PmcData.Achievements[achievementId] = _timeUtil.GetTimeStamp();
|
||||
// TODO: finish off implementation
|
||||
}
|
||||
|
||||
protected readonly List<string> gameEditions = ["edge_of_darkness", "unheard_edition"];
|
||||
|
||||
public bool HasAccessToRepeatableFreeRefreshSystem(PmcData pmcProfile)
|
||||
|
||||
@@ -27,6 +27,7 @@ public class QuestHelper(
|
||||
LocaleService _localeService,
|
||||
ProfileHelper _profileHelper,
|
||||
QuestRewardHelper _questRewardHelper,
|
||||
RewardHelper _rewardHelper,
|
||||
LocalisationService _localisationService,
|
||||
SeasonalEventService _seasonalEventService,
|
||||
TraderHelper _traderHelper,
|
||||
|
||||
@@ -26,6 +26,7 @@ public class QuestRewardHelper(
|
||||
QuestConditionHelper _questConditionHelper,
|
||||
ProfileHelper _profileHelper,
|
||||
PresetHelper _presetHelper,
|
||||
RewardHelper _rewardHelper,
|
||||
LocalisationService _localisationService,
|
||||
ICloner _cloner,
|
||||
ConfigServer _configServer
|
||||
@@ -67,104 +68,14 @@ public class QuestRewardHelper(
|
||||
questDetails = ApplyMoneyBoost(questDetails, questMoneyRewardBonusMultiplier, state);
|
||||
|
||||
// e.g. 'Success' or 'AvailableForFinish'
|
||||
var questStateAsString = state.ToString();
|
||||
var gameVersion = pmcProfile.Info.GameVersion;
|
||||
var questRewards = (List<Reward>?)questDetails.Rewards.GetType()
|
||||
.GetProperties()
|
||||
.FirstOrDefault(
|
||||
p =>
|
||||
p.Name == questStateAsString
|
||||
)
|
||||
.GetValue(questDetails.Rewards);
|
||||
foreach (var reward in questRewards)
|
||||
{
|
||||
if (!QuestRewardIsForGameEdition(reward, gameVersion))
|
||||
continue;
|
||||
|
||||
SkillTypes skillType;
|
||||
|
||||
if (SkillTypes.TryParse(reward.Target, out skillType))
|
||||
{
|
||||
_logger.Error($"Unable to get skill points for: {reward.Target}");
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (reward.Type)
|
||||
{
|
||||
case RewardType.Skill:
|
||||
_profileHelper.AddSkillPointsToPlayer(profileData, skillType, double.Parse((string)reward.Value));
|
||||
break;
|
||||
case RewardType.Experience: // this must occur first as the output object needs to take the modified profile exp value
|
||||
_profileHelper.AddExperienceToPmc(sessionId, int.Parse(reward.Target));
|
||||
break;
|
||||
case RewardType.TraderStanding:
|
||||
_traderHelper.AddStandingToTrader(sessionId, reward.Target, double.Parse((string)reward.Value));
|
||||
break;
|
||||
case RewardType.TraderUnlock:
|
||||
_traderHelper.SetTraderUnlockedState(reward.Target, true, sessionId);
|
||||
break;
|
||||
case RewardType.Item:
|
||||
// Handled by getQuestRewardItems() below
|
||||
break;
|
||||
case RewardType.AssortmentUnlock:
|
||||
// Handled by getAssort(), locked assorts are stripped out by `assortHelper.stripLockedLoyaltyAssort()` before being sent to player
|
||||
break;
|
||||
case RewardType.Achievement:
|
||||
_profileHelper.AddAchievementToProfile(fullProfile, reward.Target);
|
||||
break;
|
||||
case RewardType.StashRows: // Add specified stash rows from quest reward - requires client restart
|
||||
_profileHelper.AddStashRowsBonusToProfile(sessionId, int.Parse((string)reward.Value));
|
||||
break;
|
||||
case RewardType.ProductionScheme:
|
||||
FindAndAddHideoutProductionIdToProfile(pmcProfile, reward, questDetails, sessionId, questResponse);
|
||||
break;
|
||||
case RewardType.Pockets:
|
||||
_profileHelper.ReplaceProfilePocketTpl(pmcProfile, reward.Target);
|
||||
break;
|
||||
case RewardType.CustomizationDirect:
|
||||
_profileHelper.AddHideoutCustomisationUnlock(fullProfile, reward, CustomisationSource.UNLOCKED_IN_GAME);
|
||||
break;
|
||||
default:
|
||||
_logger.Error(
|
||||
_localisationService.GetText(
|
||||
"quest-reward_type_not_handled",
|
||||
new
|
||||
{
|
||||
rewardType = reward.Type,
|
||||
questId = questId,
|
||||
questName = questDetails.QuestName
|
||||
}
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return GetQuestRewardItems(questDetails, state, gameVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the provided quest 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
|
||||
*/
|
||||
public bool QuestRewardIsForGameEdition(Reward reward, string gameVersion)
|
||||
{
|
||||
if (reward?.AvailableInGameEditions?.Count > 0 && !reward.AvailableInGameEditions.Any(ge => ge == gameVersion))
|
||||
{
|
||||
// Reward has edition whitelist and game version isnt in it
|
||||
return false;
|
||||
}
|
||||
|
||||
if (reward?.NotAvailableInGameEditions?.Count > 0 && reward.NotAvailableInGameEditions.Any(ge => ge == gameVersion))
|
||||
{
|
||||
// Reward has edition blacklist and game version is in it
|
||||
return false;
|
||||
}
|
||||
|
||||
// No whitelist/blacklist or reward isnt blacklisted/whitelisted
|
||||
return true;
|
||||
var rewards = questDetails.Rewards[state.ToString()];
|
||||
return _rewardHelper.ApplyRewards(
|
||||
rewards,
|
||||
CustomisationSource.UNLOCKED_IN_GAME,
|
||||
fullProfile,
|
||||
profileData,
|
||||
questId,
|
||||
questResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -226,7 +137,8 @@ public class QuestRewardHelper(
|
||||
*/
|
||||
public Quest ApplyMoneyBoost(Quest quest, double bonusPercent, QuestStatusEnum questStatus)
|
||||
{
|
||||
var rewards = (List<Reward>)quest.Rewards.GetType()
|
||||
var clonedQuest = _cloner.Clone(quest);
|
||||
var rewards = (List<Reward>)clonedQuest.Rewards.GetType()
|
||||
.GetProperties()
|
||||
.FirstOrDefault(p => p.Name == questStatus.ToString())
|
||||
.GetValue(quest.Rewards) ??
|
||||
@@ -234,7 +146,7 @@ public class QuestRewardHelper(
|
||||
var currencyRewards = rewards.Where(
|
||||
r =>
|
||||
r.Type.ToString() == "Item" &&
|
||||
_paymentHelper.IsMoneyTpl(r.Items[0].Template)
|
||||
_paymentHelper.IsMoneyTpl(r.Items.FirstOrDefault().Template)
|
||||
);
|
||||
foreach (var reward in currencyRewards)
|
||||
{
|
||||
@@ -245,219 +157,6 @@ public class QuestRewardHelper(
|
||||
reward.Value = newCurrencyAmount;
|
||||
}
|
||||
|
||||
return quest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WIP - Find hideout craft id and add to unlockedProductionRecipe array in player profile
|
||||
/// also update client response recipeUnlocked array with craft id
|
||||
/// </summary>
|
||||
/// <param name="pmcData">Player profile</param>
|
||||
/// <param name="craftUnlockReward">Reward item from quest with craft unlock details</param>
|
||||
/// <param name="questDetails">Quest with craft unlock reward</param>
|
||||
/// <param name="sessionID">Session id</param>
|
||||
/// <param name="response">Response to send back to client</param>
|
||||
protected void FindAndAddHideoutProductionIdToProfile(PmcData pmcData, Reward craftUnlockReward, Quest questDetails, string sessionID,
|
||||
ItemEventRouterResponse response)
|
||||
{
|
||||
var matchingProductions = GetRewardProductionMatch(craftUnlockReward, questDetails);
|
||||
if (matchingProductions.Count != 1)
|
||||
{
|
||||
_logger.Error(
|
||||
_localisationService.GetText(
|
||||
"quest-unable_to_find_matching_hideout_production",
|
||||
new
|
||||
{
|
||||
questName = questDetails.QuestName,
|
||||
matchCount = matchingProductions.Count
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Add above match to pmc profile + client response
|
||||
var matchingCraftId = matchingProductions[0]?.Id;
|
||||
pmcData?.UnlockedInfo?.UnlockedProductionRecipe?.Add(matchingCraftId);
|
||||
response.ProfileChanges[sessionID].RecipeUnlocked[matchingCraftId] = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find hideout craft for the specified quest reward
|
||||
/// </summary>
|
||||
/// <param name="craftUnlockReward">Reward item from quest with craft unlock details</param>
|
||||
/// <param name="questDetails">Quest with craft unlock reward</param>
|
||||
/// <returns>Hideout craft</returns>
|
||||
public List<HideoutProduction> GetRewardProductionMatch(Reward craftUnlockReward, Quest questDetails)
|
||||
{
|
||||
// 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 = int.Parse((string)craftUnlockReward.TraderId);
|
||||
|
||||
var matchingProductions = craftingRecipes.Where(
|
||||
p =>
|
||||
p.AreaType == desiredHideoutAreaType &&
|
||||
p.Requirements.Any(r => r.Type == "QuestComplete") &&
|
||||
p.Requirements.Any(r => r.RequiredLevel == craftUnlockReward.LoyaltyLevel) &&
|
||||
p.EndProduct == craftUnlockReward.Items[0].Template
|
||||
);
|
||||
|
||||
// More/less than single match, above filtering wasn't strict enough
|
||||
if (matchingProductions.Count() != 1)
|
||||
matchingProductions = matchingProductions.Where(
|
||||
p =>
|
||||
p.Requirements.Any(
|
||||
r =>
|
||||
r.QuestId == questDetails.Id
|
||||
)
|
||||
);
|
||||
|
||||
return matchingProductions.ToList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a flat list of reward items for the given quest at a specific state for the specified game version (e.g. Fail/Success)
|
||||
* @param quest quest to get rewards for
|
||||
* @param status Quest status that holds the items (Started, Success, Fail)
|
||||
* @returns List of items with the correct maxStack
|
||||
*/
|
||||
protected List<Item> GetQuestRewardItems(Quest quest, QuestStatusEnum status, string gameVersion)
|
||||
{
|
||||
var rewards = (List<Reward>)quest?.Rewards.GetType()
|
||||
.GetProperties()
|
||||
.FirstOrDefault(p => p.Name == status.ToString())
|
||||
.GetValue(quest.Rewards);
|
||||
|
||||
if (rewards == null)
|
||||
return new();
|
||||
|
||||
// Iterate over all rewards with the desired status, flatten out items that have a type of Item
|
||||
var questRewards = rewards.SelectMany(
|
||||
r =>
|
||||
r.Type.ToString() == "Item" &&
|
||||
QuestRewardIsForGameEdition(r, gameVersion)
|
||||
? ProcessReward(r)
|
||||
: new()
|
||||
);
|
||||
|
||||
return questRewards.ToList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Take reward item from quest and set FiR status + fix stack sizes + fix mod Ids
|
||||
* @param questReward Reward item to fix
|
||||
* @returns Fixed rewards
|
||||
*/
|
||||
protected List<Item> ProcessReward(Reward reward)
|
||||
{
|
||||
// item with mods to return
|
||||
var rewardItems = new List<Item>();
|
||||
var targets = new List<Item>();
|
||||
var mods = new List<Item>();
|
||||
|
||||
// Is armor item that may need inserts / plates
|
||||
if (reward.Items.Count == 1 && _itemHelper.ArmorItemCanHoldMods(reward.Items[0].Template))
|
||||
{
|
||||
// Attempt to pull default preset from globals and add child items to reward (clones questReward.items)
|
||||
GenerateArmorRewardChildSlots(reward.Items[0], reward);
|
||||
}
|
||||
|
||||
foreach (var rewardItem in reward.Items)
|
||||
{
|
||||
_itemHelper.AddUpdObjectToItem(rewardItem);
|
||||
|
||||
// Reward items are granted Found in Raid status
|
||||
rewardItem.Upd.SpawnedInSession = true;
|
||||
|
||||
// Is root item, fix stacks
|
||||
if (rewardItem.Id == reward.Type.ToString())
|
||||
{
|
||||
// Is base reward item
|
||||
if (rewardItem.ParentId != null &&
|
||||
rewardItem.ParentId == "hideout" &&
|
||||
rewardItem.Upd != null &&
|
||||
rewardItem.Upd.StackObjectsCount != null &&
|
||||
rewardItem.Upd.StackObjectsCount > 0)
|
||||
{
|
||||
rewardItem.Upd.StackObjectsCount = 1;
|
||||
}
|
||||
|
||||
targets = _itemHelper.SplitStack(rewardItem);
|
||||
// splitStack created new ids for the new stacks. This would destroy the relation to possible children.
|
||||
// Instead, we reset the id to preserve relations and generate a new id in the downstream loop, where we are also reparenting if required
|
||||
|
||||
foreach (var target in targets)
|
||||
{
|
||||
target.Id = rewardItem.Id;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Is child mod
|
||||
if (reward.Items[0].Upd.SpawnedInSession ?? false) // Propigate FiR status into child items
|
||||
rewardItem.Upd.SpawnedInSession = reward.Items[0].Upd.SpawnedInSession;
|
||||
|
||||
mods.Add(rewardItem);
|
||||
}
|
||||
}
|
||||
|
||||
// Add mods to the base items, fix ids
|
||||
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<Item> { _cloner.Clone(target) };
|
||||
// Here we generate a new id for the root item
|
||||
target.Id = _hashUtil.Generate();
|
||||
|
||||
foreach (var mod in mods)
|
||||
{
|
||||
itemsClone.Add(_cloner.Clone(mod));
|
||||
}
|
||||
|
||||
rewardItems.AddRange(_itemHelper.ReparentItemAndChildren(target, itemsClone));
|
||||
}
|
||||
|
||||
return rewardItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add missing mod items to a quest armor reward
|
||||
* @param originalRewardRootItem Original armor reward item from QuestReward.items object
|
||||
* @param questReward Armor reward from quest
|
||||
*/
|
||||
protected void GenerateArmorRewardChildSlots(Item originalRewardRootItem, Reward reward)
|
||||
{
|
||||
// Look for a default preset from globals for armor
|
||||
var defaultPreset = _presetHelper.GetDefaultPreset(originalRewardRootItem.Template);
|
||||
if (defaultPreset != null)
|
||||
{
|
||||
// Found preset, use mods to hydrate reward item
|
||||
var presetAndMods = _itemHelper.ReplaceIDs(defaultPreset.Items);
|
||||
var newRootId = _itemHelper.RemapRootItemId(presetAndMods, _hashUtil.Generate());
|
||||
|
||||
reward.Items = presetAndMods;
|
||||
|
||||
// Find root item and set its stack count
|
||||
var rootItem = reward.Items.FirstOrDefault(i => i.Id == newRootId);
|
||||
|
||||
// Remap target id to the new presets root id
|
||||
reward.Target = rootItem.Id;
|
||||
|
||||
// Copy over stack count otherwise reward shows as missing in client
|
||||
_itemHelper.AddUpdObjectToItem(rootItem);
|
||||
|
||||
rootItem.Upd.StackObjectsCount = originalRewardRootItem.Upd.StackObjectsCount;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Warning($"Unable to find default preset for armor {originalRewardRootItem.Template}, adding mods manually");
|
||||
var itemDbData = _itemHelper.GetItem(originalRewardRootItem.Template).Value;
|
||||
|
||||
// Hydrate reward with only 'required' mods - necessary for things like helmets otherwise you end up with nvgs/visors etc
|
||||
reward.Items = _itemHelper.AddChildSlotItems(reward.Items, itemDbData, null, true);
|
||||
return clonedQuest;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
using Core.Models.Eft.Common.Tables;
|
||||
using Core.Models.Eft.Common;
|
||||
using Core.Models.Eft.Hideout;
|
||||
using Core.Models.Eft.ItemEvent;
|
||||
using Core.Models.Eft.Profile;
|
||||
using Core.Models.Enums;
|
||||
using Core.Models.Utils;
|
||||
using Core.Services;
|
||||
using Core.Utils;
|
||||
using Core.Utils.Cloners;
|
||||
using SptCommon.Annotations;
|
||||
|
||||
namespace Core.Helpers
|
||||
{
|
||||
[Injectable]
|
||||
public class RewardHelper
|
||||
{
|
||||
private readonly ISptLogger<RewardHelper> _logger;
|
||||
private readonly HashUtil _hashUtil;
|
||||
private readonly TimeUtil _timeUtil;
|
||||
private readonly ItemHelper _itemHelper;
|
||||
private readonly DatabaseService _databaseService;
|
||||
private readonly ProfileHelper _profileHelper;
|
||||
private readonly LocalisationService _localisationService;
|
||||
private readonly TraderHelper _traderHelper;
|
||||
private readonly PresetHelper _presetHelper;
|
||||
private readonly ICloner _cloner;
|
||||
|
||||
public RewardHelper(
|
||||
ISptLogger<RewardHelper> logger,
|
||||
HashUtil hashUtil,
|
||||
TimeUtil timeUtil,
|
||||
ItemHelper itemHelper,
|
||||
DatabaseService databaseService,
|
||||
ProfileHelper profileHelper,
|
||||
LocalisationService localisationService,
|
||||
TraderHelper traderHelper,
|
||||
PresetHelper presetHelper,
|
||||
ICloner cloner
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_hashUtil = hashUtil;
|
||||
_timeUtil = timeUtil;
|
||||
_itemHelper = itemHelper;
|
||||
_databaseService = databaseService;
|
||||
_profileHelper = profileHelper;
|
||||
_localisationService = localisationService;
|
||||
_traderHelper = traderHelper;
|
||||
_presetHelper = presetHelper;
|
||||
_cloner = cloner;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public List<Item> ApplyRewards(
|
||||
List<Reward> rewards,
|
||||
string source,
|
||||
SptProfile fullProfile,
|
||||
PmcData profileData,
|
||||
string questId,
|
||||
ItemEventRouterResponse questResponse = null
|
||||
)
|
||||
{
|
||||
var sessionId = fullProfile?.ProfileInfo?.ProfileId;
|
||||
var pmcProfile = fullProfile?.CharacterData.PmcData;
|
||||
if (pmcProfile is null)
|
||||
{
|
||||
_logger.Error($"Unable to get pmc profile for: {sessionId}, no rewards given");
|
||||
return [];
|
||||
}
|
||||
|
||||
var gameVersion = pmcProfile.Info.GameVersion;
|
||||
|
||||
foreach (var reward in rewards)
|
||||
{
|
||||
// Handle reward availability for different game versions, notAvailableInGameEditions currently not used
|
||||
if (!RewardIsForGameEdition(reward, gameVersion))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (reward.Type)
|
||||
{
|
||||
case RewardType.Skill:
|
||||
// This needs to use the passed in profileData, as it could be the scav profile
|
||||
_profileHelper.AddSkillPointsToPlayer(
|
||||
profileData,
|
||||
Enum.Parse<SkillTypes>(reward.Target),
|
||||
reward.Value as double?
|
||||
);
|
||||
break;
|
||||
case RewardType.Experience:
|
||||
_profileHelper.AddExperienceToPmc(
|
||||
sessionId,
|
||||
(int)reward.Value
|
||||
); // this must occur first as the output object needs to take the modified profile exp value
|
||||
break;
|
||||
case RewardType.TraderStanding:
|
||||
_traderHelper.AddStandingToTrader(
|
||||
sessionId,
|
||||
reward.Target,
|
||||
(double)reward.Value
|
||||
);
|
||||
break;
|
||||
case RewardType.TraderUnlock:
|
||||
_traderHelper.SetTraderUnlockedState(reward.Target, true, sessionId);
|
||||
break;
|
||||
case RewardType.Item:
|
||||
// Item rewards are retrieved by getRewardItems() below, and returned to be handled by caller
|
||||
break;
|
||||
case RewardType.AssortmentUnlock:
|
||||
// Handled by getAssort(), locked assorts are stripped out by `assortHelper.stripLockedLoyaltyAssort()` before being sent to player
|
||||
break;
|
||||
case RewardType.Achievement:
|
||||
AddAchievementToProfile(fullProfile, reward.Target);
|
||||
break;
|
||||
case RewardType.StashRows:
|
||||
_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);
|
||||
break;
|
||||
case RewardType.Pockets:
|
||||
_profileHelper.ReplaceProfilePocketTpl(pmcProfile, reward.Target);
|
||||
break;
|
||||
case RewardType.CustomizationDirect:
|
||||
_profileHelper.AddHideoutCustomisationUnlock(fullProfile, reward, source);
|
||||
break;
|
||||
default:
|
||||
_logger.Error(
|
||||
_localisationService.GetText(
|
||||
"reward-type_not_handled",
|
||||
new
|
||||
{
|
||||
rewardType = reward.Type,
|
||||
questId = questId,
|
||||
}
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return GetRewardItems(rewards, gameVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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
|
||||
return false;
|
||||
}
|
||||
|
||||
if (reward.NotAvailableInGameEditions?.Count > 0 &&
|
||||
reward.NotAvailableInGameEditions.Contains(gameVersion))
|
||||
{
|
||||
// Reward has edition blacklist and game version is in it
|
||||
return false;
|
||||
}
|
||||
|
||||
// No whitelist/blacklist or reward isn't blacklisted/whitelisted
|
||||
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
|
||||
*/
|
||||
protected void FindAndAddHideoutProductionIdToProfile(
|
||||
PmcData pmcData,
|
||||
Reward craftUnlockReward,
|
||||
string questId,
|
||||
string sessionID,
|
||||
ItemEventRouterResponse response)
|
||||
{
|
||||
var matchingProductions = GetRewardProductionMatch(craftUnlockReward, questId);
|
||||
if (matchingProductions.Count != 1)
|
||||
{
|
||||
_logger.Error(
|
||||
_localisationService.GetText(
|
||||
"reward-unable_to_find_matching_hideout_production",
|
||||
new
|
||||
{
|
||||
questId = questId,
|
||||
matchCount = matchingProductions.Count,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Add above match to pmc profile + client response
|
||||
var matchingCraftId = matchingProductions[0].Id;
|
||||
pmcData.UnlockedInfo.UnlockedProductionRecipe.Add(matchingCraftId);
|
||||
if (response is not null)
|
||||
{
|
||||
response.ProfileChanges[sessionID].RecipeUnlocked[matchingCraftId] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<HideoutProduction> 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 = int.Parse(craftUnlockReward.TraderId.ToString());
|
||||
|
||||
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
|
||||
)
|
||||
.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)
|
||||
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<Item> GetRewardItems(List<Reward> rewards, string gameVersion)
|
||||
{
|
||||
// Iterate over all rewards with the desired status, flatten out items that have a type of Item
|
||||
var rewardItems = rewards.SelectMany(
|
||||
(reward) =>
|
||||
reward.Type == RewardType.Item && RewardIsForGameEdition(reward, gameVersion)
|
||||
? ProcessReward(reward)
|
||||
: []
|
||||
);
|
||||
|
||||
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
|
||||
*/
|
||||
protected List<Item> ProcessReward(Reward reward)
|
||||
{
|
||||
/** item with mods to return */
|
||||
List<Item> rewardItems = [];
|
||||
List<Item> targets = [];
|
||||
List<Item> mods = [];
|
||||
|
||||
// Is armor item that may need inserts / plates
|
||||
if (reward.Items.Count == 1 && _itemHelper.ArmorItemCanHoldMods(reward.Items[0].Template))
|
||||
{
|
||||
// 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)
|
||||
GenerateArmorRewardChildSlots(reward.Items.FirstOrDefault(), reward);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var rewardItem in reward.Items)
|
||||
{
|
||||
_itemHelper.AddUpdObjectToItem(rewardItem);
|
||||
|
||||
// Reward items are granted Found in Raid status
|
||||
rewardItem.Upd.SpawnedInSession = true;
|
||||
|
||||
// Is root item, fix stacks
|
||||
if (rewardItem.Id == reward.Target)
|
||||
{
|
||||
// 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.Upd.StackObjectsCount > 1 // More than 1 item in stack
|
||||
)
|
||||
{
|
||||
rewardItem.Upd.StackObjectsCount = 1;
|
||||
}
|
||||
|
||||
targets = _itemHelper.SplitStack(rewardItem);
|
||||
// splitStack created new ids for the new stacks. This would destroy the relation to possible children.
|
||||
// Instead, we reset the id to preserve relations and generate a new id in the downstream loop, where we are also reparenting if required
|
||||
foreach (var target in targets)
|
||||
{
|
||||
target.Id = rewardItem.Id;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Is child mod
|
||||
if (reward.Items.FirstOrDefault().Upd.SpawnedInSession.GetValueOrDefault(false))
|
||||
{
|
||||
// Propigate FiR status into child items
|
||||
rewardItem.Upd.SpawnedInSession = reward.Items.FirstOrDefault()?.Upd.SpawnedInSession;
|
||||
}
|
||||
|
||||
mods.Add(rewardItem);
|
||||
}
|
||||
}
|
||||
|
||||
// Add mods to the base items, fix ids
|
||||
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<Item> { _cloner.Clone(target) };
|
||||
// Here we generate a new id for the root item
|
||||
target.Id = _hashUtil.Generate();
|
||||
|
||||
foreach (var mod in mods)
|
||||
{
|
||||
itemsClone.Add(_cloner.Clone(mod));
|
||||
}
|
||||
|
||||
rewardItems.AddRange(rewardItems.Concat(_itemHelper.ReparentItemAndChildren(target, itemsClone)));
|
||||
}
|
||||
|
||||
return rewardItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add missing mod items to an armor reward
|
||||
* @param originalRewardRootItem Original armor reward item from IReward.items object
|
||||
* @param reward Armor reward
|
||||
*/
|
||||
protected void GenerateArmorRewardChildSlots(Item originalRewardRootItem, Reward reward)
|
||||
{
|
||||
// Look for a default preset from globals for armor
|
||||
var defaultPreset = _presetHelper.GetDefaultPreset(originalRewardRootItem.Template);
|
||||
if (defaultPreset is not null)
|
||||
{
|
||||
// Found preset, use mods to hydrate reward item
|
||||
var presetAndMods = _itemHelper.ReplaceIDs(defaultPreset.Items);
|
||||
var newRootId = _itemHelper.RemapRootItemId(presetAndMods);
|
||||
|
||||
reward.Items = presetAndMods;
|
||||
|
||||
// Find root item and set its stack count
|
||||
var rootItem = reward.Items.FirstOrDefault((item) => item.Id == newRootId);
|
||||
|
||||
// Remap target id to the new presets root id
|
||||
reward.Target = rootItem.Id;
|
||||
|
||||
// Copy over stack count otherwise reward shows as missing in client
|
||||
_itemHelper.AddUpdObjectToItem(rootItem);
|
||||
rootItem.Upd.StackObjectsCount = originalRewardRootItem.Upd.StackObjectsCount;
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Warning(
|
||||
"Unable to find default preset for armor {originalRewardRootItem._tpl}, adding mods manually"
|
||||
);
|
||||
var itemDbData = _itemHelper.GetItem(originalRewardRootItem.Template).Value;
|
||||
|
||||
// Hydrate reward with only 'required' mods - necessary for things like helmets otherwise you end up with nvgs/visors etc
|
||||
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
|
||||
*/
|
||||
public void AddAchievementToProfile(SptProfile fullProfile, string achievementId)
|
||||
{
|
||||
// Add achievement id to profile with timestamp it was unlocked
|
||||
fullProfile.CharacterData.PmcData.Achievements[achievementId] = _timeUtil.GetTimeStamp();
|
||||
|
||||
// Check for any customisation unlocks
|
||||
var achievementDataDb = _databaseService
|
||||
.GetTemplates()
|
||||
.Achievements.FirstOrDefault((achievement) => achievement.Id == achievementId);
|
||||
if (achievementDataDb is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: At the moment, we don't know the exact quest and achievement data layout for an achievement
|
||||
// that is triggered by a quest, that gives an item, because BSG has only done this once. However
|
||||
// based on deduction, I am going to assume that the *quest* will handle the initial item reward,
|
||||
// and the achievement reward should only be handled post-wipe.
|
||||
// All of that is to say, we are going to ignore the list of returned reward items here
|
||||
var pmcProfile = fullProfile.CharacterData.PmcData;
|
||||
ApplyRewards(
|
||||
achievementDataDb.Rewards,
|
||||
CustomisationSource.ACHIEVEMENT,
|
||||
fullProfile,
|
||||
pmcProfile,
|
||||
achievementDataDb.Id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Core.Models.Enums;
|
||||
using SptCommon.Extensions;
|
||||
|
||||
namespace Core.Models.Eft.Common.Tables;
|
||||
|
||||
@@ -492,4 +493,17 @@ public record QuestRewards
|
||||
|
||||
[JsonPropertyName("Expired")]
|
||||
public List<Reward>? Expired { get; set; }
|
||||
|
||||
public List<Reward> this[string propName]
|
||||
{
|
||||
get
|
||||
{
|
||||
var matchingProp = GetType()
|
||||
.GetProperties()
|
||||
.SingleOrDefault(p => p.GetJsonName() == propName)
|
||||
?.GetValue(this);
|
||||
|
||||
return (List<Reward>)matchingProp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
using SptCommon.Annotations;
|
||||
using Core.Helpers;
|
||||
using SptCommon.Annotations;
|
||||
using Core.Models.Eft.Common;
|
||||
using Core.Models.Eft.Common.Tables;
|
||||
using Core.Models.Eft.Match;
|
||||
using Core.Models.Utils;
|
||||
|
||||
namespace Core.Services;
|
||||
|
||||
[Injectable(InjectionType.Singleton)]
|
||||
public class LocationLifecycleService
|
||||
{
|
||||
private readonly ISptLogger<LocationLifecycleService> _logger;
|
||||
private readonly RewardHelper _rewardHelper;
|
||||
|
||||
public LocationLifecycleService(
|
||||
ISptLogger<LocationLifecycleService> logger,
|
||||
RewardHelper rewardHelper)
|
||||
{
|
||||
_logger = logger;
|
||||
_rewardHelper = rewardHelper;
|
||||
}
|
||||
|
||||
/** Handle client/match/local/start */
|
||||
public void StartLocalRaid(string sessionId, StartLocalRaidRequestData request)
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ public class ProfileFixerService(
|
||||
HashUtil _hashUtil,
|
||||
JsonUtil _jsonUtil,
|
||||
ItemHelper _itemHelper,
|
||||
QuestRewardHelper _questRewardHelper,
|
||||
RewardHelper _rewardHelper,
|
||||
TraderHelper _traderHelper,
|
||||
HideoutHelper _hideoutHelper,
|
||||
DatabaseService _databaseService,
|
||||
@@ -369,9 +369,9 @@ public class ProfileFixerService(
|
||||
/// <param name="questDetails">The quest the reward belongs to</param>
|
||||
protected void VerifyQuestProductionUnlock(PmcData pmcProfile, Reward productionUnlockReward, Quest questDetails)
|
||||
{
|
||||
var matchingProductions = _questRewardHelper.GetRewardProductionMatch(
|
||||
var matchingProductions = _rewardHelper.GetRewardProductionMatch(
|
||||
productionUnlockReward,
|
||||
questDetails
|
||||
questDetails.Id
|
||||
);
|
||||
|
||||
if (matchingProductions.Count != 1)
|
||||
|
||||
Reference in New Issue
Block a user