Add config to skip aether currents/class quests/primals/crystal tower
authorLiza Carvelli <liza@carvel.li>
Sun, 13 Apr 2025 09:37:44 +0000 (11:37 +0200)
committerLiza Carvelli <liza@carvel.li>
Sun, 13 Apr 2025 09:37:44 +0000 (11:37 +0200)
QuestPaths/2.x - A Realm Reborn/MSQ-2/E6-2.55/4591_The Steps of Faith.json
Questionable.sln.DotSettings
Questionable/Configuration.cs
Questionable/Controller/QuestController.cs
Questionable/Controller/Steps/Shared/SkipCondition.cs
Questionable/Data/QuestData.cs
Questionable/Functions/QuestFunctions.cs
Questionable/Windows/ConfigComponents/DebugConfigComponent.cs
Questionable/Windows/QuestComponents/ARealmRebornComponent.cs
Questionable/Windows/UiUtils.cs

index e1d3d9e..1c91e8f 100644 (file)
@@ -31,9 +31,8 @@
           "InteractionType": "SinglePlayerDuty",
           "SinglePlayerDutyOptions": {
             "Enabled": false,
-            "TestedBossModVersion": "0.0.0.292",
+            "TestedBossModVersion": "0.1.2.4",
             "Notes": [
-              "WIP: Needs to be re-tested",
               "AI doesn't move after starting the instance, so enemies won't be triggered",
               "(First Barrier) If the player is too far south, after being stunned by Vishap's roar, AI doesn't move out of the AOE and dies to the Cauterize"
             ]
index e598240..32253a9 100644 (file)
@@ -30,6 +30,7 @@
        <s:Boolean x:Key="/Default/UserDictionary/Words/=orchestrion/@EntryIndexedValue">True</s:Boolean>
        <s:Boolean x:Key="/Default/UserDictionary/Words/=ostall/@EntryIndexedValue">True</s:Boolean>
        <s:Boolean x:Key="/Default/UserDictionary/Words/=palaka_0027s/@EntryIndexedValue">True</s:Boolean>
+       <s:Boolean x:Key="/Default/UserDictionary/Words/=primals/@EntryIndexedValue">True</s:Boolean>
        <s:Boolean x:Key="/Default/UserDictionary/Words/=rostra/@EntryIndexedValue">True</s:Boolean>
        <s:Boolean x:Key="/Default/UserDictionary/Words/=shaaloani/@EntryIndexedValue">True</s:Boolean>
        <s:Boolean x:Key="/Default/UserDictionary/Words/=sheshenewezi/@EntryIndexedValue">True</s:Boolean>
index ef4eb11..be772a5 100644 (file)
@@ -72,6 +72,10 @@ internal sealed class Configuration : IPluginConfiguration
         public bool NeverFly { get; set; }
         public bool AdditionalStatusInformation { get; set; }
         public bool DisableAutoDutyBareMode { get; set; }
+        public bool SkipAetherCurrents { get; set; }
+        public bool SkipClassJobQuests { get; set; }
+        public bool SkipARealmRebornHardModePrimals { get; set; }
+        public bool SkipCrystalTowerRaids { get; set; }
     }
 
     internal enum ECombatModule
index e67e36e..c0080f2 100644 (file)
@@ -14,6 +14,7 @@ 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;
@@ -812,7 +813,8 @@ internal sealed class QuestController : MiniTaskController<QuestController>
             return false;
 
         // "ifrit bleeds, we can kill it" isn't listed as priority quest, as we accept it during the MSQ 'Moving On'
-        if (currentQuest.Quest.Id is QuestId { Value: 1048 })
+        // the rest are priority quests, but that's fine here
+        if (QuestData.HardModePrimals.Contains(currentQuest.Quest.Id))
             return false;
 
         if (currentQuest.Quest.Info.AlliedSociety != EAlliedSociety.None)
index 1674792..e799df8 100644 (file)
@@ -51,6 +51,7 @@ internal static class SkipCondition
 
     internal sealed class CheckSkip(
         ILogger<CheckSkip> logger,
+        Configuration configuration,
         AetheryteFunctions aetheryteFunctions,
         GameFunctions gameFunctions,
         QuestFunctions questFunctions,
@@ -94,7 +95,7 @@ internal static class SkipCondition
             if (CheckAetheryteCondition(step, skipConditions))
                 return true;
 
-            if (CheckAethernetCondition(step))
+            if (CheckAetherCurrentCondition(step))
                 return true;
 
             if (CheckQuestWorkConditions(elementId, step))
@@ -297,7 +298,7 @@ internal static class SkipCondition
             return false;
         }
 
-        private bool CheckAethernetCondition(QuestStep step)
+        private bool CheckAetherCurrentCondition(QuestStep step)
         {
             if (step is { DataId: not null, InteractionType: EInteractionType.AttuneAetherCurrent } &&
                 gameFunctions.IsAetherCurrentUnlocked(step.DataId.Value))
@@ -306,6 +307,13 @@ internal static class SkipCondition
                 return true;
             }
 
+            if (step is { InteractionType: EInteractionType.AttuneAetherCurrent } &&
+                configuration.Advanced.SkipAetherCurrents)
+            {
+                logger.LogInformation("Skipping step, as aether currents should be skipped");
+                return true;
+            }
+
             return false;
         }
 
@@ -417,6 +425,22 @@ internal static class SkipCondition
                 return true;
             }
 
+            if (step.PickUpQuestId != null &&
+                configuration.Advanced.SkipAetherCurrents &&
+                QuestData.AetherCurrentQuests.Contains(step.PickUpQuestId))
+            {
+                logger.LogInformation("Skipping step, as aether current quests should be skipped");
+                return true;
+            }
+
+            if (step.PickUpQuestId != null &&
+                configuration.Advanced.SkipARealmRebornHardModePrimals &&
+                QuestData.HardModePrimals.Contains(step.PickUpQuestId))
+            {
+                logger.LogInformation("Skipping step, as hard mode primal quests should be skipped");
+                return true;
+            }
+
             return false;
         }
 
