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.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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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>();
|
||||||
|
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()
|
public override void OnClose()
|
||||||
{
|
{
|
||||||
TargetId = default;
|
TargetId = default;
|
||||||
TargetName = default;
|
TargetName = string.Empty;
|
||||||
_quests = [];
|
_quests = [];
|
||||||
_offeredQuests = [];
|
_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 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()
|
||||||
|
Loading…
Reference in New Issue
Block a user