using System.Collections.Generic; using System.Linq; using Dalamud.Game.ClientState.Conditions; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using LLib.GameData; using Microsoft.Extensions.Logging; using Questionable.Data; using Questionable.External; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; namespace Questionable.Controller; internal sealed class QuestPriorityResolver { private readonly QuestFunctions _questFunctions; private readonly QuestRegistry _questRegistry; private readonly QuestData _questData; private readonly AlliedSocietyData _alliedSocietyData; private readonly AlliedSocietyQuestFunctions _alliedSocietyQuestFunctions; private readonly Configuration _configuration; private readonly IObjectTable _objectTable; private readonly ICondition _condition; private readonly IClientState _clientState; private readonly AutoDutyIpc _autoDutyIpc; private readonly ILogger _logger; private readonly HashSet _alreadyLoggedUnobtainableQuests = new HashSet(); private ElementId? _lastLoggedLevelLockedMsq; private ElementId? _lastLoggedForcedClassQuest; private ElementId? _lastLoggedRegistryClassQuest; private ElementId? _lastLoggedPriorityClassQuest; private bool _loggedNoClassQuestsAvailable; private bool _loggedAdventurerClass; public QuestPriorityResolver(QuestFunctions questFunctions, QuestRegistry questRegistry, QuestData questData, AlliedSocietyData alliedSocietyData, AlliedSocietyQuestFunctions alliedSocietyQuestFunctions, Configuration configuration, IObjectTable objectTable, ICondition condition, IClientState clientState, AutoDutyIpc autoDutyIpc, ILogger logger) { _questFunctions = questFunctions; _questRegistry = questRegistry; _questData = questData; _alliedSocietyData = alliedSocietyData; _alliedSocietyQuestFunctions = alliedSocietyQuestFunctions; _configuration = configuration; _objectTable = objectTable; _condition = condition; _clientState = clientState; _autoDutyIpc = autoDutyIpc; _logger = logger; } public QuestReference GetCurrentQuest() { QuestResolution questResolution = ResolveFromGameState(new List(), QuestController.EAutomationType.Automatic); if (questResolution.HasQuest) { return new QuestReference(questResolution.QuestId, questResolution.Sequence, questResolution.MsqState); } return QuestReference.NoQuest(questResolution.MsqState); } public QuestResolution Resolve(QuestSlots slots) { if (slots.Simulated != null) { return QuestResolution.Slot(EQuestResolutionType.Simulated, slots.Simulated, "Simulated quest"); } if (slots.Next != null) { return QuestResolution.Slot(EQuestResolutionType.NextQuest, slots.Next, $"Next quest {slots.Next.Quest.Id}"); } return ResolveFromGameState(slots.ManualPriorityQuests, slots.AutomationType); } private QuestResolution ResolveFromGameState(List manualPriorityQuests, QuestController.EAutomationType automationType) { bool allowNewMsq = automationType != QuestController.EAutomationType.SingleQuestB; QuestReference resolvedMsqQuest = GetResolvedMsqQuest(); int num = _objectTable.LocalPlayer?.Level ?? 0; EClassJob valueOrDefault = ((EClassJob?)_objectTable.LocalPlayer?.ClassJob.RowId).GetValueOrDefault(); QuestResolution? questResolution = TryResolveManualPriority(manualPriorityQuests, resolvedMsqQuest.State); if (questResolution.HasValue) { QuestResolution valueOrDefault2 = questResolution.GetValueOrDefault(); return ApplyQuestRedirects(valueOrDefault2); } questResolution = TryResolveInProgressClassQuest(valueOrDefault, resolvedMsqQuest.State); if (questResolution.HasValue) { return questResolution.GetValueOrDefault(); } questResolution = TryResolveClassJobPriority(valueOrDefault, resolvedMsqQuest.State); if (questResolution.HasValue) { return questResolution.GetValueOrDefault(); } if (resolvedMsqQuest.CurrentQuest != null && IsMsqLevelLocked(resolvedMsqQuest, num, automationType)) { if (_lastLoggedLevelLockedMsq != resolvedMsqQuest.CurrentQuest) { _logger.LogInformation("MSQ {MsqId} requires higher level, current level {CurrentLevel}. Checking for early class quests.", resolvedMsqQuest.CurrentQuest, num); _lastLoggedLevelLockedMsq = resolvedMsqQuest.CurrentQuest; _loggedNoClassQuestsAvailable = false; _loggedAdventurerClass = false; } return TryResolveNewClassQuest(valueOrDefault, num, resolvedMsqQuest.State) ?? TryResolveLevelingMode(num, automationType, resolvedMsqQuest.State) ?? QuestResolution.None(MainScenarioQuestState.Unavailable, "MSQ level-locked, no class quests or leveling available"); } _lastLoggedLevelLockedMsq = null; _lastLoggedForcedClassQuest = null; _lastLoggedRegistryClassQuest = null; _lastLoggedPriorityClassQuest = null; _loggedNoClassQuestsAvailable = false; _loggedAdventurerClass = false; questResolution = TryResolveMsqImmediate(resolvedMsqQuest, allowNewMsq); if (questResolution.HasValue) { return questResolution.GetValueOrDefault(); } questResolution = TryResolveTrackedQuest(resolvedMsqQuest.State); if (questResolution.HasValue) { QuestResolution valueOrDefault3 = questResolution.GetValueOrDefault(); return ApplyQuestRedirects(valueOrDefault3); } questResolution = TryResolveAutoPriority(resolvedMsqQuest.State); if (questResolution.HasValue) { QuestResolution valueOrDefault4 = questResolution.GetValueOrDefault(); return ApplyQuestRedirects(valueOrDefault4); } questResolution = TryResolveMsqFallback(resolvedMsqQuest, allowNewMsq); if (questResolution.HasValue) { return questResolution.GetValueOrDefault(); } if (resolvedMsqQuest.CurrentQuest == null || resolvedMsqQuest.CurrentQuest.Value == 0) { questResolution = TryResolveStartingCity(resolvedMsqQuest.State); if (questResolution.HasValue) { return questResolution.GetValueOrDefault(); } } return QuestResolution.None(resolvedMsqQuest.State, "No quest available"); } private QuestReference GetResolvedMsqQuest() { QuestReference questReference = _questFunctions.GetMainScenarioQuest().Item1; if (questReference.CurrentQuest != null && !_questRegistry.IsKnownQuest(questReference.CurrentQuest)) { if (questReference.CurrentQuest is QuestId questId && _alreadyLoggedUnobtainableQuests.Add(questId.Value)) { _logger.LogWarning("MSQ {MsqQuestId} is not in the quest registry", questReference.CurrentQuest); } questReference = QuestReference.NoQuest(questReference.State); } return ApplyMsqSpecialCases(questReference); } private QuestReference ApplyMsqSpecialCases(QuestReference msqQuest) { var (elementId2, sequence, state) = msqQuest; if (elementId2 == null || elementId2.Value == 0) { return msqQuest; } if (elementId2.Value == 681) { if (_questFunctions.IsQuestAccepted(elementId2) || _questFunctions.IsQuestComplete(elementId2)) { return new QuestReference(elementId2, sequence, state); } GrandCompany grandCompany = _configuration.General.GrandCompany; if (grandCompany == GrandCompany.None) { grandCompany = PlayerStateHelper.GetStartTown() switch { 1 => GrandCompany.Maelstrom, 2 => GrandCompany.TwinAdder, 3 => GrandCompany.ImmortalFlames, _ => GrandCompany.None, }; } return grandCompany switch { GrandCompany.TwinAdder => new QuestReference(new QuestId(680), 0, state), GrandCompany.Maelstrom => new QuestReference(new QuestId(681), 0, state), GrandCompany.ImmortalFlames => new QuestReference(new QuestId(682), 0, state), _ => QuestReference.NoQuest(MainScenarioQuestState.Unavailable), }; } if (elementId2.Value == 3856 && !PlayerStateHelper.IsMountUnlocked(1u)) { ushort num = (GrandCompany)GrandCompanyHelper.GetGrandCompanyId() switch { GrandCompany.TwinAdder => 700, GrandCompany.Maelstrom => 701, GrandCompany.ImmortalFlames => 702, _ => 0, }; if (num != 0 && !QuestManager.IsQuestComplete(num)) { return new QuestReference(new QuestId(num), QuestManager.GetQuestSequence(num), state); } } if (elementId2.Value == 801) { QuestId questId = new QuestId(802); if (_questFunctions.IsQuestAccepted(questId)) { return new QuestReference(questId, QuestManager.GetQuestSequence(questId.Value), state); } } return msqQuest; } private QuestResolution ApplyQuestRedirects(QuestResolution resolution) { ElementId questId = resolution.QuestId; if ((object)questId != null && questId.Value == 801) { QuestId questId2 = new QuestId(802); if (_questFunctions.IsQuestAccepted(questId2)) { return resolution with { QuestId = questId2, Sequence = QuestManager.GetQuestSequence(questId2.Value), DebugReason = "Redirected 801 -> 802 (Broadening Horizons prerequisite)" }; } } return resolution; } private QuestResolution? TryResolveManualPriority(List manualPriorityQuests, MainScenarioQuestState msqState) { if (manualPriorityQuests.Count == 0) { return null; } (ElementId, byte) tuple = (from x in manualPriorityQuests where _questFunctions.IsQuestAccepted(x.Id) ? ((!_configuration.Advanced.AutoPrioritizeAlliedSocietyRankUp || !(x.Id is QuestId questId) || !_questFunctions.WouldOvercapAlliedSocietyReputation(questId)) ? true : false) : _questFunctions.IsReadyToAcceptQuest(x.Id) select (QuestId: x.Id, Sequence: _questFunctions.GetQuestProgressInfo(x.Id)?.Sequence ?? 0)).FirstOrDefault(); if ((object)tuple.Item1 != null) { return QuestResolution.ForQuest(EQuestResolutionType.ManualPriority, tuple.Item1, tuple.Item2, msqState, $"Manual priority quest {tuple.Item1}"); } return null; } private QuestResolution? TryResolveInProgressClassQuest(EClassJob currentClassJob, MainScenarioQuestState msqState) { if (currentClassJob == EClassJob.Adventurer) { return null; } QuestInfo questInfo = (from x in _questData.GetClassJobQuests(currentClassJob) where (x.Level <= 5 || !_configuration.Advanced.SkipClassJobQuests) && _questFunctions.IsQuestAccepted(x.QuestId) && !_questFunctions.IsQuestComplete(x.QuestId) orderby x.Level select x).FirstOrDefault(); if (questInfo == null) { return null; } _logger.LogTrace("Found in-progress class quest {QuestId}, continuing it", questInfo.QuestId); return QuestResolution.ForQuest(EQuestResolutionType.InProgressClassQuest, questInfo.QuestId, QuestManager.GetQuestSequence(questInfo.QuestId.Value), msqState, $"In-progress class quest {questInfo.QuestId}"); } private QuestResolution? TryResolveClassJobPriority(EClassJob currentClassJob, MainScenarioQuestState msqState) { if (_configuration.Advanced.SkipClassJobQuests || currentClassJob == EClassJob.Adventurer) { return null; } HashSet classJobQuestIds = (from x in _questRegistry.GetKnownClassJobQuests(currentClassJob, includeRoleQuests: false) select x.QuestId).ToHashSet(); ElementId elementId = (from x in _questFunctions.GetNextPriorityQuestsThatCanBeAccepted() where x.IsAvailable && classJobQuestIds.Contains(x.QuestId) select x.QuestId).FirstOrDefault(); if (elementId == null) { return null; } return QuestResolution.ForQuest(EQuestResolutionType.NewClassQuest, elementId, QuestManager.GetQuestSequence(elementId.Value), msqState, $"Class/job priority quest {elementId}"); } private bool IsMsqLevelLocked(QuestReference msqQuest, int currentLevel, QuestController.EAutomationType automationType) { if (msqQuest.CurrentQuest == null) { return false; } if (!_questRegistry.TryGetQuest(msqQuest.CurrentQuest, out Quest quest)) { return false; } if (_questFunctions.IsQuestAccepted(msqQuest.CurrentQuest)) { return false; } if (_configuration.General.MsqPriority == Configuration.EMsqPriorityMode.Manual) { return false; } return quest.Info.Level > currentLevel; } private QuestResolution? TryResolveNewClassQuest(EClassJob currentClassJob, int currentLevel, MainScenarioQuestState msqState) { if (currentClassJob == EClassJob.Adventurer) { if (!_loggedAdventurerClass) { _logger.LogWarning("Current class is Adventurer, cannot find class quests"); _loggedAdventurerClass = true; } return null; } List lockedQuests = QuestData.GetLockedClassQuests(); QuestInfo questInfo = (from x in _questData.GetClassJobQuests(currentClassJob) where x.Level <= currentLevel && x.Level <= 5 && !_questFunctions.IsQuestComplete(x.QuestId) && !lockedQuests.Contains(x.QuestId) orderby x.Level select x).FirstOrDefault(); if (questInfo != null && !_questFunctions.IsQuestComplete(questInfo.QuestId)) { if (_lastLoggedForcedClassQuest != questInfo.QuestId) { _logger.LogInformation("MSQ level locked. Forcing class quest {ClassQuestId} for {ClassJob} (level {QuestLevel})", questInfo.QuestId, currentClassJob, questInfo.Level); _lastLoggedForcedClassQuest = questInfo.QuestId; } return QuestResolution.ForQuest(EQuestResolutionType.NewClassQuest, questInfo.QuestId, QuestManager.GetQuestSequence(questInfo.QuestId.Value), msqState, $"New class quest (data) {questInfo.QuestId}"); } QuestInfo questInfo2 = (from x in _questRegistry.GetKnownClassJobQuests(currentClassJob, includeRoleQuests: false) where x.Level <= currentLevel && x.Level <= 5 && !_questFunctions.IsQuestAcceptedOrComplete(x.QuestId) && _questFunctions.IsReadyToAcceptQuest(x.QuestId) orderby x.Level select x).FirstOrDefault(); if (questInfo2 != null) { if (_lastLoggedRegistryClassQuest != questInfo2.QuestId) { _logger.LogInformation("MSQ level locked. Prioritizing class quest {ClassQuestId} for {ClassJob} (level {QuestLevel}) from registry", questInfo2.QuestId, currentClassJob, questInfo2.Level); _lastLoggedRegistryClassQuest = questInfo2.QuestId; } return QuestResolution.ForQuest(EQuestResolutionType.NewClassQuest, questInfo2.QuestId, QuestManager.GetQuestSequence(questInfo2.QuestId.Value), msqState, $"New class quest (registry) {questInfo2.QuestId}"); } Quest quest; ElementId elementId = (from x in _questFunctions.GetNextPriorityQuestsThatCanBeAccepted() where x.IsAvailable where _questRegistry.TryGetQuest(x.QuestId, out quest) && quest.Info.Level <= currentLevel && quest.Info is QuestInfo questInfo3 && questInfo3.ClassJobs.Count > 0 && questInfo3.ClassJobs.Any((EClassJob job) => !job.IsCrafter() && !job.IsGatherer()) select x.QuestId).FirstOrDefault(); if (elementId != null) { if (_lastLoggedPriorityClassQuest != elementId) { _logger.LogInformation("MSQ level locked. Prioritizing class quest {ClassQuestId} from priority list", elementId); _lastLoggedPriorityClassQuest = elementId; } return QuestResolution.ForQuest(EQuestResolutionType.NewClassQuest, elementId, QuestManager.GetQuestSequence(elementId.Value), msqState, $"New class quest (priority) {elementId}"); } if (!_loggedNoClassQuestsAvailable) { _logger.LogWarning("MSQ level locked but no available early class quests found for {ClassJob} at level {CurrentLevel}", currentClassJob, currentLevel); _loggedNoClassQuestsAvailable = true; } return null; } private QuestResolution? TryResolveMsqImmediate(QuestReference msqQuest, bool allowNewMsq) { if (msqQuest.CurrentQuest == null || msqQuest.CurrentQuest.Value == 0) { return null; } if (_questFunctions.IsQuestAccepted(msqQuest.CurrentQuest)) { return null; } if (_configuration.General.MsqPriority != Configuration.EMsqPriorityMode.Always) { return null; } if (!allowNewMsq) { return null; } return QuestResolution.ForQuest(EQuestResolutionType.MsqImmediate, msqQuest.CurrentQuest, msqQuest.Sequence, msqQuest.State, $"MSQ immediate (Always mode) {msqQuest.CurrentQuest}"); } private unsafe QuestResolution? TryResolveTrackedQuest(MainScenarioQuestState msqState) { QuestManager* ptr = QuestManager.Instance(); if (ptr == null) { return null; } List<(ElementId, byte)> list = new List<(ElementId, byte)>(); for (int num = ptr->TrackedQuests.Length - 1; num >= 0; num--) { TrackingWork trackingWork = ptr->TrackedQuests[num]; switch (trackingWork.QuestType) { case 1: { ElementId elementId = new QuestId(ptr->NormalQuests[trackingWork.Index].QuestId); if (_questRegistry.IsKnownQuest(elementId) && !_questFunctions.IsQuestBlacklisted(elementId)) { list.Add((elementId, QuestManager.GetQuestSequence(elementId.Value))); } break; } } } if (_configuration.General.SkipLowPriorityDuties && list.Count > 0) { IReadOnlyList<(uint ContentFinderConditionId, ElementId QuestId, int Sequence)> lowPriorityQuests = _questRegistry.LowPriorityContentFinderConditionQuests; list.RemoveAll(((ElementId Quest, byte Sequence) x) => lowPriorityQuests.Any<(uint, ElementId, int)>(((uint ContentFinderConditionId, ElementId QuestId, int Sequence) y) => x.Quest == y.QuestId && x.Sequence == y.Sequence)); } if (_configuration.Advanced.AutoPrioritizeAlliedSocietyRankUp && list.Count > 0) { list.RemoveAll(((ElementId Quest, byte Sequence) x) => x.Quest is QuestId questId2 && _questFunctions.WouldOvercapAlliedSocietyReputation(questId2)); } if (list.Count == 0) { return null; } (ElementId, byte) tuple = list.First(); ElementId item = tuple.Item1; byte item2 = tuple.Item2; EAlliedSociety firstTrackedAlliedSociety = _alliedSocietyData.GetCommonAlliedSocietyTurnIn(item); if (firstTrackedAlliedSociety != EAlliedSociety.None) { List<(ElementId, byte)> list2 = (from quest in list.Skip(1) where _alliedSocietyData.GetCommonAlliedSocietyTurnIn(quest.Quest) == firstTrackedAlliedSociety select quest).ToList(); if (list2.Count > 0) { if (item2 == byte.MaxValue) { foreach (var (elementId2, b) in list2) { if (b != byte.MaxValue) { return QuestResolution.ForQuest(EQuestResolutionType.TrackedQuest, elementId2, b, msqState, $"Allied society quest needs completing before turn-in {elementId2}"); } } if (_configuration.Advanced.AutoPrioritizeAlliedSocietyRankUp) { List<(ElementId, byte)> list3 = new List<(ElementId, byte)> { (item, item2) }; list3.AddRange(list2); list3.Sort(delegate((ElementId Quest, byte Sequence) a, (ElementId Quest, byte Sequence) tuple5) { ushort num2 = (ushort)((a.Quest is QuestId questId2) ? _alliedSocietyQuestFunctions.GetQuestReputationReward(questId2) : 0); ushort value = (ushort)((tuple5.Quest is QuestId questId3) ? _alliedSocietyQuestFunctions.GetQuestReputationReward(questId3) : 0); return num2.CompareTo(value); }); foreach (var (elementId3, sequence) in list3) { if (elementId3 is QuestId questId && !_alliedSocietyQuestFunctions.WouldOvercapReputation(questId)) { return QuestResolution.ForQuest(EQuestResolutionType.TrackedQuest, elementId3, sequence, msqState, $"Allied society turn-in (cheapest first) {elementId3}"); } } QuestId rankUpQuestIfAtCapacity = _questFunctions.GetRankUpQuestIfAtCapacity(firstTrackedAlliedSociety); if (rankUpQuestIfAtCapacity != null && !_questFunctions.IsQuestComplete(rankUpQuestIfAtCapacity) && (_questFunctions.IsQuestAccepted(rankUpQuestIfAtCapacity) || _questFunctions.IsReadyToAcceptQuest(rankUpQuestIfAtCapacity))) { return QuestResolution.ForQuest(EQuestResolutionType.TrackedQuest, rankUpQuestIfAtCapacity, (byte)(_questFunctions.IsQuestAccepted(rankUpQuestIfAtCapacity) ? QuestManager.GetQuestSequence(rankUpQuestIfAtCapacity.Value) : 0), msqState, $"Allied society rank-up quest {rankUpQuestIfAtCapacity}"); } return QuestResolution.ForQuest(EQuestResolutionType.TrackedQuest, list3[0].Item1, list3[0].Item2, msqState, $"Allied society turn-in (all would overcap) {list3[0].Item1}"); } } else if (!_questFunctions.IsOnAlliedSocietyMount()) { list2.Insert(0, (item, item2)); _alliedSocietyData.GetCommonAlliedSocietyNpcs(firstTrackedAlliedSociety, out uint[] normalNpcs, out uint[] _); if (normalNpcs.Length != 0) { (ElementId, byte)? tuple4 = (from x in list2 where x.Sequence < byte.MaxValue where _questFunctions.IsInteractSequence(x.Quest, x.Sequence, normalNpcs) select x).Cast<(ElementId, byte)?>().FirstOrDefault(); if (tuple4.HasValue) { return QuestResolution.ForQuest(EQuestResolutionType.TrackedQuest, tuple4.Value.Item1, tuple4.Value.Item2, msqState, $"Allied society NPC interaction {tuple4.Value.Item1}"); } } } } } return QuestResolution.ForQuest(EQuestResolutionType.TrackedQuest, item, item2, msqState, $"Tracked quest {item}"); } private QuestResolution? TryResolveAutoPriority(MainScenarioQuestState msqState) { ElementId elementId = (from x in _questFunctions.GetNextPriorityQuestsThatCanBeAccepted() where x.IsAvailable select x.QuestId).FirstOrDefault(); if (elementId == null) { return null; } return QuestResolution.ForQuest(EQuestResolutionType.AutoPriority, elementId, QuestManager.GetQuestSequence(elementId.Value), msqState, $"Auto priority quest {elementId}"); } private QuestResolution? TryResolveMsqFallback(QuestReference msqQuest, bool allowNewMsq) { if (msqQuest.CurrentQuest == null || msqQuest.CurrentQuest.Value == 0) { return null; } if (_questFunctions.IsQuestAccepted(msqQuest.CurrentQuest)) { return null; } if (_configuration.General.MsqPriority == Configuration.EMsqPriorityMode.Manual) { return null; } if (!allowNewMsq) { return null; } return QuestResolution.ForQuest(EQuestResolutionType.MsqFallback, msqQuest.CurrentQuest, msqQuest.Sequence, msqQuest.State, $"MSQ fallback {msqQuest.CurrentQuest}"); } private QuestResolution? TryResolveStartingCity(MainScenarioQuestState msqState) { if (_clientState.TerritoryType == 181) { return QuestResolution.ForQuest(EQuestResolutionType.MsqImmediate, new QuestId(107), 0, MainScenarioQuestState.Available, "Starting city: Limsa Lominsa"); } if (_clientState.TerritoryType == 182) { return QuestResolution.ForQuest(EQuestResolutionType.MsqImmediate, new QuestId(594), 0, MainScenarioQuestState.Available, "Starting city: Ul'dah"); } if (_clientState.TerritoryType == 183) { return QuestResolution.ForQuest(EQuestResolutionType.MsqImmediate, new QuestId(39), 0, MainScenarioQuestState.Available, "Starting city: Gridania"); } return null; } private QuestResolution? TryResolveLevelingMode(int currentPlayerLevel, QuestController.EAutomationType automationType, MainScenarioQuestState msqState) { if (!ShouldStartLevelingMode(currentPlayerLevel, automationType)) { return null; } var (flag, _, targetLevel, questName) = _questFunctions.GetMsqLevelLockInfo(); if (!flag) { return null; } return QuestResolution.Leveling(targetLevel, questName, msqState); } public bool ShouldStartLevelingMode(int currentPlayerLevel, QuestController.EAutomationType automationType) { if (automationType != QuestController.EAutomationType.Automatic) { return false; } if (_condition[ConditionFlag.BoundByDuty] || _condition[ConditionFlag.InDutyQueue]) { return false; } if (!_autoDutyIpc.IsConfiguredToRunLevelingMode(currentPlayerLevel)) { return false; } return true; } }