punish v6.8.18.0

This commit is contained in:
alydev 2025-10-09 07:47:19 +10:00
commit cfb4dea47e
316 changed files with 554088 additions and 0 deletions

View file

@ -0,0 +1,49 @@
using System.Collections.Generic;
using System.Linq;
using Questionable.Data;
using Questionable.Model;
using Questionable.Model.Common;
using Questionable.Model.Questing;
namespace Questionable.Validation.Validators;
internal sealed class AethernetShortcutValidator : IQuestValidator
{
private readonly AetheryteData _aetheryteData;
public AethernetShortcutValidator(AetheryteData aetheryteData)
{
_aetheryteData = aetheryteData;
}
public IEnumerable<ValidationIssue> Validate(Quest quest)
{
return (from x in quest.AllSteps()
select Validate(quest.Id, x.Sequence.Sequence, x.StepId, x.Step.AethernetShortcut) into x
where x != null
select x).Cast<ValidationIssue>();
}
private ValidationIssue? Validate(ElementId elementId, int sequenceNo, int stepId, AethernetShortcut? aethernetShortcut)
{
if (aethernetShortcut == null)
{
return null;
}
ushort valueOrDefault = _aetheryteData.AethernetGroups.GetValueOrDefault(aethernetShortcut.From);
ushort valueOrDefault2 = _aetheryteData.AethernetGroups.GetValueOrDefault(aethernetShortcut.To);
if (valueOrDefault != valueOrDefault2)
{
return new ValidationIssue
{
ElementId = elementId,
Sequence = (byte)sequenceNo,
Step = stepId,
Type = EIssueType.InvalidAethernetShortcut,
Severity = EIssueSeverity.Error,
Description = $"Invalid aethernet shortcut: {aethernetShortcut.From} to {aethernetShortcut.To}"
};
}
return null;
}
}

View file

@ -0,0 +1,101 @@
using System.Collections.Generic;
using System.Linq;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Validation.Validators;
internal sealed class BasicSequenceValidator : IQuestValidator
{
public IEnumerable<ValidationIssue> Validate(Quest quest)
{
List<QuestSequence> sequences = quest.Root.QuestSequence;
QuestSequence foundStart = sequences.FirstOrDefault((QuestSequence x) => x.Sequence == 0);
if (foundStart == null)
{
yield return new ValidationIssue
{
ElementId = quest.Id,
Sequence = 0,
Step = null,
Type = EIssueType.MissingSequence0,
Severity = EIssueSeverity.Error,
Description = "Missing quest start"
};
}
else if (quest.Info is QuestInfo { CompletesInstantly: not false })
{
foreach (QuestSequence item in sequences)
{
if (item != foundStart)
{
yield return new ValidationIssue
{
ElementId = quest.Id,
Sequence = item.Sequence,
Step = null,
Type = EIssueType.InstantQuestWithMultipleSteps,
Severity = EIssueSeverity.Error,
Description = "Instant quest should not have any sequences after the start"
};
}
}
}
else
{
if (!(quest.Info is QuestInfo))
{
yield break;
}
int maxSequence = (from x in sequences
select x.Sequence into x
where x != byte.MaxValue
select x).Max();
int i;
for (i = 0; i < maxSequence; i++)
{
List<QuestSequence> foundSequences = sequences.Where((QuestSequence x) => x.Sequence == i).ToList();
ValidationIssue validationIssue = ValidateSequences(quest, i, foundSequences);
if (validationIssue != null)
{
yield return validationIssue;
}
}
List<QuestSequence> foundSequences2 = sequences.Where((QuestSequence x) => x.Sequence == byte.MaxValue).ToList();
ValidationIssue validationIssue2 = ValidateSequences(quest, 255, foundSequences2);
if (validationIssue2 != null)
{
yield return validationIssue2;
}
}
}
private static ValidationIssue? ValidateSequences(Quest quest, int sequenceNo, List<QuestSequence> foundSequences)
{
if (foundSequences.Count == 0)
{
return new ValidationIssue
{
ElementId = quest.Id,
Sequence = (byte)sequenceNo,
Step = null,
Type = EIssueType.MissingSequence,
Severity = EIssueSeverity.Error,
Description = "Missing sequence"
};
}
if (foundSequences.Count == 2)
{
return new ValidationIssue
{
ElementId = quest.Id,
Sequence = (byte)sequenceNo,
Step = null,
Type = EIssueType.DuplicateSequence,
Severity = EIssueSeverity.Error,
Description = "Duplicate sequence"
};
}
return null;
}
}

