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 e1d3d9efb91a947d3d1ecb21f9e2a1adc81f0cd9..1c91e8f86ea89c07fc0414eaaa64f87bfaae2909 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 e59824010d38519d242507d9f9a34b9d5d002ab8..32253a9314fcb526f5f8de932f28a7f00de63798 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 ef4eb1102d8b1a543efd4fca1d28b55130cf694f..be772a55dbd1844645dbaef875f9cbffe701964e 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 e67e36e6624257b929898d595ce6c56bc22e4794..c0080f29dde62c6414e6c546047504f6c927afb3 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 1674792866ce80caf65ad42aaf2b235b688244fb..e799df8c9220a0e7ec57baf80b0c686fa7f2dba3 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 c8c41ffdd0f15b5085e66f2267f1a6bef4c530a4..19f457fec07bd94eefc7aff5a4bcdb752e1f8a8e 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 fcb107bc973bbcb3a20cc2fd1bb27d2e1bed1807..373019d9bfcf44fb1f97363c493eaa49fe761eb6 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 b6d0d2c51d44dd2977ea1479a94a29c0568f16b9..41ded003500c5cab4e9513ec2476678d4acd9c00 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 6d139a1632519c12e87d24de616095fea2d4feb3..9bd1f4120473a2980c4bffc4716d839152f6b833 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 b03fabfe0784f59c2dd56274304e992ef504162c..6b6bae2a25a125798dcd92e56edf339a5d0781f1 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);
     }
 }