Compare commits
2
Commits
v0.1.0
...
1ae0b79d1b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ae0b79d1b | ||
|
|
df702b0390 |
No files matched your search
@@ -0,0 +1,11 @@
|
||||
mod_validator_name = Validador
|
||||
mod_validator_desc = Validación estática de mods sin ejecutar el juego: esquema del manifiesto, estrategias por entidad (personajes, objetos, misiones, habilidades, recetas, acciones), referencias cruzadas, dependencias, comandos de escenario y parches de escena, con un informe de texto.
|
||||
|
||||
validator/enabled = Activado
|
||||
validator/run_on_start = Ejecutar al iniciar
|
||||
validator/parse_csharp = Analizar C#
|
||||
validator/parse_gdscript = Analizar GDScript
|
||||
validator/parse_scenarios = Analizar escenarios
|
||||
validator/write_report = Escribir informe
|
||||
validator/report_path = Ruta del informe
|
||||
validator/fail_on_error = Bloquear mods si hay errores
|
||||
@@ -0,0 +1,11 @@
|
||||
mod_validator_name = バリデーター
|
||||
mod_validator_desc = ゲームを動かさずに行う MOD の静的検証:マニフェストのスキーマ、エンティティごとの戦略(キャラクター、アイテム、クエスト、スキル、レシピ、アクション)、相互参照、depends、シナリオコマンド、シーンパッチ。テキストのレポート付き。
|
||||
|
||||
validator/enabled = 有効
|
||||
validator/run_on_start = 起動時に実行
|
||||
validator/parse_csharp = C# を解析
|
||||
validator/parse_gdscript = GDScript を解析
|
||||
validator/parse_scenarios = シナリオを解析
|
||||
validator/write_report = レポートを書き出す
|
||||
validator/report_path = レポートのパス
|
||||
validator/fail_on_error = エラー時に MOD をブロック
|
||||
@@ -0,0 +1,11 @@
|
||||
mod_validator_name = 검증기
|
||||
mod_validator_desc = 게임을 실행하지 않는 모드 정적 검증: 매니페스트 스키마, 엔티티별 전략(캐릭터, 아이템, 퀘스트, 스킬, 레시피, 액션), 교차 참조, depends, 시나리오 명령과 장면 패치, 텍스트 보고서 포함.
|
||||
|
||||
validator/enabled = 사용
|
||||
validator/run_on_start = 시작 시 실행
|
||||
validator/parse_csharp = C# 분석
|
||||
validator/parse_gdscript = GDScript 분석
|
||||
validator/parse_scenarios = 시나리오 분석
|
||||
validator/write_report = 보고서 쓰기
|
||||
validator/report_path = 보고서 경로
|
||||
validator/fail_on_error = 오류 시 모드 차단
|
||||
@@ -0,0 +1,11 @@
|
||||
mod_validator_name = Trình kiểm tra
|
||||
mod_validator_desc = Kiểm tra tĩnh các mod mà không cần chạy game: lược đồ manifest, chiến lược theo thực thể (nhân vật, vật phẩm, nhiệm vụ, kỹ năng, công thức, hành động), tham chiếu chéo, depends, lệnh kịch bản và bản vá cảnh, kèm báo cáo văn bản.
|
||||
|
||||
validator/enabled = Bật
|
||||
validator/run_on_start = Chạy khi khởi động
|
||||
validator/parse_csharp = Phân tích C#
|
||||
validator/parse_gdscript = Phân tích GDScript
|
||||
validator/parse_scenarios = Phân tích kịch bản
|
||||
validator/write_report = Ghi báo cáo
|
||||
validator/report_path = Đường dẫn báo cáo
|
||||
validator/fail_on_error = Chặn mod khi có lỗi
|
||||
@@ -0,0 +1,11 @@
|
||||
mod_validator_name = 验证器
|
||||
mod_validator_desc = 无需运行游戏的模组静态验证:清单结构、实体策略(角色、物品、任务、技能、配方、动作)、交叉引用、依赖、剧本命令和场景补丁,并生成文本报告。
|
||||
|
||||
validator/enabled = 启用
|
||||
validator/run_on_start = 启动时运行
|
||||
validator/parse_csharp = 解析 C#
|
||||
validator/parse_gdscript = 解析 GDScript
|
||||
validator/parse_scenarios = 解析剧本
|
||||
validator/write_report = 写入报告
|
||||
validator/report_path = 报告路径
|
||||
validator/fail_on_error = 出错时阻止模组
|
||||
+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