From ada2cf28333341ccca2faa5ebac4194a6ec7e5ea Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Sun, 11 Aug 2024 02:29:34 +0200 Subject: [PATCH] Start available priority quests if no msq quest is available --- Questionable/Controller/GameUiController.cs | 9 +- Questionable/Controller/QuestController.cs | 89 ++------------- .../Steps/Interactions/AethernetShard.cs | 9 +- .../Steps/Interactions/Aetheryte.cs | 9 +- .../Steps/Shared/AethernetShortcut.cs | 10 +- .../Steps/Shared/AetheryteShortcut.cs | 14 +-- .../Controller/Steps/Shared/SkipCondition.cs | 7 +- Questionable/Data/QuestData.cs | 6 + Questionable/Functions/AetheryteFunctions.cs | 77 +++++++++++++ Questionable/Functions/GameFunctions.cs | 56 ---------- Questionable/Functions/QuestFunctions.cs | 105 ++++++++++++++++-- Questionable/Model/QuestInfo.cs | 7 +- Questionable/QuestionablePlugin.cs | 1 + .../QuestComponents/ActiveQuestComponent.cs | 3 - .../QuestComponents/QuestTooltipComponent.cs | 21 ++-- 15 files changed, 243 insertions(+), 180 deletions(-) create mode 100644 Questionable/Functions/AetheryteFunctions.cs diff --git a/Questionable/Controller/GameUiController.cs b/Questionable/Controller/GameUiController.cs index 5349bcf3..3a518a1b 100644 --- a/Questionable/Controller/GameUiController.cs +++ b/Questionable/Controller/GameUiController.cs @@ -8,7 +8,6 @@ using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.ClientState.Objects; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Event; -using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; @@ -31,8 +30,8 @@ internal sealed class GameUiController : IDisposable { private readonly IAddonLifecycle _addonLifecycle; private readonly IDataManager _dataManager; - private readonly GameFunctions _gameFunctions; private readonly QuestFunctions _questFunctions; + private readonly AetheryteFunctions _aetheryteFunctions; private readonly ExcelFunctions _excelFunctions; private readonly QuestController _questController; private readonly QuestRegistry _questRegistry; @@ -46,8 +45,8 @@ internal sealed class GameUiController : IDisposable public GameUiController( IAddonLifecycle addonLifecycle, IDataManager dataManager, - GameFunctions gameFunctions, QuestFunctions questFunctions, + AetheryteFunctions aetheryteFunctions, ExcelFunctions excelFunctions, QuestController questController, QuestRegistry questRegistry, @@ -60,8 +59,8 @@ internal sealed class GameUiController : IDisposable { _addonLifecycle = addonLifecycle; _dataManager = dataManager; - _gameFunctions = gameFunctions; _questFunctions = questFunctions; + _aetheryteFunctions = aetheryteFunctions; _excelFunctions = excelFunctions; _questController = questController; _questRegistry = questRegistry; @@ -570,7 +569,7 @@ internal sealed class GameUiController : IDisposable private unsafe bool HandleTravelYesNo(AddonSelectYesno* addonSelectYesno, QuestController.QuestProgress currentQuest, string actualPrompt) { - if (_gameFunctions.ReturnRequestedAt >= DateTime.Now.AddSeconds(-2) && _returnRegex.IsMatch(actualPrompt)) + if (_aetheryteFunctions.ReturnRequestedAt >= DateTime.Now.AddSeconds(-2) && _returnRegex.IsMatch(actualPrompt)) { _logger.LogInformation("Automatically confirming return..."); addonSelectYesno->AtkUnitBase.FireCallbackInt(0); diff --git a/Questionable/Controller/QuestController.cs b/Questionable/Controller/QuestController.cs index 5619c119..140f3fa7 100644 --- a/Questionable/Controller/QuestController.cs +++ b/Questionable/Controller/QuestController.cs @@ -752,94 +752,23 @@ internal sealed class QuestController : MiniTaskController, IDi return currentStep?.AetheryteShortcut != null; } - private List GetPriorityQuests() - { - List priorityQuests = - [ - new QuestId(1157), // Garuda (Hard) - new QuestId(1158), // Titan (Hard) - ..QuestData.CrystalTowerQuests - ]; - - EClassJob classJob = (EClassJob?)_clientState.LocalPlayer?.ClassJob.Id ?? EClassJob.Adventurer; - ushort[] shadowbringersRoleQuestChapters = QuestData.AllRoleQuestChapters.Select(x => x[0]).ToArray(); - if (classJob != EClassJob.Adventurer) - { - priorityQuests.AddRange(_questRegistry.GetKnownClassJobQuests(classJob) - .Where(x => - { - if (!_questRegistry.TryGetQuest(x.QuestId, out Quest? quest) || - quest.Info is not QuestInfo questInfo) - return false; - - // if no shadowbringers role quest is complete, (at least one) is required - if (shadowbringersRoleQuestChapters.Contains(questInfo.NewGamePlusChapter)) - return !QuestData.FinalShadowbringersRoleQuests.Any(_questFunctions.IsQuestComplete); - - // ignore all other role quests - if (QuestData.AllRoleQuestChapters.Any(y => y.Contains(questInfo.NewGamePlusChapter))) - return false; - - // even job quests for the later expacs (after role quests were introduced) might have skills locked - // behind them, e.g. reaper and sage - - return true; - }) - .Select(x => x.QuestId)); - } - - return priorityQuests; - } - public bool TryPickPriorityQuest() { if (!IsInterruptible() || _nextQuest != null || _gatheringQuest != null || _simulatedQuest != null) return false; - // don't start a second priority quest until the first one is resolved - List priorityQuests = GetPriorityQuests(); - if (_startedQuest != null && priorityQuests.Contains(_startedQuest.Quest.Id)) + ElementId? priorityQuestId = _questFunctions.GetNextPriorityQuestThatCanBeAccepted(); + if (priorityQuestId == null) return false; - foreach (ElementId questId in priorityQuests) + // don't start a second priority quest until the first one is resolved + if (_startedQuest != null && priorityQuestId == _startedQuest.Quest.Id) + return false; + + if (_questRegistry.TryGetQuest(priorityQuestId, out var quest)) { - if (!_questFunctions.IsReadyToAcceptQuest(questId) || !_questRegistry.TryGetQuest(questId, out var quest)) - continue; - - var firstStep = quest.FindSequence(0)?.FindStep(0); - if (firstStep == null) - continue; - - if (firstStep.AetheryteShortcut is { } aetheryteShortcut) - { - if (_gameFunctions.IsAetheryteUnlocked(aetheryteShortcut)) - { - _logger.LogInformation("Priority quest is accessible via aetheryte {Aetheryte}", aetheryteShortcut); - SetNextQuest(quest); - - _chatGui.Print( - $"[Questionable] Picking up quest '{quest.Info.Name}' as a priority over current main story/side quests."); - return true; - } - else - { - _logger.LogWarning("Ignoring priority quest {QuestId} / {QuestName}, aetheryte locked", quest.Id, - quest.Info.Name); - } - } - - if (firstStep is { InteractionType: EInteractionType.UseItem, ItemId: UseItem.VesperBayAetheryteTicket }) - { - _logger.LogInformation("Priority quest is accessible via vesper bay"); - SetNextQuest(quest); - - _chatGui.Print( - $"[Questionable] Picking up quest '{quest.Info.Name}' as a priority over current main story/side quests."); - return true; - } - else - _logger.LogTrace("Ignoring priority quest {QuestId} / {QuestName}, as we don't know how to get there", - questId, quest.Info.Name); + SetNextQuest(quest); + return true; } return false; diff --git a/Questionable/Controller/Steps/Interactions/AethernetShard.cs b/Questionable/Controller/Steps/Interactions/AethernetShard.cs index 741d92ab..c1e14010 100644 --- a/Questionable/Controller/Steps/Interactions/AethernetShard.cs +++ b/Questionable/Controller/Steps/Interactions/AethernetShard.cs @@ -25,7 +25,10 @@ internal static class AethernetShard } } - internal sealed class DoAttune(GameFunctions gameFunctions, ILogger logger) : ITask + internal sealed class DoAttune( + AetheryteFunctions aetheryteFunctions, + GameFunctions gameFunctions, + ILogger logger) : ITask { public EAetheryteLocation AetheryteLocation { get; set; } @@ -37,7 +40,7 @@ internal static class AethernetShard public bool Start() { - if (!gameFunctions.IsAetheryteUnlocked(AetheryteLocation)) + if (!aetheryteFunctions.IsAetheryteUnlocked(AetheryteLocation)) { logger.LogInformation("Attuning to aethernet shard {AethernetShard}", AetheryteLocation); gameFunctions.InteractWith((uint)AetheryteLocation, ObjectKind.Aetheryte); @@ -49,7 +52,7 @@ internal static class AethernetShard } public ETaskResult Update() => - gameFunctions.IsAetheryteUnlocked(AetheryteLocation) + aetheryteFunctions.IsAetheryteUnlocked(AetheryteLocation) ? ETaskResult.TaskComplete : ETaskResult.StillRunning; diff --git a/Questionable/Controller/Steps/Interactions/Aetheryte.cs b/Questionable/Controller/Steps/Interactions/Aetheryte.cs index c38d20ce..4a47b744 100644 --- a/Questionable/Controller/Steps/Interactions/Aetheryte.cs +++ b/Questionable/Controller/Steps/Interactions/Aetheryte.cs @@ -24,7 +24,10 @@ internal static class Aetheryte } } - internal sealed class DoAttune(GameFunctions gameFunctions, ILogger logger) : ITask + internal sealed class DoAttune( + AetheryteFunctions aetheryteFunctions, + GameFunctions gameFunctions, + ILogger logger) : ITask { public EAetheryteLocation AetheryteLocation { get; set; } @@ -36,7 +39,7 @@ internal static class Aetheryte public bool Start() { - if (!gameFunctions.IsAetheryteUnlocked(AetheryteLocation)) + if (!aetheryteFunctions.IsAetheryteUnlocked(AetheryteLocation)) { logger.LogInformation("Attuning to aetheryte {Aetheryte}", AetheryteLocation); gameFunctions.InteractWith((uint)AetheryteLocation); @@ -48,7 +51,7 @@ internal static class Aetheryte } public ETaskResult Update() => - gameFunctions.IsAetheryteUnlocked(AetheryteLocation) + aetheryteFunctions.IsAetheryteUnlocked(AetheryteLocation) ? ETaskResult.TaskComplete : ETaskResult.StillRunning; diff --git a/Questionable/Controller/Steps/Shared/AethernetShortcut.cs b/Questionable/Controller/Steps/Shared/AethernetShortcut.cs index 236f270c..2a639390 100644 --- a/Questionable/Controller/Steps/Shared/AethernetShortcut.cs +++ b/Questionable/Controller/Steps/Shared/AethernetShortcut.cs @@ -32,7 +32,7 @@ internal static class AethernetShortcut internal sealed class UseAethernetShortcut( ILogger logger, - GameFunctions gameFunctions, + AetheryteFunctions aetheryteFunctions, IClientState clientState, AetheryteData aetheryteData, LifestreamIpc lifestreamIpc, @@ -72,22 +72,22 @@ internal static class AethernetShortcut } if (SkipConditions.AetheryteLocked != null && - !gameFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteLocked.Value)) + !aetheryteFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteLocked.Value)) { logger.LogInformation("Skipping aethernet shortcut because the target aetheryte is locked"); return false; } if (SkipConditions.AetheryteUnlocked != null && - gameFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteUnlocked.Value)) + aetheryteFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteUnlocked.Value)) { logger.LogInformation("Skipping aethernet shortcut because the target aetheryte is unlocked"); return false; } } - if (gameFunctions.IsAetheryteUnlocked(From) && - gameFunctions.IsAetheryteUnlocked(To)) + if (aetheryteFunctions.IsAetheryteUnlocked(From) && + aetheryteFunctions.IsAetheryteUnlocked(To)) { ushort territoryType = clientState.TerritoryType; Vector3 playerPosition = clientState.LocalPlayer!.Position; diff --git a/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs b/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs index cf6ecec0..b7b98d0a 100644 --- a/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs +++ b/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs @@ -17,7 +17,7 @@ internal static class AetheryteShortcut { internal sealed class Factory( IServiceProvider serviceProvider, - GameFunctions gameFunctions, + AetheryteFunctions aetheryteFunctions, AetheryteData aetheryteData) : ITaskFactory { public IEnumerable CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) @@ -29,7 +29,7 @@ internal static class AetheryteShortcut .With(step, step.AetheryteShortcut.Value, aetheryteData.TerritoryIds[step.AetheryteShortcut.Value]); return [ - new WaitConditionTask(() => gameFunctions.CanTeleport(step.AetheryteShortcut.Value), "CanTeleport"), + new WaitConditionTask(() => aetheryteFunctions.CanTeleport(step.AetheryteShortcut.Value), "CanTeleport"), task ]; } @@ -40,7 +40,7 @@ internal static class AetheryteShortcut internal sealed class UseAetheryteShortcut( ILogger logger, - GameFunctions gameFunctions, + AetheryteFunctions aetheryteFunctions, IClientState clientState, IChatGui chatGui, AetheryteData aetheryteData) : ISkippableTask @@ -80,14 +80,14 @@ internal static class AetheryteShortcut } if (skipConditions.AetheryteLocked != null && - !gameFunctions.IsAetheryteUnlocked(skipConditions.AetheryteLocked.Value)) + !aetheryteFunctions.IsAetheryteUnlocked(skipConditions.AetheryteLocked.Value)) { logger.LogInformation("Skipping aetheryte teleport due to SkipCondition (AetheryteLocked)"); return false; } if (skipConditions.AetheryteUnlocked != null && - gameFunctions.IsAetheryteUnlocked(skipConditions.AetheryteUnlocked.Value)) + aetheryteFunctions.IsAetheryteUnlocked(skipConditions.AetheryteUnlocked.Value)) { logger.LogInformation("Skipping aetheryte teleport due to SkipCondition (AetheryteUnlocked)"); return false; @@ -124,12 +124,12 @@ internal static class AetheryteShortcut } } - if (!gameFunctions.IsAetheryteUnlocked(TargetAetheryte)) + if (!aetheryteFunctions.IsAetheryteUnlocked(TargetAetheryte)) { chatGui.PrintError($"[Questionable] Aetheryte {TargetAetheryte} is not unlocked."); throw new TaskException("Aetheryte is not unlocked"); } - else if (gameFunctions.TeleportAetheryte(TargetAetheryte)) + else if (aetheryteFunctions.TeleportAetheryte(TargetAetheryte)) { logger.LogInformation("Travelling via aetheryte..."); return true; diff --git a/Questionable/Controller/Steps/Shared/SkipCondition.cs b/Questionable/Controller/Steps/Shared/SkipCondition.cs index d5e8e5d1..7abcbd81 100644 --- a/Questionable/Controller/Steps/Shared/SkipCondition.cs +++ b/Questionable/Controller/Steps/Shared/SkipCondition.cs @@ -41,6 +41,7 @@ internal static class SkipCondition internal sealed class CheckSkip( ILogger logger, + AetheryteFunctions aetheryteFunctions, GameFunctions gameFunctions, QuestFunctions questFunctions, IClientState clientState) : ITask @@ -145,21 +146,21 @@ internal static class SkipCondition DataId: not null, InteractionType: EInteractionType.AttuneAetheryte or EInteractionType.AttuneAethernetShard } && - gameFunctions.IsAetheryteUnlocked((EAetheryteLocation)Step.DataId.Value)) + aetheryteFunctions.IsAetheryteUnlocked((EAetheryteLocation)Step.DataId.Value)) { logger.LogInformation("Skipping step, as aetheryte/aethernet shard is unlocked"); return true; } if (SkipConditions.AetheryteLocked != null && - !gameFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteLocked.Value)) + !aetheryteFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteLocked.Value)) { logger.LogInformation("Skipping step, as aetheryte is locked"); return true; } if (SkipConditions.AetheryteUnlocked != null && - gameFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteUnlocked.Value)) + aetheryteFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteUnlocked.Value)) { logger.LogInformation("Skipping step, as aetheryte is unlocked"); return true; diff --git a/Questionable/Data/QuestData.cs b/Questionable/Data/QuestData.cs index 3e00ba1a..0a316fac 100644 --- a/Questionable/Data/QuestData.cs +++ b/Questionable/Data/QuestData.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Dalamud.Plugin.Services; using LLib.GameData; @@ -63,6 +64,11 @@ internal sealed class QuestData return _quests[elementId] ?? throw new ArgumentOutOfRangeException(nameof(elementId)); } + public bool TryGetQuestInfo(ElementId elementId, [NotNullWhen(true)] out IQuestInfo? questInfo) + { + return _quests.TryGetValue(elementId, out questInfo); + } + public List GetAllByIssuerDataId(uint targetId) { return _quests.Values diff --git a/Questionable/Functions/AetheryteFunctions.cs b/Questionable/Functions/AetheryteFunctions.cs new file mode 100644 index 00000000..21f5a13e --- /dev/null +++ b/Questionable/Functions/AetheryteFunctions.cs @@ -0,0 +1,77 @@ +using System; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.UI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Questionable.Model.Common; +using Questionable.Model.Questing; + +namespace Questionable.Functions; + +internal sealed unsafe class AetheryteFunctions +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + public AetheryteFunctions(IServiceProvider serviceProvider, ILogger logger) + { + _serviceProvider = serviceProvider; + _logger = logger; + } + + public DateTime ReturnRequestedAt { get; set; } = DateTime.MinValue; + + public bool IsAetheryteUnlocked(uint aetheryteId, out byte subIndex) + { + subIndex = 0; + + var uiState = UIState.Instance(); + return uiState != null && uiState->IsAetheryteUnlocked(aetheryteId); + } + + public bool IsAetheryteUnlocked(EAetheryteLocation aetheryteLocation) + { + if (aetheryteLocation == EAetheryteLocation.IshgardFirmament) + return _serviceProvider.GetRequiredService().IsQuestComplete(new QuestId(3672)); + return IsAetheryteUnlocked((uint)aetheryteLocation, out _); + } + + public bool CanTeleport(EAetheryteLocation aetheryteLocation) + { + if ((ushort)aetheryteLocation == PlayerState.Instance()->HomeAetheryteId && + ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 8) == 0) + return true; + + return ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0; + } + + public bool TeleportAetheryte(uint aetheryteId) + { + _logger.LogDebug("Attempting to teleport to aetheryte {AetheryteId}", aetheryteId); + if (IsAetheryteUnlocked(aetheryteId, out var subIndex)) + { + if (aetheryteId == PlayerState.Instance()->HomeAetheryteId && + ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 8) == 0) + { + ReturnRequestedAt = DateTime.Now; + if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 8)) + { + _logger.LogInformation("Using 'return' for home aetheryte"); + return true; + } + } + + if (ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0) + { + // fallback if return isn't available or (more likely) on a different aetheryte + _logger.LogInformation("Teleporting to aetheryte {AetheryteId}", aetheryteId); + return Telepo.Instance()->Teleport(aetheryteId, subIndex); + } + } + + return false; + } + + public bool TeleportAetheryte(EAetheryteLocation aetheryteLocation) + => TeleportAetheryte((uint)aetheryteLocation); +} diff --git a/Questionable/Functions/GameFunctions.cs b/Questionable/Functions/GameFunctions.cs index 4b0b5e13..e1c20069 100644 --- a/Questionable/Functions/GameFunctions.cs +++ b/Questionable/Functions/GameFunctions.cs @@ -74,62 +74,6 @@ internal sealed unsafe class GameFunctions .AsReadOnly(); } - public DateTime ReturnRequestedAt { get; set; } = DateTime.MinValue; - - public bool IsAetheryteUnlocked(uint aetheryteId, out byte subIndex) - { - subIndex = 0; - - var uiState = UIState.Instance(); - return uiState != null && uiState->IsAetheryteUnlocked(aetheryteId); - } - - public bool IsAetheryteUnlocked(EAetheryteLocation aetheryteLocation) - { - if (aetheryteLocation == EAetheryteLocation.IshgardFirmament) - return _questFunctions.IsQuestComplete(new QuestId(3672)); - return IsAetheryteUnlocked((uint)aetheryteLocation, out _); - } - - public bool CanTeleport(EAetheryteLocation aetheryteLocation) - { - if ((ushort)aetheryteLocation == PlayerState.Instance()->HomeAetheryteId && - ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 8) == 0) - return true; - - return ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0; - } - - public bool TeleportAetheryte(uint aetheryteId) - { - _logger.LogDebug("Attempting to teleport to aetheryte {AetheryteId}", aetheryteId); - if (IsAetheryteUnlocked(aetheryteId, out var subIndex)) - { - if (aetheryteId == PlayerState.Instance()->HomeAetheryteId && - ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 8) == 0) - { - ReturnRequestedAt = DateTime.Now; - if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 8)) - { - _logger.LogInformation("Using 'return' for home aetheryte"); - return true; - } - } - - if (ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0) - { - // fallback if return isn't available or (more likely) on a different aetheryte - _logger.LogInformation("Teleporting to aetheryte {AetheryteId}", aetheryteId); - return Telepo.Instance()->Teleport(aetheryteId, subIndex); - } - } - - return false; - } - - public bool TeleportAetheryte(EAetheryteLocation aetheryteLocation) - => TeleportAetheryte((uint)aetheryteLocation); - public bool IsFlyingUnlocked(ushort territoryId) { if (_configuration.Advanced.NeverFly) diff --git a/Questionable/Functions/QuestFunctions.cs b/Questionable/Functions/QuestFunctions.cs index 7c907f83..d1988668 100644 --- a/Questionable/Functions/QuestFunctions.cs +++ b/Questionable/Functions/QuestFunctions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Dalamud.Memory; @@ -12,8 +13,10 @@ using LLib.GameData; using LLib.GameUI; using Lumina.Excel.GeneratedSheets; using Questionable.Controller; +using Questionable.Controller.Steps.Interactions; using Questionable.Data; using Questionable.Model; +using Questionable.Model.Common; using Questionable.Model.Questing; using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany; using Quest = Questionable.Model.Quest; @@ -24,16 +27,24 @@ internal sealed unsafe class QuestFunctions { private readonly QuestRegistry _questRegistry; private readonly QuestData _questData; + private readonly AetheryteFunctions _aetheryteFunctions; private readonly Configuration _configuration; private readonly IDataManager _dataManager; private readonly IClientState _clientState; private readonly IGameGui _gameGui; - public QuestFunctions(QuestRegistry questRegistry, QuestData questData, Configuration configuration, - IDataManager dataManager, IClientState clientState, IGameGui gameGui) + public QuestFunctions( + QuestRegistry questRegistry, + QuestData questData, + AetheryteFunctions aetheryteFunctions, + Configuration configuration, + IDataManager dataManager, + IClientState clientState, + IGameGui gameGui) { _questRegistry = questRegistry; _questData = questData; + _aetheryteFunctions = aetheryteFunctions; _configuration = configuration; _dataManager = dataManager; _clientState = clientState; @@ -99,8 +110,11 @@ internal sealed unsafe class QuestFunctions { // always prioritize accepting MSQ quests, to make sure we don't turn in one MSQ quest and then go off to do // side quests until the end of time. - var msqQuest = GetMainScenarioQuest(questManager); - if (msqQuest.CurrentQuest is { Value: not 0 } && _questRegistry.IsKnownQuest(msqQuest.CurrentQuest)) + var msqQuest = GetMainScenarioQuest(); + if (msqQuest.CurrentQuest != null && !_questRegistry.IsKnownQuest(msqQuest.CurrentQuest)) + msqQuest = default; + + if (msqQuest.CurrentQuest != null && !IsQuestAccepted(msqQuest.CurrentQuest)) return msqQuest; // Use the quests in the same order as they're shown in the to-do list, e.g. if the MSQ is the first item, @@ -133,14 +147,24 @@ internal sealed unsafe class QuestFunctions return (currentQuest, QuestManager.GetQuestSequence(currentQuest.Value)); } - // if we know no quest of those currently in the to-do list, just do MSQ - return msqQuest; + ElementId? priorityQuest = GetNextPriorityQuestThatCanBeAccepted(); + if (priorityQuest != null) + { + // if we have an accepted msq quest, and know of no quest of those currently in the to-do list... + // (1) try and find a priority quest to do + return (priorityQuest, QuestManager.GetQuestSequence(priorityQuest.Value)); + } + else if (msqQuest.CurrentQuest != null) + { + // (2) just do a normal msq quest + return msqQuest; + } } return default; } - private (QuestId? CurrentQuest, byte Sequence) GetMainScenarioQuest(QuestManager* questManager) + private (QuestId? CurrentQuest, byte Sequence) GetMainScenarioQuest() { if (QuestManager.IsQuestComplete(3759)) // Memories Rekindled { @@ -177,6 +201,7 @@ internal sealed unsafe class QuestFunctions return default; // if the MSQ is hidden, we generally ignore it + QuestManager* questManager = QuestManager.Instance(); if (IsQuestAccepted(currentQuest) && questManager->GetQuestById(currentQuest.Value)->IsHidden) return default; @@ -214,6 +239,72 @@ internal sealed unsafe class QuestFunctions return null; } + public ElementId? GetNextPriorityQuestThatCanBeAccepted() + { + return GetPriorityQuestsThatCanBeAccepted() + .FirstOrDefault(x => + { + if (!_questRegistry.TryGetQuest(x, out Quest? quest)) + return false; + + var firstStep = quest.FindSequence(0)?.FindStep(0); + if (firstStep == null) + return false; + + if (firstStep.AetheryteShortcut is { } aetheryteShortcut && + _aetheryteFunctions.IsAetheryteUnlocked(aetheryteShortcut)) + return true; + + if (firstStep is + { InteractionType: EInteractionType.UseItem, ItemId: UseItem.VesperBayAetheryteTicket }) + return true; + + return false; + }); + } + + private List GetPriorityQuestsThatCanBeAccepted() + { + List priorityQuests = + [ + new QuestId(1157), // Garuda (Hard) + new QuestId(1158), // Titan (Hard) + ..QuestData.CrystalTowerQuests + ]; + + EClassJob classJob = (EClassJob?)_clientState.LocalPlayer?.ClassJob.Id ?? EClassJob.Adventurer; + ushort[] shadowbringersRoleQuestChapters = QuestData.AllRoleQuestChapters.Select(x => x[0]).ToArray(); + if (classJob != EClassJob.Adventurer) + { + priorityQuests.AddRange(_questRegistry.GetKnownClassJobQuests(classJob) + .Where(x => + { + if (!_questRegistry.TryGetQuest(x.QuestId, out Quest? quest) || + quest.Info is not QuestInfo questInfo) + return false; + + // if no shadowbringers role quest is complete, (at least one) is required + if (shadowbringersRoleQuestChapters.Contains(questInfo.NewGamePlusChapter)) + return !QuestData.FinalShadowbringersRoleQuests.Any(IsQuestComplete); + + // ignore all other role quests + if (QuestData.AllRoleQuestChapters.Any(y => y.Contains(questInfo.NewGamePlusChapter))) + return false; + + // even job quests for the later expacs (after role quests were introduced) might have skills locked + // behind them, e.g. reaper and sage + + return true; + }) + .Select(x => x.QuestId)); + } + + return priorityQuests + .Where(_questRegistry.IsKnownQuest) + .Where(IsReadyToAcceptQuest) + .ToList(); + } + public bool IsReadyToAcceptQuest(ElementId questId) { _questRegistry.TryGetQuest(questId, out var quest); diff --git a/Questionable/Model/QuestInfo.cs b/Questionable/Model/QuestInfo.cs index 504c0e57..6dba304f 100644 --- a/Questionable/Model/QuestInfo.cs +++ b/Questionable/Model/QuestInfo.cs @@ -64,7 +64,7 @@ internal sealed class QuestInfo : IQuestInfo public ushort Level { get; } public uint IssuerDataId { get; } public bool IsRepeatable { get; } - public ImmutableList PreviousQuests { get; } + public ImmutableList PreviousQuests { get; set; } public QuestJoin PreviousQuestJoin { get; } public ImmutableList QuestLocks { get; } public QuestJoin QuestLockJoin { get; } @@ -88,4 +88,9 @@ internal sealed class QuestInfo : IQuestInfo All = 1, AtLeastOne = 2, } + + public void AddPreviousQuest(QuestId questId) + { + PreviousQuests = [..PreviousQuests, questId]; + } } diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index 2e88cb19..a8e5bfe9 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -98,6 +98,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin private static void AddBasicFunctionsAndData(ServiceCollection serviceCollection) { + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs b/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs index 2e7b3e76..e3695a33 100644 --- a/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs +++ b/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs @@ -25,7 +25,6 @@ internal sealed class ActiveQuestComponent private readonly GatheringController _gatheringController; private readonly QuestFunctions _questFunctions; private readonly ICommandManager _commandManager; - private readonly IDalamudPluginInterface _pluginInterface; private readonly Configuration _configuration; private readonly QuestRegistry _questRegistry; private readonly IChatGui _chatGui; @@ -37,7 +36,6 @@ internal sealed class ActiveQuestComponent GatheringController gatheringController, QuestFunctions questFunctions, ICommandManager commandManager, - IDalamudPluginInterface pluginInterface, Configuration configuration, QuestRegistry questRegistry, IChatGui chatGui) @@ -48,7 +46,6 @@ internal sealed class ActiveQuestComponent _gatheringController = gatheringController; _questFunctions = questFunctions; _commandManager = commandManager; - _pluginInterface = pluginInterface; _configuration = configuration; _questRegistry = questRegistry; _chatGui = chatGui; diff --git a/Questionable/Windows/QuestComponents/QuestTooltipComponent.cs b/Questionable/Windows/QuestComponents/QuestTooltipComponent.cs index df2a243b..65bcf240 100644 --- a/Questionable/Windows/QuestComponents/QuestTooltipComponent.cs +++ b/Questionable/Windows/QuestComponents/QuestTooltipComponent.cs @@ -94,15 +94,22 @@ internal sealed class QuestTooltipComponent foreach (var q in quest.PreviousQuests) { - var qInfo = _questData.GetQuestInfo(q); - var (iconColor, icon, _) = _uiUtils.GetQuestStyle(q); - if (!_questRegistry.IsKnownQuest(qInfo.QuestId)) - iconColor = ImGuiColors.DalamudGrey; + if (_questData.TryGetQuestInfo(q, out var qInfo)) + { + var (iconColor, icon, _) = _uiUtils.GetQuestStyle(q); + if (!_questRegistry.IsKnownQuest(qInfo.QuestId)) + iconColor = ImGuiColors.DalamudGrey; - _uiUtils.ChecklistItem(FormatQuestUnlockName(qInfo), iconColor, icon); + _uiUtils.ChecklistItem(FormatQuestUnlockName(qInfo), iconColor, icon); - if (qInfo is QuestInfo qstInfo && (counter <= 2 || icon != FontAwesomeIcon.Check)) - DrawQuestUnlocks(qstInfo, counter + 1); + if (qInfo is QuestInfo qstInfo && (counter <= 2 || icon != FontAwesomeIcon.Check)) + DrawQuestUnlocks(qstInfo, counter + 1); + } + else + { + using var _ = ImRaii.Disabled(); + _uiUtils.ChecklistItem($"Unknown Quest ({q})", ImGuiColors.DalamudGrey, FontAwesomeIcon.Question); + } } }