diff --git a/Questionable/Configuration.cs b/Questionable/Configuration.cs index 15a07b8c..4b276961 100644 --- a/Questionable/Configuration.cs +++ b/Questionable/Configuration.cs @@ -24,5 +24,6 @@ internal sealed class Configuration : IPluginConfiguration { public bool DebugOverlay { get; set; } public bool NeverFly { get; set; } + public bool AdditionalStatusInformation { get; set; } } } diff --git a/Questionable/Controller/NavigationOverrides/MovementOverrideController.cs b/Questionable/Controller/NavigationOverrides/MovementOverrideController.cs index 1a1a1669..0cb2b4f3 100644 --- a/Questionable/Controller/NavigationOverrides/MovementOverrideController.cs +++ b/Questionable/Controller/NavigationOverrides/MovementOverrideController.cs @@ -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 diff --git a/Questionable/Controller/QuestController.cs b/Questionable/Controller/QuestController.cs index 141bc91d..b9a011d7 100644 --- a/Questionable/Controller/QuestController.cs +++ b/Questionable/Controller/QuestController.cs @@ -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); diff --git a/Questionable/Controller/Steps/Interactions/UseItem.cs b/Questionable/Controller/Steps/Interactions/UseItem.cs index 8868f811..cb6c0ff0 100644 --- a/Questionable/Controller/Steps/Interactions/UseItem.cs +++ b/Questionable/Controller/Steps/Interactions/UseItem.cs @@ -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( diff --git a/Questionable/Data/TerritoryData.cs b/Questionable/Data/TerritoryData.cs index fc541e77..0175cf6d 100644 --- a/Questionable/Data/TerritoryData.cs +++ b/Questionable/Data/TerritoryData.cs @@ -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 _territoryNames; private readonly ImmutableHashSet _territoriesWithMount; private readonly ImmutableHashSet _dutyTerritories; + private readonly ImmutableDictionary _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()! + .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); } diff --git a/Questionable/GameFunctions.cs b/Questionable/GameFunctions.cs index b2951732..ff74a708 100644 --- a/Questionable/GameFunctions.cs +++ b/Questionable/GameFunctions.cs @@ -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) diff --git a/Questionable/Model/QuestInfo.cs b/Questionable/Model/QuestInfo.cs index 4ae13547..d0d1913b 100644 --- a/Questionable/Model/QuestInfo.cs +++ b/Questionable/Model/QuestInfo.cs @@ -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 QuestLocks { get; set; } public QuestJoin QuestLockJoin { get; set; } + public List PreviousInstanceContent { get; set; } + public QuestJoin PreviousInstanceContentJoin { get; set; } public bool IsMainScenarioQuest { get; } public bool CompletesInstantly { get; set; } diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index 1cb6c4c1..90286f74 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -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(); + + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/Questionable/Windows/ConfigWindow.cs b/Questionable/Windows/ConfigWindow.cs index a24b6734..11693300 100644 --- a/Questionable/Windows/ConfigWindow.cs +++ b/Questionable/Windows/ConfigWindow.cs @@ -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 index 00000000..dbc880c6 --- /dev/null +++ b/Questionable/Windows/QuestComponents/ARealmRebornComponent.cs @@ -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 index 00000000..48b9434a --- /dev/null +++ b/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs @@ -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(); + + 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 index 00000000..a67221a4 --- /dev/null +++ b/Questionable/Windows/QuestComponents/CreationUtilsComponent.cs @@ -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 _logger; + + public CreationUtilsComponent(MovementController movementController, GameFunctions gameFunctions, + TerritoryData territoryData, QuestData questData, QuestSelectionWindow questSelectionWindow, + IClientState clientState, ITargetManager targetManager, ICondition condition, IGameGui gameGui, + ILogger 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 index 00000000..61ed51b7 --- /dev/null +++ b/Questionable/Windows/QuestComponents/QuickAccessButtonsComponent.cs @@ -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 index 00000000..bd35f699 --- /dev/null +++ b/Questionable/Windows/QuestComponents/RemainingTasksComponent.cs @@ -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(); + } + } +} diff --git a/Questionable/Windows/QuestSelectionWindow.cs b/Questionable/Windows/QuestSelectionWindow.cs index 9ed5c439..1594c497 100644 --- a/Questionable/Windows/QuestSelectionWindow.cs +++ b/Questionable/Windows/QuestSelectionWindow.cs @@ -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 _quests = []; private List _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(); } diff --git a/Questionable/Windows/QuestWindow.cs b/Questionable/Windows/QuestWindow.cs index a5e3044a..aff07320 100644 --- a/Questionable/Windows/QuestWindow.cs +++ b/Questionable/Windows/QuestWindow.cs @@ -1,102 +1,50 @@ -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 _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 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(); + _activeQuestComponent.Draw(); ImGui.Separator(); - DrawCreationUtils(); + if (_aRealmRebornComponent.ShouldDraw) + { + _aRealmRebornComponent.Draw(); + ImGui.Separator(); + } + + _creationUtilsComponent.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(); - - 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) - { - 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; - } - - 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 index 00000000..f64b06f1 --- /dev/null +++ b/Questionable/Windows/UiUtils.cs @@ -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; + } +}