Split QuestWindow into components; fix item use; show required instances to unlock...
authorLiza Carvelli <liza@carvel.li>
Sun, 21 Jul 2024 13:30:10 +0000 (15:30 +0200)
committerLiza Carvelli <liza@carvel.li>
Sun, 21 Jul 2024 13:30:10 +0000 (15:30 +0200)
17 files changed:
Questionable/Configuration.cs
Questionable/Controller/NavigationOverrides/MovementOverrideController.cs
Questionable/Controller/QuestController.cs
Questionable/Controller/Steps/Interactions/UseItem.cs
Questionable/Data/TerritoryData.cs
Questionable/GameFunctions.cs
Questionable/Model/QuestInfo.cs
Questionable/QuestionablePlugin.cs
Questionable/Windows/ConfigWindow.cs
Questionable/Windows/QuestComponents/ARealmRebornComponent.cs [new file with mode: 0644]
Questionable/Windows/QuestComponents/ActiveQuestComponent.cs [new file with mode: 0644]
Questionable/Windows/QuestComponents/CreationUtilsComponent.cs [new file with mode: 0644]
Questionable/Windows/QuestComponents/QuickAccessButtonsComponent.cs [new file with mode: 0644]
Questionable/Windows/QuestComponents/RemainingTasksComponent.cs [new file with mode: 0644]
Questionable/Windows/QuestSelectionWindow.cs
Questionable/Windows/QuestWindow.cs
Questionable/Windows/UiUtils.cs [new file with mode: 0644]

index 15a07b8c32183a6eb7809e65256f08693e851ca6..4b2769614d042a7c7b04d6398d7b7d04acba2724 100644 (file)
@@ -24,5 +24,6 @@ internal sealed class Configuration : IPluginConfiguration
     {
         public bool DebugOverlay { get; set; }
         public bool NeverFly { get; set; }
+        public bool AdditionalStatusInformation { get; set; }
     }
 }
index 1a1a166986c22e6a9c566d64a299b99c291f5905..0cb2b4f364ac06d05723e69427d4b1861e78dd57 100644 (file)
@@ -15,6 +15,9 @@ internal sealed class MovementOverrideController
         // New Gridania Navmesh workaround
         new BlacklistedPoint(128, new(2f, 40.25f, 36.5f), new(0.25f, 40.25f, 36.5f)),
 
+        // lotus stand
+        new BlacklistedPoint(205, new(26.75f, 0.5f, 20.75f), new(27.179117f, 0.26728272f, 19.714373f)),
+
         new BlacklistedPoint(132, new(45.5f, -8f, 101f), new(50.53978f, -8.046954f, 101.06045f)),
 
         // eastern thanalan
index 141bc91d68ed4372281f430fd1d34158865e206c..b9a011d7ea0e8a38d57ed5fb2c64eddff411005a 100644 (file)
@@ -391,7 +391,8 @@ internal sealed class QuestController
                 catch (Exception e)
                 {
                     _logger.LogError(e, "Failed to start task {TaskName}", upcomingTask.ToString());
-                    _chatGui.PrintError($"[Questionable] Failed to start task '{upcomingTask}', please check /xllog for details");
+                    _chatGui.PrintError(
+                        $"[Questionable] Failed to start task '{upcomingTask}', please check /xllog for details");
                     Stop("Task failed to start");
                     return;
                 }
@@ -466,6 +467,9 @@ internal sealed class QuestController
         ClearTasksInternal();
         _automatic = automatic;
 
+        if (TryPickPriorityQuest())
+            _logger.LogInformation("Using priority quest over current quest");
+
         (QuestSequence? seq, QuestStep? step) = GetNextStep();
         if (CurrentQuest == null || seq == null || step == null)
         {
@@ -590,6 +594,47 @@ internal sealed class QuestController
         _currentTask = null;
     }
 
+    public bool IsInterruptible()
+    {
+        var details = CurrentQuestDetails;
+        if (details == null)
+            return false;
+
+        var (currentQuest, type) = details.Value;
+        if (type != CurrentQuestType.Normal)
+            return false;
+
+        QuestSequence? currentSequence = currentQuest.Quest.FindSequence(currentQuest.Sequence);
+        QuestStep? currentStep = currentSequence?.FindStep(currentQuest.Step);
+        return currentStep?.AetheryteShortcut != null;
+    }
+
+    public bool TryPickPriorityQuest()
+    {
+        if (!IsInterruptible())
+            return false;
+
+        ushort[] priorityQuests =
+        [
+            1157, // Garuda (Hard)
+            1158, // Titan (Hard)
+        ];
+
+        foreach (var questId in priorityQuests)
+        {
+            if (_gameFunctions.IsReadyToAcceptQuest(questId) && _questRegistry.TryGetQuest(questId, out var quest))
+            {
+                SetNextQuest(quest);
+                _chatGui.Print(
+                    "[Questionable] Picking up quest '{Name}' as a priority over current main story/side quests",
+                    quest.Info.Name);
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     public sealed record StepProgress(
         DateTime StartedAt,
         int PointMenuCounter = 0);
index 8868f81181295b1c0f67333913e6208cd10bc7db..cb6c0ff0a73b25e9e199db6b11d9991c92fc5738 100644 (file)
@@ -121,7 +121,7 @@ internal static class UseItem
             if (DateTime.Now <= _continueAt)
                 return ETaskResult.StillRunning;
 
-            if (ItemId == VesperBayAetheryteTicket)
+            if (ItemId == VesperBayAetheryteTicket && _usedItem)
             {
                 InventoryManager* inventoryManager = InventoryManager.Instance();
                 if (inventoryManager == null)
@@ -131,7 +131,7 @@ internal static class UseItem
                 }
 
                 int itemCount = inventoryManager->GetInventoryItemCount(ItemId);
-                if (!_usedItem && itemCount == _itemCount)
+                if (itemCount == _itemCount)
                 {
                     // TODO Better handling for game-provided errors, i.e. reacting to the 'Could not use' messages. UseItem() is successful in this case (and returns 0)
                     logger.LogInformation(
index fc541e778ad37976fc005effde779c55adbb578d..0175cf6de18af8fef0b376b80626ad1118265f52 100644 (file)
@@ -1,4 +1,5 @@
-using System.Collections.Immutable;
+using System.Collections.Generic;
+using System.Collections.Immutable;
 using System.Globalization;
 using System.Linq;
 using Dalamud.Plugin.Services;
@@ -12,6 +13,7 @@ internal sealed class TerritoryData
     private readonly ImmutableDictionary<uint, string> _territoryNames;
     private readonly ImmutableHashSet<ushort> _territoriesWithMount;
     private readonly ImmutableHashSet<ushort> _dutyTerritories;
+    private readonly ImmutableDictionary<ushort, string> _instanceNames;
 
     public TerritoryData(IDataManager dataManager)
     {
@@ -35,6 +37,10 @@ internal sealed class TerritoryData
             .Where(x => x.RowId > 0 && x.ContentFinderCondition.Row != 0)
             .Select(x => (ushort)x.RowId)
             .ToImmutableHashSet();
+
+        _instanceNames = dataManager.GetExcelSheet<ContentFinderCondition>()!
+            .Where(x => x.RowId > 0 && x.Content != 0 && x.ContentLinkType == 1 && x.ContentType.Row != 6)
+            .ToImmutableDictionary(x => x.Content, x => x.Name.ToString());
     }
 
     public string? GetName(ushort territoryId) => _territoryNames.GetValueOrDefault(territoryId);
@@ -51,4 +57,6 @@ internal sealed class TerritoryData
     public bool CanUseMount(ushort territoryId) => _territoriesWithMount.Contains(territoryId);
 
     public bool IsDutyInstance(ushort territoryId) => _dutyTerritories.Contains(territoryId);
+
+    public string? GetInstanceName(ushort instanceId) => _instanceNames.GetValueOrDefault(instanceId);
 }
index b2951732076141c5ca5a474d2764cd5a34dfd1e4..ff74a7082af4f925901ac2f3239ecf0ebd0e7a5e 100644 (file)
@@ -251,6 +251,9 @@ internal sealed unsafe class GameFunctions
                 return false;
         }
 
+        if (IsQuestLocked(questId))
+            return false;
+
         // if we're not at a high enough level to continue, we also ignore it
         var currentLevel = _clientState.LocalPlayer?.Level ?? 0;
         if (currentLevel != 0 && quest != null && quest.Info.Level > currentLevel)
@@ -288,19 +291,37 @@ internal sealed unsafe class GameFunctions
                 return true;
         }
 
-        if (questInfo.PreviousQuests.Count > 0)
-        {
-            var completedQuests = questInfo.PreviousQuests.Count(x => IsQuestComplete(x) || x == extraCompletedQuest);
-            if (questInfo.PreviousQuestJoin == QuestInfo.QuestJoin.All &&
-                questInfo.PreviousQuests.Count == completedQuests)
-                return false;
-            else if (questInfo.PreviousQuestJoin == QuestInfo.QuestJoin.AtLeastOne && completedQuests > 0)
-                return false;
-            else
-                return true;
-        }
+        return !HasCompletedPreviousQuests(questInfo, extraCompletedQuest) || !HasCompletedPreviousInstances(questInfo);
+    }
 
-        return false;
+    private bool HasCompletedPreviousQuests(QuestInfo questInfo, ushort? extraCompletedQuest)
+    {
+        if (questInfo.PreviousQuests.Count == 0)
+            return true;
+
+        var completedQuests = questInfo.PreviousQuests.Count(x => IsQuestComplete(x) || x == extraCompletedQuest);
+        if (questInfo.PreviousQuestJoin == QuestInfo.QuestJoin.All &&
+            questInfo.PreviousQuests.Count == completedQuests)
+            return true;
+        else if (questInfo.PreviousQuestJoin == QuestInfo.QuestJoin.AtLeastOne && completedQuests > 0)
+            return true;
+        else
+            return false;
+    }
+
+    private static bool HasCompletedPreviousInstances(QuestInfo questInfo)
+    {
+        if (questInfo.PreviousInstanceContent.Count == 0)
+            return true;
+
+        var completedInstances = questInfo.PreviousInstanceContent.Count(x => UIState.IsInstanceContentCompleted(x));
+        if (questInfo.PreviousInstanceContentJoin == QuestInfo.QuestJoin.All &&
+            questInfo.PreviousInstanceContent.Count == completedInstances)
+            return true;
+        else if (questInfo.PreviousInstanceContentJoin == QuestInfo.QuestJoin.AtLeastOne && completedInstances > 0)
+            return true;
+        else
+            return false;
     }
 
     public bool IsAetheryteUnlocked(uint aetheryteId, out byte subIndex)
index 4ae135471b934cd0df5a64a1733139f412ab7ec4..d0d1913ba8f601dd9269e5071acbc559d7d53c97 100644 (file)
@@ -23,8 +23,11 @@ internal sealed class QuestInfo
         QuestLockJoin = (QuestJoin)quest.QuestLockJoin;
         IsMainScenarioQuest = quest.JournalGenre?.Value?.JournalCategory?.Value?.JournalSection?.Row is 0 or 1;
         CompletesInstantly = quest.ToDoCompleteSeq[0] == 0;
+        PreviousInstanceContent = quest.InstanceContent.Select(x => (ushort)x.Row).Where(x => x != 0).ToList();
+        PreviousInstanceContentJoin = (QuestJoin)quest.InstanceContentJoin;
     }
 