View file

@ -0,0 +1,50 @@
using System.Collections.Generic;
using LLib.GameData;
using Questionable.Data;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Validation.Validators;
internal sealed class ClassQuestShouldHaveShortcutValidator : IQuestValidator
{
private readonly HashSet<ElementId> _classJobQuests = new HashSet<ElementId>();
public ClassQuestShouldHaveShortcutValidator(QuestData questData)
{
foreach (EClassJob enumValue in typeof(EClassJob).GetEnumValues())
{
if (enumValue == EClassJob.Adventurer)
{
continue;
}
foreach (QuestInfo classJobQuest in questData.GetClassJobQuests(enumValue))
{
if (classJobQuest.Level > 1)
{
_classJobQuests.Add(classJobQuest.QuestId);
}
}
}
}
public IEnumerable<ValidationIssue> Validate(Quest quest)
{
if (_classJobQuests.Contains(quest.Id))
{
QuestStep questStep = quest.FindSequence(0)?.FindStep(0);
if (questStep != null && !questStep.IsTeleportableForPriorityQuests())
{
yield return new ValidationIssue
{
ElementId = quest.Id,
Sequence = 0,
Step = 0,
Type = EIssueType.ClassQuestWithoutAetheryteShortcut,
Severity = EIssueSeverity.Error,
Description = "Class quest should have an aetheryte shortcut to be done automatically"
};
}
}
}
}

View file

@ -0,0 +1,46 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Questionable.Controller.Utils;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Validation.Validators;
internal sealed class CompletionFlagsValidator : IQuestValidator
{
public IEnumerable<ValidationIssue> Validate(Quest quest)
{
if (quest.Id.Value == 5149)
{
yield break;
}
foreach (QuestSequence sequence in quest.AllSequences())
{
List<long> mappedCompletionFlags = sequence.Steps.Select((QuestStep x) => QuestWorkUtils.HasCompletionFlags(x.CompletionQuestVariablesFlags) ? Enumerable.Range(0, 6).Select(delegate(int y)
{
QuestWorkValue questWorkValue = x.CompletionQuestVariablesFlags[y];
return (long)((questWorkValue == null) ? 0 : BitOperations.RotateLeft((ulong)(questWorkValue.High.GetValueOrDefault() * 16 + questWorkValue.Low.GetValueOrDefault()), 8 * y));
}).Sum() : 0).ToList();
int i = 0;
while (i < sequence.Steps.Count)
{
long flags = mappedCompletionFlags[i];
if (flags != 0L && mappedCompletionFlags.Count((long x) => x == flags) >= 2)
{
yield return new ValidationIssue
{
ElementId = quest.Id,
Sequence = sequence.Sequence,
Step = i,
Type = EIssueType.DuplicateCompletionFlags,
Severity = EIssueSeverity.Error,
Description = "Duplicate completion flags: " + string.Join(", ", sequence.Steps[i].CompletionQuestVariablesFlags)
};
}
int num = i + 1;
i = num;
}
}
}
}

View file