index c8c41ff..19f457f 100644 (file)
@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Collections.Immutable;
+using System.Collections.ObjectModel;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using Dalamud.Plugin.Services;
@@ -16,22 +17,71 @@ namespace Questionable.Data;
 
 internal sealed class QuestData
 {
+    public static readonly IReadOnlyList<QuestId> HardModePrimals = [new(1048), new(1157), new(1158)];
+
     public static readonly IReadOnlyList<QuestId> CrystalTowerQuests =
         [new(1709), new(1200), new(1201), new(1202), new(1203), new(1474), new(494), new(495)];
 
-    public static readonly IReadOnlyList<uint> TankRoleQuests = [136, 154, 178];
-    public static readonly IReadOnlyList<uint> HealerRoleQuests = [137, 155, 179];
-    public static readonly IReadOnlyList<uint> MeleeRoleQuests = [138, 156, 180];
-    public static readonly IReadOnlyList<uint> PhysicalRangedRoleQuests = [138, 157, 181];
-    public static readonly IReadOnlyList<uint> CasterRoleQuests = [139, 158, 182];
+    public static readonly ImmutableDictionary<uint, ImmutableList<QuestId>> AetherCurrentQuestsByTerritory =
+        new Dictionary<uint, List<ushort>>
+            {
+                // Heavensward
+                { 397, [1744, 1759, 1760, 2111] },
+                { 398, [1771, 1790, 1797, 1802] },
+                { 399, [1936, 1945, 1963, 1966] },
+                { 400, [1819, 1823, 1828, 1835] },
+                { 401, [1748, 1874, 1909, 1910] },
+
+                // Stormblood
+                { 612, [2639, 2661, 2816, 2821] },
+                { 613, [2632, 2673, 2687, 2693] },
+                { 614, [2724, 2728, 2730, 2733] },
+                { 620, [2655, 2842, 2851, 2860] },
+                { 621, [2877, 2880, 2881, 2883] },
+                { 622, [2760, 2771, 2782, 2791] },
+
+                // Shadowbringers
+                { 813, [3380, 3384, 3385, 3386] },
+                { 814, [3360, 3371, 3537, 3556] },
+                { 815, [3375, 3503, 3511, 3525] },
+                { 816, [3395, 3398, 3404, 3427] },
+                { 817, [3444, 3467, 3478, 3656] },
+                { 818, [3588, 3592, 3593, 3594] },
+
+                // Endwalker
+                { 956, [4320, 4329, 4480, 4484] },
+                { 957, [4203, 4257, 4259, 4489] },
+                { 958, [4216, 4232, 4498, 4502] },
+                { 959, [4240, 4241, 4253, 4516] },
+                { 960, [4342, 4346, 4354, 4355] },
+                { 961, [4288, 4313, 4507, 4511] },
+
+                // Dawntrail
+                {1187, [5039, 5047, 5051, 5055]},
+                {1188, [5064, 5074, 5081, 5085]},
+                {1189, [5094, 5103, 5110, 5114]},
+                {1190, [5130, 5138, 5140, 5144]},
+                {1191, [5153, 5156, 5159, 5160]},
+                {1192, [5174, 5176, 5178, 5179]},
+            }
+            .ToImmutableDictionary(x => x.Key, x => x.Value.Select(y => new QuestId(y)).ToImmutableList());
+
+    public static ImmutableHashSet<QuestId> AetherCurrentQuests { get; } =
+        AetherCurrentQuestsByTerritory.Values.SelectMany(x => x).ToImmutableHashSet();
+
+    private static readonly IReadOnlyList<uint> TankRoleQuestChapters = [136, 154, 178];
+    private static readonly IReadOnlyList<uint> HealerRoleQuestChapters = [137, 155, 179];
+    private static readonly IReadOnlyList<uint> MeleeRoleQuestChapters = [138, 156, 180];
+    private static readonly IReadOnlyList<uint> PhysicalRangedRoleQuestChapters = [138, 157, 181];
+    private static readonly IReadOnlyList<uint> CasterRoleQuestChapters = [139, 158, 182];
 
     public static readonly IReadOnlyList<IReadOnlyList<uint>> AllRoleQuestChapters =
     [
-        TankRoleQuests,
-        HealerRoleQuests,
-        MeleeRoleQuests,
-        PhysicalRangedRoleQuests,
-        CasterRoleQuests
+        TankRoleQuestChapters,
+        HealerRoleQuestChapters,
+        MeleeRoleQuestChapters,
+        PhysicalRangedRoleQuestChapters,
+        CasterRoleQuestChapters
     ];
 
     public static readonly IReadOnlyList<QuestId> FinalShadowbringersRoleQuests =
@@ -383,11 +433,11 @@ internal sealed class QuestData
     {
         return classJob switch
         {
-            _ when classJob.IsTank() => TankRoleQuests,
-            _ when classJob.IsHealer() => HealerRoleQuests,
-            _ when classJob.IsMelee() => MeleeRoleQuests,
-            _ when classJob.IsPhysicalRanged() => PhysicalRangedRoleQuests,
-            _ when classJob.IsCaster() && classJob != EClassJob.BlueMage => CasterRoleQuests,
+            _ when classJob.IsTank() => TankRoleQuestChapters,
+            _ when classJob.IsHealer() => HealerRoleQuestChapters,
+            _ when classJob.IsMelee() => MeleeRoleQuestChapters,
+            _ when classJob.IsPhysicalRanged() => PhysicalRangedRoleQuestChapters,
+            _ when classJob.IsCaster() && classJob != EClassJob.BlueMage => CasterRoleQuestChapters,
             _ => []
         };
     }
index fcb107b..373019d 100644 (file)
@@ -441,36 +441,46 @@ internal sealed unsafe class QuestFunctions
         List<ElementId> priorityQuests = [];
         if (!onlyClassAndRoleQuests)
         {
-            priorityQuests.Add(new QuestId(1157)); // Garuda (Hard)
-            priorityQuests.Add(new QuestId(1158)); // Titan (Hard)
-            priorityQuests.AddRange(QuestData.CrystalTowerQuests);
+            if (!_configuration.Advanced.SkipARealmRebornHardModePrimals)
+            {
+                // Ifrit quest is handled as a pickup during MSQ
+                priorityQuests.AddRange(QuestData.HardModePrimals.Skip(1));
+            }
+
+            if (!_configuration.Advanced.SkipCrystalTowerRaids)
+            {
+                priorityQuests.AddRange(QuestData.CrystalTowerQuests);
+            }
         }
 
-        EClassJob classJob = (EClassJob?)_clientState.LocalPlayer?.ClassJob.RowId ?? EClassJob.Adventurer;
-        uint[] shadowbringersRoleQuestChapters = QuestData.AllRoleQuestChapters.Select(x => x[0]).ToArray();
-        if (classJob != EClassJob.Adventurer)
+        if (!_configuration.Advanced.SkipClassJobQuests)
         {
-            priorityQuests.AddRange(_questRegistry.GetKnownClassJobQuests(classJob)
-                .Where(x =>
-                {
-                    if (!_questRegistry.TryGetQuest(x.QuestId, out Quest? quest) ||
-                        quest.Info is not QuestInfo questInfo)
-                        return false;
+            EClassJob classJob = (EClassJob?)_clientState.LocalPlayer?.ClassJob.RowId ?? EClassJob.Adventurer;
+            uint[] 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);
+                        // 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;
+                        // 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
+                        // 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 true;
+                    })
+                    .Select(x => x.QuestId));
+            }
         }
 
         return priorityQuests
index b6d0d2c..41ded00 100644 (file)
@@ -77,6 +77,49 @@ internal sealed class DebugConfigComponent : ConfigComponent
                 "Typically, the loop settings for AutoDuty are disabled when running dungeons with Questionable, since they can cause issues (or even shut down your PC).");
         }
 