+
     public ushort QuestId { get; }
     public string Name { get; }
     public ushort Level { get; }
@@ -34,6 +37,8 @@ internal sealed class QuestInfo
     public QuestJoin PreviousQuestJoin { get; }
     public ImmutableList<ushort> QuestLocks { get; set; }
     public QuestJoin QuestLockJoin { get; set; }
+    public List<ushort> PreviousInstanceContent { get; set; }
+    public QuestJoin PreviousInstanceContentJoin { get; set; }
     public bool IsMainScenarioQuest { get; }
     public bool CompletesInstantly { get; set; }
 
index 1cb6c4c17f1608984347445b8141135243863fcc..90286f74e53d1db57bd18c7bd369e66b73c22c70 100644 (file)
@@ -19,6 +19,7 @@ using Questionable.External;
 using Questionable.Validation;
 using Questionable.Validation.Validators;
 using Questionable.Windows;
+using Questionable.Windows.QuestComponents;
 using Action = Questionable.Controller.Steps.Interactions.Action;
 
 namespace Questionable;
@@ -152,6 +153,14 @@ public sealed class QuestionablePlugin : IDalamudPlugin
 
     private static void AddWindows(ServiceCollection serviceCollection)
     {
+        serviceCollection.AddSingleton<UiUtils>();
+
+        serviceCollection.AddSingleton<ActiveQuestComponent>();
+        serviceCollection.AddSingleton<ARealmRebornComponent>();
+        serviceCollection.AddSingleton<CreationUtilsComponent>();
+        serviceCollection.AddSingleton<QuickAccessButtonsComponent>();
+        serviceCollection.AddSingleton<RemainingTasksComponent>();
+
         serviceCollection.AddSingleton<QuestWindow>();
         serviceCollection.AddSingleton<ConfigWindow>();
         serviceCollection.AddSingleton<DebugOverlay>();
index a24b6734e5f00712127daa3342381f9675460a20..11693300ed61a82c6601bb88e86ce49eacc80ae3 100644 (file)
@@ -100,6 +100,13 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
                     Save();
                 }
 
+                bool additionalStatusInformation = _configuration.Advanced.AdditionalStatusInformation;
+                if (ImGui.Checkbox("Draw additional status information", ref additionalStatusInformation))
+                {
+                    _configuration.Advanced.AdditionalStatusInformation = additionalStatusInformation;
+                    Save();
+                }
+
                 ImGui.EndTabItem();
             }
 
