From 4ed487c04a5e29693bc803fd1e4dec04e12cc36d Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Sun, 13 Apr 2025 11:37:44 +0200 Subject: [PATCH] Add config to skip aether currents/class quests/primals/crystal tower --- .../E6-2.55/4591_The Steps of Faith.json | 3 +- Questionable.sln.DotSettings | 1 + Questionable/Configuration.cs | 4 + Questionable/Controller/QuestController.cs | 4 +- .../Controller/Steps/Shared/SkipCondition.cs | 28 ++++++- Questionable/Data/QuestData.cs | 80 +++++++++++++++---- Questionable/Functions/QuestFunctions.cs | 56 +++++++------ .../ConfigComponents/DebugConfigComponent.cs | 45 ++++++++++- .../QuestComponents/ARealmRebornComponent.cs | 12 ++- Questionable/Windows/UiUtils.cs | 4 +- 10 files changed, 188 insertions(+), 49 deletions(-) diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/E6-2.55/4591_The Steps of Faith.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/E6-2.55/4591_The Steps of Faith.json index e1d3d9ef..1c91e8f8 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/E6-2.55/4591_The Steps of Faith.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/E6-2.55/4591_The Steps of Faith.json @@ -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" ] diff --git a/Questionable.sln.DotSettings b/Questionable.sln.DotSettings index e5982401..32253a93 100644 --- a/Questionable.sln.DotSettings +++ b/Questionable.sln.DotSettings @@ -30,6 +30,7 @@ True True True + True True True True diff --git a/Questionable/Configuration.cs b/Questionable/Configuration.cs index ef4eb110..be772a55 100644 --- a/Questionable/Configuration.cs +++ b/Questionable/Configuration.cs @@ -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 diff --git a/Questionable/Controller/QuestController.cs b/Questionable/Controller/QuestController.cs index e67e36e6..c0080f29 100644 --- a/Questionable/Controller/QuestController.cs +++ b/Questionable/Controller/QuestController.cs @@ -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 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) diff --git a/Questionable/Controller/Steps/Shared/SkipCondition.cs b/Questionable/Controller/Steps/Shared/SkipCondition.cs index 16747928..e799df8c 100644 --- a/Questionable/Controller/Steps/Shared/SkipCondition.cs +++ b/Questionable/Controller/Steps/Shared/SkipCondition.cs @@ -51,6 +51,7 @@ internal static class SkipCondition internal sealed class CheckSkip( ILogger 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; } diff --git a/Questionable/Data/QuestData.cs b/Questionable/Data/QuestData.cs index c8c41ffd..19f457fe 100644 --- a/Questionable/Data/QuestData.cs +++ b/Questionable/Data/QuestData.cs @@ -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 HardModePrimals = [new(1048), new(1157), new(1158)]; + public static readonly IReadOnlyList CrystalTowerQuests = [new(1709), new(1200), new(1201), new(1202), new(1203), new(1474), new(494), new(495)]; - public static readonly IReadOnlyList TankRoleQuests = [136, 154, 178]; - public static readonly IReadOnlyList HealerRoleQuests = [137, 155, 179]; - public static readonly IReadOnlyList MeleeRoleQuests = [138, 156, 180]; - public static readonly IReadOnlyList PhysicalRangedRoleQuests = [138, 157, 181]; - public static readonly IReadOnlyList CasterRoleQuests = [139, 158, 182]; + public static readonly ImmutableDictionary> AetherCurrentQuestsByTerritory = + new Dictionary> + { + // 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 AetherCurrentQuests { get; } = + AetherCurrentQuestsByTerritory.Values.SelectMany(x => x).ToImmutableHashSet(); + + private static readonly IReadOnlyList TankRoleQuestChapters = [136, 154, 178]; + private static readonly IReadOnlyList HealerRoleQuestChapters = [137, 155, 179]; + private static readonly IReadOnlyList MeleeRoleQuestChapters = [138, 156, 180]; + private static readonly IReadOnlyList PhysicalRangedRoleQuestChapters = [138, 157, 181]; + private static readonly IReadOnlyList CasterRoleQuestChapters = [139, 158, 182]; public static readonly IReadOnlyList> AllRoleQuestChapters = [ - TankRoleQuests, - HealerRoleQuests, - MeleeRoleQuests, - PhysicalRangedRoleQuests, - CasterRoleQuests + TankRoleQuestChapters, + HealerRoleQuestChapters, + MeleeRoleQuestChapters, + PhysicalRangedRoleQuestChapters, + CasterRoleQuestChapters ]; public static readonly IReadOnlyList 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, _ => [] }; } diff --git a/Questionable/Functions/QuestFunctions.cs b/Questionable/Functions/QuestFunctions.cs index fcb107bc..373019d9 100644 --- a/Questionable/Functions/QuestFunctions.cs +++ b/Questionable/Functions/QuestFunctions.cs @@ -441,36 +441,46 @@ internal sealed unsafe class QuestFunctions List 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 diff --git a/Questionable/Windows/ConfigComponents/DebugConfigComponent.cs b/Questionable/Windows/ConfigComponents/DebugConfigComponent.cs index b6d0d2c5..41ded003 100644 --- a/Questionable/Windows/ConfigComponents/DebugConfigComponent.cs +++ b/Questionable/Windows/ConfigComponents/DebugConfigComponent.cs @@ -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."); + } } } diff --git a/Questionable/Windows/QuestComponents/ARealmRebornComponent.cs b/Questionable/Windows/QuestComponents/ARealmRebornComponent.cs index 6d139a16..9bd1f412 100644 --- a/Questionable/Windows/QuestComponents/ARealmRebornComponent.cs +++ b/Questionable/Windows/QuestComponents/ARealmRebornComponent.cs @@ -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; diff --git a/Questionable/Windows/UiUtils.cs b/Questionable/Windows/UiUtils.cs index b03fabfe..6b6bae2a 100644 --- a/Questionable/Windows/UiUtils.cs +++ b/Questionable/Windows/UiUtils.cs @@ -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); } } -- 2.30.2