-        ImGui.EndTabItem();
+        ImGui.Separator();
+        ImGui.Text("Quest/Interaction Skips");
+        using (_ = ImRaii.PushIndent())
+        {
+            bool skipAetherCurrents = Configuration.Advanced.SkipAetherCurrents;
+            if (ImGui.Checkbox("Don't pick up aether currents/aether current quests", ref skipAetherCurrents))
+            {
+                Configuration.Advanced.SkipAetherCurrents = skipAetherCurrents;
+                Save();
+            }
+
+            ImGui.SameLine();
+            ImGuiComponents.HelpMarker("If not done during the MSQ by Questionable, you have to manually pick up any missed aether currents/quests. There is no way to automatically pick up all missing aether currents.");
+
+            bool skipClassJobQuests = Configuration.Advanced.SkipClassJobQuests;
+            if (ImGui.Checkbox("Don't pick up class/job/role quests", ref skipClassJobQuests))
+            {
+                Configuration.Advanced.SkipClassJobQuests = skipClassJobQuests;
+                Save();
+            }
+
+            ImGui.SameLine();
+            ImGuiComponents.HelpMarker("Class and job skills for A Realm Reborn, Heavensward and (for the Lv70 skills) Stormblood are locked behind quests. Not recommended if you plan on queueing for instances with duty finder/party finder.");
+
+            bool skipARealmRebornHardModePrimals = Configuration.Advanced.SkipARealmRebornHardModePrimals;
+            if (ImGui.Checkbox("Don't pick up ARR hard mode primal quests", ref skipARealmRebornHardModePrimals))
+            {
+                Configuration.Advanced.SkipARealmRebornHardModePrimals = skipARealmRebornHardModePrimals;
+                Save();
+            }
+
+            ImGui.SameLine();
+            ImGuiComponents.HelpMarker("Hard mode Ifrit/Garuda/Titan are required for the Patch 2.5 quest 'Good Intentions' and to start Heavensward.");
+
+            bool skipCrystalTowerRaids = Configuration.Advanced.SkipCrystalTowerRaids;
+            if (ImGui.Checkbox("Don't pick up Crystal Tower quests", ref skipCrystalTowerRaids))
+            {
+                Configuration.Advanced.SkipCrystalTowerRaids = skipCrystalTowerRaids;
+                Save();
+            }
+
+            ImGui.SameLine();
+            ImGuiComponents.HelpMarker("Crystal Tower raids are required for the Patch 2.55 quest 'A Time to Every Purpose' and to start Heavensward.");
+        }
     }
 }
index 6d139a1..9bd1f41 100644 (file)
@@ -1,5 +1,7 @@
 using System.Linq;
 using Dalamud.Interface;
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Style;
 using Dalamud.Interface.Utility.Raii;
 using FFXIVClientStructs.FFXIV.Client.Game.UI;
 using FFXIVClientStructs.FFXIV.Common.Math;
@@ -20,14 +22,16 @@ internal sealed class ARealmRebornComponent
     private readonly QuestData _questData;
     private readonly TerritoryData _territoryData;
     private readonly UiUtils _uiUtils;
+    private readonly Configuration _configuration;
 
     public ARealmRebornComponent(QuestFunctions questFunctions, QuestData questData, TerritoryData territoryData,
-        UiUtils uiUtils)
+        UiUtils uiUtils, Configuration configuration)
     {
         _questFunctions = questFunctions;
         _questData = questData;
         _territoryData = territoryData;
         _uiUtils = uiUtils;
+        _configuration = configuration;
     }
 
     public bool ShouldDraw => !_questFunctions.IsQuestAcceptedOrComplete(ATimeForEveryPurpose) &&
@@ -44,7 +48,8 @@ internal sealed class ARealmRebornComponent
     private void DrawPrimals()
     {
         bool complete = UIState.IsInstanceContentCompleted(RequiredPrimalInstances.Last());
-        bool hover = _uiUtils.ChecklistItem("Hard Mode Primals", complete);
+        bool hover = _uiUtils.ChecklistItem("Hard Mode Primals", complete,
+            _configuration.Advanced.SkipARealmRebornHardModePrimals ? ImGuiColors.DalamudGrey : null);
         if (complete || !hover)
             return;
 
@@ -62,7 +67,8 @@ internal sealed class ARealmRebornComponent
     private void DrawAllianceRaids()
     {
         bool complete = _questFunctions.IsQuestComplete(QuestData.CrystalTowerQuests[^1]);
-        bool hover = _uiUtils.ChecklistItem("Crystal Tower Raids", complete);
+        bool hover = _uiUtils.ChecklistItem("Crystal Tower Raids", complete,
+            _configuration.Advanced.SkipCrystalTowerRaids ? ImGuiColors.DalamudGrey : null);
         if (complete || !hover)
             return;
 
index b03fabf..6b6bae2 100644 (file)
@@ -70,10 +70,10 @@ internal sealed class UiUtils
         return hover;
     }
 
-    public bool ChecklistItem(string text, bool complete)
+    public bool ChecklistItem(string text, bool complete, Vector4? colorOverride = null)
     {
         return ChecklistItem(text,
-            complete ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed,
+            colorOverride ?? (complete ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed),
             complete ? FontAwesomeIcon.Check : FontAwesomeIcon.Times);
     }
 }