diff --git a/Questionable/Windows/QuestComponents/ARealmRebornComponent.cs b/Questionable/Windows/QuestComponents/ARealmRebornComponent.cs
new file mode 100644 (file)
index 0000000..dbc880c
--- /dev/null
@@ -0,0 +1,65 @@
+using System.Linq;
+using Dalamud.Interface;
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Utility.Raii;
+using FFXIVClientStructs.FFXIV.Client.Game.UI;
+using FFXIVClientStructs.FFXIV.Common.Math;
+using ImGuiNET;
+using Questionable.Data;
+
+namespace Questionable.Windows.QuestComponents;
+
+internal sealed class ARealmRebornComponent
+{
+    private const ushort ATimeForEveryPurpose = 425;
+    private const ushort TheUltimateWeapon = 524;
+    private static readonly ushort[] RequiredPrimalInstances = [20004, 20006, 20005];
+    private static readonly ushort[] RequiredAllianceRaidQuests = [1709, 1200, 1201, 1202, 1203, 1474, 494, 495];
+
+    private readonly GameFunctions _gameFunctions;
+    private readonly QuestData _questData;
+    private readonly TerritoryData _territoryData;
+    private readonly UiUtils _uiUtils;
+
+    public ARealmRebornComponent(GameFunctions gameFunctions, QuestData questData, TerritoryData territoryData,
+        UiUtils uiUtils)
+    {
+        _gameFunctions = gameFunctions;
+        _questData = questData;
+        _territoryData = territoryData;
+        _uiUtils = uiUtils;
+    }
+
+    public bool ShouldDraw => !_gameFunctions.IsQuestComplete(ATimeForEveryPurpose) &&
+                              _gameFunctions.IsQuestComplete(TheUltimateWeapon);
+
+    public void Draw()
+    {
+        var completedPrimals = UIState.IsInstanceContentCompleted(RequiredPrimalInstances.Last());
+        bool completedRaids = _gameFunctions.IsQuestComplete(RequiredAllianceRaidQuests.Last());
+        bool complete = completedPrimals && completedRaids;
+        bool hover = _uiUtils.ChecklistItem("ARR Primals & Raids",
+            complete ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed,
+            complete ? FontAwesomeIcon.Check : FontAwesomeIcon.Times);
+        if (complete || !hover)
+            return;
+
+        using var tooltip = ImRaii.Tooltip();
+        if (!tooltip)
+            return;
+
+        ImGui.Text("Primals:");
+        foreach (var instanceId in RequiredPrimalInstances)
+        {
+            (Vector4 color, FontAwesomeIcon icon) = UiUtils.GetInstanceStyle(instanceId);
+            _uiUtils.ChecklistItem(_territoryData.GetInstanceName(instanceId) ?? "?", color, icon);
+        }
+
+        ImGui.Text("Alliance Raids:");
+        foreach (var questId in RequiredAllianceRaidQuests)
+        {
+            (Vector4 color, FontAwesomeIcon icon, _) = _uiUtils.GetQuestStyle(questId);
+            _uiUtils.ChecklistItem(_questData.GetQuestInfo(questId).Name, color, icon);
+        }
+    }
+}
diff --git a/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs b/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs
new file mode 100644 (file)
index 0000000..48b9434
--- /dev/null
@@ -0,0 +1,325 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using Dalamud.Interface;
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Components;
+using Dalamud.Interface.Utility.Raii;
+using Dalamud.Plugin;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
+using ImGuiNET;
+using Questionable.Controller;
+using Questionable.Controller.Steps.Shared;
+using Questionable.Model.V1;
+
+namespace Questionable.Windows.QuestComponents;
+
+internal sealed class ActiveQuestComponent
+{
+    private readonly QuestController _questController;
+    private readonly MovementController _movementController;
+    private readonly CombatController _combatController;
+    private readonly GameFunctions _gameFunctions;
+    private readonly ICommandManager _commandManager;
+    private readonly IDalamudPluginInterface _pluginInterface;
+    private readonly Configuration _configuration;
+    private readonly QuestRegistry _questRegistry;
+
+    public ActiveQuestComponent(QuestController questController, MovementController movementController,
+        CombatController combatController, GameFunctions gameFunctions, ICommandManager commandManager,
+        IDalamudPluginInterface pluginInterface, Configuration configuration, QuestRegistry questRegistry)
+    {
+        _questController = questController;
+        _movementController = movementController;
+        _combatController = combatController;
+        _gameFunctions = gameFunctions;
+        _commandManager = commandManager;
+        _pluginInterface = pluginInterface;
+        _configuration = configuration;
+        _questRegistry = questRegistry;
+    }
+
+    public void Draw()
+    {
+        var currentQuestDetails = _questController.CurrentQuestDetails;
+        QuestController.QuestProgress? currentQuest = currentQuestDetails?.Progress;
+        QuestController.CurrentQuestType? currentQuestType = currentQuestDetails?.Type;
+        if (currentQuest != null)
+        {
+            DrawQuestNames(currentQuest, currentQuestType);
+            var questWork = DrawQuestWork(currentQuest);
+
+            if (_combatController.IsRunning)
+                ImGui.TextColored(ImGuiColors.DalamudOrange, "In Combat");
+            else
+            {
+                ImGui.BeginDisabled();
+                ImGui.TextUnformatted(_questController.DebugState ?? string.Empty);
+                ImGui.EndDisabled();
+            }
+
+            QuestSequence? currentSequence = currentQuest.Quest.FindSequence(currentQuest.Sequence);
+            QuestStep? currentStep = currentSequence?.FindStep(currentQuest.Step);
+            bool colored = currentStep is
+                { InteractionType: EInteractionType.Instruction or EInteractionType.WaitForManualProgress };
+            if (colored)
+                ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudOrange);
+            ImGui.TextUnformatted(currentStep?.Comment ??
+                                  currentSequence?.Comment ?? currentQuest.Quest.Root.Comment ?? string.Empty);
+            if (colored)
+                ImGui.PopStyleColor();
+
+            //var nextStep = _questController.GetNextStep();
+            //ImGui.BeginDisabled(nextStep.Step == null);
+            ImGui.Text(_questController.ToStatString());
+            //ImGui.EndDisabled();
+
+            DrawQuestIcons(currentQuest, currentStep, questWork);
+
+            DrawSimulationControls();
+        }
+        else
+        {
+            ImGui.Text("No active quest");
+            ImGui.TextColored(ImGuiColors.DalamudGrey, $"{_questRegistry.Count} quests loaded");
+        }
+    }
+
+    private void DrawQuestNames(QuestController.QuestProgress currentQuest,
+        QuestController.CurrentQuestType? currentQuestType)
+    {
+        if (currentQuestType == QuestController.CurrentQuestType.Simulated)
+        {
+            var simulatedQuest = _questController.SimulatedQuest ?? currentQuest;
+            using var _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
+            ImGui.TextUnformatted(
+                $"Simulated Quest: {simulatedQuest.Quest.Info.Name} / {simulatedQuest.Sequence} / {simulatedQuest.Step}");
+        }
+        else if (currentQuestType == QuestController.CurrentQuestType.Next)
+        {
+            var startedQuest = _questController.StartedQuest;
+            if (startedQuest != null)
+                DrawCurrentQuest(startedQuest);
+
+            using var _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow);
+            ImGui.TextUnformatted(
+                $"Next Quest: {currentQuest.Quest.Info.Name} / {currentQuest.Sequence} / {currentQuest.Step}");
+        }
+        else
+            DrawCurrentQuest(currentQuest);
+    }
+
+    private void DrawCurrentQuest(QuestController.QuestProgress currentQuest)
+    {
+        ImGui.TextUnformatted(
+            $"Quest: {currentQuest.Quest.Info.Name} / {currentQuest.Sequence} / {currentQuest.Step}");
+
+        if (currentQuest.Quest.Root.Disabled)
+        {
+            ImGui.SameLine();
+            ImGui.TextColored(ImGuiColors.DalamudRed, "Disabled");
+        }
+
+        if (_configuration.Advanced.AdditionalStatusInformation && _questController.IsInterruptible())
+        {
+            ImGui.SameLine();
+            ImGui.TextColored(ImGuiColors.DalamudYellow, "Interruptible");
+        }
+    }
+
+    private QuestWork? DrawQuestWork(QuestController.QuestProgress currentQuest)
+    {
+        ImGui.BeginDisabled();
+        var questWork = _gameFunctions.GetQuestEx(currentQuest.Quest.QuestId);
+        if (questWork != null)
+        {
+            var qw = questWork.Value;
+            string vars = "";
+            for (int i = 0; i < 6; ++i)
+            {
+                vars += qw.Variables[i] + " ";
+                if (i % 2 == 1)
+                    vars += "   ";
+            }
+
+            // For combat quests, a sequence to kill 3 enemies works a bit like this:
+            // Trigger enemies → 0
+            // Kill first enemy → 1
+            // Kill second enemy → 2
+            // Last enemy → increase sequence, reset variable to 0
+            // The order in which enemies are killed doesn't seem to matter.
+            // If multiple waves spawn, this continues to count up (e.g. 1 enemy from wave 1, 2 enemies from wave 2, 1 from wave 3) would count to 3 then 0
+            ImGui.Text($"QW: {vars.Trim()}");
+        }
+        else
+        {
+            if (currentQuest.Quest.QuestId == _questController.NextQuest?.Quest.QuestId)
+                ImGui.TextUnformatted("(Next quest in story line not accepted)");
+            else
+                ImGui.TextUnformatted("(Not accepted)");
+        }
+
+        ImGui.EndDisabled();
+        return questWork;
+    }
+
+    private void DrawQuestIcons(QuestController.QuestProgress currentQuest, QuestStep? currentStep,
+        QuestWork? questWork)
+    {
+        ImGui.BeginDisabled(_questController.IsRunning);
+        if (ImGuiComponents.IconButton(FontAwesomeIcon.Play))
+        {
+            // if we haven't accepted this quest, mark it as next quest so that we can optionally use aetherytes to travel
+            if (questWork == null)
+                _questController.SetNextQuest(currentQuest.Quest);
+
+            _questController.ExecuteNextStep(true);
+        }
+
+        ImGui.SameLine();
+
+        if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.StepForward, "Step"))
+        {
+            _questController.ExecuteNextStep(false);
+        }
+
+        ImGui.EndDisabled();
+        ImGui.SameLine();
+
+        if (ImGuiComponents.IconButton(FontAwesomeIcon.Stop))
+        {
+            _movementController.Stop();
+            _questController.Stop("Manual");
+        }
+
+        bool lastStep = currentStep ==
+                        currentQuest.Quest.FindSequence(currentQuest.Sequence)?.Steps.LastOrDefault();
+        bool colored = currentStep != null
+                       && !lastStep
+                       && currentStep.InteractionType == EInteractionType.Instruction
+                       && _questController.HasCurrentTaskMatching<WaitAtEnd.WaitNextStepOrSequence>();
+
+        ImGui.BeginDisabled(lastStep);
+        if (colored)
+            ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.ParsedGreen);
+        if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.ArrowCircleRight, "Skip"))
+        {
+            _movementController.Stop();
+            _questController.Skip(currentQuest.Quest.QuestId, currentQuest.Sequence);
+        }
+
+        if (colored)
+            ImGui.PopStyleColor();
+        ImGui.EndDisabled();
+
+        if (_commandManager.Commands.TryGetValue("/questinfo", out var commandInfo))
+        {
+            ImGui.SameLine();
+            if (ImGuiComponents.IconButton(FontAwesomeIcon.Atlas))
+                _commandManager.DispatchCommand("/questinfo",
+                    currentQuest.Quest.QuestId.ToString(CultureInfo.InvariantCulture), commandInfo);
+        }
+
+        bool autoAcceptNextQuest = _configuration.General.AutoAcceptNextQuest;
+        if (ImGui.Checkbox("Automatically accept next quest", ref autoAcceptNextQuest))
+        {
+            _configuration.General.AutoAcceptNextQuest = autoAcceptNextQuest;
+            _pluginInterface.SavePluginConfig(_configuration);
+        }
+    }
+
+    private void DrawSimulationControls()
+    {
+        if (_questController.SimulatedQuest == null)
+            return;
+
+        var simulatedQuest = _questController.SimulatedQuest;
+
+        ImGui.Separator();
+        ImGui.TextColored(ImGuiColors.DalamudRed, "Quest sim active (experimental)");
+        ImGui.Text($"Sequence: {simulatedQuest.Sequence}");
+
+        ImGui.BeginDisabled(simulatedQuest.Sequence == 0);
+        if (ImGuiComponents.IconButton(FontAwesomeIcon.Minus))
+        {
+            _movementController.Stop();
+            _questController.Stop("Sim-");
+
+            byte oldSequence = simulatedQuest.Sequence;
+            byte newSequence = simulatedQuest.Quest.Root.QuestSequence
+                .Select(x => (byte)x.Sequence)
+                .LastOrDefault(x => x < oldSequence, byte.MinValue);
+
+            _questController.SimulatedQuest.SetSequence(newSequence);
+        }
+
+        ImGui.EndDisabled();
+
+        ImGui.SameLine();
+        ImGui.BeginDisabled(simulatedQuest.Sequence >= 255);
+        if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus))
+        {
+            _movementController.Stop();
+            _questController.Stop("Sim+");
+
+            byte oldSequence = simulatedQuest.Sequence;
+            byte newSequence = simulatedQuest.Quest.Root.QuestSequence
+                .Select(x => (byte)x.Sequence)
+                .FirstOrDefault(x => x > oldSequence, byte.MaxValue);
+
+            simulatedQuest.SetSequence(newSequence);
+        }
+
+        ImGui.EndDisabled();
+
+        var simulatedSequence = simulatedQuest.Quest.FindSequence(simulatedQuest.Sequence);
+        if (simulatedSequence != null)
+        {
+            using var _ = ImRaii.PushId("SimulatedStep");
+
+            ImGui.Text($"Step: {simulatedQuest.Step} / {simulatedSequence.Steps.Count - 1}");
+
+            ImGui.BeginDisabled(simulatedQuest.Step == 0);
+            if (ImGuiComponents.IconButton(FontAwesomeIcon.Minus))
+            {
+                _movementController.Stop();
+                _questController.Stop("SimStep-");
+
+                simulatedQuest.SetStep(Math.Min(simulatedQuest.Step - 1,
+                    simulatedSequence.Steps.Count - 1));
+            }
+
+            ImGui.EndDisabled();
+
+            ImGui.SameLine();
+            ImGui.BeginDisabled(simulatedQuest.Step >= simulatedSequence.Steps.Count);
+            if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus))
+            {
+                _movementController.Stop();
+                _questController.Stop("SimStep+");
+
+                simulatedQuest.SetStep(
+                    simulatedQuest.Step == simulatedSequence.Steps.Count - 1
+                        ? 255
+                        : (simulatedQuest.Step + 1));
+            }
+
+            ImGui.EndDisabled();
+
+            if (ImGui.Button("Skip current task"))
+            {
+                _questController.SkipSimulatedTask();
+            }
+
+            ImGui.SameLine();
+            if (ImGui.Button("Clear sim"))
+            {
+                _questController.SimulateQuest(null);
+
+                _movementController.Stop();
+                _questController.Stop("ClearSim");
+            }
+        }
+    }
+}
diff --git a/Questionable/Windows/QuestComponents/CreationUtilsComponent.cs b/Questionable/Windows/QuestComponents/CreationUtilsComponent.cs
new file mode 100644 (file)
index 0000000..a67221a
--- /dev/null
@@ -0,0 +1,227 @@
+using System;
+using System.Diagnostics;
+using System.Globalization;
+using System.Numerics;
+using Dalamud.Game.ClientState.Conditions;
+using Dalamud.Game.ClientState.Objects;
+using Dalamud.Game.Text;
+using Dalamud.Interface;
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Components;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.Game.Control;
+using FFXIVClientStructs.FFXIV.Client.Game.Object;
+using ImGuiNET;
+using Microsoft.Extensions.Logging;
+using Questionable.Controller;
+using Questionable.Data;
+using Questionable.Model;
+using Questionable.Model.V1;
+using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
+
+namespace Questionable.Windows.QuestComponents;
+
+internal sealed class CreationUtilsComponent
+{
+    private readonly MovementController _movementController;
+    private readonly GameFunctions _gameFunctions;
+    private readonly TerritoryData _territoryData;
+    private readonly QuestData _questData;
+    private readonly QuestSelectionWindow _questSelectionWindow;
+    private readonly IClientState _clientState;
+    private readonly ITargetManager _targetManager;
+    private readonly ICondition _condition;
+    private readonly IGameGui _gameGui;
+    private readonly ILogger<CreationUtilsComponent> _logger;
+
+    public CreationUtilsComponent(MovementController movementController, GameFunctions gameFunctions,
+        TerritoryData territoryData, QuestData questData, QuestSelectionWindow questSelectionWindow,
+        IClientState clientState, ITargetManager targetManager, ICondition condition, IGameGui gameGui,
+        ILogger<CreationUtilsComponent> logger)
+    {
+        _movementController = movementController;
+        _gameFunctions = gameFunctions;
+        _territoryData = territoryData;
+        _questData = questData;
+        _questSelectionWindow = questSelectionWindow;
+        _clientState = clientState;
+        _targetManager = targetManager;
+        _condition = condition;
+        _gameGui = gameGui;
+        _logger = logger;
+    }
+
+    public unsafe void Draw()
+    {
+        Debug.Assert(_clientState.LocalPlayer != null, "_clientState.LocalPlayer != null");
+
+        string territoryName = _territoryData.GetNameAndId(_clientState.TerritoryType);
+        ImGui.Text(territoryName);
+
+        if (_gameFunctions.IsFlyingUnlockedInCurrentZone())
+        {
+            ImGui.SameLine();
+            ImGui.Text(SeIconChar.BotanistSprout.ToIconString());
+        }
+
+        var q = _gameFunctions.GetCurrentQuest();
+        ImGui.Text($"Current Quest: {q.CurrentQuest} → {q.Sequence}");
+
+#if false
+        var questManager = QuestManager.Instance();
+        if (questManager != null)
+        {
+            for (int i = questManager->TrackedQuests.Length - 1; i >= 0; --i)
+            {
+                var trackedQuest = questManager->TrackedQuests[i];
+                switch (trackedQuest.QuestType)
+                {
+                    default:
+                        ImGui.Text($"Tracked quest {i}: {trackedQuest.QuestType}, {trackedQuest.Index}");
+                        break;
+
+                    case 1:
+                        _questRegistry.TryGetQuest(questManager->NormalQuests[trackedQuest.Index].QuestId,
+                            out var quest);
+                        ImGui.Text(
+                            $"Tracked quest: {questManager->NormalQuests[trackedQuest.Index].QuestId}, {trackedQuest.Index}: {quest?.Info.Name}");
+                        break;
+                }
+            }
+        }
+#endif
+
+        if (_targetManager.Target != null)
+        {
+            ImGui.Separator();
+            ImGui.Text(string.Create(CultureInfo.InvariantCulture,
+                $"Target: {_targetManager.Target.Name}  ({_targetManager.Target.ObjectKind}; {_targetManager.Target.DataId})"));
+
+            GameObject* gameObject = (GameObject*)_targetManager.Target.Address;
+            ImGui.Text(string.Create(CultureInfo.InvariantCulture,
+                $"Distance: {(_targetManager.Target.Position - _clientState.LocalPlayer.Position).Length():F2}"));
+            ImGui.SameLine();
+
+            float verticalDistance = _targetManager.Target.Position.Y - _clientState.LocalPlayer.Position.Y;
+            string verticalDistanceText = string.Create(CultureInfo.InvariantCulture, $"Y: {verticalDistance:F2}");
+            if (Math.Abs(verticalDistance) >= MovementController.DefaultVerticalInteractionDistance)
+                ImGui.TextColored(ImGuiColors.DalamudOrange, verticalDistanceText);
+            else
+                ImGui.Text(verticalDistanceText);
+
+            ImGui.SameLine();
+            ImGui.Text($"QM: {gameObject->NamePlateIconId}");
+
+            ImGui.BeginDisabled(!_movementController.IsNavmeshReady);
+            if (!_movementController.IsPathfinding)
+            {
+                if (ImGui.Button("Move to Target"))
+                {
+                    _movementController.NavigateTo(EMovementType.DebugWindow, _targetManager.Target.DataId,
+                        _targetManager.Target.Position,
+                        fly: _condition[ConditionFlag.Mounted] && _gameFunctions.IsFlyingUnlockedInCurrentZone(),
+                        sprint: true);
+                }
+            }
+            else
+            {
+                if (ImGui.Button("Cancel pathfinding"))
+                    _movementController.ResetPathfinding();
+            }
+
+            ImGui.EndDisabled();
+
+            ImGui.SameLine();
+            ImGui.BeginDisabled(!_questData.IsIssuerOfAnyQuest(_targetManager.Target.DataId));
+            bool showQuests = ImGuiComponents.IconButton(FontAwesomeIcon.MapMarkerAlt);
+            if (ImGui.IsItemHovered())
+                ImGui.SetTooltip("Show all Quests starting with your current target.");
+            if (showQuests)
+                _questSelectionWindow.OpenForTarget(_targetManager.Target);
+
+            ImGui.EndDisabled();
+
+            ImGui.SameLine();
+            bool interact = ImGuiComponents.IconButton(FontAwesomeIcon.MousePointer);
+            if (ImGui.IsItemHovered())
+                ImGui.SetTooltip("Interact with your current target.");
+            if (interact)
+            {
+                ulong result = TargetSystem.Instance()->InteractWithObject(
+                    (GameObject*)_targetManager.Target.Address, false);
+                _logger.LogInformation("XXXXX Interaction Result: {Result}", result);
+            }
+
+            ImGui.SameLine();
+
+            bool copy = ImGuiComponents.IconButton(FontAwesomeIcon.Copy);
+            if (ImGui.IsItemHovered())
+                ImGui.SetTooltip(
+                    "Left click: Copy target position as JSON.\nRight click: Copy target position as C# code.");
+            if (copy)
+            {
+                string interactionType = gameObject->NamePlateIconId switch
+                {
+                    71201 or 71211 or 71221 or 71231 or 71341 or 71351 => "AcceptQuest",
+                    71202 or 71212 or 71222 or 71232 or 71342 or 71352 => "AcceptQuest", // repeatable
+                    71205 or 71215 or 71225 or 71235 or 71345 or 71355 => "CompleteQuest",
+                    _ => "Interact",
+                };
+                ImGui.SetClipboardText($$"""
+                                         "DataId": {{_targetManager.Target.DataId}},
+                                         "Position": {
+                                             "X": {{_targetManager.Target.Position.X.ToString(CultureInfo.InvariantCulture)}},
+                                             "Y": {{_targetManager.Target.Position.Y.ToString(CultureInfo.InvariantCulture)}},
+                                             "Z": {{_targetManager.Target.Position.Z.ToString(CultureInfo.InvariantCulture)}}
+                                         },
+                                         "TerritoryId": {{_clientState.TerritoryType}},
+                                         "InteractionType": "{{interactionType}}"
+                                         """);
+            }
+            else if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
+            {
+                if (_targetManager.Target.ObjectKind == ObjectKind.Aetheryte)
+                {
+                    EAetheryteLocation location = (EAetheryteLocation)_targetManager.Target.DataId;
+                    ImGui.SetClipboardText(string.Create(CultureInfo.InvariantCulture,
+                        $"{{EAetheryteLocation.{location}, new({_targetManager.Target.Position.X}f, {_targetManager.Target.Position.Y}f, {_targetManager.Target.Position.Z}f)}},"));
+                }
+                else
+                    ImGui.SetClipboardText(string.Create(CultureInfo.InvariantCulture,
+                        $"new({_targetManager.Target.Position.X}f, {_targetManager.Target.Position.Y}f, {_targetManager.Target.Position.Z}f)"));
+            }
+        }
+        else
+        {
+            bool copy = ImGuiComponents.IconButton(FontAwesomeIcon.Copy);
+            if (ImGui.IsItemHovered())
+                ImGui.SetTooltip(
+                    "Left click: Copy your position as JSON.\nRight click: Copy your position as C# code.");
+            if (copy)
+            {
+                ImGui.SetClipboardText($$"""
+                                         "Position": {
+                                             "X": {{_clientState.LocalPlayer.Position.X.ToString(CultureInfo.InvariantCulture)}},
+                                             "Y": {{_clientState.LocalPlayer.Position.Y.ToString(CultureInfo.InvariantCulture)}},
+                                             "Z": {{_clientState.LocalPlayer.Position.Z.ToString(CultureInfo.InvariantCulture)}}
+                                         },
+                                         "TerritoryId": {{_clientState.TerritoryType}},
+                                         "InteractionType": ""
+                                         """);
+            }
+            else if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
+            {
+                Vector3 position = _clientState.LocalPlayer!.Position;
+                ImGui.SetClipboardText(string.Create(CultureInfo.InvariantCulture,
+                    $"new({position.X}f, {position.Y}f, {position.Z}f)"));
+            }
+        }
+
+        ulong hoveredItemId = _gameGui.HoveredItem;
+        if (hoveredItemId != 0)
+        {
+            ImGui.Separator();
+            ImGui.Text($"Hovered Item: {hoveredItemId}");
+        }
+    }
+}
diff --git a/Questionable/Windows/QuestComponents/QuickAccessButtonsComponent.cs b/Questionable/Windows/QuestComponents/QuickAccessButtonsComponent.cs
new file mode 100644 (file)
index 0000000..61ed51b
--- /dev/null
@@ -0,0 +1,150 @@
+using System;
+using System.Globalization;
+using System.Numerics;
+using Dalamud.Game.ClientState.Conditions;
+using Dalamud.Interface;
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Utility;
+using Dalamud.Interface.Utility.Raii;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using ImGuiNET;
+using Questionable.Controller;
+using Questionable.External;
+
+namespace Questionable.Windows.QuestComponents;
+
+internal sealed class QuickAccessButtonsComponent
+{
+    private readonly QuestController _questController;
+    private readonly MovementController _movementController;
+    private readonly GameUiController _gameUiController;
+    private readonly GameFunctions _gameFunctions;
+    private readonly ChatFunctions _chatFunctions;
+    private readonly QuestRegistry _questRegistry;
+    private readonly NavmeshIpc _navmeshIpc;
+    private readonly QuestValidationWindow _questValidationWindow;
+    private readonly IClientState _clientState;
+    private readonly ICondition _condition;
+    private readonly IFramework _framework;
+
+    public QuickAccessButtonsComponent(QuestController questController, MovementController movementController,
+        GameUiController gameUiController, GameFunctions gameFunctions, ChatFunctions chatFunctions,
+        QuestRegistry questRegistry, NavmeshIpc navmeshIpc, QuestValidationWindow questValidationWindow,
+        IClientState clientState, ICondition condition, IFramework framework)
+    {
+        _questController = questController;
+        _movementController = movementController;
+        _gameUiController = gameUiController;
+        _gameFunctions = gameFunctions;
+        _chatFunctions = chatFunctions;
+        _questRegistry = questRegistry;
+        _navmeshIpc = navmeshIpc;
+        _questValidationWindow = questValidationWindow;
+        _clientState = clientState;
+        _condition = condition;
+        _framework = framework;
+    }
+
+    public unsafe void Draw()
+    {
+        var map = AgentMap.Instance();
+        ImGui.BeginDisabled(map == null || map->IsFlagMarkerSet == 0 ||
+                            map->FlagMapMarker.TerritoryId != _clientState.TerritoryType ||
+                            !_navmeshIpc.IsReady);
+        if (ImGui.Button("Move to Flag"))
+        {
+            _movementController.Destination = null;
+            _chatFunctions.ExecuteCommand(
+                $"/vnav {(_condition[ConditionFlag.Mounted] && _gameFunctions.IsFlyingUnlockedInCurrentZone() ? "flyflag" : "moveflag")}");
+        }
+
+        ImGui.EndDisabled();
+
+        ImGui.SameLine();
+
+        ImGui.BeginDisabled(!_movementController.IsPathRunning);
+        if (ImGui.Button("Stop Nav"))
+        {
+            _movementController.Stop();
+            _questController.Stop("Manual");
+        }
+
+        ImGui.EndDisabled();
+
+        if (ImGui.Button("Reload Data"))
+        {
+            _questController.Reload();
+            _framework.RunOnTick(() => _gameUiController.HandleCurrentDialogueChoices(),
+                TimeSpan.FromMilliseconds(200));
+        }
+
+        if (_questRegistry.ValidationIssueCount > 0)
+        {
+            ImGui.SameLine();
+            if (DrawValidationIssuesButton())
+                _questValidationWindow.IsOpen = true;
+        }
+    }
+
+    private bool DrawValidationIssuesButton()
+    {
+        int errorCount = _questRegistry.ValidationErrorCount;
+        int infoCount = _questRegistry.ValidationIssueCount - _questRegistry.ValidationErrorCount;
+        if (errorCount == 0 && infoCount == 0)
+            return false;
+
+        int partsToRender = errorCount == 0 || infoCount == 0 ? 1 : 2;
+        using var id = ImRaii.PushId("validationissues");
+
+        ImGui.PushFont(UiBuilder.IconFont);
+        var icon1 = FontAwesomeIcon.TimesCircle;
+        var icon2 = FontAwesomeIcon.InfoCircle;
+        Vector2 iconSize1 = errorCount > 0 ? ImGui.CalcTextSize(icon1.ToIconString()) : Vector2.Zero;
+        Vector2 iconSize2 = infoCount > 0 ? ImGui.CalcTextSize(icon2.ToIconString()) : Vector2.Zero;
+        ImGui.PopFont();
+
+        string text1 = errorCount > 0 ? errorCount.ToString(CultureInfo.InvariantCulture) : string.Empty;
+        string text2 = infoCount > 0 ? infoCount.ToString(CultureInfo.InvariantCulture) : string.Empty;
+        Vector2 textSize1 = errorCount > 0 ? ImGui.CalcTextSize(text1) : Vector2.Zero;
+        Vector2 textSize2 = infoCount > 0 ? ImGui.CalcTextSize(text2) : Vector2.Zero;
+        var dl = ImGui.GetWindowDrawList();
+        var cursor = ImGui.GetCursorScreenPos();
+
+        var iconPadding = 3 * ImGuiHelpers.GlobalScale;
+
+        // Draw an ImGui button with the icon and text
+        var buttonWidth = iconSize1.X + iconSize2.X + textSize1.X + textSize2.X +
+                          (ImGui.GetStyle().FramePadding.X * 2) + iconPadding * 2 * partsToRender;
+        var buttonHeight = ImGui.GetFrameHeight();
+        var button = ImGui.Button(string.Empty, new Vector2(buttonWidth, buttonHeight));
+
+        // Draw the icon on the window drawlist
+        Vector2 position = new Vector2(cursor.X + ImGui.GetStyle().FramePadding.X,
+            cursor.Y + ImGui.GetStyle().FramePadding.Y);
+        if (errorCount > 0)
+        {
+            ImGui.PushFont(UiBuilder.IconFont);
+            dl.AddText(position, ImGui.GetColorU32(ImGuiColors.DalamudRed), icon1.ToIconString());
+            ImGui.PopFont();
+            position = position with { X = position.X + iconSize1.X + iconPadding };
+
+            // Draw the text on the window drawlist
+            dl.AddText(position, ImGui.GetColorU32(ImGuiCol.Text), text1);
+            position = position with { X = position.X + textSize1.X + 2 * iconPadding };
+        }
+
+        if (infoCount > 0)
+        {
+            ImGui.PushFont(UiBuilder.IconFont);
+            dl.AddText(position, ImGui.GetColorU32(ImGuiColors.ParsedBlue), icon2.ToIconString());
+            ImGui.PopFont();
+            position = position with { X = position.X + iconSize2.X + iconPadding };
+
+            // Draw the text on the window drawlist
+            dl.AddText(position, ImGui.GetColorU32(ImGuiCol.Text), text2);
+        }
+
+        return button;
+    }
+}
diff --git a/Questionable/Windows/QuestComponents/RemainingTasksComponent.cs b/Questionable/Windows/QuestComponents/RemainingTasksComponent.cs
new file mode 100644 (file)
index 0000000..bd35f69
--- /dev/null
@@ -0,0 +1,27 @@
+using ImGuiNET;
+using Questionable.Controller;
+
+namespace Questionable.Windows.QuestComponents;
+
+internal sealed class RemainingTasksComponent
+{
+    private readonly QuestController _questController;
+
+    public RemainingTasksComponent(QuestController questController)
+    {
+        _questController = questController;
+    }
+
+    public void Draw()
+    {
+        var remainingTasks = _questController.GetRemainingTaskNames();
+        if (remainingTasks.Count > 0)
+        {
+            ImGui.Separator();
+            ImGui.BeginDisabled();
+            foreach (var task in remainingTasks)
+                ImGui.TextUnformatted(task);
+            ImGui.EndDisabled();
+        }
+    }
+}
index 9ed5c439f4515f38d00a677d7a6382f0c8089c1f..1594c497a7968fe252fac9f12874cc4f700bab10 100644 (file)
@@ -33,6 +33,7 @@ internal sealed class QuestSelectionWindow : LWindow
     private readonly IDalamudPluginInterface _pluginInterface;
     private readonly TerritoryData _territoryData;
     private readonly IClientState _clientState;