@ -0,0 +1,80 @@
using System.Collections.Generic;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Validation.Validators;
internal sealed class DialogueChoiceValidator : IQuestValidator
{
private readonly ExcelFunctions _excelFunctions;
public DialogueChoiceValidator(ExcelFunctions excelFunctions)
{
_excelFunctions = excelFunctions;
}
public IEnumerable<ValidationIssue> Validate(Quest quest)
{
foreach (var x in quest.AllSteps())
{
if (x.Step.DialogueChoices.Count == 0)
{
continue;
}
foreach (DialogueChoice dialogueChoice in x.Step.DialogueChoices)
{
ExcelRef prompt = dialogueChoice.Prompt;
if (prompt != null)
{
ValidationIssue validationIssue = Validate(quest, x.Sequence, x.StepId, dialogueChoice.ExcelSheet, prompt, "Prompt");
if (validationIssue != null)
{
yield return validationIssue;
}
}
ExcelRef answer = dialogueChoice.Answer;
if (answer != null)
{
ValidationIssue validationIssue2 = Validate(quest, x.Sequence, x.StepId, dialogueChoice.ExcelSheet, answer, "Answer");
if (validationIssue2 != null)
{
yield return validationIssue2;
}
}
}
}
}
private ValidationIssue? Validate(Quest quest, QuestSequence sequence, int stepId, string? excelSheet, ExcelRef excelRef, string label)
{
if (excelRef.Type == ExcelRef.EType.Key)
{
if (!_excelFunctions.GetRawDialogueText(quest, excelSheet, excelRef.AsKey()).HasValue)
{
return new ValidationIssue
{
ElementId = quest.Id,
Sequence = sequence.Sequence,
Step = stepId,
Type = EIssueType.InvalidExcelRef,
Severity = EIssueSeverity.Error,
Description = $"{label} invalid: {excelSheet} → {excelRef.AsKey()}"
};
}
}
else if (excelRef.Type == ExcelRef.EType.RowId && !_excelFunctions.GetRawDialogueTextByRowId(excelSheet, excelRef.AsRowId()).HasValue)
{
return new ValidationIssue
{
ElementId = quest.Id,
Sequence = sequence.Sequence,
Step = stepId,
Type = EIssueType.InvalidExcelRef,
Severity = EIssueSeverity.Error,
Description = $"{label} invalid: {excelSheet} → {excelRef.AsRowId()}"
};
}
return null;
}
}

View file

@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json.Nodes;
using Json.Schema;
using Questionable.Model;
using Questionable.Model.Questing;
using Questionable.QuestPaths;
namespace Questionable.Validation.Validators;
internal sealed class JsonSchemaValidator : IQuestValidator
{
private readonly Dictionary<ElementId, JsonNode> _questNodes = new Dictionary<ElementId, JsonNode>();
private JsonSchema? _questSchema;
public JsonSchemaValidator()
{
SchemaRegistry.Global.Register(new Uri("https://qstxiv.github.io/schema/common-aethernetshard.json"), JsonSchema.FromStream(AssemblyModelLoader.CommonAethernetShard).AsTask().Result);
SchemaRegistry.Global.Register(new Uri("https://qstxiv.github.io/schema/common-aetheryte.json"), JsonSchema.FromStream(AssemblyModelLoader.CommonAetheryte).AsTask().Result);
SchemaRegistry.Global.Register(new Uri("https://qstxiv.github.io/schema/common-classjob.json"), JsonSchema.FromStream(AssemblyModelLoader.CommonClassJob).AsTask().Result);
SchemaRegistry.Global.Register(new Uri("https://qstxiv.github.io/schema/common-completionflags.json"), JsonSchema.FromStream(AssemblyModelLoader.CommonCompletionFlags).AsTask().Result);
SchemaRegistry.Global.Register(new Uri("https://qstxiv.github.io/schema/common-vector3.json"), JsonSchema.FromStream(AssemblyModelLoader.CommonVector3).AsTask().Result);
}
public IEnumerable<ValidationIssue> Validate(Quest quest)
{
if (_questSchema == null)
{
_questSchema = JsonSchema.FromStream(AssemblyQuestLoader.QuestSchema).AsTask().Result;
}
if (_questNodes.TryGetValue(quest.Id, out JsonNode value) && !_questSchema.Evaluate(value, new EvaluationOptions
{
Culture = CultureInfo.InvariantCulture,
OutputFormat = OutputFormat.List
}).IsValid)
{
yield return new ValidationIssue
{
ElementId = quest.Id,
Sequence = null,
Step = null,
Type = EIssueType.InvalidJsonSchema,
Severity = EIssueSeverity.Error,
Description = "JSON Validation failed"
};
}
}
public void Enqueue(ElementId elementId, JsonNode questNode)
{
_questNodes[elementId] = questNode;
}
public void Reset()
{
_questNodes.Clear();
}
}

