From db618786832558bb77d66ff436cdd5a6d022ec8b Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Tue, 16 Jul 2024 00:18:10 +0200 Subject: [PATCH] Clean up quest validation --- Questionable/Controller/QuestRegistry.cs | 126 ++---------------- Questionable/DalamudInitializer.cs | 4 +- Questionable/GameFunctions.cs | 2 + Questionable/QuestionablePlugin.cs | 9 ++ Questionable/Validation/EIssueSeverity.cs | 7 + Questionable/Validation/IQuestValidator.cs | 9 ++ Questionable/Validation/QuestValidator.cs | 55 ++++++++ Questionable/Validation/ValidationIssue.cs | 10 ++ .../Validators/BasicSequenceValidator.cs | 79 +++++++++++ .../Validators/CompletionFlagsValidator.cs | 54 ++++++++ .../Validators/QuestDisabledValidator.cs | 22 +++ .../Validators/UniqueStartStopValidator.cs | 85 ++++++++++++ Questionable/Windows/QuestSelectionWindow.cs | 2 +- Questionable/Windows/QuestValidationWindow.cs | 69 ++++++++++ Questionable/Windows/QuestWindow.cs | 24 +++- 15 files changed, 435 insertions(+), 122 deletions(-) create mode 100644 Questionable/Validation/EIssueSeverity.cs create mode 100644 Questionable/Validation/IQuestValidator.cs create mode 100644 Questionable/Validation/QuestValidator.cs create mode 100644 Questionable/Validation/ValidationIssue.cs create mode 100644 Questionable/Validation/Validators/BasicSequenceValidator.cs create mode 100644 Questionable/Validation/Validators/CompletionFlagsValidator.cs create mode 100644 Questionable/Validation/Validators/QuestDisabledValidator.cs create mode 100644 Questionable/Validation/Validators/UniqueStartStopValidator.cs create mode 100644 Questionable/Windows/QuestValidationWindow.cs diff --git a/Questionable/Controller/QuestRegistry.cs b/Questionable/Controller/QuestRegistry.cs index 96d5c8478..d836b21eb 100644 --- a/Questionable/Controller/QuestRegistry.cs +++ b/Questionable/Controller/QuestRegistry.cs @@ -15,6 +15,7 @@ using Questionable.Controller.Utils; using Questionable.Data; using Questionable.Model; using Questionable.Model.V1; +using Questionable.Validation; namespace Questionable.Controller; @@ -23,21 +24,24 @@ internal sealed class QuestRegistry private readonly IDalamudPluginInterface _pluginInterface; private readonly QuestData _questData; private readonly IChatGui _chatGui; + private readonly QuestValidator _questValidator; private readonly ILogger _logger; private readonly Dictionary _quests = new(); public QuestRegistry(IDalamudPluginInterface pluginInterface, QuestData questData, IChatGui chatGui, - ILogger logger) + QuestValidator questValidator, ILogger logger) { _pluginInterface = pluginInterface; _questData = questData; _chatGui = chatGui; + _questValidator = questValidator; _logger = logger; } public IEnumerable AllQuests => _quests.Values; public int Count => _quests.Count; + public int ValidationIssueCount => _questValidator.IssueCount; public void Reload() { @@ -110,113 +114,8 @@ internal sealed class QuestRegistry [Conditional("DEBUG")] private void ValidateQuests() { - Task.Run(() => - { - 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."); - } - }); + _questValidator.ClearIssues(); + _questValidator.Validate(_quests.Values); } @@ -233,13 +132,6 @@ internal sealed class QuestRegistry Root = JsonSerializer.Deserialize(stream)!, 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; } @@ -281,8 +173,8 @@ internal sealed class QuestRegistry 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) - => _quests.TryGetValue(questId, out quest); + => _quests.TryGetValue(questId, out quest) && !quest.Root.Disabled; } diff --git a/Questionable/DalamudInitializer.cs b/Questionable/DalamudInitializer.cs index fa2cfed83..d2cf7af44 100644 --- a/Questionable/DalamudInitializer.cs +++ b/Questionable/DalamudInitializer.cs @@ -29,7 +29,8 @@ internal sealed class DalamudInitializer : IDisposable QuestWindow questWindow, DebugOverlay debugOverlay, ConfigWindow configWindow, - QuestSelectionWindow questSelectionWindow) + QuestSelectionWindow questSelectionWindow, + QuestValidationWindow questValidationWindow) { _pluginInterface = pluginInterface; _framework = framework; @@ -44,6 +45,7 @@ internal sealed class DalamudInitializer : IDisposable _windowSystem.AddWindow(configWindow); _windowSystem.AddWindow(debugOverlay); _windowSystem.AddWindow(questSelectionWindow); + _windowSystem.AddWindow(questValidationWindow); _pluginInterface.UiBuilder.Draw += _windowSystem.Draw; _pluginInterface.UiBuilder.OpenMainUi += _questWindow.Toggle; diff --git a/Questionable/GameFunctions.cs b/Questionable/GameFunctions.cs index f25cec483..7bca42414 100644 --- a/Questionable/GameFunctions.cs +++ b/Questionable/GameFunctions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; using Dalamud.Game.ClientState.Conditions; @@ -260,6 +261,7 @@ internal sealed unsafe class GameFunctions return questManager->IsQuestAccepted(questId); } + [SuppressMessage("Performance", "CA1822")] public bool IsQuestComplete(ushort questId) { return QuestManager.IsQuestComplete(questId); diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index 9647fc3ba..39c370d04 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -16,6 +16,8 @@ using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Interactions; using Questionable.Data; using Questionable.External; +using Questionable.Validation; +using Questionable.Validation.Validators; using Questionable.Windows; using Action = Questionable.Controller.Steps.Interactions.Action; @@ -128,6 +130,13 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/Questionable/Validation/EIssueSeverity.cs b/Questionable/Validation/EIssueSeverity.cs new file mode 100644 index 000000000..25f425396 --- /dev/null +++ b/Questionable/Validation/EIssueSeverity.cs @@ -0,0 +1,7 @@ +namespace Questionable.Validation; + +internal enum EIssueSeverity +{ + None, + Error, +} diff --git a/Questionable/Validation/IQuestValidator.cs b/Questionable/Validation/IQuestValidator.cs new file mode 100644 index 000000000..02a5f182d --- /dev/null +++ b/Questionable/Validation/IQuestValidator.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using Questionable.Model; + +namespace Questionable.Validation; + +internal interface IQuestValidator +{ + IEnumerable Validate(Quest quest); +} diff --git a/Questionable/Validation/QuestValidator.cs b/Questionable/Validation/QuestValidator.cs new file mode 100644 index 000000000..83bb9d425 --- /dev/null +++ b/Questionable/Validation/QuestValidator.cs @@ -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 _validators; + private readonly ILogger _logger; + + private List _validationIssues = new(); + + public QuestValidator(IEnumerable validators, ILogger logger) + { + _validators = validators.ToList(); + _logger = logger; + + _logger.LogInformation("Validators: {Validators}", + string.Join(", ", _validators.Select(x => x.GetType().Name))); + } + + public IReadOnlyList Issues => _validationIssues; + public int IssueCount => _validationIssues.Count; + + public void ClearIssues() => _validationIssues.Clear(); + + public void Validate(IReadOnlyCollection 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(); + }); + } +} diff --git a/Questionable/Validation/ValidationIssue.cs b/Questionable/Validation/ValidationIssue.cs new file mode 100644 index 000000000..6e8427602 --- /dev/null +++ b/Questionable/Validation/ValidationIssue.cs @@ -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; } +} diff --git a/Questionable/Validation/Validators/BasicSequenceValidator.cs b/Questionable/Validation/Validators/BasicSequenceValidator.cs new file mode 100644 index 000000000..8ea19a142 --- /dev/null +++ b/Questionable/Validation/Validators/BasicSequenceValidator.cs @@ -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 +{ + /// + /// A quest should have sequences from 0 to N, and (if more than 'AcceptQuest' exists), a 255 sequence. + /// + public IEnumerable 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 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; + } +} diff --git a/Questionable/Validation/Validators/CompletionFlagsValidator.cs b/Questionable/Validation/Validators/CompletionFlagsValidator.cs new file mode 100644 index 000000000..f1ea4c029 --- /dev/null +++ b/Questionable/Validation/Validators/CompletionFlagsValidator.cs @@ -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 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)}", + }; + } + } + } + } +} diff --git a/Questionable/Validation/Validators/QuestDisabledValidator.cs b/Questionable/Validation/Validators/QuestDisabledValidator.cs new file mode 100644 index 000000000..096fd3757 --- /dev/null +++ b/Questionable/Validation/Validators/QuestDisabledValidator.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Questionable.Model; + +namespace Questionable.Validation.Validators; + +internal sealed class QuestDisabledValidator : IQuestValidator +{ + public IEnumerable 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", + }; + } + } +} diff --git a/Questionable/Validation/Validators/UniqueStartStopValidator.cs b/Questionable/Validation/Validators/UniqueStartStopValidator.cs new file mode 100644 index 000000000..83c350a5d --- /dev/null +++ b/Questionable/Validation/Validators/UniqueStartStopValidator.cs @@ -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 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); + } + } + } +} diff --git a/Questionable/Windows/QuestSelectionWindow.cs b/Questionable/Windows/QuestSelectionWindow.cs index 8e0bd9714..55d81bb50 100644 --- a/Questionable/Windows/QuestSelectionWindow.cs +++ b/Questionable/Windows/QuestSelectionWindow.cs @@ -105,7 +105,7 @@ internal sealed class QuestSelectionWindow : LWindow public override void OnClose() { TargetId = default; - TargetName = default; + TargetName = string.Empty; _quests = []; _offeredQuests = []; } diff --git a/Questionable/Windows/QuestValidationWindow.cs b/Questionable/Windows/QuestValidationWindow.cs new file mode 100644 index 000000000..2cb3f5b22 --- /dev/null +++ b/Questionable/Windows/QuestValidationWindow.cs @@ -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); + } + } +} diff --git a/Questionable/Windows/QuestWindow.cs b/Questionable/Windows/QuestWindow.cs index 69793cc29..517d88940 100644 --- a/Questionable/Windows/QuestWindow.cs +++ b/Questionable/Windows/QuestWindow.cs @@ -48,6 +48,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig private readonly ICondition _condition; private readonly IGameGui _gameGui; private readonly QuestSelectionWindow _questSelectionWindow; + private readonly QuestValidationWindow _questValidationWindow; private readonly ILogger _logger; public QuestWindow(IDalamudPluginInterface pluginInterface, @@ -68,6 +69,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig ICondition condition, IGameGui gameGui, QuestSelectionWindow questSelectionWindow, + QuestValidationWindow questValidationWindow, ILogger logger) : base("Questionable###Questionable", ImGuiWindowFlags.AlwaysAutoResize) { @@ -89,6 +91,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig _condition = condition; _gameGui = gameGui; _questSelectionWindow = questSelectionWindow; + _questValidationWindow = questValidationWindow; _logger = logger; #if DEBUG @@ -414,7 +417,8 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig $"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.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; @@ -471,7 +475,8 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig 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."); + ImGui.SetTooltip( + "Left click: Copy target position as JSON.\nRight click: Copy target position as C# code."); if (copy) { string interactionType = gameObject->NamePlateIconId switch @@ -509,7 +514,8 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig { 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."); + ImGui.SetTooltip( + "Left click: Copy your position as JSON.\nRight click: Copy your position as C# code."); if (copy) { ImGui.SetClipboardText($$""" @@ -570,6 +576,18 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig _framework.RunOnTick(() => _gameUiController.HandleCurrentDialogueChoices(), 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()