+    private readonly UiUtils _uiUtils;
 
     private List<QuestInfo> _quests = [];
     private List<QuestInfo> _offeredQuests = [];
@@ -40,7 +41,7 @@ internal sealed class QuestSelectionWindow : LWindow
 
     public QuestSelectionWindow(QuestData questData, IGameGui gameGui, IChatGui chatGui, GameFunctions gameFunctions,
         QuestController questController, QuestRegistry questRegistry, IDalamudPluginInterface pluginInterface,
-        TerritoryData territoryData, IClientState clientState)
+        TerritoryData territoryData, IClientState clientState, UiUtils uiUtils)
         : base($"Quest Selection{WindowId}")
     {
         _questData = questData;
@@ -52,6 +53,7 @@ internal sealed class QuestSelectionWindow : LWindow
         _pluginInterface = pluginInterface;
         _territoryData = territoryData;
         _clientState = clientState;
+        _uiUtils = uiUtils;
 
         Size = new Vector2(500, 200);
         SizeCondition = ImGuiCond.Once;
@@ -151,7 +153,7 @@ internal sealed class QuestSelectionWindow : LWindow
             if (ImGui.TableNextColumn())
             {
                 ImGui.AlignTextToFramePadding();
-                var (color, icon, tooltipText) = GetQuestStyle(quest.QuestId);
+                var (color, icon, tooltipText) = _uiUtils.GetQuestStyle(quest.QuestId);
                 using (var _ = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
                 {
                     if (isKnownQuest)
@@ -251,18 +253,6 @@ internal sealed class QuestSelectionWindow : LWindow
         _chatGui.Print($"Copied '{fileName}' to clipboard");
     }
 
-    private (Vector4 color, FontAwesomeIcon icon, string status) GetQuestStyle(ushort questId)
-    {
-        if (_gameFunctions.IsQuestAccepted(questId))
-            return (ImGuiColors.DalamudYellow, FontAwesomeIcon.Running, "Active");
-        else if (_gameFunctions.IsQuestAcceptedOrComplete(questId))
-            return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check, "Complete");
-        else if (_gameFunctions.IsQuestLocked(questId))
-            return (ImGuiColors.DalamudRed, FontAwesomeIcon.Times, "Locked");
-        else
-            return (ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight, "Available");
-    }
-
     private void DrawQuestUnlocks(QuestInfo quest, int counter)
     {
         if (counter >= 10)
@@ -287,7 +277,7 @@ internal sealed class QuestSelectionWindow : LWindow
             foreach (var q in quest.PreviousQuests)
             {
                 var qInfo = _questData.GetQuestInfo(q);
-                var (iconColor, icon, _) = GetQuestStyle(q);
+                var (iconColor, icon, _) = _uiUtils.GetQuestStyle(q);
                 // ReSharper disable once UnusedVariable
                 using (var font = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
                 {
@@ -319,7 +309,7 @@ internal sealed class QuestSelectionWindow : LWindow
             foreach (var q in quest.QuestLocks)
             {
                 var qInfo = _questData.GetQuestInfo(q);
-                var (iconColor, icon, _) = GetQuestStyle(q);
+                var (iconColor, icon, _) = _uiUtils.GetQuestStyle(q);
                 // ReSharper disable once UnusedVariable
                 using (var font = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
                 {
@@ -334,6 +324,34 @@ internal sealed class QuestSelectionWindow : LWindow
             }
         }
 
+        if (counter == 0 && quest.PreviousInstanceContent.Count > 0)
+        {
+            if (quest.PreviousInstanceContent.Count > 1)
+            {
+                if (quest.PreviousQuestJoin == QuestInfo.QuestJoin.All)
+                    ImGui.Text("Requires all:");
+                else if (quest.PreviousQuestJoin == QuestInfo.QuestJoin.AtLeastOne)
+                    ImGui.Text("Requires one:");
+            }
+            else
+                ImGui.Text("Requires:");
+
+            foreach (var instanceId in quest.PreviousInstanceContent)
+            {
+                string instanceName = _territoryData.GetInstanceName(instanceId) ?? "?";
+                var (iconColor, icon) = UiUtils.GetInstanceStyle(instanceId);
+
+                // ReSharper disable once UnusedVariable
+                using (var font = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
+                {
+                    ImGui.TextColored(iconColor, icon.ToIconString());
+                }
+
+                ImGui.SameLine();
+                ImGui.TextUnformatted(instanceName);
+            }
+        }
+
         if (counter > 0)
             ImGui.Unindent();
     }
index a5e3044a2aa9624f1284c453b3ebb0c4f009e46d..aff07320e7e5be9d34706ad1053ca853a3fd45fd 100644 (file)
-using System;
-using System.Diagnostics;
-using System.Globalization;
-using System.Linq;
-using System.Numerics;
-using Dalamud.Game.ClientState.Conditions;
-using Dalamud.Game.ClientState.Objects;
-using Dalamud.Game.Text;
-using Dalamud.Interface;
-using Dalamud.Interface.Colors;
-using Dalamud.Interface.Components;
-using Dalamud.Interface.Utility;
-using Dalamud.Interface.Utility.Raii;
+using System.Numerics;
 using Dalamud.Plugin;
 using Dalamud.Plugin.Services;
-using FFXIVClientStructs.FFXIV.Client.Game.Control;
-using FFXIVClientStructs.FFXIV.Client.Game.Object;
-using FFXIVClientStructs.FFXIV.Client.UI.Agent;
 using ImGuiNET;
 using LLib.ImGui;
-using Microsoft.Extensions.Logging;
 using Questionable.Controller;
-using Questionable.Controller.Steps.Shared;
 using Questionable.Data;
-using Questionable.External;
-using Questionable.Model;
-using Questionable.Model.V1;
-using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
+using Questionable.Windows.QuestComponents;
 
 namespace Questionable.Windows;
 
 internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
 {
+
     private readonly IDalamudPluginInterface _pluginInterface;
-    private readonly MovementController _movementController;
     private readonly QuestController _questController;
-    private readonly GameFunctions _gameFunctions;
-    private readonly ChatFunctions _chatFunctions;
     private readonly IClientState _clientState;
-    private readonly IFramework _framework;
-    private readonly ITargetManager _targetManager;
-    private readonly GameUiController _gameUiController;
-    private readonly CombatController _combatController;
     private readonly Configuration _configuration;
-    private readonly NavmeshIpc _navmeshIpc;
-    private readonly QuestRegistry _questRegistry;
-    private readonly QuestData _questData;
     private readonly TerritoryData _territoryData;
-    private readonly ICondition _condition;
-    private readonly IGameGui _gameGui;
-    private readonly QuestSelectionWindow _questSelectionWindow;
-    private readonly QuestValidationWindow _questValidationWindow;
-    private readonly ICommandManager _commandManager;
-    private readonly ILogger<QuestWindow> _logger;
+    private readonly ActiveQuestComponent _activeQuestComponent;
+    private readonly ARealmRebornComponent _aRealmRebornComponent;
+    private readonly CreationUtilsComponent _creationUtilsComponent;
+    private readonly QuickAccessButtonsComponent _quickAccessButtonsComponent;
+    private readonly RemainingTasksComponent _remainingTasksComponent;
 
     public QuestWindow(IDalamudPluginInterface pluginInterface,
-        MovementController movementController,
         QuestController questController,
-        GameFunctions gameFunctions,
-        ChatFunctions chatFunctions,
         IClientState clientState,
-        IFramework framework,
-        ITargetManager targetManager,
-        GameUiController gameUiController,
-        CombatController combatController,
         Configuration configuration,
-        NavmeshIpc navmeshIpc,
-        QuestRegistry questRegistry,
-        QuestData questData,
         TerritoryData territoryData,
-        ICondition condition,
-        IGameGui gameGui,
-        QuestSelectionWindow questSelectionWindow,
-        QuestValidationWindow questValidationWindow,
-        ICommandManager commandManager,
-        ILogger<QuestWindow> logger)
+        ActiveQuestComponent activeQuestComponent,
+        ARealmRebornComponent aRealmRebornComponent,
+        CreationUtilsComponent creationUtilsComponent,
+        QuickAccessButtonsComponent quickAccessButtonsComponent,
+        RemainingTasksComponent remainingTasksComponent)
         : base("Questionable###Questionable", ImGuiWindowFlags.AlwaysAutoResize)
     {
         _pluginInterface = pluginInterface;
-        _movementController = movementController;
         _questController = questController;
-        _gameFunctions = gameFunctions;
-        _chatFunctions = chatFunctions;
         _clientState = clientState;
-        _framework = framework;
-        _targetManager = targetManager;
-        _gameUiController = gameUiController;
-        _combatController = combatController;
         _configuration = configuration;
-        _navmeshIpc = navmeshIpc;
-        _questRegistry = questRegistry;
-        _questData = questData;
         _territoryData = territoryData;
-        _condition = condition;
-        _gameGui = gameGui;
-        _questSelectionWindow = questSelectionWindow;
-        _questValidationWindow = questValidationWindow;
-        _commandManager = commandManager;
-        _logger = logger;
+        _activeQuestComponent = activeQuestComponent;
+        _aRealmRebornComponent = aRealmRebornComponent;
+        _creationUtilsComponent = creationUtilsComponent;
+        _quickAccessButtonsComponent = quickAccessButtonsComponent;
+        _remainingTasksComponent = remainingTasksComponent;
 
 #if DEBUG
         IsOpen = true;
@@ -127,564 +75,19 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
 
     public override void Draw()
     {
-        DrawQuest();
-        ImGui.Separator();
-
-        DrawCreationUtils();
+        _activeQuestComponent.Draw();
         ImGui.Separator();
 
-        DrawQuickAccessButtons();
-        DrawRemainingTasks();
-    }
-
-    private void DrawQuest()
-    {
-        var currentQuestDetails = _questController.CurrentQuestDetails;
-        QuestController.QuestProgress? currentQuest = currentQuestDetails?.Progress;
-        QuestController.CurrentQuestType? currentQuestType = currentQuestDetails?.Type;
-        if (currentQuest != null)
-        {
-            if (currentQuestType == QuestController.CurrentQuestType.Simulated)
-            {
-                var simulatedQuest = _questController.SimulatedQuest ?? currentQuest;
-                using var _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
-                ImGui.TextUnformatted(
-                    $"Simulated Quest: {simulatedQuest.Quest.Info.Name} / {simulatedQuest.Sequence} / {simulatedQuest.Step}");
-            }
-            else if (currentQuestType == QuestController.CurrentQuestType.Next)
-            {
-                var startedQuest = _questController.StartedQuest;
-                if (startedQuest != null)
-                {
-                    ImGui.TextUnformatted(
-                        $"Quest: {startedQuest.Quest.Info.Name} / {startedQuest.Sequence} / {startedQuest.Step}");
-
-                    if (startedQuest.Quest.Root.Disabled)
-                    {
-                        ImGui.SameLine();
-                        ImGui.TextColored(ImGuiColors.DalamudRed, "Disabled");
-                    }
-                }
-
-                using var _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow);
-                ImGui.TextUnformatted(
-                    $"Next Quest: {currentQuest.Quest.Info.Name} / {currentQuest.Sequence} / {currentQuest.Step}");
-            }
-            else
-            {
-                ImGui.TextUnformatted(
-                    $"Quest: {currentQuest.Quest.Info.Name} / {currentQuest.Sequence} / {currentQuest.Step}");
-
-                if (currentQuest.Quest.Root.Disabled)
-                {
-                    ImGui.SameLine();
-                    ImGui.TextColored(ImGuiColors.DalamudRed, "Disabled");
-                }
-            }
-
-
-            ImGui.BeginDisabled();
-            var questWork = _gameFunctions.GetQuestEx(currentQuest.Quest.QuestId);
-            if (questWork != null)
-            {
-                var qw = questWork.Value;
-                string vars = "";
-                for (int i = 0; i < 6; ++i)
-                {
-                    vars += qw.Variables[i] + " ";
-                    if (i % 2 == 1)
-                        vars += "   ";
-                }
-
-                // For combat quests, a sequence to kill 3 enemies works a bit like this:
-                // Trigger enemies → 0
-                // Kill first enemy → 1
-                // Kill second enemy → 2
-                // Last enemy → increase sequence, reset variable to 0
-                // The order in which enemies are killed doesn't seem to matter.
-                // If multiple waves spawn, this continues to count up (e.g. 1 enemy from wave 1, 2 enemies from wave 2, 1 from wave 3) would count to 3 then 0
-                ImGui.Text($"QW: {vars.Trim()}");
-            }
-            else
-            {
-                if (currentQuest.Quest.QuestId == _questController.NextQuest?.Quest.QuestId)
-                    ImGui.TextUnformatted("(Next quest in story line not accepted)");
-                else
-                    ImGui.TextUnformatted("(Not accepted)");
-            }
-
-            ImGui.EndDisabled();
-
-            if (_combatController.IsRunning)
-                ImGui.TextColored(ImGuiColors.DalamudOrange, "In Combat");
-            else
-            {
-                ImGui.BeginDisabled();
-                ImGui.TextUnformatted(_questController.DebugState ?? string.Empty);
-                ImGui.EndDisabled();
-            }
-
-            QuestSequence? currentSequence = currentQuest.Quest.FindSequence(currentQuest.Sequence);
-            QuestStep? currentStep = currentSequence?.FindStep(currentQuest.Step);
-            bool colored = currentStep is
-                { InteractionType: EInteractionType.Instruction or EInteractionType.WaitForManualProgress };
-            if (colored)
-                ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudOrange);
-            ImGui.TextUnformatted(currentStep?.Comment ?? currentSequence?.Comment ?? currentQuest.Quest.Root.Comment ?? string.Empty);
-            if (colored)
-                ImGui.PopStyleColor();
-
-            //var nextStep = _questController.GetNextStep();
-            //ImGui.BeginDisabled(nextStep.Step == null);
-            ImGui.Text(_questController.ToStatString());
-            //ImGui.EndDisabled();
-
-            ImGui.BeginDisabled(_questController.IsRunning);
-            if (ImGuiComponents.IconButton(FontAwesomeIcon.Play))
-            {
-                // if we haven't accepted this quest, mark it as next quest so that we can optionally use aetherytes to travel
-                if (questWork == null)
-                    _questController.SetNextQuest(currentQuest.Quest);
-
-                _questController.ExecuteNextStep(true);
-            }
-
-            ImGui.SameLine();
-
-            if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.StepForward, "Step"))
-            {
-                _questController.ExecuteNextStep(false);
-            }
-
-            ImGui.EndDisabled();
-            ImGui.SameLine();
-
-            if (ImGuiComponents.IconButton(FontAwesomeIcon.Stop))
-            {
-                _movementController.Stop();
-                _questController.Stop("Manual");
-            }
-
-            bool lastStep = currentStep ==
-                            currentQuest.Quest.FindSequence(currentQuest.Sequence)?.Steps.LastOrDefault();
-            colored = currentStep != null
-                      && !lastStep
-                      && currentStep.InteractionType == EInteractionType.Instruction
-                      && _questController.HasCurrentTaskMatching<WaitAtEnd.WaitNextStepOrSequence>();
-
-            ImGui.BeginDisabled(lastStep);
-            if (colored)
-                ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.HealerGreen);
-            if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.ArrowCircleRight, "Skip"))
-            {
-                _movementController.Stop();
-                _questController.Skip(currentQuest.Quest.QuestId, currentQuest.Sequence);
-            }
-
-            if (colored)
-                ImGui.PopStyleColor();
-            ImGui.EndDisabled();
-
-            if (_commandManager.Commands.TryGetValue("/questinfo", out var commandInfo))
-            {
-                ImGui.SameLine();
-                if (ImGuiComponents.IconButton(FontAwesomeIcon.Atlas))
-                    _commandManager.DispatchCommand("/questinfo",
-                        currentQuest.Quest.QuestId.ToString(CultureInfo.InvariantCulture), commandInfo);
-            }
-
-            bool autoAcceptNextQuest = _configuration.General.AutoAcceptNextQuest;
-            if (ImGui.Checkbox("Automatically accept next quest", ref autoAcceptNextQuest))
-            {
-                _configuration.General.AutoAcceptNextQuest = autoAcceptNextQuest;
-                _pluginInterface.SavePluginConfig(_configuration);
-            }
-
-
-            if (_questController.SimulatedQuest != null)
-            {
-                var simulatedQuest = _questController.SimulatedQuest;
-
-                ImGui.Separator();
-                ImGui.TextColored(ImGuiColors.DalamudRed, "Quest sim active (experimental)");
-                ImGui.Text($"Sequence: {simulatedQuest.Sequence}");
-
-                ImGui.BeginDisabled(simulatedQuest.Sequence == 0);
-                if (ImGuiComponents.IconButton(FontAwesomeIcon.Minus))
-                {
-                    _movementController.Stop();
-                    _questController.Stop("Sim-");
-
-                    byte oldSequence = simulatedQuest.Sequence;
-                    byte newSequence = simulatedQuest.Quest.Root.QuestSequence
-                        .Select(x => (byte)x.Sequence)
-                        .LastOrDefault(x => x < oldSequence, byte.MinValue);
-
-                    _questController.SimulatedQuest.SetSequence(newSequence);
-                }
-
-                ImGui.EndDisabled();
-
-                ImGui.SameLine();
-                ImGui.BeginDisabled(simulatedQuest.Sequence >= 255);
-                if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus))
-                {
-                    _movementController.Stop();
-                    _questController.Stop("Sim+");
-
-                    byte oldSequence = simulatedQuest.Sequence;
-                    byte newSequence = simulatedQuest.Quest.Root.QuestSequence
-                        .Select(x => (byte)x.Sequence)
-                        .FirstOrDefault(x => x > oldSequence, byte.MaxValue);
-
-                    simulatedQuest.SetSequence(newSequence);
-                }
-
-                ImGui.EndDisabled();
-
-                var simulatedSequence = simulatedQuest.Quest.FindSequence(simulatedQuest.Sequence);
-                if (simulatedSequence != null)
-                {
-                    using var _ = ImRaii.PushId("SimulatedStep");
-
-                    ImGui.Text($"Step: {simulatedQuest.Step} / {simulatedSequence.Steps.Count - 1}");
-
-                    ImGui.BeginDisabled(simulatedQuest.Step == 0);
-                    if (ImGuiComponents.IconButton(FontAwesomeIcon.Minus))
-                    {
-                        _movementController.Stop();
-                        _questController.Stop("SimStep-");
-
-                        simulatedQuest.SetStep(Math.Min(simulatedQuest.Step - 1,
-                            simulatedSequence.Steps.Count - 1));
-                    }
-
-                    ImGui.EndDisabled();
-
-                    ImGui.SameLine();
-                    ImGui.BeginDisabled(simulatedQuest.Step >= simulatedSequence.Steps.Count);
-                    if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus))
-                    {
-                        _movementController.Stop();
-                        _questController.Stop("SimStep+");
-
-                        simulatedQuest.SetStep(
-                            simulatedQuest.Step == simulatedSequence.Steps.Count - 1
-                                ? 255
-                                : (simulatedQuest.Step + 1));
-                    }
-
-                    ImGui.EndDisabled();
-
-                    if (ImGui.Button("Skip current task"))
-                    {
-                        _questController.SkipSimulatedTask();
-                    }
-
-                    ImGui.SameLine();
-                    if (ImGui.Button("Clear sim"))
-                    {
-                        _questController.SimulateQuest(null);
-
-                        _movementController.Stop();
-                        _questController.Stop("ClearSim");
-                    }
-                }
-            }
-        }
-        else
-        {
-            ImGui.Text("No active quest");
-            ImGui.TextColored(ImGuiColors.DalamudGrey, $"{_questRegistry.Count} quests loaded");
-        }
-    }
-
-    private unsafe void DrawCreationUtils()
-    {
-        Debug.Assert(_clientState.LocalPlayer != null, "_clientState.LocalPlayer != null");
-
-        string territoryName = _territoryData.GetNameAndId(_clientState.TerritoryType);
-        ImGui.Text(territoryName);
-
-        if (_gameFunctions.IsFlyingUnlockedInCurrentZone())
-        {
-            ImGui.SameLine();
-            ImGui.Text(SeIconChar.BotanistSprout.ToIconString());
-        }
-
-        var q = _gameFunctions.GetCurrentQuest();
-        ImGui.Text($"Current Quest: {q.CurrentQuest} → {q.Sequence}");
-
-#if false
-        var questManager = QuestManager.Instance();
-        if (questManager != null)
-        {
-            for (int i = questManager->TrackedQuests.Length - 1; i >= 0; --i)
-            {
-                var trackedQuest = questManager->TrackedQuests[i];
-                switch (trackedQuest.QuestType)
-                {
-                    default:
-                        ImGui.Text($"Tracked quest {i}: {trackedQuest.QuestType}, {trackedQuest.Index}");
-                        break;
-
-                    case 1:
-                        _questRegistry.TryGetQuest(questManager->NormalQuests[trackedQuest.Index].QuestId,
-                            out var quest);
-                        ImGui.Text(
-                            $"Tracked quest: {questManager->NormalQuests[trackedQuest.Index].QuestId}, {trackedQuest.Index}: {quest?.Info.Name}");
-                        break;
-                }
-            }
-        }
-#endif
-
-        if (_targetManager.Target != null)
+        if (_aRealmRebornComponent.ShouldDraw)
         {
+            _aRealmRebornComponent.Draw();
             ImGui.Separator();
-            ImGui.Text(string.Create(CultureInfo.InvariantCulture,
-                $"Target: {_targetManager.Target.Name}  ({_targetManager.Target.ObjectKind}; {_targetManager.Target.DataId})"));
-
-            GameObject* gameObject = (GameObject*)_targetManager.Target.Address;
-            ImGui.Text(string.Create(CultureInfo.InvariantCulture,
-                $"Distance: {(_targetManager.Target.Position - _clientState.LocalPlayer.Position).Length():F2}"));
-            ImGui.SameLine();
-
-            float verticalDistance = _targetManager.Target.Position.Y - _clientState.LocalPlayer.Position.Y;
-            string verticalDistanceText = string.Create(CultureInfo.InvariantCulture, $"Y: {verticalDistance:F2}");
-            if (Math.Abs(verticalDistance) >= MovementController.DefaultVerticalInteractionDistance)
-                ImGui.TextColored(ImGuiColors.DalamudOrange, verticalDistanceText);
-            else
-                ImGui.Text(verticalDistanceText);
-
-            ImGui.SameLine();
-            ImGui.Text($"QM: {gameObject->NamePlateIconId}");
-
-            ImGui.BeginDisabled(!_movementController.IsNavmeshReady);
-            if (!_movementController.IsPathfinding)
-            {
-                if (ImGui.Button("Move to Target"))
-                {
-                    _movementController.NavigateTo(EMovementType.DebugWindow, _targetManager.Target.DataId,
-                        _targetManager.Target.Position,
-                        fly: _condition[ConditionFlag.Mounted] && _gameFunctions.IsFlyingUnlockedInCurrentZone(),
-                        sprint: true);
-                }
-            }
-            else
-            {
-                if (ImGui.Button("Cancel pathfinding"))
-                    _movementController.ResetPathfinding();
-            }
-
-            ImGui.EndDisabled();
-
-            ImGui.SameLine();
-            ImGui.BeginDisabled(!_questData.IsIssuerOfAnyQuest(_targetManager.Target.DataId));
-            bool showQuests = ImGuiComponents.IconButton(FontAwesomeIcon.MapMarkerAlt);
-            if (ImGui.IsItemHovered())
-                ImGui.SetTooltip("Show all Quests starting with your current target.");
-            if (showQuests)
-                _questSelectionWindow.OpenForTarget(_targetManager.Target);
-
-            ImGui.EndDisabled();
-
-            ImGui.SameLine();
-            bool interact = ImGuiComponents.IconButton(FontAwesomeIcon.MousePointer);
-            if (ImGui.IsItemHovered())
-                ImGui.SetTooltip("Interact with your current target.");
-            if (interact)
-            {
-                ulong result = TargetSystem.Instance()->InteractWithObject(
-                    (GameObject*)_targetManager.Target.Address, false);
-                _logger.LogInformation("XXXXX Interaction Result: {Result}", result);
-            }
-
-            ImGui.SameLine();
-
-            bool copy = ImGuiComponents.IconButton(FontAwesomeIcon.Copy);
-            if (ImGui.IsItemHovered())
-                ImGui.SetTooltip(
-                    "Left click: Copy target position as JSON.\nRight click: Copy target position as C# code.");
-            if (copy)
-            {
-                string interactionType = gameObject->NamePlateIconId switch
-                {
-                    71201 or 71211 or 71221 or 71231 or 71341 or 71351 => "AcceptQuest",
-                    71202 or 71212 or 71222 or 71232 or 71342 or 71352 => "AcceptQuest", // repeatable
-                    71205 or 71215 or 71225 or 71235 or 71345 or 71355 => "CompleteQuest",
-                    _ => "Interact",
-                };
-                ImGui.SetClipboardText($$"""
-                                         "DataId": {{_targetManager.Target.DataId}},
-                                         "Position": {
-                                             "X": {{_targetManager.Target.Position.X.ToString(CultureInfo.InvariantCulture)}},
-                                             "Y": {{_targetManager.Target.Position.Y.ToString(CultureInfo.InvariantCulture)}},
-                                             "Z": {{_targetManager.Target.Position.Z.ToString(CultureInfo.InvariantCulture)}}
-                                         },
-                                         "TerritoryId": {{_clientState.TerritoryType}},
-                                         "InteractionType": "{{interactionType}}"
-                                         """);
-            }
-            else if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
-            {
-                if (_targetManager.Target.ObjectKind == ObjectKind.Aetheryte)
-                {
-                    EAetheryteLocation location = (EAetheryteLocation)_targetManager.Target.DataId;
-                    ImGui.SetClipboardText(string.Create(CultureInfo.InvariantCulture,
-                        $"{{EAetheryteLocation.{location}, new({_targetManager.Target.Position.X}f, {_targetManager.Target.Position.Y}f, {_targetManager.Target.Position.Z}f)}},"));
-                }
-                else
-                    ImGui.SetClipboardText(string.Create(CultureInfo.InvariantCulture,
-                        $"new({_targetManager.Target.Position.X}f, {_targetManager.Target.Position.Y}f, {_targetManager.Target.Position.Z}f)"));
-            }
-        }
-        else
-        {
-            bool copy = ImGuiComponents.IconButton(FontAwesomeIcon.Copy);
-            if (ImGui.IsItemHovered())
-                ImGui.SetTooltip(
-                    "Left click: Copy your position as JSON.\nRight click: Copy your position as C# code.");
-            if (copy)
-            {
-                ImGui.SetClipboardText($$"""
-                                         "Position": {
-                                             "X": {{_clientState.LocalPlayer.Position.X.ToString(CultureInfo.InvariantCulture)}},
-                                             "Y": {{_clientState.LocalPlayer.Position.Y.ToString(CultureInfo.InvariantCulture)}},
-                                             "Z": {{_clientState.LocalPlayer.Position.Z.ToString(CultureInfo.InvariantCulture)}}
-                                         },
-                                         "TerritoryId": {{_clientState.TerritoryType}},
-                                         "InteractionType": ""
-                                         """);
-            }
-            else if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
-            {
-                Vector3 position = _clientState.LocalPlayer!.Position;
-                ImGui.SetClipboardText(string.Create(CultureInfo.InvariantCulture,
-                    $"new({position.X}f, {position.Y}f, {position.Z}f)"));
-            }
-        }
-
-        ulong hoveredItemId = _gameGui.HoveredItem;
-        if (hoveredItemId != 0)
-        {
-            ImGui.Separator();
-            ImGui.Text($"Hovered Item: {hoveredItemId}");
-        }
-    }
-
-    private unsafe void DrawQuickAccessButtons()
-    {
-        var map = AgentMap.Instance();
-        ImGui.BeginDisabled(map == null || map->IsFlagMarkerSet == 0 ||
-                            map->FlagMapMarker.TerritoryId != _clientState.TerritoryType ||
-                            !_navmeshIpc.IsReady);
-        if (ImGui.Button("Move to Flag"))
-        {
-            _movementController.Destination = null;
-            _chatFunctions.ExecuteCommand(
-                $"/vnav {(_condition[ConditionFlag.Mounted] && _gameFunctions.IsFlyingUnlockedInCurrentZone() ? "flyflag" : "moveflag")}");
-        }
-
-        ImGui.EndDisabled();
-
-        ImGui.SameLine();
-
-        ImGui.BeginDisabled(!_movementController.IsPathRunning);
-        if (ImGui.Button("Stop Nav"))
-        {
-            _movementController.Stop();
-            _questController.Stop("Manual");
         }
 
-        ImGui.EndDisabled();
-
-        if (ImGui.Button("Reload Data"))
-        {
-            _questController.Reload();
-            _framework.RunOnTick(() => _gameUiController.HandleCurrentDialogueChoices(),
-                TimeSpan.FromMilliseconds(200));
-        }
-
-        if (_questRegistry.ValidationIssueCount > 0)
-        {
-            ImGui.SameLine();
-            if (DrawValidationIssuesButton())
-                _questValidationWindow.IsOpen = true;
-        }
-    }
-
-    private bool DrawValidationIssuesButton()
-    {
-        int errorCount = _questRegistry.ValidationErrorCount;
-        int infoCount = _questRegistry.ValidationIssueCount - _questRegistry.ValidationErrorCount;
-        if (errorCount == 0 && infoCount == 0)
-            return false;
-
-        int partsToRender = errorCount == 0 || infoCount == 0 ? 1 : 2;
-        using var id = ImRaii.PushId("validationissues");
-
-        ImGui.PushFont(UiBuilder.IconFont);
-        var icon1 = FontAwesomeIcon.TimesCircle;
-        var icon2 = FontAwesomeIcon.InfoCircle;
-        Vector2 iconSize1 = errorCount > 0 ? ImGui.CalcTextSize(icon1.ToIconString()) : Vector2.Zero;
-        Vector2 iconSize2 = infoCount > 0 ? ImGui.CalcTextSize(icon2.ToIconString()) : Vector2.Zero;
-        ImGui.PopFont();
-
-        string text1 = errorCount > 0 ? errorCount.ToString(CultureInfo.InvariantCulture) : string.Empty;
-        string text2 = infoCount > 0 ? infoCount.ToString(CultureInfo.InvariantCulture) : string.Empty;
-        Vector2 textSize1 = errorCount > 0 ? ImGui.CalcTextSize(text1) : Vector2.Zero;
-        Vector2 textSize2 = infoCount > 0 ? ImGui.CalcTextSize(text2) : Vector2.Zero;
-        var dl = ImGui.GetWindowDrawList();
-        var cursor = ImGui.GetCursorScreenPos();
-
-        var iconPadding = 3 * ImGuiHelpers.GlobalScale;
-
-        // Draw an ImGui button with the icon and text
-        var buttonWidth = iconSize1.X + iconSize2.X + textSize1.X + textSize2.X +
-                          (ImGui.GetStyle().FramePadding.X * 2) + iconPadding * 2 * partsToRender;
-        var buttonHeight = ImGui.GetFrameHeight();
-        var button = ImGui.Button(string.Empty, new Vector2(buttonWidth, buttonHeight));
-
-        // Draw the icon on the window drawlist
-        Vector2 position = new Vector2(cursor.X + ImGui.GetStyle().FramePadding.X,
-            cursor.Y + ImGui.GetStyle().FramePadding.Y);
-        if (errorCount > 0)
-        {
-            ImGui.PushFont(UiBuilder.IconFont);
-            dl.AddText(position, ImGui.GetColorU32(ImGuiColors.DalamudRed), icon1.ToIconString());
-            ImGui.PopFont();
-            position = position with { X = position.X + iconSize1.X + iconPadding };
-
-            // Draw the text on the window drawlist
-            dl.AddText(position, ImGui.GetColorU32(ImGuiCol.Text), text1);
-            position = position with { X = position.X + textSize1.X + 2 * iconPadding };
-        }
-
-        if (infoCount > 0)
-        {
-            ImGui.PushFont(UiBuilder.IconFont);
-            dl.AddText(position, ImGui.GetColorU32(ImGuiColors.ParsedBlue), icon2.ToIconString());
-            ImGui.PopFont();
-            position = position with { X = position.X + iconSize2.X + iconPadding };
-
-            // Draw the text on the window drawlist
-            dl.AddText(position, ImGui.GetColorU32(ImGuiCol.Text), text2);
-        }
-
-        return button;
-    }
+        _creationUtilsComponent.Draw();
+        ImGui.Separator();
 
