1
0
forked from liza/Questionable

Split QuestWindow into components; fix item use; show required instances to unlock quests

This commit is contained in:
Liza 2024-07-21 15:30:10 +02:00
parent 07b4765478
commit a9b25e3722
Signed by: liza
GPG Key ID: 7199F8D727D55F67
17 changed files with 1028 additions and 657 deletions

View 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; }
}
}

View 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

View 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);

View 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(

View 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);
}

View 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,18 +291,36 @@ internal sealed unsafe class GameFunctions
return true;
}
if (questInfo.PreviousQuests.Count > 0)
return !HasCompletedPreviousQuests(questInfo, extraCompletedQuest) || !HasCompletedPreviousInstances(questInfo);
}
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 false;
else if (questInfo.PreviousQuestJoin == QuestInfo.QuestJoin.AtLeastOne && completedQuests > 0)
return false;
else
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;
}

View 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; }

View 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>();

View 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();
}

View File

@ -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);
}
}
}

View File

@ -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");
}
}
}
}

View File

@ -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}");
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}

View 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();
}

View File

@ -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<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();
_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<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)
{
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();
}
}

View File

@ -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;
}
}