View file

@ -0,0 +1,27 @@
using System.Collections.Generic;
using System.Linq;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Validation.Validators;
internal sealed class NextQuestValidator : IQuestValidator
{
public IEnumerable<ValidationIssue> Validate(Quest quest)
{
foreach (var item in from x in quest.AllSteps()
where x.Step.NextQuestId == quest.Id
select x)
{
yield return new ValidationIssue
{
ElementId = quest.Id,
Sequence = item.Item1.Sequence,
Step = item.Item2,
Type = EIssueType.InvalidNextQuestId,
Severity = EIssueSeverity.Error,
Description = "Next quest should not reference itself"
};
}
}
}

View file

@ -0,0 +1,23 @@
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
{
ElementId = quest.Id,
Sequence = null,
Step = null,
Type = EIssueType.QuestDisabled,
Severity = EIssueSeverity.None,
Description = "Quest is disabled"
};
}
}
}

View file

@ -0,0 +1,44 @@
using System.Collections.Generic;
using System.Linq;
using Lumina.Text.ReadOnly;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Validation.Validators;
internal sealed class SayValidator : IQuestValidator
{
private readonly ExcelFunctions _excelFunctions;
public SayValidator(ExcelFunctions excelFunctions)
{
_excelFunctions = excelFunctions;
}
public IEnumerable<ValidationIssue> Validate(Quest quest)
{
foreach (var item in from x in quest.AllSteps()
where x.Step.InteractionType == EInteractionType.Say
select x)
{
ChatMessage chatMessage = item.Item3.ChatMessage;
if (chatMessage != null)
{
ReadOnlySeString? rawDialogueText = _excelFunctions.GetRawDialogueText(quest, chatMessage.ExcelSheet, chatMessage.Key);
if (rawDialogueText.HasValue && rawDialogueText.Value.PayloadCount != 1)
{
yield return new ValidationIssue
{
ElementId = quest.Id,
Sequence = item.Item1.Sequence,
Step = item.Item2,
Type = EIssueType.InvalidChatMessage,
Severity = EIssueSeverity.Error,
Description = $"Invalid chat message: {rawDialogueText.Value}"
};
}
}
}
}
}

View file

@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Linq;
using Questionable.Data;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Validation.Validators;
internal sealed class SinglePlayerInstanceValidator : IQuestValidator
{
private readonly Dictionary<ElementId, List<byte>> _questIdToDutyIndexes;
public SinglePlayerInstanceValidator(TerritoryData territoryData)
{
_questIdToDutyIndexes = (from x in territoryData.GetAllQuestsWithQuestBattles()
group x by x.QuestId).ToDictionary((IGrouping<ElementId, (ElementId QuestId, byte Index, TerritoryData.ContentFinderConditionData Data)> x) => x.Key, (IGrouping<ElementId, (ElementId QuestId, byte Index, TerritoryData.ContentFinderConditionData Data)> x) => x.Select(((ElementId QuestId, byte Index, TerritoryData.ContentFinderConditionData Data) y) => y.Index).ToList());
}
public IEnumerable<ValidationIssue> Validate(Quest quest)
{
if (!_questIdToDutyIndexes.TryGetValue(quest.Id, out List<byte> value))
{
yield break;
}
foreach (byte index in value)
{
if (!quest.AllSteps().Any<(QuestSequence, int, QuestStep)>(((QuestSequence Sequence, int StepId, QuestStep Step) x) => x.Step.InteractionType == EInteractionType.SinglePlayerDuty && x.Step.SinglePlayerDutyIndex == index))
{
yield return new ValidationIssue
{
ElementId = quest.Id,
Sequence = null,
Step = null,
Type = EIssueType.UnusedSinglePlayerInstance,
Severity = EIssueSeverity.Error,
Description = $"Single player instance {index} not used"
};
}
}
}
}

View file