-    private void DrawRemainingTasks()
-    {
-        var remainingTasks = _questController.GetRemainingTaskNames();
-        if (remainingTasks.Count > 0)
-        {
-            ImGui.Separator();
-            ImGui.BeginDisabled();
-            foreach (var task in remainingTasks)
-                ImGui.TextUnformatted(task);
-            ImGui.EndDisabled();
-        }
+        _quickAccessButtonsComponent.Draw();
+        _remainingTasksComponent.Draw();
     }
 }
diff --git a/Questionable/Windows/UiUtils.cs b/Questionable/Windows/UiUtils.cs
new file mode 100644 (file)
index 0000000..f64b06f
--- /dev/null
@@ -0,0 +1,57 @@
+using System.Numerics;
+using Dalamud.Interface;
+using Dalamud.Interface.Colors;
+using Dalamud.Plugin;
+using FFXIVClientStructs.FFXIV.Client.Game.UI;
+using ImGuiNET;
+
+namespace Questionable.Windows;
+
+internal sealed class UiUtils
+{
+    private readonly GameFunctions _gameFunctions;
+    private readonly IDalamudPluginInterface _pluginInterface;
+
+    public UiUtils(GameFunctions gameFunctions, IDalamudPluginInterface pluginInterface)
+    {
+        _gameFunctions = gameFunctions;
+        _pluginInterface = pluginInterface;
+    }
+
+    public (Vector4 color, FontAwesomeIcon icon, string status) GetQuestStyle(ushort questId)
+    {
+        if (_gameFunctions.IsQuestAccepted(questId))
+            return (ImGuiColors.DalamudYellow, FontAwesomeIcon.Running, "Active");
+        else if (_gameFunctions.IsQuestAcceptedOrComplete(questId))
+            return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check, "Complete");
+        else if (_gameFunctions.IsQuestLocked(questId))
+            return (ImGuiColors.DalamudRed, FontAwesomeIcon.Times, "Locked");
+        else
+            return (ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight, "Available");
+    }
+
+    public static (Vector4 color, FontAwesomeIcon icon) GetInstanceStyle(ushort instanceId)
+    {
+        if (UIState.IsInstanceContentCompleted(instanceId))
+            return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check);
+        else if (UIState.IsInstanceContentUnlocked(instanceId))
+            return (ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight);
+        else
+            return (ImGuiColors.DalamudRed, FontAwesomeIcon.Times);
+    }
+
+    public bool ChecklistItem(string text, Vector4 color, FontAwesomeIcon icon)
+    {
+        // ReSharper disable once UnusedVariable
+        using (var font = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
+        {
+            ImGui.TextColored(color, icon.ToIconString());
+        }
+
+        bool hover = ImGui.IsItemHovered();
+
+        ImGui.SameLine();
+        ImGui.TextUnformatted(text);
+        return hover;
+    }
+}