Clean up quest validation

This commit is contained in:
Liza 2024-07-16 00:18:10 +02:00
parent 202abcf3a8
commit db61878683
Signed by: liza
GPG Key ID: 7199F8D727D55F67
15 changed files with 435 additions and 122 deletions

View File

@ -15,6 +15,7 @@ using Questionable.Controller.Utils;
using Questionable.Data; using Questionable.Data;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.V1; using Questionable.Model.V1;
using Questionable.Validation;
namespace Questionable.Controller; namespace Questionable.Controller;
@ -23,21 +24,24 @@ internal sealed class QuestRegistry
private readonly IDalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _pluginInterface;
private readonly QuestData _questData; private readonly QuestData _questData;
private readonly IChatGui _chatGui; private readonly IChatGui _chatGui;
private readonly QuestValidator _questValidator;
private readonly ILogger<QuestRegistry> _logger; private readonly ILogger<QuestRegistry> _logger;
private readonly Dictionary<ushort, Quest> _quests = new(); private readonly Dictionary<ushort, Quest> _quests = new();
public QuestRegistry(IDalamudPluginInterface pluginInterface, QuestData questData, IChatGui chatGui, public QuestRegistry(IDalamudPluginInterface pluginInterface, QuestData questData, IChatGui chatGui,
ILogger<QuestRegistry> logger) QuestValidator questValidator, ILogger<QuestRegistry> logger)
{ {
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_questData = questData; _questData = questData;
_chatGui = chatGui; _chatGui = chatGui;
_questValidator = questValidator;
_logger = logger; _logger = logger;
} }
public IEnumerable<Quest> AllQuests => _quests.Values; public IEnumerable<Quest> AllQuests => _quests.Values;
public int Count => _quests.Count; public int Count => _quests.Count;
public int ValidationIssueCount => _questValidator.IssueCount;
public void Reload() public void Reload()
{ {
@ -110,113 +114,8 @@ internal sealed class QuestRegistry
[Conditional("DEBUG")] [Conditional("DEBUG")]
private void ValidateQuests() private void ValidateQuests()
{ {
Task.Run(() => _questValidator.ClearIssues();
{ _questValidator.Validate(_quests.Values);
try
{
int foundProblems = 0;
foreach (var quest in _quests.Values)
{
int missingSteps = quest.Root.QuestSequence.Where(x => x.Sequence < 255).Max(x => x.Sequence) -
quest.Root.QuestSequence.Count(x => x.Sequence < 255) + 1;
if (missingSteps != 0)
{
_logger.LogWarning("Quest has missing steps: {QuestId} / {QuestName} → {Count}", quest.QuestId,
quest.Info.Name, missingSteps);
++foundProblems;
}
var totalSequenceCount = quest.Root.QuestSequence.Count;
var distinctSequenceCount = quest.Root.QuestSequence.Select(x => x.Sequence).Distinct().Count();
if (totalSequenceCount != distinctSequenceCount)
{
_logger.LogWarning("Quest has duplicate sequence numbers: {QuestId} / {QuestName}",
quest.QuestId,
quest.Info.Name);
++foundProblems;
}
foreach (var sequence in quest.Root.QuestSequence)
{
if (sequence.Sequence == 0 &&
sequence.Steps.LastOrDefault()?.InteractionType != EInteractionType.AcceptQuest)
{
_logger.LogWarning(
"Quest likely has AcceptQuest configured wrong: {QuestId} / {QuestName} → {Sequence} / {Step}",
quest.QuestId, quest.Info.Name, sequence.Sequence, sequence.Steps.Count - 1);
++foundProblems;
}
else if (sequence.Sequence == 255 &&
sequence.Steps.LastOrDefault()?.InteractionType != EInteractionType.CompleteQuest)
{
_logger.LogWarning(
"Quest likely has CompleteQuest configured wrong: {QuestId} / {QuestName} → {Sequence} / {Step}",
quest.QuestId, quest.Info.Name, sequence.Sequence, sequence.Steps.Count - 1);
++foundProblems;
}
var acceptQuestSteps = sequence.Steps
.Where(x => x is { InteractionType: EInteractionType.AcceptQuest, PickupQuestId: null })
.Where(x => sequence.Sequence != 0 || x != sequence.Steps.Last());
foreach (var step in acceptQuestSteps)
{
_logger.LogWarning(
"Quest has unexpected AcceptQuest steps: {QuestId} / {QuestName} → {Sequence} / {Step}",
quest.QuestId, quest.Info.Name, sequence.Sequence, sequence.Steps.IndexOf(step));
++foundProblems;
}
var completeQuestSteps = sequence.Steps
.Where(x => x is { InteractionType: EInteractionType.CompleteQuest, TurnInQuestId: null })
.Where(x => sequence.Sequence != 255 || x != sequence.Steps.Last());
foreach (var step in completeQuestSteps)
{
_logger.LogWarning(
"Quest has unexpected CompleteQuest steps: {QuestId} / {QuestName} → {Sequence} / {Step}",
quest.QuestId, quest.Info.Name, sequence.Sequence, sequence.Steps.IndexOf(step));
++foundProblems;
}
var completionFlags = sequence.Steps.Select(x => x.CompletionQuestVariablesFlags)
.Where(QuestWorkUtils.HasCompletionFlags)
.GroupBy(x =>
{
return Enumerable.Range(0, 6).Select(y =>
{
short? value = x[y];
if (value == null || value.Value < 0)
return (long)0;
return (long)BitOperations.RotateLeft((ulong)value.Value, 8 * y);
})
.Sum();
})
.Where(x => x.Key != 0)
.Where(x => x.Count() > 1);
foreach (var duplicate in completionFlags)
{
_logger.LogWarning(
"Quest step has duplicate completion flags: {QuestId} / {QuestName} → {Sequence} → {Flags}",
quest.QuestId, quest.Info.Name, sequence.Sequence,
string.Join(", ", duplicate.First()));
++foundProblems;
}
}
}
if (foundProblems > 0)
{
_chatGui.Print(
$"[Questionable] Quest validation has found {foundProblems} problems. Check the log for details.");
}
}
catch (Exception e)
{
_logger.LogError(e, "Unable to validate quests");
_chatGui.PrintError(
$"[Questionable] Unable to validate quests. Check the log for details.");
}
});
} }
@ -233,13 +132,6 @@ internal sealed class QuestRegistry
Root = JsonSerializer.Deserialize<QuestRoot>(stream)!, Root = JsonSerializer.Deserialize<QuestRoot>(stream)!,
Info = _questData.GetQuestInfo(questId.Value), Info = _questData.GetQuestInfo(questId.Value),
}; };
if (quest.Root.Disabled)
{
_logger.LogWarning("Quest {QuestId} / {QuestName} is disabled and won't be loaded", questId,
quest.Info.Name);
return;
}
_quests[questId.Value] = quest; _quests[questId.Value] = quest;
} }
@ -281,8 +173,8 @@ internal sealed class QuestRegistry
return ushort.Parse(parts[0], CultureInfo.InvariantCulture); return ushort.Parse(parts[0], CultureInfo.InvariantCulture);
} }
public bool IsKnownQuest(ushort questId) => _quests.ContainsKey(questId); public bool IsKnownQuest(ushort questId) => TryGetQuest(questId, out _);
public bool TryGetQuest(ushort questId, [NotNullWhen(true)] out Quest? quest) public bool TryGetQuest(ushort questId, [NotNullWhen(true)] out Quest? quest)
=> _quests.TryGetValue(questId, out quest); => _quests.TryGetValue(questId, out quest) && !quest.Root.Disabled;
} }