@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Linq;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Validation.Validators;
internal sealed class UniqueSinglePlayerInstanceValidator : IQuestValidator
{
public IEnumerable<ValidationIssue> Validate(Quest quest)
{
List<(QuestSequence, int, byte)> list = (from x in quest.AllSteps()
where x.Step.InteractionType == EInteractionType.SinglePlayerDuty
select (Sequence: x.Sequence, StepId: x.StepId, SinglePlayerDutyIndex: x.Step.SinglePlayerDutyIndex)).ToList();
if (list.DistinctBy<(QuestSequence, int, byte), byte>(((QuestSequence Sequence, int StepId, byte SinglePlayerDutyIndex) x) => x.SinglePlayerDutyIndex).Count() >= list.Count)
{
yield break;
}
foreach (var item in list)
{
yield return new ValidationIssue
{
ElementId = quest.Id,
Sequence = item.Item1.Sequence,
Step = item.Item2,
Type = EIssueType.DuplicateSinglePlayerInstance,
Severity = EIssueSeverity.Error,
Description = $"Duplicate singleplayer duty index: {item.Item3}"
};
}
}
}

View file

@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Validation.Validators;
internal sealed class UniqueStartStopValidator : IQuestValidator
{
public IEnumerable<ValidationIssue> Validate(Quest quest)
{
ElementId id = quest.Id;
if ((id is SatisfactionSupplyNpcId || id is AlliedSocietyDailyId) ? true : false)
{
yield break;
}
int num = 1;
List<EInteractionType> list = new List<EInteractionType>(num);
CollectionsMarshal.SetCount(list, num);
Span<EInteractionType> span = CollectionsMarshal.AsSpan(list);
int index = 0;
span[index] = EInteractionType.AcceptQuest;
List<(QuestSequence Sequence, int StepId, QuestStep Step)> questAccepts = (from x in FindQuestStepsWithInteractionType(quest, list)
where x.Step.PickUpQuestId == null
select x).ToList();
foreach (var item in questAccepts)
{
if (item.Sequence.Sequence != 0 || item.StepId != quest.FindSequence(0).Steps.Count - 1)
{
yield return new ValidationIssue
{
ElementId = quest.Id,
Sequence = item.Sequence.Sequence,
Step = item.StepId,
Type = EIssueType.UnexpectedAcceptQuestStep,
Severity = EIssueSeverity.Error,
Description = "Unexpected AcceptQuest step"
};
}
}
if (quest.FindSequence(0) != null && questAccepts.Count == 0)
{
yield return new ValidationIssue
{
ElementId = quest.Id,
Sequence = 0,
Step = null,
Type = EIssueType.MissingQuestAccept,
Severity = EIssueSeverity.Error,
Description = "No AcceptQuest step"
};
}
index = 1;
List<EInteractionType> list2 = new List<EInteractionType>(index);
CollectionsMarshal.SetCount(list2, index);
span = CollectionsMarshal.AsSpan(list2);
num = 0;
span[num] = EInteractionType.CompleteQuest;
List<(QuestSequence Sequence, int StepId, QuestStep Step)> questCompletes = (from x in FindQuestStepsWithInteractionType(quest, list2)
where x.Step.TurnInQuestId == null
select x).ToList();
foreach (var item2 in questCompletes)
{
if (item2.Sequence.Sequence != byte.MaxValue || item2.StepId != quest.FindSequence(byte.MaxValue).Steps.Count - 1)
{
yield return new ValidationIssue
{
ElementId = quest.Id,
Sequence = item2.Sequence.Sequence,
Step = item2.StepId,
Type = EIssueType.UnexpectedCompleteQuestStep,
Severity = EIssueSeverity.Error,
Description = "Unexpected CompleteQuest step"
};
}
}
if (quest.FindSequence(byte.MaxValue) != null && questCompletes.Count == 0)
{
yield return new ValidationIssue
{
ElementId = quest.Id,
Sequence = (byte)byte.MaxValue,
Step = null,
Type = EIssueType.MissingQuestComplete,
Severity = EIssueSeverity.Error,
Description = "No CompleteQuest step"
};
}
}
private static IEnumerable<(QuestSequence Sequence, int StepId, QuestStep Step)> FindQuestStepsWithInteractionType(Quest quest, List<EInteractionType> interactionType)
{
return from x in quest.AllSteps()
where interactionType.Contains(x.Step.InteractionType)
select x;
}
}