Validator reads schemas: schema checks, references and kinds from mod schemas, option and when conditions checked
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
df702b0390
commit
1ae0b79d1b
6 files changed
+311
-120
No files matched your search
+1
-1
@@ -6,5 +6,5 @@
|
||||
"description": "Static validation of mods without running the game: manifest schema, entity strategies (characters, items, quests, skills, recipes, actions), cross references, depends, scenario commands and scene patches, with an optional text report.",
|
||||
"category": "debug",
|
||||
"sources": ["src/**/*.cs"],
|
||||
"depends": ["dialog_base>=1", "interactive_base>=1"]
|
||||
"depends": ["dialog_base>=1", "interactive_base>=1", "schema_base>=1"]
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Cthangover.Validator
|
||||
{
|
||||
/// <summary>
|
||||
/// Declarative map "scenario action → named argument → <see cref="ValidationKind"/>"
|
||||
/// for actions whose arguments reference declared content. The runtime
|
||||
/// <c>IScenarioAction</c> contract carries no parameter metadata, so the
|
||||
/// validator keeps the knowledge here instead: each entry names one action
|
||||
/// argument and the entity kind it must resolve to. Scenario validation adds
|
||||
/// a graph reference per literal value (see
|
||||
/// <see cref="ScenarioCommandValidationStrategy"/>), and
|
||||
/// <see cref="ReferenceStrategy"/> reports a missing declaration with the
|
||||
/// exact scenario file and line — the static answer to the typo class the
|
||||
/// runtime factories can only log.
|
||||
/// <para/>
|
||||
/// The table intentionally covers only actions with literal content ids
|
||||
/// today (party.add / character.send_notification /
|
||||
/// character.add_skill / battle.init). Extend it when a new action starts
|
||||
/// accepting content references; the mechanics need no code changes.
|
||||
/// Comma-separated lists (battle.init enemies=) and runtime-computed values
|
||||
/// (the ${name} marker) are supported by the caller, not here.
|
||||
/// </summary>
|
||||
public static class ActionArgumentReferences
|
||||
{
|
||||
private static readonly Dictionary<string, string> CharacterType = new Dictionary<string, string>
|
||||
{
|
||||
["type"] = ValidationKind.Character,
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, string> PartyAdd = new Dictionary<string, string>
|
||||
{
|
||||
["id"] = ValidationKind.Character,
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, string> BattleInit = new Dictionary<string, string>
|
||||
{
|
||||
["scene"] = ValidationKind.Scene,
|
||||
["enemies"] = ValidationKind.Character,
|
||||
["quest_id"] = ValidationKind.Quest,
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, string> AddSkill = new Dictionary<string, string>
|
||||
{
|
||||
["id"] = ValidationKind.Skill,
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, IReadOnlyDictionary<string, string>> Table =
|
||||
new Dictionary<string, IReadOnlyDictionary<string, string>>(System.StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["party.add"] = PartyAdd,
|
||||
["character.send_notification"] = CharacterType,
|
||||
["character.add_skill"] = AddSkill,
|
||||
["battle.init"] = BattleInit,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns the argument→kind map for an action, or false when the action
|
||||
/// carries no known content references. Lookup is case-insensitive to
|
||||
/// match the reference check for the action name itself.
|
||||
/// </summary>
|
||||
public static bool TryGetKinds(string actionName, out IReadOnlyDictionary<string, string> kinds)
|
||||
{
|
||||
return Table.TryGetValue(actionName, out kinds);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,6 +92,9 @@ namespace Cthangover.Validator
|
||||
continue;
|
||||
|
||||
var strategy = EntityKindStrategyRegistry.Resolve(kind);
|
||||
// A kind some mod describes in a schema is known without a strategy of its own.
|
||||
if (strategy == null && ValidationSchemas.Of(context).Kinds.ContainsKey(kind))
|
||||
continue;
|
||||
if (strategy == null)
|
||||
{
|
||||
context.AddWarning("UNKNOWN_ENTITY_KIND",
|
||||
|
||||
@@ -25,15 +25,16 @@ namespace Cthangover.Validator
|
||||
/// <item>references collected by <see cref="ICommandReferenceMetadata"/> are added to the
|
||||
/// shared graph (backgrounds, scenes, music, sounds, effects, avatars) and checked by
|
||||
/// <see cref="ReferenceStrategy"/>; scenario actions are checked against the action registry.</item>
|
||||
/// <item>content references inside action arguments (<see cref="ActionArgumentReferences"/>:
|
||||
/// character ids, skills, quest ids, battle enemy lists) are added to the same graph, so a
|
||||
/// typo is reported against the declaring mods with file and line.</item>
|
||||
/// <item>content references inside action arguments (character ids, skills, quest ids,
|
||||
/// battle enemy lists) come from the actions' schemas (<see cref="SchemaValidationStrategy"/>)
|
||||
/// and go into the same graph, so a typo is reported against the declaring mods with file
|
||||
/// and line.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public class ScenarioCommandValidationStrategy : IValidationStrategy
|
||||
{
|
||||
private static readonly string[] SelectAllowedNamed = { "key", "first", "second" };
|
||||
private static readonly string[] OptionAllowedNamed = { "key", "goto" };
|
||||
private static readonly string[] OptionAllowedNamed = { "key", "goto", "if" };
|
||||
|
||||
private static readonly object actionLock = new();
|
||||
private static HashSet<string> actionNames;
|
||||
@@ -144,6 +145,8 @@ namespace Cthangover.Validator
|
||||
|
||||
CheckEmptyNamed(context, mod, file, lineNumber, command, named);
|
||||
CheckAllowedNamed(context, mod, file, lineNumber, command, named, OptionAllowedNamed);
|
||||
if (named.TryGetValue("if", out var optionCondition))
|
||||
CheckCondition(context, mod, file, lineNumber, command, optionCondition);
|
||||
|
||||
var target = arrowTarget;
|
||||
if (string.IsNullOrEmpty(target) && named.TryGetValue("goto", out var goTo))
|
||||
@@ -167,6 +170,9 @@ namespace Cthangover.Validator
|
||||
continue;
|
||||
}
|
||||
|
||||
if (command == "when")
|
||||
CheckCondition(context, mod, file, lineNumber, command, string.Join(" ", positional));
|
||||
|
||||
var spec = strategy as IScenarioCommandSpec;
|
||||
if (spec != null)
|
||||
CheckSpec(context, mod, file, lineNumber, command, positional, named, arrowTarget, spec, labels);
|
||||
@@ -190,13 +196,34 @@ namespace Cthangover.Validator
|
||||
AddReference(context, mod, file, lineNumber, referenceMetadata.Positional0Kind, positional[0]);
|
||||
}
|
||||
|
||||
if (command == "action" && positional.Count > 0)
|
||||
AddActionArgumentReferences(context, mod, file, lineNumber, positional[0], named);
|
||||
}
|
||||
|
||||
FinalizeSelect(context, mod, file, ref pendingSelectLine, ref pendingSelectHasOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a condition of an <c>option if=</c> or a <c>when</c> line with the game's own
|
||||
/// condition parser (every leaf parser the loaded mods registered), so a broken
|
||||
/// condition is an error here and not a silently hidden option at play time.
|
||||
/// </summary>
|
||||
private static void CheckCondition(ValidationContext context, IModInfo mod, string file, int line, string command, string condition)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(condition))
|
||||
{
|
||||
context.AddError("SCENARIO_EMPTY_ARGUMENT", $"'{command}' needs a condition", mod.Id, file, line);
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
ConditionParser.Parse(condition);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
context.AddError("SCENARIO_BAD_CONDITION", $"'{command}': {ex.Message.Split('\n')[0]}", mod.Id, file, line,
|
||||
"quote a condition with spaces whole (\"if=a && b\") and check names and parentheses");
|
||||
}
|
||||
}
|
||||
|
||||
private static HashSet<string> CollectLabels(ValidationContext context, IModInfo mod, string file, string[] lines, int lineOffset)
|
||||
{
|
||||
var labels = new HashSet<string>(StringComparer.Ordinal);
|
||||
@@ -391,52 +418,6 @@ namespace Cthangover.Validator
|
||||
context.Graph.AddReference(ValidationKind.Avatar, value, mod.Id, file, line);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collects content references hidden in the named arguments of an
|
||||
/// <c>action</c> command. The scenario DSL treats action arguments as
|
||||
/// opaque dialog variables, so without this pass a typo like
|
||||
/// <c>action party.add id=Murakamiy</c> would only surface
|
||||
/// at runtime as a factory error log. Values from
|
||||
/// <see cref="ActionArgumentReferences"/> become graph references and are
|
||||
/// reconciled by <see cref="ReferenceStrategy"/>, which reports the
|
||||
/// offending scenario file and line. Comma-separated values (e.g.
|
||||
/// battle.init enemies=) produce one reference per entry; values with the
|
||||
/// ${name} marker are computed at play time and cannot be resolved
|
||||
/// statically, so they are skipped instead of misreported.
|
||||
/// </summary>
|
||||
private static void AddActionArgumentReferences(
|
||||
ValidationContext context,
|
||||
IModInfo mod,
|
||||
string file,
|
||||
int line,
|
||||
string actionName,
|
||||
Dictionary<string, string> named)
|
||||
{
|
||||
if (!ActionArgumentReferences.TryGetKinds(actionName, out var kinds))
|
||||
return;
|
||||
|
||||
foreach (var pair in kinds)
|
||||
{
|
||||
if (!named.TryGetValue(pair.Key, out var value) || string.IsNullOrWhiteSpace(value))
|
||||
continue;
|
||||
|
||||
if (value.Contains("$" + "{"))
|
||||
continue;
|
||||
|
||||
foreach (var part in value.Split(','))
|
||||
{
|
||||
var target = part.Trim();
|
||||
if (target.Length == 0)
|
||||
continue;
|
||||
|
||||
if (pair.Value == ValidationKind.Scene && SceneManager.IsNativeSceneName(target))
|
||||
continue;
|
||||
|
||||
context.Graph.AddReference(pair.Value, target, mod.Id, file, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddReference(ValidationContext context, IModInfo mod, string file, int line, PositionalReferenceKind kind, string target)
|
||||
{
|
||||
switch (kind)
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
namespace Cthangover.Validator
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Mods.Dialog.Scenarios;
|
||||
using Cthangover.Mods.Schemas;
|
||||
|
||||
/// <summary>
|
||||
/// The validator's reading of the schemas every mod ships (<c>schemas/*.json</c>, merged
|
||||
/// by <see cref="SchemaCatalog"/>). It replaced the table of action arguments the
|
||||
/// validator kept in code — four actions, one of them with a wrong parameter name —
|
||||
/// with descriptions that live next to the actions and that any mod may add:
|
||||
/// <list type="bullet">
|
||||
/// <item><c>action</c> lines: unknown and missing parameters, bad numbers and choices, and
|
||||
/// graph references for every entity a parameter names (characters, quests, skills,
|
||||
/// scenes, items…), which <see cref="ReferenceStrategy"/> reports when undeclared;</item>
|
||||
/// <item>other described commands: positional values and their references (a check's stat);</item>
|
||||
/// <item>entities of described kinds: references in fields, in list elements (a trait's
|
||||
/// <c>Stat</c>) and in map keys (a character's <c>Stats</c>).</item>
|
||||
/// </list>
|
||||
/// Kinds no other strategy declares (shops, stats, archetypes, equipment slots…) are
|
||||
/// declared here from the mods' data, so their references can be checked and the
|
||||
/// resource atlas sees their owners and patches. References are added only for kinds
|
||||
/// that are declared at all; a kind nothing in the graph knows (a map, a tileset) is
|
||||
/// not judged. Like every strategy it reads the mods' files, never the running game.
|
||||
/// </summary>
|
||||
public class SchemaValidationStrategy : IValidationStrategy
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Id => "schemas";
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValidationPhase Phase => ValidationPhase.Contracts;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Order => 900;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValidationFeature Feature => ValidationFeature.Json;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Validate(ValidationContext context)
|
||||
{
|
||||
var catalog = ValidationSchemas.Of(context);
|
||||
foreach (var error in catalog.Errors)
|
||||
context.AddError("SCHEMA_PARSE_ERROR", error, error.Split(':')[0]);
|
||||
|
||||
// Every data file is parsed once: for the declarations and for the field checks.
|
||||
var entities = context.Mods.Where(m => m.FileProvider != null).ToDictionary(m => m, m => Entities(m).ToList());
|
||||
var ownKinds = DeclareDescribedKinds(context, catalog, entities);
|
||||
var declared = new HashSet<string>(context.Graph.Declarations.Select(d => d.Kind), StringComparer.OrdinalIgnoreCase) { ValidationKind.Scene };
|
||||
|
||||
foreach (var (mod, list) in entities)
|
||||
{
|
||||
context.ThrowIfCancellationRequested();
|
||||
foreach (var file in ModFileReader.ListFiles(mod.FileProvider, "scenarios", true).Where(f => f.EndsWith(".scenario", StringComparison.OrdinalIgnoreCase)))
|
||||
CheckScenario(context, catalog, mod, file, declared, ownKinds);
|
||||
foreach (var (file, kind, entity) in list)
|
||||
{
|
||||
if (catalog.Kinds.TryGetValue(kind, out var schema))
|
||||
CheckEntity(context, mod, file, schema, entity, ownKinds.Contains(kind) ? declared : ownKinds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static HashSet<string> DeclareDescribedKinds(ValidationContext context, SchemaCatalog catalog, Dictionary<IModInfo, List<(string File, string Kind, JsonElement Entity)>> entities)
|
||||
{
|
||||
var known = new HashSet<string>(context.Graph.Declarations.Select(d => d.Kind), StringComparer.OrdinalIgnoreCase);
|
||||
var own = new HashSet<string>(catalog.Kinds.Keys.Where(k => !known.Contains(k)), StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var (mod, list) in entities)
|
||||
{
|
||||
foreach (var (file, kind, entity) in list)
|
||||
{
|
||||
if (own.Contains(kind) && entity.TryGetProperty("Id", out var id))
|
||||
context.Graph.AddDeclaration(kind, id.ToString(), mod.Id, file, 0);
|
||||
}
|
||||
}
|
||||
return own;
|
||||
}
|
||||
|
||||
private static void CheckScenario(ValidationContext context, SchemaCatalog catalog, IModInfo mod, string file, HashSet<string> declared, HashSet<string> ownKinds)
|
||||
{
|
||||
string text;
|
||||
try
|
||||
{
|
||||
text = ModFileReader.ReadResolved(mod.FileProvider, file);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (text == null)
|
||||
return;
|
||||
var body = ScenarioParser.ParseMetadata(text).remainingText.Split('\n');
|
||||
var offset = text.Split('\n').Length - body.Length;
|
||||
for (var i = 0; i < body.Length; i++)
|
||||
{
|
||||
var line = body[i].Trim();
|
||||
if (line.Length == 0 || line.StartsWith('#') || line.StartsWith(':'))
|
||||
continue;
|
||||
var positional = new List<string>();
|
||||
var named = new Dictionary<string, string>();
|
||||
var command = ScenarioParser.SplitLine(line, positional, named, out _);
|
||||
var lineNumber = offset + i + 1;
|
||||
if (command == "action" && positional.Count > 0 && catalog.Actions.TryGetValue(positional[0], out var action))
|
||||
{
|
||||
Report(context, SchemaChecker.CheckNamed(action.Id, action.Params, named, null), mod.Id, file, lineNumber);
|
||||
foreach (var field in action.Params ?? new List<FieldSchema>())
|
||||
{
|
||||
var value = named.FirstOrDefault(p => string.Equals(p.Key, field.Key, StringComparison.OrdinalIgnoreCase)).Value;
|
||||
AddReferences(context, field, value, declared, mod.Id, file, lineNumber);
|
||||
}
|
||||
}
|
||||
else if (command != null && command != "action" && catalog.Commands.TryGetValue(command, out var schema))
|
||||
{
|
||||
Report(context, SchemaChecker.CheckPositional(command, schema.Positional, positional, null), mod.Id, file, lineNumber);
|
||||
// The commands' own strategies already reference their scenes and resources;
|
||||
// only kinds the schemas brought in are referenced here.
|
||||
var fields = schema.Positional ?? new List<FieldSchema>();
|
||||
for (var p = 0; p < fields.Count && p < positional.Count; p++)
|
||||
AddReferences(context, fields[p], positional[p], ownKinds, mod.Id, file, lineNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckEntity(ValidationContext context, IModInfo mod, string file, KindSchema schema, JsonElement entity, HashSet<string> targets)
|
||||
{
|
||||
var id = entity.TryGetProperty("Id", out var idProp) ? idProp.ToString() : "?";
|
||||
var owner = $"{schema.Kind} '{id}'";
|
||||
foreach (var field in schema.Fields)
|
||||
{
|
||||
if (!TryGet(entity, field.Key, out var value))
|
||||
continue;
|
||||
switch (field.Type?.ToLowerInvariant())
|
||||
{
|
||||
case "list" when value.ValueKind == JsonValueKind.Array && field.Of != null:
|
||||
foreach (var element in value.EnumerateArray().Where(e => e.ValueKind == JsonValueKind.Object))
|
||||
{
|
||||
foreach (var sub in field.Of)
|
||||
{
|
||||
if (!TryGet(element, sub.Key, out var subValue))
|
||||
continue;
|
||||
Report(context, SchemaChecker.CheckValue($"{owner}.{field.Key}", sub, Text(subValue), null), mod.Id, file, 0);
|
||||
AddReferences(context, sub, Text(subValue), targets, mod.Id, file, 0);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "map" when value.ValueKind == JsonValueKind.Object && targets.Contains(field.Kind ?? string.Empty):
|
||||
foreach (var key in value.EnumerateObject().Select(p => p.Name))
|
||||
context.Graph.AddReference(field.Kind, key, mod.Id, file, 0);
|
||||
break;
|
||||
case "ref_list" when value.ValueKind == JsonValueKind.Array:
|
||||
AddReferences(context, field, string.Join(",", value.EnumerateArray().Select(Text)), targets, mod.Id, file, 0);
|
||||
break;
|
||||
default:
|
||||
if (value.ValueKind is JsonValueKind.String or JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False)
|
||||
{
|
||||
Report(context, SchemaChecker.CheckValue(owner, field, Text(value), null), mod.Id, file, 0);
|
||||
AddReferences(context, field, Text(value), targets, mod.Id, file, 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddReferences(ValidationContext context, FieldSchema field, string value, HashSet<string> kinds, string mod, string file, int line)
|
||||
{
|
||||
foreach (var (kind, id) in SchemaReferences.Of(field, value))
|
||||
{
|
||||
if (!kinds.Contains(kind))
|
||||
continue;
|
||||
if (kind == ValidationKind.Scene && SceneManager.IsNativeSceneName(id))
|
||||
continue;
|
||||
context.Graph.AddReference(kind, id, mod, file, line);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<(string File, string Kind, JsonElement Entity)> Entities(IModInfo mod)
|
||||
{
|
||||
foreach (var file in ModFileReader.ListFiles(mod.FileProvider, string.Empty, true).Where(IsDataFile))
|
||||
{
|
||||
JsonDocument doc;
|
||||
try
|
||||
{
|
||||
doc = JsonDocument.Parse(ModFileReader.ReadResolved(mod.FileProvider, file) ?? string.Empty);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
using (doc)
|
||||
{
|
||||
var root = doc.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object || !root.TryGetProperty("Kind", out var kind) || kind.ValueKind != JsonValueKind.String
|
||||
|| !root.TryGetProperty("Items", out var items) || items.ValueKind != JsonValueKind.Array)
|
||||
continue;
|
||||
foreach (var entity in items.EnumerateArray().Where(e => e.ValueKind == JsonValueKind.Object).ToList())
|
||||
yield return (file, kind.GetString(), entity.Clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsDataFile(string path)
|
||||
=> path.EndsWith(".json", StringComparison.OrdinalIgnoreCase)
|
||||
&& !path.StartsWith("schemas/", StringComparison.OrdinalIgnoreCase)
|
||||
&& !path.StartsWith("patches/", StringComparison.OrdinalIgnoreCase)
|
||||
&& !path.EndsWith("manifest.json", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool TryGet(JsonElement obj, string key, out JsonElement value)
|
||||
{
|
||||
foreach (var prop in obj.EnumerateObject())
|
||||
{
|
||||
if (string.Equals(prop.Name, key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = prop.Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string Text(JsonElement value) => value.ValueKind == JsonValueKind.String ? value.GetString() : value.ToString();
|
||||
|
||||
private static void Report(ValidationContext context, IEnumerable<SchemaIssue> issues, string mod, string file, int line)
|
||||
{
|
||||
foreach (var issue in issues)
|
||||
{
|
||||
if (issue.IsError)
|
||||
context.AddError(issue.Code, issue.Message, mod, file, line);
|
||||
else
|
||||
context.AddWarning(issue.Code, issue.Message, mod, file, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
namespace Cthangover.Validator
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Cthangover.Mods.Schemas;
|
||||
|
||||
/// <summary>
|
||||
/// The schemas of the validated mods (their <c>schemas/*.json</c>, read from the providers
|
||||
/// in the context's mod order), built once per validation run and shared by the
|
||||
/// strategies that need them: the entity strategy treats a described kind as known, the
|
||||
/// schema strategy checks content against it.
|
||||
/// </summary>
|
||||
public static class ValidationSchemas
|
||||
{
|
||||
private static readonly ConditionalWeakTable<ValidationContext, SchemaCatalog> Catalogs = new();
|
||||
|
||||
/// <summary>The merged catalog of <paramref name="context"/>'s mods.</summary>
|
||||
public static SchemaCatalog Of(ValidationContext context)
|
||||
=> Catalogs.GetValue(context, Build);
|
||||
|
||||
private static SchemaCatalog Build(ValidationContext context) => SchemaCatalog.Build(Files(context));
|
||||
|
||||
private static IEnumerable<(string, string, string)> Files(ValidationContext context)
|
||||
{
|
||||
foreach (var mod in context.Mods.Where(m => m.FileProvider != null))
|
||||
{
|
||||
foreach (var file in ModFileReader.ListFiles(mod.FileProvider, "schemas", true).Where(f => f.EndsWith(".json", StringComparison.OrdinalIgnoreCase)).OrderBy(f => f, StringComparer.Ordinal))
|
||||
yield return (mod.Id, file, ModFileReader.ReadResolved(mod.FileProvider, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user