diff --git a/README.md b/README.md index 41f9526..60a5ca5 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ English | [日本語](README_JA.md) -Generates a server-specific Skript syntax snapshot for LSP and tooling use. The snapshot records the active Skript version, server, plugins, registration order, capabilities, and 19 data files behind a stable schema. +Generates a server-specific Skript syntax snapshot for LSP and tooling use. The snapshot records the active Skript version, server, plugins, registration order, capabilities, and 20 data files behind a stable schema. For a field-by-field description of every generated file, including nullability, value ranges, concepts, and version differences, see the [snapshot JSON format reference](docs/json-format.md). ## Generator artifacts -Two adapters write the same 20-file snapshot contract: +Two adapters write the same 21-file snapshot contract: 20 data files plus `Manifest.json`. | Skript | Artifact | Runtime | | --- | --- | --- | @@ -17,19 +17,21 @@ Two adapters write the same 20-file snapshot contract: Place the matching artifact in the server's `plugins` directory, start the server, and run `/skgen`. Files are written to `plugins/SkriptSyntaxGenerator` by default. A server snapshot should be generated again whenever the server, Skript, installed addons, or addon load order changes. -Both adapters always emit the same files. Features unavailable in an older Skript version use the contract's empty root (`[]`, `{}` for `Operations.json`, or the documented object roots for `Aliases.json`, `Language.json`, and `PluralRules.json`) and are described by `Manifest.json.capabilities`. +Both adapters always emit the same files. Features unavailable in an older Skript version use the contract's empty root (`[]`, `{}` for `Operations.json`, or the documented object roots for `Aliases.json`, `BlockData.json`, `Language.json`, and `PluralRules.json`) and are described by the relevant file state or `Manifest.json.capabilities`. ## Manifest capabilities -`Manifest.json` uses schema version 6 and records: +`Manifest.json` uses schema version 7 and records: - `syntaxApi`: `legacy-static` or `registry` - `eventValueApi`: `legacy`, `modern-2.15`, or `modern-2.16` - `syntaxKinds`: availability of each collected registry - `aliases.supported` and `aliases.collected` -Schema 6 replaces the former `Types.json.defaultExpressionClass` scalar with -structured `defaultExpression` metadata. It records the implementation class +`BlockData.json` has its own runtime state because BlockData availability depends on the Minecraft/Bukkit API, not only on the Skript version. Consumers should use that file's `state` and `complete` fields instead of inferring support from `syntaxKinds`. + +Schema 6 introduced, and schema 7 retains, the replacement of the former +`Types.json.defaultExpressionClass` scalar with structured `defaultExpression` metadata. It records the implementation class and whether the implementation is a literal, plus `returnType` and `single` when those methods can be queried without parse context. Time-state and initialization checks remain parser/WASM responsibility. @@ -68,22 +70,24 @@ initialization remains parser/WASM work. Supporting registries and relationships: -| Skript | Arithmetic | Converters | Comparators | Event values | Properties | Class hierarchy | Global aliases | Language registry | Plural rules | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| 2.6.4 | No | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | -| 2.7.3 | No | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | -| 2.8.7 | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | -| 2.9.5 | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | -| 2.10.2 | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | -| 2.11.2 | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | -| 2.12.2 | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | -| 2.13.2 | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| 2.14.3 | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| 2.15.4 | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| 2.16.0 | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| Skript | Arithmetic | Converters | Comparators | Event values | Properties | Class hierarchy | Global aliases | Language registry | Plural rules | Block data | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 2.6.4 | No | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | Runtime | +| 2.7.3 | No | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | Runtime | +| 2.8.7 | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | Runtime | +| 2.9.5 | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | Runtime | +| 2.10.2 | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | Runtime | +| 2.11.2 | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | Runtime | +| 2.12.2 | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | Runtime | +| 2.13.2 | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Runtime | +| 2.14.3 | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Runtime | +| 2.15.4 | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Runtime | +| 2.16.0 | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Runtime | `Arithmetic` covers `Operators.json`, `Operations.json`, and `Differences.json` as one capability. `Plural rules` covers `PluralRules.json`; every supported Skript version has a built-in conversion table, while `pluralOverrideSupported` records whether addons can prepend runtime overrides. Event values are available for every tested version, but their metadata shape changes: 2.6.4-2.14.3 use `eventValueApi: legacy`, while 2.15.4 and 2.16.0 expose the `modern-2.16` shape. The exact detected shape must be read from the Manifest instead of inferred only from the Skript version. +`Block data` is runtime-dependent: Minecraft 1.13 and newer can report `collected` when the Bukkit/BlockState registry is readable; Minecraft 1.12.2 and older report `unsupported`; a registry/API inspection failure reports `unresolved`. The table's `Runtime` value intentionally avoids pretending that this is a Skript-only capability. + Skript 2.6.4 has no enumerable Structure registry. Its command, function, options, variables, aliases, and event top-level constructs are handled by dedicated `ScriptLoader` branches. They are not synthesized into `Structures.json`, because this generator preserves registered raw data instead of reconstructing syntax declarations. ## Compatibility matrix @@ -103,6 +107,14 @@ The integration suite currently covers these boundaries: | 2.15.4 | 26.1.2 | 25 | experimental current API | | 2.16.0 | 26.2 | 25 | experimental current API | +BlockData compatibility is determined by the Minecraft runtime: + +| Minecraft runtime | `BlockData.json.state` | Meaning | +| --- | --- | --- | +| 1.13+ | `collected` | Read from the runtime Bukkit/BlockState registry without a hardcoded block list. | +| 1.12.2 and older | `unsupported` | The `org.bukkit.block.data.BlockData` API is unavailable. | +| Any version with an inspection failure | `unresolved` | The API exists or was expected, but the registry could not be read safely. | + The `2.6.4 + Minecraft 1.12.2 + Java 8` profile is a required compatibility test, not an inferred target. Minecraft 1.16.5, 1.17.1, 1.18.2, 26.1.2, and 26.2 are representative patch releases selected from [Skript's bStats Minecraft Version chart](https://bstats.org/plugin/bukkit/Skript/722). The chart changes over time; these profiles cover the currently significant version families without replacing the Skript API boundary profiles. Paper 26 currently prevents complete registration-hook capture through plugin class-loader isolation, so its profiles remain available as explicit experimental tasks but are excluded from `integrationTest`. The interactive `runServer` task uses Skript 2.15.4 on Paper 1.21.11. diff --git a/README_JA.md b/README_JA.md index 9c1eead..a890cff 100644 --- a/README_JA.md +++ b/README_JA.md @@ -2,13 +2,13 @@ [English](README.md) | 日本語 -LSPや各種ツールで利用するために、サーバー固有のSkript構文スナップショットを生成します。スナップショットには、安定したスキーマのもとで、使用中のSkriptバージョン、サーバー、プラグイン、登録順序、capability、および19個のデータファイルが記録されます。 +LSPや各種ツールで利用するために、サーバー固有のSkript構文スナップショットを生成します。スナップショットには、安定したスキーマのもとで、使用中のSkriptバージョン、サーバー、プラグイン、登録順序、capability、および20個のdata fileが記録されます。 生成される全ファイルについて、各フィールド、null・省略、値域、概念、バージョン差を確認するには、[スナップショットJSON形式リファレンス](docs/json-format.ja.md)を参照してください。 ## Generatorの成果物 -2種類のadapterが、同じ20ファイルのスナップショット契約に従って出力します。 +2種類のadapterが、20個のdata fileと`Manifest.json`を含む同じ21ファイルのスナップショット契約に従って出力します。 | Skript | 成果物 | 実行環境 | | --- | --- | --- | @@ -17,11 +17,11 @@ LSPや各種ツールで利用するために、サーバー固有のSkript構 対応する成果物をサーバーの`plugins`ディレクトリに配置し、サーバーを起動して`/skgen`を実行してください。デフォルトでは、ファイルは`plugins/SkriptSyntaxGenerator`に出力されます。サーバー、Skript、導入addon、またはaddonの読み込み順序が変わった場合は、サーバースナップショットを再生成してください。 -どちらのadapterも常に同じファイルを出力します。古いSkriptバージョンで利用できない機能は、契約で定めた空のroot(`[]`、`Operations.json`では`{}`、`Aliases.json`、`Language.json`、`PluralRules.json`では文書化されたobject root)として出力され、利用可否は`Manifest.json.capabilities`に記録されます。 +どちらのadapterも常に同じファイルを出力します。古いSkriptバージョンで利用できない機能は、契約で定めた空のroot(`[]`、`Operations.json`では`{}`、`Aliases.json`、`BlockData.json`、`Language.json`、`PluralRules.json`では文書化されたobject root)として出力され、利用可否は各ファイルのstateまたは`Manifest.json.capabilities`に記録されます。 ## Manifest capabilities -`Manifest.json`はschema version 6を使用し、次の情報を記録します。 +`Manifest.json`はschema version 7を使用し、次の情報を記録します。 - `syntaxApi`: `legacy-static`または`registry` - `eventValueApi`: `legacy`、`modern-2.15`、または`modern-2.16` @@ -66,24 +66,32 @@ schema 6では実装classとliteral性を保持し、context依存の初期化 補助registryと関係データ: -| Skript | Arithmetic | Converters | Comparators | Event values | Properties | Class hierarchy | Global aliases | Language registry | Plural rules | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| 2.6.4 | No | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | -| 2.7.3 | No | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | -| 2.8.7 | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | -| 2.9.5 | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | -| 2.10.2 | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | -| 2.11.2 | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | -| 2.12.2 | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | -| 2.13.2 | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| 2.14.3 | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| 2.15.4 | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| 2.16.0 | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| Skript | Arithmetic | Converters | Comparators | Event values | Properties | Class hierarchy | Global aliases | Language registry | Plural rules | Block data | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 2.6.4 | No | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | Runtime | +| 2.7.3 | No | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | Runtime | +| 2.8.7 | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | Runtime | +| 2.9.5 | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | Runtime | +| 2.10.2 | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | Runtime | +| 2.11.2 | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | Runtime | +| 2.12.2 | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | Runtime | +| 2.13.2 | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Runtime | +| 2.14.3 | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Runtime | +| 2.15.4 | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Runtime | +| 2.16.0 | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Runtime | `Arithmetic`は、`Operators.json`、`Operations.json`、`Differences.json`を1つのcapabilityとして表します。 `Plural rules`は`PluralRules.json`を表し、対応する全Skript versionにbuilt-in変換tableがあります。addonがruntime overrideを先頭追加できるかは`pluralOverrideSupported`で判定します。Event valueはテスト済みの全バージョンで取得できますが、metadataの形状が異なります。2.6.4-2.14.3では`eventValueApi: legacy`、2.15.4と2.16.0では`modern-2.16`です。Skriptバージョンだけから推測せず、Manifestから実際に検出された形状を参照してください。 Skript 2.6.4には列挙可能なStructure registryがありません。command、function、options、variables、aliases、eventのトップレベル構造は、`ScriptLoader`内の専用分岐で処理されます。このGeneratorは構文宣言を再構築するのではなく、登録された生データを保存するため、これらを`Structures.json`へ擬似的に追加しません。 +BlockDataはSkriptのversionだけでは決まりません。`BlockData.json.state`は、Minecraft 1.13以降では`collected`、1.12.2以前では`unsupported`、registryの検査に失敗した場合は`unresolved`です。 + +| Minecraft runtime | `BlockData.json.state` | 意味 | +| --- | --- | --- | +| 1.13以降 | `collected` | Bukkit/BlockState registryからハードコードなしで取得。 | +| 1.12.2以前 | `unsupported` | `org.bukkit.block.data.BlockData` APIが存在しない。 | +| 検査失敗 | `unresolved` | APIは期待されるが、registryを安全に読めない。 | + ## 互換性マトリクス integration suiteでは、現在次の境界をテストしています。 diff --git a/build.gradle.kts b/build.gradle.kts index 12eaa40..761b008 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -137,6 +137,14 @@ data class IntegrationProfile( "Comparators.json", "EventValues.json" ) + val minecraftParts = minecraft.split('.').mapNotNull(String::toIntOrNull) + if ( + minecraftParts.firstOrNull()?.let { major -> + major > 1 || (major == 1 && (minecraftParts.getOrNull(1) ?: 0) >= 13) + } == true + ) { + files += "BlockData.json" + } val minor = skript.split(".").getOrNull(1)?.toIntOrNull() ?: return files if (minor >= 7) files += "Structures.json" if (minor >= 8) { diff --git a/docs/json-format.ja.md b/docs/json-format.ja.md index f921870..a908753 100644 --- a/docs/json-format.ja.md +++ b/docs/json-format.ja.md @@ -2,7 +2,7 @@ [English](json-format.md) | 日本語 -この文書は、`/skgen` が出力するschema version `6`の20ファイルと、各ファイルが表すSkriptの概念を説明します。SkriptのJava APIを知らなくても生成物を利用できることを目的としています。 +この文書は、`/skgen`が出力するschema version `7`の21ファイル(20個のdata fileと`Manifest.json`)と、各ファイルが表すSkriptの概念を説明します。SkriptのJava APIを知らなくても生成物を利用できることを目的としています。schema 6の説明は、version境界を理解するための履歴として残しています。 ## 形式の読み方 @@ -48,6 +48,7 @@ generatorはJacksonの`NON_NULL`設定でDTOを直列化します。後述する | `Sections.json` | array | 実行コード内でインデントされたblockを持つ構文。 | | `Structures.json` | array | top-level宣言やconfig nodeとして解析される構文。 | | `Types.json` | array | Skript上の値型と対応するJava class。 | +| `BlockData.json` | object | Minecraftのblock ID、canonicalなdefault state、runtime property値。 | | `Functions.json` | array | 登録済みfunctionとsignature。 | | `Converters.json` | array | ある値型から別の値型への自動変換規則。 | | `Comparators.json` | array | 2つの値型を比較する規則。 | @@ -61,7 +62,11 @@ generatorはJacksonの`NON_NULL`設定でDTOを直列化します。後述する | `Language.json` | object | Skriptのglobal language registryにロードされた実効key/value。 | | `PluralRules.json` | object | runtime優先順に並んだ英語の単数形・複数形変換rule。 | -全ファイルが常に出力されます。空rootは、配列ファイルが`[]`、`Operations.json`と`Language.json`が`{}`、`Aliases.json`が`{"aliases":{},"targets":[]}`、`PluralRules.json`が`{"algorithm":"unresolved","pluralOverrideSupported":false,"rules":[]}`です。 +全21ファイルが常に出力されます。schema 7の空rootは、配列ファイルが`[]`、`Operations.json`と`Language.json`が`{}`、`Aliases.json`が`{"aliases":{},"targets":[]}`、`BlockData.json`が`{"state":"unsupported","complete":false,"blocks":{},"failures":[]}`、`PluralRules.json`が`{"algorithm":"unresolved","pluralOverrideSupported":false,"rules":[]}`です。 + +## schema 7の空root + +全21ファイルは常に出力されます。array rootの空値は`[]`、`Operations.json`と`Language.json`は`{}`、`Aliases.json`は`{"aliases":{},"targets":[]}`、`PluralRules.json`は`{"algorithm":"unresolved","pluralOverrideSupported":false,"rules":[]}`です。`BlockData.json`の空rootは`{"state":"unsupported","complete":false,"blocks":{},"failures":[]}`です。必須fieldの空array・空objectは「空であること」が意味を持つため、省略と同一視しないでください。 ## 共通object @@ -233,6 +238,61 @@ addon独自の`EntryData` subclassは`kind: "unknown"`になり得ます。gener ## Registryファイル +### `BlockData.json` + +`BlockData.json`はMinecraftのblockをruntimeから取得した結果です。1回の収集処理の状態を表すため、rootはarrayではなくobjectです。block一覧やproperty一覧をgenerator側にハードコードせず、Minecraft 1.13以降のBukkit/BlockState registryを列挙して、各blockについてBukkitのcanonical stateを取得します。 + +| Field | Type | Presence | 説明 | +| --- | --- | --- | --- | +| `state` | string enum | 必須 | `collected`、`unsupported`、`unresolved`のいずれか。BlockDataの利用可能状態を表す正規のfieldです。 | +| `complete` | boolean | 必須 | 失敗なく収集できた場合だけ`true`です。unsupported、unresolved、partial collectionでは`false`です。 | +| `registryProvider` | string | `collected`では必須、それ以外は任意 | runtime registry readerの識別子です。現在は`bukkit-runtime-registry`です。registryを読めない場合は省略できます。snapshot内の他の`provider` fieldは`AddonInfo` objectであり、このfieldとは意味が異なります。 | +| `blocks` | `object` | 必須 | namespaced block IDをkeyにしたmapです。keyはsort済みです。データがない場合は`{}`です。 | +| `failures` | `array` | 必須 | 収集中に発生した失敗です。記録がなければ`[]`です。 | + +`BlockDataBlock`のfieldはすべて必須です。 + +| Field | Type | Presence | 説明 | +| --- | --- | --- | --- | +| `defaultState` | string | 必須 | runtimeが返したblockのdefault stateのcanonical文字列表現です。例は`minecraft:chest[facing=north]`です。許可値一覧そのものではありません。 | +| `properties` | `object>` | 必須 | property名から、そのpropertyが取り得る文字列表現へのmapです。property keyはsort済み、各arrayはsort済みかつ重複なしで、空文字を含みません。propertyがないblockでは`{}`です。 | + +`BlockDataFailure`は`message`(必須string)と`block`(任意string)を持ちます。`block`がある場合はそのnamespaced block IDに関する失敗で、省略時は収集処理全体に関する失敗です。`state: "collected"`かつ`complete: false`は有効なpartial resultなので、成功した`blocks`を利用しながら`failures`も表示してください。 + +| Field | Type | Presence | 説明 | +| --- | --- | --- | --- | +| `block` | string | 任意 | 収集に失敗したnamespaced block IDです。全体の失敗では省略されます。 | +| `message` | string | 必須 | 失敗理由を表す人間向けのmessageです。 | + +```json +{ + "state": "collected", + "complete": true, + "registryProvider": "bukkit-runtime-registry", + "blocks": { + "minecraft:chest": { + "defaultState": "minecraft:chest[facing=north]", + "properties": { + "facing": ["east", "north", "south", "west"] + } + } + }, + "failures": [] +} +``` + +Minecraft 1.13以降では、実行中のBukkit/BlockState registryからハードコードなしで収集します。1.12.2以前は`org.bukkit.block.data.BlockData` APIがないため、`state: "unsupported"`、`complete: false`、`blocks: {}`、`failures: []`になります。APIが存在するはずなのにregistryの検査やstate作成に失敗した場合は`unresolved`になり、理由が`failures`に入ります。これはSkript 2.16の[`BlockUtils.java`](https://github.com/SkriptLang/Skript/blob/2.16.0/src/main/java/ch/njol/skript/util/BlockUtils.java)と[`Bukkit.createBlockData(String)`](https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/Bukkit.html#createBlockData(java.lang.String))の経路と同じ目的で、server自身にversionごとのblock stateの妥当性と語彙を判断させるための設計です。 + +ここでいう`properties`はMinecraft BlockDataのstate propertyです。`Properties.json`が表すSkriptのproperty system(値からlocationを取得するなどの再利用可能な能力)とは別の概念です。 + +| Minecraft runtime | `BlockData.json.state` | 意味 | +| --- | --- | --- | +| 1.13以降 | `collected` | Bukkit/BlockState registryからblockとpropertyをハードコードなしで収集します。 | +| 1.12.2以前 | `unsupported` | `org.bukkit.block.data.BlockData` APIが存在しません。 | +| registry検査の失敗 | `unresolved` | APIは期待されますが、安全にregistryを読めません。 | + +`collected`でも個別blockの失敗があれば`complete: false`になります。成功したblockは`blocks`に残り、失敗は`failures`に記録されます。 + ### `Types.json` typeはnumber、player、locationなどのSkript型を、Javaでの解析・変換・保存・変更処理へ結びつけます。 @@ -573,15 +633,17 @@ Skript source: [2.6.4の`Language.java`](https://github.com/SkriptLang/Skript/bl | フィールド | 型 | 有無 | 意味 | | --- | --- | --- | --- | -| `schemaVersion` | int | 必須 | この文書ではexact `6`。未知のmajor schemaは拒否または別処理する。 | +| `schemaVersion` | int | 必須 | schema 7ではexact `7`。未知のmajor schemaは拒否または別処理する。 | | `snapshotId` | sha256 | 必須 | schema、content、server、language、plugin list、capability、file list由来のidentity。 | -| `contentDigest` | sha256 | 必須 | Manifestを除く19 data fileのserialized content digest。 | +| `contentDigest` | sha256 | 必須 | `Manifest.json`を除く20個のdata fileのserialized content digestです。`BlockData.json`も含みます。 | | `generatedAt` | ISO-8601 string | 必須 | UTC `Instant`。`snapshotId`には含まれない。 | | `server` | `ServerManifestData` | 必須 | 実行server identity。 | | `language` | string | 必須 | active Skript language。legacyで取得不能なら`unknown`。type nounにも影響する。 | | `plugins` | `array` | 必須 | Bukkit load orderのplugin一覧。 | -| `capabilities` | `SnapshotCapabilitiesData` | 必須 | API shapeと対応registry。 | -| `files` | `array` | 必須 | `Manifest.json`を含む20ファイル名のsort済み一覧。 | +| `capabilities` | `SnapshotCapabilitiesData` | 必須 | API shapeと対応registry。BlockDataの状態は`BlockData.json`自身を参照する。 | +| `files` | `array` | 必須 | `Manifest.json`を含む21ファイル名(20 data fileとManifest)のsort済み一覧。 | + +schema 7では、`files`の件数は常に21、`contentDigest`の対象は常に20です。Manifest自身はdigestの対象外ですが、`BlockData.json`は対象に含まれます。 `ServerManifestData`は必須stringの`name`、`version`、`bukkitVersion`、`minecraftVersion`、`javaVersion`を持ちます。 diff --git a/docs/json-format.md b/docs/json-format.md index feb03bf..4ed8312 100644 --- a/docs/json-format.md +++ b/docs/json-format.md @@ -2,7 +2,7 @@ English | [Japanese](json-format.ja.md) -This document describes schema version `6`, the 20 files emitted by `/skgen`, and the Skript concepts represented by those files. It is written for consumers that do not already know Skript's Java API. +This document describes the current schema version `7`, the 21 files emitted by `/skgen` (20 data files plus `Manifest.json`), and the Skript concepts represented by those files. It is written for consumers that do not already know Skript's Java API. The older schema 6 details below are retained where they explain a historical boundary. ## Reading the format @@ -48,6 +48,7 @@ Consumers should read `Manifest.json` first. Do not infer support only from a Sk | `Sections.json` | array | Syntax inside executable code that owns an indented block. | | `Structures.json` | array | Top-level declarations and other configuration-node syntax. | | `Types.json` | array | Skript-visible value types and their Java representations. | +| `BlockData.json` | object | Minecraft block IDs, their canonical default states, and runtime property values. | | `Functions.json` | array | Registered callable functions and their signatures. | | `Converters.json` | array | Allowed automatic conversions from one value type to another. | | `Comparators.json` | array | Rules for comparing two value types. | @@ -61,7 +62,7 @@ Consumers should read `Manifest.json` first. Do not infer support only from a Sk | `Language.json` | object | Effective key/value entries loaded by Skript's global language registry. | | `PluralRules.json` | object | Effective English singular/plural conversion rules in runtime priority order. | -Every file is always emitted. The empty root is `[]` for array files, `{}` for `Operations.json` and `Language.json`, `{"aliases":{},"targets":[]}` for `Aliases.json`, and `{"algorithm":"unresolved","pluralOverrideSupported":false,"rules":[]}` for `PluralRules.json`. +Every file is always emitted. The empty root is `[]` for array files, `{}` for `Operations.json` and `Language.json`, `{"aliases":{},"targets":[]}` for `Aliases.json`, `{"state":"unsupported","complete":false,"blocks":{},"failures":[]}` for `BlockData.json`, and `{"algorithm":"unresolved","pluralOverrideSupported":false,"rules":[]}` for `PluralRules.json`. ## Shared objects @@ -237,6 +238,55 @@ A structure is top-level/configuration syntax, such as a command or function dec ## Registry files +### `BlockData.json` + +`BlockData.json` is a runtime-derived description of Minecraft blocks. It is an object rather than an array because it describes one collection attempt and its outcome. The generator does not maintain a hardcoded block or property table: on Minecraft 1.13 and newer it enumerates the Bukkit/BlockState registry and asks Bukkit to create each block's canonical state. + +| Field | Type | Presence | Meaning | +| --- | --- | --- | --- | +| `state` | string enum | Required | `collected`, `unsupported`, or `unresolved`. This is the authoritative availability state for BlockData. | +| `complete` | boolean | Required | `true` only when collection finished without failures. `false` also covers unsupported, unresolved, and partial collection. | +| `registryProvider` | string | Required when `state` is `collected`; otherwise optional | Identifier of the runtime registry reader. The current value is `bukkit-runtime-registry`. It may be omitted when no registry can be read. This differs from the `provider` fields elsewhere in the snapshot, which contain an `AddonInfo` object. | +| `blocks` | `object` | Required | Map keyed by sorted, namespaced block IDs such as `minecraft:chest`. Use `{}` when no block data was collected. | +| `failures` | `array` | Required | Collection failures. An empty array means no failures were recorded. | + +`BlockDataBlock` has the following required fields: + +| Field | Type | Presence | Meaning | +| --- | --- | --- | --- | +| `defaultState` | string | Required | Canonical state string returned by the runtime for the block's default state, such as `minecraft:chest[facing=north]`. It is a state representation, not a list of allowed values. | +| `properties` | `object>` | Required | Each property name maps to its possible textual values. Property keys are sorted; each value array is sorted, duplicate-free, and contains only nonempty strings. `{}` is valid for a block with no exposed properties. | + +`BlockDataFailure` has `message` as a required string and `block` as an optional string. When `block` is present, the failure belongs to that namespaced block ID; when it is omitted, the failure is global to the collection attempt. A snapshot with `state: "collected"` and `complete: false` is a valid partial result: use the successfully collected `blocks` and surface `failures` instead of discarding the whole file. + +| Field | Type | Presence | Meaning | +| --- | --- | --- | --- | +| `block` | string | Optional | Namespaced block ID whose collection failed. Omitted for a collection-wide failure. | +| `message` | string | Required | Human-readable reason for the failure. | + +Example: + +```json +{ + "state": "collected", + "complete": true, + "registryProvider": "bukkit-runtime-registry", + "blocks": { + "minecraft:chest": { + "defaultState": "minecraft:chest[facing=north]", + "properties": { + "facing": ["east", "north", "south", "west"] + } + } + }, + "failures": [] +} +``` + +On Minecraft 1.13 and newer, collection is attempted from the live Bukkit/BlockState registry without a hardcoded list. On Minecraft 1.12.2 and older, the BlockData API does not exist and the file is `unsupported` with `complete: false`, empty `blocks`, and empty `failures`. If the API is expected but registry inspection or state creation fails, the file is `unresolved` and the failure explains why. The design follows the purpose of Skript 2.16's [`BlockUtils.java`](https://github.com/SkriptLang/Skript/blob/2.16.0/src/main/java/ch/njol/skript/util/BlockUtils.java) and [`Bukkit.createBlockData(String)`](https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/Bukkit.html#createBlockData(java.lang.String)) path: let the server validate block-state syntax and provide the version-specific vocabulary. + +These `properties` are Minecraft BlockData state properties. They are unrelated to the Skript property system documented in `Properties.json`, which describes reusable capabilities such as obtaining a location from a value. + ### `Types.json` A type connects a Skript type name such as a number, player, or location to Java parsing, conversion, serialization, and change behavior. @@ -579,15 +629,15 @@ Skript sources: [`Language.java` in 2.6.4](https://github.com/SkriptLang/Skript/ | Field | Type | Presence | Meaning | | --- | --- | --- | --- | -| `schemaVersion` | int | Required | Exact value `6` for this document. Reject or negotiate unknown major schema values. | +| `schemaVersion` | int | Required | Exact value `7` for this document. Reject or negotiate unknown major schema values. | | `snapshotId` | sha256 | Required | Identity derived from schema, content, server, language, plugin list, capabilities, and file list. | -| `contentDigest` | sha256 | Required | Digest of the 19 serialized data files, excluding the manifest. | +| `contentDigest` | sha256 | Required | Digest of the 20 serialized data files, including `BlockData.json` and excluding the manifest. | | `generatedAt` | ISO-8601 string | Required | UTC `Instant` timestamp. It is not part of `snapshotId`. | | `server` | `ServerManifestData` | Required | Runtime server identity. | | `language` | string | Required | Active Skript language; legacy collection may use `unknown`. Language affects localized type nouns. | | `plugins` | `array` | Required | Installed plugins in Bukkit load order. | | `capabilities` | `SnapshotCapabilitiesData` | Required | API shapes and supported registries. | -| `files` | `array` | Required | Sorted list of all 20 expected filenames, including `Manifest.json`. | +| `files` | `array` | Required | Sorted list of all 21 expected filenames: 20 data files plus `Manifest.json`. | `ServerManifestData` has required string fields `name`, `version`, `bukkitVersion`, `minecraftVersion`, and `javaVersion`. @@ -620,6 +670,7 @@ The tested compatibility matrix is maintained in the main README. The important | --- | --- | | Core conditions, effects, events, expressions, sections, types, functions, converters, comparators, event values | Collected in every tested version from 2.6.4 through 2.16.0. | | Structures | `Structures.json` is empty on 2.6.4 because there is no enumerable structure registry. Registrations appear from 2.7.x. Rich `entryValidator`/`nodeType` fields require the current adapter (2.14+). | +| BlockData | `BlockData.json` is collected from the live Bukkit/BlockState registry on Minecraft 1.13+; 1.12.2 and older are `unsupported`. Registry/API inspection failures are `unresolved`, and individual block failures produce `state: "collected", complete: false`. | | Arithmetic registries | Empty on 2.6.4 and 2.7.3. The enumerable `Arithmetics` registry appears in 2.8.0; all three arithmetic files are collected from 2.8.x onward. | | Properties | `Properties.json` is empty before 2.13.0. | | Expression multiplicity/changers and implementation metadata | Legacy adapter keeps multiplicity/changer state unresolved and omits current implementation metadata. Current adapter resolves it where bytecode/instance inspection is safe. | diff --git a/legacy/src/main/java/jp/nlaocs/skriptSyntaxGenerator/legacy/LegacySkriptSyntaxGenerator.java b/legacy/src/main/java/jp/nlaocs/skriptSyntaxGenerator/legacy/LegacySkriptSyntaxGenerator.java index 4d362b4..2c7d383 100644 --- a/legacy/src/main/java/jp/nlaocs/skriptSyntaxGenerator/legacy/LegacySkriptSyntaxGenerator.java +++ b/legacy/src/main/java/jp/nlaocs/skriptSyntaxGenerator/legacy/LegacySkriptSyntaxGenerator.java @@ -52,6 +52,13 @@ public void run() { getLogger().info("Automated legacy Skript syntax generation completed!"); } catch (Throwable throwable) { getLogger().log(Level.SEVERE, "Automated legacy Skript syntax generation failed.", throwable); + throwable.printStackTrace(); + System.err.flush(); + try { + Thread.sleep(250L); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + } Runtime.getRuntime().halt(1); } finally { Bukkit.shutdown(); diff --git a/legacy/src/main/java/jp/nlaocs/skriptSyntaxGenerator/legacy/LegacySnapshotGenerator.java b/legacy/src/main/java/jp/nlaocs/skriptSyntaxGenerator/legacy/LegacySnapshotGenerator.java index d8094c8..d9eb371 100644 --- a/legacy/src/main/java/jp/nlaocs/skriptSyntaxGenerator/legacy/LegacySnapshotGenerator.java +++ b/legacy/src/main/java/jp/nlaocs/skriptSyntaxGenerator/legacy/LegacySnapshotGenerator.java @@ -11,6 +11,7 @@ import jp.nlaocs.skriptSyntaxGenerator.data.SyntaxApi; import jp.nlaocs.skriptSyntaxGenerator.data.SyntaxKindCapabilitiesData; import jp.nlaocs.skriptSyntaxGenerator.generator.GlobalAliasesReader; +import jp.nlaocs.skriptSyntaxGenerator.generator.BlockDataReader; import jp.nlaocs.skriptSyntaxGenerator.generator.LanguageReader; import jp.nlaocs.skriptSyntaxGenerator.generator.PluralRulesReader; import jp.nlaocs.skriptSyntaxGenerator.generator.SnapshotFormat; @@ -87,6 +88,7 @@ void generate() { outputs.put("Operations.json", arithmetic.collectOperations()); outputs.put("ClassHierarchy.json", hierarchy.toData()); outputs.put(SnapshotFormat.ALIASES_FILE, GlobalAliasesReader.read(skriptClassLoader)); + outputs.put(SnapshotFormat.BLOCK_DATA_FILE, BlockDataReader.read(skriptClassLoader)); outputs.put(SnapshotFormat.LANGUAGE_FILE, LanguageReader.read(skriptClassLoader)); Plugin skriptPlugin = Bukkit.getPluginManager().getPlugin("Skript"); if (skriptPlugin == null) throw new IllegalStateException("Skript is not installed"); diff --git a/snapshot-contract/src/main/java/jp/nlaocs/skriptSyntaxGenerator/data/BlockDataBlockData.java b/snapshot-contract/src/main/java/jp/nlaocs/skriptSyntaxGenerator/data/BlockDataBlockData.java new file mode 100644 index 0000000..23ba8a1 --- /dev/null +++ b/snapshot-contract/src/main/java/jp/nlaocs/skriptSyntaxGenerator/data/BlockDataBlockData.java @@ -0,0 +1,17 @@ +package jp.nlaocs.skriptSyntaxGenerator.data; + +import java.util.List; +import java.util.Map; + +public final class BlockDataBlockData { + private final String defaultState; + private final Map> properties; + + public BlockDataBlockData(String defaultState, Map> properties) { + this.defaultState = defaultState; + this.properties = properties; + } + + public String getDefaultState() { return defaultState; } + public Map> getProperties() { return properties; } +} diff --git a/snapshot-contract/src/main/java/jp/nlaocs/skriptSyntaxGenerator/data/BlockDataFailureData.java b/snapshot-contract/src/main/java/jp/nlaocs/skriptSyntaxGenerator/data/BlockDataFailureData.java new file mode 100644 index 0000000..006f7db --- /dev/null +++ b/snapshot-contract/src/main/java/jp/nlaocs/skriptSyntaxGenerator/data/BlockDataFailureData.java @@ -0,0 +1,14 @@ +package jp.nlaocs.skriptSyntaxGenerator.data; + +public final class BlockDataFailureData { + private final String block; + private final String message; + + public BlockDataFailureData(String block, String message) { + this.block = block; + this.message = message; + } + + public String getBlock() { return block; } + public String getMessage() { return message; } +} diff --git a/snapshot-contract/src/main/java/jp/nlaocs/skriptSyntaxGenerator/data/BlockDataSnapshotData.java b/snapshot-contract/src/main/java/jp/nlaocs/skriptSyntaxGenerator/data/BlockDataSnapshotData.java new file mode 100644 index 0000000..3e8dd2a --- /dev/null +++ b/snapshot-contract/src/main/java/jp/nlaocs/skriptSyntaxGenerator/data/BlockDataSnapshotData.java @@ -0,0 +1,32 @@ +package jp.nlaocs.skriptSyntaxGenerator.data; + +import java.util.List; +import java.util.Map; + +public final class BlockDataSnapshotData { + private final String state; + private final boolean complete; + private final String registryProvider; + private final Map blocks; + private final List failures; + + public BlockDataSnapshotData( + String state, + boolean complete, + String registryProvider, + Map blocks, + List failures + ) { + this.state = state; + this.complete = complete; + this.registryProvider = registryProvider; + this.blocks = blocks; + this.failures = failures; + } + + public String getState() { return state; } + public boolean isComplete() { return complete; } + public String getRegistryProvider() { return registryProvider; } + public Map getBlocks() { return blocks; } + public List getFailures() { return failures; } +} diff --git a/snapshot-contract/src/main/java/jp/nlaocs/skriptSyntaxGenerator/generator/BlockDataReader.java b/snapshot-contract/src/main/java/jp/nlaocs/skriptSyntaxGenerator/generator/BlockDataReader.java new file mode 100644 index 0000000..44a7c6a --- /dev/null +++ b/snapshot-contract/src/main/java/jp/nlaocs/skriptSyntaxGenerator/generator/BlockDataReader.java @@ -0,0 +1,270 @@ +package jp.nlaocs.skriptSyntaxGenerator.generator; + +import jp.nlaocs.skriptSyntaxGenerator.data.BlockDataBlockData; +import jp.nlaocs.skriptSyntaxGenerator.data.BlockDataFailureData; +import jp.nlaocs.skriptSyntaxGenerator.data.BlockDataSnapshotData; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; +import java.util.TreeSet; + +public final class BlockDataReader { + private static final String BUKKIT_CLASS = "org.bukkit.Bukkit"; + private static final String MATERIAL_CLASS = "org.bukkit.Material"; + private static final String BLOCK_DATA_CLASS = "org.bukkit.block.data.BlockData"; + private static final String PROVIDER = "bukkit-runtime-registry"; + + private BlockDataReader() { + } + + public static BlockDataSnapshotData read(ClassLoader classLoader) { + try { + Class bukkit = Class.forName(BUKKIT_CLASS, false, classLoader); + Class material = Class.forName(MATERIAL_CLASS, false, classLoader); + Class blockData = Class.forName(BLOCK_DATA_CLASS, false, classLoader); + if (findStaticMethod(bukkit, "createBlockData", String.class) == null) { + return unsupported(); + } + return read(bukkit, material, blockData); + } catch (ClassNotFoundException ignored) { + return unsupported(); + } catch (LinkageError error) { + return unresolved(errorMessage(error)); + } + } + + static BlockDataSnapshotData read( + Class bukkitClass, + Class materialClass, + Class blockDataClass + ) { + Object[] constants = materialClass.getEnumConstants(); + if (constants == null) return unresolved("Bukkit Material is not an enum"); + + TreeMap materials = new TreeMap(); + for (Object material : constants) { + if (!Boolean.TRUE.equals(invokeOrNull(material, "isBlock"))) continue; + if (Boolean.TRUE.equals(invokeOrNull(material, "isLegacy"))) continue; + materials.put(materialId(material), material); + } + + Map blocks = new LinkedHashMap(); + List failures = new ArrayList(); + for (Map.Entry entry : materials.entrySet()) { + try { + Object blockData = invokeStatic(bukkitClass, "createBlockData", entry.getKey()); + if (!blockDataClass.isInstance(blockData)) { + throw new IllegalStateException("Bukkit returned a non-BlockData value"); + } + blocks.put(entry.getKey(), block(blockData)); + } catch (RuntimeException exception) { + failures.add(new BlockDataFailureData(entry.getKey(), errorMessage(exception))); + } + } + + if (blocks.isEmpty() && !materials.isEmpty()) { + return new BlockDataSnapshotData( + "unresolved", + false, + PROVIDER, + blocks, + failures.isEmpty() + ? Collections.singletonList(new BlockDataFailureData(null, "No BlockData states could be read")) + : failures + ); + } + return new BlockDataSnapshotData( + "collected", + failures.isEmpty(), + PROVIDER, + blocks, + failures + ); + } + + private static BlockDataBlockData block(Object blockData) { + Object state = requireInvoke(blockData, "getState"); + Map values = stateValues(state); + Map> properties = new LinkedHashMap>(); + TreeMap sorted = new TreeMap(); + for (Object property : values.keySet()) { + Object name = invokeOrNull(property, "getName"); + if (name != null) sorted.put(String.valueOf(name), property); + } + for (Map.Entry entry : sorted.entrySet()) { + Object possibleValues = invokeFirst(entry.getValue(), "getPossibleValues", "getValues"); + if (!(possibleValues instanceof Collection)) { + throw new IllegalStateException("Cannot read values for BlockData property " + entry.getKey()); + } + TreeSet names = new TreeSet(); + for (Object value : (Collection) possibleValues) { + Object name = invokeOrNull(entry.getValue(), "getName", value); + names.add(name == null + ? String.valueOf(value).toLowerCase(Locale.ROOT) + : String.valueOf(name)); + } + properties.put(entry.getKey(), new ArrayList(names)); + } + Object serialized = requireInvoke(blockData, "getAsString"); + return new BlockDataBlockData(String.valueOf(serialized), properties); + } + + private static Map stateValues(Object state) { + Object values = invokeFirst(state, "getValues", "getStateMap"); + if (values instanceof Map) return (Map) values; + + for (Class type = state.getClass(); type != null; type = type.getSuperclass()) { + for (Field field : type.getDeclaredFields()) { + if (!Map.class.isAssignableFrom(field.getType())) continue; + try { + field.setAccessible(true); + Object candidate = field.get(state); + if (candidate instanceof Map) return (Map) candidate; + } catch (ReflectiveOperationException ignored) { + // Try the next map field. + } + } + } + throw new IllegalStateException("Cannot read the Minecraft BlockState property map"); + } + + private static String materialId(Object material) { + Object key = invokeOrNull(material, "getKey"); + if (key != null) return String.valueOf(key); + return "minecraft:" + String.valueOf(material).toLowerCase(Locale.ROOT); + } + + private static BlockDataSnapshotData unsupported() { + return new BlockDataSnapshotData( + "unsupported", + false, + null, + Collections.emptyMap(), + Collections.emptyList() + ); + } + + private static BlockDataSnapshotData unresolved(String message) { + return new BlockDataSnapshotData( + "unresolved", + false, + PROVIDER, + Collections.emptyMap(), + Collections.singletonList(new BlockDataFailureData(null, message)) + ); + } + + private static Object requireInvoke(Object target, String name, Object... arguments) { + Object value = invokeOrNull(target, name, arguments); + if (value == null) { + throw new IllegalStateException("Method returned no value: " + target.getClass().getName() + "." + name); + } + return value; + } + + private static Object invokeFirst(Object target, String... names) { + for (String name : names) { + Object value = invokeOrNull(target, name); + if (value != null) return value; + } + return null; + } + + private static Object invokeOrNull(Object target, String name, Object... arguments) { + Method method = findMethod(target.getClass(), name, arguments); + if (method == null) return null; + try { + method.setAccessible(true); + return method.invoke(target, arguments); + } catch (IllegalAccessException exception) { + throw new IllegalStateException("Cannot invoke " + method, exception); + } catch (InvocationTargetException exception) { + Throwable cause = exception.getCause(); + if (cause instanceof RuntimeException) throw (RuntimeException) cause; + throw new IllegalStateException("Cannot invoke " + method, cause); + } + } + + private static Object invokeStatic(Class type, String name, Object... arguments) { + Method method = findMethod(type, name, arguments); + if (method == null || !Modifier.isStatic(method.getModifiers())) { + throw new IllegalStateException("Static method not found: " + type.getName() + "." + name); + } + try { + method.setAccessible(true); + return method.invoke(null, arguments); + } catch (IllegalAccessException exception) { + throw new IllegalStateException("Cannot invoke " + method, exception); + } catch (InvocationTargetException exception) { + Throwable cause = exception.getCause(); + if (cause instanceof RuntimeException) throw (RuntimeException) cause; + throw new IllegalStateException("Cannot invoke " + method, cause); + } + } + + private static Method findStaticMethod(Class type, String name, Class parameter) { + try { + Method method = type.getMethod(name, parameter); + return Modifier.isStatic(method.getModifiers()) ? method : null; + } catch (NoSuchMethodException ignored) { + return null; + } + } + + private static Method findMethod(Class type, String name, Object[] arguments) { + for (Class current = type; current != null; current = current.getSuperclass()) { + for (Method method : current.getDeclaredMethods()) { + if (compatible(method, name, arguments)) return method; + } + } + for (Method method : type.getMethods()) { + if (compatible(method, name, arguments)) return method; + } + return null; + } + + private static boolean compatible(Method method, String name, Object[] arguments) { + if (!method.getName().equals(name) || method.getParameterTypes().length != arguments.length) { + return false; + } + Class[] parameters = method.getParameterTypes(); + for (int index = 0; index < parameters.length; index++) { + if (arguments[index] != null && !wrap(parameters[index]).isAssignableFrom(arguments[index].getClass())) { + return false; + } + } + return true; + } + + private static Class wrap(Class type) { + if (!type.isPrimitive()) return type; + if (type == boolean.class) return Boolean.class; + if (type == byte.class) return Byte.class; + if (type == short.class) return Short.class; + if (type == int.class) return Integer.class; + if (type == long.class) return Long.class; + if (type == float.class) return Float.class; + if (type == double.class) return Double.class; + if (type == char.class) return Character.class; + return type; + } + + private static String errorMessage(Throwable throwable) { + Throwable current = throwable; + while (current.getCause() != null && current.getCause() != current) current = current.getCause(); + String message = current.getMessage(); + return message == null || message.trim().isEmpty() + ? current.getClass().getName() + : current.getClass().getSimpleName() + ": " + message; + } +} diff --git a/snapshot-contract/src/main/java/jp/nlaocs/skriptSyntaxGenerator/generator/SnapshotFormat.java b/snapshot-contract/src/main/java/jp/nlaocs/skriptSyntaxGenerator/generator/SnapshotFormat.java index b2768bb..7aeda9a 100644 --- a/snapshot-contract/src/main/java/jp/nlaocs/skriptSyntaxGenerator/generator/SnapshotFormat.java +++ b/snapshot-contract/src/main/java/jp/nlaocs/skriptSyntaxGenerator/generator/SnapshotFormat.java @@ -10,15 +10,16 @@ import java.util.Set; public final class SnapshotFormat { - public static final int SCHEMA_VERSION = 6; + public static final int SCHEMA_VERSION = 7; public static final String MANIFEST_FILE = "Manifest.json"; public static final String ALIASES_FILE = "Aliases.json"; + public static final String BLOCK_DATA_FILE = "BlockData.json"; public static final String LANGUAGE_FILE = "Language.json"; public static final String OPERATIONS_FILE = "Operations.json"; public static final String PLURAL_RULES_FILE = "PluralRules.json"; private static final List DATA_FILES = Collections.unmodifiableList(Arrays.asList( - ALIASES_FILE, "ClassHierarchy.json", "Comparators.json", "Conditions.json", "Converters.json", + ALIASES_FILE, BLOCK_DATA_FILE, "ClassHierarchy.json", "Comparators.json", "Conditions.json", "Converters.json", "Differences.json", "Effects.json", "EventValues.json", "Events.json", "Expressions.json", "Functions.json", LANGUAGE_FILE, OPERATIONS_FILE, "Operators.json", PLURAL_RULES_FILE, "Properties.json", "Sections.json", "Structures.json", "Types.json" @@ -67,6 +68,14 @@ private static Object emptyRoot(String fileName) { aliases.put("targets", Collections.emptyList()); return aliases; } + if (BLOCK_DATA_FILE.equals(fileName)) { + Map blockData = new LinkedHashMap(); + blockData.put("state", "unsupported"); + blockData.put("complete", false); + blockData.put("blocks", Collections.emptyMap()); + blockData.put("failures", Collections.emptyList()); + return blockData; + } if (LANGUAGE_FILE.equals(fileName)) { return Collections.emptyMap(); } diff --git a/src/main/kotlin/jp/nlaocs/skriptSyntaxGenerator/SkriptSyntaxGenerator.kt b/src/main/kotlin/jp/nlaocs/skriptSyntaxGenerator/SkriptSyntaxGenerator.kt index aae9b00..bacffcd 100644 --- a/src/main/kotlin/jp/nlaocs/skriptSyntaxGenerator/SkriptSyntaxGenerator.kt +++ b/src/main/kotlin/jp/nlaocs/skriptSyntaxGenerator/SkriptSyntaxGenerator.kt @@ -49,6 +49,13 @@ class SkriptSyntaxGenerator : JavaPlugin() { logger.info("Automated Skript syntax generation completed!") } catch (throwable: Throwable) { logger.log(Level.SEVERE, "Automated Skript syntax generation failed.", throwable) + throwable.printStackTrace() + System.err.flush() + try { + Thread.sleep(250L) + } catch (_: InterruptedException) { + Thread.currentThread().interrupt() + } Runtime.getRuntime().halt(1) } finally { Bukkit.shutdown() diff --git a/src/main/kotlin/jp/nlaocs/skriptSyntaxGenerator/generator/SnapshotDataSource.kt b/src/main/kotlin/jp/nlaocs/skriptSyntaxGenerator/generator/SnapshotDataSource.kt index 96b4c4b..e29e90d 100644 --- a/src/main/kotlin/jp/nlaocs/skriptSyntaxGenerator/generator/SnapshotDataSource.kt +++ b/src/main/kotlin/jp/nlaocs/skriptSyntaxGenerator/generator/SnapshotDataSource.kt @@ -63,6 +63,7 @@ class ModernSnapshotDataSource : SnapshotDataSource { } outputs["ClassHierarchy.json"] = ClassHierarchyCollector().collect(outputs.values) outputs[SnapshotFormat.ALIASES_FILE] = GlobalAliasesReader.read(javaClass.classLoader) + outputs[SnapshotFormat.BLOCK_DATA_FILE] = BlockDataReader.read(javaClass.classLoader) outputs[SnapshotFormat.LANGUAGE_FILE] = LanguageReader.read(Skript::class.java.classLoader) return outputs } diff --git a/src/test/kotlin/jp/nlaocs/skriptSyntaxGenerator/generator/BlockDataReaderTest.kt b/src/test/kotlin/jp/nlaocs/skriptSyntaxGenerator/generator/BlockDataReaderTest.kt new file mode 100644 index 0000000..29b4a21 --- /dev/null +++ b/src/test/kotlin/jp/nlaocs/skriptSyntaxGenerator/generator/BlockDataReaderTest.kt @@ -0,0 +1,103 @@ +package jp.nlaocs.skriptSyntaxGenerator.generator + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class BlockDataReaderTest { + @Test + fun `collects sorted runtime block properties without hardcoded states`() { + val data = BlockDataReader.read( + FakeBukkit::class.java, + FakeMaterial::class.java, + FakeBlockData::class.java, + ) + + assertEquals("collected", data.state) + assertTrue(data.isComplete) + assertEquals("bukkit-runtime-registry", data.registryProvider) + assertEquals(listOf("minecraft:chest", "minecraft:stone"), data.blocks.keys.toList()) + assertEquals( + listOf("east", "north", "south", "west"), + data.blocks.getValue("minecraft:chest").properties.getValue("facing"), + ) + assertEquals("minecraft:chest[facing=north]", data.blocks.getValue("minecraft:chest").defaultState) + assertTrue(data.failures.isEmpty()) + } + + @Test + fun `retains partial runtime failures instead of claiming completeness`() { + FakeBukkit.failStone = true + try { + val data = BlockDataReader.read( + FakeBukkit::class.java, + FakeMaterial::class.java, + FakeBlockData::class.java, + ) + + assertEquals("collected", data.state) + assertFalse(data.isComplete) + assertEquals(setOf("minecraft:chest"), data.blocks.keys) + assertEquals("minecraft:stone", data.failures.single().block) + } finally { + FakeBukkit.failStone = false + } + } + + private enum class FakeMaterial( + private val block: Boolean, + private val legacy: Boolean = false, + ) { + CHEST(true), + LEGACY_STONE(true, legacy = true), + STONE(true), + STICK(false); + + fun isBlock(): Boolean = block + fun isLegacy(): Boolean = legacy + fun getKey(): FakeKey { + check(!legacy) { "Cannot get key of Legacy Material" } + return FakeKey("minecraft:${name.lowercase()}") + } + } + + private data class FakeKey(private val value: String) { + override fun toString(): String = value + } + + private class FakeProperty( + private val name: String, + private val values: List, + ) { + fun getName(): String = name + fun getPossibleValues(): Collection = values + fun getName(value: String): String = value + } + + private class FakeState(private val values: Map) { + fun getValues(): Map = values + } + + private class FakeBlockData(private val id: String) { + fun getState(): FakeState = if (id.endsWith("chest")) { + FakeState(mapOf(FakeProperty("facing", listOf("south", "north", "west", "east")) to "north")) + } else { + FakeState(emptyMap()) + } + + fun getAsString(): String = if (id.endsWith("chest")) "$id[facing=north]" else id + } + + private class FakeBukkit { + companion object { + var failStone = false + + @JvmStatic + fun createBlockData(id: String): FakeBlockData { + if (failStone && id.endsWith("stone")) error("broken stone") + return FakeBlockData(id) + } + } + } +} diff --git a/src/test/kotlin/jp/nlaocs/skriptSyntaxGenerator/generator/SnapshotFormatTest.kt b/src/test/kotlin/jp/nlaocs/skriptSyntaxGenerator/generator/SnapshotFormatTest.kt index abe7a15..68a7ae3 100644 --- a/src/test/kotlin/jp/nlaocs/skriptSyntaxGenerator/generator/SnapshotFormatTest.kt +++ b/src/test/kotlin/jp/nlaocs/skriptSyntaxGenerator/generator/SnapshotFormatTest.kt @@ -28,6 +28,15 @@ class SnapshotFormatTest { mapOf("aliases" to emptyMap(), "targets" to emptyList()), normalized.getValue("Aliases.json") ) + assertEquals( + mapOf( + "state" to "unsupported", + "complete" to false, + "blocks" to emptyMap(), + "failures" to emptyList(), + ), + normalized.getValue(SnapshotFormat.BLOCK_DATA_FILE), + ) assertEquals(emptyMap(), normalized.getValue(SnapshotFormat.LANGUAGE_FILE)) assertEquals(emptyList(), normalized.getValue("Types.json")) } @@ -43,7 +52,7 @@ class SnapshotFormatTest { @Test fun `manifest is part of all files but not normalized data outputs`() { - assertEquals(20, SnapshotFormat.getAllFiles().size) + assertEquals(21, SnapshotFormat.getAllFiles().size) assertEquals(SnapshotFormat.getAllFiles().sorted(), SnapshotFormat.getAllFiles()) assertEquals(true, SnapshotFormat.getAllFiles().contains(SnapshotFormat.MANIFEST_FILE)) assertEquals(false, SnapshotFormat.getDataFiles().contains(SnapshotFormat.MANIFEST_FILE)) diff --git a/src/test/kotlin/jp/nlaocs/skriptSyntaxGenerator/integration/SnapshotValidator.kt b/src/test/kotlin/jp/nlaocs/skriptSyntaxGenerator/integration/SnapshotValidator.kt index 2c567f4..5b07348 100644 --- a/src/test/kotlin/jp/nlaocs/skriptSyntaxGenerator/integration/SnapshotValidator.kt +++ b/src/test/kotlin/jp/nlaocs/skriptSyntaxGenerator/integration/SnapshotValidator.kt @@ -90,7 +90,7 @@ object SnapshotValidator { failIfAny(errors) - (requiredFiles - setOf("Manifest.json", "Operations.json", "Aliases.json", "Language.json", "PluralRules.json")).forEach { fileName -> + (requiredFiles - setOf("Manifest.json", "Operations.json", "Aliases.json", "BlockData.json", "Language.json", "PluralRules.json")).forEach { fileName -> expect(documents.getValue(fileName).isArray, errors) { "$fileName root must be an array" } } expect(documents.getValue("Operations.json").isObject, errors) { @@ -99,6 +99,9 @@ object SnapshotValidator { expect(documents.getValue("Aliases.json").isObject, errors) { "Aliases.json root must be an object" } + expect(documents.getValue("BlockData.json").isObject, errors) { + "BlockData.json root must be an object" + } expect(documents.getValue("PluralRules.json").isObject, errors) { "PluralRules.json root must be an object" } @@ -119,6 +122,7 @@ object SnapshotValidator { val size = when (fileName) { "Operations.json" -> flattenObjectArrays(document).size "Aliases.json" -> document["aliases"]?.size() ?: 0 + "BlockData.json" -> document["blocks"]?.size() ?: 0 "PluralRules.json" -> document["rules"]?.size() ?: 0 else -> document.size() } @@ -172,6 +176,7 @@ object SnapshotValidator { } validatePluralRules(documents.getValue("PluralRules.json"), errors) validateAliases(documents.getValue("Aliases.json"), errors) + validateBlockData(documents.getValue("BlockData.json"), errors) validateLanguage(documents.getValue("Language.json"), errors) validateTypeReferences(documents, errors) validateEventValueReferences(documents, errors) @@ -734,6 +739,70 @@ object SnapshotValidator { } } + private fun validateBlockData(root: JsonNode, errors: MutableList) { + val state = root["state"]?.takeIf(JsonNode::isTextual)?.asText() + expect(state in setOf("collected", "unsupported", "unresolved"), errors) { + "BlockData.json.state must be collected, unsupported, or unresolved" + } + expect(root["complete"]?.isBoolean == true, errors) { + "BlockData.json.complete must be boolean" + } + val blocks = root["blocks"] + val failures = root["failures"] + expect(blocks?.isObject == true, errors) { "BlockData.json.blocks must be an object" } + expect(failures?.isArray == true, errors) { "BlockData.json.failures must be an array" } + + if (state == "collected") { + expect( + root["registryProvider"]?.isTextual == true && + root["registryProvider"].asText().isNotBlank(), + errors + ) { + "BlockData.json.registryProvider must identify the collected runtime registry" + } + } + if (state == "unsupported") { + expect(blocks?.isEmpty == true, errors) { + "Unsupported BlockData registries must not contain blocks" + } + } + + val blockIds = mutableListOf() + blocks?.fields()?.forEachRemaining { (blockId, block) -> + blockIds += blockId + expect(':' in blockId, errors) { "BlockData.json block ID must be namespaced: $blockId" } + expect(block["defaultState"]?.isTextual == true, errors) { + "BlockData.json.blocks[$blockId].defaultState must be a string" + } + val properties = block["properties"] + expect(properties?.isObject == true, errors) { + "BlockData.json.blocks[$blockId].properties must be an object" + } + properties?.fields()?.forEachRemaining { (property, values) -> + expect(property.isNotBlank(), errors) { + "BlockData.json.blocks[$blockId] contains a blank property" + } + expect(values.isArray && values.size() > 0, errors) { + "BlockData.json.blocks[$blockId].properties[$property] must be a non-empty array" + } + val names = values.takeIf(JsonNode::isArray)?.map(JsonNode::asText).orEmpty() + expect(names.all(String::isNotBlank), errors) { + "BlockData.json.blocks[$blockId].properties[$property] contains a blank value" + } + expect(names == names.distinct().sorted(), errors) { + "BlockData.json.blocks[$blockId].properties[$property] must be unique and sorted" + } + } + } + expect(blockIds == blockIds.sorted(), errors) { "BlockData.json blocks are not sorted" } + + failures?.takeIf(JsonNode::isArray)?.forEachIndexed { index, failure -> + expect(failure["message"]?.isTextual == true && failure["message"].asText().isNotBlank(), errors) { + "BlockData.json.failures[$index].message must be a non-empty string" + } + } + } + private fun validateEvents(events: JsonNode, errors: MutableList) { events.forEachIndexed { index, event -> val referenceEvents = event["referenceEvents"] diff --git a/src/test/kotlin/jp/nlaocs/skriptSyntaxGenerator/integration/SnapshotValidatorTest.kt b/src/test/kotlin/jp/nlaocs/skriptSyntaxGenerator/integration/SnapshotValidatorTest.kt index 477fc17..1afad86 100644 --- a/src/test/kotlin/jp/nlaocs/skriptSyntaxGenerator/integration/SnapshotValidatorTest.kt +++ b/src/test/kotlin/jp/nlaocs/skriptSyntaxGenerator/integration/SnapshotValidatorTest.kt @@ -28,7 +28,7 @@ class SnapshotValidatorTest { val report = SnapshotValidator.validate(tempDirectory) - assertEquals(20, report.files) + assertEquals(21, report.files) assertEquals(0, report.aliases) assertEquals(0, report.registrations) } @@ -239,6 +239,8 @@ class SnapshotValidatorTest { when (fileName) { SnapshotFormat.OPERATIONS_FILE -> "{}" SnapshotFormat.ALIASES_FILE -> "{\"aliases\":{},\"targets\":[]}" + SnapshotFormat.BLOCK_DATA_FILE -> + "{\"state\":\"unsupported\",\"complete\":false,\"blocks\":{},\"failures\":[]}" SnapshotFormat.PLURAL_RULES_FILE -> """{"algorithm":"singular-aware","pluralOverrideSupported":true,"rules":[{"ruleOrder":0,"singular":"","plural":"s","completeWord":false,"origin":"built-in","addon":{"name":"Skript","version":"2.14.3"}}]}""" SnapshotFormat.LANGUAGE_FILE -> "{}"