From e1ec67619daa75391d35175fa07c6901fb4b00ff Mon Sep 17 00:00:00 2001 From: CWX Date: Wed, 22 Jan 2025 11:15:14 +0000 Subject: [PATCH] questHelper done --- Libraries/Core/Helpers/QuestHelper.cs | 325 +++++++++++++++++++++-- SptCommon/Extensions/ObjectExtensions.cs | 29 ++ 2 files changed, 339 insertions(+), 15 deletions(-) diff --git a/Libraries/Core/Helpers/QuestHelper.cs b/Libraries/Core/Helpers/QuestHelper.cs index 0e1c5ee0..557eb5f3 100644 --- a/Libraries/Core/Helpers/QuestHelper.cs +++ b/Libraries/Core/Helpers/QuestHelper.cs @@ -11,6 +11,7 @@ using Core.Servers; using Core.Services; using Core.Utils; using Core.Utils.Cloners; +using SptCommon.Extensions; using Product = Core.Models.Eft.ItemEvent.Product; namespace Core.Helpers; @@ -38,6 +39,7 @@ public class QuestHelper( ) { protected QuestConfig _questConfig = _configServer.GetConfig(); + protected List _newlyQuestCheck = [QuestStatusEnum.Started, QuestStatusEnum.AvailableForFinish]; /// /// Get status of a quest in player profile by its id @@ -202,13 +204,66 @@ public class QuestHelper( * @param newState State the new quest should be in when returned * @param acceptedQuest Details of accepted quest from client */ - public QuestStatus GetQuestReadyForProfile( - PmcData pmcData, - QuestStatusEnum newState, - AcceptQuestRequestData acceptedQuest - ) + public QuestStatus GetQuestReadyForProfile(PmcData pmcData, QuestStatusEnum newState, AcceptQuestRequestData acceptedQuest) { - throw new NotImplementedException(); + var currentTimestamp = _timeUtil.GetTimeStamp(); + var existingQuest = pmcData.Quests.FirstOrDefault(q => q.QId == acceptedQuest.QuestId); + if (existingQuest is not null) + { + // Quest exists, update its status + existingQuest.StartTime = currentTimestamp; + existingQuest.Status = newState; + existingQuest.StatusTimers[newState] = currentTimestamp; + existingQuest.CompletedConditions = []; + + if (existingQuest.AvailableAfter is not null) + { + existingQuest.AvailableAfter = null; + } + + return existingQuest; + } + + // Quest doesn't exists, add it + QuestStatus newQuest = new QuestStatus + { + QId = acceptedQuest.QuestId, + StartTime = currentTimestamp, + Status = newState, + StatusTimers = new Dictionary(), + }; + + // Check if quest has a prereq to be placed in a 'pending' state, otherwise set status timers value + var questDbData = GetQuestFromDb(acceptedQuest.QuestId, pmcData); + if (questDbData is null) + { + _logger.Error( + _localisationService.GetText( + "quest-unable_to_find_quest_in_db", + new + { + questId = acceptedQuest.QuestId, + questType = acceptedQuest.Type, + } + ) + ); + } + + var waitTime = questDbData?.Conditions.AvailableForStart.FirstOrDefault(x => x.AvailableAfter > 0); + if (waitTime is not null && acceptedQuest.Type != "repeatable") + { + // Quest should be put into 'pending' state + newQuest.StartTime = 0; + newQuest.Status = QuestStatusEnum.AvailableAfter; // 9 + newQuest.AvailableAfter = currentTimestamp + waitTime.AvailableAfter; + } + else + { + newQuest.StatusTimers[newState] = currentTimestamp; + newQuest.CompletedConditions = []; + } + + return newQuest; } /** @@ -219,7 +274,84 @@ public class QuestHelper( */ public List GetNewlyAccessibleQuestsWhenStartingQuest(string startedQuestId, string sessionID) { - throw new NotImplementedException(); + // Get quest acceptance data from profile + PmcData profile = _profileHelper.GetPmcProfile(sessionID); + var startedQuestInProfile = profile.Quests.FirstOrDefault(profileQuest => profileQuest.QId == startedQuestId); + + // Get quests that + var eligibleQuests = GetQuestsFromDb() + .Where( + quest => + { + // Quest is accessible to player when the accepted quest passed into param is started + // e.g. Quest A passed in, quest B is looped over and has requirement of A to be started, include it + var acceptedQuestCondition = quest.Conditions.AvailableForStart.FirstOrDefault( + condition => + { + return ( + condition.ConditionType == "Quest" && + (condition.Target?.List.Contains(startedQuestId) ?? false) && + (condition.Status?.Contains(QuestStatusEnum.Started) ?? false) + ); + } + ); + + // Not found, skip quest + if (acceptedQuestCondition is null) + { + return false; + } + + // Skip locked event quests + if (!ShowEventQuestToPlayer(quest.Id)) + { + return false; + } + + // Skip quest if its flagged as for other side + if (QuestIsForOtherSide(profile.Info.Side, quest.Id)) + { + return false; + } + + if (QuestIsProfileBlacklisted(profile.Info.GameVersion, quest.Id)) + { + return false; + } + + if (!QuestIsProfileWhitelisted(profile.Info.GameVersion, quest.Id)) + { + return false; + } + + var standingRequirements = _questConditionHelper.GetStandingConditions( + quest.Conditions.AvailableForStart + ); + foreach (var condition in standingRequirements) + { + if (!TraderStandingRequirementCheck(condition, profile)) + { + return false; + } + } + + var loyaltyRequirements = _questConditionHelper.GetLoyaltyConditions( + quest.Conditions.AvailableForStart + ); + foreach (var condition in loyaltyRequirements) + { + if (!TraderLoyaltyLevelRequirementCheck(condition, profile)) + { + return false; + } + } + + // Include if quest found in profile and is started or ready to hand in + return ((startedQuestInProfile is not null) && _newlyQuestCheck.Contains((QuestStatusEnum)startedQuestInProfile.Status)); + } + ); + + return GetQuestsWithOnlyLevelRequirementStartCondition(eligibleQuests.ToList()); } /** @@ -919,7 +1051,8 @@ public class QuestHelper( ? conditionToFulfil.Target.List : (conditionToFulfil.Target.Item == null ? null - : [conditionToFulfil.Target.Item])) ?? []; + : [conditionToFulfil.Target.Item])) ?? + []; var prerequisiteQuest = profile.Quests.FirstOrDefault(profileQuest => questIdsToFulfil.Contains(profileQuest.QId)); @@ -1000,7 +1133,24 @@ public class QuestHelper( */ protected List UpdateQuestsForGameEdition(List quests, string gameVersion) { - throw new NotImplementedException(); + _logger.Debug("[UpdateQuestsForGameEdition] If you are hitting this method, please confirm the return is compared to Node"); + var modifiedQuests = _cloner.Clone(quests); + foreach (var quest in modifiedQuests) + { + // Remove any reward that doesn't pass the game edition check + var propsAsDict = quest.Rewards.GetAllPropsAsDict>(); + foreach (var rewardType in propsAsDict) + { + propsAsDict[rewardType.Key] = propsAsDict[rewardType.Key] + .Where( + reward => + _rewardHelper.RewardIsForGameEdition(reward, gameVersion) + ) + .ToList(); + } + } + + return modifiedQuests; } /** @@ -1010,7 +1160,26 @@ public class QuestHelper( */ protected List GetQuestsFromProfileFailedByCompletingQuest(string completedQuestId, PmcData pmcProfile) { - throw new NotImplementedException(); + var questsInDb = GetQuestsFromDb(); + return questsInDb.Where( + quest => + { + // No fail conditions, skip + if (quest.Conditions.Fail is null || quest.Conditions.Fail.Count == 0) + { + return false; + } + + // Quest already failed in profile, skip + if (pmcProfile.Quests.Any(profileQuest => profileQuest.QId == quest.Id && profileQuest.Status == QuestStatusEnum.Fail)) + { + return false; + } + + return quest.Conditions.Fail.Any(condition => (condition.Target.List?.Contains(completedQuestId) ?? false)); + } + ) + .ToList(); } /** @@ -1028,7 +1197,50 @@ public class QuestHelper( ItemEventRouterResponse output ) { - throw new NotImplementedException(); + foreach (var questToFail in questsToFail) + { + // Skip failing a quest that has a fail status of something other than success + if (questToFail.Conditions.Fail?.Any(x => x.Status?.Any(status => status != QuestStatusEnum.Success) ?? false) ?? false) + { + continue; + } + + var isActiveQuestInPlayerProfile = pmcData.Quests.FirstOrDefault(quest => quest.QId == questToFail.Id); + if (isActiveQuestInPlayerProfile is not null) + { + if (isActiveQuestInPlayerProfile.Status != QuestStatusEnum.Fail) + { + FailQuestRequestData failBody = new FailQuestRequestData + { + Action = "QuestFail", + QuestId = questToFail.Id, + RemoveExcessItems = true, + }; + FailQuest(pmcData, failBody, sessionID, output); + } + } + else + { + // Failing an entirely new quest that doesn't exist in profile + Dictionary statusTimers = new Dictionary(); + + if (!statusTimers.TryGetValue(QuestStatusEnum.Fail, out var _)) + { + statusTimers.Add(QuestStatusEnum.Fail, 0); + } + + statusTimers[QuestStatusEnum.Fail] = _timeUtil.GetTimeStamp(); + QuestStatus questData = new QuestStatus + { + QId = questToFail.Id, + StartTime = _timeUtil.GetTimeStamp(), + StatusTimers = statusTimers, + Status = QuestStatusEnum.Fail, + } + ; + pmcData.Quests.Add(questData); + } + } } /** @@ -1045,7 +1257,16 @@ public class QuestHelper( List questRewards ) { - throw new NotImplementedException(); + var quest = GetQuestFromDb(completedQuestId, pmcData); + + _mailSendService.SendLocalisedNpcMessageToPlayer( + sessionID, + _traderHelper.GetTraderById(quest.TraderId).ToString(), + MessageType.QUEST_SUCCESS, + quest.SuccessMessageText, + questRewards, + _timeUtil.GetHoursAsSeconds((int)GetMailItemRedeemTimeHoursForProfile(pmcData)) + ); } /** @@ -1056,7 +1277,46 @@ public class QuestHelper( */ protected void AddTimeLockedQuestsToProfile(PmcData pmcData, List quests, string completedQuestId) { - throw new NotImplementedException(); + // Iterate over quests, look for quests with right criteria + foreach (var quest in quests) + { + // If quest has prereq of completed quest + availableAfter value > 0 (quest has wait time) + var nextQuestWaitCondition = quest.Conditions.AvailableForStart.FirstOrDefault( + x => (x.Target?.List.Contains(completedQuestId) ?? false) && x.AvailableAfter > 0 + ); + + if (nextQuestWaitCondition is not null) + { + // Now + wait time + var availableAfterTimestamp = _timeUtil.GetTimeStamp() + nextQuestWaitCondition.AvailableAfter; + + // Update quest in profile with status of AvailableAfter + var existingQuestInProfile = pmcData.Quests.FirstOrDefault(x => x.QId == quest.Id); + if (existingQuestInProfile is not null) + { + existingQuestInProfile.AvailableAfter = availableAfterTimestamp; + existingQuestInProfile.Status = QuestStatusEnum.AvailableAfter; + existingQuestInProfile.StartTime = 0; + existingQuestInProfile.StatusTimers = new Dictionary(); + + continue; + } + + pmcData.Quests.Add( + new QuestStatus + { + QId = quest.Id, + StartTime = 0, + Status = QuestStatusEnum.AvailableAfter, + StatusTimers = new Dictionary + { + { QuestStatusEnum.AvailableAfter, _timeUtil.GetTimeStamp() } + }, + AvailableAfter = availableAfterTimestamp, + } + ); + } + } } /** @@ -1066,7 +1326,25 @@ public class QuestHelper( */ protected void RemoveQuestFromScavProfile(string sessionId, string questIdToRemove) { - throw new NotImplementedException(); + var fullProfile = _profileHelper.GetFullProfile(sessionId); + var repeatableInScavProfile = fullProfile.CharacterData.ScavData.Quests?.FirstOrDefault(x => x.QId == questIdToRemove); + if (repeatableInScavProfile is null) + { + _logger.Warning( + _localisationService.GetText( + "quest-unable_to_remove_scav_quest_from_profile", + new + { + scavQuestId = questIdToRemove, + profileId = sessionId + } + ) + ); + + return; + } + + fullProfile.CharacterData.ScavData.Quests.Remove(repeatableInScavProfile); } /** @@ -1080,7 +1358,24 @@ public class QuestHelper( List postQuestStatuses ) { - throw new NotImplementedException(); + List result = []; + + foreach (var quest in postQuestStatuses) + { + // Add quest if status differs or quest not found + var preQuest = preQuestStatuses.FirstOrDefault(x => x.QId == quest.QId); + if (preQuest is null || preQuest.Status != quest.Status) + { + result.Add(quest); + } + } + + if (result.Count == 0) + { + return null; + } + + return result; } /** diff --git a/SptCommon/Extensions/ObjectExtensions.cs b/SptCommon/Extensions/ObjectExtensions.cs index 9fde7371..6ca1d718 100644 --- a/SptCommon/Extensions/ObjectExtensions.cs +++ b/SptCommon/Extensions/ObjectExtensions.cs @@ -48,5 +48,34 @@ namespace SptCommon.Extensions return default; return (T?)cachedProperty.GetValue(obj); } + + public static List GetAllPropValuesAsList(this object? obj) + { + ArgumentNullException.ThrowIfNull(obj); + + var list = obj.GetType().GetProperties(); + var result = new List(); + + foreach (var prop in list) + { + result.Add((T?)prop.GetValue(obj)); + } + + return result; + } + + public static Dictionary GetAllPropsAsDict(this object? obj) + { + var result = new Dictionary(); + + var props = obj.GetType().GetProperties(); + + foreach (var prop in props) + { + result.Add(prop.Name, (T?)prop.GetValue(obj)); + } + + return result; + } } }