using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Runtime.InteropServices; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using Json.Schema; using Questionable.Model; using Questionable.Model.Questing; namespace Questionable.Validation.Validators; internal sealed class JsonSchemaValidator : IQuestValidator { private readonly SchemaRegistrar _schemaRegistrar; private readonly Dictionary _questNodes = new Dictionary(); private JsonSchema? _questSchema; private const string QuestSchemaUri = "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/QuestPaths/quest-v1.json"; private static readonly JsonSerializerOptions ValidationSerializationOptions = new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, IncludeFields = false, WriteIndented = false, TypeInfoResolver = new DefaultJsonTypeInfoResolver { Modifiers = { (Action)NoEmptyCollectionModifier } } }; public JsonSchemaValidator(SchemaRegistrar schemaRegistrar) { _schemaRegistrar = schemaRegistrar; } public IEnumerable Validate(Quest quest) { if (!_questNodes.TryGetValue(quest.Id, out JsonNode questNode)) { yield break; } if (_questSchema == null) { _questSchema = _schemaRegistrar.Get("https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/QuestPaths/quest-v1.json"); } EvaluationResults evaluationResults = _questSchema.Evaluate(JsonSerializer.SerializeToElement(questNode), new EvaluationOptions { Culture = CultureInfo.InvariantCulture, OutputFormat = OutputFormat.Hierarchical }); if (evaluationResults.IsValid) { yield break; } Dictionary sequenceMap = BuildSequenceMap(questNode); List list = new List(); SchemaValidationHelper.CollectLeafErrors(evaluationResults, list); if (list.Count == 0) { yield return new ValidationIssue { PathType = EPathType.Quest, Id = quest.Id.ToString(), Location = null, Type = EIssueType.InvalidJsonSchema, Severity = EIssueSeverity.Error, Description = "JSON schema validation failed" }; yield break; } List<(byte?, int?, string, string)> source = list.Select(delegate(SchemaValidationHelper.ParsedError e) { var (item, item2, rawPath) = ParseInstanceLocation(e.InstancePath, sequenceMap); return (Sequence: item, Step: item2, PropertyPath: SchemaValidationHelper.FormatPath(rawPath), Message: e.Message); }).ToList(); foreach (IGrouping<(byte?, int?), (byte?, int?, string, string)> item3 in from e in source group e by (Sequence: e.Sequence, Step: e.Step)) { List list2 = item3.Select<(byte?, int?, string, string), string>(((byte? Sequence, int? Step, string PropertyPath, string Message) e) => (!string.IsNullOrEmpty(e.PropertyPath)) ? (e.PropertyPath + ": " + e.Message) : e.Message).Distinct().ToList(); SimplifyConditionalRequiredErrors(list2, FindStepNode(questNode, item3.Key.Item1, item3.Key.Item2)); if (list2.Count != 0) { yield return new ValidationIssue { PathType = EPathType.Quest, Id = quest.Id.ToString(), Location = SchemaValidationHelper.FormatQuestLocation(item3.Key.Item1, item3.Key.Item2), Type = EIssueType.InvalidJsonSchema, Severity = EIssueSeverity.Error, Description = string.Join("\n", list2) }; } } } public List ValidateImmediate(ElementId elementId, JsonNode questNode) { if (_questSchema == null) { _questSchema = _schemaRegistrar.Get("https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/QuestPaths/quest-v1.json"); } EvaluationResults evaluationResults = _questSchema.Evaluate(JsonSerializer.SerializeToElement(questNode), new EvaluationOptions { Culture = CultureInfo.InvariantCulture, OutputFormat = OutputFormat.Hierarchical }); if (evaluationResults.IsValid) { return new List(); } Dictionary sequenceMap = BuildSequenceMap(questNode); List list = new List(); SchemaValidationHelper.CollectLeafErrors(evaluationResults, list); if (list.Count == 0) { int num = 1; List list2 = new List(num); CollectionsMarshal.SetCount(list2, num); CollectionsMarshal.AsSpan(list2)[0] = new ValidationIssue { PathType = EPathType.Quest, Id = elementId.ToString(), Location = null, Type = EIssueType.InvalidJsonSchema, Severity = EIssueSeverity.Error, Description = "JSON schema validation failed" }; return list2; } List list3 = new List(); foreach (IGrouping<(byte?, int?), (byte?, int?, string, string)> item3 in from e in list.Select(delegate(SchemaValidationHelper.ParsedError e) { var (item, item2, rawPath) = ParseInstanceLocation(e.InstancePath, sequenceMap); return (Sequence: item, Step: item2, PropertyPath: SchemaValidationHelper.FormatPath(rawPath), Message: e.Message); }).ToList() group e by (Sequence: e.Sequence, Step: e.Step)) { List list4 = item3.Select<(byte?, int?, string, string), string>(((byte? Sequence, int? Step, string PropertyPath, string Message) e) => (!string.IsNullOrEmpty(e.PropertyPath)) ? (e.PropertyPath + ": " + e.Message) : e.Message).Distinct().ToList(); if (list4.Count != 0) { list3.Add(new ValidationIssue { PathType = EPathType.Quest, Id = elementId.ToString(), Location = SchemaValidationHelper.FormatQuestLocation(item3.Key.Item1, item3.Key.Item2), Type = EIssueType.InvalidJsonSchema, Severity = EIssueSeverity.Error, Description = string.Join("\n", list4) }); } } return list3; } private static void SimplifyConditionalRequiredErrors(List lines, JsonObject? step) { if (step == null) { return; } foreach (string property in from x in step where x.Value != null select x.Key) { lines.RemoveAll((string x) => IsRequiredPropertyError(x, property)); } if (step["InteractionType"]?.GetValue() != "Combat") { lines.RemoveAll((string x) => IsRequiredPropertyError(x, "EnemySpawnType") || IsRequiredPropertyError(x, "KillEnemyDataIds") || IsRequiredPropertyError(x, "ComplexCombatData")); return; } string obj = step["EnemySpawnType"]?.GetValue(); if (obj != "AfterItemUse") { lines.RemoveAll((string x) => IsRequiredPropertyError(x, "ItemId")); } if (obj == "FinishCombatIfAny") { lines.RemoveAll((string x) => IsRequiredPropertyError(x, "KillEnemyDataIds") || IsRequiredPropertyError(x, "ComplexCombatData")); return; } bool num = lines.RemoveAll((string x) => IsRequiredPropertyError(x, "KillEnemyDataIds")) > 0; bool flag = lines.RemoveAll((string x) => IsRequiredPropertyError(x, "ComplexCombatData")) > 0; if (num && flag) { lines.Add("One combat target definition is required: KillEnemyDataIds or ComplexCombatData"); } } private static bool IsRequiredPropertyError(string message, string property) { return message.Contains("Required properties [\"" + property + "\"]", StringComparison.Ordinal); } private static JsonObject? FindStepNode(JsonNode questNode, byte? sequence, int? step) { if (!sequence.HasValue || !step.HasValue || !(questNode["QuestSequence"] is JsonArray source)) { return null; } if (!(source.OfType().FirstOrDefault((JsonObject x) => x["Sequence"]?.GetValue() == sequence.Value)?["Steps"] is JsonArray jsonArray) || step.Value < 0 || step.Value >= jsonArray.Count) { return null; } return jsonArray[step.Value] as JsonObject; } public void Enqueue(ElementId elementId, JsonNode questNode) { _questNodes[elementId] = questNode; } public void Reset() { _questNodes.Clear(); } private static Dictionary BuildSequenceMap(JsonNode questNode) { Dictionary dictionary = new Dictionary(); if (!(questNode["QuestSequence"] is JsonArray jsonArray)) { return dictionary; } for (int i = 0; i < jsonArray.Count; i++) { JsonNode jsonNode = jsonArray[i]?["Sequence"]; if (jsonNode != null && jsonNode.GetValueKind() == JsonValueKind.Number) { dictionary[i] = (byte)jsonNode.GetValue(); } } return dictionary; } private static (byte? Sequence, int? Step, string Path) ParseInstanceLocation(string instancePath, Dictionary sequenceMap) { if (string.IsNullOrEmpty(instancePath)) { return (Sequence: null, Step: null, Path: string.Empty); } string[] array = instancePath.Split('/'); byte? item = null; int? item2 = null; int num = 1; for (int i = 1; i < array.Length - 1; i++) { if (!(array[i] != "QuestSequence") && int.TryParse(array[i + 1], out var result)) { item = (sequenceMap.TryGetValue(result, out var value) ? new byte?(value) : ((byte?)null)); num = i + 2; if (i + 3 < array.Length && array[i + 2] == "Steps" && int.TryParse(array[i + 3], out var result2)) { item2 = result2; num = i + 4; } break; } } string item3 = ((num < array.Length) ? string.Join("/", array, num, array.Length - num) : string.Empty); return (Sequence: item, Step: item2, Path: item3); } private static void NoEmptyCollectionModifier(JsonTypeInfo typeInfo) { foreach (JsonPropertyInfo property in typeInfo.Properties) { if (typeof(ICollection).IsAssignableFrom(property.PropertyType)) { property.ShouldSerialize = (object _, object? val) => val is ICollection collection && collection.Count > 0; } } } public IEnumerable ValidateFromQuestRoot(Quest quest) { return ValidateFromQuestRoot(quest.Id, quest.Root); } public IEnumerable ValidateFromQuestRoot(ElementId elementId, QuestRoot questRoot) { if (!(JsonSerializer.SerializeToNode(questRoot, ValidationSerializationOptions) is JsonObject jsonObject)) { return Array.Empty(); } JsonObject jsonObject2 = new JsonObject { ["$schema"] = "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/QuestPaths/quest-v1.json" }; foreach (KeyValuePair item in jsonObject) { item.Deconstruct(out var key, out var value); string propertyName = key; jsonObject2[propertyName] = value?.DeepClone(); } return ValidateImmediate(elementId, jsonObject2); } }