1
0
Fork 0
forked from aly/qstbak

muffin v7.5.5

This commit is contained in:
alydev 2026-08-17 20:25:32 +10:00
parent a8f2c1df37
commit 6266f9e6b7
110 changed files with 14907 additions and 6073 deletions

View file

@ -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;
}
}
}
}

View file

@ -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)"
};
}
}
}
}
}