View File

@ -29,7 +29,8 @@ internal sealed class DalamudInitializer : IDisposable
QuestWindow questWindow, QuestWindow questWindow,
DebugOverlay debugOverlay, DebugOverlay debugOverlay,
ConfigWindow configWindow, ConfigWindow configWindow,
QuestSelectionWindow questSelectionWindow) QuestSelectionWindow questSelectionWindow,
QuestValidationWindow questValidationWindow)
{ {
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_framework = framework; _framework = framework;
@ -44,6 +45,7 @@ internal sealed class DalamudInitializer : IDisposable
_windowSystem.AddWindow(configWindow); _windowSystem.AddWindow(configWindow);
_windowSystem.AddWindow(debugOverlay); _windowSystem.AddWindow(debugOverlay);
_windowSystem.AddWindow(questSelectionWindow); _windowSystem.AddWindow(questSelectionWindow);
_windowSystem.AddWindow(questValidationWindow);
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw; _pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
_pluginInterface.UiBuilder.OpenMainUi += _questWindow.Toggle; _pluginInterface.UiBuilder.OpenMainUi += _questWindow.Toggle;

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
@ -260,6 +261,7 @@ internal sealed unsafe class GameFunctions
return questManager->IsQuestAccepted(questId); return questManager->IsQuestAccepted(questId);
} }
[SuppressMessage("Performance", "CA1822")]
public bool IsQuestComplete(ushort questId) public bool IsQuestComplete(ushort questId)
{ {
return QuestManager.IsQuestComplete(questId); return QuestManager.IsQuestComplete(questId);

View File

@ -16,6 +16,8 @@ using Questionable.Controller.Steps.Common;
using Questionable.Controller.Steps.Interactions; using Questionable.Controller.Steps.Interactions;
using Questionable.Data; using Questionable.Data;
using Questionable.External; using Questionable.External;
using Questionable.Validation;
using Questionable.Validation.Validators;
using Questionable.Windows; using Questionable.Windows;
using Action = Questionable.Controller.Steps.Interactions.Action; using Action = Questionable.Controller.Steps.Interactions.Action;
@ -128,6 +130,13 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<ConfigWindow>(); serviceCollection.AddSingleton<ConfigWindow>();
serviceCollection.AddSingleton<DebugOverlay>(); serviceCollection.AddSingleton<DebugOverlay>();
serviceCollection.AddSingleton<QuestSelectionWindow>(); serviceCollection.AddSingleton<QuestSelectionWindow>();
serviceCollection.AddSingleton<QuestValidationWindow>();
serviceCollection.AddSingleton<QuestValidator>();
serviceCollection.AddSingleton<IQuestValidator, QuestDisabledValidator>();
serviceCollection.AddSingleton<IQuestValidator, BasicSequenceValidator>();
serviceCollection.AddSingleton<IQuestValidator, UniqueStartStopValidator>();
serviceCollection.AddSingleton<IQuestValidator, CompletionFlagsValidator>();
serviceCollection.AddSingleton<CommandHandler>(); serviceCollection.AddSingleton<CommandHandler>();
serviceCollection.AddSingleton<DalamudInitializer>(); serviceCollection.AddSingleton<DalamudInitializer>();

View File

@ -0,0 +1,7 @@
namespace Questionable.Validation;
internal enum EIssueSeverity
{
None,
Error,
}

View File

@ -0,0 +1,9 @@
using System.Collections.Generic;
using Questionable.Model;
namespace Questionable.Validation;
internal interface IQuestValidator
{
IEnumerable<ValidationIssue> Validate(Quest quest);
}

View File

@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Questionable.Model;
namespace Questionable.Validation;
internal sealed class QuestValidator
{
private readonly IReadOnlyList<IQuestValidator> _validators;
private readonly ILogger<QuestValidator> _logger;
private List<ValidationIssue> _validationIssues = new();
public QuestValidator(IEnumerable<IQuestValidator> validators, ILogger<QuestValidator> logger)
{
_validators = validators.ToList();
_logger = logger;
_logger.LogInformation("Validators: {Validators}",
string.Join(", ", _validators.Select(x => x.GetType().Name)));
}
public IReadOnlyList<ValidationIssue> Issues => _validationIssues;
public int IssueCount => _validationIssues.Count;
public void ClearIssues() => _validationIssues.Clear();
public void Validate(IReadOnlyCollection<Quest> quests)
{
Task.Run(() =>
{
foreach (var quest in quests)
{
foreach (var validator in _validators)
{
foreach (var issue in validator.Validate(quest))
{
_logger.LogWarning(
"Validation failed: {QuestId} ({QuestName}) / {QuestSequence} / {QuestStep} - {Description}",
issue.QuestId, quest.Info.Name, issue.Sequence, issue.Step, issue.Description);
_validationIssues.Add(issue);
}
}
}
_validationIssues = _validationIssues.OrderBy(x => x.QuestId)
.ThenBy(x => x.Sequence)
.ThenBy(x => x.Step)
.ThenBy(x => x.Description)
.ToList();
});
}
}

View File

@ -0,0 +1,10 @@
namespace Questionable.Validation;
internal sealed record ValidationIssue
{
public required ushort QuestId { get; init; }
public required byte? Sequence { get; init; }
public required int? Step { get; init; }
public required EIssueSeverity Severity { get; init; }
public required string Description { get; init; }
}

View File

@ -0,0 +1,79 @@
using System.Collections.Generic;
using System.Linq;
using Questionable.Model;
using Questionable.Model.V1;
namespace Questionable.Validation.Validators;
internal sealed class BasicSequenceValidator : IQuestValidator
{
/// <summary>
/// A quest should have sequences from 0 to N, and (if more than 'AcceptQuest' exists), a 255 sequence.
/// </summary>
public IEnumerable<ValidationIssue> Validate(Quest quest)
{
var sequences = quest.Root.QuestSequence;
var foundStart = sequences.FirstOrDefault(x => x.Sequence == 0);
if (foundStart == null)
{
yield return new ValidationIssue
{
QuestId = quest.QuestId,
Sequence = 0,
Step = null,
Severity = EIssueSeverity.Error,
Description = "Missing quest start",
};
yield break;
}
int maxSequence = sequences.Select(x => x.Sequence)
.Where(x => x != 255)
.Max();
for (int i = 0; i < maxSequence; i++)
{
var foundSequences = sequences.Where(x => x.Sequence == i).ToList();
var issue = ValidateSequences(quest, i, foundSequences);
if (issue != null)
yield return issue;
}
// some quests finish instantly
if (maxSequence > 0 || foundStart.Steps.Count > 1)
{
var foundEnding = sequences.Where(x => x.Sequence == 255).ToList();
var endingIssue = ValidateSequences(quest, 255, foundEnding);
if (endingIssue != null)
yield return endingIssue;
}
}
private static ValidationIssue? ValidateSequences(Quest quest, int sequenceNo, List<QuestSequence> foundSequences)
{
if (foundSequences.Count == 0)
{
return new ValidationIssue
{
QuestId = quest.QuestId,
Sequence = (byte)sequenceNo,
Step = null,
Severity = EIssueSeverity.Error,
Description = "Missing sequence",
};
}
else if (foundSequences.Count == 2)
{
return new ValidationIssue
{
QuestId = quest.QuestId,
Sequence = (byte)sequenceNo,
Step = null,
Severity = EIssueSeverity.Error,
Description = "Duplicate sequence",
};
}
else
return null;
}
}

View File

@ -0,0 +1,54 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Questionable.Controller.Utils;
using Questionable.Model;
namespace Questionable.Validation.Validators;
internal sealed class CompletionFlagsValidator : IQuestValidator
{
public IEnumerable<ValidationIssue> Validate(Quest quest)
{
foreach (var sequence in quest.Root.QuestSequence)
{
var mappedCompletionFlags = sequence.Steps
.Select(x =>
{
if (QuestWorkUtils.HasCompletionFlags(x.CompletionQuestVariablesFlags))
{
return Enumerable.Range(0, 6).Select(y =>
{
short? value = x.CompletionQuestVariablesFlags[y];
if (value == null || value.Value < 0)
return 0;
return (long)BitOperations.RotateLeft((ulong)value.Value, 8 * y);
})
.Sum();
}
else
return 0;
})
.ToList();
for (int i = 0; i < sequence.Steps.Count; ++i)
{
var flags = mappedCompletionFlags[i];
if (flags == 0)
continue;
if (mappedCompletionFlags.Count(x => x == flags) >= 2)
{
yield return new ValidationIssue
{
QuestId = quest.QuestId,
Sequence = (byte)sequence.Sequence,
Step = i,
Severity = EIssueSeverity.Error,
Description = $"Duplicate completion flags: {string.Join(", ", sequence.Steps[i].CompletionQuestVariablesFlags)}",
};
}
}
}
}
}

View File

@ -0,0 +1,22 @@
using System.Collections.Generic;
using Questionable.Model;
namespace Questionable.Validation.Validators;
internal sealed class QuestDisabledValidator : IQuestValidator
{
public IEnumerable<ValidationIssue> Validate(Quest quest)
{
if (quest.Root.Disabled)
{
yield return new ValidationIssue
{
QuestId = quest.QuestId,
Sequence = null,
Step = null,
Severity = EIssueSeverity.None,
Description = "Quest is disabled",
};
}
}
}

View File

@ -0,0 +1,85 @@
using System.Collections.Generic;
using System.Linq;
using Questionable.Model;
using Questionable.Model.V1;
namespace Questionable.Validation.Validators;
internal sealed class UniqueStartStopValidator : IQuestValidator
{
public IEnumerable<ValidationIssue> Validate(Quest quest)
{
var questAccepts = FindQuestStepsWithInteractionType(quest, EInteractionType.AcceptQuest)
.Where(x => x.Step.PickupQuestId == null)
.ToList();
foreach (var accept in questAccepts)
{
if (accept.SequenceId != 0 || accept.StepId != quest.FindSequence(0)!.Steps.Count - 1)
{
yield return new ValidationIssue
{
QuestId = quest.QuestId,
Sequence = (byte)accept.SequenceId,
Step = accept.StepId,
Severity = EIssueSeverity.Error,
Description = "Unexpected AcceptQuest step",
};
}
}
if (quest.FindSequence(0) != null && questAccepts.Count == 0)
{
yield return new ValidationIssue
{
QuestId = quest.QuestId,
Sequence = 0,
Step = null,
Severity = EIssueSeverity.Error,
Description = "No AcceptQuest step",
};
}
var questCompletes = FindQuestStepsWithInteractionType(quest, EInteractionType.CompleteQuest)
.Where(x => x.Step.TurnInQuestId == null)
.ToList();
foreach (var complete in questCompletes)
{
if (complete.SequenceId != 255 || complete.StepId != quest.FindSequence(255)!.Steps.Count - 1)
{
yield return new ValidationIssue
{
QuestId = quest.QuestId,
Sequence = (byte)complete.SequenceId,
Step = complete.StepId,
Severity = EIssueSeverity.Error,
Description = "Unexpected CompleteQuest step",
};
}
}
if (quest.FindSequence(255) != null && questCompletes.Count == 0)
{
yield return new ValidationIssue
{
QuestId = quest.QuestId,
Sequence = 255,
Step = null,
Severity = EIssueSeverity.Error,
Description = "No CompleteQuest step",
};
}
}
private static IEnumerable<(int SequenceId, int StepId, QuestStep Step)> FindQuestStepsWithInteractionType(Quest quest, EInteractionType interactionType)
{
foreach (var sequence in quest.Root.QuestSequence)
{
for (int i = 0; i < sequence.Steps.Count; ++i)
{
var step = sequence.Steps[i];
if (step.InteractionType == interactionType)
yield return (sequence.Sequence, i, step);
}
}
}
}

View File

@ -105,7 +105,7 @@ internal sealed class QuestSelectionWindow : LWindow
public override void OnClose() public override void OnClose()
{ {
TargetId = default; TargetId = default;
TargetName = default; TargetName = string.Empty;
_quests = []; _quests = [];
_offeredQuests = []; _offeredQuests = [];
} }

View File

@ -0,0 +1,69 @@
using System.Globalization;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility.Raii;
using FFXIVClientStructs.FFXIV.Common.Math;
using ImGuiNET;
using LLib.ImGui;
using Questionable.Data;
using Questionable.Model;
using Questionable.Validation;
namespace Questionable.Windows;
internal sealed class QuestValidationWindow : LWindow
{
private readonly QuestValidator _questValidator;
private readonly QuestData _questData;
public QuestValidationWindow(QuestValidator questValidator, QuestData questData) : base("Quest Validation###QuestionableValidator")
{
_questValidator = questValidator;
_questData = questData;
Size = new Vector2(600, 200);
SizeCondition = ImGuiCond.Once;
SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new Vector2(600, 200),
};
}
public override void Draw()
{
using var table = ImRaii.Table("QuestSelection", 5, ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY);
if (!table)
{
ImGui.Text("Not table");
return;
}
ImGui.TableSetupColumn("Quest", ImGuiTableColumnFlags.WidthFixed, 50);
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 200);
ImGui.TableSetupColumn("Sq", ImGuiTableColumnFlags.WidthFixed, 30);
ImGui.TableSetupColumn("Sp", ImGuiTableColumnFlags.WidthFixed, 30);
ImGui.TableSetupColumn("Issue", ImGuiTableColumnFlags.None, 200);
ImGui.TableHeadersRow();
foreach (ValidationIssue validationIssue in _questValidator.Issues)
{
ImGui.TableNextRow();
if (ImGui.TableNextColumn())
ImGui.TextUnformatted(validationIssue.QuestId.ToString(CultureInfo.InvariantCulture));
if (ImGui.TableNextColumn())
ImGui.TextUnformatted(_questData.GetQuestInfo(validationIssue.QuestId).Name);
if (ImGui.TableNextColumn())
ImGui.TextUnformatted(validationIssue.Sequence?.ToString(CultureInfo.InvariantCulture) ?? string.Empty);
if (ImGui.TableNextColumn())
ImGui.TextUnformatted(validationIssue.Step?.ToString(CultureInfo.InvariantCulture) ?? string.Empty);
if (ImGui.TableNextColumn())
ImGui.TextUnformatted(validationIssue.Description);
}
}
}

