Clean up quest validation
This commit is contained in:
parent
202abcf3a8
commit
db61878683
@ -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<QuestRegistry> _logger;
|
||||
|
||||
private readonly Dictionary<ushort, Quest> _quests = new();
|
||||
|
||||
public QuestRegistry(IDalamudPluginInterface pluginInterface, QuestData questData, IChatGui chatGui,
|
||||
ILogger<QuestRegistry> logger)
|
||||
QuestValidator questValidator, ILogger<QuestRegistry> logger)
|
||||
{
|
||||
_pluginInterface = pluginInterface;
|
||||
_questData = questData;
|
||||
_chatGui = chatGui;
|
||||
_questValidator = questValidator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public IEnumerable<Quest> 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<QuestRoot>(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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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<ConfigWindow>();
|
||||
serviceCollection.AddSingleton<DebugOverlay>();
|
||||
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<DalamudInitializer>();
|
||||
|
7
Questionable/Validation/EIssueSeverity.cs
Normal file
7
Questionable/Validation/EIssueSeverity.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Questionable.Validation;
|
||||
|
||||
internal enum EIssueSeverity
|
||||
{
|
||||
None,
|
||||
Error,
|
||||
}
|
9
Questionable/Validation/IQuestValidator.cs
Normal file
9
Questionable/Validation/IQuestValidator.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using Questionable.Model;
|
||||
|
||||
namespace Questionable.Validation;
|
||||
|
||||
internal interface IQuestValidator
|
||||
{
|
||||
IEnumerable<ValidationIssue> Validate(Quest quest);
|
||||
}
|
55
Questionable/Validation/QuestValidator.cs
Normal file
55
Questionable/Validation/QuestValidator.cs
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
10
Questionable/Validation/ValidationIssue.cs
Normal file
10
Questionable/Validation/ValidationIssue.cs
Normal 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; }
|
||||
}
|
79
Questionable/Validation/Validators/BasicSequenceValidator.cs
Normal file
79
Questionable/Validation/Validators/BasicSequenceValidator.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -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)}",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
22
Questionable/Validation/Validators/QuestDisabledValidator.cs
Normal file
22
Questionable/Validation/Validators/QuestDisabledValidator.cs
Normal 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",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -105,7 +105,7 @@ internal sealed class QuestSelectionWindow : LWindow
|
||||
public override void OnClose()
|
||||
{
|
||||
TargetId = default;
|
||||
TargetName = default;
|
||||
TargetName = string.Empty;
|
||||
_quests = [];
|
||||
_offeredQuests = [];
|
||||
}
|
||||
|
69
Questionable/Windows/QuestValidationWindow.cs
Normal file
69
Questionable/Windows/QuestValidationWindow.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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<QuestWindow> _logger;
|
||||
|
||||
public QuestWindow(IDalamudPluginInterface pluginInterface,
|
||||
@ -68,6 +69,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
|
||||
ICondition condition,
|
||||
IGameGui gameGui,
|
||||
QuestSelectionWindow questSelectionWindow,
|
||||
QuestValidationWindow questValidationWindow,
|
||||
ILogger<QuestWindow> 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()
|
||||
|
Loading…
Reference in New Issue
Block a user