diff --git a/Questionable/Model/EBeastTribe.cs b/Questionable/Model/EBeastTribe.cs new file mode 100644 index 000000000..e26fb6725 --- /dev/null +++ b/Questionable/Model/EBeastTribe.cs @@ -0,0 +1,28 @@ +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; + +namespace Questionable.Model; + +[SuppressMessage("Design", "CA1028", Justification = "Game type")] +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +public enum EBeastTribe : byte +{ + None = 0, + Amaljaa = 1, + Sylphs = 2, + Kobolds = 3, + Sahagin = 4, + Ixal = 5, + VanuVanu = 6, + Vath = 7, + Moogles = 8, + Kojin = 9, + Ananta = 10, + Namazu = 11, + Pixies = 12, + Qitari = 13, + Dwarves = 14, + Arkasodara = 15, + Omicrons = 16, + Loporrits = 17, +} diff --git a/Questionable/Model/QuestInfo.cs b/Questionable/Model/QuestInfo.cs index db4ded12c..9caaef123 100644 --- a/Questionable/Model/QuestInfo.cs +++ b/Questionable/Model/QuestInfo.cs @@ -27,6 +27,7 @@ internal sealed class QuestInfo PreviousInstanceContent = quest.InstanceContent.Select(x => (ushort)x.Row).Where(x => x != 0).ToList(); PreviousInstanceContentJoin = (QuestJoin)quest.InstanceContentJoin; GrandCompany = (GrandCompany)quest.GrandCompany.Row; + BeastTribe = (EBeastTribe)quest.BeastTribe.Row; } @@ -44,6 +45,7 @@ internal sealed class QuestInfo public bool IsMainScenarioQuest { get; } public bool CompletesInstantly { get; } public GrandCompany GrandCompany { get; } + public EBeastTribe BeastTribe { get; } public string SimplifiedName => Name .TrimStart(SeIconChar.QuestSync.ToIconChar(), SeIconChar.QuestRepeatable.ToIconChar(), ' '); diff --git a/Questionable/Questionable.csproj b/Questionable/Questionable.csproj index e25f2e49e..d3dcfe2db 100644 --- a/Questionable/Questionable.csproj +++ b/Questionable/Questionable.csproj @@ -1,6 +1,6 @@  - 1.18 + 1.19 dist $(SolutionDir)=X:\ x64 diff --git a/Questionable/Validation/EIssueType.cs b/Questionable/Validation/EIssueType.cs new file mode 100644 index 000000000..a6ff28b66 --- /dev/null +++ b/Questionable/Validation/EIssueType.cs @@ -0,0 +1,18 @@ +namespace Questionable.Validation; + +public enum EIssueType +{ + None, + InvalidJsonSchema, + MissingSequence0, + MissingSequence, + DuplicateSequence, + MissingQuestAccept, + MissingQuestComplete, + InstantQuestWithMultipleSteps, + DuplicateCompletionFlags, + InvalidNextQuestId, + QuestDisabled, + UnexpectedAcceptQuestStep, + UnexpectedCompleteQuestStep, +} diff --git a/Questionable/Validation/QuestValidator.cs b/Questionable/Validation/QuestValidator.cs index d53a5b52c..794324f9e 100644 --- a/Questionable/Validation/QuestValidator.cs +++ b/Questionable/Validation/QuestValidator.cs @@ -41,6 +41,7 @@ internal sealed class QuestValidator { try { + Dictionary disabledTribeQuests = new(); foreach (var quest in quests) { foreach (var validator in _validators) @@ -53,7 +54,13 @@ internal sealed class QuestValidator _logger.Log(level, "Validation failed: {QuestId} ({QuestName}) / {QuestSequence} / {QuestStep} - {Description}", issue.QuestId, quest.Info.Name, issue.Sequence, issue.Step, issue.Description); - _validationIssues.Add(issue); + if (issue.Type == EIssueType.QuestDisabled && quest.Info.BeastTribe != EBeastTribe.None) + { + disabledTribeQuests.TryAdd(quest.Info.BeastTribe, 0); + disabledTribeQuests[quest.Info.BeastTribe]++; + } + else + _validationIssues.Add(issue); } } } @@ -62,6 +69,7 @@ internal sealed class QuestValidator .ThenBy(x => x.Sequence) .ThenBy(x => x.Step) .ThenBy(x => x.Description) + .Concat(DisabledTribesAsIssues(disabledTribeQuests)) .ToList(); } catch (Exception e) @@ -70,4 +78,20 @@ internal sealed class QuestValidator } }, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default); } + + private static IEnumerable DisabledTribesAsIssues(Dictionary disabledTribeQuests) + { + return disabledTribeQuests + .OrderBy(x => x.Key) + .Select(x => new ValidationIssue + { + QuestId = null, + Sequence = null, + Step = null, + BeastTribe = x.Key, + Type = EIssueType.QuestDisabled, + Severity = EIssueSeverity.None, + Description = $"{x.Value} disabled quest(s)", + }); + } } diff --git a/Questionable/Validation/ValidationIssue.cs b/Questionable/Validation/ValidationIssue.cs index 6e8427602..5988fd5ba 100644 --- a/Questionable/Validation/ValidationIssue.cs +++ b/Questionable/Validation/ValidationIssue.cs @@ -1,10 +1,14 @@ -namespace Questionable.Validation; +using Questionable.Model; + +namespace Questionable.Validation; internal sealed record ValidationIssue { - public required ushort QuestId { get; init; } + public required ushort? QuestId { get; init; } public required byte? Sequence { get; init; } public required int? Step { get; init; } + public EBeastTribe BeastTribe { get; init; } = EBeastTribe.None; + public required EIssueType Type { 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 index c8d40a369..0c10e0328 100644 --- a/Questionable/Validation/Validators/BasicSequenceValidator.cs +++ b/Questionable/Validation/Validators/BasicSequenceValidator.cs @@ -21,6 +21,7 @@ internal sealed class BasicSequenceValidator : IQuestValidator QuestId = quest.QuestId, Sequence = 0, Step = null, + Type = EIssueType.MissingSequence0, Severity = EIssueSeverity.Error, Description = "Missing quest start", }; @@ -39,6 +40,7 @@ internal sealed class BasicSequenceValidator : IQuestValidator QuestId = quest.QuestId, Sequence = (byte)sequence.Sequence, Step = null, + Type = EIssueType.InstantQuestWithMultipleSteps, Severity = EIssueSeverity.Error, Description = "Instant quest should not have any sequences after the start", }; @@ -74,6 +76,7 @@ internal sealed class BasicSequenceValidator : IQuestValidator QuestId = quest.QuestId, Sequence = (byte)sequenceNo, Step = null, + Type = EIssueType.MissingSequence, Severity = EIssueSeverity.Error, Description = "Missing sequence", }; @@ -85,6 +88,7 @@ internal sealed class BasicSequenceValidator : IQuestValidator QuestId = quest.QuestId, Sequence = (byte)sequenceNo, Step = null, + Type = EIssueType.DuplicateSequence, Severity = EIssueSeverity.Error, Description = "Duplicate sequence", }; diff --git a/Questionable/Validation/Validators/CompletionFlagsValidator.cs b/Questionable/Validation/Validators/CompletionFlagsValidator.cs index 0cf97f1d5..462e92895 100644 --- a/Questionable/Validation/Validators/CompletionFlagsValidator.cs +++ b/Questionable/Validation/Validators/CompletionFlagsValidator.cs @@ -44,6 +44,7 @@ internal sealed class CompletionFlagsValidator : IQuestValidator QuestId = quest.QuestId, Sequence = (byte)sequence.Sequence, Step = i, + Type = EIssueType.DuplicateCompletionFlags, Severity = EIssueSeverity.Error, Description = $"Duplicate completion flags: {string.Join(", ", sequence.Steps[i].CompletionQuestVariablesFlags)}", }; diff --git a/Questionable/Validation/Validators/JsonSchemaValidator.cs b/Questionable/Validation/Validators/JsonSchemaValidator.cs index 04abc0127..b3ed6f4ec 100644 --- a/Questionable/Validation/Validators/JsonSchemaValidator.cs +++ b/Questionable/Validation/Validators/JsonSchemaValidator.cs @@ -30,6 +30,7 @@ internal sealed class JsonSchemaValidator : IQuestValidator QuestId = quest.QuestId, Sequence = null, Step = null, + Type = EIssueType.InvalidJsonSchema, Severity = EIssueSeverity.Error, Description = "JSON Validation failed" }; diff --git a/Questionable/Validation/Validators/NextQuestValidator.cs b/Questionable/Validation/Validators/NextQuestValidator.cs index 891c38645..325d8fbfa 100644 --- a/Questionable/Validation/Validators/NextQuestValidator.cs +++ b/Questionable/Validation/Validators/NextQuestValidator.cs @@ -15,6 +15,7 @@ internal sealed class NextQuestValidator : IQuestValidator QuestId = quest.QuestId, Sequence = (byte)invalidNextQuest.Sequence.Sequence, Step = invalidNextQuest.StepId, + Type = EIssueType.InvalidNextQuestId, Severity = EIssueSeverity.Error, Description = "Next quest should not reference itself", }; diff --git a/Questionable/Validation/Validators/QuestDisabledValidator.cs b/Questionable/Validation/Validators/QuestDisabledValidator.cs index 096fd3757..298b34239 100644 --- a/Questionable/Validation/Validators/QuestDisabledValidator.cs +++ b/Questionable/Validation/Validators/QuestDisabledValidator.cs @@ -14,6 +14,7 @@ internal sealed class QuestDisabledValidator : IQuestValidator QuestId = quest.QuestId, Sequence = null, Step = null, + Type = EIssueType.QuestDisabled, Severity = EIssueSeverity.None, Description = "Quest is disabled", }; diff --git a/Questionable/Validation/Validators/UniqueStartStopValidator.cs b/Questionable/Validation/Validators/UniqueStartStopValidator.cs index 678e3500f..b95522f1f 100644 --- a/Questionable/Validation/Validators/UniqueStartStopValidator.cs +++ b/Questionable/Validation/Validators/UniqueStartStopValidator.cs @@ -21,6 +21,7 @@ internal sealed class UniqueStartStopValidator : IQuestValidator QuestId = quest.QuestId, Sequence = (byte)accept.Sequence.Sequence, Step = accept.StepId, + Type = EIssueType.UnexpectedAcceptQuestStep, Severity = EIssueSeverity.Error, Description = "Unexpected AcceptQuest step", }; @@ -34,6 +35,7 @@ internal sealed class UniqueStartStopValidator : IQuestValidator QuestId = quest.QuestId, Sequence = 0, Step = null, + Type = EIssueType.MissingQuestAccept, Severity = EIssueSeverity.Error, Description = "No AcceptQuest step", }; @@ -51,6 +53,7 @@ internal sealed class UniqueStartStopValidator : IQuestValidator QuestId = quest.QuestId, Sequence = (byte)complete.Sequence.Sequence, Step = complete.StepId, + Type = EIssueType.UnexpectedCompleteQuestStep, Severity = EIssueSeverity.Error, Description = "Unexpected CompleteQuest step", }; @@ -64,6 +67,7 @@ internal sealed class UniqueStartStopValidator : IQuestValidator QuestId = quest.QuestId, Sequence = 255, Step = null, + Type = EIssueType.MissingQuestComplete, Severity = EIssueSeverity.Error, Description = "No CompleteQuest step", }; diff --git a/Questionable/Windows/QuestValidationWindow.cs b/Questionable/Windows/QuestValidationWindow.cs index 83e6d2ecf..be931d6c7 100644 --- a/Questionable/Windows/QuestValidationWindow.cs +++ b/Questionable/Windows/QuestValidationWindow.cs @@ -19,7 +19,9 @@ internal sealed class QuestValidationWindow : LWindow private readonly QuestData _questData; private readonly IDalamudPluginInterface _pluginInterface; - public QuestValidationWindow(QuestValidator questValidator, QuestData questData, IDalamudPluginInterface pluginInterface) : base("Quest Validation###QuestionableValidator") + public QuestValidationWindow(QuestValidator questValidator, QuestData questData, + IDalamudPluginInterface pluginInterface) + : base("Quest Validation###QuestionableValidator") { _questValidator = questValidator; _questData = questData; @@ -54,10 +56,12 @@ internal sealed class QuestValidationWindow : LWindow ImGui.TableNextRow(); if (ImGui.TableNextColumn()) - ImGui.TextUnformatted(validationIssue.QuestId.ToString(CultureInfo.InvariantCulture)); + ImGui.TextUnformatted(validationIssue.QuestId?.ToString(CultureInfo.InvariantCulture) ?? string.Empty); if (ImGui.TableNextColumn()) - ImGui.TextUnformatted(_questData.GetQuestInfo(validationIssue.QuestId).Name); + ImGui.TextUnformatted(validationIssue.QuestId != null + ? _questData.GetQuestInfo(validationIssue.QuestId.Value).Name + : validationIssue.BeastTribe.ToString()); if (ImGui.TableNextColumn()) ImGui.TextUnformatted(validationIssue.Sequence?.ToString(CultureInfo.InvariantCulture) ?? string.Empty); @@ -81,6 +85,7 @@ internal sealed class QuestValidationWindow : LWindow ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString()); } } + ImGui.SameLine(); ImGui.TextUnformatted(validationIssue.Description); }