View File

@ -48,6 +48,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
private readonly ICondition _condition; private readonly ICondition _condition;
private readonly IGameGui _gameGui; private readonly IGameGui _gameGui;
private readonly QuestSelectionWindow _questSelectionWindow; private readonly QuestSelectionWindow _questSelectionWindow;
private readonly QuestValidationWindow _questValidationWindow;
private readonly ILogger<QuestWindow> _logger; private readonly ILogger<QuestWindow> _logger;
public QuestWindow(IDalamudPluginInterface pluginInterface, public QuestWindow(IDalamudPluginInterface pluginInterface,
@ -68,6 +69,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
ICondition condition, ICondition condition,
IGameGui gameGui, IGameGui gameGui,
QuestSelectionWindow questSelectionWindow, QuestSelectionWindow questSelectionWindow,
QuestValidationWindow questValidationWindow,
ILogger<QuestWindow> logger) ILogger<QuestWindow> logger)
: base("Questionable###Questionable", ImGuiWindowFlags.AlwaysAutoResize) : base("Questionable###Questionable", ImGuiWindowFlags.AlwaysAutoResize)
{ {
@ -89,6 +91,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
_condition = condition; _condition = condition;
_gameGui = gameGui; _gameGui = gameGui;
_questSelectionWindow = questSelectionWindow; _questSelectionWindow = questSelectionWindow;
_questValidationWindow = questValidationWindow;
_logger = logger; _logger = logger;
#if DEBUG #if DEBUG
@ -414,7 +417,8 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
$"Target: {_targetManager.Target.Name} ({_targetManager.Target.ObjectKind}; {_targetManager.Target.DataId})")); $"Target: {_targetManager.Target.Name} ({_targetManager.Target.ObjectKind}; {_targetManager.Target.DataId})"));
GameObject* gameObject = (GameObject*)_targetManager.Target.Address; GameObject* gameObject = (GameObject*)_targetManager.Target.Address;
ImGui.Text(string.Create(CultureInfo.InvariantCulture, $"Distance: {(_targetManager.Target.Position - _clientState.LocalPlayer.Position).Length():F2}")); ImGui.Text(string.Create(CultureInfo.InvariantCulture,
$"Distance: {(_targetManager.Target.Position - _clientState.LocalPlayer.Position).Length():F2}"));
ImGui.SameLine(); ImGui.SameLine();
float verticalDistance = _targetManager.Target.Position.Y - _clientState.LocalPlayer.Position.Y; float verticalDistance = _targetManager.Target.Position.Y - _clientState.LocalPlayer.Position.Y;
@ -471,7 +475,8 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
bool copy = ImGuiComponents.IconButton(FontAwesomeIcon.Copy); bool copy = ImGuiComponents.IconButton(FontAwesomeIcon.Copy);
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip("Left click: Copy target position as JSON.\nRight click: Copy target position as C# code."); ImGui.SetTooltip(
"Left click: Copy target position as JSON.\nRight click: Copy target position as C# code.");
if (copy) if (copy)
{ {
string interactionType = gameObject->NamePlateIconId switch string interactionType = gameObject->NamePlateIconId switch
@ -509,7 +514,8 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
{ {
bool copy = ImGuiComponents.IconButton(FontAwesomeIcon.Copy); bool copy = ImGuiComponents.IconButton(FontAwesomeIcon.Copy);
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip("Left click: Copy your position as JSON.\nRight click: Copy your position as C# code."); ImGui.SetTooltip(
"Left click: Copy your position as JSON.\nRight click: Copy your position as C# code.");
if (copy) if (copy)
{ {
ImGui.SetClipboardText($$""" ImGui.SetClipboardText($$"""
@ -570,6 +576,18 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
_framework.RunOnTick(() => _gameUiController.HandleCurrentDialogueChoices(), _framework.RunOnTick(() => _gameUiController.HandleCurrentDialogueChoices(),
TimeSpan.FromMilliseconds(200)); TimeSpan.FromMilliseconds(200));
} }
#if DEBUG
if (_questRegistry.ValidationIssueCount > 0)
{
ImGui.SameLine();
using var textColor = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.ExclamationTriangle,
$"{_questRegistry.ValidationIssueCount}"))
_questValidationWindow.IsOpen = true;
}
#endif
} }
private void DrawRemainingTasks() private void DrawRemainingTasks()