forked from aly/qstbak
muffin v7.5.5
This commit is contained in:
parent
a8f2c1df37
commit
6266f9e6b7
110 changed files with 14907 additions and 6073 deletions
|
|
@ -1,11 +1,16 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using System.Text.RegularExpressions;
|
||||
using Json.Schema;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.Questing;
|
||||
|
|
@ -15,8 +20,21 @@ namespace Questionable.Validation.Validators;
|
|||
|
||||
internal sealed class JsonSchemaValidator : IQuestValidator
|
||||
{
|
||||
private const string QuestSchemaUri = "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/QuestPaths/quest-v1.json";
|
||||
|
||||
private readonly Dictionary<ElementId, JsonNode> _questNodes = new Dictionary<ElementId, JsonNode>();
|
||||
|
||||
private static readonly JsonSerializerOptions ValidationSerializationOptions = new JsonSerializerOptions
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
|
||||
IncludeFields = false,
|
||||
WriteIndented = false,
|
||||
TypeInfoResolver = new DefaultJsonTypeInfoResolver
|
||||
{
|
||||
Modifiers = { (Action<JsonTypeInfo>)NoEmptyCollectionModifier }
|
||||
}
|
||||
};
|
||||
|
||||
private JsonSchema? _questSchema;
|
||||
|
||||
public JsonSchemaValidator()
|
||||
|
|
@ -100,58 +118,205 @@ internal sealed class JsonSchemaValidator : IQuestValidator
|
|||
{
|
||||
yield break;
|
||||
}
|
||||
EvaluationResults evaluationResults = _questSchema.Evaluate(value, new EvaluationOptions
|
||||
EvaluationResults evaluationResults = null;
|
||||
ValidationIssue validationIssue = null;
|
||||
try
|
||||
{
|
||||
Culture = CultureInfo.InvariantCulture,
|
||||
OutputFormat = OutputFormat.List
|
||||
});
|
||||
if (evaluationResults.IsValid)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
var array = (from r in GetInvalidResults(evaluationResults).ToArray()
|
||||
group r by r.InstanceLocation?.ToString() ?? "<root>").Select(delegate(IGrouping<string, EvaluationResults> g)
|
||||
{
|
||||
string[] messages = (from m in g.SelectMany((EvaluationResults r) => r.Errors?.Values ?? Enumerable.Empty<string>())
|
||||
where !string.IsNullOrWhiteSpace(m)
|
||||
select m.Trim()).Distinct().ToArray();
|
||||
return new
|
||||
evaluationResults = _questSchema.Evaluate(value, new EvaluationOptions
|
||||
{
|
||||
Path = g.Key,
|
||||
Messages = messages
|
||||
};
|
||||
}).ToArray();
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
stringBuilder.AppendLine("JSON Validation failed:");
|
||||
if (array.Length == 0)
|
||||
Culture = CultureInfo.InvariantCulture,
|
||||
OutputFormat = OutputFormat.List
|
||||
});
|
||||
}
|
||||
catch (ArgumentException ex) when (ex.Message.Contains("same key", StringComparison.Ordinal))
|
||||
{
|
||||
stringBuilder.AppendLine(" - <unknown>: validation failed");
|
||||
Match match = Regex.Match(ex.Message, "Key: (.+)");
|
||||
string text = (match.Success ? match.Groups[1].Value.Trim() : "unknown");
|
||||
validationIssue = new ValidationIssue
|
||||
{
|
||||
ElementId = quest.Id,
|
||||
Sequence = null,
|
||||
Step = null,
|
||||
Type = EIssueType.InvalidJsonSyntax,
|
||||
Severity = EIssueSeverity.Error,
|
||||
Description = "Duplicate JSON property '" + text + "' in quest file. Remove the duplicate key."
|
||||
};
|
||||
}
|
||||
if (validationIssue != null)
|
||||
{
|
||||
yield return validationIssue;
|
||||
}
|
||||
else
|
||||
{
|
||||
var array2 = array;
|
||||
foreach (var anon in array2)
|
||||
if (evaluationResults == null || evaluationResults.IsValid)
|
||||
{
|
||||
string value2 = ((anon.Messages.Length != 0) ? string.Join("; ", anon.Messages) : "validation failed");
|
||||
StringBuilder stringBuilder2 = stringBuilder;
|
||||
IFormatProvider invariantCulture = CultureInfo.InvariantCulture;
|
||||
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(5, 2, stringBuilder2, invariantCulture);
|
||||
handler.AppendLiteral(" - ");
|
||||
handler.AppendFormatted(anon.Path);
|
||||
handler.AppendLiteral(": ");
|
||||
handler.AppendFormatted(value2);
|
||||
stringBuilder2.AppendLine(invariantCulture, ref handler);
|
||||
yield break;
|
||||
}
|
||||
var array = (from r in GetInvalidResults(evaluationResults).ToArray()
|
||||
where !(r.EvaluationPath?.ToString() ?? string.Empty).Contains("/if", StringComparison.Ordinal)
|
||||
group r by r.InstanceLocation?.ToString() ?? "<root>").Select(delegate(IGrouping<string, EvaluationResults> g)
|
||||
{
|
||||
string[] messages = (from m in g.SelectMany((EvaluationResults r) => r.Errors?.Values ?? Enumerable.Empty<string>())
|
||||
where !string.IsNullOrWhiteSpace(m)
|
||||
select m.Trim()).Distinct().ToArray();
|
||||
return new
|
||||
{
|
||||
Path = g.Key,
|
||||
Messages = messages
|
||||
};
|
||||
}).ToArray();
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
stringBuilder.AppendLine("JSON Validation failed:");
|
||||
if (array.Length == 0)
|
||||
{
|
||||
stringBuilder.AppendLine(" - <unknown>: validation failed");
|
||||
}
|
||||
else
|
||||
{
|
||||
var array2 = array;
|
||||
foreach (var anon in array2)
|
||||
{
|
||||
string value2 = ((anon.Messages.Length != 0) ? string.Join("; ", anon.Messages) : "validation failed");
|
||||
StringBuilder stringBuilder2 = stringBuilder;
|
||||
IFormatProvider invariantCulture = CultureInfo.InvariantCulture;
|
||||
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(5, 2, stringBuilder2, invariantCulture);
|
||||
handler.AppendLiteral(" - ");
|
||||
handler.AppendFormatted(anon.Path);
|
||||
handler.AppendLiteral(": ");
|
||||
handler.AppendFormatted(value2);
|
||||
stringBuilder2.AppendLine(invariantCulture, ref handler);
|
||||
}
|
||||
}
|
||||
yield return new ValidationIssue
|
||||
{
|
||||
ElementId = quest.Id,
|
||||
Sequence = null,
|
||||
Step = null,
|
||||
Type = EIssueType.InvalidJsonSchema,
|
||||
Severity = EIssueSeverity.Error,
|
||||
Description = stringBuilder.ToString().TrimEnd()
|
||||
};
|
||||
}
|
||||
static IEnumerable<EvaluationResults> GetInvalidResults(EvaluationResults result)
|
||||
{
|
||||
if (!result.IsValid)
|
||||
{
|
||||
yield return result;
|
||||
}
|
||||
if (result.HasDetails)
|
||||
{
|
||||
foreach (EvaluationResults detail in result.Details)
|
||||
{
|
||||
foreach (EvaluationResults invalidResult in GetInvalidResults(detail))
|
||||
{
|
||||
yield return invalidResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
yield return new ValidationIssue
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationIssue> ValidateFromQuestRoot(Quest quest)
|
||||
{
|
||||
if (!(JsonSerializer.SerializeToNode(quest.Root, ValidationSerializationOptions) is JsonObject jsonObject))
|
||||
{
|
||||
ElementId = quest.Id,
|
||||
Sequence = null,
|
||||
Step = null,
|
||||
Type = EIssueType.InvalidJsonSchema,
|
||||
Severity = EIssueSeverity.Error,
|
||||
Description = stringBuilder.ToString().TrimEnd()
|
||||
};
|
||||
return Array.Empty<ValidationIssue>();
|
||||
}
|
||||
JsonObject jsonObject2 = new JsonObject { ["$schema"] = "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/QuestPaths/quest-v1.json" };
|
||||
foreach (KeyValuePair<string, JsonNode> item in jsonObject)
|
||||
{
|
||||
item.Deconstruct(out var key, out var value);
|
||||
string propertyName = key;
|
||||
jsonObject2[propertyName] = value?.DeepClone();
|
||||
}
|
||||
return EvaluateQuestNode(quest, jsonObject2);
|
||||
}
|
||||
|
||||
private IEnumerable<ValidationIssue> EvaluateQuestNode(Quest quest, JsonNode questNode)
|
||||
{
|
||||
if (_questSchema == null)
|
||||
{
|
||||
_questSchema = JsonSchema.FromStream(AssemblyQuestLoader.QuestSchema).AsTask().Result;
|
||||
}
|
||||
EvaluationResults evaluationResults = null;
|
||||
ValidationIssue validationIssue = null;
|
||||
try
|
||||
{
|
||||
evaluationResults = _questSchema.Evaluate(questNode, new EvaluationOptions
|
||||
{
|
||||
Culture = CultureInfo.InvariantCulture,
|
||||
OutputFormat = OutputFormat.List
|
||||
});
|
||||
}
|
||||
catch (ArgumentException ex) when (ex.Message.Contains("same key", StringComparison.Ordinal))
|
||||
{
|
||||
Match match = Regex.Match(ex.Message, "Key: (.+)");
|
||||
string text = (match.Success ? match.Groups[1].Value.Trim() : "unknown");
|
||||
validationIssue = new ValidationIssue
|
||||
{
|
||||
ElementId = quest.Id,
|
||||
Sequence = null,
|
||||
Step = null,
|
||||
Type = EIssueType.InvalidJsonSyntax,
|
||||
Severity = EIssueSeverity.Error,
|
||||
Description = "Duplicate JSON property '" + text + "' in quest file. Remove the duplicate key."
|
||||
};
|
||||
}
|
||||
if (validationIssue != null)
|
||||
{
|
||||
yield return validationIssue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (evaluationResults == null || evaluationResults.IsValid)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
var array = (from r in GetInvalidResults(evaluationResults).ToArray()
|
||||
where !(r.EvaluationPath?.ToString() ?? string.Empty).Contains("/if", StringComparison.Ordinal)
|
||||
group r by r.InstanceLocation?.ToString() ?? "<root>").Select(delegate(IGrouping<string, EvaluationResults> g)
|
||||
{
|
||||
string[] messages = (from m in g.SelectMany((EvaluationResults r) => r.Errors?.Values ?? Enumerable.Empty<string>())
|
||||
where !string.IsNullOrWhiteSpace(m)
|
||||
select m.Trim()).Distinct().ToArray();
|
||||
return new
|
||||
{
|
||||
Path = g.Key,
|
||||
Messages = messages
|
||||
};
|
||||
}).ToArray();
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
stringBuilder.AppendLine("JSON Validation failed:");
|
||||
if (array.Length == 0)
|
||||
{
|
||||
stringBuilder.AppendLine(" - <unknown>: validation failed");
|
||||
}
|
||||
else
|
||||
{
|
||||
var array2 = array;
|
||||
foreach (var anon in array2)
|
||||
{
|
||||
string value = ((anon.Messages.Length != 0) ? string.Join("; ", anon.Messages) : "validation failed");
|
||||
StringBuilder stringBuilder2 = stringBuilder;
|
||||
IFormatProvider invariantCulture = CultureInfo.InvariantCulture;
|
||||
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(5, 2, stringBuilder2, invariantCulture);
|
||||
handler.AppendLiteral(" - ");
|
||||
handler.AppendFormatted(anon.Path);
|
||||
handler.AppendLiteral(": ");
|
||||
handler.AppendFormatted(value);
|
||||
stringBuilder2.AppendLine(invariantCulture, ref handler);
|
||||
}
|
||||
}
|
||||
yield return new ValidationIssue
|
||||
{
|
||||
ElementId = quest.Id,
|
||||
Sequence = null,
|
||||
Step = null,
|
||||
Type = EIssueType.InvalidJsonSchema,
|
||||
Severity = EIssueSeverity.Error,
|
||||
Description = stringBuilder.ToString().TrimEnd()
|
||||
};
|
||||
}
|
||||
static IEnumerable<EvaluationResults> GetInvalidResults(EvaluationResults result)
|
||||
{
|
||||
if (!result.IsValid)
|
||||
|
|
@ -180,4 +345,15 @@ internal sealed class JsonSchemaValidator : IQuestValidator
|
|||
{
|
||||
_questNodes.Clear();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.Questing;
|
||||
|
||||
namespace Questionable.Validation.Validators;
|
||||
|
||||
internal sealed class LandingZoneValidator : IQuestValidator
|
||||
{
|
||||
public IEnumerable<ValidationIssue> Validate(Quest quest)
|
||||
{
|
||||
foreach (var stepInfo in quest.AllSteps().Where<(QuestSequence, int, QuestStep)>(delegate((QuestSequence Sequence, int StepId, QuestStep Step) x)
|
||||
{
|
||||
List<LandingZone> landingZones = x.Step.LandingZones;
|
||||
return landingZones != null && landingZones.Count > 0;
|
||||
}))
|
||||
{
|
||||
for (int z = 0; z < stepInfo.Item3.LandingZones.Count; z++)
|
||||
{
|
||||
LandingZone zone = stepInfo.Item3.LandingZones[z];
|
||||
if (zone.Vertices.Count < 3)
|
||||
{
|
||||
yield return new ValidationIssue
|
||||
{
|
||||
ElementId = quest.Id,
|
||||
Sequence = stepInfo.Item1.Sequence,
|
||||
Step = stepInfo.Item2,
|
||||
Type = EIssueType.InvalidLandingZone,
|
||||
Severity = EIssueSeverity.Error,
|
||||
Description = $"Landing zone {z} has fewer than 3 vertices"
|
||||
};
|
||||
continue;
|
||||
}
|
||||
if (!LandingZoneMath.IsConvex(zone.Vertices))
|
||||
{
|
||||
yield return new ValidationIssue
|
||||
{
|
||||
ElementId = quest.Id,
|
||||
Sequence = stepInfo.Item1.Sequence,
|
||||
Step = stepInfo.Item2,
|
||||
Type = EIssueType.InvalidLandingZone,
|
||||
Severity = EIssueSeverity.Error,
|
||||
Description = $"Landing zone {z} is not convex"
|
||||
};
|
||||
}
|
||||
if (LandingZoneMath.CalculatePolygonArea(zone.Vertices) < 0.01f)
|
||||
{
|
||||
yield return new ValidationIssue
|
||||
{
|
||||
ElementId = quest.Id,
|
||||
Sequence = stepInfo.Item1.Sequence,
|
||||
Step = stepInfo.Item2,
|
||||
Type = EIssueType.InvalidLandingZone,
|
||||
Severity = EIssueSeverity.Error,
|
||||
Description = $"Landing zone {z} has zero area (collinear vertices)"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue