Add class quests as priority quests
authorLiza Carvelli <liza@carvel.li>
Sat, 10 Aug 2024 17:50:15 +0000 (19:50 +0200)
committerLiza Carvelli <liza@carvel.li>
Sat, 10 Aug 2024 17:50:15 +0000 (19:50 +0200)
Questionable/Controller/QuestController.cs
Questionable/Controller/Steps/IToastAware.cs [new file with mode: 0644]
Questionable/Controller/Steps/Interactions/EquipItem.cs
Questionable/Data/QuestData.cs
Questionable/Windows/QuestComponents/ARealmRebornComponent.cs

index e031d98845bf485e5c0030a7838fc5907a570ec9..420c236013e7103c739523a0d64e03199f7a6093 100644 (file)
@@ -4,11 +4,15 @@ using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using Dalamud.Game.ClientState.Conditions;
 using Dalamud.Game.ClientState.Keys;
+using Dalamud.Game.Text.SeStringHandling;
 using Dalamud.Plugin.Services;
 using FFXIVClientStructs.FFXIV.Client.Game;
+using LLib.GameData;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.Steps;
+using Questionable.Controller.Steps.Interactions;
 using Questionable.Controller.Steps.Shared;
+using Questionable.Data;
 using Questionable.External;
 using Questionable.Functions;
 using Questionable.Model;
@@ -16,7 +20,7 @@ using Questionable.Model.Questing;
 
 namespace Questionable.Controller;
 
-internal sealed class QuestController : MiniTaskController<QuestController>
+internal sealed class QuestController : MiniTaskController<QuestController>, IDisposable
 {
     private readonly IClientState _clientState;
     private readonly GameFunctions _gameFunctions;
@@ -27,6 +31,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
     private readonly QuestRegistry _questRegistry;
     private readonly IKeyState _keyState;
     private readonly ICondition _condition;
+    private readonly IToastGui _toastGui;
     private readonly Configuration _configuration;
     private readonly YesAlreadyIpc _yesAlreadyIpc;
     private readonly IReadOnlyList<ITaskFactory> _taskFactories;
@@ -64,6 +69,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
         IKeyState keyState,
         IChatGui chatGui,
         ICondition condition,
+        IToastGui toastGui,
         Configuration configuration,
         YesAlreadyIpc yesAlreadyIpc,
         IEnumerable<ITaskFactory> taskFactories)
@@ -78,11 +84,13 @@ internal sealed class QuestController : MiniTaskController<QuestController>
         _questRegistry = questRegistry;
         _keyState = keyState;
         _condition = condition;
+        _toastGui = toastGui;
         _configuration = configuration;
         _yesAlreadyIpc = yesAlreadyIpc;
         _taskFactories = taskFactories.ToList().AsReadOnly();
 
         _condition.ConditionChange += OnConditionChange;
+        _toastGui.ErrorToast += OnErrorToast;
     }
 
     public (QuestProgress Progress, ECurrentQuestType Type)? CurrentQuestDetails
@@ -216,6 +224,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
                     Stop("Pending quest accepted", continueIfAutomatic: true);
                 }
             }
+
             if (_simulatedQuest == null && _nextQuest != null)
             {
                 // if the quest is accepted, we no longer track it
@@ -231,7 +240,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
                         _nextQuest.Quest.Id);
 
                     // if (_nextQuest.Quest.Id is LeveId)
-                      //  _startedQuest = _nextQuest;
+                    //  _startedQuest = _nextQuest;
 
                     _nextQuest = null;
                 }
@@ -693,34 +702,86 @@ internal sealed class QuestController : MiniTaskController<QuestController>
             return false;
 
         QuestSequence? currentSequence = currentQuest.Quest.FindSequence(currentQuest.Sequence);
-        QuestStep? currentStep = currentSequence?.FindStep(currentQuest.Step);
+        if (currentQuest.Step > 0)
+            return false;
 
-        // TODO Should this check that all previous steps have CompletionFlags so that we avoid running to places
-        // no longer relevant for the non-priority quest (after we're done with the priority quest)?
+        QuestStep? currentStep = currentSequence?.FindStep(currentQuest.Step);
         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;
