Start available priority quests if no msq quest is available
authorLiza Carvelli <liza@carvel.li>
Sun, 11 Aug 2024 00:29:34 +0000 (02:29 +0200)
committerLiza Carvelli <liza@carvel.li>
Sun, 11 Aug 2024 00:29:34 +0000 (02:29 +0200)
15 files changed:
Questionable/Controller/GameUiController.cs
Questionable/Controller/QuestController.cs
Questionable/Controller/Steps/Interactions/AethernetShard.cs
Questionable/Controller/Steps/Interactions/Aetheryte.cs
Questionable/Controller/Steps/Shared/AethernetShortcut.cs
Questionable/Controller/Steps/Shared/AetheryteShortcut.cs
Questionable/Controller/Steps/Shared/SkipCondition.cs
Questionable/Data/QuestData.cs
Questionable/Functions/AetheryteFunctions.cs [new file with mode: 0644]
Questionable/Functions/GameFunctions.cs
Questionable/Functions/QuestFunctions.cs
Questionable/Model/QuestInfo.cs
Questionable/QuestionablePlugin.cs
Questionable/Windows/QuestComponents/ActiveQuestComponent.cs
Questionable/Windows/QuestComponents/QuestTooltipComponent.cs

index 5349bcf32a81e0d0618f7fccff79b343df330dc3..3a518a1b687b0bb16082be3bd32569665952a830 100644 (file)
@@ -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);
index 5619c11998ee4a15a6e42fefb1ab89280bdd60bd..140f3fa78b0d7900093bf91c5c4c7361b86dc3a4 100644 (file)
@@ -752,94 +752,23 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
         return currentStep?.AetheryteShortcut != null;
     }
 
-    private List<ElementId> GetPriorityQuests()
-    {
-        List<ElementId> 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;
 
+        ElementId? priorityQuestId = _questFunctions.GetNextPriorityQuestThatCanBeAccepted();
+        if (priorityQuestId == null)
+            return false;
+
         // don't start a second priority quest until the first one is resolved
-        List<ElementId> priorityQuests = GetPriorityQuests();
-        if (_startedQuest != null && priorityQuests.Contains(_startedQuest.Quest.Id))
+        if (_startedQuest != null && priorityQuestId == _startedQuest.Quest.Id)
             return false;
 
-        foreach (ElementId questId in priorityQuests)
+        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;
index 741d92abe1fd90316ebed95024e9a37ad44880dc..c1e14010f3d8e89bebf161fd9fa7def5c3c727a0 100644 (file)
@@ -25,7 +25,10 @@ internal static class AethernetShard
         }
     }
 
-    internal sealed class DoAttune(GameFunctions gameFunctions, ILogger<DoAttune> logger) : ITask
+    internal sealed class DoAttune(
+        AetheryteFunctions aetheryteFunctions,
+        GameFunctions gameFunctions,
+        ILogger<DoAttune> 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;
 
index c38d20cea448ef059ea1c8b5c7dedc74c060b736..4a47b7442fdd7cd661069b3f70b619f77ce52868 100644 (file)
@@ -24,7 +24,10 @@ internal static class Aetheryte
         }
     }
 
-    internal sealed class DoAttune(GameFunctions gameFunctions, ILogger<DoAttune> logger) : ITask
+    internal sealed class DoAttune(
+        AetheryteFunctions aetheryteFunctions,
+        GameFunctions gameFunctions,
+        ILogger<DoAttune> 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;
 
index 236f270c442e6e30e56680fc38c65c4801b2bdaf..2a639390e24ca2966473141e44a44bb79e682847 100644 (file)
@@ -32,7 +32,7 @@ internal static class AethernetShortcut
 
     internal sealed class UseAethernetShortcut(
         ILogger<UseAethernetShortcut> 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;
index cf6ecec0cf0e7d6046f13947b440eba2df65124a..b7b98d0a74c402ab267ebdc8f04898a4405b1eef 100644 (file)
@@ -17,7 +17,7 @@ internal static class AetheryteShortcut
 {
     internal sealed class Factory(
         IServiceProvider serviceProvider,
-        GameFunctions gameFunctions,
+        AetheryteFunctions aetheryteFunctions,
         AetheryteData aetheryteData) : ITaskFactory
     {
         public IEnumerable<ITask> 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<UseAetheryteShortcut> 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;
index d5e8e5d12a3c3818f9d937fdc32e42cd33d279f2..7abcbd81855ae3cf8295a47c5161dab525d6ee8a 100644 (file)
@@ -41,6 +41,7 @@ internal static class SkipCondition
 
     internal sealed class CheckSkip(
         ILogger<CheckSkip> 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;
index 3e00ba1a82a4e2efe8dae3d624a4b6c87e8ffeab..0a316facda81a68cf66d23f428fe63ecaad1c368 100644 (file)
@@ -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<IQuestInfo> GetAllByIssuerDataId(uint targetId)
     {
         return _quests.Values
diff --git a/Questionable/Functions/AetheryteFunctions.cs b/Questionable/Functions/AetheryteFunctions.cs
new file mode 100644 (file)
index 0000000..21f5a13
--- /dev/null
@@ -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<AetheryteFunctions> _logger;
+
+    public AetheryteFunctions(IServiceProvider serviceProvider, ILogger<AetheryteFunctions> 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<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);
+}
index 4b0b5e13a09b0c877d6a3844e9cbf314918b7a2f..e1c20069863157432fe17e275aed8234ee02b015 100644 (file)
@@ -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)
index 7c907f8368d231f3650ff6f3b7ca4fcb564d3d25..d1988668a6ad4b99ea1dd004706ffcc243124de7 100644 (file)
@@ -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<ElementId> GetPriorityQuestsThatCanBeAccepted()
+    {
+        List<ElementId> 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);
index 504c0e570297da3bfaf95c15460935b47c1eeb43..6dba304f191aa87a8b083eaded3c99766b19a15b 100644 (file)
@@ -64,7 +64,7 @@ internal sealed class QuestInfo : IQuestInfo
     public ushort Level { get; }
     public uint IssuerDataId { get; }
     public bool IsRepeatable { get; }
-    public ImmutableList<QuestId> PreviousQuests { get; }
+    public ImmutableList<QuestId> PreviousQuests { get; set; }
     public QuestJoin PreviousQuestJoin { get; }
     public ImmutableList<QuestId> 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];
+    }
 }
index 2e88cb19c9456f96195f9e12abe7faabc988c782..a8e5bfe96eb6feea36b527350daf990490b10466 100644 (file)
@@ -98,6 +98,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
 
     private static void AddBasicFunctionsAndData(ServiceCollection serviceCollection)
     {
+        serviceCollection.AddSingleton<AetheryteFunctions>();
         serviceCollection.AddSingleton<ExcelFunctions>();
         serviceCollection.AddSingleton<GameFunctions>();
         serviceCollection.AddSingleton<ChatFunctions>();
index 2e7b3e76a7400345613c6e4bf6dd3666c391d0cd..e3695a33cfe706bdd1c21c95fae6337cf35d4dbf 100644 (file)
@@ -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;
index df2a243bbd11d248d2a9b46842f6634bd38ed58a..65bcf24063c65836b71e729e8e76e10d753caf76 100644 (file)
@@ -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;
-
-                _uiUtils.ChecklistItem(FormatQuestUnlockName(qInfo), iconColor, icon);
-
-                if (qInfo is QuestInfo qstInfo && (counter <= 2 || icon != FontAwesomeIcon.Check))
-                    DrawQuestUnlocks(qstInfo, counter + 1);
+                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);
+
+                    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);
+                }
             }
         }