Compare commits
4
Commits
v0.1.0
...
0648a9efd0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0648a9efd0 | ||
|
|
42aef4919e | ||
|
|
1ae0b79d1b | ||
|
|
df702b0390 |
No files matched your search
@@ -12,6 +12,7 @@ obj/
|
||||
gen/
|
||||
obj/
|
||||
bin/
|
||||
art_sources/
|
||||
|
||||
*.tres~
|
||||
*.tscn~
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
# Validator — checking mods without playing them
|
||||
|
||||
The Validator reads every mod of the running game — manifests, data files, scenarios, scene patches and C# sources — and reports what is broken or suspicious: a reference to an item, scene or background nobody declares, an unknown scenario command, a wrong parameter, a missing dependency. It runs in the background each time a game starts and writes a report, so a typo is found before a player reaches the line that contains it.
|
||||
|
||||
It checks what can be seen in the files. It does not play the game: a scenario the Validator accepts can still behave differently from what the author meant, and code that builds ids at run time cannot be followed.
|
||||
|
||||
## A mistake and its correction
|
||||
|
||||
A scenario line that gives the player wolf meat, with a typo in the item id:
|
||||
|
||||
```scenario
|
||||
scene: home_kitchen
|
||||
priority: 0
|
||||
---
|
||||
action item.add id=food/wolf_meet count=1
|
||||
end
|
||||
```
|
||||
|
||||
After the next start of Cthangover the report holds:
|
||||
|
||||
```json
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "REF_NOT_FOUND",
|
||||
"message": "item 'food/wolf_meet' is referenced but never declared",
|
||||
"mod": "cthangover_game",
|
||||
"file": "scenarios/doc_validator_demo.scenario",
|
||||
"line": 4,
|
||||
"suggestion": "declare item 'food/wolf_meet' or fix the reference",
|
||||
"related": null
|
||||
}
|
||||
```
|
||||
|
||||
The mod, the file and the line point at the mistake. With `food/wolf_meat` the issue is gone and the error count in the summary goes down by one (from 18 to 17 in this run).
|
||||
|
||||
## Running it and reading the report
|
||||
|
||||
- It starts by itself when a game that includes it is entered (Cthangover includes it with its developer tools), after the mods are loaded, and runs on a background thread; the game is not blocked.
|
||||
- The log gets one line per pass: `validator: started, 33 mod(s) queued`, then the totals of errors, warnings and infos.
|
||||
- The report is written to `user://validation/report.json` (in the game's user data folder). It holds a summary, the checked mods with their versions and checksums, and the list of issues.
|
||||
|
||||
Each issue has:
|
||||
|
||||
- `severity` — `error` (something is broken), `warning` (probably a mistake) or `info` (worth a look, for example an item nothing refers to);
|
||||
- `code` — the kind of problem, for example `REF_NOT_FOUND`, `JSON_PARSE_ERROR`, `SCHEMA_MISSING_FIELD`, `SCHEMA_UNKNOWN_PARAM`, `DECL_UNUSED`, `DYNAMIC_KEY`;
|
||||
- `mod`, `file`, `line` — where to fix it; `suggestion` — what to do.
|
||||
|
||||
`DYNAMIC_KEY` infos mean a key is built at run time, so the Validator cannot check it; they are not errors.
|
||||
|
||||
## What is checked
|
||||
|
||||
The checks run in three phases:
|
||||
|
||||
1. **Schema** — `manifest.json` has a string `id` and `version`; JSON files parse.
|
||||
2. **Contracts** — what each mod declares and refers to: entities of every kind (characters, items, quests, skills, recipes, actions and the kinds described by schemas), scenario commands and actions with their parameters, scene patches, properties read and written by C# code.
|
||||
3. **Reconciliation** — every reference has a declaration, `depends` covers what a mod uses from others, declarations nothing uses.
|
||||
|
||||
Scenario actions and their parameters are checked against the schemas mods ship (see Schemas), with the same rules as the command palette.
|
||||
|
||||
## Settings
|
||||
|
||||
In the mod's settings tab of the mod manager:
|
||||
|
||||
- **Enabled**, **Run on start** — turn the pass off;
|
||||
- **Parse C#**, **Parse GDScript** (off by default, slow), **Parse scenarios** — which sources are read;
|
||||
- **Write report**, **Report path** — where `report.json` goes;
|
||||
- **Block mods on error** — present in the settings, but in the current version it does not change anything: mods are never blocked.
|
||||
|
||||
## If something goes wrong
|
||||
|
||||
1. **There is no report.** The game was closed before the pass ended (it takes about half a minute for Cthangover), or Write report is off.
|
||||
2. **Many `DYNAMIC_KEY` lines.** Normal for code that builds keys; they are infos.
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
# Validator — revisar los mods sin jugar
|
||||
|
||||
El Validator lee cada mod del juego en marcha — manifiestos, archivos de datos, escenarios, parches de escenas y fuentes C# — e informa de lo que está roto o es sospechoso: una referencia a un objeto, escena o fondo que nadie declara, un comando de escenario desconocido, un parámetro incorrecto, una dependencia ausente. Se ejecuta en segundo plano cada vez que arranca un juego y escribe un informe, así que una errata se descubre antes de que un jugador llegue a la línea que la contiene.
|
||||
|
||||
Comprueba lo que se ve en los archivos. No juega la partida: un escenario que el Validator acepta todavía puede comportarse distinto de lo que el autor quería, y el código que construye id al vuelo no se puede seguir.
|
||||
|
||||
## Un error y su corrección
|
||||
|
||||
Una línea de escenario que da al jugador carne de lobo, con una errata en el id del objeto:
|
||||
|
||||
```scenario
|
||||
scene: home_kitchen
|
||||
priority: 0
|
||||
---
|
||||
action item.add id=food/wolf_meet count=1
|
||||
end
|
||||
```
|
||||
|
||||
Tras el siguiente arranque de Cthangover, el informe contiene:
|
||||
|
||||
```json
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "REF_NOT_FOUND",
|
||||
"message": "item 'food/wolf_meet' is referenced but never declared",
|
||||
"mod": "cthangover_game",
|
||||
"file": "scenarios/doc_validator_demo.scenario",
|
||||
"line": 4,
|
||||
"suggestion": "declare item 'food/wolf_meet' or fix the reference",
|
||||
"related": null
|
||||
}
|
||||
```
|
||||
|
||||
El mod, el archivo y la línea señalan el error. Con `food/wolf_meat` el problema desaparece y el contador de errores del resumen baja en uno (de 18 a 17 en esta pasada).
|
||||
|
||||
## Cómo se ejecuta y cómo leer el informe
|
||||
|
||||
- Arranca solo al entrar en un juego que lo incluye (Cthangover lo incluye con sus herramientas de desarrollo), cuando los mods ya están cargados, y corre en un hilo de fondo; no bloquea el juego.
|
||||
- El registro recibe una línea por pasada: `validator: started, 33 mod(s) queued`, y después los totales de errores, avisos e informaciones.
|
||||
- El informe se escribe en `user://validation/report.json` (en la carpeta de datos de usuario del juego). Contiene un resumen, los mods comprobados con sus versiones y sumas de comprobación, y la lista de incidencias.
|
||||
|
||||
Cada incidencia tiene:
|
||||
|
||||
- `severity` — `error` (algo está roto), `warning` (probablemente un fallo) o `info` (merece una mirada, por ejemplo un objeto al que nada remite);
|
||||
- `code` — el tipo de problema, por ejemplo `REF_NOT_FOUND`, `JSON_PARSE_ERROR`, `SCHEMA_MISSING_FIELD`, `SCHEMA_UNKNOWN_PARAM`, `DECL_UNUSED`, `DYNAMIC_KEY`;
|
||||
- `mod`, `file`, `line` — dónde corregirlo; `suggestion` — qué hacer.
|
||||
|
||||
Las informaciones `DYNAMIC_KEY` significan que la clave se construye al ejecutarse, de modo que el Validator no puede comprobarla; no son errores.
|
||||
|
||||
## Qué se comprueba
|
||||
|
||||
Las comprobaciones corren en tres fases:
|
||||
|
||||
1. **Esquema** — `manifest.json` tiene un `id` y una `version` de texto; los archivos JSON se analizan.
|
||||
2. **Contratos** — lo que cada mod declara y remite: entidades de todos los tipos (personajes, objetos, misiones, habilidades, recetas, acciones y los tipos descritos por esquemas), comandos y acciones de escenario con sus parámetros, parches de escenas, propiedades que el código C# lee y escribe.
|
||||
3. **Reconciliación** — cada referencia tiene una declaración, `depends` cubre lo que el mod toma de otros, y se listan las declaraciones que nada usa.
|
||||
|
||||
Las acciones de escenario y sus parámetros se comprueban contra los esquemas que los mods incluyen (ver Schemas), con las mismas reglas que la paleta de comandos.
|
||||
|
||||
## Ajustes
|
||||
|
||||
En la pestaña de ajustes del mod, dentro del gestor de mods:
|
||||
|
||||
- **Enabled**, **Run on start** — apagan la pasada;
|
||||
- **Parse C#**, **Parse GDScript** (desactivado por defecto, lento), **Parse scenarios** — qué fuentes se leen;
|
||||
- **Write report**, **Report path** — dónde va `report.json`;
|
||||
- **Block mods on error** — está en los ajustes, pero en la versión actual no cambia nada: nunca se bloquean mods.
|
||||
|
||||
## Si algo sale mal
|
||||
|
||||
1. **No hay informe.** El juego se cerró antes de acabar la pasada (Cthangover tarda cerca de medio minuto) o **Write report** está desactivado.
|
||||
2. **Muchas líneas `DYNAMIC_KEY`.** Normal en código que construye claves; son informaciones.
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
# Validator — プレイせずに MOD を検査する
|
||||
|
||||
Validator は、実行中のゲームのすべての MOD——マニフェスト、データファイル、シナリオ、シーンパッチ、C# ソース——を読み、壊れているか怪しいものを報告します。誰も宣言していないアイテム・シーン・背景への参照、不明なシナリオコマンド、誤ったパラメーター、不足した依存関係。ゲームが始まるたびにバックグラウンドで走り、レポートを書くので、タイプミスはプレイヤーがその行に到達する前に見つかります。
|
||||
|
||||
検査するのはファイルに見えるものだけです。Validator はゲームをプレイしません: 受け入れられたシナリオでも、作者の意図どおりに動くとは限らず、実行時に id を組み立てるコードは追えません。
|
||||
|
||||
## 誤りとその修正
|
||||
|
||||
プレイヤーに狼の肉を与えるシナリオの行。アイテム id にタイプミスがあります:
|
||||
|
||||
```scenario
|
||||
scene: home_kitchen
|
||||
priority: 0
|
||||
---
|
||||
action item.add id=food/wolf_meet count=1
|
||||
end
|
||||
```
|
||||
|
||||
次に Cthangover を起動すると、レポートにこれが入ります:
|
||||
|
||||
```json
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "REF_NOT_FOUND",
|
||||
"message": "item 'food/wolf_meet' is referenced but never declared",
|
||||
"mod": "cthangover_game",
|
||||
"file": "scenarios/doc_validator_demo.scenario",
|
||||
"line": 4,
|
||||
"suggestion": "declare item 'food/wolf_meet' or fix the reference",
|
||||
"related": null
|
||||
}
|
||||
```
|
||||
|
||||
MOD、ファイル、行が誤りを指し示します。`food/wolf_meat` に直せば問題は消え、概要のエラー数がひとつ減ります(この実行では 18 から 17 へ)。
|
||||
|
||||
## 実行のしかたとレポートの読み方
|
||||
|
||||
- それを含むゲームに入ると(Cthangover は開発者ツールと一緒に同梱します)、MOD の読み込みのあとで自動的に始まり、バックグラウンドスレッドで走ります。ゲームは止まりません。
|
||||
- ログには一巡ごとに一行入ります: `validator: started, 33 mod(s) queued`、そのあとにエラー・警告・情報の合計。
|
||||
- レポートは `user://validation/report.json`(ゲームのユーザーデータフォルダー)に書かれます。概要、バージョンとチェックサム付きの検査済み MOD、そして問題の一覧が入ります。
|
||||
|
||||
問題ごとに:
|
||||
|
||||
- `severity` — `error`(壊れている)、`warning`(おそらく誤り)、`info`(見ておく価値がある。たとえばどこからも参照されないアイテム);
|
||||
- `code` — 問題の種類。たとえば `REF_NOT_FOUND`、`JSON_PARSE_ERROR`、`SCHEMA_MISSING_FIELD`、`SCHEMA_UNKNOWN_PARAM`、`DECL_UNUSED`、`DYNAMIC_KEY`;
|
||||
- `mod`、`file`、`line` — どこを直すか; `suggestion` — 何をすればいいか。
|
||||
|
||||
`DYNAMIC_KEY` の情報は、キーが実行時に組み立てられるため Validator が検査できないことを意味します。エラーではありません。
|
||||
|
||||
## 何を検査するか
|
||||
|
||||
検査は三つのフェーズで走ります:
|
||||
|
||||
1. **スキーマ** — `manifest.json` に文字列の `id` と `version` がある; JSON ファイルが解釈できる。
|
||||
2. **契約** — 各 MOD が何を宣言し、何を参照するか: あらゆる種類のエンティティ(キャラクター、アイテム、クエスト、スキル、レシピ、行動、そしてスキーマが記述した種類)、シナリオのコマンドと行動とそのパラメーター、シーンパッチ、C# コードが読み書きするプロパティ。
|
||||
3. **突き合わせ** — すべての参照に宣言がある、`depends` が他の MOD から使うものを覆っている、誰にも使われない宣言の一覧。
|
||||
|
||||
シナリオの行動とパラメーターは、MOD が同梱するスキーマに対して(Schemas を参照)、コマンドパレットと同じ規則で検査されます。
|
||||
|
||||
## 設定
|
||||
|
||||
Modマネージャーのこの MOD の設定タブで:
|
||||
|
||||
- **Enabled**、**Run on start** — 一巡を止める;
|
||||
- **Parse C#**、**Parse GDScript**(既定ではオフ、遅い)、**Parse scenarios** — どのソースを読むか;
|
||||
- **Write report**、**Report path** — `report.json` の行き先;
|
||||
- **Block mods on error** — 設定にはありますが、現行バージョンでは何も変わりません: MOD が止められることはありません。
|
||||
|
||||
## うまくいかないとき
|
||||
|
||||
1. **レポートがない。** 一巡が終わる前にゲームが閉じられた(Cthangover では 30 秒ほど)か、**Write report** がオフです。
|
||||
2. **`DYNAMIC_KEY` の行が多い。** キーを組み立てるコードでは普通のことです。情報扱いです。
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
# Validator — 게임을 하지 않고 모드 검사하기
|
||||
|
||||
Validator는 실행 중인 게임의 모든 모드 — 매니페스트, 데이터 파일, 시나리오, 장면 패치, C# 소스 — 를 읽고, 깨졌거나 수상한 것을 보고합니다. 아무도 선언하지 않는 아이템·장면·배경 참조, 알 수 없는 시나리오 명령, 잘못된 매개변수, 빠진 의존성. 게임이 시작될 때마다 백그라운드로 돌고 보고서를 쓰므로, 오타는 플레이어가 그 줄에 닿기 전에 발견됩니다.
|
||||
|
||||
검사하는 것은 파일에 보이는 것뿐입니다. Validator는 게임을 플레이하지 않습니다: 통과시킨 시나리오라도 작성자의 의도와 다르게 동작할 수 있고, 실행 중에 id를 만들어 내는 코드는 쫓을 수 없습니다.
|
||||
|
||||
## 실수와 그 바로잡기
|
||||
|
||||
플레이어에게 늑대 고기를 주는 시나리오 줄. 아이템 id에 오타가 있습니다:
|
||||
|
||||
```scenario
|
||||
scene: home_kitchen
|
||||
priority: 0
|
||||
---
|
||||
action item.add id=food/wolf_meet count=1
|
||||
end
|
||||
```
|
||||
|
||||
다음에 Cthangover를 시작하면 보고서에 이것이 담깁니다:
|
||||
|
||||
```json
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "REF_NOT_FOUND",
|
||||
"message": "item 'food/wolf_meet' is referenced but never declared",
|
||||
"mod": "cthangover_game",
|
||||
"file": "scenarios/doc_validator_demo.scenario",
|
||||
"line": 4,
|
||||
"suggestion": "declare item 'food/wolf_meet' or fix the reference",
|
||||
"related": null
|
||||
}
|
||||
```
|
||||
|
||||
모드, 파일, 줄이 실수를 가리킵니다. `food/wolf_meat`로 고치면 문제가 사라지고, 요약의 오류 개수가 하나 줄어듭니다(이번 실행에서는 18에서 17로).
|
||||
|
||||
## 실행 방법과 보고서 읽기
|
||||
|
||||
- 그것을 포함하는 게임에 들어가면(Cthangover는 개발자 도구와 함께 넣어 둡니다) 모드가 불러와진 뒤 저절로 시작되고, 백그라운드 스레드에서 돕니다. 게임을 막지 않습니다.
|
||||
- 로그에는 한 번 돌 때마다 한 줄이 남습니다: `validator: started, 33 mod(s) queued`, 그다음에 오류·경고·정보의 총계.
|
||||
- 보고서는 `user://validation/report.json`(게임 사용자 데이터 폴더)에 기록됩니다. 요약, 버전과 체크섬이 담긴 검사된 모드들, 문제 목록이 들어 있습니다.
|
||||
|
||||
문제마다 다음이 있습니다:
|
||||
|
||||
- `severity` — `error`(무언가 깨짐), `warning`(아마 실수), `info`(한 번 볼 만함. 예컨대 아무도 참조하지 않는 아이템);
|
||||
- `code` — 문제의 종류. 예: `REF_NOT_FOUND`, `JSON_PARSE_ERROR`, `SCHEMA_MISSING_FIELD`, `SCHEMA_UNKNOWN_PARAM`, `DECL_UNUSED`, `DYNAMIC_KEY`;
|
||||
- `mod`, `file`, `line` — 어디를 고칠지; `suggestion` — 무엇을 할지.
|
||||
|
||||
`DYNAMIC_KEY` 정보는 키가 실행 중에 만들어져 Validator가 검사할 수 없다는 뜻입니다. 오류가 아닙니다.
|
||||
|
||||
## 무엇을 검사하는가
|
||||
|
||||
검사는 세 단계로 돕니다:
|
||||
|
||||
1. **스키마** — `manifest.json`에 문자열 `id`와 `version`이 있음; JSON 파일이 해석됨.
|
||||
2. **계약** — 각 모드가 무엇을 선언하고 무엇을 참조하는지: 모든 종류의 엔티티(캐릭터, 아이템, 퀘스트, 스킬, 제작법, 행동, 그리고 스키마가 설명한 종류), 시나리오 명령과 행동과 그 매개변수, 장면 패치, C# 코드가 읽고 쓰는 속성.
|
||||
3. **대조** — 모든 참조에 선언이 있는지, `depends`가 모드가 남에게서 쓰는 것을 커버하는지, 아무도 쓰지 않는 선언 목록.
|
||||
|
||||
시나리오 행동과 매개변수는 모드가 담는 스키마와 비교해(Schemas 참고) 명령 팔레트와 같은 규칙으로 검사됩니다.
|
||||
|
||||
## 설정
|
||||
|
||||
모드 관리자에서 이 모드의 설정 탭에서:
|
||||
|
||||
- **Enabled**, **Run on start** — 실행을 끕니다;
|
||||
- **Parse C#**, **Parse GDScript**(기본값 꺼짐, 느림), **Parse scenarios** — 어떤 소스를 읽을지;
|
||||
- **Write report**, **Report path** — `report.json`이 가는 곳;
|
||||
- **Block mods on error** — 설정에는 있지만 현재 버전에서는 아무것도 바꾸지 않습니다: 모드는 막히지 않습니다.
|
||||
|
||||
## 문제가 생겼을 때
|
||||
|
||||
1. **보고서가 없습니다.** 실행이 끝나기 전에 게임이 닫혔거나(Cthangover는 30초쯤 걸립니다) **Write report**가 꺼져 있습니다.
|
||||
2. **`DYNAMIC_KEY` 줄이 많습니다.** 키를 만들어 내는 코드에서는 정상입니다. 정보입니다.
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
# Validator — проверка модов без запуска игры
|
||||
|
||||
Validator читает каждый мод запущенной игры — манифесты, файлы данных, сценарии, патчи сцен и исходники C# — и сообщает, что сломано или подозрительно: ссылку на предмет, сцену или фон, которые никто не объявляет, неизвестную команду сценария, неверный параметр, недостающую зависимость. Проверка запускается в фоне при каждом старте игры и пишет отчёт, так что опечатка находится раньше, чем игрок доберётся до строки с ней.
|
||||
|
||||
Проверяется то, что видно в файлах. Validator не играет в игру: сценарий, который он принял, всё равно может вести себя не так, как задумывал автор, а код, собирающий идентификаторы на ходу, проследить невозможно.
|
||||
|
||||
## Ошибка и её исправление
|
||||
|
||||
Строка сценария, выдающая игроку волчье мясо, с опечаткой в id предмета:
|
||||
|
||||
```scenario
|
||||
scene: home_kitchen
|
||||
priority: 0
|
||||
---
|
||||
action item.add id=food/wolf_meet count=1
|
||||
end
|
||||
```
|
||||
|
||||
После следующего запуска Cthangover в отчёте появляется:
|
||||
|
||||
```json
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "REF_NOT_FOUND",
|
||||
"message": "item 'food/wolf_meet' is referenced but never declared",
|
||||
"mod": "cthangover_game",
|
||||
"file": "scenarios/doc_validator_demo.scenario",
|
||||
"line": 4,
|
||||
"suggestion": "declare item 'food/wolf_meet' or fix the reference",
|
||||
"related": null
|
||||
}
|
||||
```
|
||||
|
||||
Мод, файл и строка указывают на ошибку. С `food/wolf_meat` проблема исчезает, а счётчик ошибок в сводке уменьшается на единицу (с 18 до 17 в этом прогоне).
|
||||
|
||||
## Как запустить и как читать отчёт
|
||||
|
||||
- Проверка стартует сама, когда входишь в игру, которая его содержит (Cthangover включает его в свой набор инструментов разработчика), после загрузки модов и идёт в фоновом потоке; игре она не мешает.
|
||||
- В лог на каждый прогон пишется строка: `validator: started, 33 mod(s) queued`, затем итоги по ошибкам, предупреждениям и заметкам.
|
||||
- Отчёт пишется в `user://validation/report.json` (в папке пользовательских данных игры). В нём сводка, проверенные моды с их версиями и контрольными суммами и список проблем.
|
||||
|
||||
У каждой проблемы есть:
|
||||
|
||||
- `severity` — `error` (что-то сломано), `warning` (похоже на ошибку) или `info` (стоит взглянуть, например предмет, на который никто не ссылается);
|
||||
- `code` — род проблемы, например `REF_NOT_FOUND`, `JSON_PARSE_ERROR`, `SCHEMA_MISSING_FIELD`, `SCHEMA_UNKNOWN_PARAM`, `DECL_UNUSED`, `DYNAMIC_KEY`;
|
||||
- `mod`, `file`, `line` — где исправлять; `suggestion` — что делать.
|
||||
|
||||
Заметки `DYNAMIC_KEY` означают, что ключ собирается во время выполнения, и Validator не может его проверить; ошибками они не являются.
|
||||
|
||||
## Что проверяется
|
||||
|
||||
Проверки идут в три фазы:
|
||||
|
||||
1. **Схема** — в `manifest.json` есть строки `id` и `version`; JSON-файлы разбираются.
|
||||
2. **Контракты** — что мод объявляет и на что ссылается: сущности всех видов (персонажи, предметы, задания, навыки, рецепты, действия и виды, описанные схемами), команды и действия сценария с их параметрами, патчи сцен, свойства, которые читает и пишет код C#.
|
||||
3. **Сверка** — у каждой ссылки есть объявление, `depends` покрывает то, что мод берёт у других, а объявления, которыми никто не пользуется, перечислены.
|
||||
|
||||
Сценарные действия и их параметры сверяются со схемами, которые поставляют моды (см. Schemas), по тем же правилам, что и в палитре команд.
|
||||
|
||||
## Настройки
|
||||
|
||||
На вкладке настроек мода в менеджере модов:
|
||||
|
||||
- **Enabled**, **Run on start** — отключают прогон;
|
||||
- **Parse C#**, **Parse GDScript** (по умолчанию выключено, медленно), **Parse scenarios** — какие источники читать;
|
||||
- **Write report**, **Report path** — куда пишется `report.json`;
|
||||
- **Block mods on error** — есть в настройках, но в текущей версии ни на что не влияет: моды не блокируются никогда.
|
||||
|
||||
## Если что-то пошло не так
|
||||
|
||||
1. **Отчёта нет.** Игра закрылась до конца прогона (для Cthangover это около полуминуты) или выключен **Write report**.
|
||||
2. **Много строк `DYNAMIC_KEY`.** Нормально для кода, который собирает ключи; это заметки.
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
# Validator — kiểm tra mod mà không cần chơi
|
||||
|
||||
Validator đọc từng mod của game đang chạy — manifest, tệp dữ liệu, kịch bản, bản vá cảnh và mã nguồn C# — rồi báo những gì hỏng hoặc đáng ngờ: tham chiếu tới một vật phẩm, cảnh hay phông nền mà không ai khai báo, lệnh kịch bản lạ, tham số sai, phụ thuộc còn thiếu. Nó chạy nền mỗi khi game khởi động và ghi báo cáo, nên lỗi gõ sẽ được phát hiện trước khi người chơi tới dòng chứa nó.
|
||||
|
||||
Nó kiểm tra những gì nhìn thấy được trong tệp. Nó không chơi game: một kịch bản mà Validator chấp nhận vẫn có thể chạy khác ý tác giả, còn mã tự ghép id lúc chạy thì không theo được.
|
||||
|
||||
## Một lỗi và cách sửa
|
||||
|
||||
Một dòng kịch bản cho người chơi thịt sói, với lỗi gõ trong id vật phẩm:
|
||||
|
||||
```scenario
|
||||
scene: home_kitchen
|
||||
priority: 0
|
||||
---
|
||||
action item.add id=food/wolf_meet count=1
|
||||
end
|
||||
```
|
||||
|
||||
Sau lần khởi động Cthangover tiếp theo, báo cáo có:
|
||||
|
||||
```json
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "REF_NOT_FOUND",
|
||||
"message": "item 'food/wolf_meet' is referenced but never declared",
|
||||
"mod": "cthangover_game",
|
||||
"file": "scenarios/doc_validator_demo.scenario",
|
||||
"line": 4,
|
||||
"suggestion": "declare item 'food/wolf_meet' or fix the reference",
|
||||
"related": null
|
||||
}
|
||||
```
|
||||
|
||||
Mod, tệp và dòng chỉ tay vào lỗi. Sửa thành `food/wolf_meat` thì vấn đề biến mất, và số lỗi trong phần tổng giảm đi một (từ 18 xuống 17 trong lần chạy này).
|
||||
|
||||
## Cách chạy và cách đọc báo cáo
|
||||
|
||||
- Tự khởi động khi vào một game có chứa nó (Cthangover kèm nó cùng bộ công cụ dành cho nhà phát triển), sau khi các mod được tải, và chạy trên một luồng nền; không chặn game.
|
||||
- Mỗi lượt chạy thêm một dòng vào nhật ký: `validator: started, 33 mod(s) queued`, rồi tới tổng số lỗi, cảnh báo và thông tin.
|
||||
- Báo cáo được ghi vào `user://validation/report.json` (trong thư mục dữ liệu người dùng của game). Trong đó có phần tổng, các mod đã kiểm tra cùng phiên bản và tổng kiểm tra, và danh sách vấn đề.
|
||||
|
||||
Mỗi vấn đề có:
|
||||
|
||||
- `severity` — `error` (có thứ hỏng), `warning` (có lẽ là sai sót) hoặc `info` (đáng xem qua, ví dụ một vật phẩm chẳng ai nhắc tới);
|
||||
- `code` — loại vấn đề, ví dụ `REF_NOT_FOUND`, `JSON_PARSE_ERROR`, `SCHEMA_MISSING_FIELD`, `SCHEMA_UNKNOWN_PARAM`, `DECL_UNUSED`, `DYNAMIC_KEY`;
|
||||
- `mod`, `file`, `line` — sửa ở đâu; `suggestion` — làm gì.
|
||||
|
||||
Những thông tin `DYNAMIC_KEY` nghĩa là khóa được ghép lúc chạy nên Validator không kiểm tra được; chúng không phải lỗi.
|
||||
|
||||
## Kiểm tra những gì
|
||||
|
||||
Các phép kiểm tra chạy theo ba pha:
|
||||
|
||||
1. **Lược đồ** — `manifest.json` có `id` và `version` dạng chuỗi; các tệp JSON đọc được.
|
||||
2. **Hợp đồng** — mỗi mod khai báo gì và nhắc tới gì: thực thể của mọi loại (nhân vật, vật phẩm, nhiệm vụ, kỹ năng, công thức, hành động và các loại do lược đồ mô tả), lệnh và hành động kịch bản cùng tham số, bản vá cảnh, các thuộc tính mà mã C# đọc và ghi.
|
||||
3. **Đối chiếu** — mọi tham chiếu đều có khai báo, `depends` phủ những gì mod dùng của người khác, và các khai báo không ai dùng được liệt kê ra.
|
||||
|
||||
Hành động kịch bản và tham số của chúng được đối chiếu với lược đồ mà các mod kèm theo (xem Schemas), theo đúng luật của bảng lệnh.
|
||||
|
||||
## Cài đặt
|
||||
|
||||
Trong thẻ cài đặt của mod này trong trình quản lý mod:
|
||||
|
||||
- **Enabled**, **Run on start** — tắt lượt chạy;
|
||||
- **Parse C#**, **Parse GDScript** (mặc định tắt, chậm), **Parse scenarios** — đọc những nguồn nào;
|
||||
- **Write report**, **Report path** — `report.json` ghi vào đâu;
|
||||
- **Block mods on error** — có trong cài đặt nhưng ở phiên bản hiện tại nó không đổi gì cả: không bao giờ chặn mod.
|
||||
|
||||
## Khi có trục trặc
|
||||
|
||||
1. **Không có báo cáo.** Game bị đóng trước khi lượt chạy kết thúc (với Cthangover mất chừng nửa phút), hoặc **Write report** đang tắt.
|
||||
2. **Nhiều dòng `DYNAMIC_KEY`.** Bình thường với mã tự ghép khóa; chúng chỉ là thông tin.
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
# Validator — 不玩游戏也能检查模组
|
||||
|
||||
Validator 会读取运行中游戏的每一个模组——清单、数据文件、剧本、场景补丁和 C# 源码——并报告损坏或可疑之处:指向无人声明的物品、场景或背景的引用、未知的剧本命令、错误的参数、缺失的依赖。每次启动游戏时它都会在后台运行并写出报告,错字会在玩家走到那一行之前就被发现。
|
||||
|
||||
它检查的是文件里能看到的东西。它不玩游戏:Validator 放行的剧本仍可能与作者的本意不同,而运行时才拼出 id 的代码无从追查。
|
||||
|
||||
## 一个错误及其修正
|
||||
|
||||
给玩家加狼肉的剧本行,物品 id 打了错字:
|
||||
|
||||
```scenario
|
||||
scene: home_kitchen
|
||||
priority: 0
|
||||
---
|
||||
action item.add id=food/wolf_meet count=1
|
||||
end
|
||||
```
|
||||
|
||||
下次启动 Cthangover 后,报告里会有一条:
|
||||
|
||||
```json
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "REF_NOT_FOUND",
|
||||
"message": "item 'food/wolf_meet' is referenced but never declared",
|
||||
"mod": "cthangover_game",
|
||||
"file": "scenarios/doc_validator_demo.scenario",
|
||||
"line": 4,
|
||||
"suggestion": "declare item 'food/wolf_meet' or fix the reference",
|
||||
"related": null
|
||||
}
|
||||
```
|
||||
|
||||
模组、文件和行号都指向这个错误。改回 `food/wolf_meat` 问题就消失了,汇总里的错误计数少了一(这次运行从 18 降到 17)。
|
||||
|
||||
## 如何运行、如何读报告
|
||||
|
||||
- 进入包含它的游戏时(Cthangover 把它和开发者工具一起附带)它会自动开始,在模组加载之后,于后台线程运行;不会阻塞游戏。
|
||||
- 每次运行都会往日志写一行:`validator: started, 33 mod(s) queued`,然后是错误、警告和信息三类总数。
|
||||
- 报告写在 `user://validation/report.json`(游戏用户数据文件夹里)。里面有汇总、被检查的模组及其版本与校验和,以及问题清单。
|
||||
|
||||
每个问题都有:
|
||||
|
||||
- `severity` —— `error`(有东西坏了)、`warning`(多半是个错误)或 `info`(值得看一眼,比如没有任何东西引用的物品);
|
||||
- `code` —— 问题种类,例如 `REF_NOT_FOUND`、`JSON_PARSE_ERROR`、`SCHEMA_MISSING_FIELD`、`SCHEMA_UNKNOWN_PARAM`、`DECL_UNUSED`、`DYNAMIC_KEY`;
|
||||
- `mod`、`file`、`line` —— 去哪里修;`suggestion` —— 怎么修。
|
||||
|
||||
`DYNAMIC_KEY` 类的信息表示键是运行时拼出来的,Validator 无法检查;它们不是错误。
|
||||
|
||||
## 检查什么
|
||||
|
||||
检查分三个阶段:
|
||||
|
||||
1. **模式** —— `manifest.json` 有字符串 `id` 和 `version`;JSON 文件能解析。
|
||||
2. **契约** —— 每个模组声明什么、引用什么:各种类型的实体(角色、物品、任务、技能、配方、动作,以及模式描述的类型)、剧本命令和动作及其参数、场景补丁、C# 代码读写过的属性。
|
||||
3. **对账** —— 每条引用都有声明,`depends` 覆盖模组从别人那里用到的东西,没有被动用的声明会列出。
|
||||
|
||||
剧本动作和参数会对照模组附带的各种模式来检查(见 Schemas),规则与命令面板相同。
|
||||
|
||||
## 设置
|
||||
|
||||
在模组管理器中该模组的设置标签页里:
|
||||
|
||||
- **Enabled**、**Run on start** —— 关掉检查;
|
||||
- **Parse C#**、**Parse GDScript**(默认关闭,速度慢)、**Parse scenarios** —— 读取哪些来源;
|
||||
- **Write report**、**Report path** —— `report.json` 写到哪里;
|
||||
- **Block mods on error** —— 设置里有这一项,但在当前版本它不起任何作用:模组永远不会被拦截。
|
||||
|
||||
## 出问题了怎么办
|
||||
|
||||
1. **没有报告。** 检查还没结束游戏就关了(对 Cthangover 大约要半分钟),或者 **Write report** 关着。
|
||||
2. **很多 `DYNAMIC_KEY` 条目。** 对会拼键的代码来说很正常;它们只是信息。
|
||||
@@ -1,5 +1,5 @@
|
||||
mod_validator_name = Validator
|
||||
mod_validator_desc = 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 a text report.
|
||||
mod_validator_desc = Checks every mod without running the game: broken references, unknown scenario commands, wrong parameters, missing dependencies — a report on every game start.
|
||||
|
||||
validator/enabled = Enabled
|
||||
validator/run_on_start = Run on start
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
mod_validator_name = Validador
|
||||
mod_validator_desc = Comprueba cada mod sin ejecutar el juego: referencias rotas, comandos de escenario desconocidos, parámetros incorrectos, dependencias ausentes — un informe en cada arranque del juego.
|
||||
|
||||
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 を検査します: 壊れた参照、不明なシナリオコマンド、誤ったパラメーター、不足した依存関係——ゲーム開始のたびにレポートを出します。
|
||||
|
||||
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 = 게임을 실행하지 않고 모든 모드를 검사합니다: 깨진 참조, 알 수 없는 시나리오 명령, 잘못된 매개변수, 빠진 의존성 — 게임을 시작할 때마다 보고서를 남깁니다.
|
||||
|
||||
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,5 +1,5 @@
|
||||
mod_validator_name = Валидатор
|
||||
mod_validator_desc = Статическая проверка модов без запуска игры: схема манифеста, стратегии сущностей (персонажи, предметы, квесты, умения, рецепты, действия), перекрёстные ссылки, depends, команды сценариев и патчи сцен, текстовый отчёт.
|
||||
mod_validator_desc = Проверяет каждый мод, не запуская игру: битые ссылки, неизвестные команды сценария, неверные параметры, недостающие зависимости — отчёт при каждом старте игры.
|
||||
|
||||
validator/enabled = Включён
|
||||
validator/run_on_start = Запускать при старте
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
mod_validator_name = Trình kiểm tra
|
||||
mod_validator_desc = Kiểm tra mọi mod mà không cần chạy game: tham chiếu hỏng, lệnh kịch bản lạ, tham số sai, phụ thuộc còn thiếu — báo cáo mỗi lần khởi động game.
|
||||
|
||||
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 = 出错时阻止模组
|
||||
+2
-2
@@ -3,8 +3,8 @@
|
||||
"name": "Validator",
|
||||
"version": "0.1.0",
|
||||
"author": "ct",
|
||||
"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.",
|
||||
"description": "Checks every mod without running the game: broken references, unknown scenario commands, wrong parameters, missing dependencies — a report on every game start.",
|
||||
"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",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Utils;
|
||||
|
||||
namespace Cthangover.Validator
|
||||
@@ -10,6 +12,8 @@ namespace Cthangover.Validator
|
||||
/// to the filesystem entry name for a missing id and tolerates a missing
|
||||
/// version, so these are exactly the problems an author should fix before
|
||||
/// publishing.
|
||||
/// A mounted test suite (<c><host>__<suite></c>) is exempt from the id and version rule: the mount
|
||||
/// gives it the virtual id and the host's version, so its own manifest leaves them out by design.
|
||||
/// </summary>
|
||||
public class ManifestSchemaStrategy : IValidationStrategy
|
||||
{
|
||||
@@ -45,8 +49,9 @@ namespace Cthangover.Validator
|
||||
|
||||
using (document)
|
||||
{
|
||||
ValidateStringField(context, mod.Id, document.RootElement, "id", required: true);
|
||||
ValidateStringField(context, mod.Id, document.RootElement, "version", required: true);
|
||||
var suite = ModTestSuites.ActiveIds.Contains(mod.Id);
|
||||
ValidateStringField(context, mod.Id, document.RootElement, "id", required: !suite);
|
||||
ValidateStringField(context, mod.Id, document.RootElement, "version", required: !suite);
|
||||
ValidateStringField(context, mod.Id, document.RootElement, "name", required: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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