+        if (classJob != EClassJob.Adventurer)
+        {
+            priorityQuests.AddRange(_questRegistry.GetKnownClassJobQuests(classJob)
+                .Where(x => _questRegistry.TryGetQuest(x.QuestId, out Quest? quest) && quest.Info is QuestInfo
+                {
+                    // ignore Endwalker/Dawntrail, as the class quests are optional
+                    Expansion: EExpansionVersion.ARealmReborn or EExpansionVersion.Heavensward or EExpansionVersion.Stormblood or EExpansionVersion.Shadowbringers
+                })
+                .Select(x => x.QuestId));
+        }
+
+        return priorityQuests;
+    }
+
     public bool TryPickPriorityQuest()
     {
-        if (!IsInterruptible())
+        if (!IsInterruptible() || _nextQuest != null || _gatheringQuest != null || _simulatedQuest != null)
             return false;
 
-        ushort[] priorityQuests =
-        [
-            1157, // Garuda (Hard)
-            1158, // Titan (Hard)
-        ];
+        // 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))
+            return false;
 
-        foreach (var id in priorityQuests)
+        foreach (ElementId questId in priorityQuests)
         {
-            var questId = new QuestId(id);
-            if (_questFunctions.IsReadyToAcceptQuest(questId) && _questRegistry.TryGetQuest(questId, 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);
         }
 
         return false;
@@ -738,6 +799,18 @@ internal sealed class QuestController : MiniTaskController<QuestController>
             conditionChangeAware.OnConditionChange(flag, value);
     }
 
+    private void OnErrorToast(ref SeString message, ref bool ishandled)
+    {
+        if (_currentTask is IToastAware toastAware)
+            toastAware.OnErrorToast(message);
+    }
+
+    public void Dispose()
+    {
+        _toastGui.ErrorToast -= OnErrorToast;
+        _condition.ConditionChange -= OnConditionChange;
+    }
+
     public sealed record StepProgress(
         DateTime StartedAt,
         int PointMenuCounter = 0);
diff --git a/Questionable/Controller/Steps/IToastAware.cs b/Questionable/Controller/Steps/IToastAware.cs
new file mode 100644 (file)
index 0000000..aea7eaf
--- /dev/null
@@ -0,0 +1,8 @@
+using Dalamud.Game.Text.SeStringHandling;
+
+namespace Questionable.Controller.Steps;
+
+public interface IToastAware
+{
+    void OnErrorToast(SeString message);
+}
index 95aad266795fe2be5d912c9cf64c1a5a2d19db6e..3f3b48f45130cade81e315a4112ca21fd4f980cd 100644 (file)
@@ -1,11 +1,14 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using Dalamud.Game.Text.SeStringHandling;
 using Dalamud.Plugin.Services;
 using FFXIVClientStructs.FFXIV.Client.Game;
+using LLib;
 using Lumina.Excel.GeneratedSheets;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
+using Questionable.Functions;
 using Questionable.Model.Questing;
 using Quest = Questionable.Model.Quest;
 
@@ -26,8 +29,9 @@ internal static class EquipItem
         }
     }
 
-    internal sealed class DoEquip(IDataManager dataManager, ILogger<DoEquip> logger) : ITask
+    internal sealed class DoEquip(IDataManager dataManager, ILogger<DoEquip> logger) : ITask, IToastAware
     {
+        private const int MaxAttempts = 3;
         private static readonly IReadOnlyList<InventoryType> SourceInventoryTypes =
         [
             InventoryType.ArmoryMainHand,
@@ -98,7 +102,7 @@ internal static class EquipItem
         private unsafe void Equip()
         {
             ++_attempts;
-            if (_attempts > 3)
+            if (_attempts > MaxAttempts)
                 throw new TaskException("Unable to equip gear.");
 
             var inventoryManager = InventoryManager.Instance();
@@ -169,5 +173,12 @@ internal static class EquipItem
         }
 
         public override string ToString() => $"Equip({_item.Name})";
+
+        public void OnErrorToast(SeString message)
+        {
+            string? insufficientArmoryChestSpace = dataManager.GetString<LogMessage>(709, x => x.Text);
+            if (GameFunctions.GameStringEquals(message.TextValue, insufficientArmoryChestSpace))
+                _attempts = MaxAttempts;
+        }
     }
 }
index 10224d1acebb9e4c59775e6820c7f6c4e70c7678..8b961c817818c543e4a0123ea5ae2f79a84d3b47 100644 (file)
@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.Linq;
 using Dalamud.Plugin.Services;
+using LLib.GameData;
 using Lumina.Excel.GeneratedSheets;
 using Questionable.Model;
 using Questionable.Model.Questing;
@@ -11,6 +12,8 @@ namespace Questionable.Data;
 
 internal sealed class QuestData
 {
+    public static readonly IReadOnlyList<QuestId> CrystalTowerQuests =
+        [new(1709), new(1200), new(1201), new(1202), new(1203), new(1474), new(494), new(495)];
     private readonly Dictionary<ElementId, IQuestInfo> _quests;
 
     public QuestData(IDataManager dataManager)
index a94f253be7e2206fde0e0bf8efd41899764e0615..6d139a1632519c12e87d24de616095fea2d4feb3 100644 (file)
@@ -16,9 +16,6 @@ internal sealed class ARealmRebornComponent
     private static readonly QuestId GoodIntentions = new(363);
     private static readonly ushort[] RequiredPrimalInstances = [20004, 20006, 20005];
 
-    private static readonly QuestId[] RequiredAllianceRaidQuests =
-        [new(1709), new(1200), new(1201), new(1202), new(1203), new(1474), new(494), new(495)];
-
     private readonly QuestFunctions _questFunctions;
     private readonly QuestData _questData;
     private readonly TerritoryData _territoryData;
@@ -64,7 +61,7 @@ internal sealed class ARealmRebornComponent
 
     private void DrawAllianceRaids()
     {
-        bool complete = _questFunctions.IsQuestComplete(RequiredAllianceRaidQuests.Last());
+        bool complete = _questFunctions.IsQuestComplete(QuestData.CrystalTowerQuests[^1]);
         bool hover = _uiUtils.ChecklistItem("Crystal Tower Raids", complete);
         if (complete || !hover)
             return;
@@ -73,7 +70,7 @@ internal sealed class ARealmRebornComponent
         if (!tooltip)
             return;
 
-        foreach (var questId in RequiredAllianceRaidQuests)
+        foreach (var questId in QuestData.CrystalTowerQuests)
         {
             (Vector4 color, FontAwesomeIcon icon, _) = _uiUtils.GetQuestStyle(questId);
             _uiUtils.ChecklistItem(_questData.GetQuestInfo(questId).Name, color, icon);