diff --git a/.gitattributes b/.gitattributes index 2d9af2e..2fd643e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,6 @@ * text=auto eol=lf -# 共通テストベクタはバイナリとして厳密に扱う(改行変換されると検証が壊れるため) +# 共通テストベクタはバイナリとして厳密に扱う(改行変換されると検証がおかしくなるため) spec/testdata/**/*.nbt binary spec/testdata/**/*.mca binary spec/testdata/**/*.mcc binary diff --git a/.github/release-common.md b/.github/release-common.md index e447bcf..f9b9449 100644 --- a/.github/release-common.md +++ b/.github/release-common.md @@ -1,5 +1,4 @@ -Minecraft **Java版 26.1 以降**のワールドデータ・NBTファイルを -読み書きするライブラリです。 +Minecraft **Java版26.1以降**のワールドデータ・NBTファイルを読み書きするライブラリです。 下の成果物をダウンロードして、自分のプロジェクトへ組み込んでください。 @@ -7,18 +6,18 @@ Minecraft **Java版 26.1 以降**のワールドデータ・NBTファイルを | 言語 | ファイル | 組み込み方 | |---|---|---| -| C# | `SpringNBTLibrary.*.nupkg` | 置き場所を `nuget.config` で教えて `dotnet add package` | -| C# | `SpringNBTLibrary-*-dotnet8.zip` | 展開して `.dll` を参照に追加(`.xml` を隣に置くと補完が効く) | -| Java | `spring-nbt-library-*.jar` | クラスパスへ追加、またはローカルリポジトリへ install | +| C# | `SpringNBTLibrary.*.nupkg` | 置き場所を`nuget.config`で教えて`dotnet add package` | +| C# | `SpringNBTLibrary-*-dotnet8.zip` | 展開して`.dll`を参照に追加(`.xml`を隣に置くと補完が効く) | +| Java | `spring-nbt-library-*.jar` | クラスパスへ追加、またはローカルリポジトリへinstall | | TypeScript | `spring-nbt-library-*.tgz` | `npm install ./spring-nbt-library-*.tgz` | | Python | `spring_nbt_library-*.whl` | `pip install spring_nbt_library-*.whl` | -| Rust | `spring-nbt-library-*.crate` | 展開してパス参照。git 参照でも使えます | +| Rust | `spring-nbt-library-*.crate` | 展開してパス参照。git参照でも使えます | -言語ごとの詳しい手順は [はじめに](https://github.com/ScriptArts/SpringNBTLibrary/tree/main/docs/getting-started) を参照してください。 +言語ごとの詳しい手順は[はじめに](https://github.com/ScriptArts/SpringNBTLibrary/tree/main/docs/getting-started)を参照してください。 ## 確認 -`SHA256SUMS.txt` で、落としたファイルが壊れていないか確かめられます。 +`SHA256SUMS.txt`で、落としたファイルが破損していないか確かめられます。 ```bash sha256sum -c SHA256SUMS.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed796bf..57570b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,13 +5,13 @@ on: branches: [main] pull_request: -# 同じブランチへ続けて push したら、前の実行は打ち切る +# 同じブランチへ続けてpushしたら、前の実行は打ち切る concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: - # 言語ごとの単体テスト。1 つ落ちても他の結果を見たいので fail-fast は切る + # 言語ごとの単体テスト。1つ落ちても他の結果を見たいのでfail-fastは切る test: name: ${{ matrix.language }} runs-on: ubuntu-latest @@ -85,7 +85,7 @@ jobs: working-directory: rust run: cargo test --all-features - # このライブラリの核。全言語に同じ入力を与えて出力バイト列を突き合わせる + # このライブラリでいちばん肝心な検査。全言語に同じ入力を与えて出力バイト列を比べる conformance: name: クロス言語適合性 runs-on: ubuntu-latest @@ -122,7 +122,7 @@ jobs: - name: 全言語の出力を突き合わせる run: ./spec/run-conformance.sh - # 最低限の MSRV を割っていないか + # 最低限のMSRVを割っていないか msrv: name: Rust MSRV (1.75) runs-on: ubuntu-latest diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b11eda5..1d47f88 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,8 +7,7 @@ on: # ドキュメント・コメントの規約から外れたものを検出する。 # -# 人手で5言語ぶんを揃え続けるのは必ず破綻するので、ここで機械的に守る -# (docs/adr/0009-static-api-extraction.md)。 +# 人手で5言語ぶんを揃え続けると必ずうまくいかなくなるので、ここで機械的に守る。 jobs: check: name: ドキュメントとコメントの規約 @@ -28,6 +27,6 @@ jobs: run: python3 spec/tools/check_links.py # 「if / ループにコメントを入れる」は独自の規約なので、 - # 各言語の標準 Linter には該当ルールが無い。自前で見るしかない + # 各言語の標準Linterには該当ルールが無い。自前で見るしかない - name: コメントと禁止記法の規約適合を調べる run: python3 spec/tools/check_comments.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9a32a28..2bd2862 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,6 @@ name: release -# タグを打つとビルドして GitHub Releases へ成果物を添付する。 +# タグを打つとビルドしてGitHub Releasesへ成果物を添付する。 # # パッケージレジストリ(NuGet / Maven Central / npm / PyPI / crates.io)へは公開せず、 # ここに置いたファイルを利用者が自分のプロジェクトへ組み込む形をとる。 @@ -10,7 +10,7 @@ on: # タグを打つ前に組み立てだけ試せるようにしておく workflow_dispatch: -# gh release create で成果物を上げるために要る +# gh release createで成果物を上げるために要る permissions: contents: write @@ -44,7 +44,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable - # タグ名から版を取る。手動実行のときは Cargo.toml の版を使う + # タグ名から版を取る。手動実行のときはCargo.tomlの版を使う - name: 版を決める id: version run: | @@ -56,7 +56,7 @@ jobs: echo "value=${version}" >> "$GITHUB_OUTPUT" echo "組み立てる版: ${version}" - # 壊れたものを配らないよう、公開前に必ず全部通す + # 正しく動かないものを配らないよう、公開前に必ず全部通す - name: 全言語のテスト run: | (cd csharp && dotnet test --configuration Release) @@ -68,8 +68,8 @@ jobs: - name: 全言語の挙動が一致することの検証 run: ./spec/run-conformance.sh - # dll を直接参照したい人向け。xml を隣に置かないと補完に説明が出ないので、 - # LICENSE と合わせてひとまとめにする + # dllを直接参照したい人向け。xmlを隣に置かないと補完に説明が出ないので、 + # LICENSEと合わせてひとまとめにする - name: C# (zip) run: | dotnet build csharp/src/SpringNBTLibrary/SpringNBTLibrary.csproj \ @@ -82,7 +82,7 @@ jobs: (cd artifacts/csharp && zip -q -r \ "../SpringNBTLibrary-${{ steps.version.outputs.value }}-dotnet8.zip" .) - # NuGet へは公開しないが、落としたファイルをローカルのソースにできる + # NuGetへは公開しないが、落としたファイルをローカルのソースにできる - name: C# (nupkg) run: | dotnet pack csharp/src/SpringNBTLibrary/SpringNBTLibrary.csproj \ @@ -113,14 +113,14 @@ jobs: mkdir -p artifacts cp rust/target/package/spring-nbt-library-*.crate artifacts/ - # 落としたファイルが壊れていないか、利用者が確かめられるようにする + # 落としたファイルが元のままか、利用者が確かめられるようにする - name: チェックサムを作る run: | rm -rf artifacts/csharp (cd artifacts && sha256sum * > SHA256SUMS.txt) ls -la artifacts - # CHANGELOG からその版の節を取り出し、案内文と合わせて本文にする + # CHANGELOGからその版の節を取り出し、案内文と合わせて本文にする # 節が無ければここで落ちるので、変更点を書き忘れたまま公開できない - name: リリース本文を組み立てる run: | diff --git a/.gitignore b/.gitignore index 450fa07..e1718a2 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,7 @@ # --- TypeScript / Node --- node_modules/ /typescript/dist/ -# npm pack が作る配布用の tarball +# npm packが作る配布用のtarball /typescript/*.tgz # --- Python --- diff --git a/CHANGELOG.md b/CHANGELOG.md index c5ecb84..99dfc12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ # 変更履歴 -このファイルの書式は [Keep a Changelog](https://keepachangelog.com/ja/1.1.0/) に従う。 +このファイルの書式は[Keep a Changelog](https://keepachangelog.com/ja/1.1.0/)に従う。 ## バージョン番号の付け方 -`x.y.z` の各桁は次を表す。 +`x.y.z`の各桁は次を表す。 | 桁 | 上がるとき | |---|---| @@ -14,97 +14,112 @@ 対応言語はすべて、常に同じバージョン番号で公開する。 +## [未リリース] + +### 修正 + +- Rust: ワールド経由でチャンクを読むと、`WorldOpenOptions`に渡した警告の通知先が呼ばれなかった +- Python: binary32の範囲を超える値を`NbtFloat`に渡すと`OverflowError`が出ていた。他の言語と同じく符号付きの無限大にする +- Python: 途中で切れたGZipや中身の壊れた圧縮データを読むと、`MALFORMED_DATA`ではなく標準の例外がそのまま出ていた +- Python / TypeScript: `TAG_Float`のシグナリングNaNを読んで書き戻すと、ビットパターンが変わっていた +- Python: SNBTの`"😀"`のように対になったサロゲートが、補助文字1文字と別の文字列として扱われていた +- SNBTの`\U`でサロゲートの範囲を書いたときの扱いが言語ごとに違っていた(C#は想定外の例外が漏れていた)。`\u`と同じく孤立サロゲートとして読む +- C# / Java / TypeScript / Python: SNBTやAPIで、孤立サロゲートを含むCompoundのキーを受け付けていた。書き出すと読み戻せないため、SNBTでは`MALFORMED_DATA`、APIでは`INVALID_ARGUMENT`にする +- TypeScript: `NbtList`の`get` / `set` / `insert` / `removeAt`が位置を検査せず、範囲外へ`set`すると配列が伸びていた。`RangeError`にする +- C# / Java / Python / TypeScript / Rust: リストの範囲外の位置へ`set` / `insert`して失敗したときに、空のリストの要素型が確定していた +- Rust: `NbtList::insert`で範囲外の位置を渡すとパニックしていた。`INVALID_ARGUMENT`にする +- 読み書きモードで存在しないリージョンフォルダを開くと、書き出しの時点でディレクトリが無く失敗していた。書き出すときにディレクトリを作る +- `level.dat`の保存で、一時ファイルの内容をディスクへ書き出してから置き換えるようにした(fsync) +- TypeScript / Rust: SNBTの`\N{…}`のエラーメッセージで波括弧が抜けていた + ## [1.0.0] -最初のリリース。対象は **26.1 で導入されたワールド形式**(DataVersion 4786 以降)。 -動作は Java版 26.2(DataVersion 4903)の実ワールドで確かめている。 +最初のリリース。対象は**26.1で導入されたワールド形式**(DataVersion 4786以降)。 +動作はJava版26.2(DataVersion 4903)の実ワールドで確かめている。 -対応言語は **C# / Java / TypeScript / Python / Rust**。 -標準ライブラリだけで動き、外部の依存は要らない(Rust の `flate2` を除く)。 +対応言語は**C# / Java / TypeScript / Python / Rust**。 +標準ライブラリだけで動き、外部の依存は要らない(Rustの`flate2`を除く)。 ### NBT [ガイド](docs/guide/01-nbt.md) / [仕様](docs/spec/10-nbt-binary.md) - 全13タグの読み書き -- MUTF-8 の文字列(`U+0000` の 2 バイト表現、補助文字のサロゲートペア表現、孤立サロゲートの保持) -- 圧縮 Gzip / Zlib / 無圧縮と、先頭バイトからの自動判定 -- Java 形式(名前付きルート)と Network 形式(1.20.2 以降の無名ルート) -- 挿入順を保持する `NbtCompound` -- 型付き取得子 `get_int` / `opt_int`。「キーが無い」と「型が違う」を区別する -- 型付き設定子 `set_int(key, 42)`。取得子と対になる -- 位置を指定した読み込み `read_bytes_at` / `read_bytes_all`。連なった NBT を順に読める -- 等値比較と深い複製。浮動小数点はビットパターンで、`NbtCompound` は挿入順も含めて比べる -- SNBT のパースと出力(1行 / 整形)。1.21.5 以降の拡張構文に対応 +- MUTF-8の文字列(`U+0000`の2バイト表現、補助文字のサロゲートペア表現、孤立サロゲートの保持) +- 圧縮Gzip / Zlib / 無圧縮と、先頭バイトからの自動判定 +- Java形式(名前付きルート)とNetwork形式(1.20.2以降の無名ルート) +- 挿入順を保持する`NbtCompound` +- 型付き取得子`get_int` / `opt_int`。「キーが無い」と「型が違う」を区別する +- 型付き設定子`set_int(key, 42)`。取得子と対になる +- 位置を指定した読み込み`read_bytes_at` / `read_bytes_all`。連なったNBTを順に読める +- 等値比較と深い複製。浮動小数点はビットパターンで、`NbtCompound`は挿入順も含めて比べる +- SNBTのパースと出力(1行 / 整形)。1.21.5以降の拡張構文に対応 - 浮動小数点の正準10進表記 -- 安全上限。ネスト深さ 512、宣言長の先行検証、展開後サイズの上限 +- 安全上限。ネスト深さ512、宣言長の先行検証、展開後サイズの上限 ### Anvil [ガイド](docs/guide/03-anvil-region.md) / [仕様](docs/spec/20-anvil-region.md) -- `.mca` リージョンファイルの読み書き +- `.mca`リージョンファイルの読み書き - 無変更で書き戻すと、原本とバイト単位で一致する - セクタの再配置と、解放したセクタの再利用 -- 圧縮ID 1 (GZip) / 2 (Zlib) / 3 (無圧縮) の読み書き -- 圧縮ID 4 (LZ4) の読み込み。書き込みは Zlib になる -- 外部ファイル `.mcc` への自動退避と復帰 +- 圧縮ID 1 (GZip) / 2 (Zlib) / 3 (無圧縮)の読み書き +- 圧縮ID 4 (LZ4)の読み込み。書き込みはZlibになる +- 外部ファイル`.mcc`への自動退避と復帰 - 生バイトでのチャンク入出力。扱えない圧縮方式でもそのまま移せる -- 断片化解消 `optimize()` +- 断片化解消`optimize()` - セクタ重複・不正オフセット・ファイル長の整列検査 -- `region/` `entities/` `poi/` の横断アクセス -- 同時に開くリージョンの上限管理(既定 8 件) +- `region/` `entities/` `poi/`の横断アクセス +- 同時に開くリージョンの上限管理(既定8件) ### World / Block [ガイド](docs/guide/05-blocks-and-biomes.md) / [仕様](docs/spec/30-chunk-format.md) -- `level.dat` の読み込みと、一時ファイル経由の安全な書き込み -- 26.x のディレクトリ構成(`dimensions/` への集約、`players/` への改名、`data/minecraft/*.dat` への分離) +- `level.dat`の読み込みと、一時ファイル経由の安全な書き込み +- 26.xのディレクトリ構成(`dimensions/`への集約、`players/`への改名、`data/minecraft/*.dat`への分離) - 標準3次元とカスタム次元の解決。次元IDは定数で参照できる - 絶対座標でのブロック・バイオームの取得と設定 -- ブロックは `BlockState` でも `"minecraft:oak_stairs[facing=north]"` の文字列でも置ける -- 置き換え時に、その座標を指す `block_entities` / `block_ticks` / `fluid_ticks` を取り除く -- ブロック座標 `BlockPos` と範囲 `Cuboid`。範囲内の座標を順に走査できる -- チャンクの変更フラグ `is_modified`。書き戻す対象を決める -- パレットの自動拡張とビット幅の再計算、未使用パレットの掃除 `compact()` -- 非正準な BitStorage の救済読み +- ブロックは`BlockState`でも`"minecraft:oak_stairs[facing=north]"`の文字列でも置ける +- 置き換え時に、その座標を指す`block_entities` / `block_ticks` / `fluid_ticks`を取り除く +- ブロック座標`BlockPos`と範囲`Cuboid`。範囲内の座標を順に走査できる +- チャンクの変更フラグ`is_modified`。書き戻す対象を決める +- パレットの自動拡張とビット幅の再計算、未使用パレットの掃除`compact()` +- 非正準なBitStorageの救済読み ### 共通 -- 全言語で一致する `ErrorCode` -- ワールド形式による対応判定。下限(26.1)以降なら警告を出さず、 - 書き戻すときも `DataVersion` を書き換えない([adr/0003](docs/adr/0003-version-policy.md)) +- 全言語で一致する`ErrorCode` +- ワールド形式による対応判定。下限(26.1)以降なら警告を出さず、書き戻すときも`DataVersion`を書き換えない([adr/0003](docs/adr/0003-version-policy.md)) - 扱えない形式を見つけたときの警告/エラー切り替え - クロス言語適合性検証(`spec/run-conformance.sh`) - 実ワールド走査ツール(`spec/tools/scan_world.py`) -- 実装から生成する [API 対応表](docs/api/overview.md) +- 実装から生成する[API対応表](docs/api/overview.md) ### 対応していないもの -- `Heightmaps` と光源の再計算(無効化のみ提供。[adr/0004](docs/adr/0004-defer-heightmap-recalc.md)) -- 圧縮ID 4 (LZ4) での書き込み、圧縮ID 127(カスタム方式)の展開 -- SNBT の異種リストと `\N{文字名}` エスケープ -- `entities` / `poi` の型付きAPI(生NBTとしてなら読み書きできる) +- `Heightmaps`と光源の再計算(無効化のみ提供。[adr/0004](docs/adr/0004-defer-heightmap-recalc.md)) +- 圧縮ID 4 (LZ4)での書き込み、圧縮ID 127(カスタム方式)の展開 +- SNBTの異種リストと`\N{文字名}`エスケープ +- `entities` / `poi`の型付きAPI(生NBTとしてなら読み書きできる) - チャンクの新規生成(ワールド生成は本ライブラリの範囲外) - 過去バージョンのワールド改変([adr/0003](docs/adr/0003-version-policy.md)) - Bedrock版(LevelDB形式) ### 言語ごとの差異 -言語の性質上どうしても揃えられない箇所は -[機能一覧の「言語ごとの差異」](docs/features.md#言語ごとの差異)にまとめてある。 +言語の性質上どうしても揃えられない箇所は、[機能一覧の「言語ごとの差異」](docs/features.md#言語ごとの差異)にまとめてある。 -- `session.lock` の確認は C# / Java / Python(POSIX) のみ([adr/0008](docs/adr/0008-session-lock.md)) -- TypeScript は `i64` に `bigint` を使う([adr/0007](docs/adr/0007-typescript-bigint.md)) -- TypeScript にストリーム入出力は無い。Node のストリームは非同期しか無いため -- Rust のみ例外ではなく `Result` を返す([adr/0005](docs/adr/0005-unified-error-model.md)) -- Rust の `NbtString` は 2 形態の列挙。孤立サロゲートを保持するため +- `session.lock`の確認はC# / Java / Python(POSIX)のみ([adr/0008](docs/adr/0008-session-lock.md)) +- TypeScriptは`i64`に`bigint`を使う([adr/0007](docs/adr/0007-typescript-bigint.md)) +- TypeScriptにストリーム入出力は無い。Nodeのストリームは非同期しか無いため +- Rustのみ例外ではなく`Result`を返す([adr/0005](docs/adr/0005-unified-error-model.md)) +- Rustの`NbtString`は2形態の列挙。孤立サロゲートを保持するため ### 検証 -- 各言語の単体テスト 888 件 -- クロス言語適合性 834 件(全言語で出力バイト列が一致) -- 実ワールド(Java版 26.2): `.dat` 23個 + チャンク 3,717個 すべてラウンドトリップ成功。 - World レイヤでも 3,481 チャンク・83,544 セクションの再エンコードが原本と一致し、 - 393万ブロックの読み出しに成功 +- 各言語の単体テスト888件 +- クロス言語適合性834件(全言語で出力バイト列が一致) +- 実ワールド(Java版26.2): `.dat` 23個 + チャンク3,717個すべてラウンドトリップ成功。 + Worldレイヤでも3,481チャンク・83,544セクションの再エンコードが原本と一致し、393万ブロックの読み出しに成功 diff --git a/README.md b/README.md index 61dabf0..999ee2b 100644 --- a/README.md +++ b/README.md @@ -4,30 +4,30 @@ [![lint](https://github.com/ScriptArts/SpringNBTLibrary/actions/workflows/lint.yml/badge.svg)](https://github.com/ScriptArts/SpringNBTLibrary/actions/workflows/lint.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -Minecraft Java版 26.1 以降のワールドデータ・NBTファイルを読み書きするライブラリです。 +Minecraft Java版26.1以降のワールドデータ・NBTファイルを読み書きするライブラリです。 複数の言語に、同じ概念モデルと同じ挙動のものを用意しています。 -いまのワールド形式は 26.1 で入ったものです。 +いまのワールド形式は26.1で入ったものです。 それ以降のバージョンなら、形式が変わらない限りそのまま扱えます。 -動作は 26.2 の実ワールドで確かめています。 +動作は26.2の実ワールドで確かめています。 ## 対応言語 -[Releases](https://github.com/ScriptArts/SpringNBTLibrary/releases) から落として、自分のプロジェクトに組み込んでください。 +[Releases](https://github.com/ScriptArts/SpringNBTLibrary/releases)から落として、自分のプロジェクトに組み込んでください。 | 言語 | 必要環境 | 入手するもの | 組み込み方 | |---|---|---|---| -| [C#](docs/getting-started/csharp.md) | .NET 8 / C# 12 | `SpringNBTLibrary.<版>.nupkg` | 置き場所を `nuget.config` で教えて `dotnet add package`(`.zip` で dll 直参照も可) | +| [C#](docs/getting-started/csharp.md) | .NET 8 / C# 12 | `SpringNBTLibrary.<版>.nupkg` | 置き場所を`nuget.config`で教えて`dotnet add package`(`.zip`でdll直参照も可) | | [Java](docs/getting-started/java.md) | Java 21 (LTS) | `spring-nbt-library-<版>.jar` | クラスパスへ追加 | | [TypeScript](docs/getting-started/typescript.md) | Node.js 20+ | `spring-nbt-library-<版>.tgz` | `npm install ./<ファイル>` | | [Python](docs/getting-started/python.md) | Python 3.10+ | `spring_nbt_library-<版>-py3-none-any.whl` | `pip install <ファイル>` | -| [Rust](docs/getting-started/rust.md) | Rust 1.75+ | — | `Cargo.toml` で git 参照 | +| [Rust](docs/getting-started/rust.md) | Rust 1.75+ | 不要(オフライン用に`spring-nbt-library-<版>.crate`もある) | `Cargo.toml`でgit参照(`.crate`は展開してパス参照) | -同梱の `SHA256SUMS.txt` でファイルの整合を確認できます。 +同梱の`SHA256SUMS.txt`でファイルの整合性を確認できます。 ## バージョン番号 -`x.y.z` の各桁は次を表します。 +`x.y.z`の各桁は次を表します。 | 桁 | 上がるとき | 例 | |---|---|---| @@ -37,7 +37,7 @@ Minecraft Java版 26.1 以降のワールドデータ・NBTファイルを読み 対応言語はすべて同じ番号で公開します。 -**Minecraft が更新されても、ワールドの保存形式が変わらなければ `x` は上がりません。** +**Minecraftが更新されても、ワールドの保存形式が変わらなければ`x`は上がりません。** 新しいバージョンのワールドはそのまま読み書きできます。 ## ドキュメント @@ -55,4 +55,4 @@ Minecraft Java版 26.1 以降のワールドデータ・NBTファイルを読み MIT License — Copyright (c) 2026 ScriptArts -詳細は [LICENSE](LICENSE) を参照。 +詳細は[LICENSE](LICENSE)を参照。 diff --git a/csharp/README.md b/csharp/README.md index 8f77d96..61ffc63 100644 --- a/csharp/README.md +++ b/csharp/README.md @@ -1,20 +1,19 @@ -# SpringNBTLibrary(C# 版) +# SpringNBTLibrary(C#版) -Minecraft Java版 26.1 以降のワールド・NBTファイルを読み書きするライブラリです。 -.NET 8 / C# 12 以上で動きます。 +Minecraft Java版26.1以降のワールド・NBTファイルを読み書きするライブラリです。 +.NET 8 / C# 12以上で動きます。 -いまのワールド形式は 26.1 で入ったものです。 +いまのワールド形式は26.1で入ったものです。 それ以降のバージョンなら、形式が変わらない限りそのまま扱えます。 -C# / Java / TypeScript / Python / Rust 版があり、どれも同じ仕様書から作っています。 +C# / Java / TypeScript / Python / Rust版があり、どれも同じ仕様書から作っています。 同じ入力からは同じ結果が出ます([対応言語の一覧](../README.md#対応言語))。 -揃っているかは[適合性テスト](../docs/spec/90-conformance.md)で毎回確かめています。 +結果が揃っているかは[適合性テスト](../docs/spec/90-conformance.md)で毎回確かめています。 ## 導入 -[Releases](https://github.com/ScriptArts/SpringNBTLibrary/releases) から -`SpringNBTLibrary.<版>.nupkg` を落とし、プロジェクト直下の `packages/` へ置きます。 -`.csproj` の隣に `nuget.config` を作ってソースを教えたら、参照に足せます。 +[Releases](https://github.com/ScriptArts/SpringNBTLibrary/releases)から`SpringNBTLibrary.<版>.nupkg`を落とし、プロジェクト直下の`packages/`へ置きます。 +`.csproj`の隣に`nuget.config`を作ってソースを教えたら、参照に足せます。 ```xml @@ -28,17 +27,17 @@ C# / Java / TypeScript / Python / Rust 版があり、どれも同じ仕様書 dotnet add package SpringNBTLibrary --version 1.0.0 ``` -dll を直接参照したいときは `SpringNBTLibrary-<版>-dotnet8.zip` のほうを使います。 -同梱の `.xml` を dll と同じ場所に置くと、IDE の補完に説明が出ます。 +dllを直接参照したいときは`SpringNBTLibrary-<版>-dotnet8.zip`のほうを使います。 +同梱の`.xml`をdllと同じ場所に置くと、IDEの補完に説明が出ます。 ## 使い方 -- [はじめに(C#)](../docs/getting-started/csharp.md) — まずここから -- [ガイド](../docs/guide/01-nbt.md) — 目的別の使い方 -- [API 対応表](../docs/api/overview.md) — 他言語版との対応 -- [機能一覧](../docs/features.md) — 何ができて何ができないか +- [はじめに(C#)](../docs/getting-started/csharp.md): まずここから +- [ガイド](../docs/guide/01-nbt.md): 目的別の使い方 +- [API対応表](../docs/api/overview.md): 他言語版との対応 +- [機能一覧](../docs/features.md): 何ができて何ができないか -説明は [`docs/`](../docs/README.md) にまとめてあります。 +説明は[`docs/`](../docs/README.md)にまとめてあります。 ## ライセンス diff --git a/csharp/src/SpringNBTLibrary/Anvil/ChunkCompression.cs b/csharp/src/SpringNBTLibrary/Anvil/ChunkCompression.cs index 71b85d4..0c6476a 100644 --- a/csharp/src/SpringNBTLibrary/Anvil/ChunkCompression.cs +++ b/csharp/src/SpringNBTLibrary/Anvil/ChunkCompression.cs @@ -6,10 +6,9 @@ namespace SpringNBTLibrary.Anvil; /// /// /// -/// NBT 層の とは別物であることに注意 -/// あちらはファイル全体の圧縮を表し、こちらはリージョン内の 1 チャンクに付く 1 バイトのIDを表す +/// NBT層のとは違うものであることに注意 +/// あちらはファイル全体の圧縮を表し、こちらはリージョン内の1チャンクに付く1バイトのIDを表す /// -/// 仕様: docs/spec/20-anvil-region.md 3.1章 /// public enum ChunkCompression { @@ -20,16 +19,17 @@ public enum ChunkCompression /// Zlib (RFC 1950) /// ID=2 - /// Minecraft が実際に書き出す方式 + /// Minecraftが実際に書き出す方式 Zlib = 2, /// 無圧縮 /// ID=3 None = 3, - /// LZ4(ブロック形式) + /// LZ4(独自ヘッダ付きのブロック連結) /// ID=4 - /// 任意依存 + /// 読み込みのみ対応 + /// 展開は自前で行い、外部の依存は使わない Lz4 = 4, /// サードパーティ製サーバのカスタム方式 @@ -38,7 +38,7 @@ public enum ChunkCompression Custom = 127, } -/// の拡張メソッド +/// の拡張メソッド public static class ChunkCompressionExtensions { /// 適合性テストで言語間比較に使う識別子を返す @@ -63,12 +63,12 @@ public static string AsString(this ChunkCompression compression) } /// - /// 圧縮方式IDから を得る + /// 圧縮方式IDからを得る /// /// 未知のIDの場合() public static ChunkCompression FromId(int id) { - // 仕様が定めるのは 1・2・3・4・127 の 5 種類だけ + // 仕様が定めるのは1・2・3・4・127の5種類だけ if (id == 1 || id == 2 || id == 3 || id == 4 || id == 127) { return (ChunkCompression)id; diff --git a/csharp/src/SpringNBTLibrary/Anvil/ChunkPos.cs b/csharp/src/SpringNBTLibrary/Anvil/ChunkPos.cs index fff45bb..1d1c6ef 100644 --- a/csharp/src/SpringNBTLibrary/Anvil/ChunkPos.cs +++ b/csharp/src/SpringNBTLibrary/Anvil/ChunkPos.cs @@ -5,7 +5,6 @@ namespace SpringNBTLibrary.Anvil; /// /// チャンクの絶対座標 /// -/// 仕様: docs/spec/20-anvil-region.md 1章 public readonly struct ChunkPos : IEquatable { /// 座標を指定して作る @@ -56,7 +55,7 @@ public override string ToString() => /// /// リージョンの座標 -/// 1リージョンは 32×32 チャンクを担当する +/// 1リージョンは32×32チャンクを担当する /// public readonly struct RegionPos : IEquatable { @@ -78,8 +77,8 @@ public RegionPos(int x, int z) string.Create(CultureInfo.InvariantCulture, $"r.{X}.{Z}.mca"); /// - /// r.X.Z.mca 形式のファイル名から座標を得る - /// 解釈できなければ null + /// r.X.Z.mca形式のファイル名から座標を得る + /// 解釈できなければnull /// public static RegionPos? FromFileName(string fileName) { @@ -87,7 +86,7 @@ public RegionPos(int x, int z) string[] parts = fileName.Split('.'); - // "r" "" "" "mca" の 4 つに分かれるはず + // "r" "" "" "mca"の4つに分かれるはず if (parts.Length != 4 || parts[0] != "r" || parts[3] != "mca") { return null; diff --git a/csharp/src/SpringNBTLibrary/Anvil/Lz4.cs b/csharp/src/SpringNBTLibrary/Anvil/Lz4.cs index 1deee20..deece04 100644 --- a/csharp/src/SpringNBTLibrary/Anvil/Lz4.cs +++ b/csharp/src/SpringNBTLibrary/Anvil/Lz4.cs @@ -3,35 +3,33 @@ namespace SpringNBTLibrary.Anvil; /// -/// 圧縮方式ID 4 (LZ4) のチャンクを展開する +/// 圧縮方式ID 4(LZ4)のチャンクを展開する /// /// /// -/// 素の LZ4 ブロックでも LZ4 フレーム形式でもなく、 -/// 独自ヘッダを持つブロックの連結である +/// 素のLZ4ブロックでもLZ4フレーム形式でもなく、独自ヘッダを持つブロックの連結である /// /// 書き込みには対応しない -/// 書き戻すときは Zlib になる -/// 仕様: docs/spec/20-anvil-region.md 3.1.1 / 3.1.2 +/// NBTとして書き戻すときは既定でZlibになり、生バイトのまま書き戻すとLZ4のまま残る /// internal static class Lz4 { - /// ブロックの先頭に必ず置かれる 8 バイト + /// ブロックの先頭に必ず置かれる8バイト private static readonly byte[] Magic = "LZ4Block"u8.ToArray(); /// ブロックヘッダの長さ private const int HeaderLength = 21; - /// トークン上位 4 ビット: 本体が無圧縮 + /// トークン上位4ビット: 本体が無圧縮 private const int MethodStored = 0x10; - /// トークン上位 4 ビット: 本体が LZ4 圧縮 + /// トークン上位4ビット: 本体がLZ4圧縮 private const int MethodCompressed = 0x20; /// マッチの最小長 private const int MinMatch = 4; - /// LZ4Block の連結を展開する + /// LZ4Blockの連結を展開する /// /// 形式に反する入力() /// @@ -49,7 +47,7 @@ internal static byte[] Decompress(byte[] payload) return output.ToArray(); } - /// ブロックを 1 つ展開し、次のブロックの開始位置を返す + /// ブロックを1つ展開し、次のブロックの開始位置を返す private static int DecompressBlock(byte[] payload, int position, MemoryStream output) { if (position + HeaderLength > payload.Length) @@ -58,7 +56,7 @@ private static int DecompressBlock(byte[] payload, int position, MemoryStream ou $"LZ4: ブロックヘッダが足りない({payload.Length - position} バイト)"); } - // マジックが違えばそもそも LZ4Block ではない + // マジックが違えばそもそもLZ4Blockではない for (int index = 0; index < Magic.Length; index++) { if (payload[position + index] != Magic[index]) @@ -83,7 +81,7 @@ private static int DecompressBlock(byte[] payload, int position, MemoryStream ou if (method == MethodStored) { - // 無圧縮なら 2 つの長さは一致していなければならない + // 無圧縮なら2つの長さは一致していなければならない if (compressedLength != originalLength) { throw SpringNbtException.Malformed( @@ -107,7 +105,7 @@ private static int DecompressBlock(byte[] payload, int position, MemoryStream ou return body + compressedLength; } - /// ヘッダに書かれた 2 つの長さが妥当か調べる + /// ヘッダに書かれた2つの長さが妥当か調べる private static void ValidateLengths(int compressedLength, int originalLength) { if (compressedLength < 0 || originalLength < 0) @@ -115,14 +113,14 @@ private static void ValidateLengths(int compressedLength, int originalLength) throw SpringNbtException.Malformed("LZ4: ブロックの長さが負値"); } - // 片方だけが 0 になることはない + // 片方だけが0になることはない if ((compressedLength == 0) != (originalLength == 0)) { throw SpringNbtException.Malformed("LZ4: ブロックの長さが片方だけ 0"); } } - /// 素の LZ4 ブロックを展開する + /// 素のLZ4ブロックを展開する private static byte[] DecompressRawBlock(ReadOnlySpan source, int originalLength) { byte[] output = new byte[originalLength]; @@ -137,7 +135,7 @@ private static byte[] DecompressRawBlock(ReadOnlySpan source, int original int literalLength = token >> 4; - // 15 なら追加バイトで長さが続く + // 15なら追加バイトで長さが続く if (literalLength == 15) { literalLength += ReadLength(source, ref input); @@ -166,7 +164,7 @@ private static byte[] DecompressRawBlock(ReadOnlySpan source, int original int matchLength = (token & 0x0F) + MinMatch; - // 下位 4 ビットが 15 なら追加バイトで長さが続く + // 下位4ビットが15なら追加バイトで長さが続く if ((token & 0x0F) == 15) { matchLength += ReadLength(source, ref input); @@ -184,12 +182,12 @@ private static byte[] DecompressRawBlock(ReadOnlySpan source, int original return output; } - /// 255 が続く形式の追加長さを読む + /// 255が続く形式の追加長さを読む private static int ReadLength(ReadOnlySpan source, ref int input) { int total = 0; - // 255 未満のバイトが出るまで足し続ける + // 255未満のバイトが出るまで足し続ける while (true) { if (input >= source.Length) @@ -237,7 +235,7 @@ private static void CopyMatch(byte[] output, ref int written, int offset, int le int from = written - offset; - // コピー元と先は重なりうるので 1 バイトずつ写す + // コピー元と先は重なりうるので1バイトずつ写す for (int index = 0; index < length; index++) { output[written + index] = output[from + index]; diff --git a/csharp/src/SpringNBTLibrary/Anvil/RawChunk.cs b/csharp/src/SpringNBTLibrary/Anvil/RawChunk.cs index 7bbeb10..73925ae 100644 --- a/csharp/src/SpringNBTLibrary/Anvil/RawChunk.cs +++ b/csharp/src/SpringNBTLibrary/Anvil/RawChunk.cs @@ -5,11 +5,9 @@ namespace SpringNBTLibrary.Anvil; /// /// /// -/// 本ライブラリが解釈できない圧縮方式(LZ4 未導入、カスタム方式)でも -/// これなら取り出せる +/// 本ライブラリが解釈できない圧縮方式(カスタム方式)のチャンクでも、これなら取り出せる /// バックアップや別ツールへの受け渡しに使う /// -/// 仕様: docs/spec/20-anvil-region.md 5章 /// public sealed class RawChunk { @@ -29,11 +27,11 @@ public RawChunk(ChunkCompression compression, byte[] data, bool external = false public byte[] Data { get; } /// - /// 外部ファイル c.X.Z.mcc に格納されていたか + /// 外部ファイルc.X.Z.mccに格納されていたか /// /// /// 書き込み時にこの値を指定する必要はない - /// サイズに応じて が自動的に判断する + /// サイズに応じてが自動的に判断する /// public bool External { get; } diff --git a/csharp/src/SpringNBTLibrary/Anvil/RegionFile.cs b/csharp/src/SpringNBTLibrary/Anvil/RegionFile.cs index 3a83488..c4b112a 100644 --- a/csharp/src/SpringNBTLibrary/Anvil/RegionFile.cs +++ b/csharp/src/SpringNBTLibrary/Anvil/RegionFile.cs @@ -9,7 +9,7 @@ namespace SpringNBTLibrary.Anvil; public enum RegionFileMode { /// 読み取り専用 - /// 書き込み系の操作はエラーになる + /// 書き込み系の操作はエラーになる(Flushは何もしない) ReadOnly, /// 読み書き @@ -18,17 +18,16 @@ public enum RegionFileMode } /// -/// Anvil のリージョンファイル (r.X.Z.mca) -/// 32×32 チャンクを格納する +/// Anvilのリージョンファイル (r.X.Z.mca) +/// 32×32チャンクを格納する /// /// /// /// ファイル全体をメモリに読み込んで扱う -/// 実データのリージョンは数 MB 程度で、 -/// この方が「触っていないチャンクのバイト配置をそのまま保つ」ことを保証しやすい -/// 開いて何も変えずに すると、バイト単位で元と同じファイルになる +/// 実データのリージョンは数MB程度で、この方が「触っていないチャンクのバイト配置をそのまま保つ」ことを保証しやすい +/// 開いて何も変えずにすると、バイト単位で元と同じファイルになる +/// ただし空のファイルは、8KiBのヘッダだけのファイルになる /// -/// 仕様: docs/spec/20-anvil-region.md /// public sealed class RegionFile : IDisposable { @@ -41,7 +40,7 @@ public sealed class RegionFile : IDisposable /// 1リージョンに入るチャンク数 private const int ChunkCount = 1024; - /// 1チャンクが確保できるセクタ数の上限(長さフィールドが u8 のため) + /// 1チャンクが確保できるセクタ数の上限(長さフィールドがu8のため) private const int MaxSectors = 255; /// リージョン内に収められるペイロードの上限 @@ -66,7 +65,7 @@ private RegionFile(string path, RegionFileMode mode, RegionPos position, byte[] this.data = data; string? parent = Path.GetDirectoryName(path); - // パスが階層を含まない場合、.mcc の置き場としてカレントディレクトリを使う + // パスが階層を含まない場合、.mccの置き場としてカレントディレクトリを使う if (string.IsNullOrEmpty(parent)) { this.directory = "."; @@ -91,7 +90,7 @@ private RegionFile(string path, RegionFileMode mode, RegionPos position, byte[] /// リージョンファイルを開く /// /// - /// r.X.Z.mca という名前のファイル + /// r.X.Z.mcaという名前のファイル /// 座標はファイル名から読み取る /// /// 読み取り専用か読み書きか @@ -144,7 +143,7 @@ public static RegionFile Open(string path, RegionFileMode mode = RegionFileMode. /// ヘッダを解析し、ロケーションとタイムスタンプを取り込む private void ParseHeader() { - // 空ファイルは「チャンクが 1 つも無いリージョン」として受け入れる + // 空ファイルは「チャンクが1つも無いリージョン」として受け入れる if (data.Length == 0) { data = new byte[HeaderSectors * SectorSize]; @@ -166,7 +165,7 @@ private void ParseHeader() int totalSectors = data.Length / SectorSize; Dictionary sectorOwner = new Dictionary(); - // ロケーションテーブルの 1024 エントリを順に取り込む + // ロケーションテーブルの1024エントリを順に取り込む for (int index = 0; index < ChunkCount; index++) { uint entry = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(index * 4, 4)); @@ -199,7 +198,7 @@ private void ParseHeader() $"チャンク {index} の割り当てがファイル外へはみ出している"); } - // 同じセクタを 2 つのチャンクが指していたら、どちらかが壊れている + // 同じセクタを2つのチャンクが指していたら、どちらかがおかしくなっている for (int sector = offset; sector < offset + count; sector++) { if (sectorOwner.TryGetValue(sector, out int owner)) @@ -260,7 +259,7 @@ public IEnumerable ChunkPositions() { EnsureOpen(); - // 添字の昇順に走査する(localZ が外、localX が内) + // 添字の昇順に走査する(localZが外、localXが内) for (int index = 0; index < ChunkCount; index++) { if (sectorCounts[index] == 0) @@ -274,8 +273,8 @@ public IEnumerable ChunkPositions() } } - /// チャンクの最終更新時刻(Unix 秒) - /// 存在しなければ 0 + /// チャンクの最終更新時刻(Unix秒) + /// タイムスタンプテーブルの値をそのまま返すため、チャンクが無くても0とは限らない public int Timestamp(int chunkX, int chunkZ) { EnsureOpen(); @@ -293,9 +292,9 @@ public void SetTimestamp(int chunkX, int chunkZ, int value) /// /// チャンクを圧縮されたまま取り出す - /// 存在しなければ null + /// 存在しなければnull /// - /// 格納内容が壊れている場合 + /// 格納内容がおかしくなっている場合 public RawChunk? ReadChunkRaw(int chunkX, int chunkZ) { EnsureOpen(); @@ -329,7 +328,7 @@ public void SetTimestamp(int chunkX, int chunkZ, int value) if (external) { - // 最上位ビットが立っている場合、本体は c.X.Z.mcc にある + // 最上位ビットが立っている場合、本体はc.X.Z.mccにある byte[] payload = ReadExternalFile(chunkX, chunkZ); return new RawChunk(compression, payload, external: true); } @@ -339,11 +338,11 @@ public void SetTimestamp(int chunkX, int chunkZ, int value) } /// - /// チャンクを NBT として読む - /// 存在しなければ null + /// チャンクをNBTとして読む + /// 存在しなければnull /// /// - /// 対応していない圧縮方式、または NBT として壊れている場合 + /// 対応していない圧縮方式、またはNBTとしておかしくなっている場合 /// public NbtCompound? ReadChunk(int chunkX, int chunkZ) { @@ -359,14 +358,14 @@ public void SetTimestamp(int chunkX, int chunkZ, int value) return NbtIo.ReadBytes(plain, options).Tag; } - /// チャンクを NBT として書き込む - /// 圧縮方式は Zlib + /// チャンクをNBTとして書き込む + /// 圧縮方式はZlib public void WriteChunk(int chunkX, int chunkZ, NbtCompound tag) { WriteChunk(chunkX, chunkZ, tag, ChunkCompression.Zlib); } - /// チャンクを NBT として、圧縮方式を指定して書き込む + /// チャンクをNBTとして、圧縮方式を指定して書き込む public void WriteChunk(int chunkX, int chunkZ, NbtCompound tag, ChunkCompression compression) { ArgumentNullException.ThrowIfNull(tag); @@ -391,7 +390,7 @@ public void WriteChunkRaw(int chunkX, int chunkZ, RawChunk raw) if (useExternal) { - // 1MiB を超えるチャンクは外部ファイルへ退避し、リージョンには目印だけ残す + // 255セクタ(約1MiB)に収まらないチャンクは外部ファイルへ退避し、リージョンには目印だけ残す WriteExternalFile(chunkX, chunkZ, raw.Data); payload = Array.Empty(); schemeByte = (int)raw.Compression | 0x80; @@ -429,7 +428,7 @@ public void WriteChunkRaw(int chunkX, int chunkZ, RawChunk raw) } /// チャンクを削除する - /// 削除できたら true + /// 削除できたらtrue public bool DeleteChunk(int chunkX, int chunkZ) { EnsureOpen(); @@ -454,7 +453,7 @@ public bool DeleteChunk(int chunkX, int chunkZ) /// 必要なセクタ数を確保し、開始セクタ番号を返す /// /// - /// 既存の割り当てがちょうど同じ大きさならその場を使い、 + /// 既存の割り当てがちょうど同じ大きさなら、その場所をそのまま使う /// そうでなければ先頭から空き領域を探し、無ければ末尾へ追加する /// private int AllocateSectors(int index, int needed) @@ -498,14 +497,14 @@ private int AllocateSectors(int index, int needed) /// /// セクタの使用状況を作る - /// のチャンクは空きとして扱う + /// のチャンクは空きとして扱う /// private bool[] BuildSectorUsage(int ignoreIndex) { int totalSectors = data.Length / SectorSize; bool[] used = new bool[totalSectors]; - // ヘッダの 2 セクタは常に使用中 + // ヘッダの2セクタは常に使用中 for (int sector = 0; sector < HeaderSectors && sector < totalSectors; sector++) { used[sector] = true; @@ -616,6 +615,8 @@ public void Flush() } WriteHeader(); + // 読み書きで開いたフォルダがまだ無ければ、書き出す前に作る + EnsureDirectory(); try { @@ -642,10 +643,10 @@ public byte[] ToBytes() return (byte[])data.Clone(); } - /// ロケーションテーブルとタイムスタンプテーブルを先頭 2 セクタへ書き戻す + /// ロケーションテーブルとタイムスタンプテーブルを先頭2セクタへ書き戻す private void WriteHeader() { - // 位置表とタイムスタンプ表を、添字順に組み立て直す + // ロケーションテーブルとタイムスタンプテーブルを、添字順に組み立て直す for (int index = 0; index < ChunkCount; index++) { uint entry = ((uint)offsets[index] << 8) | (uint)sectorCounts[index]; @@ -699,9 +700,33 @@ private byte[] ReadExternalFile(int chunkX, int chunkZ) } } + /// + /// ファイルを置くディレクトリが無ければ作る + /// + /// + /// 読むだけの操作で空のディレクトリができないよう、書き出す直前にだけ呼ぶ + /// + private void EnsureDirectory() + { + try + { + System.IO.Directory.CreateDirectory(directory); + } + catch (IOException error) + { + throw new SpringNbtException(ErrorCode.Io, $"ディレクトリを作れない: {directory}", error); + } + catch (UnauthorizedAccessException error) + { + throw new SpringNbtException(ErrorCode.Io, $"ディレクトリを作れない: {directory}", error); + } + } + private void WriteExternalFile(int chunkX, int chunkZ, byte[] payload) { string external = ExternalPath(chunkX, chunkZ); + // 読み書きで開いたフォルダがまだ無ければ、書き出す前に作る + EnsureDirectory(); try { @@ -771,7 +796,7 @@ internal static byte[] Compress(byte[] plain, ChunkCompression compression) using MemoryStream destination = new MemoryStream(); - // using を閉じてフッタを書かせてから ToArray する必要がある + // usingを閉じてフッタを書かせてからToArrayする必要がある using (Stream encoder = CreateEncoder(destination, compression)) { encoder.Write(plain, 0, plain.Length); diff --git a/csharp/src/SpringNBTLibrary/Anvil/RegionFolder.cs b/csharp/src/SpringNBTLibrary/Anvil/RegionFolder.cs index 59c0561..7c22721 100644 --- a/csharp/src/SpringNBTLibrary/Anvil/RegionFolder.cs +++ b/csharp/src/SpringNBTLibrary/Anvil/RegionFolder.cs @@ -3,34 +3,30 @@ namespace SpringNBTLibrary.Anvil; /// -/// リージョンファイルが並ぶディレクトリ 1 つ分(region/、entities/、poi/ のいずれか) +/// リージョンファイルが並ぶディレクトリ1つ分(region/、entities/、poi/のいずれか) /// /// /// -/// 開いたリージョンファイルはキャッシュし、 でまとめて閉じる +/// 開いたリージョンファイルはキャッシュし、でまとめて閉じる /// チャンク座標からリージョンを解決するので、利用側はリージョンの存在を意識しなくてよい /// /// -/// はファイル全体をメモリへ載せるため、キャッシュには -/// 件の上限がある -/// 上限を超えると、最も長く使われていない -/// ものから書き出して閉じる +/// はファイル全体をメモリへ載せるため、キャッシュには件の上限がある +/// 上限を超えると、最も長く使われていないものから書き出して閉じる /// 大きなワールドを端から走査してもメモリを使い切らない /// /// -/// このため が返した参照は、 -/// **別のリージョンへアクセスすると閉じられている場合がある** +/// このためが返した参照は、**別のリージョンへアクセスすると閉じられている場合がある** /// 参照を保持せず、必要なたびに取得すること /// -/// 仕様: docs/spec/20-anvil-region.md 5章 /// public sealed class RegionFolder : IDisposable { /// 同時に開いておくリージョンファイル数の既定の上限 /// - /// 1 リージョンは最大 255 セクタ × 1024 チャンク=理論上 1GiB になりうる - /// 実データでは数 MB から数十 MB 程度 - /// 8 件なら通常のワールドで数百 MB に収まる + /// 1リージョンは最大255セクタ × 1024チャンク=理論上1GiBになりうる + /// 実データでは数MBから数十MB程度 + /// 8件なら通常のワールドで数百MBに収まる /// public const int DefaultMaxCachedRegions = 8; @@ -66,13 +62,14 @@ private RegionFolder(string directory, RegionFileMode mode, int maxCachedRegions /// リージョンフォルダを開く /// /// - /// 読み取り専用でディレクトリが存在しない場合() + /// 読み取り専用でディレクトリが存在しない場合()、 + /// または上限が1未満の場合() /// /// リージョンファイルが並ぶディレクトリ /// 読み取り専用か、読み書きか /// /// 同時に開いておくリージョンファイル数の上限 - /// 既定は + /// 既定は /// public static RegionFolder Open( string directory, @@ -101,7 +98,7 @@ public IEnumerable RegionPositions() { EnsureOpen(); - // フォルダがまだ無いなら、リージョンは 1 つも無い + // フォルダがまだ無いなら、リージョンは1つも無い if (!System.IO.Directory.Exists(Directory)) { yield break; @@ -109,12 +106,12 @@ public IEnumerable RegionPositions() List found = new List(); - // r.X.Z.mca として解釈できるファイルだけを拾う + // r.X.Z.mcaとして解釈できるファイルだけを拾う foreach (string path in System.IO.Directory.EnumerateFiles(Directory, "r.*.mca")) { RegionPos? position = RegionPos.FromFileName(Path.GetFileName(path)); - // r.X.Z.mca として解釈できるファイルだけを拾う + // r.X.Z.mcaとして解釈できるファイルだけを拾う if (position is not null) { found.Add(position.Value); @@ -134,7 +131,7 @@ public IEnumerable RegionPositions() return left.X.CompareTo(right.X); }); - // 座標順に並べたものを、1 件ずつ返す + // 座標順に並べたものを、1件ずつ返す foreach (RegionPos position in found) { yield return position; @@ -143,7 +140,7 @@ public IEnumerable RegionPositions() /// /// リージョンファイルを取得する - /// 読み取り専用で存在しなければ null + /// 読み取り専用で存在しなければnull /// public RegionFile? Region(int regionX, int regionZ) { @@ -159,7 +156,7 @@ public IEnumerable RegionPositions() string path = Path.Combine(Directory, position.FileName); - // 読み取り専用では、存在しないリージョンは「チャンクが無い」として null を返す + // 読み取り専用では、存在しないリージョンは「チャンクが無い」としてnullを返す if (!File.Exists(path) && mode == RegionFileMode.ReadOnly) { return null; @@ -189,7 +186,7 @@ private void Touch(RegionPos position) recentlyUsedNodes[position] = recentlyUsed.AddLast(position); } - /// 新しく 1 件開けるよう、上限を下回るまで古いものを閉じる + /// 新しく1件開けるよう、上限を下回るまで古いものを閉じる private void EvictUntilBelowLimit() { // 上限に達している間、いちばん長く使っていないものから閉じる @@ -223,8 +220,8 @@ public bool HasChunk(int chunkX, int chunkZ) return file.HasChunk(chunkX, chunkZ); } - /// チャンクを NBT として読む - /// 存在しなければ null + /// チャンクをNBTとして読む + /// 存在しなければnull public NbtCompound? ReadChunk(int chunkX, int chunkZ) { RegionPos region = new ChunkPos(chunkX, chunkZ).Region; @@ -238,7 +235,7 @@ public bool HasChunk(int chunkX, int chunkZ) return file.ReadChunk(chunkX, chunkZ); } - /// チャンクを NBT として書き込む + /// チャンクをNBTとして書き込む public void WriteChunk(int chunkX, int chunkZ, NbtCompound tag) { RegionPos region = new ChunkPos(chunkX, chunkZ).Region; @@ -254,7 +251,7 @@ public void WriteChunk(int chunkX, int chunkZ, NbtCompound tag) } /// チャンクを削除する - /// 削除できたら true + /// 削除できたらtrue public bool DeleteChunk(int chunkX, int chunkZ) { RegionPos region = new ChunkPos(chunkX, chunkZ).Region; @@ -281,7 +278,7 @@ public IEnumerable ChunkPositions() continue; } - // リージョンごとに、その中のチャンク座標を順に返す + // そのリージョンにあるチャンクの座標を順に返す foreach (ChunkPos chunk in file.ChunkPositions()) { yield return chunk; diff --git a/csharp/src/SpringNBTLibrary/ErrorCode.cs b/csharp/src/SpringNBTLibrary/ErrorCode.cs index c918e27..ee3a2e3 100644 --- a/csharp/src/SpringNBTLibrary/ErrorCode.cs +++ b/csharp/src/SpringNBTLibrary/ErrorCode.cs @@ -2,9 +2,8 @@ namespace SpringNBTLibrary; /// /// エラーの分類 -/// 4言語すべてで同一の集合を持つ +/// 全言語で同一の集合を持つ /// -/// 仕様: docs/spec/00-conventions.md 4章 public enum ErrorCode { /// 下位の入出力失敗 @@ -16,7 +15,7 @@ public enum ErrorCode /// 期待した型と違うタグを取り出した UnexpectedTagType, - /// 仕様上は妥当だが、このビルドでは扱えない + /// 仕様上は妥当だが、このライブラリでは扱えない UnsupportedFeature, /// 安全上限を超えた @@ -25,11 +24,11 @@ public enum ErrorCode /// 呼び出し側の引数が不正 InvalidArgument, - /// 対象バージョン外のデータ + /// 扱える形式より古いデータ UnsupportedDataVersion, } -/// の拡張メソッド +/// の拡張メソッド public static class ErrorCodeExtensions { /// diff --git a/csharp/src/SpringNBTLibrary/Nbt/CanonicalDecimal.cs b/csharp/src/SpringNBTLibrary/Nbt/CanonicalDecimal.cs index 3e74631..a0ea748 100644 --- a/csharp/src/SpringNBTLibrary/Nbt/CanonicalDecimal.cs +++ b/csharp/src/SpringNBTLibrary/Nbt/CanonicalDecimal.cs @@ -9,11 +9,9 @@ namespace SpringNBTLibrary.Nbt; /// /// /// 各言語の標準の数値書式は互いに一致しない -/// 指数表記へ切り替わる閾値も、 -/// 指数部の桁数も、E の大文字小文字も処理系ごとに違う -/// そのままでは SNBT 出力の言語間一致が成立しないため、書式をここで固定する +/// 指数表記へ切り替わる閾値も、指数部の桁数も、Eの大文字小文字も処理系ごとに違う +/// そのままではSNBTの出力が言語間で一致しないため、書式をここで固定する /// -/// 仕様: docs/spec/11-snbt.md 5.1章 /// internal static class CanonicalDecimal { @@ -23,7 +21,7 @@ internal static class CanonicalDecimal /// 固定小数点表記を使う10進指数の上限 private const int MaxFixedExponent = 16; - /// binary32 を正準10進表記へ変換する + /// binary32を正準10進表記へ変換する internal static string FromFloat(float value) { if (float.IsNaN(value)) @@ -41,7 +39,7 @@ internal static string FromFloat(float value) return "-Infinity"; } - // 有効数字を 1 桁ずつ増やし、読み戻してビット一致する最短の表記を探す + // 有効数字を1桁ずつ増やし、読み戻したときにビットが一致する最短の表記を探す for (int precision = 1; precision <= 9; precision++) { string candidate = value.ToString( @@ -56,11 +54,11 @@ internal static string FromFloat(float value) } } - // 9 桁あれば binary32 は必ず往復するので、ここへは来ない + // 9桁あればbinary32は必ず往復するので、ここへは来ない return Format(value.ToString("E8", CultureInfo.InvariantCulture)); } - /// binary64 を正準10進表記へ変換する + /// binary64を正準10進表記へ変換する internal static string FromDouble(double value) { if (double.IsNaN(value)) @@ -78,7 +76,7 @@ internal static string FromDouble(double value) return "-Infinity"; } - // 有効数字を 1 桁ずつ増やし、読み戻してビット一致する最短の表記を探す + // 有効数字を1桁ずつ増やし、読み戻したときにビットが一致する最短の表記を探す for (int precision = 1; precision <= 17; precision++) { string candidate = value.ToString( @@ -93,12 +91,12 @@ internal static string FromDouble(double value) } } - // 17 桁あれば binary64 は必ず往復するので、ここへは来ない + // 17桁あればbinary64は必ず往復するので、ここへは来ない return Format(value.ToString("E16", CultureInfo.InvariantCulture)); } /// - /// 指数表記の文字列(例 "7.5E-001")から、仕様が定める正準表記を組み立てる + /// 指数表記の文字列(例"7.5E-001")から、仕様が定める正準表記を組み立てる /// private static string Format(string exponential) { @@ -115,7 +113,7 @@ private static string Format(string exponential) // 仮数部の数字だけを集める StringBuilder digitsBuilder = new StringBuilder(); - // 指数部 (E) の手前までが仮数部 + // 指数部 (E)の手前までが仮数部 while (index < exponential.Length && exponential[index] != 'E' && exponential[index] != 'e') { @@ -139,7 +137,7 @@ private static string Format(string exponential) } /// 末尾のゼロを取り除く - /// すべてゼロなら "0" を残す + /// すべてゼロなら"0"を残す private static string TrimTrailingZeros(string digits) { int end = digits.Length; @@ -164,7 +162,7 @@ private static string Compose(bool negative, string digits, int exponent) builder.Append('-'); } - // 値が 0 のときは指数に関わらず 0.0 と書く + // 値が0のときは指数に関わらず0.0と書く if (digits == "0") { builder.Append("0.0"); @@ -177,7 +175,7 @@ private static string Compose(bool negative, string digits, int exponent) builder.Append(digits[0]); builder.Append('.'); - // 2 桁目以降があれば小数点のうしろへ回す + // 2桁目以降があれば小数点のうしろへ回す if (digits.Length > 1) { builder.Append(digits, 1, digits.Length - 1); @@ -194,8 +192,8 @@ private static string Compose(bool negative, string digits, int exponent) if (exponent >= 0) { - // 整数部は先頭 (exponent + 1) 桁 - // 足りなければゼロで右詰めする + // 整数部は先頭 (exponent + 1)桁 + // 足りなければ数字の右側をゼロで埋める int integerDigits = exponent + 1; // 整数部が数字の並びに収まるなら、そのまま切り出す @@ -207,7 +205,7 @@ private static string Compose(bool negative, string digits, int exponent) { builder.Append(digits); - // 数字が足りない分は 0 で埋めて桁を合わせる + // 数字が足りない分は0で埋めて桁を合わせる for (int i = digits.Length; i < integerDigits; i++) { builder.Append('0'); @@ -229,10 +227,10 @@ private static string Compose(bool negative, string digits, int exponent) return builder.ToString(); } - // 指数が負なら "0." に続けてゼロを詰めてから数字を置く + // 指数が負なら"0."に続けてゼロを詰めてから数字を置く builder.Append("0."); - // 指数のぶんだけ 0.000... と 0 を並べる + // 指数の絶対値より1個少ない0を並べ、0.000...の形にする for (int i = 0; i < (-exponent) - 1; i++) { builder.Append('0'); diff --git a/csharp/src/SpringNBTLibrary/Nbt/Mutf8.cs b/csharp/src/SpringNBTLibrary/Nbt/Mutf8.cs index c4018c1..364d557 100644 --- a/csharp/src/SpringNBTLibrary/Nbt/Mutf8.cs +++ b/csharp/src/SpringNBTLibrary/Nbt/Mutf8.cs @@ -3,47 +3,45 @@ namespace SpringNBTLibrary.Nbt; /// -/// Modified UTF-8 (MUTF-8) の符号化・復号 +/// Modified UTF-8 (MUTF-8)の符号化・復号 /// /// -/// 標準 UTF-8 との違いは 2 点だけ +/// 標準UTF-8との違いは2点だけ /// -/// U+0000 を C0 80 の 2 バイトで表す -/// U+10000 以上をサロゲートペアへ分解し、3 バイト × 2 で表す (CESU-8) +/// U+0000をC0 80の2バイトで表す +/// U+10000以上をサロゲートペアへ分解し、3バイト × 2で表す (CESU-8) /// /// -/// C# の は UTF-16 コード単位の列なので、 -/// サロゲートペアも孤立サロゲートもそのまま保持できる +/// C#のはUTF-16コード単位の列なので、サロゲートペアも孤立サロゲートもそのまま保持できる /// -/// 仕様: docs/spec/10-nbt-binary.md 2章 /// public static class Mutf8 { - /// MUTF-8 の文字列が取りうる最大バイト長(長さフィールドが u16 のため) + /// MUTF-8の文字列が取りうる最大バイト長(長さフィールドがu16のため) public const int MaxByteLength = 65535; /// - /// MUTF-8 バイト列を文字列へ復号する + /// MUTF-8バイト列を文字列へ復号する /// /// - /// バイト列が MUTF-8 として不正な場合() + /// バイト列がMUTF-8として不正な場合() /// public static string Decode(ReadOnlySpan bytes) { StringBuilder builder = new StringBuilder(bytes.Length); int i = 0; - // 先頭から 1 文字ずつ取り出す + // 先頭から1文字ずつ取り出す while (i < bytes.Length) { byte b0 = bytes[i]; if ((b0 & 0x80) == 0x00) { - // 1 バイト形式: 0xxxxxxx (U+0001..U+007F) + // 1バイト形式: 0xxxxxxx (U+0001..U+007F) if (b0 == 0x00) { - // 素の 0x00 は MUTF-8 では現れてはならない (C0 80 を使う) + // 素の0x00はMUTF-8では現れてはならない (C0 80を使う) throw SpringNbtException.Malformed("MUTF-8: 素の 0x00 が現れた (U+0000 は C0 80 で表す)"); } @@ -52,7 +50,7 @@ public static string Decode(ReadOnlySpan bytes) } else if ((b0 & 0xE0) == 0xC0) { - // 2 バイト形式: 110xxxxx 10xxxxxx + // 2バイト形式: 110xxxxx 10xxxxxx if (i + 1 >= bytes.Length) { throw SpringNbtException.Malformed("MUTF-8: 2バイト形式が途中で切れた"); @@ -66,8 +64,8 @@ public static string Decode(ReadOnlySpan bytes) int value = ((b0 & 0x1F) << 6) | (b1 & 0x3F); - // C0 80 (U+0000) だけは正当 - // それ以外の 0x80 未満は冗長符号化 + // C0 80 (U+0000)だけは正当 + // それ以外の0x80未満は冗長符号化 if (value < 0x80 && !(b0 == 0xC0 && b1 == 0x80)) { throw SpringNbtException.Malformed("MUTF-8: 冗長な2バイト符号化"); @@ -78,7 +76,7 @@ public static string Decode(ReadOnlySpan bytes) } else if ((b0 & 0xF0) == 0xE0) { - // 3 バイト形式: 1110xxxx 10xxxxxx 10xxxxxx + // 3バイト形式: 1110xxxx 10xxxxxx 10xxxxxx if (i + 2 >= bytes.Length) { throw SpringNbtException.Malformed("MUTF-8: 3バイト形式が途中で切れた"); @@ -93,7 +91,7 @@ public static string Decode(ReadOnlySpan bytes) int value = ((b0 & 0x0F) << 12) | ((b1 & 0x3F) << 6) | (b2 & 0x3F); - // 3 バイトで表すべき範囲は U+0800 以上 + // 3バイトで表すべき範囲はU+0800以上 if (value < 0x800) { throw SpringNbtException.Malformed("MUTF-8: 冗長な3バイト符号化"); @@ -104,7 +102,7 @@ public static string Decode(ReadOnlySpan bytes) } else { - // 4 バイト形式 (標準 UTF-8) や継続バイト単独は MUTF-8 では不正 + // 4バイト形式 (標準UTF-8)や継続バイト単独はMUTF-8では不正 throw SpringNbtException.Malformed($"MUTF-8: 不正な先頭バイト 0x{b0:X2}"); } } @@ -113,22 +111,21 @@ public static string Decode(ReadOnlySpan bytes) } /// - /// 文字列を MUTF-8 バイト列へ符号化する + /// 文字列をMUTF-8バイト列へ符号化する /// /// - /// サロゲートは対になっているかどうかに関わらず 1 つずつ 3 バイトで符号化されるため、 - /// 孤立サロゲートもそのまま往復できる + /// サロゲートは対になっているかどうかに関わらず1つずつ3バイトで符号化されるため、孤立サロゲートもそのまま往復できる /// public static byte[] Encode(string text) { byte[] buffer = new byte[ByteLength(text)]; int position = 0; - // コード単位ごとに 1〜3 バイトへ展開する + // コード単位ごとに1〜3バイトへ展開する foreach (char unit in text) { - // U+0001..U+007F だけが 1 バイト - // U+0000 は 2 バイトになる + // U+0001..U+007Fだけが1バイト + // U+0000は2バイトになる if (unit >= 0x0001 && unit <= 0x007F) { buffer[position] = (byte)unit; @@ -136,7 +133,7 @@ public static byte[] Encode(string text) } else if (unit == 0x0000 || unit <= 0x07FF) { - // U+0000 もこの経路で C0 80 になる + // U+0000もこの経路でC0 80になる buffer[position] = (byte)(0xC0 | ((unit >> 6) & 0x1F)); buffer[position + 1] = (byte)(0x80 | (unit & 0x3F)); position += 2; @@ -154,7 +151,7 @@ public static byte[] Encode(string text) } /// - /// 文字列を MUTF-8 で符号化したときのバイト長を求める + /// 文字列をMUTF-8で符号化したときのバイト長を求める /// 実際に符号化はしない /// public static int ByteLength(string text) @@ -164,8 +161,8 @@ public static int ByteLength(string text) // 各コード単位が何バイトになるかを数える foreach (char unit in text) { - // U+0001..U+007F だけが 1 バイト - // U+0000 は 2 バイトになる + // U+0001..U+007Fだけが1バイト + // U+0000は2バイトになる if (unit >= 0x0001 && unit <= 0x007F) { length += 1; @@ -182,4 +179,37 @@ public static int ByteLength(string text) return length; } + + /// 対になっていないサロゲートを含むか + /// + /// 正しいサロゲートペアは1文字として数え、孤立したものだけを探す + /// Compoundのキーには孤立サロゲートを許さないので、その検査に使う + /// + internal static bool HasLoneSurrogate(string text) + { + // コード単位を1つずつ見て、サロゲート対をまとめる + for (int index = 0; index < text.Length; index++) + { + char c = text[index]; + + // 上位サロゲートは、対になる下位サロゲートとまとめて1文字を成す + if (char.IsHighSurrogate(c)) + { + // 対が揃っていなければ孤立サロゲート + if (index + 1 >= text.Length || !char.IsLowSurrogate(text[index + 1])) + { + return true; + } + + index += 1; + } + else if (char.IsLowSurrogate(c)) + { + // 上位サロゲートを伴わない下位サロゲートは孤立している + return true; + } + } + + return false; + } } diff --git a/csharp/src/SpringNBTLibrary/Nbt/NbtArrays.cs b/csharp/src/SpringNBTLibrary/Nbt/NbtArrays.cs index eaaac65..dd04867 100644 --- a/csharp/src/SpringNBTLibrary/Nbt/NbtArrays.cs +++ b/csharp/src/SpringNBTLibrary/Nbt/NbtArrays.cs @@ -1,11 +1,11 @@ namespace SpringNBTLibrary.Nbt; /// TAG_Byte_Array -/// 8bit 符号付き整数の配列 +/// 8bit符号付き整数の配列 public sealed class NbtByteArray : NbtTag { /// 配列を指定して作る - /// 渡した配列をそのまま保持する(コピーしない) + /// 渡した配列をコピーせずにそのまま保持する public NbtByteArray(sbyte[] value) { ArgumentNullException.ThrowIfNull(value); @@ -35,11 +35,11 @@ public override bool Equals(object? obj) } /// TAG_Int_Array -/// 32bit 符号付き整数の配列 +/// 32bit符号付き整数の配列 public sealed class NbtIntArray : NbtTag { /// 配列を指定して作る - /// 渡した配列をそのまま保持する(コピーしない) + /// 渡した配列をコピーせずにそのまま保持する public NbtIntArray(int[] value) { ArgumentNullException.ThrowIfNull(value); @@ -69,11 +69,11 @@ public override bool Equals(object? obj) } /// TAG_Long_Array -/// 64bit 符号付き整数の配列 +/// 64bit符号付き整数の配列 public sealed class NbtLongArray : NbtTag { /// 配列を指定して作る - /// 渡した配列をそのまま保持する(コピーしない) + /// 渡した配列をコピーせずにそのまま保持する public NbtLongArray(long[] value) { ArgumentNullException.ThrowIfNull(value); diff --git a/csharp/src/SpringNBTLibrary/Nbt/NbtBinaryReader.cs b/csharp/src/SpringNBTLibrary/Nbt/NbtBinaryReader.cs index d75934e..d25e03f 100644 --- a/csharp/src/SpringNBTLibrary/Nbt/NbtBinaryReader.cs +++ b/csharp/src/SpringNBTLibrary/Nbt/NbtBinaryReader.cs @@ -3,15 +3,14 @@ namespace SpringNBTLibrary.Nbt; /// -/// 展開済みのバイト列から NBT を読み出す +/// 展開済みのバイト列からNBTを読み出す /// /// /// /// 入力全体をあらかじめメモリに持つ設計にしている /// 「宣言された長さが残り入力長を超えていないか」を確保前に検査できるようにするため -/// これがないと、長さ 0x7FFFFFFF を宣言しただけの数バイトの入力でメモリを枯渇させられる +/// これがないと、長さ0x7FFFFFFFを宣言しただけの数バイトの入力でメモリを枯渇させられる /// -/// 仕様: docs/spec/10-nbt-binary.md /// internal sealed class NbtBinaryReader { @@ -54,14 +53,14 @@ internal NamedTag ReadRoot(NbtFormat format) return tag; } - /// ルートタグを 1 つ読む + /// ルートタグを1つ読む /// 末尾の余りは見ない internal NamedTag ReadRootTag(NbtFormat format) { byte id = ReadByteRaw(); TagType type = TagTypeExtensions.FromId(id); - // Java版のファイル形式でもネットワーク形式でも、ルートは必ず TAG_Compound + // Java版のファイル形式でもネットワーク形式でも、ルートは必ずTAG_Compound if (type != TagType.Compound) { throw SpringNbtException.Malformed( @@ -87,7 +86,7 @@ internal NamedTag ReadRootTag(NbtFormat format) /// 指定した型のペイロードを読む private NbtTag ReadPayload(TagType type, int depth) { - // 深さ上限は再帰する型に入る手前で検査する + // 深さ上限は型を問わず、ペイロードを読む前に検査する if (depth > maxDepth) { throw SpringNbtException.LimitExceeded($"ネストが深すぎる (上限 {maxDepth})"); @@ -126,12 +125,12 @@ private NbtTag ReadPayload(TagType type, int depth) } } - /// TAG_Compound のペイロード(名前付きタグの並び + TAG_End)を読む + /// TAG_Compoundのペイロード(名前付きタグの並び + TAG_End)を読む private NbtCompound ReadCompoundPayload(int depth) { NbtCompound compound = new NbtCompound(); - // TAG_End が現れるまで名前付きタグを読み続ける + // TAG_Endが現れるまで名前付きタグを読み続ける while (true) { byte id = ReadByteRaw(); @@ -148,7 +147,7 @@ private NbtCompound ReadCompoundPayload(int depth) } } - /// TAG_List のペイロードを読む + /// TAG_Listのペイロードを読む private NbtList ReadListPayload(int depth) { byte elementId = ReadByteRaw(); @@ -157,7 +156,7 @@ private NbtList ReadListPayload(int depth) if (elementType == TagType.End) { - // 要素型 End のリストは空でなければならない + // 要素型Endのリストは空でなければならない if (count != 0) { throw SpringNbtException.Malformed($"要素型 End のリストに {count} 個の要素が宣言されている"); @@ -166,7 +165,7 @@ private NbtList ReadListPayload(int depth) return new NbtList(TagType.End); } - // 1 要素の最小バイト数から、宣言された個数が入力に収まるかを先に検査する + // 1要素の最小バイト数から、宣言された個数が入力に収まるかを先に検査する EnsureAvailable((long)count * MinimumPayloadSize(elementType)); NbtList list = new NbtList(elementType); @@ -204,7 +203,7 @@ private int[] ReadIntArrayPayload() int[] result = new int[count]; - // 4 バイトずつビッグエンディアンで読む + // 4バイトずつビッグエンディアンで読む for (int i = 0; i < count; i++) { result[i] = BinaryPrimitives.ReadInt32BigEndian(data.AsSpan(position + (i * 4), 4)); @@ -221,7 +220,7 @@ private long[] ReadLongArrayPayload() long[] result = new long[count]; - // 8 バイトずつビッグエンディアンで読む + // 8バイトずつビッグエンディアンで読む for (int i = 0; i < count; i++) { result[i] = BinaryPrimitives.ReadInt64BigEndian(data.AsSpan(position + (i * 8), 8)); @@ -231,7 +230,7 @@ private long[] ReadLongArrayPayload() return result; } - /// MUTF-8 の文字列(u16 の長さ + 本体)を読む + /// MUTF-8の文字列(u16の長さ + 本体)を読む private string ReadString() { int length = BinaryPrimitives.ReadUInt16BigEndian(Take(2)); @@ -248,7 +247,7 @@ private int ReadLength() { int length = BinaryPrimitives.ReadInt32BigEndian(Take(4)); - // 長さは i32 だが、負値は仕様上ありえない + // 長さはi32だが、負値は仕様上ありえない if (length < 0) { throw SpringNbtException.Malformed($"長さが負値: {length}"); @@ -289,11 +288,11 @@ private void EnsureAvailable(long required) /// キーやルート名として使える文字列か検査する /// /// - /// 値と違い、キーには孤立サロゲートを許さない(仕様 10 の 2.2章) - /// Minecraft が書き出すキーは ASCII の識別子のみで、 + /// 値と違い、キーには孤立サロゲートを許さない + /// Minecraftが書き出すキーはASCIIの識別子のみで、 /// 孤立サロゲートが現れるのはデータ破損を意味する /// またキーを「ただの文字列」として扱えることが、 - /// 4言語すべてでマップ型をそのまま使えるという実装上の利点につながる + /// 5言語すべてでマップ型をそのまま使えるという実装上の利点につながる /// private static string RequireUtf8Representable(string text, string role) { @@ -302,10 +301,10 @@ private static string RequireUtf8Representable(string text, string role) { char c = text[index]; - // 上位サロゲートは、対になる下位サロゲートとまとめて 1 文字を成す + // 上位サロゲートは、対になる下位サロゲートとまとめて1文字を成す if (char.IsHighSurrogate(c)) { - // 対が揃っていれば 2 コード単位を消費する + // 対が揃っていれば2コード単位を消費する // 揃わなければ孤立サロゲート if (index + 1 < text.Length && char.IsLowSurrogate(text[index + 1])) { @@ -346,16 +345,16 @@ private static int MinimumPayloadSize(TagType type) case TagType.ByteArray: case TagType.IntArray: case TagType.LongArray: - // 長さフィールドの 4 バイトは必ずある + // 長さフィールドの4バイトは必ずある return 4; case TagType.String: - // 長さフィールドの 2 バイトは必ずある + // 長さフィールドの2バイトは必ずある return 2; case TagType.List: - // 要素型 1 バイト + 個数 4 バイト + // 要素型1バイト + 個数4バイト return 5; case TagType.Compound: - // 終端の TAG_End 1 バイトは必ずある + // 終端のTAG_End 1バイトは必ずある return 1; default: return 1; diff --git a/csharp/src/SpringNBTLibrary/Nbt/NbtBinaryWriter.cs b/csharp/src/SpringNBTLibrary/Nbt/NbtBinaryWriter.cs index 4fa178d..fe10c2f 100644 --- a/csharp/src/SpringNBTLibrary/Nbt/NbtBinaryWriter.cs +++ b/csharp/src/SpringNBTLibrary/Nbt/NbtBinaryWriter.cs @@ -3,14 +3,13 @@ namespace SpringNBTLibrary.Nbt; /// -/// NBT を展開済みのバイト列へ書き出す +/// NBTを展開済みのバイト列へ書き出す /// /// /// /// 出力は一意でなければならない(ラウンドトリップ検証が成立するため) -/// Compound は挿入順のまま、浮動小数点はビットパターンのまま書き出す +/// Compoundは挿入順のまま、浮動小数点はビットパターンのまま書き出す /// -/// 仕様: docs/spec/10-nbt-binary.md 5章 /// internal sealed class NbtBinaryWriter { @@ -48,7 +47,7 @@ private void WritePayload(NbtTag tag) WriteInt64(value.Value); break; case NbtFloat value: - // NaN や -0.0 を保つため、ビットパターンをそのまま書く + // NaNや-0.0を保つため、ビットパターンをそのまま書く WriteInt32(BitConverter.SingleToInt32Bits(value.Value)); break; case NbtDouble value: @@ -106,7 +105,7 @@ private void WriteByteArrayPayload(sbyte[] values) { WriteInt32(values.Length); - // 1 バイトずつそのまま書く + // 1バイトずつそのまま書く foreach (sbyte value in values) { buffer.Add((byte)value); @@ -117,7 +116,7 @@ private void WriteIntArrayPayload(int[] values) { WriteInt32(values.Length); - // 4 バイトずつビッグエンディアンで書く + // 4バイトずつビッグエンディアンで書く foreach (int value in values) { WriteInt32(value); @@ -128,7 +127,7 @@ private void WriteLongArrayPayload(long[] values) { WriteInt32(values.Length); - // 8 バイトずつビッグエンディアンで書く + // 8バイトずつビッグエンディアンで書く foreach (long value in values) { WriteInt64(value); @@ -139,8 +138,8 @@ private void WriteString(string text) { byte[] encoded = Mutf8.Encode(text); - // 長さフィールドは u16 - // NbtString 側でも検査しているが、キー名は素の string なのでここでも見る + // 長さフィールドはu16 + // NbtString側でも検査しているが、キー名は素のstringなのでここでも見る if (encoded.Length > Mutf8.MaxByteLength) { throw SpringNbtException.InvalidArgument( @@ -167,7 +166,7 @@ private void WriteInt32(int value) Span bytes = stackalloc byte[4]; BinaryPrimitives.WriteInt32BigEndian(bytes, value); - // Span は AddRange に渡せないので 1 バイトずつ積む + // SpanはAddRangeに渡せないので1バイトずつ積む for (int i = 0; i < bytes.Length; i++) { buffer.Add(bytes[i]); @@ -179,7 +178,7 @@ private void WriteInt64(long value) Span bytes = stackalloc byte[8]; BinaryPrimitives.WriteInt64BigEndian(bytes, value); - // Span は AddRange に渡せないので 1 バイトずつ積む + // SpanはAddRangeに渡せないので1バイトずつ積む for (int i = 0; i < bytes.Length; i++) { buffer.Add(bytes[i]); diff --git a/csharp/src/SpringNBTLibrary/Nbt/NbtCompound.Accessors.cs b/csharp/src/SpringNBTLibrary/Nbt/NbtCompound.Accessors.cs index 5559414..5a7f722 100644 --- a/csharp/src/SpringNBTLibrary/Nbt/NbtCompound.Accessors.cs +++ b/csharp/src/SpringNBTLibrary/Nbt/NbtCompound.Accessors.cs @@ -1,20 +1,19 @@ namespace SpringNBTLibrary.Nbt; /// -/// の型付き取得子 +/// の型付き取得子 /// /// /// /// 「キーが無い」と「型が違う」は区別する -/// Opt* はキーが無ければ null を返し、Get* は例外を送出する -/// どちらも型が違えば必ず の例外になる +/// Opt*はキーが無ければnullを返し、Get*は例外を送出する +/// どちらも型が違えば必ずの例外になる /// -/// 仕様: docs/spec/10-nbt-binary.md 7.1章 /// public sealed partial class NbtCompound { - /// TAG_Byte を取得する - /// キーが無ければ null + /// TAG_Byteを取得する + /// キーが無ければnull public sbyte? OptByte(string key) { NbtByte? tag = Cast(key); @@ -27,12 +26,12 @@ public sealed partial class NbtCompound return tag.Value; } - /// TAG_Byte を取得する + /// TAG_Byteを取得する /// キーが無ければ例外 public sbyte GetByte(string key) => Require(key).Value; - /// TAG_Short を取得する - /// キーが無ければ null + /// TAG_Shortを取得する + /// キーが無ければnull public short? OptShort(string key) { NbtShort? tag = Cast(key); @@ -45,12 +44,12 @@ public sealed partial class NbtCompound return tag.Value; } - /// TAG_Short を取得する + /// TAG_Shortを取得する /// キーが無ければ例外 public short GetShort(string key) => Require(key).Value; - /// TAG_Int を取得する - /// キーが無ければ null + /// TAG_Intを取得する + /// キーが無ければnull public int? OptInt(string key) { NbtInt? tag = Cast(key); @@ -63,12 +62,12 @@ public sealed partial class NbtCompound return tag.Value; } - /// TAG_Int を取得する + /// TAG_Intを取得する /// キーが無ければ例外 public int GetInt(string key) => Require(key).Value; - /// TAG_Long を取得する - /// キーが無ければ null + /// TAG_Longを取得する + /// キーが無ければnull public long? OptLong(string key) { NbtLong? tag = Cast(key); @@ -81,12 +80,12 @@ public sealed partial class NbtCompound return tag.Value; } - /// TAG_Long を取得する + /// TAG_Longを取得する /// キーが無ければ例外 public long GetLong(string key) => Require(key).Value; - /// TAG_Float を取得する - /// キーが無ければ null + /// TAG_Floatを取得する + /// キーが無ければnull public float? OptFloat(string key) { NbtFloat? tag = Cast(key); @@ -99,12 +98,12 @@ public sealed partial class NbtCompound return tag.Value; } - /// TAG_Float を取得する + /// TAG_Floatを取得する /// キーが無ければ例外 public float GetFloat(string key) => Require(key).Value; - /// TAG_Double を取得する - /// キーが無ければ null + /// TAG_Doubleを取得する + /// キーが無ければnull public double? OptDouble(string key) { NbtDouble? tag = Cast(key); @@ -117,13 +116,13 @@ public sealed partial class NbtCompound return tag.Value; } - /// TAG_Double を取得する + /// TAG_Doubleを取得する /// キーが無ければ例外 public double GetDouble(string key) => Require(key).Value; - /// TAG_Byte を真偽値として取得する - /// 0 以外が true - /// キーが無ければ null + /// TAG_Byteを真偽値として取得する + /// 0以外がtrue + /// キーが無ければnull public bool? OptBool(string key) { sbyte? raw = OptByte(key); @@ -136,13 +135,13 @@ public sealed partial class NbtCompound return raw.Value != 0; } - /// TAG_Byte を真偽値として取得する - /// 0 以外が true + /// TAG_Byteを真偽値として取得する + /// 0以外がtrue /// キーが無ければ例外 public bool GetBool(string key) => GetByte(key) != 0; - /// TAG_String を取得する - /// キーが無ければ null + /// TAG_Stringを取得する + /// キーが無ければnull public string? OptString(string key) { NbtString? tag = Cast(key); @@ -155,12 +154,12 @@ public sealed partial class NbtCompound return tag.Value; } - /// TAG_String を取得する + /// TAG_Stringを取得する /// キーが無ければ例外 public string GetString(string key) => Require(key).Value; - /// TAG_Byte_Array を取得する - /// キーが無ければ null + /// TAG_Byte_Arrayを取得する + /// キーが無ければnull public sbyte[]? OptByteArray(string key) { NbtByteArray? tag = Cast(key); @@ -173,12 +172,12 @@ public sealed partial class NbtCompound return tag.Value; } - /// TAG_Byte_Array を取得する + /// TAG_Byte_Arrayを取得する /// キーが無ければ例外 public sbyte[] GetByteArray(string key) => Require(key).Value; - /// TAG_Int_Array を取得する - /// キーが無ければ null + /// TAG_Int_Arrayを取得する + /// キーが無ければnull public int[]? OptIntArray(string key) { NbtIntArray? tag = Cast(key); @@ -191,12 +190,12 @@ public sealed partial class NbtCompound return tag.Value; } - /// TAG_Int_Array を取得する + /// TAG_Int_Arrayを取得する /// キーが無ければ例外 public int[] GetIntArray(string key) => Require(key).Value; - /// TAG_Long_Array を取得する - /// キーが無ければ null + /// TAG_Long_Arrayを取得する + /// キーが無ければnull public long[]? OptLongArray(string key) { NbtLongArray? tag = Cast(key); @@ -209,29 +208,29 @@ public sealed partial class NbtCompound return tag.Value; } - /// TAG_Long_Array を取得する + /// TAG_Long_Arrayを取得する /// キーが無ければ例外 public long[] GetLongArray(string key) => Require(key).Value; - /// TAG_List を取得する - /// キーが無ければ null + /// TAG_Listを取得する + /// キーが無ければnull public NbtList? OptList(string key) => Cast(key); - /// TAG_List を取得する + /// TAG_Listを取得する /// キーが無ければ例外 public NbtList GetList(string key) => Require(key); - /// TAG_Compound を取得する - /// キーが無ければ null + /// TAG_Compoundを取得する + /// キーが無ければnull public NbtCompound? OptCompound(string key) => Cast(key); - /// TAG_Compound を取得する + /// TAG_Compoundを取得する /// キーが無ければ例外 public NbtCompound GetCompound(string key) => Require(key); /// /// キーに対応するタグを目的の型として取り出す - /// キーが無ければ null、型が違えば例外 + /// キーが無ければnull、型が違えば例外 /// private T? Cast(string key) where T : NbtTag diff --git a/csharp/src/SpringNBTLibrary/Nbt/NbtCompound.Setters.cs b/csharp/src/SpringNBTLibrary/Nbt/NbtCompound.Setters.cs index 78a2107..2327194 100644 --- a/csharp/src/SpringNBTLibrary/Nbt/NbtCompound.Setters.cs +++ b/csharp/src/SpringNBTLibrary/Nbt/NbtCompound.Setters.cs @@ -1,42 +1,41 @@ namespace SpringNBTLibrary.Nbt; /// -/// の型付き設定子 +/// の型付き設定子 /// /// /// -/// Set(key, new NbtInt(42)) と書かずに済むようにするための糖衣 -/// 取得子の GetInt と対になる +/// Set(key, new NbtInt(42))と書かずに済むようにするための糖衣 +/// 取得子のGetIntと対になる /// -/// 仕様: docs/spec/10-nbt-binary.md 7.1章 /// public sealed partial class NbtCompound { - /// TAG_Byte として設定する + /// TAG_Byteとして設定する public void SetByte(string key, sbyte value) => Set(key, new NbtByte(value)); - /// TAG_Short として設定する + /// TAG_Shortとして設定する public void SetShort(string key, short value) => Set(key, new NbtShort(value)); - /// TAG_Int として設定する + /// TAG_Intとして設定する public void SetInt(string key, int value) => Set(key, new NbtInt(value)); - /// TAG_Long として設定する + /// TAG_Longとして設定する public void SetLong(string key, long value) => Set(key, new NbtLong(value)); - /// TAG_Float として設定する + /// TAG_Floatとして設定する public void SetFloat(string key, float value) => Set(key, new NbtFloat(value)); - /// TAG_Double として設定する + /// TAG_Doubleとして設定する public void SetDouble(string key, double value) => Set(key, new NbtDouble(value)); /// - /// TAG_Byte として設定する - /// true は 1、false は 0 + /// TAG_Byteとして設定する + /// trueは1、falseは0 /// public void SetBool(string key, bool value) { - // NBT に真偽値の専用型は無いので TAG_Byte の 0 / 1 で表す + // NBTに真偽値の専用型は無いので、TAG_Byteの0 / 1で表す if (value) { SetByte(key, 1); @@ -47,18 +46,18 @@ public void SetBool(string key, bool value) } } - /// TAG_String として設定する + /// TAG_Stringとして設定する /// - /// MUTF-8 に符号化すると 65535 バイトを超える場合() + /// MUTF-8に符号化すると65535バイトを超える場合() /// public void SetString(string key, string value) => Set(key, new NbtString(value)); - /// TAG_Byte_Array として設定する + /// TAG_Byte_Arrayとして設定する public void SetByteArray(string key, sbyte[] value) => Set(key, new NbtByteArray(value)); - /// TAG_Int_Array として設定する + /// TAG_Int_Arrayとして設定する public void SetIntArray(string key, int[] value) => Set(key, new NbtIntArray(value)); - /// TAG_Long_Array として設定する + /// TAG_Long_Arrayとして設定する public void SetLongArray(string key, long[] value) => Set(key, new NbtLongArray(value)); } diff --git a/csharp/src/SpringNBTLibrary/Nbt/NbtCompound.cs b/csharp/src/SpringNBTLibrary/Nbt/NbtCompound.cs index c6b0121..1e5a4f0 100644 --- a/csharp/src/SpringNBTLibrary/Nbt/NbtCompound.cs +++ b/csharp/src/SpringNBTLibrary/Nbt/NbtCompound.cs @@ -11,14 +11,13 @@ namespace SpringNBTLibrary.Nbt; /// 既存キーへの再設定は位置を維持したまま値だけを置き換える /// これにより読み込んだ順序が書き出しでも保たれ、ラウンドトリップが成立する /// -/// 仕様: docs/spec/10-nbt-binary.md 7.1章 /// public sealed partial class NbtCompound : NbtTag, IEnumerable> { private readonly List> entries = new List>(); private readonly Dictionary index = new Dictionary(StringComparer.Ordinal); - /// 空の Compound を作る + /// 空のCompoundを作る public NbtCompound() { } @@ -34,7 +33,7 @@ public IEnumerable Keys { get { - // 挿入順を保つため entries 側から取り出す + // 挿入順を保つため、entries側から取り出す foreach (KeyValuePair entry in entries) { yield return entry.Key; @@ -78,7 +77,13 @@ public void Set(string key, NbtTag value) ArgumentNullException.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(value); - // TAG_End は Compound の終端マーカーなので値として持てない + // 孤立サロゲートを含むキーは書き出すと読み戻せないので、ここで止める + if (Mutf8.HasLoneSurrogate(key)) + { + throw SpringNbtException.InvalidArgument("キーに孤立サロゲートは使えない"); + } + + // TAG_EndはCompoundの終端マーカーなので値として持てない if (value.Type == TagType.End) { throw SpringNbtException.UnexpectedTagType("TAG_End は Compound の値にできない"); @@ -97,7 +102,7 @@ public void Set(string key, NbtTag value) } /// キーを削除する - /// 削除できたら true + /// 削除できたらtrue public bool Remove(string key) { ArgumentNullException.ThrowIfNull(key); @@ -127,7 +132,7 @@ public void Clear() } /// キーに対応するタグを返す - /// 存在しなければ null + /// 存在しなければnull public NbtTag? Opt(string key) { ArgumentNullException.ThrowIfNull(key); diff --git a/csharp/src/SpringNBTLibrary/Nbt/NbtFormat.cs b/csharp/src/SpringNBTLibrary/Nbt/NbtFormat.cs index b66dd1d..2a92d59 100644 --- a/csharp/src/SpringNBTLibrary/Nbt/NbtFormat.cs +++ b/csharp/src/SpringNBTLibrary/Nbt/NbtFormat.cs @@ -1,20 +1,19 @@ namespace SpringNBTLibrary.Nbt; /// -/// NBT のルートタグの並び方 +/// NBTのルートタグの並び方 /// -/// 仕様: docs/spec/10-nbt-binary.md 3章 public enum NbtFormat { /// /// ファイル形式 /// ルートは「タグID + 名前長 + 名前 + ペイロード」の順に並ぶ - /// level.dat やチャンクなど、保存されるデータはすべてこちら + /// level.datやチャンクなど、保存されるデータはすべてこちら /// Java, /// - /// ネットワーク形式 (1.20.2 以降) + /// ネットワーク形式 (1.20.2以降) /// ルートに名前が付かない /// Network, @@ -23,7 +22,6 @@ public enum NbtFormat /// /// 圧縮方式 /// -/// 仕様: docs/spec/10-nbt-binary.md 4章 public enum Compression { /// 無圧縮 @@ -46,7 +44,7 @@ public enum Compression /// ルート名とルートタグの組 /// /// -/// ではルート名は通常空文字列だが、 +/// ではルート名は通常空文字列だが、 /// 読んだ値をそのまま保持し、書き出しでも同じ値を出力する /// public sealed class NamedTag diff --git a/csharp/src/SpringNBTLibrary/Nbt/NbtIo.cs b/csharp/src/SpringNBTLibrary/Nbt/NbtIo.cs index d8bffab..85c914b 100644 --- a/csharp/src/SpringNBTLibrary/Nbt/NbtIo.cs +++ b/csharp/src/SpringNBTLibrary/Nbt/NbtIo.cs @@ -3,12 +3,11 @@ namespace SpringNBTLibrary.Nbt; /// -/// NBT のファイル・バイト列・ストリームからの読み書き +/// NBTのファイル・バイト列・ストリームからの読み書き /// -/// 仕様: docs/spec/10-nbt-binary.md 3章・4章 public static class NbtIo { - /// ファイルから NBT を読む + /// ファイルからNBTを読む /// 読み込みに失敗した場合 public static NamedTag ReadFile(string path, NbtReadOptions? options = null) { @@ -32,7 +31,7 @@ public static NamedTag ReadFile(string path, NbtReadOptions? options = null) return ReadBytes(raw, options); } - /// バイト列から NBT を読む + /// バイト列からNBTを読む /// 読み込みに失敗した場合 public static NamedTag ReadBytes(byte[] bytes, NbtReadOptions? options = null) { @@ -54,11 +53,11 @@ public static NamedTag ReadBytes(byte[] bytes, NbtReadOptions? options = null) return reader.ReadRoot(effective.Format); } - /// バイト列の指定した位置から NBT を 1 つ読む + /// バイト列の指定した位置からNBTを1つ読む /// /// - /// 複数の NBT が連なっているデータを、先頭から順に読み進めるために使う - /// 戻り値の が次の開始位置になる + /// 複数のNBTが連なっているデータを、先頭から順に読み進めるために使う + /// 戻り値のが次の開始位置になる /// /// /// 位置は渡したバイト列そのものを指すので、圧縮されたデータは扱えない @@ -84,11 +83,11 @@ public static NbtReadResult ReadBytesAt(byte[] bytes, int offset, return new NbtReadResult(tag, reader.Position); } - /// バイト列に連なっている NBT をすべて読む + /// バイト列に連なっているNBTをすべて読む /// /// 入力を使い切るまで読み続ける /// 空のバイト列なら空の一覧を返す - /// 圧縮は入力全体に 1 回かかっているものとして扱う + /// 圧縮は入力全体に1回かかっているものとして扱う /// /// 読み込みに失敗した場合 public static IReadOnlyList ReadBytesAll(byte[] bytes, @@ -99,7 +98,7 @@ public static IReadOnlyList ReadBytesAll(byte[] bytes, NbtReadOptions effective = EffectiveOptions(options); List tags = new List(); - // 空の入力は「0 個」であってエラーではない + // 空の入力は「0個」であってエラーではない if (bytes.Length == 0) { return tags; @@ -138,7 +137,7 @@ private static void RequirePlainInput(NbtReadOptions options) } } - /// ストリームから NBT を読む + /// ストリームからNBTを読む /// ストリームは最後まで読み切る /// 読み込みに失敗した場合 public static NamedTag ReadStream(Stream stream, NbtReadOptions? options = null) @@ -158,7 +157,7 @@ public static NamedTag ReadStream(Stream stream, NbtReadOptions? options = null) return ReadBytes(memory.ToArray(), options); } - /// NBT をファイルへ書き出す + /// NBTをファイルへ書き出す /// 書き込みに失敗した場合 public static void WriteFile(string path, NamedTag tag, NbtWriteOptions? options = null) { @@ -180,7 +179,7 @@ public static void WriteFile(string path, NamedTag tag, NbtWriteOptions? options } } - /// NBT をバイト列へ書き出す + /// NBTをバイト列へ書き出す /// 書き込みに失敗した場合 public static byte[] WriteBytes(NamedTag tag, NbtWriteOptions? options = null) { @@ -197,7 +196,7 @@ public static byte[] WriteBytes(NamedTag tag, NbtWriteOptions? options = null) effective = options; } - // 書き込み時に Auto は決められない + // 書き込み時にAutoは決められない if (effective.Compression == Compression.Auto) { throw SpringNbtException.InvalidArgument("書き込みで Compression.Auto は指定できない"); @@ -208,7 +207,7 @@ public static byte[] WriteBytes(NamedTag tag, NbtWriteOptions? options = null) return Compress(plain, effective.Compression); } - /// NBT をストリームへ書き出す + /// NBTをストリームへ書き出す /// 書き込みに失敗した場合 public static void WriteStream(Stream stream, NamedTag tag, NbtWriteOptions? options = null) { @@ -241,7 +240,7 @@ public static Compression DetectCompression(byte[] bytes) throw SpringNbtException.Malformed("入力が空で圧縮方式を判定できない"); } - // GZip は必ず 1F 8B で始まる + // GZipは必ず1F 8Bで始まる if (bytes.Length >= 2 && bytes[0] == 0x1F && bytes[1] == 0x8B) { return Compression.Gzip; @@ -249,7 +248,7 @@ public static Compression DetectCompression(byte[] bytes) if (bytes.Length >= 2) { - // zlib ヘッダは「圧縮法が 8 (deflate)」かつ「先頭2バイトが 31 の倍数」 + // zlibヘッダは「圧縮法が8 (deflate)」かつ「先頭2バイトが31の倍数」 bool isDeflate = (bytes[0] & 0x0F) == 0x08; int header = (bytes[0] << 8) | bytes[1]; if (isDeflate && header % 31 == 0) @@ -258,7 +257,7 @@ public static Compression DetectCompression(byte[] bytes) } } - // 無圧縮なら先頭は TAG_Compound のタグID + // 無圧縮なら先頭はTAG_CompoundのタグID if (bytes[0] == (byte)TagType.Compound) { return Compression.None; @@ -271,7 +270,7 @@ public static Compression DetectCompression(byte[] bytes) private static byte[] Decompress(byte[] bytes, NbtReadOptions options) { Compression method; - // Auto なら先頭バイトから圧縮方式を見分ける + // Autoなら先頭バイトから圧縮方式を見分ける if (options.Compression == Compression.Auto) { method = DetectCompression(bytes); @@ -312,7 +311,7 @@ private static byte[] Compress(byte[] plain, Compression method) using MemoryStream destination = new MemoryStream(); - // using ブロックを閉じてフッタを書かせてから ToArray する必要がある + // usingブロックを閉じてフッタを書かせてからToArrayする必要がある using (Stream encoder = CreateEncoder(destination, method)) { encoder.Write(plain, 0, plain.Length); diff --git a/csharp/src/SpringNBTLibrary/Nbt/NbtList.cs b/csharp/src/SpringNBTLibrary/Nbt/NbtList.cs index bdc820d..58e8d1e 100644 --- a/csharp/src/SpringNBTLibrary/Nbt/NbtList.cs +++ b/csharp/src/SpringNBTLibrary/Nbt/NbtList.cs @@ -4,15 +4,14 @@ namespace SpringNBTLibrary.Nbt; /// /// TAG_List -/// 要素型が 1 つに固定されたタグの列 +/// 要素型が1つに固定されたタグの列 /// /// /// -/// 空リストの要素型は +/// 空リストの要素型は /// 最初の要素を追加した時点で型が確定する -/// 全要素を削除しても確定済みの要素型は維持される(読み書きの往復で型が消えないようにするため) +/// 読み書きの往復で型が消えないよう、全要素を削除しても確定済みの要素型は維持される /// -/// 仕様: docs/spec/10-nbt-binary.md 7.2章 /// public sealed class NbtList : NbtTag, IList { @@ -40,7 +39,7 @@ public NbtList(TagType elementType, IEnumerable elements) { ArgumentNullException.ThrowIfNull(elements); - // 1 要素ずつ型検査しながら追加する + // 1要素ずつ型検査しながら追加する foreach (NbtTag element in elements) { Add(element); @@ -48,7 +47,7 @@ public NbtList(TagType elementType, IEnumerable elements) } /// 要素の型 - /// 空で未確定なら + /// 空で未確定なら public TagType ElementType { get; private set; } /// @@ -70,6 +69,13 @@ public NbtTag this[int index] set { ArgumentNullException.ThrowIfNull(value); + + // 要素型を確定させる前に位置を確かめ、失敗したときに状態を変えない + if (index < 0 || index >= items.Count) + { + throw new ArgumentOutOfRangeException(nameof(index), index, "位置が範囲外"); + } + EnsureElementType(value); items[index] = value; } @@ -93,6 +99,13 @@ public void Add(NbtTag item) public void Insert(int index, NbtTag item) { ArgumentNullException.ThrowIfNull(item); + + // 要素型を確定させる前に位置を確かめ、失敗したときに状態を変えない + if (index < 0 || index > items.Count) + { + throw new ArgumentOutOfRangeException(nameof(index), index, "位置が範囲外"); + } + EnsureElementType(item); items.Insert(index, item); } @@ -172,7 +185,7 @@ public override bool Equals(object? obj) /// private void EnsureElementType(NbtTag item) { - // TAG_End はリストの要素になれない + // TAG_Endはリストの要素になれない if (item.Type == TagType.End) { throw SpringNbtException.UnexpectedTagType("TAG_End はリストの要素にできない"); diff --git a/csharp/src/SpringNBTLibrary/Nbt/NbtOptions.cs b/csharp/src/SpringNBTLibrary/Nbt/NbtOptions.cs index 29607f8..e0bd1ee 100644 --- a/csharp/src/SpringNBTLibrary/Nbt/NbtOptions.cs +++ b/csharp/src/SpringNBTLibrary/Nbt/NbtOptions.cs @@ -1,36 +1,34 @@ namespace SpringNBTLibrary.Nbt; /// -/// NBT 読み込みのオプション +/// NBT読み込みのオプション /// -/// 仕様: docs/spec/10-nbt-binary.md 6章 / docs/spec/00-conventions.md 5章 public sealed class NbtReadOptions { /// 既定のオプション public static NbtReadOptions Default { get; } = new NbtReadOptions(); /// ルートタグの並び方 - /// 既定は + /// 既定は public NbtFormat Format { get; set; } = NbtFormat.Java; /// 圧縮方式 - /// 既定は (先頭バイトから判定) + /// 既定は(先頭バイトから判定) public Compression Compression { get; set; } = Compression.Auto; /// ネストの深さ上限 - /// 既定は 512 + /// 既定は512 public int MaxDepth { get; set; } = 512; /// 展開後の総バイト数の上限 /// 負値なら無制限 - /// 既定は -1 + /// 既定は-1 public long MaxDecompressedSize { get; set; } = -1; } /// -/// NBT 書き込みのオプション +/// NBT書き込みのオプション /// -/// 仕様: docs/spec/10-nbt-binary.md 6章 public sealed class NbtWriteOptions { /// 既定のオプション @@ -41,10 +39,10 @@ public sealed class NbtWriteOptions new NbtWriteOptions { Compression = Compression.None }; /// ルートタグの並び方 - /// 既定は + /// 既定は public NbtFormat Format { get; set; } = NbtFormat.Java; /// 圧縮方式 - /// 既定は + /// 既定は public Compression Compression { get; set; } = Compression.Gzip; } diff --git a/csharp/src/SpringNBTLibrary/Nbt/NbtReadResult.cs b/csharp/src/SpringNBTLibrary/Nbt/NbtReadResult.cs index a086d91..46fd480 100644 --- a/csharp/src/SpringNBTLibrary/Nbt/NbtReadResult.cs +++ b/csharp/src/SpringNBTLibrary/Nbt/NbtReadResult.cs @@ -6,9 +6,8 @@ namespace SpringNBTLibrary.Nbt; /// /// 読んだタグと、その直後の位置を持つ /// -/// 続けて読むときは を次の開始位置として渡す +/// 続けて読むときはを次の開始位置として渡す /// -/// 仕様: docs/spec/10-nbt-binary.md 3.1章 /// public sealed class NbtReadResult { diff --git a/csharp/src/SpringNBTLibrary/Nbt/NbtTag.cs b/csharp/src/SpringNBTLibrary/Nbt/NbtTag.cs index f48be6a..0378e90 100644 --- a/csharp/src/SpringNBTLibrary/Nbt/NbtTag.cs +++ b/csharp/src/SpringNBTLibrary/Nbt/NbtTag.cs @@ -1,15 +1,14 @@ namespace SpringNBTLibrary.Nbt; /// -/// NBT のタグ -/// 具象型は などの派生クラス +/// NBTのタグ +/// 具象型はなどの派生クラス /// /// /// /// パターンマッチで分岐することを想定している -/// if (tag is NbtInt intTag) { ... } のように使う +/// if (tag is NbtInt intTag) { ... }のように使う /// -/// 仕様: docs/spec/10-nbt-binary.md 1章 /// public abstract class NbtTag { @@ -26,7 +25,7 @@ internal NbtTag() } /// TAG_Byte -/// 8bit 符号付き整数 +/// 8bit符号付き整数 public sealed class NbtByte : NbtTag { /// 値を指定して作る @@ -55,7 +54,7 @@ public NbtByte(sbyte value) } /// TAG_Short -/// 16bit 符号付き整数 +/// 16bit符号付き整数 public sealed class NbtShort : NbtTag { /// 値を指定して作る @@ -84,7 +83,7 @@ public NbtShort(short value) } /// TAG_Int -/// 32bit 符号付き整数 +/// 32bit符号付き整数 public sealed class NbtInt : NbtTag { /// 値を指定して作る @@ -113,7 +112,7 @@ public NbtInt(int value) } /// TAG_Long -/// 64bit 符号付き整数 +/// 64bit符号付き整数 public sealed class NbtLong : NbtTag { /// 値を指定して作る @@ -161,7 +160,7 @@ public NbtFloat(float value) public override NbtTag Copy() => new NbtFloat(Value); /// - /// NaN や -0.0 を区別するため、値ではなくビットパターンで比較する + /// NaNや-0.0を区別するため、値ではなくビットパターンで比較する public override bool Equals(object? obj) { return obj is NbtFloat other @@ -195,7 +194,7 @@ public NbtDouble(double value) public override NbtTag Copy() => new NbtDouble(Value); /// - /// NaN や -0.0 を区別するため、値ではなくビットパターンで比較する + /// NaNや-0.0を区別するため、値ではなくビットパターンで比較する public override bool Equals(object? obj) { return obj is NbtDouble other @@ -210,14 +209,14 @@ public override bool Equals(object? obj) } /// TAG_String -/// MUTF-8 で符号化される文字列 +/// MUTF-8で符号化される文字列 public sealed class NbtString : NbtTag { private string value; /// 値を指定して作る /// - /// MUTF-8 に符号化すると 65535 バイトを超える場合() + /// MUTF-8に符号化すると65535バイトを超える場合() /// public NbtString(string value) { @@ -228,7 +227,7 @@ public NbtString(string value) /// 保持している値 /// - /// MUTF-8 に符号化すると 65535 バイトを超える場合() + /// MUTF-8に符号化すると65535バイトを超える場合() /// public string Value { @@ -259,13 +258,13 @@ public override bool Equals(object? obj) /// public override string ToString() => value; - /// 長さフィールドが u16 のため、符号化後 65535 バイトを超える文字列は保持できない + /// 長さフィールドがu16のため、符号化後65535バイトを超える文字列は保持できない private static void Validate(string candidate) { int byteLength = Mutf8.ByteLength(candidate); - // 長さフィールドは u16 - // 65535 を超えると書き出せない + // 長さフィールドはu16 + // 65535を超えると書き出せない if (byteLength > Mutf8.MaxByteLength) { throw SpringNbtException.InvalidArgument( diff --git a/csharp/src/SpringNBTLibrary/Nbt/Snbt.cs b/csharp/src/SpringNBTLibrary/Nbt/Snbt.cs index b4140e5..a69e1e5 100644 --- a/csharp/src/SpringNBTLibrary/Nbt/Snbt.cs +++ b/csharp/src/SpringNBTLibrary/Nbt/Snbt.cs @@ -4,20 +4,19 @@ namespace SpringNBTLibrary.Nbt; /// -/// SNBT (Stringified NBT) のパースと出力 +/// SNBT (Stringified NBT)のパースと出力 /// /// /// -/// 対応範囲は「バイナリ NBT へ損失なく写せる部分集合」 -/// 1.21.5 以降の異種リスト([1, "a"])は受理しない +/// 対応範囲は「バイナリNBTへ損失なく写せる部分集合」 +/// 1.21.5以降の異種リスト([1, "a"])は受理しない /// -/// 仕様: docs/spec/11-snbt.md / docs/adr/0006-snbt-scope.md /// public static class Snbt { private const string IndentUnit = " "; - /// SNBT 文字列をタグへ変換する + /// SNBT文字列をタグへ変換する /// 構文が不正な場合 public static NbtTag Parse(string text) { @@ -26,9 +25,9 @@ public static NbtTag Parse(string text) return parser.ParseWhole(); } - /// SNBT 文字列を Compound へ変換する + /// SNBT文字列をCompoundへ変換する /// - /// 構文が不正、またはルートが Compound でない場合 + /// 構文が不正、またはルートがCompoundでない場合 /// public static NbtCompound ParseCompound(string text) { @@ -43,7 +42,7 @@ public static NbtCompound ParseCompound(string text) $"ルートが compound でない: {tag.Type.AsString()}"); } - /// タグを 1 行の SNBT へ変換する + /// タグを1行のSNBTへ変換する public static string Write(NbtTag tag) { ArgumentNullException.ThrowIfNull(tag); @@ -52,8 +51,8 @@ public static string Write(NbtTag tag) return builder.ToString(); } - /// タグを整形した SNBT へ変換する - /// インデントは空白 4 個 + /// タグを整形したSNBTへ変換する + /// インデントは空白4個 public static string WritePretty(NbtTag tag) { ArgumentNullException.ThrowIfNull(tag); @@ -64,7 +63,7 @@ public static string WritePretty(NbtTag tag) /// /// タグを書き出す - /// が負なら 1 行、0 以上なら整形して出力する + /// が負なら1行、0以上なら整形して出力する /// private static void WriteTag(StringBuilder builder, NbtTag tag, int depth) { @@ -113,7 +112,7 @@ private static void WriteTag(StringBuilder builder, NbtTag tag, int depth) private static void WriteCompound(StringBuilder builder, NbtCompound compound, int depth) { - // 空の compound は改行もインデントも入れず {} と書く + // 空のcompoundは改行もインデントも入れず{}と書く if (compound.Count == 0) { builder.Append("{}"); @@ -126,7 +125,7 @@ private static void WriteCompound(StringBuilder builder, NbtCompound compound, i // 挿入順のまま「キー: 値」を並べる foreach (KeyValuePair entry in compound) { - // 2 つ目以降の前に区切りのカンマを置く + // 2つ目以降の前に区切りのカンマを置く if (!first) { builder.Append(','); @@ -152,7 +151,7 @@ private static void WriteCompound(StringBuilder builder, NbtCompound compound, i private static void WriteList(StringBuilder builder, NbtList list, int depth) { - // 空のリストは改行もインデントも入れず [] と書く + // 空のリストは改行もインデントも入れず[]と書く if (list.Count == 0) { builder.Append("[]"); @@ -165,7 +164,7 @@ private static void WriteList(StringBuilder builder, NbtList list, int depth) // 要素型は共通なので値だけを並べる foreach (NbtTag item in list) { - // 2 つ目以降の前に区切りのカンマを置く + // 2つ目以降の前に区切りのカンマを置く if (!first) { builder.Append(','); @@ -184,11 +183,11 @@ private static void WriteByteArray(StringBuilder builder, NbtByteArray array) { builder.Append("[B;"); - // 型付き配列は 1 行に収める + // 型付き配列は1行に収める // 要素には接尾辞を付ける for (int i = 0; i < array.Value.Length; i++) { - // 2 つ目以降の前に区切りのカンマを置く + // 2つ目以降の前に区切りのカンマを置く if (i > 0) { builder.Append(','); @@ -204,10 +203,10 @@ private static void WriteIntArray(StringBuilder builder, NbtIntArray array) { builder.Append("[I;"); - // IntArray の要素は接尾辞なし + // IntArrayの要素は接尾辞なし for (int i = 0; i < array.Value.Length; i++) { - // 2 つ目以降の前に区切りのカンマを置く + // 2つ目以降の前に区切りのカンマを置く if (i > 0) { builder.Append(','); @@ -223,10 +222,10 @@ private static void WriteLongArray(StringBuilder builder, NbtLongArray array) { builder.Append("[L;"); - // LongArray の要素には L 接尾辞を付ける + // LongArrayの要素にはL接尾辞を付ける for (int i = 0; i < array.Value.Length; i++) { - // 2 つ目以降の前に区切りのカンマを置く + // 2つ目以降の前に区切りのカンマを置く if (i > 0) { builder.Append(','); @@ -238,7 +237,7 @@ private static void WriteLongArray(StringBuilder builder, NbtLongArray array) builder.Append(']'); } - /// 整形出力なら改行とインデントを、1 行出力なら何も入れない + /// 整形出力なら改行とインデントを、1行出力なら何も入れない private static void AppendSeparator(StringBuilder builder, int depth) { if (depth < 0) @@ -255,7 +254,7 @@ private static void AppendSeparator(StringBuilder builder, int depth) } } - /// 整形出力のときだけ深さを 1 段進める + /// 整形出力のときだけ深さを1段進める private static int NextDepth(int depth) { if (depth < 0) @@ -303,7 +302,7 @@ private static string QuoteString(string text) StringBuilder builder = new StringBuilder(text.Length + 2); builder.Append('"'); - // 1 文字ずつ見てエスケープが要るものだけ置き換える + // 1文字ずつ見てエスケープが要るものだけ置き換える for (int index = 0; index < text.Length; index++) { char c = text[index]; @@ -343,7 +342,7 @@ private static string QuoteString(string text) } else if (c < 0x20 || c == 0x7F || char.IsSurrogate(c)) { - // 制御文字と孤立サロゲートは \uXXXX で表す + // 制御文字と孤立サロゲートは\uXXXXで表す builder.Append("\\u").Append(((int)c).ToString("x4", CultureInfo.InvariantCulture)); } else diff --git a/csharp/src/SpringNBTLibrary/Nbt/SnbtParser.cs b/csharp/src/SpringNBTLibrary/Nbt/SnbtParser.cs index 4413af9..5c46b72 100644 --- a/csharp/src/SpringNBTLibrary/Nbt/SnbtParser.cs +++ b/csharp/src/SpringNBTLibrary/Nbt/SnbtParser.cs @@ -4,9 +4,8 @@ namespace SpringNBTLibrary.Nbt; /// -/// SNBT (Stringified NBT) のパーサ +/// SNBT (Stringified NBT)のパーサ /// -/// 仕様: docs/spec/11-snbt.md internal sealed class SnbtParser { private readonly string text; @@ -18,7 +17,7 @@ internal SnbtParser(string text) this.position = 0; } - /// 入力全体を 1 つの値として読む + /// 入力全体を1つの値として読む /// 末尾に余りがあれば例外 internal NbtTag ParseWhole() { @@ -69,14 +68,14 @@ private NbtCompound ParseCompound() NbtCompound compound = new NbtCompound(); SkipWhitespace(); - // 空の Compound + // 空のCompound if (Peek() == '}') { position += 1; return compound; } - // 要素を 1 つずつ読む + // 要素を1つずつ読む while (true) { SkipWhitespace(); @@ -119,13 +118,13 @@ private NbtTag ParseListOrArray() { Expect('['); - // "[B;" のような型付き配列かどうかを先に判定する + // "[B;"のような型付き配列かどうかを先に判定する if (position + 1 < text.Length && text[position + 1] == ';') { char marker = text[position]; - // [B; [I; [L; は型付き配列の印 - // ただの List と見分ける + // [B; [I; [L;は型付き配列の印 + // ただのListと見分ける if (marker == 'B' || marker == 'I' || marker == 'L') { position += 2; @@ -148,7 +147,7 @@ private NbtList ParseList() return list; } - // 要素を 1 つずつ読む + // 要素を1つずつ読む while (true) { SkipWhitespace(); @@ -162,7 +161,7 @@ private NbtList ParseList() NbtTag value = ParseValue(); - // 異種リストはバイナリ NBT へ写せないため受理しない (adr/0006) + // 異種リストはバイナリNBTへ写せないため受理しない if (list.ElementType != TagType.End && list.ElementType != value.Type) { throw Malformed( @@ -240,12 +239,12 @@ private NbtTag ParseTypedArray(char marker) } } - // [B; は TAG_Byte_Array + // [B;はTAG_Byte_Array if (marker == 'B') { sbyte[] result = new sbyte[values.Count]; - // 各要素が Byte の範囲に収まるか確認しながら詰める + // 各要素がByteの範囲に収まるか確認しながら詰める for (int i = 0; i < values.Count; i++) { if (values[i] < sbyte.MinValue || values[i] > sbyte.MaxValue) @@ -259,13 +258,13 @@ private NbtTag ParseTypedArray(char marker) return new NbtByteArray(result); } - // [I; は TAG_Int_Array - // 残りは TAG_Long_Array + // [I;はTAG_Int_Array + // 残りはTAG_Long_Array if (marker == 'I') { int[] result = new int[values.Count]; - // 各要素が Int の範囲に収まるか確認しながら詰める + // 各要素がIntの範囲に収まるか確認しながら詰める for (int i = 0; i < values.Count; i++) { if (values[i] < int.MinValue || values[i] > int.MaxValue) @@ -305,9 +304,18 @@ private string ParseKey() { char c = Peek(); + // 引用符で始まるキーは、エスケープを解いて読む if (c == '"' || c == '\'') { - return ParseQuotedString(); + string quoted = ParseQuotedString(); + + // バイナリの読み込みと同じく、キーには孤立サロゲートを許さない + if (Mutf8.HasLoneSurrogate(quoted)) + { + throw Malformed("Compound のキーが UTF-8 に写せない(孤立サロゲートを含む)"); + } + + return quoted; } string bare = ReadBareToken(); @@ -422,7 +430,7 @@ private long ReadHexDigits(int count) long value = 0; - // 指定桁数ぶん 16進数字を読む + // 指定桁数ぶん16進数字を読む for (int i = 0; i < count; i++) { char c = text[position + i]; @@ -442,16 +450,23 @@ private long ReadHexDigits(int count) private void AppendCodePoint(StringBuilder builder, long codePoint) { - // Unicode のコードポイント範囲を外れていないか確認する + // Unicodeのコードポイント範囲を外れていないか確認する if (codePoint < 0 || codePoint > 0x10FFFF) { throw Malformed($"コードポイントが範囲外: U+{codePoint:X}"); } + // サロゲートの範囲は、\uXXXXと同じくそのコード単位を1つ置く + if (codePoint >= 0xD800 && codePoint <= 0xDFFF) + { + builder.Append((char)codePoint); + return; + } + builder.Append(char.ConvertFromUtf32((int)codePoint)); } - /// Unicode 文字名によるエスケープ \N{...} を読む + /// Unicode文字名によるエスケープ\N{...}を読む private void AppendNamedCharacter(StringBuilder builder) { Expect('{'); @@ -471,7 +486,7 @@ private void AppendNamedCharacter(StringBuilder builder) string name = text.Substring(start, position - start); position += 1; - // .NET には Unicode 文字名の表が無い + // .NETにはUnicode文字名の表が無い // 実装間で表が揃わないため対応しない throw new SpringNbtException( ErrorCode.UnsupportedFeature, @@ -487,7 +502,7 @@ private NbtTag ParseUnquoted() throw Malformed($"値が来るべき位置に解釈できない文字がある: '{PeekOrNul()}'"); } - // bool(...) / uuid(...) の関数呼び出し + // bool(...) / uuid(...)の関数呼び出し SkipWhitespace(); if (PeekOrNul() == '(' && (token == "bool" || token == "uuid")) { @@ -523,7 +538,7 @@ private NbtTag ParseFunction(string name) if (name == "bool") { - // 0 以外を真とする + // 0以外を真とする if (ToIntegral(argument) != 0) { return new NbtByte(1); @@ -547,11 +562,11 @@ private NbtIntArray UuidToIntArray(NbtTag argument) throw Malformed($"UUID として解釈できない: {stringTag.Value}"); } - // UUID を上位から 32bit ずつ 4 要素の IntArray へ写す + // UUIDを上位から32bitずつ4要素のIntArrayへ写す byte[] bytes = guid.ToByteArray(bigEndian: true); int[] result = new int[4]; - // UUID の 16 バイトを、4 バイトずつ 4 つの i32 へ詰める + // UUIDの16バイトを、4バイトずつ4つのi32へ詰める for (int i = 0; i < 4; i++) { result[i] = (bytes[i * 4] << 24) @@ -565,7 +580,7 @@ private NbtIntArray UuidToIntArray(NbtTag argument) /// /// 数値トークンを解釈する - /// 数値として読めなければ null を返す(文字列として扱われる) + /// 数値として読めなければnullを返す(文字列として扱われる) /// private NbtTag? TryParseNumber(string token) { @@ -594,11 +609,10 @@ private NbtIntArray UuidToIntArray(NbtTag argument) bool unsignedSuffix = false; // 幅接尾辞を末尾から剥がす - // 16進では b/d/f が数字と紛れるため s/l だけを認める char last = body[body.Length - 1]; bool suffixAllowed; - // 16 進では b/d/f が数字なので、型の印として使えるのは s と l だけ + // 16進ではb/d/fが数字と紛れるためs/lだけを認める if (isHex) { suffixAllowed = last == 's' || last == 'S' || last == 'l' || last == 'L'; @@ -608,20 +622,20 @@ private NbtIntArray UuidToIntArray(NbtTag argument) suffixAllowed = "bBsSlLfFdD".IndexOf(last) >= 0; } - // 末尾 1 文字が型の印なら切り離す - // 1 文字だけの token は数字そのもの + // 末尾1文字が型の印なら切り離す + // 符号を除いた本体が1文字だけなら、接尾辞とみなさず剥がさない if (suffixAllowed && body.Length >= 2) { widthSuffix = char.ToLowerInvariant(last); body = body.Substring(0, body.Length - 1); - // 符号接尾辞 u / s は幅接尾辞の手前に置かれる + // 符号接尾辞u / sは幅接尾辞の手前に置かれる if (body.Length >= 2) { char signChar = body[body.Length - 1]; - // u / U は符号なしの印 - // 1.21.5 以降の拡張構文 + // u / Uは符号なしの印 + // 1.21.5以降の拡張構文 if (signChar == 'u' || signChar == 'U') { unsignedSuffix = true; @@ -669,7 +683,7 @@ private NbtIntArray UuidToIntArray(NbtTag argument) bool looksFloating = body.IndexOf('.') >= 0 || body.IndexOf('e') >= 0 || body.IndexOf('E') >= 0; - // 小数点・指数・f/d の印があれば浮動小数点として読む + // 小数点・指数・f/dの印があれば浮動小数点として読む if (looksFloating || widthSuffix == 'f' || widthSuffix == 'd') { if (!double.TryParse(body, NumberStyles.Float, CultureInfo.InvariantCulture, out double parsed)) @@ -690,7 +704,7 @@ private static bool IsBinaryBody(string body) return false; } - // 2進リテラルの本体は 0 と 1 と桁区切りだけ + // 2進リテラルの本体は0と1と桁区切りだけ foreach (char c in body) { if (c != '0' && c != '1' && c != '_') @@ -721,7 +735,7 @@ private NbtTag MakeFloating(double value, bool negative, char widthSuffix) return new NbtFloat((float)signed); } - // 接尾辞なしの小数は Double + // 接尾辞なしの小数はDouble if (widthSuffix == '\0' || widthSuffix == 'd') { return new NbtDouble(signed); @@ -796,8 +810,8 @@ private NbtTag MakeIntegral(ulong magnitude, bool negative, char widthSuffix, bo case 'd': return new NbtDouble(value); default: - // 接尾辞なしの整数は Int - // 暗黙に Long へ格上げしない + // 接尾辞なしの整数はInt + // 暗黙にLongへ格上げしない return new NbtInt((int)CheckRange(value, int.MinValue, int.MaxValue, "int")); } } @@ -816,7 +830,7 @@ private long ToSigned(ulong magnitude, bool negative) { if (negative) { - // long.MinValue の絶対値は long に収まらないため個別に扱う + // long.MinValueの絶対値はlongに収まらないため個別に扱う if (magnitude == 9223372036854775808UL) { return long.MinValue; @@ -934,7 +948,7 @@ private char Peek() } /// 末尾でも例外にしない先読み - /// 入力が尽きていれば NUL を返す + /// 入力が尽きていればNULを返す private char PeekOrNul() { if (position >= text.Length) diff --git a/csharp/src/SpringNBTLibrary/Nbt/TagType.cs b/csharp/src/SpringNBTLibrary/Nbt/TagType.cs index b67aead..fcf484e 100644 --- a/csharp/src/SpringNBTLibrary/Nbt/TagType.cs +++ b/csharp/src/SpringNBTLibrary/Nbt/TagType.cs @@ -1,14 +1,13 @@ namespace SpringNBTLibrary.Nbt; /// -/// NBT のタグ型 +/// NBTのタグ型 /// 値は仕様が定めるタグIDと一致する /// -/// 仕様: docs/spec/10-nbt-binary.md 1章 public enum TagType : byte { /// TAG_End (0) - /// Compound の終端を表す + /// Compoundの終端を表す End = 0, /// TAG_Byte (1) @@ -48,7 +47,7 @@ public enum TagType : byte LongArray = 12, } -/// の拡張メソッド +/// の拡張メソッド public static class TagTypeExtensions { /// @@ -90,13 +89,13 @@ public static string AsString(this TagType type) } /// - /// タグIDから を得る + /// タグIDからを得る /// 未知のIDなら例外を送出する /// /// 未知のタグID() public static TagType FromId(byte id) { - // 0..12 の範囲外はすべて不正なタグID + // 0..12の範囲外はすべて不正なタグID if (id > (byte)TagType.LongArray) { throw SpringNbtException.Malformed($"未知のタグID: {id}"); diff --git a/csharp/src/SpringNBTLibrary/SpringNBTLibrary.csproj b/csharp/src/SpringNBTLibrary/SpringNBTLibrary.csproj index 25e62a8..899d56a 100644 --- a/csharp/src/SpringNBTLibrary/SpringNBTLibrary.csproj +++ b/csharp/src/SpringNBTLibrary/SpringNBTLibrary.csproj @@ -7,7 +7,7 @@ SpringNBTLibrary 1.0.0 - Minecraft Java Edition の NBT / Anvil ワールドデータを読み書きするライブラリ + Minecraft Java EditionのNBT / Anvilワールドデータを読み書きするライブラリ MIT https://github.com/scriptarts/SpringNBTLibrary minecraft;nbt;anvil;mca @@ -15,7 +15,7 @@ README.md - + diff --git a/csharp/src/SpringNBTLibrary/SpringNbt.cs b/csharp/src/SpringNBTLibrary/SpringNbt.cs index 0c3b8dc..f68690c 100644 --- a/csharp/src/SpringNBTLibrary/SpringNbt.cs +++ b/csharp/src/SpringNBTLibrary/SpringNbt.cs @@ -1,29 +1,28 @@ namespace SpringNBTLibrary; /// -/// SpringNBTLibrary のライブラリ全体に関わる定数 +/// SpringNBTLibraryのライブラリ全体に関わる定数 /// /// -/// 仕様は docs/spec/ を唯一の正とする -/// 対象は 26.1 で導入されたワールド形式 (DataVersion 4786 以降) +/// 対象は26.1で導入されたワールド形式 (DataVersion 4786以降) /// public static class SpringNbt { /// - /// このライブラリが扱えるワールド形式の下限となる DataVersion (26.1) + /// このライブラリが扱えるワールド形式の下限となるDataVersion (26.1) /// /// /// - /// 26.1 で次元とプレイヤーデータの置き場が変わり、いまの形式になった + /// 26.1で次元とプレイヤーデータの置き場が変わり、いまの形式になった /// これ以降のバージョンは、形式が同じであればそのまま読み書きできる /// /// - /// これより古いワールドは構成そのものが違うので、 - /// 読み込み時に の対象になる + /// これより古いワールドは構成そのものが違う + /// そうしたチャンクは、読み込み時は既定で警告を出し、書き戻しは既定でにする /// /// public const int MinSupportedDataVersion = 4786; - /// 動作を確かめた Minecraft Java版の DataVersion (26.2) + /// 動作を確かめたMinecraft Java版のDataVersion (26.2) public const int TargetDataVersion = 4903; } diff --git a/csharp/src/SpringNBTLibrary/SpringNbtException.cs b/csharp/src/SpringNBTLibrary/SpringNbtException.cs index fe90c39..cbe5dc7 100644 --- a/csharp/src/SpringNBTLibrary/SpringNbtException.cs +++ b/csharp/src/SpringNBTLibrary/SpringNbtException.cs @@ -2,9 +2,8 @@ namespace SpringNBTLibrary; /// /// 本ライブラリが送出する例外 -/// 分類は で判別する +/// 分類はで判別する /// -/// 仕様: docs/spec/00-conventions.md 4章 / docs/adr/0005-unified-error-model.md public sealed class SpringNbtException : Exception { /// 分類とメッセージから例外を作る @@ -30,31 +29,31 @@ public override string ToString() return $"[{Code.AsString()}] {Message}"; } - /// の例外を作る + /// の例外を作る public static SpringNbtException Malformed(string message) { return new SpringNbtException(ErrorCode.MalformedData, message); } - /// の例外を作る + /// の例外を作る public static SpringNbtException InvalidArgument(string message) { return new SpringNbtException(ErrorCode.InvalidArgument, message); } - /// の例外を作る + /// の例外を作る public static SpringNbtException UnexpectedTagType(string message) { return new SpringNbtException(ErrorCode.UnexpectedTagType, message); } - /// の例外を作る + /// の例外を作る public static SpringNbtException LimitExceeded(string message) { return new SpringNbtException(ErrorCode.LimitExceeded, message); } - /// の例外を作る + /// の例外を作る public static SpringNbtException UnsupportedFeature(string message) { return new SpringNbtException(ErrorCode.UnsupportedFeature, message); diff --git a/csharp/src/SpringNBTLibrary/World/BitStorage.cs b/csharp/src/SpringNBTLibrary/World/BitStorage.cs index a41e1bc..888d7e2 100644 --- a/csharp/src/SpringNBTLibrary/World/BitStorage.cs +++ b/csharp/src/SpringNBTLibrary/World/BitStorage.cs @@ -1,15 +1,14 @@ namespace SpringNBTLibrary.World; /// -/// 添字を 64bit 整数の配列へ詰めた表現 -/// 1.16 以降の**跨ぎなし**パッキング +/// 添字を64bit整数の配列へ詰めた表現 +/// 1.16以降の**跨ぎなし**パッキング /// /// /// -/// 1 つの i64 に入りきらない分は、その i64 の残りビットを未使用のまま捨て、 -/// 次の i64 の最下位ビットから始める +/// 1つのi64に入りきらない分は、そのi64の残りビットを未使用のまま捨て、 +/// 次のi64の最下位ビットから始める /// -/// 仕様: docs/spec/31-paletted-container.md 2章 /// public sealed class BitStorage { @@ -22,14 +21,14 @@ private BitStorage(long[] data, int bitsPerEntry, int entryCount) EntryCount = entryCount; } - /// 1 エントリあたりのビット数 + /// 1エントリあたりのビット数 public int BitsPerEntry { get; } /// エントリ数 - /// ブロックなら 4096、バイオームなら 64 + /// ブロックなら4096、バイオームなら64 public int EntryCount { get; } - /// 1 つの i64 に入るエントリ数 + /// 1つのi64に入るエントリ数 public int ValuesPerLong => 64 / BitsPerEntry; /// すべてゼロで初期化した記憶域を作る @@ -44,13 +43,13 @@ public static BitStorage Create(int bitsPerEntry, int entryCount) } /// - /// 既存の i64 配列から作る + /// 既存のi64配列から作る /// - /// packed な配列 + /// packedな配列 /// パレット長から求めたビット幅 /// エントリ数 /// - /// true なら、配列長が期待値と違う場合に配列長からビット幅を逆算して読む + /// trueなら、配列長が期待値と違う場合に配列長からビット幅を逆算して読む /// 第三者ツールが書いたデータの救済用 /// /// @@ -86,7 +85,7 @@ public static BitStorage FromLongs(long[] data, int bitsPerEntry, int entryCount $"data の長さ {data.Length} long に合うビット幅が無い(エントリ数 {entryCount})"); } - /// 必要な i64 の個数を求める + /// 必要なi64の個数を求める public static int LongCount(int bitsPerEntry, int entryCount) { int valuesPerLong = 64 / bitsPerEntry; @@ -106,7 +105,7 @@ public int Get(int index) int bitOffset = (index % valuesPerLong) * BitsPerEntry; long mask = (1L << BitsPerEntry) - 1; - // 符号付きの算術シフトだと上位ビットが伸びるので、いったん ulong にして論理シフトする + // 符号付きの算術シフトだと上位ビットが伸びるので、いったんulongにして論理シフトする return (int)(((ulong)data[longIndex] >> bitOffset) & (ulong)mask); } @@ -134,7 +133,7 @@ public void Set(int index, int value) data[longIndex] = (data[longIndex] & ~mask) | ((long)value << bitOffset & mask); } - /// packed な配列を返す + /// packedな配列を返す /// 内部の配列をそのまま返す(コピーしない) public long[] ToLongs() => data; diff --git a/csharp/src/SpringNBTLibrary/World/BlockPos.cs b/csharp/src/SpringNBTLibrary/World/BlockPos.cs index ac3f9eb..2b55fe7 100644 --- a/csharp/src/SpringNBTLibrary/World/BlockPos.cs +++ b/csharp/src/SpringNBTLibrary/World/BlockPos.cs @@ -6,7 +6,6 @@ namespace SpringNBTLibrary.World; /// /// ブロックの絶対座標 /// -/// 仕様: docs/spec/30-chunk-format.md 5章 public sealed class BlockPos : IEquatable { /// 座標を指定して作る diff --git a/csharp/src/SpringNBTLibrary/World/BlockState.cs b/csharp/src/SpringNBTLibrary/World/BlockState.cs index ac92f4b..d3a5281 100644 --- a/csharp/src/SpringNBTLibrary/World/BlockState.cs +++ b/csharp/src/SpringNBTLibrary/World/BlockState.cs @@ -10,13 +10,10 @@ namespace SpringNBTLibrary.World; /// /// /// プロパティは**常に名前の昇順で保持する** -/// こうしておくと文字列表現が一意になり、 -/// 全言語で同じ出力になる -/// Minecraft が書き出した並び順は -/// がパレットを生の NBT のまま持つことで守られるので、 -/// 触っていないブロックの並びが崩れることはない +/// こうしておくと文字列表現が一意になり、全言語で同じ出力になる +/// Minecraftが書き出した並び順は、がパレットを生のNBTのまま持つことで守られる +/// そのため、触っていないブロックの並びが崩れることはない /// -/// 仕様: docs/spec/30-chunk-format.md 2.1章 /// public sealed class BlockState : IEquatable { @@ -25,10 +22,10 @@ public sealed class BlockState : IEquatable /// 名前とプロパティを指定して作る /// /// ブロックID - /// 名前空間が省略されていたら minecraft: を補う + /// 名前空間が省略されていたらminecraft:を補う /// /// プロパティ - /// null なら空 + /// nullなら空 public BlockState(string name, IEnumerable>? properties = null) { ArgumentNullException.ThrowIfNull(name); @@ -56,7 +53,7 @@ public BlockState(string name, IEnumerable>? proper public IReadOnlyDictionary Properties => properties; /// プロパティを取得する - /// 無ければ null + /// 無ければnull public string? Property(string key) { if (properties.TryGetValue(key, out string? value)) @@ -67,7 +64,7 @@ public BlockState(string name, IEnumerable>? proper return null; } - /// プロパティを 1 つ差し替えた新しい状態を返す + /// プロパティを1つ差し替えた新しい状態を返す public BlockState With(string key, string value) { ArgumentNullException.ThrowIfNull(key); @@ -79,10 +76,10 @@ public BlockState With(string key, string value) } /// - /// minecraft:oak_stairs[facing=north,half=top] 形式の文字列から作る + /// minecraft:oak_stairs[facing=north,half=top]形式の文字列から作る /// /// - /// 形式が不正な場合() + /// 形式が不正な場合() /// public static BlockState Parse(string text) { @@ -115,7 +112,7 @@ public static BlockState Parse(string text) return state; } - // "key=value" をカンマ区切りで読む + // "key=value"をカンマ区切りで読む foreach (string pair in body.Split(',')) { int equals = pair.IndexOf('=', StringComparison.Ordinal); @@ -145,9 +142,9 @@ public static BlockState Parse(string text) return state; } - /// パレット要素の NBT から作る + /// パレット要素のNBTから作る /// - /// Name が無い、または Properties の値が文字列でない場合 + /// Nameが無い、またはPropertiesの値が文字列でない場合 /// public static BlockState FromNbt(NbtCompound nbt) { @@ -160,7 +157,7 @@ public static BlockState FromNbt(NbtCompound nbt) return state; } - // Properties の値はすべて文字列(数値や真偽値も文字列で入る) + // Propertiesの値はすべて文字列(数値や真偽値も文字列で入る) foreach (KeyValuePair entry in propertiesTag) { if (entry.Value is not NbtString text) @@ -175,10 +172,10 @@ public static BlockState FromNbt(NbtCompound nbt) return state; } - /// パレット要素の NBT へ変換する + /// パレット要素のNBTへ変換する /// - /// プロパティが空なら Properties キー自体を出力しない - /// Minecraft と同じ振る舞い + /// プロパティが空ならPropertiesキー自体を出力しない + /// Minecraftと同じ振る舞い /// public NbtCompound ToNbt() { @@ -202,7 +199,7 @@ public NbtCompound ToNbt() return result; } - /// 名前空間が省略されていたら minecraft: を補う + /// 名前空間が省略されていたらminecraft:を補う private static string Normalize(string name) { if (name.Contains(':', StringComparison.Ordinal)) @@ -226,11 +223,11 @@ public bool Equals(BlockState? other) return false; } - // 昇順で持っているので、順に突き合わせれば足りる + // 昇順で持っているので、順に比べれば足りる using IEnumerator> left = properties.GetEnumerator(); using IEnumerator> right = other.properties.GetEnumerator(); - // 名前と値を先頭から突き合わせる + // 名前と値を先頭から比べる // 並びは昇順に揃っている while (left.MoveNext() && right.MoveNext()) { @@ -255,7 +252,7 @@ public bool Equals(BlockState? other) public override int GetHashCode() => HashCode.Combine(Name, properties.Count); /// - /// minecraft:oak_stairs[facing=north,half=top] 形式の文字列を返す + /// minecraft:oak_stairs[facing=north,half=top]形式の文字列を返す /// public override string ToString() { @@ -271,7 +268,7 @@ public override string ToString() // 名前の昇順で並べるので、同じ状態なら必ず同じ文字列になる foreach (KeyValuePair entry in properties) { - // 2 つ目以降の前に区切りのカンマを置く + // 2つ目以降の前に区切りのカンマを置く if (!first) { builder.Append(','); diff --git a/csharp/src/SpringNBTLibrary/World/Chunk.cs b/csharp/src/SpringNBTLibrary/World/Chunk.cs index 568995f..5501add 100644 --- a/csharp/src/SpringNBTLibrary/World/Chunk.cs +++ b/csharp/src/SpringNBTLibrary/World/Chunk.cs @@ -3,14 +3,14 @@ namespace SpringNBTLibrary.World; -/// DataVersion が対象と違ったときの動作 +/// DataVersionが扱える形式より古いときの動作 public enum VersionMismatchAction { /// 警告コールバックを呼んで続行する /// 既定 Warn, - /// の例外にする + /// の例外にする Error, /// 何もしない @@ -18,21 +18,20 @@ public enum VersionMismatchAction } /// チャンク読み込みのオプション -/// 仕様: docs/spec/30-chunk-format.md 5章 public sealed class ChunkReadOptions { /// 既定のオプション public static ChunkReadOptions Default { get; } = new ChunkReadOptions(); - /// DataVersion が扱える形式より古いときの動作 + /// DataVersionが扱える形式より古いときの動作 public VersionMismatchAction OnVersionMismatch { get; set; } = VersionMismatchAction.Warn; /// 警告の通知先 - /// null なら何もしない + /// nullなら何もしない public Action? OnWarning { get; set; } /// - /// data の長さが期待値と違うとき、長さからビット幅を逆算して読むか + /// dataの長さが期待値と違うとき、長さからビット幅を逆算して読むか /// 第三者ツールが書いたデータの救済用 /// public bool LenientBitStorage { get; set; } @@ -45,33 +44,32 @@ public sealed class ChunkWriteOptions public static ChunkWriteOptions Default { get; } = new ChunkWriteOptions(); /// - /// 対象バージョン以外の DataVersion を持つチャンクの書き戻しを許すか + /// 扱える形式より古いDataVersionを持つチャンクの書き戻しを許すか /// /// - /// 既定は false - /// 古いワールドを黙って新形式で上書きし、 - /// 利用者が気づかないうちに壊すことを防ぐため(docs/adr/0003-version-policy.md) + /// 既定はfalse + /// エラーを出さずに古いワールドを新形式で上書きし、 + /// 利用者が気づかないうちに使えなくすることを防ぐため /// public bool AllowForeignDataVersion { get; set; } } /// -/// チャンク 1 つ分 -/// 地形の読み書きの入口 +/// チャンク1つ分 +/// 地形の読み書きはここから行う /// /// /// -/// 読んだ NBT をそのまま保持し、変更した部分だけを書き戻す -/// 未知のキーを落とさないので、将来の追加要素があってもデータを壊さない +/// 読んだNBTをそのまま保持し、書き戻すときに作り直すのはセクションだけにする +/// 未知のキーを落とさないので、将来の追加要素があってもデータをおかしくしない /// -/// 仕様: docs/spec/30-chunk-format.md /// public sealed class Chunk { - /// セクション 1 つに入るブロック数 + /// セクション1つに入るブロック数 public const int BlocksPerSection = 4096; - /// セクション 1 つに入るバイオームのエントリ数(4×4×4 単位) + /// セクション1つに入るバイオームのエントリ数(4×4×4単位) public const int BiomesPerSection = 64; /// @@ -100,10 +98,10 @@ private Chunk(NbtCompound root) public int Z => root.GetInt("zPos"); /// 最下段セクションのY位置 - /// オーバーワールドは -4 + /// オーバーワールドは-4 public int MinSectionY => root.GetInt("yPos"); - /// 生成段階(minecraft:full など) + /// 生成段階(minecraft:fullなど) public string Status => root.GetString("Status"); /// 生成が完了しているか @@ -116,11 +114,10 @@ private Chunk(NbtCompound root) /// /// /// ブロックやバイオームを書き換えると立つ - /// はこれが立っているチャンクだけを書き戻す + /// はこれが立っているチャンクだけを書き戻す /// /// - /// を直接いじった場合はここが立たないので、 - /// 自分で true にすること + /// を直接いじった場合は立たないので、自分でtrueにすること /// /// public bool IsModified { get; set; } @@ -129,12 +126,12 @@ private Chunk(NbtCompound root) /// 昇順 public IEnumerable SectionYs => sections.Keys; - /// 元の NBT + /// 元のNBT /// 解釈していないキーもここに残っている public NbtCompound Raw => root; /// - /// NBT からチャンクを読む + /// NBTからチャンクを読む /// /// /// 必須のキーが無い、または構造が想定と違う場合 @@ -164,7 +161,7 @@ public static Chunk FromNbt(NbtCompound nbt, ChunkReadOptions? options = null) return chunk; } - // 並び順に依存しないよう、Y から索引を作る + // 並び順に依存しないよう、Yから索引を作る foreach (NbtTag entry in sectionList) { if (entry is not NbtCompound sectionTag) @@ -180,7 +177,7 @@ public static Chunk FromNbt(NbtCompound nbt, ChunkReadOptions? options = null) return chunk; } - /// DataVersion を検査し、オプションに従って警告またはエラーにする + /// DataVersionを検査し、オプションに従って警告またはエラーにする private void CheckDataVersion(ChunkReadOptions options) { int version = DataVersion; @@ -212,12 +209,12 @@ private void CheckDataVersion(ChunkReadOptions options) } /// - /// NBT へ書き戻す - /// 変更したセクションだけを反映し、他のキーはそのまま残す + /// NBTへ書き戻す + /// 全セクションを書き戻し、他のキーはそのまま残す /// /// - /// DataVersion が対象と違い、かつ - /// が false の場合 + /// DataVersionが扱える形式より古く、かつ + /// がfalseの場合 /// public NbtCompound ToNbt(ChunkWriteOptions? options = null) { @@ -234,7 +231,7 @@ public NbtCompound ToNbt(ChunkWriteOptions? options = null) int version = DataVersion; - // 形式の違う古いチャンクは、書き戻すと壊しかねない + // 形式の違う古いチャンクは、書き戻すと使えなくなりかねない if (version < SpringNbt.MinSupportedDataVersion && !effective.AllowForeignDataVersion) { string message = string.Create( @@ -246,7 +243,7 @@ public NbtCompound ToNbt(ChunkWriteOptions? options = null) message + "。許可するなら ChunkWriteOptions.AllowForeignDataVersion を立てること"); } - // DataVersion は読んだ値のまま残す + // DataVersionは読んだ値のまま残す // 書き換えると、そのワールドを開くゲーム側の判断を誤らせる if (sections.Count == 0) @@ -256,7 +253,7 @@ public NbtCompound ToNbt(ChunkWriteOptions? options = null) NbtList sectionList = new NbtList(TagType.Compound); - // Y の昇順で書き出す + // Yの昇順で書き出す foreach (ChunkSection section in sections.Values) { sectionList.Add(section.ToNbt()); @@ -267,7 +264,7 @@ public NbtCompound ToNbt(ChunkWriteOptions? options = null) } /// Y位置からセクションを得る - /// 無ければ null + /// 無ければnull public ChunkSection? Section(int sectionY) { if (sections.TryGetValue(sectionY, out ChunkSection? section)) @@ -285,7 +282,7 @@ public NbtCompound ToNbt(ChunkWriteOptions? options = null) /// 絶対Y座標 /// チャンク内相対Z座標 (0..15) /// ブロック - /// セクションが無い、または block_states を持たない場合は null + /// セクションが無い、またはblock_statesを持たない場合はnull public BlockState? GetBlock(int x, int y, int z) { CheckLocalCoordinates(x, z); @@ -309,7 +306,7 @@ public NbtCompound ToNbt(ChunkWriteOptions? options = null) /// /// ブロックを設定する - /// minecraft:oak_stairs[facing=north] の形の文字列で指定する + /// minecraft:oak_stairs[facing=north]の形の文字列で指定する /// /// /// 文字列を解釈できない場合() @@ -325,14 +322,11 @@ public void SetBlock(int x, int y, int z, string state) /// /// /// 置き換えによって不整合になる付随データ(block_entities / - /// block_ticks / fluid_ticks のうち、その座標を指すもの)は - /// 同時に取り除く - /// 残すとブロックと中身が食い違い、 - /// Minecraft 側で予期しない挙動になるため - /// 仕様: docs/spec/30-chunk-format.md 2.4章 + /// block_ticks / fluid_ticksのうち、その座標を指すもの)は同時に取り除く + /// 残すとブロックと中身が食い違い、Minecraft側で予期しない挙動になるため /// /// - /// 対象のセクションが無い、または block_states を持たない場合 + /// 対象のセクションが無い、またはblock_statesを持たない場合 /// () /// public void SetBlock(int x, int y, int z, BlockState state) @@ -354,7 +348,7 @@ public void SetBlock(int x, int y, int z, BlockState state) } // 同じ状態を置き直すだけなら、付随データを触る理由がない - // プロパティの並び順に左右されないよう、NBT ではなく BlockState として比べる + // プロパティの並び順に左右されないよう、NBTではなくBlockStateとして比べる BlockState? current = GetBlock(x, y, z); if (current is not null && current.Equals(state)) @@ -371,15 +365,15 @@ public void SetBlock(int x, int y, int z, BlockState state) /// その座標を指す付随データを取り除く /// /// - /// block_entities / block_ticks / fluid_ticks の要素は - /// いずれも x y z を**絶対座標**で持つ + /// block_entities / block_ticks / fluid_ticksの要素は + /// いずれもx y zを**絶対座標**で持つ /// private void RemoveBlockData(int x, int y, int z) { int absoluteX = (X * 16) + x; int absoluteZ = (Z * 16) + z; - // 3 つのリストは形が同じなので、まとめて同じ処理をかける + // 3つのリストは形が同じなので、まとめて同じ処理をかける foreach (string key in BlockDataKeys) { NbtList? list = root.OptList(key); @@ -420,10 +414,10 @@ private static bool MatchesPosition(NbtCompound entry, int x, int y, int z) /// /// バイオームを取得する - /// 4×4×4 の単位なので、座標は自動的に丸められる + /// 4×4×4の単位なので、座標は自動的に丸められる /// /// バイオームID - /// セクションが無い場合は null + /// セクションが無い場合はnull public string? GetBiome(int x, int y, int z) { CheckLocalCoordinates(x, z); @@ -446,7 +440,7 @@ private static bool MatchesPosition(NbtCompound entry, int x, int y, int z) } /// バイオームを設定する - /// 4×4×4 の単位 + /// 4×4×4の単位 public void SetBiome(int x, int y, int z, string biome) { ArgumentNullException.ThrowIfNull(biome); @@ -467,12 +461,11 @@ public void SetBiome(int x, int y, int z, string biome) } /// - /// Heightmaps を削除し、Minecraft に再計算させる + /// Heightmapsを削除し、Minecraftに再計算させる /// /// /// 本ライブラリは高さマップを再計算しない /// ブロックを改変したら呼ぶこと - /// (docs/adr/0004-defer-heightmap-recalc.md) /// public void ClearHeightmaps() { @@ -481,7 +474,7 @@ public void ClearHeightmaps() } /// - /// isLightOn を 0 にし、光源の再計算を促す + /// isLightOnを0にし、光源の再計算を促す /// public void InvalidateLighting() { @@ -500,18 +493,18 @@ public void Compact() } /// セクション内のブロック添字 - /// & 15 により負のY座標でも正しく求まる + /// & 15により負のY座標でも正しく求まる public static int BlockIndex(int x, int y, int z) => ((y & 15) * 256) + ((z & 15) * 16) + (x & 15); /// セクション内のバイオーム添字 - /// 1 エントリが 4×4×4 ブロック + /// 1エントリが4×4×4ブロック public static int BiomeIndex(int x, int y, int z) => (((y & 15) / 4) * 16) + (((z & 15) / 4) * 4) + ((x & 15) / 4); private static void CheckLocalCoordinates(int x, int z) { - // チャンク内相対座標は 0..15 でなければならない + // チャンク内相対座標は0..15でなければならない if (x < 0 || x > 15 || z < 0 || z > 15) { throw SpringNbtException.InvalidArgument(string.Create( diff --git a/csharp/src/SpringNBTLibrary/World/ChunkSection.cs b/csharp/src/SpringNBTLibrary/World/ChunkSection.cs index 5efc4bb..cdc6a66 100644 --- a/csharp/src/SpringNBTLibrary/World/ChunkSection.cs +++ b/csharp/src/SpringNBTLibrary/World/ChunkSection.cs @@ -3,14 +3,13 @@ namespace SpringNBTLibrary.World; /// -/// チャンクを Y 方向に 16 ブロックずつ区切った 16×16×16 の立方体 +/// チャンクをY方向に16ブロックずつ区切った16×16×16の立方体 /// /// /// -/// BlockLight / SkyLight などの解釈していないキーは元の NBT に残り、 +/// BlockLight / SkyLightなどの解釈していないキーは元のNBTに残り、 /// 書き戻しでそのまま出力される /// -/// 仕様: docs/spec/30-chunk-format.md 2章 /// public sealed class ChunkSection { @@ -23,15 +22,15 @@ private ChunkSection(NbtCompound raw, int y) } /// セクションのY位置 - /// オーバーワールドは -5..20 + /// オーバーワールドは-5..20 public int Y { get; } /// ブロック状態 - /// 持たないセクション(光源専用)では null + /// 持たないセクション(光源専用)ではnull public PalettedContainer? BlockStates { get; private set; } /// バイオーム - /// 持たないセクションでは null + /// 持たないセクションではnull public PalettedContainer? Biomes { get; private set; } /// ブロック状態を持つか @@ -40,11 +39,11 @@ private ChunkSection(NbtCompound raw, int y) /// バイオームを持つか public bool HasBiomes => Biomes is not null; - /// 元の NBT + /// 元のNBT /// 解釈していないキーもここに残っている public NbtCompound Raw => raw; - /// NBT からセクションを読む + /// NBTからセクションを読む public static ChunkSection FromNbt(NbtCompound nbt, ChunkReadOptions options) { ArgumentNullException.ThrowIfNull(nbt); @@ -52,7 +51,7 @@ public static ChunkSection FromNbt(NbtCompound nbt, ChunkReadOptions options) NbtCompound? blockStates = nbt.OptCompound("block_states"); - // 光源専用のセクションは block_states を持たない + // 光源専用のセクションはblock_statesを持たない if (blockStates is not null) { section.BlockStates = PalettedContainer.FromNbt( @@ -71,7 +70,7 @@ public static ChunkSection FromNbt(NbtCompound nbt, ChunkReadOptions options) return section; } - /// NBT へ書き戻す + /// NBTへ書き戻す /// 解釈していないキーはそのまま残る public NbtCompound ToNbt() { diff --git a/csharp/src/SpringNBTLibrary/World/Cuboid.cs b/csharp/src/SpringNBTLibrary/World/Cuboid.cs index fab50ab..e56ac9c 100644 --- a/csharp/src/SpringNBTLibrary/World/Cuboid.cs +++ b/csharp/src/SpringNBTLibrary/World/Cuboid.cs @@ -3,11 +3,11 @@ namespace SpringNBTLibrary.World; /// -/// ブロック座標の直方体な範囲 +/// ブロック座標の直方体の範囲 /// /// /// 両端を含む -/// Of(0, 0, 0, 0, 0, 0) は 1 ブロック +/// Of(0, 0, 0, 0, 0, 0)は1ブロック /// 範囲内のブロックを順に処理したいときに使う /// public sealed class Cuboid : IEquatable @@ -32,31 +32,31 @@ public Cuboid(BlockPos first, BlockPos second) public static Cuboid Of(int x1, int y1, int z1, int x2, int y2, int z2) => new Cuboid(new BlockPos(x1, y1, z1), new BlockPos(x2, y2, z2)); - /// X の最小値 + /// Xの最小値 public int MinX { get; } - /// Y の最小値 + /// Yの最小値 public int MinY { get; } - /// Z の最小値 + /// Zの最小値 public int MinZ { get; } - /// X の最大値(含む) + /// Xの最大値(含む) public int MaxX { get; } - /// Y の最大値(含む) + /// Yの最大値(含む) public int MaxY { get; } - /// Z の最大値(含む) + /// Zの最大値(含む) public int MaxZ { get; } - /// X 方向の長さ + /// X方向の長さ public int SizeX => MaxX - MinX + 1; - /// Y 方向の長さ + /// Y方向の長さ public int SizeY => MaxY - MinY + 1; - /// Z 方向の長さ + /// Z方向の長さ public int SizeZ => MaxZ - MinZ + 1; /// 含まれるブロックの個数 @@ -69,16 +69,16 @@ public bool Contains(int x, int y, int z) => /// /// 範囲内の座標を順に返す /// - /// 並びは Y、Z、X の順で、X がいちばん内側で動く + /// 並びはY、Z、Xの順で、Xがいちばん内側で動く public IEnumerable Positions() { - // 内側から X が動くので、同じチャンクの並びを続けて触れる + // Xがいちばん内側で動くので、同じチャンクの並びを続けて扱える for (int y = MinY; y <= MaxY; y++) { - // Z を進めながら、その行の X を端から端まで返す + // Zを進めながら、その行のXを端から端まで返す for (int z = MinZ; z <= MaxZ; z++) { - // X はチャンク内で連続するので、まとめて触れる + // Xはチャンク内で連続するので、まとめて扱える for (int x = MinX; x <= MaxX; x++) { yield return new BlockPos(x, y, z); diff --git a/csharp/src/SpringNBTLibrary/World/Dimension.cs b/csharp/src/SpringNBTLibrary/World/Dimension.cs index ddcfcf7..c3a3a66 100644 --- a/csharp/src/SpringNBTLibrary/World/Dimension.cs +++ b/csharp/src/SpringNBTLibrary/World/Dimension.cs @@ -4,15 +4,13 @@ namespace SpringNBTLibrary.World; /// -/// ワールド内の次元 1 つ分 -/// region/ entities/ poi/ をまとめて扱う +/// ワールド内の次元1つ分 +/// region/ entities/ poi/をまとめて扱う /// /// /// -/// ブロックの取得・設定は**絶対ワールド座標**で行い、 -/// リージョン・チャンク・セクションの解決は内部で済ませる +/// ブロックの取得・設定は**絶対ワールド座標**で行い、リージョン・チャンク・セクションの解決は内部で済ませる /// -/// 仕様: docs/spec/40-world-layout.md 4章 /// public sealed class Dimension : IDisposable { @@ -40,30 +38,30 @@ internal Dimension(string id, string directory, WorldOpenOptions options) this.options = options; } - /// 次元ID(minecraft:overworld など) + /// 次元ID(minecraft:overworldなど) public string Id { get; } /// この次元のディレクトリ public string Directory { get; } /// 地形のリージョンフォルダ - /// 無ければ null + /// 無ければnull public RegionFolder? RegionFolder => Folder(ref regions, "region"); /// エンティティのリージョンフォルダ - /// 無ければ null + /// 無ければnull public RegionFolder? EntityFolder => Folder(ref entities, "entities"); - /// POI のリージョンフォルダ - /// 無ければ null + /// POIのリージョンフォルダ + /// 無ければnull public RegionFolder? PoiFolder => Folder(ref poi, "poi"); /// - /// data/minecraft/<name>.dat を読む - /// 存在しなければ null + /// data/minecraft/<name>.datを読む + /// 存在しなければnull /// /// - /// 次元ごとの world_border / raids / chunk_tickets などが入る + /// 次元ごとのworld_border / raids / chunk_ticketsなどが入る /// public NbtCompound? DataFile(string name) { @@ -94,7 +92,7 @@ public IEnumerable ChunkPositions() /// /// チャンクを読む - /// 存在しなければ null + /// 存在しなければnull /// /// 読み込んだチャンクはキャッシュされ、次回は同じインスタンスが返る public Chunk? Chunk(int chunkX, int chunkZ) @@ -146,7 +144,7 @@ public void SaveChunk(Chunk chunk) /// /// 絶対座標でブロックを取得する - /// チャンクが無ければ null + /// チャンクが無ければnull /// public BlockState? GetBlock(int x, int y, int z) { @@ -162,7 +160,7 @@ public void SaveChunk(Chunk chunk) /// /// 絶対座標でブロックを設定する - /// minecraft:oak_stairs[facing=north] の形の文字列で指定する + /// minecraft:oak_stairs[facing=north]の形の文字列で指定する /// /// /// 文字列を解釈できない場合() @@ -177,8 +175,8 @@ public void SetBlock(int x, int y, int z, string state) /// 絶対座標でブロックを設定する /// /// - /// 変更したチャンクには印が付き、 でまとめて書き戻される - /// 本ライブラリはチャンクを新規生成しないので、存在しない座標はエラーになる + /// 変更したチャンクには印が付き、でまとめて書き戻される + /// 本ライブラリはチャンクを新規生成しないので、チャンクが存在しない座標はエラーになる /// public void SetBlock(int x, int y, int z, BlockState state) { @@ -199,7 +197,7 @@ public void SetBlock(int x, int y, int z, BlockState state) } /// 絶対座標でバイオームを取得する - /// 4×4×4 の単位 + /// 4×4×4の単位 public string? GetBiome(int x, int y, int z) { Chunk? chunk = Chunk(x >> 4, z >> 4); @@ -213,7 +211,7 @@ public void SetBlock(int x, int y, int z, BlockState state) } /// 絶対座標でバイオームを設定する - /// 4×4×4 の単位 + /// 4×4×4の単位 public void SetBiome(int x, int y, int z, string biome) { ArgumentNullException.ThrowIfNull(biome); @@ -256,7 +254,7 @@ public void Flush() // 開いているフォルダだけを書き出す foreach (RegionFolder? folder in new[] { regions, entities, poi }) { - // 開いているフォルダだけを書き出す + // 開いていないフォルダは触らない if (folder is not null) { folder.Flush(); @@ -281,7 +279,7 @@ public void Close() // 開いているフォルダだけを閉じる foreach (RegionFolder? folder in new[] { regions, entities, poi }) { - // 開いているフォルダだけを閉じる + // 開いていないフォルダは触らない if (folder is not null) { folder.Close(); @@ -295,7 +293,7 @@ public void Close() public void Dispose() => Close(); /// フォルダを遅延して開く - /// 存在しなければ null のまま + /// 存在しなければnullのまま private RegionFolder? Folder(ref RegionFolder? slot, string name) { EnsureOpen(); @@ -344,7 +342,7 @@ private void EnsureOpen() } } - /// チャンク座標を 1 つの long に詰めてキャッシュの鍵にする + /// チャンク座標を1つのlongに詰めてキャッシュのキーにする private static long ChunkKey(int chunkX, int chunkZ) => ((long)chunkX << 32) | (uint)chunkZ; } diff --git a/csharp/src/SpringNBTLibrary/World/MinecraftWorld.cs b/csharp/src/SpringNBTLibrary/World/MinecraftWorld.cs index a46bbf4..c0fc958 100644 --- a/csharp/src/SpringNBTLibrary/World/MinecraftWorld.cs +++ b/csharp/src/SpringNBTLibrary/World/MinecraftWorld.cs @@ -14,10 +14,10 @@ public sealed class WorldOpenOptions public bool Writable { get; set; } /// - /// session.lock の確認を飛ばすか + /// session.lockの確認を飛ばすか /// /// - /// Minecraft が起動中のワールドへ書き込むとデータが壊れる + /// Minecraftが起動中のワールドへ書き込むとデータがおかしくなる /// 既定では書き込みモードで開くときに必ず確認する /// これを立てるのは自己責任 /// @@ -31,14 +31,12 @@ public sealed class WorldOpenOptions } /// -/// Minecraft Java版のセーブデータ 1 つ分 +/// Minecraft Java版のセーブデータ1つ分 /// /// /// -/// 26.x では構成が大きく変わっており、標準の3次元も -/// dimensions/<名前空間>/<パス>/ の下に並ぶ +/// 26.xでは構成が大きく変わっており、標準の3次元もdimensions/<名前空間>/<パス>/の下に並ぶ /// -/// 仕様: docs/spec/40-world-layout.md /// public sealed class MinecraftWorld : IDisposable { @@ -56,15 +54,15 @@ private MinecraftWorld(string directory, WorldOpenOptions options, NamedTag leve /// ワールドディレクトリのパス public string Directory { get; } - /// level.dat の内容 + /// level.datの内容 public LevelData Level { get; } /// /// ワールドを開く /// /// - /// ディレクトリや level.dat が無い場合、 - /// または書き込みモードで session.lock を取得できない場合 + /// ディレクトリやlevel.datが無い場合、 + /// または書き込みモードでsession.lockを取得できない場合 /// public static MinecraftWorld Open(string directory, WorldOpenOptions? options = null) { @@ -93,7 +91,7 @@ public static MinecraftWorld Open(string directory, WorldOpenOptions? options = throw new SpringNbtException(ErrorCode.Io, $"level.dat が無い: {levelPath}"); } - // 書き込むなら、Minecraft が起動中でないことを先に確かめる + // 書き込むなら、Minecraftが起動中でないことを先に確かめる if (effective.Writable && !effective.IgnoreSessionLock) { CheckSessionLock(directory); @@ -104,7 +102,7 @@ public static MinecraftWorld Open(string directory, WorldOpenOptions? options = } /// - /// session.lock を排他で開けるか確かめる + /// session.lockを排他で開けるか確かめる /// private static void CheckSessionLock(string directory) { @@ -131,12 +129,11 @@ private static void CheckSessionLock(string directory) } /// - /// data/minecraft/<name>.dat を読む - /// 存在しなければ null + /// data/minecraft/<name>.datを読む + /// 存在しなければnull /// /// - /// 26.x では game_rules / weather / world_gen_settings などが - /// この形で level.dat から分離されている + /// 26.xではgame_rules / weather / world_gen_settingsなどがこの形でlevel.datから分離されている /// public NbtCompound? DataFile(string name) { @@ -157,7 +154,7 @@ public IEnumerable DimensionIds() EnsureOpen(); string root = Path.Combine(Directory, "dimensions"); - // dimensions/ が無いワールドには次元が 1 つも無い + // dimensions/が無いワールドには次元が1つも無い if (!System.IO.Directory.Exists(root)) { yield break; @@ -165,12 +162,12 @@ public IEnumerable DimensionIds() List found = new List(); - // dimensions/<名前空間>/<パス>/ の 2 段を辿る + // dimensions/<名前空間>/<パス>/の2段を辿る foreach (string namespaceDir in System.IO.Directory.EnumerateDirectories(root)) { string namespaceName = Path.GetFileName(namespaceDir); - // dimensions/<名前空間>/<パス>/ の 2 段目を次元の名前として拾う + // dimensions/<名前空間>/<パス>/の2段目を次元の名前として拾う foreach (string pathDir in System.IO.Directory.EnumerateDirectories(namespaceDir)) { found.Add(namespaceName + ":" + Path.GetFileName(pathDir)); @@ -180,7 +177,7 @@ public IEnumerable DimensionIds() // 走査順がファイルシステム依存にならないよう並べる found.Sort(StringComparer.Ordinal); - // 並べ替えたものを 1 件ずつ返す + // 並べ替えたものを1件ずつ返す foreach (string id in found) { yield return id; @@ -189,11 +186,11 @@ public IEnumerable DimensionIds() /// /// 次元を得る - /// ディレクトリが無ければ null + /// ディレクトリが無ければnull /// /// - /// minecraft:overworld のような名前空間つきのID - /// 名前空間が省略されていたら minecraft: を補う + /// minecraft:overworldのような名前空間つきのID + /// 名前空間が省略されていたらminecraft:を補う /// public Dimension? Dimension(string dimensionId) { @@ -228,7 +225,7 @@ public IEnumerable PlayerIds() EnsureOpen(); string directory = Path.Combine(Directory, "players", "data"); - // players/data/ が無ければプレイヤーは 1 人もいない + // players/data/が無ければプレイヤーは1人もいない if (!System.IO.Directory.Exists(directory)) { yield break; @@ -236,7 +233,7 @@ public IEnumerable PlayerIds() List found = new List(); - // .dat の名前部分が UUID にあたる + // .datの名前部分がUUIDにあたる foreach (string path in System.IO.Directory.EnumerateFiles(directory, "*.dat")) { found.Add(Path.GetFileNameWithoutExtension(path)); @@ -244,7 +241,7 @@ public IEnumerable PlayerIds() found.Sort(StringComparer.Ordinal); - // 並べ替えたものを 1 件ずつ返す + // 並べ替えたものを1件ずつ返す foreach (string id in found) { yield return id; @@ -252,7 +249,7 @@ public IEnumerable PlayerIds() } /// プレイヤーデータを読む - /// 存在しなければ null + /// 存在しなければnull public NbtCompound? Player(string uuid) { EnsureOpen(); @@ -269,11 +266,11 @@ public IEnumerable PlayerIds() } /// - /// level.dat を書き戻す + /// level.datを書き戻す /// /// - /// 壊れるとワールド全体が開けなくなるため、 - /// 一時ファイルへ書いてから level.dat_old へ退避し、最後に置き換える + /// level.datがおかしくなるとワールド全体が開けなくなるため、 + /// 一時ファイルへ書いて内容をディスクへ確実に書き出してから、既存のlevel.datをlevel.dat_oldへ退避し、最後に置き換える /// public void SaveLevel() { @@ -290,9 +287,16 @@ public void SaveLevel() try { - NbtIo.WriteFile(temporary, Level.ToNamedTag()); + byte[] encoded = NbtIo.WriteBytes(Level.ToNamedTag()); - // 既存の level.dat は、置き換える前に level.dat_old へ退避する + // 一時ファイルへ書き、置き換える前に内容をディスクへ確実に書き出す + using (FileStream stream = new FileStream(temporary, FileMode.Create, FileAccess.Write)) + { + stream.Write(encoded); + stream.Flush(flushToDisk: true); + } + + // 既存のlevel.datは、置き換える前にlevel.dat_oldへ退避する if (File.Exists(path)) { File.Copy(path, backup, overwrite: true); @@ -335,7 +339,7 @@ private void EnsureOpen() } } - /// 名前空間が省略されていたら minecraft: を補う + /// 名前空間が省略されていたらminecraft:を補う private static string NormalizeDimensionId(string dimensionId) { if (dimensionId.Contains(':', StringComparison.Ordinal)) @@ -348,13 +352,10 @@ private static string NormalizeDimensionId(string dimensionId) } /// -/// level.dat の内容 +/// level.datの内容 /// /// -/// 26.x では大幅に軽量化されており、ゲームルールやワールド生成設定は -/// data/minecraft/ 配下の個別ファイルへ分離されている -/// -/// 仕様: docs/spec/40-world-layout.md 2章 +/// 26.xでは大幅に軽量化されており、ゲームルールやワールド生成設定はdata/minecraft/配下の個別ファイルへ分離されている /// public sealed class LevelData { @@ -367,11 +368,11 @@ internal LevelData(NamedTag named) Data = named.Tag.GetCompound("Data"); } - /// ルートの NBT - /// Data を含む + /// ルートのNBT + /// Dataを含む public NbtCompound Raw { get; } - /// Data の中身 + /// Dataの中身 /// 実際の設定はここに入っている public NbtCompound Data { get; } @@ -388,21 +389,21 @@ internal LevelData(NamedTag named) /// 0=サバイバル 1=クリエイティブ 2=アドベンチャー 3=スペクテイター public int GameType => Data.GetInt("GameType"); - /// スポーン地点の [x, y, z] + /// スポーン地点の[x, y, z] public int[] SpawnPos => Data.GetCompound("spawn").GetIntArray("pos"); /// スポーン地点の次元ID public string SpawnDimension => Data.GetCompound("spawn").GetString("dimension"); - /// 難易度(normal など) + /// 難易度(normalなど) public string Difficulty => Data.GetCompound("difficulty_settings").GetString("difficulty"); /// ハードコアか public bool IsHardcore => Data.GetCompound("difficulty_settings").GetBool("hardcore"); - /// バージョン名(26.2 など) + /// バージョン名(26.2など) public string VersionName => Data.GetCompound("Version").GetString("Name"); - /// 書き出し用の を作る + /// 書き出し用のを作る public NamedTag ToNamedTag() => new NamedTag(rootName, Raw); } diff --git a/csharp/src/SpringNBTLibrary/World/PalettedContainer.cs b/csharp/src/SpringNBTLibrary/World/PalettedContainer.cs index c8ed8af..1b3d2e6 100644 --- a/csharp/src/SpringNBTLibrary/World/PalettedContainer.cs +++ b/csharp/src/SpringNBTLibrary/World/PalettedContainer.cs @@ -8,11 +8,10 @@ namespace SpringNBTLibrary.World; /// /// /// -/// パレットの要素は**生の のまま**持つ -/// こうすると、触っていないブロックについては Minecraft が書き出したときの +/// パレットの要素は**生ののまま**持つ +/// こうすると、触っていないブロックについてはMinecraftが書き出したときの /// プロパティの並び順まで含めてそのまま書き戻せる /// -/// 仕様: docs/spec/31-paletted-container.md /// public sealed class PalettedContainer { @@ -26,11 +25,11 @@ private PalettedContainer(int entryCount, int minBits) } /// エントリ数 - /// ブロックなら 4096、バイオームなら 64 + /// ブロックなら4096、バイオームなら64 public int EntryCount { get; } /// ビット幅の下限 - /// ブロックなら 4、バイオームなら 1 + /// ブロックなら4、バイオームなら1 public int MinBits { get; } /// パレット @@ -38,7 +37,7 @@ private PalettedContainer(int entryCount, int minBits) public IReadOnlyList Palette => palette; /// 現在のビット幅 - /// パレットが 1 要素なら 0(記憶域を持たない) + /// パレットが1要素なら0(記憶域を持たない) public int BitsPerEntry { get @@ -64,10 +63,10 @@ public static PalettedContainer Filled(NbtTag value, int entryCount, int minBits } /// - /// NBT から読み込む + /// NBTから読み込む /// /// - /// パレットが空、data の長さが合わない、添字がパレット範囲外のいずれか + /// パレットが空、dataの長さが合わない、添字がパレット範囲外のいずれか /// public static PalettedContainer FromNbt( NbtCompound nbt, int entryCount, int minBits, bool lenientBitStorage = false) @@ -82,7 +81,7 @@ public static PalettedContainer FromNbt( throw SpringNbtException.Malformed("palette が無いか空"); } - // パレットの要素は生の NbtTag のまま持つ + // パレットの要素は生のNbtTagのまま持つ // 並び順まで元どおりに書き戻すため foreach (NbtTag entry in paletteTag) { @@ -93,7 +92,7 @@ public static PalettedContainer FromNbt( if (data is null) { - // パレットが 1 要素なら data は無くてよい + // パレットが1要素ならdataは無くてよい if (result.palette.Count != 1) { throw SpringNbtException.Malformed( @@ -107,7 +106,7 @@ public static PalettedContainer FromNbt( result.storage = BitStorage.FromLongs(data, bits, entryCount, lenientBitStorage); // 取り出した添字がパレットの範囲に収まっているか確かめる - // 黙って 0 番目で代替すると、壊れたデータをそうと分からない形で書き戻してしまう + // エラーを出さずに0番目で代替すると、おかしくなったデータをそうと分からない形で書き戻してしまう for (int index = 0; index < entryCount; index++) { int value = result.storage.Get(index); @@ -122,7 +121,7 @@ public static PalettedContainer FromNbt( return result; } - /// NBT へ変換する + /// NBTへ変換する public NbtCompound ToNbt() { NbtCompound result = new NbtCompound(); @@ -134,8 +133,8 @@ public NbtCompound ToNbt() paletteTag.Add(entry); } - // パレットが 1 要素なら data は書かない - // Minecraft と同じ振る舞い + // パレットが1要素ならdataは書かない + // Minecraftと同じ振る舞い if (storage is not null && palette.Count > 1) { result.Set("data", new NbtLongArray(storage.ToLongs())); @@ -153,7 +152,7 @@ public NbtTag Get(int index) throw SpringNbtException.InvalidArgument($"添字が範囲外: {index} (0..{EntryCount - 1})"); } - // 記憶域が無いということは、全エントリがパレットの 0 番目 + // 記憶域が無いということは、全エントリがパレットの0番目 if (storage is null) { return palette[0]; @@ -175,7 +174,7 @@ public void Set(int index, NbtTag value) int paletteIndex = IndexOfOrAdd(value); - // 記憶域が無く、書き込む値も 0 番目なら何もしなくてよい + // 記憶域が無く、書き込む値も0番目なら何もしなくてよい if (storage is null && paletteIndex == 0) { return; @@ -185,8 +184,8 @@ public void Set(int index, NbtTag value) storage!.Set(index, paletteIndex); } - /// 全エントリを 1 つの値で埋める - /// パレットもその 1 要素だけにする + /// 全エントリを1つの値で埋める + /// パレットもその1要素だけにする public void Fill(NbtTag value) { ArgumentNullException.ThrowIfNull(value); @@ -199,7 +198,7 @@ public void Fill(NbtTag value) /// どのエントリからも参照されていないパレット要素を取り除き、添字を振り直す /// /// - /// 大量の Set を行う用途で遅くならないよう、明示的に呼んだときだけ実行する + /// 大量のSetを行う用途で遅くならないよう、明示的に呼んだときだけ実行する /// public void Compact() { @@ -252,7 +251,7 @@ public void Compact() if (compacted.Count == 1) { - // 1 要素になったら記憶域を捨てる + // 1要素になったら記憶域を捨てる storage = null; } else @@ -265,7 +264,7 @@ public void Compact() /// 無ければ末尾へ追加する private int IndexOfOrAdd(NbtTag value) { - // パレットは高々 4096 要素なので線形探索で足りる + // パレットは高々4096要素なので線形探索で足りる for (int index = 0; index < palette.Count; index++) { if (palette[index].Equals(value)) @@ -285,7 +284,7 @@ private void EnsureStorage() if (storage is null) { - // これまで単一値だったので、全エントリが 0 番目のまま始まる + // これまで単一値だったので、全エントリが0番目のまま始まる storage = BitStorage.Create(required, EntryCount); return; } @@ -299,13 +298,13 @@ private void EnsureStorage() storage = storage.Resize(required); } - /// 個の値を表すのに必要な最小ビット数 - /// 1 なら 0 + /// 個の値を表すのに必要な最小ビット数 + /// 1なら0 public static int CeilLog2(int count) { int bits = 0; - // 1 を超える分だけシフトして数える + // 1を超える分だけシフトして数える while ((1 << bits) < count) { bits += 1; diff --git a/csharp/tests/SpringNBTLibrary.Conformance/ChunkReport.cs b/csharp/tests/SpringNBTLibrary.Conformance/ChunkReport.cs index acb0972..2102bf6 100644 --- a/csharp/tests/SpringNBTLibrary.Conformance/ChunkReport.cs +++ b/csharp/tests/SpringNBTLibrary.Conformance/ChunkReport.cs @@ -6,17 +6,16 @@ namespace SpringNBTLibrary.Conformance; /// -/// チャンクの中身を、言語をまたいで文字列として完全一致する形へ写す。 +/// チャンクの中身を、言語をまたいで文字列として完全一致する形へ写す /// -/// 仕様: docs/spec/90-conformance.md 2.3章 internal static class ChunkReport { /// - /// チャンクの全ブロック・全バイオームを走査して集計する。 + /// チャンクの全ブロック・全バイオームを走査して集計する /// /// /// パレットとビットストレージを端から端まで通すので、 - /// ビット詰めの実装が 1 か所でもずれれば集計値が変わる。 + /// ビット詰めの実装が1か所でもずれれば集計値が変わる /// internal static string Describe(Chunk chunk) { @@ -52,7 +51,7 @@ internal static string Describe(Chunk chunk) CultureInfo.InvariantCulture, $"section {sectionY} {blockPalette} {blockBits} {biomePalette} {biomeBits}\n")); - // 全ブロックを 1 つずつ読んで、状態の文字列表現ごとに数える + // 全ブロックを1つずつ読んで、状態の文字列表現ごとに数える for (int y = 0; y < 16; y++) { for (int z = 0; z < 16; z++) @@ -73,7 +72,7 @@ internal static string Describe(Chunk chunk) } } - // バイオームは 4×4×4 単位なので、4 ブロックおきに見る + // バイオームは4×4×4単位なので、4ブロックおきに見る for (int y = 0; y < 16; y += 4) { for (int z = 0; z < 16; z += 4) @@ -109,11 +108,12 @@ internal static string Describe(Chunk chunk) } /// - /// 決まった手順でチャンクを編集する。全言語で同じ結果になるはず。 + /// 決まった手順でチャンクを編集する + /// 全言語で同じ結果になるはず /// /// - /// パレット拡張・ビット幅の再計算・未使用要素の掃除を一通り通す。 - /// 出力バイト列を比較すれば、これらの実装が一致していることを確かめられる。 + /// パレット拡張・ビット幅の再計算・未使用要素の掃除を一通り通す + /// 出力バイト列を比較すれば、これらの実装が一致していることを確かめられる /// internal static void Edit(Chunk chunk) { @@ -127,7 +127,8 @@ internal static void Edit(Chunk chunk) chunk.SetBlock(index % 16, baseY + (index / 16), index % 16, state); } - // プロパティ付きのブロックを、名前は同じで状態違いで置く + // プロパティ付きのブロックを置く + // 1つ目と2つ目はプロパティの並び順だけが違う同じ状態、3つ目は名前空間を省いて書いた別の状態 chunk.SetBlock(1, baseY + 2, 1, BlockState.Parse("minecraft:oak_stairs[facing=north,half=top]")); chunk.SetBlock(2, baseY + 2, 2, BlockState.Parse("minecraft:oak_stairs[half=top,facing=north]")); chunk.SetBlock(3, baseY + 2, 3, BlockState.Parse("oak_stairs[facing=south]")); @@ -139,7 +140,7 @@ internal static void Edit(Chunk chunk) // 使われなくなったパレット要素を掃除する chunk.Compact(); - // 高さマップと光源は再計算しないので、無効化して Minecraft に任せる + // 高さマップと光源は再計算しないので、無効化してMinecraftに任せる chunk.ClearHeightmaps(); chunk.InvalidateLighting(); } diff --git a/csharp/tests/SpringNBTLibrary.Conformance/NormalizedJson.cs b/csharp/tests/SpringNBTLibrary.Conformance/NormalizedJson.cs index 12b4dc4..dffe6db 100644 --- a/csharp/tests/SpringNBTLibrary.Conformance/NormalizedJson.cs +++ b/csharp/tests/SpringNBTLibrary.Conformance/NormalizedJson.cs @@ -5,19 +5,21 @@ namespace SpringNBTLibrary.Conformance; /// -/// NBT を、言語をまたいで文字列として完全一致する JSON へ写す。 +/// NBTを、言語をまたいで文字列として完全一致するJSONへ写す /// /// /// -/// 浮動小数点をビットパターンで、64bit 整数を10進文字列で表すのが要。 -/// 10進表記の丸めや JSON 数値の精度は処理系ごとに差が出るため、 -/// そのまま出すと4言語の出力が一致しない。 +/// 浮動小数点をビットパターンで、64bit整数を10進文字列で表すのが要 +/// 10進表記の丸めやJSON数値の精度は処理系ごとに差が出るため、 +/// そのまま出すと言語間で出力が一致しない /// -/// 仕様: docs/spec/00-conventions.md 6章 / docs/spec/90-conformance.md /// internal static class NormalizedJson { - /// ルートを含む全体を JSON 文字列へ変換する。末尾に改行を1つ付ける。 + /// + /// ルートを含む全体をJSON文字列へ変換する + /// 末尾に改行を1つ付ける + /// internal static string Write(NamedTag named, NbtFormat format) { StringBuilder builder = new StringBuilder(); @@ -46,7 +48,7 @@ private static void AppendTag(StringBuilder builder, NbtTag tag) builder.Append("{\"type\":"); AppendString(builder, tag.Type.AsString()); - // list だけは value の前に element_type が入る(仕様が定めるキー順) + // listだけはvalueの前にelement_typeが入る(仕様が定めるキー順) if (tag is NbtList listTag) { builder.Append(",\"element_type\":"); @@ -67,7 +69,7 @@ private static void AppendTag(StringBuilder builder, NbtTag tag) builder.Append(value.Value.ToString(CultureInfo.InvariantCulture)); break; case NbtLong value: - // 64bit 整数は JSON 数値だと処理系によって精度が落ちるため10進文字列で表す + // 64bit整数はJSON数値だと処理系によって精度が落ちるため10進文字列で表す AppendString(builder, value.Value.ToString(CultureInfo.InvariantCulture)); break; case NbtFloat value: @@ -79,7 +81,8 @@ private static void AppendTag(StringBuilder builder, NbtTag tag) case NbtString value: AppendString(builder, value.Value); - // MUTF-8 のバイト列も併記する。孤立サロゲートなど UTF-8 に写せない値を厳密に比較するため + // MUTF-8のバイト列も併記する + // 孤立サロゲートなどUTF-8に写せない値を厳密に比較するため builder.Append(",\"mutf8\":"); AppendString(builder, ToHex(Mutf8.Encode(value.Value))); break; @@ -127,7 +130,7 @@ private static void AppendList(StringBuilder builder, NbtList list) private static void AppendCompound(StringBuilder builder, NbtCompound compound) { - // JSON オブジェクトだと挿入順の保持が処理系依存になるため、組の配列で表す + // JSONオブジェクトだと挿入順の保持が処理系依存になるため、組の配列で表す builder.Append('['); bool first = true; @@ -187,7 +190,7 @@ private static void AppendLongArray(StringBuilder builder, long[] values) { builder.Append('['); - // 64bit 整数は10進文字列の配列で表す + // 64bit整数は10進文字列の配列で表す for (int i = 0; i < values.Length; i++) { if (i > 0) @@ -201,7 +204,7 @@ private static void AppendLongArray(StringBuilder builder, long[] values) builder.Append(']'); } - /// 浮動小数点のビットパターンを "0x..." 形式で表す。 + /// 浮動小数点のビットパターンを"0x..."形式で表す private static string HexBits(ulong bits, int digits) { return "0x" + bits.ToString("x" + digits.ToString(CultureInfo.InvariantCulture), CultureInfo.InvariantCulture); @@ -220,10 +223,11 @@ private static string ToHex(byte[] bytes) } /// - /// JSON 文字列を書き出す。非 ASCII は必ず \uXXXX へ逃がす。 + /// JSON文字列を書き出す + /// 非ASCIIは必ず\uXXXXへエスケープする /// /// - /// 言語ごとに既定のエスケープ方針が違うため、ここで一律に固定しないと出力が一致しない。 + /// 言語ごとに既定のエスケープ方針が違うため、ここで一律に固定しないと出力が一致しない /// private static void AppendString(StringBuilder builder, string text) { @@ -255,7 +259,7 @@ private static void AppendString(StringBuilder builder, string text) builder.Append("\\t"); break; default: - // ASCII の印字可能文字だけ生で出し、それ以外は \\uXXXX にする + // ASCIIの印字可能文字だけ生で出し、それ以外は\uXXXXにする if (c >= 0x20 && c <= 0x7E) { builder.Append(c); diff --git a/csharp/tests/SpringNBTLibrary.Conformance/Program.cs b/csharp/tests/SpringNBTLibrary.Conformance/Program.cs index e6ea2e3..ad840cf 100644 --- a/csharp/tests/SpringNBTLibrary.Conformance/Program.cs +++ b/csharp/tests/SpringNBTLibrary.Conformance/Program.cs @@ -8,14 +8,14 @@ namespace SpringNBTLibrary.Conformance; /// -/// 適合性検証ツール。4言語すべてが同じインターフェースで同じ出力を出す。 +/// 適合性検証ツール +/// 全言語が同じインターフェースで同じ出力を出す /// /// /// -/// spec/run-conformance.sh がこのツールを4言語ぶん起動し、 -/// 出力を相互に diff することで「4言語が同一に振る舞う」ことを機械的に確かめる。 +/// spec/run-conformance.shがこのツールを全言語ぶん起動し、 +/// 出力を相互にdiffすることで「全言語が同一に振る舞う」ことを機械的に確かめる /// -/// 仕様: docs/spec/90-conformance.md 2.3章 /// internal static class Program { @@ -57,13 +57,13 @@ private static int Main(string[] args) } catch (SpringNbtException error) { - // 4言語で同じ ErrorCode を出すことが検証対象なので、コードを機械可読な形で出す + // 言語間で同じErrorCodeを出すことが検証対象なので、コードを機械可読な形で出す Console.Error.Write($"ERROR {error.Code.AsString()} {error.Message}\n"); return 1; } } - /// 入力を読み、正規化JSON を書き出す。 + /// 入力を読み、正規化JSONを書き出す private static int RunDecode(string[] args) { if (args.Length < 3) @@ -80,7 +80,10 @@ private static int RunDecode(string[] args) return 0; } - /// 入力を読み、無圧縮で書き直す。ラウンドトリップ検証に使う。 + /// + /// 入力を読み、無圧縮で書き直す + /// ラウンドトリップ検証に使う + /// private static int RunEncode(string[] args) { if (args.Length < 3) @@ -101,7 +104,7 @@ private static int RunEncode(string[] args) return 0; } - /// 入力を読み、1行の SNBT を書き出す。 + /// 入力を読み、1行のSNBTを書き出す private static int RunSnbt(string[] args) { if (args.Length < 3) @@ -117,7 +120,7 @@ private static int RunSnbt(string[] args) return 0; } - /// 連なった NBT を、位置を追いながら一覧として書き出す。 + /// 連なったNBTを、位置を追いながら一覧として書き出す private static int RunNbtList(string[] args) { if (args.Length < 3) @@ -159,7 +162,7 @@ private static int RunNbtList(string[] args) return 0; } - /// リージョンの中身を一覧として書き出す。 + /// リージョンの中身を一覧として書き出す private static int RunRegionList(string[] args) { if (args.Length < 3) @@ -173,7 +176,7 @@ private static int RunRegionList(string[] args) return 0; } - /// リージョンを読み直し、無圧縮で詰め直して書き出す。 + /// リージョンを読み直し、無圧縮で詰め直して書き出す private static int RunRegionRewrite(string[] args) { if (args.Length < 3) @@ -187,7 +190,7 @@ private static int RunRegionRewrite(string[] args) return 0; } - /// チャンクの全ブロック・全バイオームを走査して集計を書き出す。 + /// チャンクの全ブロック・全バイオームを走査して集計を書き出す private static int RunChunkReport(string[] args) { if (args.Length < 3) @@ -201,7 +204,7 @@ private static int RunChunkReport(string[] args) return 0; } - /// 決まった手順でチャンクを編集し、無圧縮で書き出す。 + /// 決まった手順でチャンクを編集し、無圧縮で書き出す private static int RunChunkEdit(string[] args) { if (args.Length < 3) @@ -218,12 +221,12 @@ private static int RunChunkEdit(string[] args) return 0; } - /// チャンク NBT のファイルを読む。 + /// チャンクNBTのファイルを読む private static Chunk ReadChunkFile(string path) { NamedTag named = NbtIo.ReadFile(path); - // 検証では DataVersion の違いを警告にせず、そのまま読む + // 検証では、DataVersionが扱える形式より古くても警告にせずそのまま読む ChunkReadOptions options = new ChunkReadOptions { OnVersionMismatch = VersionMismatchAction.Ignore, @@ -231,10 +234,10 @@ private static Chunk ReadChunkFile(string path) return Chunk.FromNbt(named.Tag, options); } - /// --format network が指定されていればネットワーク形式として読む。 + /// --format networkが指定されていればネットワーク形式として読む private static NbtFormat ParseFormat(string[] args) { - // 3 番目以降の引数からオプションを探す + // 3番目以降の引数からオプションを探す for (int i = 3; i < args.Length - 1; i++) { if (args[i] == "--format" && args[i + 1] == "network") @@ -246,7 +249,7 @@ private static NbtFormat ParseFormat(string[] args) return NbtFormat.Java; } - /// 改行を変換せず、BOM も付けずに UTF-8 で書く。 + /// 改行を変換せず、BOMも付けずにUTF-8で書く private static void WriteTextFile(string path, string content) { File.WriteAllBytes(path, new UTF8Encoding(false).GetBytes(content)); diff --git a/csharp/tests/SpringNBTLibrary.Conformance/RegionReport.cs b/csharp/tests/SpringNBTLibrary.Conformance/RegionReport.cs index 0e74c2b..18cb621 100644 --- a/csharp/tests/SpringNBTLibrary.Conformance/RegionReport.cs +++ b/csharp/tests/SpringNBTLibrary.Conformance/RegionReport.cs @@ -6,17 +6,17 @@ namespace SpringNBTLibrary.Conformance; /// -/// リージョンファイルの中身を、言語をまたいで文字列として完全一致する形へ写す。 +/// リージョンファイルの中身を、言語をまたいで文字列として完全一致する形へ写す /// -/// 仕様: docs/spec/90-conformance.md 2.3章 internal static class RegionReport { /// - /// 存在するチャンクを 1 行 1 チャンクで書き出す。並びはロケーションテーブルの添字順。 + /// 存在するチャンクを1行1チャンクで書き出す + /// 並びはロケーションテーブルの添字順 /// /// - /// 各行は「絶対X 絶対Z セクタ数 タイムスタンプ 圧縮方式 展開後バイト数 ルート直下キー数」。 - /// 展開後バイト数を含めるのは、ヘッダ解析だけでなく展開と NBT 解釈まで通ったことを確かめるため。 + /// 各行は「絶対X 絶対Z タイムスタンプ 圧縮方式 圧縮後バイト数 展開後バイト数 ルート直下キー数」 + /// 展開後バイト数を含めるのは、ヘッダ解析だけでなく展開とNBT解釈まで通ったことを確かめるため /// internal static string List(RegionFile region) { @@ -63,12 +63,12 @@ internal static string List(RegionFile region) } /// - /// 全チャンクを読み直し、無圧縮で新しいリージョンへ詰め直して書き出す。 + /// 全チャンクを読み直し、無圧縮で新しいリージョンへ詰め直して書き出す /// /// - /// 無圧縮にするのは、zlib の出力が処理系ごとに違い、 - /// 圧縮したままでは言語間でバイトが一致しないため。 - /// これによりセクタ確保とヘッダ生成のロジックを直接比較できる。 + /// 無圧縮にするのは、zlibの出力が処理系ごとに違い、 + /// 圧縮したままでは言語間でバイトが一致しないため + /// これによりセクタ確保とヘッダ生成のロジックを直接比較できる /// internal static void Rewrite(RegionFile source, string outputPath) { diff --git a/csharp/tests/SpringNBTLibrary.Tests/BlockPosTests.cs b/csharp/tests/SpringNBTLibrary.Tests/BlockPosTests.cs index 9c1a8c5..adb0f87 100644 --- a/csharp/tests/SpringNBTLibrary.Tests/BlockPosTests.cs +++ b/csharp/tests/SpringNBTLibrary.Tests/BlockPosTests.cs @@ -4,7 +4,7 @@ namespace SpringNBTLibrary.Tests; /// -/// ブロック座標と範囲。仕様: docs/spec/30-chunk-format.md 5章 +/// ブロック座標と範囲 /// public class BlockPosTests { @@ -31,7 +31,7 @@ public void OffsetsEachAxis() [Fact] public void NormalizesTheOrderOfTheTwoCorners() { - // 端の順序は問わない + // 両端の順序は問わない Cuboid box = Cuboid.Of(10, 20, 30, 0, 5, 15); Assert.Equal(0, box.MinX); @@ -61,7 +61,7 @@ public void WalksEveryPositionInside() Assert.Equal(box.Volume, positions.Count); Assert.Equal(2 * 3 * 4, positions.Count); - // 並びは Y、Z、X の順で、X がいちばん内側で動く + // 並びはY、Z、Xの順で、Xがいちばん内側で動く Assert.Equal(new BlockPos(0, 0, 0), positions[0]); Assert.Equal(new BlockPos(1, 0, 0), positions[1]); Assert.Equal(new BlockPos(0, 0, 1), positions[2]); diff --git a/csharp/tests/SpringNBTLibrary.Tests/CanonicalDecimalTests.cs b/csharp/tests/SpringNBTLibrary.Tests/CanonicalDecimalTests.cs index 0590be4..fb549d0 100644 --- a/csharp/tests/SpringNBTLibrary.Tests/CanonicalDecimalTests.cs +++ b/csharp/tests/SpringNBTLibrary.Tests/CanonicalDecimalTests.cs @@ -3,11 +3,11 @@ namespace SpringNBTLibrary.Tests; /// -/// 浮動小数点の正準10進表記。仕様: docs/spec/11-snbt.md 5.1章 +/// 浮動小数点の正準10進表記 /// /// -/// ここが言語ごとにずれると SNBT 出力の言語間一致が崩れるため、 -/// 期待値は仕様の記述から手で書き下している。 +/// この表記が言語ごとにずれると、SNBT出力が言語間で一致しなくなるため、 +/// 期待値は仕様の記述から手で書き下している /// public class CanonicalDecimalTests { diff --git a/csharp/tests/SpringNBTLibrary.Tests/Mutf8Tests.cs b/csharp/tests/SpringNBTLibrary.Tests/Mutf8Tests.cs index a185e1e..5e2b537 100644 --- a/csharp/tests/SpringNBTLibrary.Tests/Mutf8Tests.cs +++ b/csharp/tests/SpringNBTLibrary.Tests/Mutf8Tests.cs @@ -2,7 +2,9 @@ namespace SpringNBTLibrary.Tests; -/// MUTF-8 の符号化・復号。仕様: docs/spec/10-nbt-binary.md 2章 +/// +/// MUTF-8の符号化・復号 +/// public class Mutf8Tests { [Fact] @@ -16,7 +18,7 @@ public void AsciiRoundtrip() [Fact] public void NulIsTwoBytes() { - // U+0000 は 1 バイトの 0x00 ではなく C0 80 で表す + // U+0000は1バイトの0x00ではなく、C0 80で表す string sample = "a\u0000b"; byte[] bytes = Mutf8.Encode(sample); Assert.Equal(new byte[] { 0x61, 0xC0, 0x80, 0x62 }, bytes); @@ -26,7 +28,7 @@ public void NulIsTwoBytes() [Fact] public void SupplementaryCharIsCesu8() { - // U+1F600 は UTF-16 で D83D DE00。MUTF-8 では 3 バイト × 2 になる + // U+1F600はUTF-16でD83D DE00、MUTF-8では3バイト × 2になる string sample = "\U0001F600"; byte[] bytes = Mutf8.Encode(sample); Assert.Equal(new byte[] { 0xED, 0xA0, 0xBD, 0xED, 0xB8, 0x80 }, bytes); @@ -36,7 +38,7 @@ public void SupplementaryCharIsCesu8() [Fact] public void LoneSurrogateSurvivesRoundtrip() { - // 対にならない上位サロゲート。UTF-8 には写せないが MUTF-8 では往復できる + // 対にならない上位サロゲートはUTF-8には写せないが、MUTF-8では往復できる string lone = "\ud83d"; byte[] bytes = Mutf8.Encode(lone); Assert.Equal(new byte[] { 0xED, 0xA0, 0xBD }, bytes); @@ -54,7 +56,7 @@ public void RawNulIsRejected() [Fact] public void OverlongTwoByteIsRejected() { - // U+0041 を 2 バイトで表した冗長符号化 + // U+0041を2バイトで表した冗長符号化 SpringNbtException error = Assert.Throws( () => Mutf8.Decode(new byte[] { 0xC1, 0x81 })); Assert.Equal(ErrorCode.MalformedData, error.Code); @@ -63,7 +65,7 @@ public void OverlongTwoByteIsRejected() [Fact] public void FourByteUtf8IsRejected() { - // 標準 UTF-8 の 4 バイト形式は MUTF-8 では不正 + // 標準UTF-8の4バイト形式はMUTF-8では不正 SpringNbtException error = Assert.Throws( () => Mutf8.Decode(new byte[] { 0xF0, 0x9F, 0x98, 0x80 })); Assert.Equal(ErrorCode.MalformedData, error.Code); diff --git a/csharp/tests/SpringNBTLibrary.Tests/NbtIoTests.cs b/csharp/tests/SpringNBTLibrary.Tests/NbtIoTests.cs index 85c8dfa..e3a01cb 100644 --- a/csharp/tests/SpringNBTLibrary.Tests/NbtIoTests.cs +++ b/csharp/tests/SpringNBTLibrary.Tests/NbtIoTests.cs @@ -3,11 +3,11 @@ namespace SpringNBTLibrary.Tests; /// -/// NBT のバイナリ読み書き。仕様: docs/spec/10-nbt-binary.md +/// NBTのバイナリ読み書き /// /// -/// ここで使うバイト列は仕様書の記述だけを根拠に手で組み立てている。 -/// 実装が生成した値を期待値にすると、実装のバグをそのまま正としてしまうため。 +/// ここで使うバイト列は仕様書の記述だけを根拠に手で組み立てている +/// 実装が生成した値を期待値にすると、実装のバグをそのまま正としてしまうため /// public class NbtIoTests { @@ -101,7 +101,7 @@ public void TypedSettersMirrorTheGetters() Assert.Equal(new int[] { 1, -1 }, root.GetIntArray("ia")); Assert.Equal(new long[] { 1L, -1L }, root.GetLongArray("la")); - // 真偽値は TAG_Byte の 0 / 1 として入る + // 真偽値はTAG_Byteの0 / 1として入る Assert.Equal(TagType.Byte, root.Get("t").Type); } @@ -119,7 +119,7 @@ public void TypedSettersKeepTheInsertionOrder() Assert.Equal(3, root.GetInt("a")); } - /// 複数のバイト列をつなぐ。 + /// 複数のバイト列をつなぐ private static byte[] Concat(params byte[][] parts) { List joined = new List(); @@ -134,8 +134,8 @@ private static byte[] Concat(params byte[][] parts) } /// - /// 仕様書どおりに組んだ最小の NBT。 - /// ルート名 "hello world" の Compound に、文字列 "Bananrama" が入っている。 + /// 仕様書どおりに組んだ最小のNBT + /// ルート名 "hello world" のCompoundに、文字列 "Bananrama" が入っている /// private static byte[] HelloWorldBytes() { @@ -169,6 +169,19 @@ public void ReadsHandBuiltHelloWorld() Assert.Equal("Bananrama", named.Tag.GetString("name")); } + [Fact] + public void NanBitPatternsAreWrittenBackUnchanged() + { + // ペイロード付き・負・シグナリングのNaNを、Float / Doubleの両方で持つCompound + byte[] original = + { + 0x0A, 0x00, 0x00, 0x05, 0x00, 0x01, 0x61, 0x7F, 0xC0, 0x00, 0x01, 0x05, 0x00, 0x01, 0x62, 0xFF, 0xC0, 0x00, 0x00, 0x05, 0x00, 0x01, 0x63, 0x7F, 0x80, 0x00, 0x01, 0x06, 0x00, 0x01, 0x64, 0x7F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x06, 0x00, 0x01, 0x65, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x66, 0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + }; + NamedTag named = NbtIo.ReadBytes(original, UncompressedRead()); + + Assert.Equal(original, NbtIo.WriteBytes(named, NbtWriteOptions.Uncompressed)); + } + [Fact] public void WritesBackTheSameBytes() { @@ -207,7 +220,7 @@ public void FloatSpecialsKeepTheirBitPattern() byte[] encoded = NbtIo.WriteBytes(new NamedTag(string.Empty, root), NbtWriteOptions.Uncompressed); NbtCompound decoded = NbtIo.ReadBytes(encoded, UncompressedRead()).Tag; - // -0.0 と +0.0 は == では区別できないので、ビットパターンで比較する + // -0.0と+0.0は == では区別できないので、ビットパターンで比較する Assert.Equal( BitConverter.DoubleToInt64Bits(-0.0), BitConverter.DoubleToInt64Bits(decoded.GetDouble("negative_zero"))); @@ -257,13 +270,25 @@ public void ListRejectsMixedTypes() Assert.Equal(ErrorCode.UnexpectedTagType, error.Code); } + [Fact] + public void ListRejectsOutOfRangePositionWithoutFixingElementType() + { + NbtList list = new NbtList(); + + Assert.Throws(() => list[0] = new NbtInt(1)); + Assert.Throws(() => list.Insert(1, new NbtInt(1))); + + // 失敗した操作で要素型が確定していない + Assert.Equal(TagType.End, list.ElementType); + } + [Fact] public void TypedGetterDistinguishesMissingKeyFromWrongType() { NbtCompound root = new NbtCompound(); root.Set("value", new NbtString("text")); - // キーが無い場合は null + // キーが無い場合はnull Assert.Null(root.OptInt("missing")); // 型が違う場合はキーの有無に関わらず例外 @@ -274,6 +299,21 @@ public void TypedGetterDistinguishesMissingKeyFromWrongType() Assert.Equal(ErrorCode.InvalidArgument, missing.Code); } + [Fact] + public void BrokenCompressedDataIsMalformedData() + { + NamedTag named = NbtIo.ReadBytes(HelloWorldBytes(), UncompressedRead()); + byte[] gzip = NbtIo.WriteBytes(named, new NbtWriteOptions { Compression = Compression.Gzip }); + byte[] truncated = gzip.AsSpan(0, gzip.Length / 2).ToArray(); + byte[] brokenGzip = { 0x1F, 0x8B, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; + byte[] brokenZlib = { 0x78, 0x9C, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; + + // 途中で切れたGZip、中身の壊れたGZip、中身の壊れたZlibは、どれも仕様に反するデータとして扱う + Assert.Equal(ErrorCode.MalformedData, Assert.Throws(() => NbtIo.ReadBytes(truncated)).Code); + Assert.Equal(ErrorCode.MalformedData, Assert.Throws(() => NbtIo.ReadBytes(brokenGzip)).Code); + Assert.Equal(ErrorCode.MalformedData, Assert.Throws(() => NbtIo.ReadBytes(brokenZlib)).Code); + } + [Theory] [InlineData(Compression.Gzip)] [InlineData(Compression.Zlib)] @@ -285,7 +325,7 @@ public void CompressionIsDetectedAutomatically(Compression method) Assert.Equal(method, NbtIo.DetectCompression(encoded)); - // 既定の ReadOptions は Auto なので、方式を指定しなくても読める + // 既定のReadOptionsはAutoなので、方式を指定しなくても読める NamedTag decoded = NbtIo.ReadBytes(encoded); Assert.Equal("Bananrama", decoded.Tag.GetString("name")); } @@ -303,7 +343,7 @@ public void NetworkFormatHasNoRootName() }; byte[] encoded = NbtIo.WriteBytes(new NamedTag("ignored", root), write); - // タグID + ペイロード のみで、名前長の 2 バイトが無い + // タグID + ペイロードのみで、名前長の2バイトが無い Assert.Equal(0x0A, encoded[0]); Assert.Equal(0x03, encoded[1]); @@ -332,7 +372,7 @@ public void TruncatedInputIsRejected() [Fact] public void HugeDeclaredLengthIsRejectedBeforeAllocating() { - // ルート直下に「長さ 0x7FFFFFFF の ByteArray」を宣言するだけの入力 + // ルート直下に「長さ0x7FFFFFFFのByteArray」を宣言するだけの入力 List bytes = new List(); bytes.Add(0x0A); bytes.AddRange(new byte[] { 0x00, 0x00 }); @@ -362,7 +402,7 @@ public void UnknownTagIdIsRejected() [Fact] public void ExcessiveNestingIsRejected() { - // 上限を超える深さの Compound を組み立てる + // 上限を超える深さのCompoundを組み立てる byte[] encoded = BuildNestedCompound(600); NbtReadOptions options = UncompressedRead(); @@ -398,13 +438,16 @@ public void WriteRejectsAutoCompression() Assert.Equal(ErrorCode.InvalidArgument, error.Code); } - /// 圧縮なしで読むための ReadOptions を作る。 + /// 圧縮なしで読むためのReadOptionsを作る private static NbtReadOptions UncompressedRead() { return new NbtReadOptions { Compression = Compression.None }; } - /// 全13タグ型(TAG_End を除く12種 + 入れ子の Compound)を含む Compound を作る。 + /// + /// TAG_Endを除く12種のタグ型をすべて含むCompoundを作る + /// TAG_EndはCompoundの終端として書き出されるので、往復させれば13種すべてを読み書きする + /// private static NbtCompound BuildAllTagsCompound() { NbtCompound root = new NbtCompound(); @@ -432,12 +475,12 @@ private static NbtCompound BuildAllTagsCompound() return root; } - /// 指定した深さまで Compound を入れ子にしたバイト列を作る。 + /// 指定した深さまでCompoundを入れ子にしたバイト列を作る private static byte[] BuildNestedCompound(int depth) { List bytes = new List(); - // ルート + (depth - 1) 段の入れ子 + // ルート + (depth - 1)段の入れ子 bytes.Add(0x0A); bytes.AddRange(new byte[] { 0x00, 0x00 }); diff --git a/csharp/tests/SpringNBTLibrary.Tests/RegionFileTests.cs b/csharp/tests/SpringNBTLibrary.Tests/RegionFileTests.cs index d56a8a3..583c276 100644 --- a/csharp/tests/SpringNBTLibrary.Tests/RegionFileTests.cs +++ b/csharp/tests/SpringNBTLibrary.Tests/RegionFileTests.cs @@ -4,7 +4,7 @@ namespace SpringNBTLibrary.Tests; /// -/// Anvil リージョンファイルの読み書き。仕様: docs/spec/20-anvil-region.md +/// Anvilリージョンファイルの読み書き /// public class RegionFileTests : IDisposable { @@ -27,12 +27,12 @@ public void Dispose() GC.SuppressFinalize(this); } - /// 共通テストベクタのディレクトリ。 + /// 共通テストベクタのディレクトリ private static string VectorDirectory(string name) { string current = AppContext.BaseDirectory; - // ビルド出力からリポジトリ直下まで遡って spec/testdata を探す + // ビルド出力からリポジトリ直下まで遡ってspec/testdataを探す while (current is not null) { string candidate = Path.Combine(current, "spec", "testdata", "anvil", name); @@ -55,7 +55,7 @@ private static string VectorDirectory(string name) throw new DirectoryNotFoundException($"テストベクタが見つからない: anvil/{name}"); } - /// ベクタを一時ディレクトリへ複製し、書き込みテストで原本を汚さないようにする。 + /// ベクタを一時ディレクトリへ複製し、書き込みテストで原本を汚さないようにする private string CopyVector(string name) { string source = VectorDirectory(name); @@ -170,7 +170,7 @@ public void ReadsLz4Chunks() using RegionFile region = RegionFile.Open( Path.Combine(VectorDirectory("lz4"), "r.0.0.mca")); - // 1 ブロック / 2 ブロック連結 / 無圧縮ブロック / 重なりのあるマッチ + // 1ブロック / 2ブロック連結 / 無圧縮ブロック / 重なりのあるマッチ for (int x = 0; x < 4; x++) { Assert.Equal(ChunkCompression.Lz4, region.ReadChunkRaw(x, 0)!.Compression); @@ -192,6 +192,21 @@ public void RejectsLz4BlockWithBrokenMagic() Assert.Equal(ErrorCode.MalformedData, error.Code); } + [Fact] + public void TruncatedGzipChunkIsMalformedData() + { + string directory = CopyVector("lz4"); + using RegionFile region = RegionFile.Open( + Path.Combine(directory, "r.0.0.mca"), RegionFileMode.ReadWrite); + + // GZipのヘッダだけで本体が無い、途中で切れたチャンク + byte[] truncated = { 0x1F, 0x8B, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF }; + region.WriteChunkRaw(0, 0, new RawChunk(ChunkCompression.Gzip, truncated)); + + SpringNbtException error = Assert.Throws(() => region.ReadChunk(0, 0)); + Assert.Equal(ErrorCode.MalformedData, error.Code); + } + [Fact] public void WritingLz4IsRejected() { @@ -199,7 +214,7 @@ public void WritingLz4IsRejected() using RegionFile region = RegionFile.Open( Path.Combine(directory, "r.0.0.mca"), RegionFileMode.ReadWrite); - // LZ4 は読み込みのみ対応なので、圧縮して書き出すことはできない + // LZ4は読み込みのみ対応なので、圧縮して書き出すことはできない NbtCompound chunk = region.ReadChunk(0, 0)!; SpringNbtException error = Assert.Throws( () => region.WriteChunk(0, 0, chunk, ChunkCompression.Lz4)); @@ -215,7 +230,8 @@ public void UntouchedLz4ChunksKeepTheirCompression() using (RegionFile region = RegionFile.Open(path, RegionFileMode.ReadWrite)) { - // 触らずに閉じるだけ。生バイトを素通しするので LZ4 のまま残る + // 触らずに閉じるだけ + // 生バイトに手を加えないので、LZ4のまま残る } Assert.Equal(before, File.ReadAllBytes(path)); @@ -254,7 +270,7 @@ public void ChunkOutsideTheRegionIsRejected() using RegionFile region = RegionFile.Open( Path.Combine(VectorDirectory("empty"), "r.0.0.mca")); - // r.0.0 が担当するのは 0..31 の範囲だけ + // r.0.0が担当するのは0..31の範囲だけ SpringNbtException error = Assert.Throws(() => region.HasChunk(32, 0)); Assert.Equal(ErrorCode.InvalidArgument, error.Code); } @@ -275,7 +291,7 @@ public void ReadOnlyRegionRejectsWrites() [Fact] public void OpeningAndFlushingWithoutChangesKeepsBytesIdentical() { - // 触っていないチャンクの配置を保つことが、既存ワールドを壊さない前提になる + // 触っていないチャンクの配置を保つことが、既存ワールドをおかしくしないための前提になる string directory = CopyVector("fragmented"); string path = Path.Combine(directory, "r.0.0.mca"); byte[] original = File.ReadAllBytes(path); @@ -336,7 +352,7 @@ public void GrowingChunkIsRelocatedWithoutBreakingOthers() string directory = CopyVector("fragmented"); string path = Path.Combine(directory, "r.0.0.mca"); - // 5 セクタぶんになる大きなチャンクを作る + // 5セクタぶんになる大きなチャンクを作る NbtCompound big = SampleChunk(0, 0); big.Set("filler", new NbtByteArray(BuildIncompressibleBytes(5 * RegionFile.SectorSize))); @@ -348,7 +364,7 @@ public void GrowingChunkIsRelocatedWithoutBreakingOthers() using RegionFile reopened = RegionFile.Open(path); - // 動かした結果、他の 2 チャンクが壊れていないこと + // 動かした結果、他の2チャンクがおかしくなっていないこと Assert.Equal(3, reopened.ChunkPositions().Count()); Assert.Equal(5, reopened.ReadChunk(5, 3)!.GetInt("xPos")); Assert.Equal(31, reopened.ReadChunk(31, 31)!.GetInt("xPos")); @@ -428,7 +444,7 @@ public void HugeChunkGoesToExternalFileAndComesBack() { string path = Path.Combine(workDirectory, "r.0.0.mca"); - // 1MiB を超えるよう、圧縮の効かないデータを詰める + // 1MiBを超えるよう、圧縮しても縮まないデータを詰める NbtCompound huge = SampleChunk(1, 2); huge.Set("filler", new NbtByteArray(BuildIncompressibleBytes(1200 * 1024))); @@ -491,7 +507,7 @@ public void FolderResolvesChunksAcrossRegions() folder.Flush(); } - // 3 つの異なるリージョンへ振り分けられる + // 3つの異なるリージョンへ振り分けられる Assert.True(File.Exists(Path.Combine(workDirectory, "r.0.0.mca"))); Assert.True(File.Exists(Path.Combine(workDirectory, "r.-1.-1.mca"))); Assert.True(File.Exists(Path.Combine(workDirectory, "r.1.1.mca"))); @@ -504,10 +520,34 @@ public void FolderResolvesChunksAcrossRegions() Assert.False(reopened.HasChunk(100, 100)); } + [Fact] + public void ReadWriteFolderCreatesMissingDirectoryOnlyWhenWriting() + { + string missing = Path.Combine(workDirectory, "missing", "region"); + + // 読むだけなら、無いディレクトリは作らない + using (RegionFolder folder = RegionFolder.Open(missing, RegionFileMode.ReadWrite)) + { + Assert.Null(folder.ReadChunk(0, 0)); + } + + Assert.False(Directory.Exists(missing)); + + // 書き出すときに作る + using (RegionFolder folder = RegionFolder.Open(missing, RegionFileMode.ReadWrite)) + { + folder.WriteChunk(0, 0, SampleChunk(0, 0)); + } + + using RegionFolder reopened = RegionFolder.Open(missing); + Assert.Equal(0, reopened.ReadChunk(0, 0)!.GetInt("xPos")); + } + [Fact] public void FolderKeepsCachedRegionsUnderTheLimit() { - // 上限 2 で 4 リージョンへ書く。古いものは閉じられるが内容は失われない + // 上限2で4リージョンへ書く + // 古いものは閉じられるが、内容は失われない using (RegionFolder folder = RegionFolder.Open( workDirectory, RegionFileMode.ReadWrite, maxCachedRegions: 2)) { @@ -538,13 +578,17 @@ public void FolderRejectsInvalidCacheLimit() Assert.Equal(ErrorCode.InvalidArgument, error.Code); } - /// 圧縮しても縮まないバイト列を作る。サイズの制御が効くようにするため。 + /// + /// 圧縮しても縮まないバイト列を作る + /// サイズを狙いどおりに制御できるようにするため + /// private static sbyte[] BuildIncompressibleBytes(int length) { sbyte[] result = new sbyte[length]; uint state = 0x12345678; - // 線形合同法で疑似乱数を作る。テストの再現性を保つため固定の種を使う + // 線形合同法で疑似乱数を作る + // テストの再現性を保つため、固定の種を使う for (int index = 0; index < length; index++) { state = (state * 1664525) + 1013904223; diff --git a/csharp/tests/SpringNBTLibrary.Tests/SnbtTests.cs b/csharp/tests/SpringNBTLibrary.Tests/SnbtTests.cs index f6bc247..86f7488 100644 --- a/csharp/tests/SpringNBTLibrary.Tests/SnbtTests.cs +++ b/csharp/tests/SpringNBTLibrary.Tests/SnbtTests.cs @@ -2,7 +2,9 @@ namespace SpringNBTLibrary.Tests; -/// SNBT のパースと出力。仕様: docs/spec/11-snbt.md +/// +/// SNBTのパースと出力 +/// public class SnbtTests { [Theory] @@ -43,7 +45,8 @@ public void ExtendedIntegerLiterals(string source, int expected) [Fact] public void HexSuffixRuleIsFixed() { - // 仕様 11 の 2.1: 16進では b/d/f を数字として読む。幅接尾辞は s/l のみ + // 16進ではb/d/fを数字として読む + // 幅接尾辞はs/lのみ Assert.Equal(new NbtInt(255), Snbt.Parse("0xFF")); Assert.Equal(new NbtInt(4091), Snbt.Parse("0xFFb")); Assert.Equal(new NbtLong(255), Snbt.Parse("0xFFl")); @@ -53,11 +56,12 @@ public void HexSuffixRuleIsFixed() [Fact] public void ZeroByteLiteralIsNotBinary() { - // 0b は「10進の 0 に Byte 接尾辞」。真偽値の false として広く使われる形 + // 0bは「10進の0にByte接尾辞」 + // 真偽値のfalseとして広く使われる形 Assert.Equal(new NbtByte(0), Snbt.Parse("0b")); Assert.Equal(new NbtByte(1), Snbt.Parse("1b")); - // 2進数字が続く場合だけ 2 進リテラルになる + // 2進数字が続く場合だけ2進リテラルになる Assert.Equal(new NbtInt(1), Snbt.Parse("0b1")); Assert.Equal(new NbtByte(9), Snbt.Parse("0b1001b")); } @@ -65,7 +69,7 @@ public void ZeroByteLiteralIsNotBinary() [Fact] public void UnsignedSuffixWrapsToSigned() { - // 255ub は符号なし 255 として読み、Byte へは -1 として格納される + // 255ubは符号なし255として読み、Byteへは-1として格納される Assert.Equal(new NbtByte(-1), Snbt.Parse("255ub")); Assert.Equal(new NbtShort(-1), Snbt.Parse("65535us")); } @@ -80,7 +84,7 @@ public void UnsignedOverflowIsRejected() [Fact] public void SuffixlessIntegerIsNotPromotedToLong() { - // Int の範囲を超える接尾辞なし整数は、暗黙に Long へ格上げせずエラーにする + // Intの範囲を超える接尾辞なし整数は、暗黙にLongへ格上げせずエラーにする SpringNbtException error = Assert.Throws(() => Snbt.Parse("2147483648")); Assert.Equal(ErrorCode.MalformedData, error.Code); @@ -106,6 +110,14 @@ public void SpecialFloatingValues() Assert.True(float.IsPositiveInfinity(Assert.IsType(Snbt.Parse("Infinityf")).Value)); } + [Fact] + public void FloatOutOfRangeBecomesSignedInfinity() + { + // binary32の範囲を超える値は、符号付きの無限大になる + Assert.True(float.IsPositiveInfinity(Assert.IsType(Snbt.Parse("1e39f")).Value)); + Assert.True(float.IsNegativeInfinity(Assert.IsType(Snbt.Parse("-1e39f")).Value)); + } + [Fact] public void TypedArrays() { @@ -113,7 +125,7 @@ public void TypedArrays() Assert.Equal(new NbtIntArray(new[] { 1, 2 }), Snbt.Parse("[I; 1, 2]")); Assert.Equal(new NbtLongArray(new[] { 1L, 2L }), Snbt.Parse("[L; 1L, 2L]")); - // 接尾辞なしでも範囲内なら受理する(Minecraft 自身がそう書き出すため) + // 接尾辞なしでも範囲内なら受理する(Minecraft自身がそう書き出すため) Assert.Equal(new NbtByteArray(new sbyte[] { 1, 2 }), Snbt.Parse("[B; 1, 2]")); } @@ -137,7 +149,7 @@ public void TrailingCommasAreAllowed() [Fact] public void HeterogeneousListIsRejected() { - // 異種リストはバイナリ NBT へ写せないため受理しない (adr/0006) + // 異種リストはバイナリNBTへ写せないため受理しない SpringNbtException error = Assert.Throws(() => Snbt.Parse("[1, \"a\"]")); Assert.Equal(ErrorCode.MalformedData, error.Code); } @@ -152,6 +164,32 @@ public void EscapeSequences() Assert.Equal("\U0001F600", Assert.IsType(Snbt.Parse("\"\\U0001F600\"")).Value); } + [Fact] + public void SurrogateEscapesFollowTheSameRulesEverywhere() + { + string lone = ((char)0xD800).ToString(); + + // \Uでサロゲートの範囲を書くと、\uと同じく孤立サロゲートになる + Assert.Equal(new NbtString(lone), Snbt.Parse("\"\\U0000D800\"")); + Assert.Equal(new NbtString(lone), Snbt.Parse("\"\\uD800\"")); + + // 対になったサロゲートは補助文字1文字と同じ + Assert.Equal(new NbtString(char.ConvertFromUtf32(0x1F600)), Snbt.Parse("\"\\uD83D\\uDE00\"")); + + // コードポイントの範囲外と、キーの孤立サロゲートは仕様に反する + Assert.Equal(ErrorCode.MalformedData, + Assert.Throws(() => Snbt.Parse("\"\\U00110000\"")).Code); + Assert.Equal(ErrorCode.MalformedData, + Assert.Throws(() => Snbt.Parse("{\"\\uD800\":1}")).Code); + Assert.Equal(ErrorCode.MalformedData, + Assert.Throws(() => Snbt.Parse("{\"\\U0000D800\":1}")).Code); + + // APIからも孤立サロゲートのキーは設定できない + NbtCompound compound = new NbtCompound(); + Assert.Equal(ErrorCode.InvalidArgument, + Assert.Throws(() => compound.Set(lone, new NbtInt(1))).Code); + } + [Fact] public void NamedCharacterEscapeIsUnsupported() { @@ -187,7 +225,7 @@ public void UuidFunction() [Fact] public void SnbtToNbtToSnbtToNbtIsStable() { - // 仕様 11 の 5章: 保証するのは「SNBT -> NBT -> SNBT -> NBT」で NBT が一致すること + // 保証するのは「SNBT -> NBT -> SNBT -> NBT」でNBTが一致すること string source = "{ name : 'Bananrama' , list : [ 1L , 2L ] , nested : { flag : true } , " + "bytes : [B; 1b, -2b] , ratio : 0.5f }"; @@ -196,7 +234,7 @@ public void SnbtToNbtToSnbtToNbtIsStable() Assert.Equal(first, second); - // 整形出力からも同じ NBT が得られること + // 整形出力からも同じNBTが得られること NbtTag third = Snbt.Parse(Snbt.WritePretty(first)); Assert.Equal(first, third); } diff --git a/csharp/tests/SpringNBTLibrary.Tests/TagEqualityTests.cs b/csharp/tests/SpringNBTLibrary.Tests/TagEqualityTests.cs index 29d20a7..2fbbe13 100644 --- a/csharp/tests/SpringNBTLibrary.Tests/TagEqualityTests.cs +++ b/csharp/tests/SpringNBTLibrary.Tests/TagEqualityTests.cs @@ -3,17 +3,17 @@ namespace SpringNBTLibrary.Tests; /// -/// タグの等値比較と深い複製。仕様: docs/spec/10-nbt-binary.md 7.3 +/// タグの等値比較と深い複製 /// /// -/// 規則は全言語で同じでなければならない。 -/// 各言語の同名テストと突き合わせて読むこと。 +/// 規則は全言語で同じでなければならない +/// 各言語の同名テストと見比べながら読むこと /// public class TagEqualityTests { - // NbtList / NbtCompound は IEnumerable なので、Assert.Equal を使うと - // xUnit がコレクション比較に切り替えてしまう - // 等値の規則そのものを見たいので、Equals を直接呼ぶ + // NbtList / NbtCompoundはIEnumerableなので、Assert.Equalを使うと + // xUnitがコレクション比較に切り替えてしまう + // 等値の規則そのものを見たいので、Equalsを直接呼ぶ [Fact] public void SameTypeSameValueIsEqual() @@ -31,7 +31,7 @@ public void SameTypeSameValueIsEqual() [Fact] public void DifferentTagTypeIsNotEqual() { - // 値が同じでもタグの型が違えば別物 + // 値が同じでもタグの型が違えば等しくない Assert.False(new NbtInt(1).Equals(new NbtShort(1))); Assert.False(new NbtInt(1).Equals(null)); } @@ -39,7 +39,7 @@ public void DifferentTagTypeIsNotEqual() [Fact] public void FloatsCompareByBitPattern() { - // NaN 同士は等しく、+0.0 と -0.0 は等しくない + // NaN同士は等しく、+0.0と-0.0は等しくない Assert.True(new NbtFloat(float.NaN).Equals(new NbtFloat(float.NaN))); Assert.True(new NbtDouble(double.NaN).Equals(new NbtDouble(double.NaN))); Assert.False(new NbtFloat(0.0f).Equals(new NbtFloat(-0.0f))); @@ -63,7 +63,7 @@ public void ListComparesElementTypeAndOrder() reversed.Add(new NbtInt(1)); Assert.False(left.Equals(reversed)); - // 空でも要素型が違えば別物 + // 空でも要素型が違えば等しくない Assert.False(new NbtList(TagType.Int).Equals(new NbtList(TagType.Byte))); } @@ -79,7 +79,7 @@ public void CompoundComparesInsertionOrder() same.Set("b", new NbtInt(2)); Assert.True(left.Equals(same)); - // 中身は同じでも挿入順が違えば別物 + // 中身は同じでも挿入順が違えば等しくない NbtCompound reordered = new NbtCompound(); reordered.Set("b", new NbtInt(2)); reordered.Set("a", new NbtInt(1)); diff --git a/csharp/tests/SpringNBTLibrary.Tests/WorldTests.cs b/csharp/tests/SpringNBTLibrary.Tests/WorldTests.cs index 87ef85a..2407ecb 100644 --- a/csharp/tests/SpringNBTLibrary.Tests/WorldTests.cs +++ b/csharp/tests/SpringNBTLibrary.Tests/WorldTests.cs @@ -1,19 +1,20 @@ +using SpringNBTLibrary.Anvil; using SpringNBTLibrary.Nbt; using SpringNBTLibrary.World; namespace SpringNBTLibrary.Tests; /// -/// World / Block レイヤ。仕様: docs/spec/30-chunk-format.md / 31-paletted-container.md / 40-world-layout.md +/// World / Blockレイヤ /// public class WorldTests { - /// 共通テストベクタ(world/*.nbt)のパス。 + /// 共通テストベクタ(world/*.nbt)のパス private static string VectorPath(string name) { string current = AppContext.BaseDirectory; - // ビルド出力からリポジトリ直下まで遡って spec/testdata を探す + // ビルド出力からリポジトリ直下まで遡ってspec/testdataを探す while (current is not null) { string candidate = Path.Combine(current, "spec", "testdata", "world", name + ".nbt"); @@ -36,7 +37,7 @@ private static string VectorPath(string name) throw new InvalidOperationException("テストベクタが見つからない: world/" + name + ".nbt"); } - /// テストベクタをチャンクとして読む。 + /// テストベクタをチャンクとして読む private static Chunk LoadChunk(string name, ChunkReadOptions? options = null) { NamedTag named = NbtIo.ReadFile(VectorPath(name)); @@ -164,7 +165,8 @@ public class BitStorageTests { BitStorage storage = BitStorage.Create(5, 4096); - // bits=5 なら 1 つの long に 12 個。12 個目は次の long の最下位から始まる + // bits=5なら1つのlongに12個 + // 添字12は次のlongの最下位から始まる storage.Set(11, 31); storage.Set(12, 1); long[] longs = storage.ToLongs(); @@ -204,7 +206,7 @@ public class BitStorageTests [Fact] public void 寛容モードなら長さからビット幅を逆算する() { - // 4096 エントリを 342 long で表せるのは bits=5 のときだけ + // 4096エントリを342 longで表せるのはbits=5のときだけ BitStorage storage = BitStorage.FromLongs(new long[342], 4, 4096, lenient: true); Assert.Equal(5, storage.BitsPerEntry); } @@ -266,7 +268,7 @@ private static NbtCompound BlockEntry(string name) { PalettedContainer container = PalettedContainer.Filled(BlockEntry("minecraft:air"), 4096, 4); - // パレットを 17 要素まで増やして bits=4 から 5 への拡張を起こす + // 17種のブロックを足してパレットを18要素にし、bits=4から5への拡張を起こす for (int index = 0; index < 17; index++) { container.Set(index, BlockEntry("minecraft:block_" + index)); @@ -297,7 +299,7 @@ private static NbtCompound BlockEntry(string name) container.Compact(); - // 残るのは実際に使われている air と dirt の 2 つ + // 残るのは実際に使われているairとdirtの2つ Assert.Equal(2, container.Palette.Count); Assert.Equal("minecraft:dirt", ((NbtCompound)container.Get(0)).GetString("Name")); } @@ -354,7 +356,8 @@ public class ChunkTests "minecraft:stone", }; - // ベクタの添字は (位置 * 11) % 17。パレット先頭 2 つだけ名前が違う + // ベクタの添字は (位置 * 11) % 17 + // パレットの先頭2つだけ名前が違う for (int position = 0; position < 4096; position++) { int paletteIndex = (position * 11) % 17; @@ -421,7 +424,7 @@ public class ChunkTests chunk.SetBlock(3, -60, 7, "minecraft:stone"); Assert.True(chunk.IsModified); - // 保存済みとして印を下ろせる + // 保存済みとして印を外せる chunk.IsModified = false; Assert.False(chunk.IsModified); @@ -439,7 +442,7 @@ public class ChunkTests Chunk chunk = LoadChunk("palette_1"); chunk.SetBiome(0, -64, 0, "minecraft:desert"); - // 同じ 4×4×4 の枠内はまとめて変わる + // 同じ4×4×4の枠内はまとめて変わる Assert.Equal("minecraft:desert", chunk.GetBiome(3, -61, 3)); Assert.Equal("minecraft:plains", chunk.GetBiome(4, -64, 0)); } @@ -479,18 +482,18 @@ public class ChunkTests Assert.Equal(2, before.GetList("block_ticks").Count); Assert.Single(before.GetList("fluid_ticks")); - // (0,-64,0) には chest と block_tick、(1,-64,1) には furnace と fluid_tick がある + // (0,-64,0)にはchestとblock_tick、(1,-64,1)にはfurnaceとfluid_tickがある chunk.SetBlock(0, -64, 0, BlockState.Parse("minecraft:stone")); chunk.SetBlock(1, -64, 1, BlockState.Parse("minecraft:stone")); NbtCompound after = chunk.Raw; NbtList entities = after.GetList("block_entities"); - // 触っていない (15,-50,15) の barrel だけが残る + // 触っていない(15,-50,15)のbarrelだけが残る Assert.Single(entities); Assert.Equal("minecraft:barrel", ((NbtCompound)entities[0]).GetString("id")); - // block_ticks も同様に、対象の座標のものだけ消える + // block_ticksも同様に、対象の座標のものだけ消える NbtList ticks = after.GetList("block_ticks"); Assert.Single(ticks); Assert.Equal(15, ((NbtCompound)ticks[0]).GetInt("x")); @@ -514,8 +517,7 @@ public class ChunkTests [Fact] public void 別のチャンクの同じ相対座標は消さない() { - // 付随データは絶対座標で持つので、チャンク座標を取り違えると - // 無関係な要素を消してしまう + // 付随データは絶対座標で持つので、チャンク座標を間違えると無関係な要素を消してしまう NamedTag named = NbtIo.ReadFile(VectorPath("block_entities")); NbtCompound root = (NbtCompound)named.Tag; root.Set("xPos", new NbtInt(1)); @@ -524,7 +526,8 @@ public class ChunkTests Chunk chunk = Chunk.FromNbt(root); chunk.SetBlock(0, -64, 0, BlockState.Parse("minecraft:stone")); - // このチャンクの (0,-64,0) は絶対座標 (16,-64,16)。どれとも一致しない + // このチャンクの(0,-64,0)は絶対座標(16,-64,16) + // どれとも一致しない Assert.Equal(3, chunk.Raw.GetList("block_entities").Count); } @@ -566,12 +569,12 @@ public class ChunkTests } // ------------------------------------------------------------------ - // DataVersion の扱い + // DataVersionの扱い // ------------------------------------------------------------------ public class DataVersionTests { - /// DataVersion だけを差し替えたチャンクを作る。 + /// DataVersionだけを差し替えたチャンクを作る private static NbtCompound ForeignChunk() { NamedTag named = NbtIo.ReadFile(VectorPath("palette_1")); @@ -639,11 +642,11 @@ private static NbtCompound ForeignChunk() Chunk chunk = Chunk.FromNbt(ForeignChunk(), read); ChunkWriteOptions write = new ChunkWriteOptions { AllowForeignDataVersion = true }; - // DataVersion は読んだ値のまま残す + // DataVersionは読んだ値のまま残す Assert.Equal(3953, chunk.ToNbt(write).GetInt("DataVersion")); } - /// 対象より新しい DataVersion を持つチャンクを作る。 + /// 対象より新しいDataVersionを持つチャンクを作る private static NbtCompound NewerChunk() { NamedTag named = NbtIo.ReadFile(VectorPath("palette_1")); @@ -662,7 +665,7 @@ private static NbtCompound NewerChunk() OnWarning = warnings.Add, }; - // 形式が同じであれば、新しいバージョンでも黙って読める + // 形式が同じであれば、新しいバージョンでも警告を出さずに読める Chunk chunk = Chunk.FromNbt(NewerChunk(), options); Assert.Equal(5015, chunk.DataVersion); Assert.Empty(warnings); @@ -684,7 +687,7 @@ private static NbtCompound NewerChunk() { Chunk chunk = Chunk.FromNbt(NewerChunk()); - // 許可を出さなくても書き戻せて、DataVersion も変わらない + // 許可を出さなくても書き戻せて、DataVersionも変わらない Assert.Equal(5015, chunk.ToNbt().GetInt("DataVersion")); } @@ -726,5 +729,47 @@ public class MinecraftWorldTests System.IO.Directory.Delete(empty, recursive: true); } } + + [Fact] + public void 次元経由で読んだチャンクの警告も通知先へ届く() + { + string root = Path.Combine(Path.GetTempPath(), "springnbt-warn-" + Guid.NewGuid().ToString("N")); + string region = Path.Combine(root, "dimensions", "minecraft", "overworld", "region"); + System.IO.Directory.CreateDirectory(region); + + try + { + // DataVersionが扱える形式より古いチャンクを1つだけ持つワールドを作る + NbtCompound level = new NbtCompound(); + level.Set("Data", new NbtCompound()); + NbtIo.WriteFile(Path.Combine(root, "level.dat"), new NamedTag("", level)); + + NbtCompound chunk = (NbtCompound)NbtIo.ReadFile(VectorPath("palette_1")).Tag; + chunk.Set("DataVersion", new NbtInt(3953)); + + using (RegionFile file = RegionFile.Open(Path.Combine(region, "r.0.0.mca"), RegionFileMode.ReadWrite)) + { + file.WriteChunk(0, 0, chunk); + } + + List warnings = new List(); + WorldOpenOptions options = new WorldOpenOptions + { + ChunkRead = new ChunkReadOptions { OnWarning = message => warnings.Add(message) }, + }; + + using (MinecraftWorld world = MinecraftWorld.Open(root, options)) + { + Assert.NotNull(world.Dimension(Dimension.Overworld)!.Chunk(0, 0)); + } + + // ワールドを開くときに渡した通知先が、次元経由の読み込みでも呼ばれる + Assert.Single(warnings); + } + finally + { + System.IO.Directory.Delete(root, recursive: true); + } + } } } diff --git a/docs/README.md b/docs/README.md index b8629c5..cdc16f7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,9 +1,8 @@ -# SpringNBTLibrary ドキュメント +# SpringNBTLibraryドキュメント -Minecraft Java版 26.1 以降のワールド/NBTファイルを -複数の言語で同一に読み書きするライブラリのドキュメントです。 +Minecraft Java版26.1以降のワールド/NBTファイルを、複数の言語で同じように読み書きするライブラリのドキュメントです。 -まずどこを読むか: +まずどこを読むかは次のとおりです。 | 目的 | 読むもの | |---|---| @@ -20,7 +19,7 @@ Minecraft Java版 26.1 以降のワールド/NBTファイルを ### 機能 -- [機能一覧](features.md) — 機能 × 全言語の対応マトリクス +- [機能一覧](features.md): 機能 × 全言語の対応マトリクス ### はじめに @@ -32,15 +31,15 @@ Minecraft Java版 26.1 以降のワールド/NBTファイルを ### ガイド -- [01. NBT の読み書き](guide/01-nbt.md) +- [01. NBTの読み書き](guide/01-nbt.md) - [02. SNBT](guide/02-snbt.md) - [03. リージョンファイル (.mca)](guide/03-anvil-region.md) -- [04. ワールドと level.dat](guide/04-world-and-level-dat.md) +- [04. ワールドとlevel.dat](guide/04-world-and-level-dat.md) - [05. ブロックとバイオーム](guide/05-blocks-and-biomes.md) - [06. エラーと安全上限](guide/06-errors-and-limits.md) - [07. バージョンポリシー](guide/07-version-policy.md) -### API 対応表 +### API対応表 - [概要と命名規則](api/overview.md) - [NBT](api/nbt.md) @@ -50,17 +49,17 @@ Minecraft Java版 26.1 以降のワールド/NBTファイルを ### 仕様(実装者向け) - [00. 共通規約](spec/00-conventions.md) -- [10. NBT バイナリ形式](spec/10-nbt-binary.md) +- [10. NBTバイナリ形式](spec/10-nbt-binary.md) - [11. SNBT](spec/11-snbt.md) -- [20. Anvil リージョン形式](spec/20-anvil-region.md) +- [20. Anvilリージョン形式](spec/20-anvil-region.md) - [30. チャンク形式](spec/30-chunk-format.md) -- [31. パレット付きコンテナと BitStorage](spec/31-paletted-container.md) +- [31. パレット付きコンテナとBitStorage](spec/31-paletted-container.md) - [40. ワールドのディレクトリ構成](spec/40-world-layout.md) - [90. 適合性](spec/90-conformance.md) ### 開発 -- [実ワールドで検証する](spec/90-conformance.md#24-実ワールド走査) — `spec/tools/scan_world.py` +- [実ワールドで検証する](spec/90-conformance.md#24-実ワールド走査): `spec/tools/scan_world.py` - [新しい言語を追加する](contributing/porting-guide.md) - [リリース手順](contributing/release.md) - [変更履歴](../CHANGELOG.md) @@ -70,20 +69,18 @@ Minecraft Java版 26.1 以降のワールド/NBTファイルを - [0001. モノレポにする](adr/0001-monorepo.md) - [0002. 命名は各言語のイディオムに従う](adr/0002-idiomatic-naming.md) - [0003. ワールド形式で対応を判定する](adr/0003-version-policy.md) -- [0004. Heightmap と光源の再計算は行わない](adr/0004-defer-heightmap-recalc.md) +- [0004. Heightmapと光源の再計算は行わない](adr/0004-defer-heightmap-recalc.md) - [0005. 全言語で共通のエラーモデルを使う](adr/0005-unified-error-model.md) -- [0006. SNBT はバイナリへ写せる範囲だけを扱う](adr/0006-snbt-scope.md) -- [0007. TypeScript では i64 に bigint を使う](adr/0007-typescript-bigint.md) -- [0008. `session.lock` の確認は言語ごとにベストエフォートとする](adr/0008-session-lock.md) -- [0009. 公開APIの抽出は静的解析で行い、API 対応表は生成する](adr/0009-static-api-extraction.md) +- [0006. SNBTはバイナリへ写せる範囲だけを扱う](adr/0006-snbt-scope.md) +- [0007. TypeScriptではi64にbigintを使う](adr/0007-typescript-bigint.md) +- [0008. `session.lock`の確認は言語ごとにベストエフォートとする](adr/0008-session-lock.md) +- [0009. 公開APIの抽出はソースの静的解析で行い、API対応表は生成する](adr/0009-static-api-extraction.md) --- ## 仕様が先にある -実装より先に [`spec/`](spec/00-conventions.md) に仕様を書き、各言語はそれを実装しています。 -どの言語版でも同じ入力から同じバイト列が出るかは[適合性テスト](spec/90-conformance.md)が確かめていて、 -[機能一覧](features.md)が実装からずれていないかも CI で見ています。 +実装より先に[`spec/`](spec/00-conventions.md)に仕様を書き、各言語はそれを実装しています。 +どの言語版でも同じ入力から同じバイト列が出るかは[適合性テスト](spec/90-conformance.md)が確かめていて、[機能一覧](features.md)が実装からずれていないかもCIで見ています。 -Rust のツールで書いたワールドを Java のプラグインで読む、といった場面で、 -ライブラリごとの違いに悩まされずに済むのが狙いです。 +Rustのツールで書いたワールドをJavaのプラグインで読む、といった場面で、ライブラリごとの違いに悩まされずに済むのが狙いです。 diff --git a/docs/adr/0001-monorepo.md b/docs/adr/0001-monorepo.md index c8f58d9..592c06f 100644 --- a/docs/adr/0001-monorepo.md +++ b/docs/adr/0001-monorepo.md @@ -10,19 +10,17 @@ ## 決定 -**1つのリポジトリに `rust/` `csharp/` `java/` `python/` を並べる。** -仕様 (`docs/spec/`) と共通テストベクタ (`spec/testdata/`) はリポジトリ直下に置き、 -全言語がそれを参照する。 +**1つのリポジトリに`rust/` `csharp/` `java/` `typescript/` `python/`を並べる。** +仕様(`docs/spec/`)と共通テストベクタ(`spec/testdata/`)はリポジトリ直下に置き、全言語がそれを参照する。 ## 理由 - 仕様変更が全言語に波及するため、**1つのコミットで全言語を更新できる**ことが決定的に重要 -- テストベクタを共有リポジトリに置くと、バージョンずれが起きた瞬間に - 「どの言語が正しいのか」が分からなくなる -- クロス言語の適合性テストが、submodule や外部取得なしにそのまま走る +- テストベクタを共有リポジトリに置くと、バージョンずれが起きた時点で「どの言語が正しいのか」が分からなくなる +- クロス言語の適合性テストが、submoduleや外部取得なしにそのまま走る ## 結果として受け入れること -- 各言語のエコシステム慣習(`crates.io` からリポジトリ直下を期待する等)から外れる。 - 各パッケージのメタデータで `repository` とサブディレクトリを明示して補う -- CI が全言語分のツールチェーンを用意する必要がある +- 各言語のエコシステム慣習(`crates.io`がリポジトリ直下を期待する等)から外れる。 + 各パッケージのメタデータでリポジトリのURLを明示して補う(サブディレクトリまで書けるnpmでは`directory`も指定する) +- CIが全言語分のツールチェーンを用意する必要がある diff --git a/docs/adr/0002-idiomatic-naming.md b/docs/adr/0002-idiomatic-naming.md index 3ca425c..7645a17 100644 --- a/docs/adr/0002-idiomatic-naming.md +++ b/docs/adr/0002-idiomatic-naming.md @@ -11,34 +11,27 @@ ## 決定 **概念・型構成・機能の集合は完全に統一するが、命名は各言語の慣習に従う。** -論理名から各言語名への変換規則を -[spec/00-conventions.md 3章](../spec/00-conventions.md#3-命名変換規則)に定め、 -CI の `docs-sync` がその規則で公開APIを論理名へ写して全言語を突き合わせる。 +論理名から各言語名への変換規則を[spec/00-conventions.md 3章](../spec/00-conventions.md#3-命名変換規則)に定め、CIの`docs-sync`がその規則で公開APIを論理名へ写して全言語を比べる。 ## 理由 -- `region_file.ReadChunk()` のような、その言語では明らかに異物な名前は - 利用者の学習コストをむしろ上げる -- 変換規則が機械的である限り、「完全一致」でなくても - **論理名の集合が一致していることは自動検証できる**。同一性の保証は失われない +- `region_file.ReadChunk()`のような、その言語では明らかに異質な名前は、利用者の学習コストをむしろ上げる +- 変換規則が機械的である限り、「完全一致」でなくても論理名の集合が一致していることは自動検証できる。同一性の保証は失われない - 各言語利用者にとって自然に書けることが、全言語提供の価値を実際に届ける条件になる ## 結果として受け入れること -- 変換規則から外れる名前を付けると CI が落ちる。命名の自由度は下がる -- 言語をまたいで移植する際、名前は機械変換で追える一方、 - ドキュメントには全言語の対応表 (`docs/api/`) が必要になる +- 変換規則から外れる名前を付けるとCIが落ちる。命名の自由度は下がる +- 言語をまたいで移植する際、名前は機械変換で追える一方、ドキュメントには全言語の対応表(`docs/api/`)が必要になる -## 関連: 基準実装は C# +## 関連: 基準実装はC# -実装は **C# を基準**として先に書き、他3言語へ移植する。 +実装は**C#を基準**として先に書き、他4言語へ移植する。 理由は、設計者が最も慣れている言語だから。 5言語を1人で保守する体制では、**仕様の穴に気づく速さが設計者の習熟度に直結する**。 -不慣れな言語を基準にすると、言語と格闘している時間と -仕様を詰めている時間の区別がつかなくなり、判断を誤りやすい。 +不慣れな言語を基準にすると、言語と格闘している時間と仕様を詰めている時間の区別がつかなくなり、判断を誤りやすい。 -移植の順序は C# → Java → Python → Rust とする。 -C# と Java は型システムが近く差分が小さい。 -Rust を最後に置くのは、所有権と網羅的パターンマッチが -「他3言語では通ってしまう曖昧さ」を最後に炙り出す網として働くため。 +移植はC#の次にJavaを書き、Rustを最後に置く。 +C#とJavaは型システムが近く、差分が小さい。 +Rustを最後に置くのは、所有権と網羅的パターンマッチが、「他の言語では通ってしまう曖昧さ」を最後に洗い出す役目を果たすため。 diff --git a/docs/adr/0003-version-policy.md b/docs/adr/0003-version-policy.md index fa162cc..72aded7 100644 --- a/docs/adr/0003-version-policy.md +++ b/docs/adr/0003-version-policy.md @@ -5,48 +5,39 @@ ## 背景 -当初は「対象は Java版 26.2 (DataVersion 4903) の 1 バージョンだけ」と決め、 -それ以外は読み込み時に警告、書き込み時にエラーとしていた。 +当初は「対象はJava版26.2(DataVersion 4903)の1バージョンだけ」と決め、それ以外は読み込み時に警告、書き込み時にエラーとしていた。 -しかしこの決め方だと、Minecraft が更新されるたびに、 -**中身が何も変わっていなくても**利用者に警告が出る。 +しかしこの決め方だと、Minecraftが更新されるたびに、中身が何も変わっていなくても利用者に警告が出る。 ライブラリ側も追随して版を上げ続けることになる。 -実際に調べたところ、いまのワールド形式は 26.1 で入ったもので、26.2 でも変わっていない。 -26.3 のスナップショットにも形式変更は入っていない。 +実際に調べたところ、いまのワールド形式は26.1で入ったもので、26.2でも変わっていない。 +26.3のスナップショットにも形式変更は入っていない。 バージョン番号の一致を条件にしたのが間違いだった。 ## 決定 -判定の基準を **ワールドの形式** に変える。 +判定の基準を**ワールドの形式**に変える。 -- **NBT レイヤ (レイヤ1) は完全にバージョン非依存**。DataVersion を一切見ない -- **扱える形式の下限**を `MIN_SUPPORTED_DATA_VERSION = 4786`(26.1)とする -- **読み込み**: `4786` 以上なら何もしない。警告も出さない。 - 下限未満のときだけ `Warn`(既定)/ `Error` / `Ignore` から選ぶ -- **書き込み**: `DataVersion` は読んだ値をそのまま残す。書き換えない。 - 下限未満のチャンクは既定で `UNSUPPORTED_DATA_VERSION`。 - `allow_foreign_data_version = true` で明示的に許可できる -- `TARGET_DATA_VERSION = 4903` は「動作を確かめたバージョン」として残す +- **NBTレイヤ(レイヤ1)は完全にバージョン非依存**。DataVersionを一切見ない +- **扱える形式の下限**を`MIN_SUPPORTED_DATA_VERSION = 4786`(26.1)とする +- **読み込み**: `4786`以上なら何もしない。警告も出さない。 + 下限未満のときだけ`Warn`(既定)/ `Error` / `Ignore`から選ぶ +- **書き込み**: `DataVersion`は読んだ値をそのまま残す。書き換えない。 + 下限未満のチャンクは既定で`UNSUPPORTED_DATA_VERSION`。 + `allow_foreign_data_version = true`で明示的に許可できる +- `TARGET_DATA_VERSION = 4903`は「動作を確かめたバージョン」として残す ## 理由 -- 形式が同じなら読み書きできる。できることに警告を出すと、 - **本当に見るべき警告が埋もれる** -- `DataVersion` を書き換えると、そのワールドを開いたゲーム側が - アップグレードの要否を誤る。読んだ値を残すのが唯一の正解 -- 読み込みを厳格にすると、**壊れたワールドの調査**という有力な用途が塞がれる。 - 読むだけならデータを壊しようがない -- 書き込みを寛容にすると、形式の違うワールドを黙って上書きし、 - **利用者が気づかないうちに壊す**。ここは既定で止めるべき -- 許可フラグの名前を `allow_foreign_data_version` としているのは、 - 「変換する」ではなく「異物をそのまま許す」意味を名前で示すため +- 形式が同じなら読み書きできる。できることに警告を出すと、**本当に見るべき警告が埋もれる** +- `DataVersion`を書き換えると、そのワールドを開いたゲーム側がアップグレードの要否を誤る。読んだ値を残すのが唯一の正解 +- 読み込みを厳格にすると、おかしくなったワールドの調査という有力な用途に使えなくなる。 + 読むだけならデータを損ないようがない +- 書き込みを寛容にすると、形式の違うワールドでもエラーを出さずに上書きし、**利用者が気づかないうちにワールドをおかしくする**。ここは既定で止めるべき +- 許可フラグの名前を`allow_foreign_data_version`としているのは、「変換する」ではなく「異物をそのまま許す」意味を名前で示すため ## 結果として受け入れること -- 下限より古いワールドを新形式へ移行する用途には使えない。それは Minecraft 本体の仕事 -- 形式が変わったことに気づかないまま新しいワールドを読むと、 - 黙って誤読する余地がある。構造が想定と違えば `MALFORMED_DATA` で止まるが、 - それをすり抜ける変更はありうる -- 形式が変わったときは下限を上げる。扱えなくなるワールドが出るので、 - バージョン番号のいちばん上の桁も上げる +- 下限より古いワールドを新形式へ移行する用途には使えない。それはMinecraft本体の仕事 +- 形式が変わったことに気づかないまま新しいワールドを読むと、エラーを出さずに誤読する余地がある。構造が想定と違えば`MALFORMED_DATA`で止まるが、それをすり抜ける変更はありうる +- 形式が変わったときは下限を上げる。扱えなくなるワールドが出るので、バージョン番号のいちばん上の桁も上げる diff --git a/docs/adr/0004-defer-heightmap-recalc.md b/docs/adr/0004-defer-heightmap-recalc.md index c2b44ac..a212d15 100644 --- a/docs/adr/0004-defer-heightmap-recalc.md +++ b/docs/adr/0004-defer-heightmap-recalc.md @@ -1,34 +1,30 @@ -# 0004. Heightmap と光源の再計算は行わない +# 0004. Heightmapと光源の再計算は行わない - 状態: 採用 - 日付: 2026-08-29 ## 背景 -`set_block` でブロックを書き換えると、チャンクの `Heightmaps` と光源データが -実際の地形と食い違う。これを本ライブラリが再計算すべきかどうか。 +`set_block`でブロックを書き換えると、チャンクの`Heightmaps`と光源データが実際の地形と食い違う。 +これを本ライブラリが再計算すべきかどうか。 ## 決定 -**再計算しない。** 既存の値はそのまま保持し、 -代わりに無効化するAPIを提供する。 +**再計算しない。** 既存の値はそのまま保持し、代わりに無効化するAPIを提供する。 -- `chunk.clear_heightmaps()` — `Heightmaps` を削除し、Minecraft に再計算させる -- `chunk.invalidate_lighting()` — `isLightOn` を `0` にする +- `chunk.clear_heightmaps()`: `Heightmaps`を削除し、Minecraftに再計算させる +- `chunk.invalidate_lighting()`: `isLightOn`を`0`にする ## 理由 -- `MOTION_BLOCKING` などの判定には - **「どのブロックが移動を妨げるか」「どれが葉か」のブロック定義テーブル**が要る。 - これは全ブロック分のデータであり、バージョンごとに変わり、 - それ自体が本ライブラリと同規模の成果物になる -- 光源伝播の再計算はさらに重く、実装を誤ると見た目が壊れる -- Minecraft は読み込み時にこれらを再計算できる。 +- `MOTION_BLOCKING`などの判定には「どのブロックが移動を妨げるか」「どれが葉か」のブロック定義テーブルが要る。 + このテーブルは全ブロック分のデータであり、バージョンごとに変わり、それ自体が本ライブラリと同規模の成果物になる +- 光源伝播の再計算はさらに重く、実装を誤ると見た目がおかしくなる +- Minecraftは読み込み時にこれらを再計算できる。 **削除して任せるほうが、中途半端に計算するより安全** ## 結果として受け入れること - ブロック改変後、そのままでは草の生成やモブスポーン判定が一時的にずれうる。 - この制約は [機能一覧](../features.md) と - [ガイド05](../guide/05-blocks-and-biomes.md) に明記する + この制約は[機能一覧](../features.md)と[ガイド05](../guide/05-blocks-and-biomes.md)に明記する - ブロック定義テーブルを持つことになったら、この決定を見直す diff --git a/docs/adr/0005-unified-error-model.md b/docs/adr/0005-unified-error-model.md index abff140..0651eea 100644 --- a/docs/adr/0005-unified-error-model.md +++ b/docs/adr/0005-unified-error-model.md @@ -5,35 +5,32 @@ ## 背景 -エラー表現は言語ごとに大きく異なる(Rust の `Result`、Java の検査例外、 -C# / Python の非検査例外)。そのままだと -「同じ入力に対して同じエラーになる」ことを検証できない。 +エラー表現は言語ごとに大きく異なる(Rustの`Result`、Javaの検査例外、C# / TypeScript / Pythonの非検査例外)。 +そのままだと、「同じ入力に対して同じエラーになる」ことを検証できない。 ## 決定 -**共通の `ErrorCode` 列挙を全言語に置き、単一のエラー型がそれを持つ。** +**共通の`ErrorCode`列挙を全言語に置き、単一のエラー型がそれを持つ。** | 言語 | 表現 | |---|---| -| Rust | `Error { code, message, source }` を `Result` で返す | -| C# | `SpringNbtException : Exception`(`Code` プロパティ) | +| Rust | `Error { code, message, source }`を`Result`で返す | +| C# | `SpringNbtException : Exception`(`Code`プロパティ) | | Java | `SpringNbtException extends RuntimeException`(`code()`) | -| Python | `SpringNbtError(Exception)`(`code` 属性) | +| TypeScript | `SpringNbtError extends Error`(`code`プロパティ) | +| Python | `SpringNbtError(Exception)`(`code`属性) | -Java は**検査例外を使わない**。`IOException` は `ErrorCode.IO` でラップして送出する。 +Javaは**検査例外を使わない**。`IOException`は`ErrorCode.IO`でラップして送出する。 ## 理由 -- `ErrorCode` の集合が全言語で一致していることは `docs-sync` で機械検証できる。 - 適合性テストで「同じ入力 → 同じ `ErrorCode`」まで検証できるようになる -- Java で検査例外を使うと、Rust の `Result` や Python の例外と - メソッドシグネチャの形が変わり、[命名変換規則](../spec/00-conventions.md#3-命名変換規則)で - 対応づけられなくなる -- 原因例外は必ず `source` / `InnerException` / `getCause()` / `__cause__` に保持するので、 - 情報は失われない +- `ErrorCode`の集合が全言語で一致していることは`docs-sync`で機械検証できる。 + 適合性テストで「同じ入力 → 同じ`ErrorCode`」まで検証できるようになる +- Javaで検査例外を使うと、Rustの`Result`やPythonの例外とメソッドシグネチャの形が変わり、[命名変換規則](../spec/00-conventions.md#3-命名変換規則)で対応づけられなくなる +- 原因例外は必ず`source` / `InnerException` / `getCause()` / `cause` / `__cause__`に保持するので、情報は失われない ## 結果として受け入れること -- Java 利用者にとっては、`IOException` を明示的に扱う慣習から外れる。 - この理由を [ガイド06](../guide/06-errors-and-limits.md) に記載する -- Rust では `?` 演算子のために `From` を実装する必要がある +- Java利用者にとっては、`IOException`を明示的に扱う慣習から外れる。 + この理由を[ガイド06](../guide/06-errors-and-limits.md)に記載する +- Rustでは`?`演算子のために`From`を実装する必要がある diff --git a/docs/adr/0006-snbt-scope.md b/docs/adr/0006-snbt-scope.md index 7564bdb..1e20ee1 100644 --- a/docs/adr/0006-snbt-scope.md +++ b/docs/adr/0006-snbt-scope.md @@ -1,32 +1,31 @@ -# 0006. SNBT はバイナリへ写せる範囲だけを扱う +# 0006. SNBTはバイナリへ写せる範囲だけを扱う - 状態: 採用 - 日付: 2026-08-29 ## 背景 -1.21.5 以降の SNBT は `[1, "a", {}]` のような**異種要素のリスト**を許す。 -一方、バイナリ NBT の `TAG_List` は単一型しか表現できない。 +1.21.5以降のSNBTは`[1, "a", {}]`のような異種要素のリストを許す。 +一方、バイナリNBTの`TAG_List`は単一型しか表現できない。 ## 決定 -**異種リストをパースしたら `MALFORMED_DATA` を返す。** -SNBT の対応範囲は「バイナリ NBT へ損失なく写せる部分集合」とする。 +**異種リストをパースしたら`MALFORMED_DATA`を返す。** +SNBTの対応範囲は「バイナリNBTへ損失なく写せる部分集合」とする。 -その他の 1.21.5+ 拡張(16進・2進リテラル、桁区切り `_`、符号接尾辞、 -拡張エスケープ、末尾カンマ、`bool()` / `uuid()`)は**すべて対応する**。 +その他の1.21.5+拡張(16進・2進リテラル、桁区切り`_`、符号接尾辞、拡張エスケープ(`\N{文字名}`を除く)、末尾カンマ、`bool()` / `uuid()`)は**すべて対応する**。 これらはバイナリへ写せるため。 +`\N{文字名}`は、実装間でUnicode文字名の表が揃わないため対応せず、`UNSUPPORTED_FEATURE`を返す。 ## 理由 -- 異種リストは Minecraft 側でコマンド引数として特別な正規化を受ける。 - その正規化を再現するのは**コマンドパーサの領域**であり、 - データ形式ライブラリの責務を超える -- 中途半端に受理して勝手な型へ丸めると、 - 利用者が気づかないうちに意図と違うデータを書き込むことになる +- Minecraftは異種リストをバイナリへ保存するとき、Compoundでない要素を空のキーのCompound(`{"": <値>}`)に包み、ゲーム内では見えないように外して扱う。 + この付け外しは**ゲーム内部の変換**であり、データ形式ライブラリの責務を超える。 + 再現すると、利用者が書いた`{"": <値>}`のCompoundと包みとを区別できなくなる +- 中途半端に受理して勝手な型へ丸めると、利用者が気づかないうちに意図と違うデータを書き込むことになる - 受理しないと決めれば、挙動が全言語で一致することを保証しやすい ## 結果として受け入れること -- コマンド文字列をそのまま食わせる用途では使えない場合がある。 - その制約を [機能一覧](../features.md) と [ガイド02](../guide/02-snbt.md) に明記する +- コマンド文字列をそのまま渡す用途では使えない場合がある。 + その制約を[機能一覧](../features.md)と[ガイド02](../guide/02-snbt.md)に明記する diff --git a/docs/adr/0007-typescript-bigint.md b/docs/adr/0007-typescript-bigint.md index a17ad98..a6badc5 100644 --- a/docs/adr/0007-typescript-bigint.md +++ b/docs/adr/0007-typescript-bigint.md @@ -1,40 +1,35 @@ -# 0007. TypeScript では i64 に bigint を使う +# 0007. TypeScriptではi64にbigintを使う - 状態: 採用 - 日付: 2026-08-29 ## 背景 -TypeScript / JavaScript の `number` は IEEE 754 の倍精度で、 -整数として正確に表せるのは `±2^53 - 1` までである。 -NBT の `TAG_Long` は 64bit 符号付き整数なので、`number` では表しきれない。 +TypeScript / JavaScriptの`number`はIEEE 754の倍精度で、整数として正確に表せるのは`±2^53 - 1`までである。 +NBTの`TAG_Long`は64bit符号付き整数なので、`number`では表しきれない。 -実際に困る例として、`level.dat` の**ワールドシード**、`LastUpdate`、 -`InhabitedTime`、そしてブロック配置を格納する `LongArray` がある。 -どれも `number` で受けると値が静かに変わる。 +実際に困る例として、`world_gen_settings.dat`のワールドシード、`LastUpdate`、`InhabitedTime`、そしてブロック配置を格納する`LongArray`がある。 +どれも`number`で受けると、値が気づかないうちに変わる。 ## 決定 -**`TAG_Long` は `bigint`、`TAG_Long_Array` は `BigInt64Array` で保持する。** +**`TAG_Long`は`bigint`、`TAG_Long_Array`は`BigInt64Array`で保持する。** -`i8` / `i16` / `i32` は `number` のまま扱い、構築時に範囲検証を行う。 -`f32` は `Math.fround` で丸めてから保持する。 +`i8` / `i16` / `i32`は`number`のまま扱い、構築時に範囲検証を行う。 +`f32`は`Math.fround`で丸めてから保持する。 ## 理由 -- **静かに壊れるのが最悪**だから。`number` で受けると例外も警告も出ないまま - シード値が別の値になり、利用者はワールドを書き戻すまで気づけない -- `bigint` は ES2020 以降の標準で、Node.js 20 以上・現行ブラウザで使える。 +- **気づかないうちにおかしくなるのが最悪**だから。`number`で受けると例外も警告も出ないままシード値が別の値になり、利用者はワールドを書き戻すまで気づけない +- `bigint`はES2020以降の標準で、Node.js 20以上・現行ブラウザで使える。 対象環境を絞る理由にはならない -- `DataView.getBigInt64` / `setBigInt64` がそのまま使えるので、 - 読み書きのコードもむしろ素直になる +- `DataView.getBigInt64` / `setBigInt64`がそのまま使えるので、読み書きのコードもむしろ素直になる ## 結果として受け入れること -- 利用者は `tag.value` に `1n` のようなリテラルを書く必要がある。 - `number` と `bigint` は暗黙に混ぜられないため、 - `Number(...)` / `BigInt(...)` の変換を明示することになる -- JSON へそのまま渡せない(`JSON.stringify` は `bigint` で例外になる)。 - この点は [ガイド01](../guide/01-nbt.md) に明記する -- 型の対応表で TypeScript だけ `i64` の行が他言語と見た目が変わる。 - [spec/00-conventions.md](../spec/00-conventions.md#2-バイト順と数値型) に理由を併記する +- 利用者は`tag.value`に`1n`のようなリテラルを書く必要がある。 + `number`と`bigint`は暗黙に混ぜられないため、`Number(...)` / `BigInt(...)`の変換を明示することになる +- JSONへそのまま渡せない(`JSON.stringify`は`bigint`で例外になる)。 + この点は[ガイド01](../guide/01-nbt.md)に明記する +- 型の対応表では、TypeScriptだけ`i64`の行の見た目が他言語と変わる。 + [spec/00-conventions.md](../spec/00-conventions.md#2-バイト順と数値型)に理由を併記する diff --git a/docs/adr/0008-session-lock.md b/docs/adr/0008-session-lock.md index 8f69e97..9eeaca9 100644 --- a/docs/adr/0008-session-lock.md +++ b/docs/adr/0008-session-lock.md @@ -1,75 +1,63 @@ -# ADR 0008: `session.lock` の確認は言語ごとにベストエフォートとする +# 0008. `session.lock`の確認は言語ごとにベストエフォートとする - 状態: 採用 - 日付: 2026-08-29 ## 背景 -Minecraft はワールドを開いている間、`<ワールド>/session.lock` を開いたまま -**排他ロックを保持し続ける**。起動中のワールドに外部から書き込むと、 -Minecraft がメモリ上に持っているチャンクで上書きされてデータが失われる。 +Minecraftはワールドを開いている間、`<ワールド>/session.lock`を開いたまま排他ロックを保持し続ける。 +起動中のワールドに外部から書き込むと、Minecraftがメモリ上に持っているチャンクで上書きされてデータが失われる。 -したがって書き込みモードでワールドを開くときは、 -起動中でないことを確かめたい。 +したがって書き込みモードでワールドを開くときは、起動中でないことを確かめたい。 -問題は判定方法である。`session.lock` は Minecraft を終了しても**残り続ける**ため、 -ファイルの存在確認では何も判定できない。 +問題は判定方法である。`session.lock`はMinecraftを終了しても残り続けるため、ファイルの存在確認では何も判定できない。 「ロックが取れるか」を試すしか手が無い。 そしてファイルロックは、言語の標準ライブラリで扱えるかどうかが分かれる。 | 言語 | 標準の範囲で排他ロックを試せるか | 手段 | |---|:--:|---| -| C# | ○ | `FileStream` を `FileShare.None` で開く | +| C# | ○ | `FileStream`を`FileShare.None`で開く | | Java | ○ | `FileChannel.tryLock()` | -| Python | ○(POSIX のみ) | `fcntl.flock`。Windows には `fcntl` が無い | -| TypeScript | ✕ | Node に移植性のあるファイルロック API が無い | -| Rust | ✕ | `std` に無い。`fs2` などの外部クレートが要る | +| Python | ○(POSIXのみ) | `fcntl.flock`。Windowsには`fcntl`が無い | +| TypeScript | ✕ | Nodeに移植性のあるファイルロックAPIが無い | +| Rust | ✕ | `std`に無い。`fs2`などの外部クレートが要る | -本ライブラリは「1人の開発者が複数言語へ同一挙動のライブラリを提供する」ことを -差別化要因に据えている([0001](0001-monorepo.md))。 +本ライブラリは「1人の開発者が複数言語へ同一挙動のライブラリを提供する」ことを差別化要因に据えている([0001](0001-monorepo.md))。 その方針からすると、言語によって挙動が変わるのは望ましくない。 ## 選択肢 -1. **全言語で確認しない** — 挙動は完全に揃うが、C#/Java/Python で実現できる - 安全策をわざわざ捨てることになる。ワールドの破壊は取り返しがつかない -2. **外部依存を足して全言語で確認する** — Rust に `fs2`、TypeScript に - ネイティブアドオンが必要。依存を最小限に保つ方針と衝突し、 - TypeScript ではネイティブビルドが要るため導入の敷居も上がる -3. **できる言語だけ確認し、できない言語は素通しする** — 挙動は揃わないが、 - 揃わない箇所と理由が仕様に明記されていれば利用者は判断できる +1. **全言語で確認しない**。挙動は完全に揃うが、C#/Java/Pythonで実現できる安全策をわざわざ捨てることになる。ワールドの破壊は取り返しがつかない +2. **外部依存を足して全言語で確認する**。Rustに`fs2`、TypeScriptにネイティブアドオンが必要。依存を最小限に保つ方針と衝突し、TypeScriptではネイティブビルドが要るため導入の敷居も上がる +3. **できる言語だけ確認し、できない言語は確認しない**。挙動は揃わないが、揃わない箇所と理由が仕様に明記されていれば利用者は判断できる ## 決定 -**選択肢 3 を採る。** +**選択肢3を採る。** -- C# / Java / Python(POSIX)は排他ロックを試し、取れなければ `IO` エラーにする +- C# / Java / Python(POSIX)は排他ロックを試し、取れなければ`IO`エラーにする - TypeScript / Rust / Python(Windows)は**確認しない** -- `WorldOpenOptions.ignore_session_lock` は**全言語に用意する**。 - 確認しない言語では効果が無いが、API の形を揃えるほうが移植時に迷わない +- `WorldOpenOptions.ignore_session_lock`は**全言語に用意する**。 + 確認しない言語では効果が無いが、APIの形を揃えるほうが移植時に迷わない - 確認しない言語では、そのことをオプションのドキュメントコメントに明記する -- [`docs/features.md`](../features.md) では ✅ と 🔶 を使い分け、 - この差異が一覧から読み取れるようにする +- [`docs/features.md`](../features.md)では✅と🔶を使い分け、この差異が一覧から読み取れるようにする -## 根拠 +## 理由 -**「安全側に倒す」の向きが、この件では確認できる言語を優先する側にある。** +**「安全を優先する」という考え方は、この件では確認できる言語を優先する方向に働く。** 確認できる言語で確認をやめても、確認できない言語が安全になるわけではない。 一方で確認を残せば、その言語の利用者だけは守られる。 -揃えるために安全策を捨てるのは、揃えること自体が目的化している。 +揃えるために安全策を捨てれば、揃えること自体が目的になってしまう。 -**誤検出しないことを優先する。** TypeScript 版で「`session.lock` が存在するから -起動中」と判定すると、終了済みのワールドをすべて拒否してしまい実用にならない。 +**誤検出しないことを優先する。** TypeScript版で「`session.lock`が存在するから起動中」と判定すると、終了済みのワールドをすべて拒否してしまい、実用にならない。 判定できないなら、何もしないほうが正しい。 -**この差異は隠さない。** 利用者が「TypeScript 版でも守られている」と -誤解したまま起動中のワールドへ書き込むほうが、 -「守られていない」と知って自分で確かめるより危険である。 +**この差異は隠さない。** 利用者が「TypeScript版でも守られている」と誤解したまま起動中のワールドへ書き込むほうが、「守られていない」と知って自分で確かめるより危険である。 -## 影響 +## 結果として受け入れること - 適合性検証([spec/90](../spec/90-conformance.md))はこの挙動を対象にしない。 - ロック状態を再現するテストは環境依存が大きく、言語間 diff の題材に向かない -- Rust に `fs2` 相当を任意機能(feature)として足す余地は残す。 + ロック状態を再現するテストは環境依存が大きく、言語間diffの題材に向かない +- Rustに`fs2`相当を任意機能(feature)として足す余地は残す。 そのときも既定は「確認しない」のままとし、有効時のみ挙動が変わる形にする diff --git a/docs/adr/0009-static-api-extraction.md b/docs/adr/0009-static-api-extraction.md index 3567925..5140173 100644 --- a/docs/adr/0009-static-api-extraction.md +++ b/docs/adr/0009-static-api-extraction.md @@ -1,19 +1,18 @@ -# ADR 0009: 公開APIの抽出はソースの静的解析で行い、API 対応表は生成する +# 0009. 公開APIの抽出はソースの静的解析で行い、API対応表は生成する - 状態: 採用 - 日付: 2026-08-29 ## 背景 -本ライブラリの差別化要因は「1人の開発者が全言語へ同一の API を提供すること」に尽きる -([0001](0001-monorepo.md))。これを人手で維持するのは必ず破綻する。 -C# に `WriteBytes` を足して Rust に `write_bytes` を足し忘れれば、 -その時点でライブラリの価値そのものが失われる。 +本ライブラリの差別化要因は「1人の開発者が全言語へ同一のAPIを提供すること」に尽きる([0001](0001-monorepo.md))。 +人手での維持は必ずうまくいかなくなる。 +C#に`WriteBytes`を足してRustに`write_bytes`を足し忘れれば、その時点でライブラリの価値そのものが失われる。 したがって機械で守る必要がある。守るべきものは2つ。 1. **全言語の公開API集合が一致していること** -2. **[`docs/api/`](../api/overview.md) の対応表が実装と一致していること** +2. **[`docs/api/`](../api/overview.md)の対応表が実装と一致していること** ## 選択肢 @@ -21,8 +20,8 @@ C# に `WriteBytes` を足して Rust に `write_bytes` を足し忘れれば、 | 方法 | 長所 | 短所 | |---|---|---| -| 各言語の公式ツール(リフレクション / `javap` / `cargo public-api` / `.d.ts` 解析 / `inspect`) | 正確。マクロ展開後も見える | **CI に5言語ぶんのツールチェーンとビルド成果物が要る**。ツール自身のバージョン差で壊れる。5つの別々のツールが5通りの形式で出力するので、それを揃える層が結局要る | -| ソースの静的解析(正規表現) | 依存は Python 3 だけ。ビルド不要で数秒。壊れても原因が1か所 | マクロ生成やメタプログラミングは見えない。書き方の揺れに弱い | +| 各言語の公式ツール(リフレクション / `javap` / `cargo public-api` / `.d.ts`解析 / `inspect`) | 正確。マクロ展開後も見える | **CIに5言語ぶんのツールチェーンとビルド成果物が要る**。ツール自身のバージョン差で動作がおかしくなる。5つの別々のツールが5通りの形式で出力するので、それを揃える層が結局要る | +| ソースの静的解析(正規表現) | 依存はPython 3だけ。ビルド不要で数秒。おかしくなっても原因が1か所 | マクロ生成やメタプログラミングは見えない。書き方の揺れに弱い | ### 対応表の作り方 @@ -33,57 +32,48 @@ C# に `WriteBytes` を足して Rust に `write_bytes` を足し忘れれば、 ## 決定 -- **抽出はソースの静的解析で行う。** 実装は - [`spec/tools/extract_api.py`](../../spec/tools/extract_api.py) -- **`docs/api/*.md` の対応表は生成する。** 生成部分は - `` / `` で囲み、 - [`check_docs_sync.py`](../../spec/tools/check_docs_sync.py) が書き出す。 +- **抽出はソースの静的解析で行う。** 実装は[`spec/tools/extract_api.py`](../../spec/tools/extract_api.py) +- **`docs/api/*.md`の対応表は生成する。** 生成部分は`` / ``で囲み、[`check_docs_sync.py`](../../spec/tools/check_docs_sync.py)が書き出す。 導入の説明文はその外側に手書きで置く -- **説明文は基準実装である C# のドキュメントコメントから取る**([0002](0002-idiomatic-naming.md))。 - 説明を直すときは C# のソースを直す -- **CI は生成結果と現在のファイルを突き合わせ、違えば落とす。** - 開発者は `--write` で書き直す +- **説明文は基準実装であるC#のドキュメントコメントから取る**([0002](0002-idiomatic-naming.md))。 + 説明を直すときはC#のソースを直す +- **CIは生成結果と現在のファイルを比べ、違えば落とす。** + 開発者は`--write`で書き直す ### 一致検査の粒度 **型は完全一致を求め、メンバは求めない。** -型が欠けているのは機能そのものの欠落を意味するので、 -差異は `extract_api.EXPECTED_TYPE_GAPS` に**理由つきで**登録しなければ CI が落ちる。 +型が欠けているのは機能そのものの欠落を意味するので、差異は`extract_api.EXPECTED_TYPE_GAPS`に理由つきで登録しなければCIが落ちる。 -一方メンバは、言語の作法による差異が正当に生じる。 +一方、メンバには言語の作法による差異が正当に生じる。 -- Rust の `section_mut()` — 借用規則のため可変参照用の別メソッドが要る -- Rust の `as_longs()` / `into_longs()` — 所有権を渡すかどうかで2つに分かれる -- Java の `setFoo()` — 他言語はプロパティへ直接代入する -- `iter` / `items` / `entries` / `keys` — コレクションの走査は言語ごとに作法が違う +- Rustの`section_mut()`: 借用規則のため可変参照用の別メソッドが要る +- Rustの`as_longs()` / `into_longs()`: 所有権を渡すかどうかで2つに分かれる +- Javaの`setFoo()`: 他言語はプロパティへ直接代入する +- `iter` / `items` / `entries` / `keys`: コレクションの走査は言語ごとに作法が違う -これらを無理に揃えると、どの言語でも不自然な API になる。 -メンバの差異は対応表に「—」として**そのまま表示**し、 -利用者が事実を見て判断できるようにする。 +これらを無理に揃えると、どの言語でも不自然なAPIになる。 +メンバの差異は対応表に「—」としてそのまま表示し、利用者が事実を見て判断できるようにする。 -## 根拠 +## 理由 -**静的解析の弱点は、この設計では実際には効かない。** -マクロ生成が見えない問題は Rust の `scalar_accessors!` だけで、 -マクロ呼び出し行から名前を拾えば足りる。 -書き方の揺れに弱い点は、全言語を同じ人間が同じ方針で書いているので起きにくい。 +**静的解析の弱点は、この設計では実際には問題にならない。** +マクロ生成が見えない問題が起きるのはRustの`scalar_accessors!`だけで、マクロ呼び出し行から名前を拾えば足りる。 +書き方の揺れについては、全言語を同じ人間が同じ方針で書いているので起きにくい。 むしろ「揺れたら検出される」ほうが望ましい。 -**CI の軽さは、この検査が実際に回り続けるかを左右する。** -5言語のビルドを揃えないと動かない検査は、 -壊れたときに直されず放置されて形骸化する。 -Python 3 だけで数秒で終わるなら、そうならない。 +**CIの軽さは、この検査が実際に回り続けるかを左右する。** +5言語のビルドを揃えないと動かない検査は、おかしくなったときに直されず放置されて形骸化する。 +Python 3だけで数秒で終わるなら、そうならない。 **対応表を生成にしたことで、ドキュメントが実装より価値の高い成果物になった。** -手書きの表は「書いた時点の実装のスナップショット」でしかないが、 -生成した表は常に現在の実装そのものである。 +手書きの表は「書いた時点の実装のスナップショット」でしかないが、生成した表は常に現在の実装そのものである。 -## 影響 +## 結果として受け入れること -- 新しい公開型を足したら、`check_docs_sync.API_PAGES` にも足さないと CI が落ちる。 - これは意図した動作で、ドキュメントに載らない API を作らせないための仕掛けである -- 静的解析が読めない書き方(複雑なメタプログラミング)を新たに導入する場合は、 - 抽出器も同時に直す必要がある。実装より抽出器のほうが先に壊れるので、気づける -- 検証ツール(`conformance`)や内部ヘルパ(`_` 始まりのファイル)は - `extract_api.EXCLUDED_FILES` で対象外にしている +- 新しい公開型を足したら、`check_docs_sync.API_PAGES`にも足さないとCIが落ちる。 + これは意図した動作で、ドキュメントに載らないAPIを作らせないための仕掛けである +- 静的解析が読めない書き方(複雑なメタプログラミング)を新たに導入する場合は、抽出器も同時に直す必要がある。 + 実装より抽出器のほうが先におかしくなるので、気づける +- 検証ツール(`conformance`)や内部ヘルパ(`_`始まりのファイル)は、`extract_api.EXCLUDED_FILES`で対象外にしている diff --git a/docs/api/anvil.md b/docs/api/anvil.md index 0748e3c..ec54c6e 100644 --- a/docs/api/anvil.md +++ b/docs/api/anvil.md @@ -1,32 +1,32 @@ -# API 対応表:Anvil +# API対応表:Anvil -`.mca` リージョンファイルの読み書き。チャンクの中身は解釈せず、NBT として返す。 +`.mca`リージョンファイルの読み書き。チャンクの中身は解釈せず、NBTとして返す。 -- 使い方は [ガイド 03](../guide/03-anvil-region.md) -- バイトレベルの仕様は [spec/20](../spec/20-anvil-region.md) -- 命名の変換規則は [概要](overview.md#2-変換規則) +- 使い方は[ガイド03](../guide/03-anvil-region.md) +- バイトレベルの仕様は[spec/20](../spec/20-anvil-region.md) +- 命名の変換規則は[概要](overview.md#2-変換規則) | 論理名 | C# | Java | TypeScript | Python | Rust | 概要 | |---|---|---|---|---|---|---| -| **RegionFile** | `RegionFile` | `RegionFile` | `RegionFile` | `RegionFile` | `RegionFile` | Anvil のリージョンファイル (r.X.Z.mca) | +| **RegionFile** | `RegionFile` | `RegionFile` | `RegionFile` | `RegionFile` | `RegionFile` | Anvilのリージョンファイル (r.X.Z.mca) | | `chunk_positions` | `ChunkPositions()` | `chunkPositions()` | `chunkPositions()` | `chunk_positions()` | `chunk_positions()` | 存在するチャンクの座標を、ロケーションテーブルの並び順で列挙する | | `delete_chunk` | `DeleteChunk()` | `deleteChunk()` | `deleteChunk()` | `delete_chunk()` | `delete_chunk()` | | | `flush` | `Flush()` | `flush()` | `flush()` | `flush()` | `flush()` | 変更をファイルへ書き出す | | `has_chunk` | `HasChunk()` | `hasChunk()` | `hasChunk()` | `has_chunk()` | `has_chunk()` | チャンクが存在するか | | `open` | `Open()` | `open()` | `open()` | `open()` | `open()` | リージョンファイルを開く | | `optimize` | `Optimize()` | `optimize()` | `optimize()` | `optimize()` | `optimize()` | 全チャンクを隙間なく詰め直す | -| `read_chunk` | `ReadChunk()` | `readChunk()` | `readChunk()` | `read_chunk()` | `read_chunk()` | チャンクを NBT として読む | +| `read_chunk` | `ReadChunk()` | `readChunk()` | `readChunk()` | `read_chunk()` | `read_chunk()` | チャンクをNBTとして読む | | `read_chunk_raw` | `ReadChunkRaw()` | `readChunkRaw()` | `readChunkRaw()` | `read_chunk_raw()` | `read_chunk_raw()` | チャンクを圧縮されたまま取り出す | | `region_x` | `RegionX` | `regionX()` | `regionX` | `region_x` | `region_x()` | このリージョンのX座標 | | `region_z` | `RegionZ` | `regionZ()` | `regionZ` | `region_z` | `region_z()` | このリージョンのZ座標 | | `sector_size` | `SectorSize` | `SECTOR_SIZE` | `SECTOR_SIZE` | `SECTOR_SIZE` | `SECTOR_SIZE` | セクタ長 | | `timestamp` | `Timestamp()` | `timestamp()` | `timestamp()` | `timestamp()` | `timestamp()` | | | `to_bytes` | `ToBytes()` | `toBytes()` | `toBytes()` | `to_bytes()` | `to_bytes()` | | -| `write_chunk` | `WriteChunk()` | `writeChunk()` | `writeChunk()` | `write_chunk()` | `write_chunk()` | チャンクを NBT として、圧縮方式を指定して書き込む | +| `write_chunk` | `WriteChunk()` | `writeChunk()` | `writeChunk()` | `write_chunk()` | `write_chunk()` | チャンクをNBTとして、圧縮方式を指定して書き込む | | `write_chunk_raw` | `WriteChunkRaw()` | `writeChunkRaw()` | `writeChunkRaw()` | `write_chunk_raw()` | `write_chunk_raw()` | 圧縮済みのチャンクをそのまま書き込む | -| **RegionFolder** | `RegionFolder` | `RegionFolder` | `RegionFolder` | `RegionFolder` | `RegionFolder` | リージョンファイルが並ぶディレクトリ 1 つ分(region/、entities/、poi/ のいずれか) | +| **RegionFolder** | `RegionFolder` | `RegionFolder` | `RegionFolder` | `RegionFolder` | `RegionFolder` | リージョンファイルが並ぶディレクトリ1つ分(region/、entities/、poi/のいずれか) | | `cached_region_count` | `CachedRegionCount` | `cachedRegionCount()` | `cachedRegionCount()` | `cached_region_count` | `cached_region_count()` | いま開いているリージョンファイル数 | | `chunk_positions` | `ChunkPositions()` | `chunkPositions()` | `chunkPositions()` | `chunk_positions()` | `chunk_positions()` | このフォルダに存在する全チャンクの座標を列挙する | | `default_max_cached_regions` | `DefaultMaxCachedRegions` | `DEFAULT_MAX_CACHED_REGIONS` | `DEFAULT_MAX_CACHED_REGIONS` | `DEFAULT_MAX_CACHED_REGIONS` | `DEFAULT_MAX_CACHED_REGIONS` | 同時に開いておくリージョンファイル数の既定の上限 | @@ -40,7 +40,7 @@ | `read_chunk` | `ReadChunk()` | `readChunk()` | `readChunk()` | `read_chunk()` | `read_chunk()` | | | `region` | `Region` | `region()` | `region()` | `region()` | `region()` | リージョンファイルを取得する | | `region_positions` | `RegionPositions()` | `regionPositions()` | `regionPositions()` | `region_positions()` | `region_positions()` | このフォルダに存在するリージョンの座標を列挙する | -| `write_chunk` | `WriteChunk()` | `writeChunk()` | `writeChunk()` | `write_chunk()` | `write_chunk()` | チャンクを NBT として書き込む | +| `write_chunk` | `WriteChunk()` | `writeChunk()` | `writeChunk()` | `write_chunk()` | `write_chunk()` | チャンクをNBTとして書き込む | | `write_chunk_nbt` | — | — | — | — | `write_chunk_nbt()` | | | **RegionFileMode** | `RegionFileMode` | `RegionFileMode` | `RegionFileMode` | `RegionFileMode` | `RegionFileMode` | リージョンファイルを開くときの動作 | | `copy` | `Copy()` | `copy()` | `copy()` | `copy()` | `clone()` | | @@ -61,7 +61,7 @@ | `copy` | `Copy()` | `copy()` | `copy()` | `copy()` | `clone()` | | | `equals` | `Equals()` | `equals()` | `equals()` | `==` | `==` | | | `file_name` | `FileName` | `fileName()` | `fileName()` | `file_name()` | `file_name()` | このリージョンのファイル名(r.X.Z.mca) | -| `from_file_name` | `FromFileName()` | `fromFileName()` | `fromFileName()` | `from_file_name()` | `from_file_name()` | r.X.Z.mca 形式のファイル名から座標を得る | +| `from_file_name` | `FromFileName()` | `fromFileName()` | `fromFileName()` | `from_file_name()` | `from_file_name()` | r.X.Z.mca形式のファイル名から座標を得る | | `key` | — | — | `key()` | — | — | | | `to_string` | `ToString()` | `toString()` | `toString()` | `==` | `to_string()` | | | `x` | `X` | `x()` | `x` | `x` | `x` | リージョンX座標 | @@ -71,7 +71,7 @@ | `copy` | `Copy()` | `copy()` | `copy()` | `copy()` | `clone()` | | | `custom` | `Custom` | `CUSTOM` | `Custom` | `CUSTOM` | `Custom` | | | `equals` | `Equals()` | `equals()` | `equals()` | `==` | `==` | | -| `from_id` | `FromId()` | `fromId()` | `chunkCompressionFromId()` | `from_id()` | `from_id()` | 圧縮方式IDから ChunkCompression を得る | +| `from_id` | `FromId()` | `fromId()` | `chunkCompressionFromId()` | `from_id()` | `from_id()` | 圧縮方式IDからChunkCompressionを得る | | `gzip` | `Gzip` | `GZIP` | `Gzip` | `GZIP` | `Gzip` | | | `id` | `Id` | `id()` | `id` | `id()` | `id()` | | | `lz4` | `Lz4` | `LZ4` | `Lz4` | `LZ4` | `Lz4` | | @@ -82,10 +82,9 @@ | `copy` | `Copy()` | `copy()` | `copy()` | `copy()` | `clone()` | | | `data` | `Data` | `data()` | `data` | `data` | `data` | 圧縮されたままの本体 | | `equals` | `Equals()` | `equals()` | `equals()` | `==` | `==` | | -| `external` | `External` | `external()` | `external` | `external` | `external` | 外部ファイル c.X.Z.mcc に格納されていたか | +| `external` | `External` | `external()` | `external` | `external` | `external` | 外部ファイルc.X.Z.mccに格納されていたか | | `to_string` | `ToString()` | `toString()` | `toString()` | `==` | `to_string()` | | -この表は実装から生成している。手で直さず、 -`python3 spec/tools/check_docs_sync.py --write` で更新すること。 +この表は実装から生成している。手で直さず、`python3 spec/tools/check_docs_sync.py --write`で更新すること。 diff --git a/docs/api/nbt.md b/docs/api/nbt.md index 32c5f7b..6188d4f 100644 --- a/docs/api/nbt.md +++ b/docs/api/nbt.md @@ -1,19 +1,19 @@ -# API 対応表:NBT +# API対応表:NBT -NBT のタグ型・読み書き・SNBT。バージョンに依存しない純粋なコーデック。 +NBTのタグ型・読み書き・SNBT。バージョンに依存しない純粋なコーデック。 -- 使い方は [ガイド 01](../guide/01-nbt.md) / [ガイド 02](../guide/02-snbt.md) -- バイトレベルの仕様は [spec/10](../spec/10-nbt-binary.md) / [spec/11](../spec/11-snbt.md) -- 命名の変換規則は [概要](overview.md#2-変換規則) +- 使い方は[ガイド01](../guide/01-nbt.md) / [ガイド02](../guide/02-snbt.md) +- バイトレベルの仕様は[spec/10](../spec/10-nbt-binary.md) / [spec/11](../spec/11-snbt.md) +- 命名の変換規則は[概要](overview.md#2-変換規則) 「—」はその言語に対応するものが無いことを示す。 -理由は [概要 4章](overview.md#4-言語ごとに揃わないところ) にまとめてある。 +理由は[概要4章](overview.md#4-言語ごとに揃わないところ)にまとめてある。 | 論理名 | C# | Java | TypeScript | Python | Rust | 概要 | |---|---|---|---|---|---|---| -| **TagType** | `TagType` | `TagType` | `TagType` | `TagType` | `TagType` | NBT のタグ型 | +| **TagType** | `TagType` | `TagType` | `TagType` | `TagType` | `TagType` | NBTのタグ型 | | `as_string` | `AsString()` | `asString()` | `tagTypeAsString()` | `as_string()` | `as_str()` | 適合性テストで言語間比較に使う識別子を返す | | `byte` | `Byte` | `BYTE` | `Byte` | `BYTE` | `Byte` | TAG_Byte (1) | | `byte_array` | `ByteArray` | `BYTE_ARRAY` | `ByteArray` | `BYTE_ARRAY` | `ByteArray` | TAG_Byte_Array (7) | @@ -23,7 +23,7 @@ NBT のタグ型・読み書き・SNBT。バージョンに依存しない純粋 | `end` | `End` | `END` | `End` | `END` | `End` | | | `equals` | `Equals()` | `equals()` | `equals()` | `==` | `==` | | | `float` | `Float` | `FLOAT` | `Float` | `FLOAT` | `Float` | TAG_Float (5) | -| `from_id` | `FromId()` | `fromId()` | `tagTypeFromId()` | `from_id()` | `from_id()` | タグIDから TagType を得る | +| `from_id` | `FromId()` | `fromId()` | `tagTypeFromId()` | `from_id()` | `from_id()` | タグIDからTagTypeを得る | | `id` | `Id` | `id()` | `id` | `id()` | `id()` | | | `int` | `Int` | `INT` | `Int` | `INT` | `Int` | TAG_Int (3) | | `int_array` | `IntArray` | `INT_ARRAY` | `IntArray` | `INT_ARRAY` | `IntArray` | TAG_Int_Array (11) | @@ -32,7 +32,7 @@ NBT のタグ型・読み書き・SNBT。バージョンに依存しない純粋 | `long_array` | `LongArray` | `LONG_ARRAY` | `LongArray` | `LONG_ARRAY` | `LongArray` | TAG_Long_Array (12) | | `short` | `Short` | `SHORT` | `Short` | `SHORT` | `Short` | TAG_Short (2) | | `string` | `String` | `STRING` | `String` | `STRING` | `String` | TAG_String (8) | -| **NbtTag** | `NbtTag` | `NbtTag` | — | `NbtTag` | `NbtTag` | NBT のタグ | +| **NbtTag** | `NbtTag` | `NbtTag` | — | `NbtTag` | `NbtTag` | NBTのタグ | | `byte` | `Byte` | `BYTE` | `Byte` | `BYTE` | `Byte` | | | `byte_array` | `ByteArray` | `BYTE_ARRAY` | `ByteArray` | `BYTE_ARRAY` | `ByteArray` | | | `compound` | `Compound` | `COMPOUND` | `Compound` | `COMPOUND` | `Compound` | | @@ -163,17 +163,17 @@ NBT のタグ型・読み書き・SNBT。バージョンに依存しない純粋 | `opt_string_tag` | — | — | — | — | `opt_string_tag()` | | | `remove` | `Remove()` | `remove()` | `remove()` | `remove()` | `remove()` | | | `set` | `Set()` | `set()` | `set()` | `set()` | `set()` | 値を設定する | -| `set_bool` | `SetBool()` | `setBool()` | `setBool()` | `set_bool()` | `set_bool()` | TAG_Byte として設定する | -| `set_byte` | `SetByte()` | `setByte()` | `setByte()` | `set_byte()` | `set_byte()` | TAG_Byte として設定する | -| `set_byte_array` | `SetByteArray()` | `setByteArray()` | `setByteArray()` | `set_byte_array()` | `set_byte_array()` | TAG_Byte_Array として設定する | -| `set_double` | `SetDouble()` | `setDouble()` | `setDouble()` | `set_double()` | `set_double()` | TAG_Double として設定する | -| `set_float` | `SetFloat()` | `setFloat()` | `setFloat()` | `set_float()` | `set_float()` | TAG_Float として設定する | -| `set_int` | `SetInt()` | `setInt()` | `setInt()` | `set_int()` | `set_int()` | TAG_Int として設定する | -| `set_int_array` | `SetIntArray()` | `setIntArray()` | `setIntArray()` | `set_int_array()` | `set_int_array()` | TAG_Int_Array として設定する | -| `set_long` | `SetLong()` | `setLong()` | `setLong()` | `set_long()` | `set_long()` | TAG_Long として設定する | -| `set_long_array` | `SetLongArray()` | `setLongArray()` | `setLongArray()` | `set_long_array()` | `set_long_array()` | TAG_Long_Array として設定する | -| `set_short` | `SetShort()` | `setShort()` | `setShort()` | `set_short()` | `set_short()` | TAG_Short として設定する | -| `set_string` | `SetString()` | `setString()` | `setString()` | `set_string()` | `set_string()` | TAG_String として設定する | +| `set_bool` | `SetBool()` | `setBool()` | `setBool()` | `set_bool()` | `set_bool()` | TAG_Byteとして設定する | +| `set_byte` | `SetByte()` | `setByte()` | `setByte()` | `set_byte()` | `set_byte()` | TAG_Byteとして設定する | +| `set_byte_array` | `SetByteArray()` | `setByteArray()` | `setByteArray()` | `set_byte_array()` | `set_byte_array()` | TAG_Byte_Arrayとして設定する | +| `set_double` | `SetDouble()` | `setDouble()` | `setDouble()` | `set_double()` | `set_double()` | TAG_Doubleとして設定する | +| `set_float` | `SetFloat()` | `setFloat()` | `setFloat()` | `set_float()` | `set_float()` | TAG_Floatとして設定する | +| `set_int` | `SetInt()` | `setInt()` | `setInt()` | `set_int()` | `set_int()` | TAG_Intとして設定する | +| `set_int_array` | `SetIntArray()` | `setIntArray()` | `setIntArray()` | `set_int_array()` | `set_int_array()` | TAG_Int_Arrayとして設定する | +| `set_long` | `SetLong()` | `setLong()` | `setLong()` | `set_long()` | `set_long()` | TAG_Longとして設定する | +| `set_long_array` | `SetLongArray()` | `setLongArray()` | `setLongArray()` | `set_long_array()` | `set_long_array()` | TAG_Long_Arrayとして設定する | +| `set_short` | `SetShort()` | `setShort()` | `setShort()` | `set_short()` | `set_short()` | TAG_Shortとして設定する | +| `set_string` | `SetString()` | `setString()` | `setString()` | `set_string()` | `set_string()` | TAG_Stringとして設定する | | `to_string` | `ToString()` | `toString()` | `toString()` | `==` | `to_string()` | | | `type` | `Type` | `type()` | `type` | `type` | `tag_type()` | | | **NamedTag** | `NamedTag` | `NamedTag` | `NamedTag` | `NamedTag` | `NamedTag` | ルート名とルートタグの組 | @@ -189,12 +189,12 @@ NBT のタグ型・読み書き・SNBT。バージョンに依存しない純粋 | `gzip` | `Gzip` | `GZIP` | `Gzip` | `GZIP` | `Gzip` | GZip (RFC 1952) | | `none` | `None` | `NONE` | `None` | `NONE` | `None` | 無圧縮 | | `zlib` | `Zlib` | `ZLIB` | `Zlib` | `ZLIB` | `Zlib` | Zlib (RFC 1950) | -| **NbtFormat** | `NbtFormat` | `NbtFormat` | `NbtFormat` | `NbtFormat` | `NbtFormat` | NBT のルートタグの並び方 | +| **NbtFormat** | `NbtFormat` | `NbtFormat` | `NbtFormat` | `NbtFormat` | `NbtFormat` | NBTのルートタグの並び方 | | `copy` | `Copy()` | `copy()` | `copy()` | `copy()` | `clone()` | | | `equals` | `Equals()` | `equals()` | `equals()` | `==` | `==` | | | `java` | `Java` | `JAVA` | `Java` | `JAVA` | `Java` | ファイル形式 | -| `network` | `Network` | `NETWORK` | `Network` | `NETWORK` | `Network` | ネットワーク形式 (1.20.2 以降) | -| **NbtReadOptions** | `NbtReadOptions` | `NbtReadOptions` | `NbtReadOptions` | `NbtReadOptions` | `NbtReadOptions` | NBT 読み込みのオプション | +| `network` | `Network` | `NETWORK` | `Network` | `NETWORK` | `Network` | ネットワーク形式 (1.20.2以降) | +| **NbtReadOptions** | `NbtReadOptions` | `NbtReadOptions` | `NbtReadOptions` | `NbtReadOptions` | `NbtReadOptions` | NBT読み込みのオプション | | `compression` | `Compression` | `compression()` | `compression` | `compression` | `compression` | | | `copy` | `Copy()` | `copy()` | `copy()` | `copy()` | `clone()` | | | `default_max_depth` | — | `DEFAULT_MAX_DEPTH` | — | — | — | | @@ -207,7 +207,7 @@ NBT のタグ型・読み書き・SNBT。バージョンに依存しない純粋 | `equals` | `Equals()` | `equals()` | `equals()` | `==` | `==` | | | `tag` | `Tag` | `tag()` | `tag` | `tag` | `tag` | 読んだタグ | | `to_string` | `ToString()` | `toString()` | `toString()` | `==` | `to_string()` | | -| **NbtWriteOptions** | `NbtWriteOptions` | `NbtWriteOptions` | `NbtWriteOptions` | `NbtWriteOptions` | `NbtWriteOptions` | NBT 書き込みのオプション | +| **NbtWriteOptions** | `NbtWriteOptions` | `NbtWriteOptions` | `NbtWriteOptions` | `NbtWriteOptions` | `NbtWriteOptions` | NBT書き込みのオプション | | `compression` | `Compression` | `compression()` | `compression` | `compression` | `compression` | | | `copy` | `Copy()` | `copy()` | `copy()` | `copy()` | `clone()` | | | `format` | `Format` | `format()` | `format` | `format` | `format` | | @@ -217,15 +217,15 @@ NBT のタグ型・読み書き・SNBT。バージョンに依存しない純粋 | `biomes_per_section` | `BiomesPerSection` | `BIOMES_PER_SECTION` | `BIOMES_PER_SECTION` | `BIOMES_PER_SECTION` | `BIOMES_PER_SECTION` | | | `block_index` | `BlockIndex()` | `blockIndex()` | `blockIndex()` | `block_index()` | `block_index()` | | | `blocks_per_section` | `BlocksPerSection` | `BLOCKS_PER_SECTION` | `BLOCKS_PER_SECTION` | `BLOCKS_PER_SECTION` | `BLOCKS_PER_SECTION` | | -| `byte_length` | `ByteLength()` | `byteLength()` | `byteLength()` | `byte_length()` | `byte_length()` | 文字列を MUTF-8 で符号化したときのバイト長を求める | +| `byte_length` | `ByteLength()` | `byteLength()` | `byteLength()` | `byte_length()` | `byte_length()` | 文字列をMUTF-8で符号化したときのバイト長を求める | | `ceil_log2` | `CeilLog2()` | `ceilLog2()` | `ceilLog2()` | `ceil_log2()` | `ceil_log2()` | | -| `decode` | `Decode()` | `decode()` | `decode()` | `decode()` | `decode()` | MUTF-8 バイト列を文字列へ復号する | +| `decode` | `Decode()` | `decode()` | `decode()` | `decode()` | `decode()` | MUTF-8バイト列を文字列へ復号する | | `decode_to_utf16` | — | — | — | — | `decode_to_utf16()` | | | `decompress` | — | — | — | — | `decompress()` | | | `decompress_lz4` | — | — | `decompressLz4()` | `decompress_lz4()` | — | | | `default_max_cached_regions` | `DefaultMaxCachedRegions` | `DEFAULT_MAX_CACHED_REGIONS` | `DEFAULT_MAX_CACHED_REGIONS` | `DEFAULT_MAX_CACHED_REGIONS` | `DEFAULT_MAX_CACHED_REGIONS` | | | `detect_compression` | `DetectCompression()` | `detectCompression()` | `detectCompression()` | `detect_compression()` | `detect_compression()` | 先頭バイトから圧縮方式を判定する | -| `encode` | `Encode()` | `encode()` | `encode()` | `encode()` | `encode()` | 文字列を MUTF-8 バイト列へ符号化する | +| `encode` | `Encode()` | `encode()` | `encode()` | `encode()` | `encode()` | 文字列をMUTF-8バイト列へ符号化する | | `encode_from_utf16` | — | — | — | — | `encode_from_utf16()` | | | `from_double` | — | — | `fromDouble()` | `from_double()` | — | | | `from_f32` | — | — | — | — | `from_f32()` | | @@ -234,28 +234,27 @@ NBT のタグ型・読み書き・SNBT。バージョンに依存しない純粋 | `header_length` | — | — | — | `HEADER_LENGTH` | — | | | `is_bare_char` | — | — | — | `is_bare_char()` | — | | | `magic` | — | — | — | `MAGIC` | — | | -| `max_byte_length` | `MaxByteLength` | `MAX_BYTE_LENGTH` | `MAX_BYTE_LENGTH` | `MAX_BYTE_LENGTH` | `MAX_BYTE_LENGTH` | MUTF-8 の文字列が取りうる最大バイト長(長さフィールドが u16 のため) | +| `max_byte_length` | `MaxByteLength` | `MAX_BYTE_LENGTH` | `MAX_BYTE_LENGTH` | `MAX_BYTE_LENGTH` | `MAX_BYTE_LENGTH` | MUTF-8の文字列が取りうる最大バイト長(長さフィールドがu16のため) | | `method_compressed` | — | — | — | `METHOD_COMPRESSED` | — | | | `method_stored` | — | — | — | `METHOD_STORED` | — | | | `min_match` | — | — | — | `MIN_MATCH` | — | | -| `min_supported_data_version` | `MinSupportedDataVersion` | `MIN_SUPPORTED_DATA_VERSION` | `MIN_SUPPORTED_DATA_VERSION` | `MIN_SUPPORTED_DATA_VERSION` | `MIN_SUPPORTED_DATA_VERSION` | このライブラリが扱えるワールド形式の下限となる DataVersion (26.1) | -| `parse` | `Parse()` | `parse()` | `parse()` | `parse()` | `parse()` | SNBT 文字列をタグへ変換する | -| `parse_compound` | `ParseCompound()` | `parseCompound()` | `parseCompound()` | `parse_compound()` | `parse_compound()` | SNBT 文字列を Compound へ変換する | -| `read_bytes` | `ReadBytes()` | `readBytes()` | `readBytes()` | `read_bytes()` | `read_bytes()` | バイト列から NBT を読む | -| `read_bytes_all` | `ReadBytesAll()` | `readBytesAll()` | `readBytesAll()` | `read_bytes_all()` | `read_bytes_all()` | バイト列に連なっている NBT をすべて読む | -| `read_bytes_at` | `ReadBytesAt()` | `readBytesAt()` | `readBytesAt()` | `read_bytes_at()` | `read_bytes_at()` | バイト列の指定した位置から NBT を 1 つ読む | -| `read_file` | `ReadFile()` | `readFile()` | `readFile()` | `read_file()` | `read_file()` | ファイルから NBT を読む | +| `min_supported_data_version` | `MinSupportedDataVersion` | `MIN_SUPPORTED_DATA_VERSION` | `MIN_SUPPORTED_DATA_VERSION` | `MIN_SUPPORTED_DATA_VERSION` | `MIN_SUPPORTED_DATA_VERSION` | このライブラリが扱えるワールド形式の下限となるDataVersion (26.1) | +| `parse` | `Parse()` | `parse()` | `parse()` | `parse()` | `parse()` | SNBT文字列をタグへ変換する | +| `parse_compound` | `ParseCompound()` | `parseCompound()` | `parseCompound()` | `parse_compound()` | `parse_compound()` | SNBT文字列をCompoundへ変換する | +| `read_bytes` | `ReadBytes()` | `readBytes()` | `readBytes()` | `read_bytes()` | `read_bytes()` | バイト列からNBTを読む | +| `read_bytes_all` | `ReadBytesAll()` | `readBytesAll()` | `readBytesAll()` | `read_bytes_all()` | `read_bytes_all()` | バイト列に連なっているNBTをすべて読む | +| `read_bytes_at` | `ReadBytesAt()` | `readBytesAt()` | `readBytesAt()` | `read_bytes_at()` | `read_bytes_at()` | バイト列の指定した位置からNBTを1つ読む | +| `read_file` | `ReadFile()` | `readFile()` | `readFile()` | `read_file()` | `read_file()` | ファイルからNBTを読む | | `read_stream` | `ReadStream()` | `readStream()` | — | `read_stream()` | `read_reader()` | | | `sector_size` | `SectorSize` | `SECTOR_SIZE` | `SECTOR_SIZE` | `SECTOR_SIZE` | `SECTOR_SIZE` | | -| `target_data_version` | `TargetDataVersion` | `TARGET_DATA_VERSION` | `TARGET_DATA_VERSION` | `TARGET_DATA_VERSION` | `TARGET_DATA_VERSION` | 動作を確かめた Minecraft Java版の DataVersion (26.2) | +| `target_data_version` | `TargetDataVersion` | `TARGET_DATA_VERSION` | `TARGET_DATA_VERSION` | `TARGET_DATA_VERSION` | `TARGET_DATA_VERSION` | 動作を確かめたMinecraft Java版のDataVersion (26.2) | | `utf16_to_string` | — | — | — | — | `utf16_to_string()` | | -| `write` | `Write()` | `write()` | `write()` | `write()` | `write()` | タグを 1 行の SNBT へ変換する | -| `write_bytes` | `WriteBytes()` | `writeBytes()` | `writeBytes()` | `write_bytes()` | `write_bytes()` | NBT をバイト列へ書き出す | -| `write_file` | `WriteFile()` | `writeFile()` | `writeFile()` | `write_file()` | `write_file()` | NBT をファイルへ書き出す | +| `write` | `Write()` | `write()` | `write()` | `write()` | `write()` | タグを1行のSNBTへ変換する | +| `write_bytes` | `WriteBytes()` | `writeBytes()` | `writeBytes()` | `write_bytes()` | `write_bytes()` | NBTをバイト列へ書き出す | +| `write_file` | `WriteFile()` | `writeFile()` | `writeFile()` | `write_file()` | `write_file()` | NBTをファイルへ書き出す | | `write_pretty` | `WritePretty()` | `writePretty()` | `writePretty()` | `write_pretty()` | `write_pretty()` | | -| `write_stream` | `WriteStream()` | `writeStream()` | — | `write_stream()` | `write_writer()` | NBT をストリームへ書き出す | +| `write_stream` | `WriteStream()` | `writeStream()` | — | `write_stream()` | `write_writer()` | NBTをストリームへ書き出す | -この表は実装から生成している。手で直さず、 -`python3 spec/tools/check_docs_sync.py --write` で更新すること。 +この表は実装から生成している。手で直さず、`python3 spec/tools/check_docs_sync.py --write`で更新すること。 diff --git a/docs/api/overview.md b/docs/api/overview.md index e75081d..c641440 100644 --- a/docs/api/overview.md +++ b/docs/api/overview.md @@ -1,19 +1,18 @@ -# API 対応表:概要と命名規則 +# API対応表:概要と命名規則 同じ概念・同じ機能を全言語に提供している。違うのは**名前の綴りだけ**である。 このページは綴りの変換規則を示す。実際の対応表は次の3つ。 -- [NBT](nbt.md) — タグ、読み書き、SNBT -- [Anvil](anvil.md) — リージョンファイル -- [World](world.md) — ワールド、チャンク、ブロック +- [NBT](nbt.md)(タグ、読み書き、SNBT) +- [Anvil](anvil.md)(リージョンファイル) +- [World](world.md)(ワールド、チャンク、ブロック) --- ## 1. 論理名という考え方 -本ライブラリは、まず**言語に依存しない論理名**で API を定義し、 -各言語はそこから機械的に変換した名前を使う。 +本ライブラリは、まず言語に依存しない論理名でAPIを定義し、各言語はそこから機械的に変換した名前を使う。 ``` 論理名 read_file @@ -25,9 +24,7 @@ Python read_file(path) Rust read_file(path, &NbtReadOptions::default()) ``` -こうしておくと「C# で `WriteBytes` を足したのに Rust に `write_bytes` が無い」 -といった取りこぼしを、[`spec/tools/check_docs_sync.py`](../../spec/tools/check_docs_sync.py) -が機械的に見つけられる(→ [adr/0009](../adr/0009-static-api-extraction.md))。 +こうしておくと「C#で`WriteBytes`を足したのにRustに`write_bytes`が無い」といった取りこぼしを、[`spec/tools/check_docs_sync.py`](../../spec/tools/check_docs_sync.py)が機械的に見つけられる(→ [adr/0009](../adr/0009-static-api-extraction.md))。 対応表の「論理名」列がこれにあたる。 @@ -44,10 +41,9 @@ Rust read_file(path, &NbtReadOptions::default()) | 設定子 | `set_data_version` | `DataVersion` | `setDataVersion()` | `dataVersion` | `data_version` | `set_data_version()` | | 定数 | `TARGET_DATA_VERSION` | `TargetDataVersion` | `TARGET_DATA_VERSION` | `TARGET_DATA_VERSION` | `TARGET_DATA_VERSION` | `TARGET_DATA_VERSION` | -真偽値を返す取得子は論理名を `is_*` / `has_*` で始める -(`is_fully_generated`、`has_block_states`)。 +真偽値を返す取得子は論理名を`is_*` / `has_*`で始める(`is_fully_generated`、`has_block_states`)。 -詳しくは [仕様 00 の3章](../spec/00-conventions.md#3-命名変換規則)。 +詳しくは[仕様00の3章](../spec/00-conventions.md#3-命名変換規則)。 --- @@ -57,50 +53,44 @@ Rust read_file(path, &NbtReadOptions::default()) |---|---| | C# | `using SpringNBTLibrary.Nbt;` / `.Anvil` / `.World` | | Java | `import io.github.scriptarts.springnbt.nbt.*;` / `.anvil` / `.world` | -| TypeScript | `import { ... } from "spring-nbt-library";`(`/nbt` `/anvil` `/world` のサブパスも可) | +| TypeScript | `import { ... } from "spring-nbt-library";`(`/nbt` `/anvil` `/world`のサブパスも可) | | Python | `from spring_nbt_library.nbt import ...` | | Rust | `use spring_nbt_library::nbt::*;` | -C# と Java には自由関数が無いため、`read_file` のようなモジュール関数は -静的クラス(`NbtIo`、`Snbt`、`Mutf8`、`SpringNbt`)に置いている。 -対応表では**(モジュール関数)**の欄にまとめてある。 +C#とJavaには自由関数が無いため、`read_file`のようなモジュール関数は静的クラス(`NbtIo`、`Snbt`、`Mutf8`、`SpringNbt`)に置いている。 +対応表では「(モジュール関数)」の欄にまとめてある。 --- ## 4. 言語ごとに揃わないところ -言語の性質上どうしても揃えられない箇所は、[機能一覧の「言語ごとの差異」](../features.md#言語ごとの差異)に -理由つきで集約している。要点だけ挙げると次のとおり。 +言語の性質上どうしても揃えられない箇所は、[機能一覧の「言語ごとの差異」](../features.md#言語ごとの差異)に理由つきで集約している。要点だけ挙げると次のとおり。 | 箇所 | 差異 | |---|---| -| `TAG_Long` の値 | TypeScript のみ `bigint` | -| `TAG_String` の値 | Rust のみ 2 形態の列挙(孤立サロゲートを保持するため) | -| タグの表現 | Rust は `NbtTag` 列挙のバリアント、TypeScript は型の合併。個別の `NbtByte` 型は無い | -| エラー | Rust のみ `Result`。他4言語は例外 | -| 例外の型名 | C# / Java は `SpringNbtException`、TypeScript / Python / Rust は `SpringNbtError` / `Error` | -| `session.lock` | C# / Java / Python(POSIX) のみ確認する([adr/0008](../adr/0008-session-lock.md)) | +| `TAG_Long`の値 | TypeScriptのみ`bigint` | +| `TAG_String`の値 | Rustのみ2形態の列挙(孤立サロゲートを保持するため) | +| タグの表現 | Rustは`NbtTag`列挙のバリアントで、数値と配列のタグには個別の型が無い(`NbtByte`ではなく`NbtTag::Byte(i8)`)。TypeScriptの`NbtTag`は`NbtByte`などのクラスの合併型 | +| エラー | Rustのみ`Result`。他4言語は例外 | +| 例外の型名 | C# / Javaは`SpringNbtException`、TypeScript / Pythonは`SpringNbtError`、Rustは`Error` | +| `session.lock` | C# / Java / Python(POSIX)のみ確認する([adr/0008](../adr/0008-session-lock.md)) | -`ErrorCode` の値は全言語で一致する。分類と使い分けは -[ガイド 06](../guide/06-errors-and-limits.md) を参照。 +`ErrorCode`の値は全言語で一致する。分類と使い分けは[ガイド06](../guide/06-errors-and-limits.md)を参照。 --- ## 5. この対応表の作られ方 -各ページの表は**実装から自動生成している**。 -人手で書くと必ず実装から遅れるため、 -[`spec/tools/extract_api.py`](../../spec/tools/extract_api.py) が -全言語のソースから公開APIを抜き出し、 -[`check_docs_sync.py`](../../spec/tools/check_docs_sync.py) が表を組み立てる。 +各ページの表は実装から自動生成している。 +人手で書くと必ず実装から遅れるため、[`spec/tools/extract_api.py`](../../spec/tools/extract_api.py)が全言語のソースから公開APIを抜き出し、[`check_docs_sync.py`](../../spec/tools/check_docs_sync.py)が表を組み立てる。 ```bash # 実装に合わせて表を書き直す python3 spec/tools/check_docs_sync.py --write -# ずれていないか確かめる(CI で実行) +# ずれていないか確かめる(CIで実行) python3 spec/tools/check_docs_sync.py ``` -説明文は**基準実装である C# のドキュメントコメント**から取っている。 -説明を直したいときは C# のソースを直す。 +説明文は**基準実装であるC#のドキュメントコメント**から取っている。 +説明を直したいときはC#のソースを直す。 diff --git a/docs/api/world.md b/docs/api/world.md index 5fb4518..15985b6 100644 --- a/docs/api/world.md +++ b/docs/api/world.md @@ -1,55 +1,55 @@ -# API 対応表:World +# API対応表:World -ワールド・次元・チャンク・ブロック。ここだけが Minecraft のバージョンに依存する。 +ワールド・次元・チャンク・ブロック。ここだけがMinecraftのバージョンに依存する。 -- 使い方は [ガイド 04](../guide/04-world-and-level-dat.md) / [ガイド 05](../guide/05-blocks-and-biomes.md) -- 仕様は [spec/30](../spec/30-chunk-format.md) / [spec/31](../spec/31-paletted-container.md) / [spec/40](../spec/40-world-layout.md) -- 命名の変換規則は [概要](overview.md#2-変換規則) +- 使い方は[ガイド04](../guide/04-world-and-level-dat.md) / [ガイド05](../guide/05-blocks-and-biomes.md) +- 仕様は[spec/30](../spec/30-chunk-format.md) / [spec/31](../spec/31-paletted-container.md) / [spec/40](../spec/40-world-layout.md) +- 命名の変換規則は[概要](overview.md#2-変換規則) | 論理名 | C# | Java | TypeScript | Python | Rust | 概要 | |---|---|---|---|---|---|---| -| **MinecraftWorld** | `MinecraftWorld` | `MinecraftWorld` | `MinecraftWorld` | `MinecraftWorld` | `MinecraftWorld` | Minecraft Java版のセーブデータ 1 つ分 | -| `data_file` | `DataFile()` | `dataFile()` | `dataFile()` | `data_file()` | `data_file()` | data/minecraft/<name>.dat を読む | +| **MinecraftWorld** | `MinecraftWorld` | `MinecraftWorld` | `MinecraftWorld` | `MinecraftWorld` | `MinecraftWorld` | Minecraft Java版のセーブデータ1つ分 | +| `data_file` | `DataFile()` | `dataFile()` | `dataFile()` | `data_file()` | `data_file()` | data/minecraft/<name>.datを読む | | `dimension` | `Dimension` | `dimension()` | `dimension()` | `dimension()` | `dimension()` | 次元を得る | | `dimension_ids` | `DimensionIds()` | `dimensionIds()` | `dimensionIds()` | `dimension_ids()` | `dimension_ids()` | 存在する次元のIDを列挙する | | `directory` | `Directory` | `directory()` | `directory` | `directory` | `directory()` | ワールドディレクトリのパス | -| `level` | `Level` | `level()` | `level` | `level` | `level()` | level.dat の内容 | +| `level` | `Level` | `level()` | `level` | `level` | `level()` | level.datの内容 | | `open` | `Open()` | `open()` | `open()` | `open()` | `open()` | ワールドを開く | | `player` | `Player()` | `player()` | `player()` | `player()` | `player()` | | | `player_ids` | `PlayerIds()` | `playerIds()` | `playerIds()` | `player_ids()` | `player_ids()` | プレイヤーのUUID一覧 | -| `save_level` | `SaveLevel()` | `saveLevel()` | `saveLevel()` | `save_level()` | `save_level()` | level.dat を書き戻す | +| `save_level` | `SaveLevel()` | `saveLevel()` | `saveLevel()` | `save_level()` | `save_level()` | level.datを書き戻す | | **WorldOpenOptions** | `WorldOpenOptions` | `WorldOpenOptions` | `WorldOpenOptions` | `WorldOpenOptions` | `WorldOpenOptions` | ワールドを開くときの動作 | | `chunk_read` | `ChunkRead` | `chunkRead()` | `chunkRead` | `chunk_read` | `chunk_read` | チャンク読み込みのオプション | | `chunk_write` | `ChunkWrite` | `chunkWrite()` | `chunkWrite` | `chunk_write` | `chunk_write` | チャンク書き込みのオプション | -| `ignore_session_lock` | `IgnoreSessionLock` | `ignoreSessionLock()` | `ignoreSessionLock` | `ignore_session_lock` | `ignore_session_lock` | session.lock の確認を飛ばすか | +| `ignore_session_lock` | `IgnoreSessionLock` | `ignoreSessionLock()` | `ignoreSessionLock` | `ignore_session_lock` | `ignore_session_lock` | session.lockの確認を飛ばすか | | `writable` | `Writable` | `writable()` | `writable` | `writable` | `writable` | | -| **LevelData** | `LevelData` | `LevelData` | `LevelData` | `LevelData` | `LevelData` | level.dat の内容 | +| **LevelData** | `LevelData` | `LevelData` | `LevelData` | `LevelData` | `LevelData` | level.datの内容 | | `copy` | `Copy()` | `copy()` | `copy()` | `copy()` | `clone()` | | | `data` | `Data` | `data()` | `data` | `data` | `data()` | | | `data_version` | `DataVersion` | `dataVersion()` | `dataVersion()` | `data_version` | `data_version()` | チャンク構造のバージョン | -| `difficulty` | `Difficulty` | `difficulty()` | `difficulty()` | `difficulty` | `difficulty()` | 難易度(normal など) | +| `difficulty` | `Difficulty` | `difficulty()` | `difficulty()` | `difficulty` | `difficulty()` | 難易度(normalなど) | | `game_type` | `GameType` | `gameType()` | `gameType()` | `game_type` | `game_type()` | | | `is_hardcore` | `IsHardcore` | `isHardcore()` | `isHardcore()` | `is_hardcore` | `is_hardcore()` | ハードコアか | | `level_name` | `LevelName` | `levelName()` | `levelName()` | `level_name` | `level_name()` | ワールド名 | | `raw` | `Raw` | `raw()` | `raw` | `raw` | `raw()` | | | `spawn_dimension` | `SpawnDimension` | `spawnDimension()` | `spawnDimension()` | `spawn_dimension` | `spawn_dimension()` | スポーン地点の次元ID | -| `spawn_pos` | `SpawnPos` | `spawnPos()` | `spawnPos()` | `spawn_pos` | `spawn_pos()` | スポーン地点の [x, y, z] | +| `spawn_pos` | `SpawnPos` | `spawnPos()` | `spawnPos()` | `spawn_pos` | `spawn_pos()` | スポーン地点の[x, y, z] | | `time` | `Time` | `time()` | `time()` | `time` | `time()` | ワールドの経過時間(tick) | -| `to_named_tag` | `ToNamedTag()` | `toNamedTag()` | `toNamedTag()` | `to_named_tag()` | `to_named_tag()` | 書き出し用の NamedTag を作る | -| `version_name` | `VersionName` | `versionName()` | `versionName()` | `version_name` | `version_name()` | バージョン名(26.2 など) | -| **Dimension** | `Dimension` | `Dimension` | `Dimension` | `Dimension` | `Dimension` | ワールド内の次元 1 つ分 | +| `to_named_tag` | `ToNamedTag()` | `toNamedTag()` | `toNamedTag()` | `to_named_tag()` | `to_named_tag()` | 書き出し用のNamedTagを作る | +| `version_name` | `VersionName` | `versionName()` | `versionName()` | `version_name` | `version_name()` | バージョン名(26.2など) | +| **Dimension** | `Dimension` | `Dimension` | `Dimension` | `Dimension` | `Dimension` | ワールド内の次元1つ分 | | `chunk` | `Chunk` | `chunk()` | `chunk()` | `chunk()` | `chunk()` | チャンクを読む | | `chunk_mut` | — | — | — | — | `chunk_mut()` | | | `chunk_positions` | `ChunkPositions()` | `chunkPositions()` | `chunkPositions()` | `chunk_positions()` | `chunk_positions()` | この次元に存在する全チャンクの座標を列挙する | -| `data_file` | `DataFile()` | `dataFile()` | `dataFile()` | `data_file()` | `data_file()` | data/minecraft/<name>.dat を読む | +| `data_file` | `DataFile()` | `dataFile()` | `dataFile()` | `data_file()` | `data_file()` | data/minecraft/<name>.datを読む | | `directory` | `Directory` | `directory()` | `directory` | `directory` | `directory()` | この次元のディレクトリ | | `entity_folder` | `EntityFolder` | `entityFolder()` | `entityFolder()` | `entity_folder()` | `entity_folder()` | | | `flush` | `Flush()` | `flush()` | `flush()` | `flush()` | `flush()` | 変更したチャンクをすべて書き戻し、リージョンをディスクへ反映する | | `get_biome` | `GetBiome()` | `getBiome()` | `getBiome()` | `get_biome()` | `get_biome()` | | | `get_block` | `GetBlock()` | `getBlock()` | `getBlock()` | `get_block()` | `get_block()` | 絶対座標でブロックを取得する | -| `id` | `Id` | `id()` | `id` | `id` | `id()` | 次元ID(minecraft:overworld など) | +| `id` | `Id` | `id()` | `id` | `id` | `id()` | 次元ID(minecraft:overworldなど) | | `overworld` | `Overworld` | `OVERWORLD` | `OVERWORLD` | `OVERWORLD` | `OVERWORLD` | オーバーワールドの次元ID | | `poi_folder` | `PoiFolder` | `poiFolder()` | `poiFolder()` | `poi_folder()` | `poi_folder()` | | | `region_folder` | `RegionFolder` | `regionFolder()` | `regionFolder()` | `region_folder()` | `region_folder()` | | @@ -58,19 +58,19 @@ | `set_block` | `SetBlock()` | `setBlock()` | `setBlock()` | `set_block()` | `set_block()` | 絶対座標でブロックを設定する | | `the_end` | `TheEnd` | `THE_END` | `THE_END` | `THE_END` | `THE_END` | エンドの次元ID | | `the_nether` | `TheNether` | `THE_NETHER` | `THE_NETHER` | `THE_NETHER` | `THE_NETHER` | ネザーの次元ID | -| **Chunk** | `Chunk` | `Chunk` | `Chunk` | `Chunk` | `Chunk` | チャンク 1 つ分 | +| **Chunk** | `Chunk` | `Chunk` | `Chunk` | `Chunk` | `Chunk` | チャンク1つ分 | | `biome_index` | `BiomeIndex()` | `biomeIndex()` | `biomeIndex()` | `biome_index()` | `biome_index()` | | -| `biomes_per_section` | `BiomesPerSection` | `BIOMES_PER_SECTION` | `BIOMES_PER_SECTION` | `BIOMES_PER_SECTION` | `BIOMES_PER_SECTION` | セクション 1 つに入るバイオームのエントリ数(4×4×4 単位) | +| `biomes_per_section` | `BiomesPerSection` | `BIOMES_PER_SECTION` | `BIOMES_PER_SECTION` | `BIOMES_PER_SECTION` | `BIOMES_PER_SECTION` | セクション1つに入るバイオームのエントリ数(4×4×4単位) | | `block_index` | `BlockIndex()` | `blockIndex()` | `blockIndex()` | `block_index()` | `block_index()` | セクション内のブロック添字 | -| `blocks_per_section` | `BlocksPerSection` | `BLOCKS_PER_SECTION` | `BLOCKS_PER_SECTION` | `BLOCKS_PER_SECTION` | `BLOCKS_PER_SECTION` | セクション 1 つに入るブロック数 | -| `clear_heightmaps` | `ClearHeightmaps()` | `clearHeightmaps()` | `clearHeightmaps()` | `clear_heightmaps()` | `clear_heightmaps()` | Heightmaps を削除し、Minecraft に再計算させる | +| `blocks_per_section` | `BlocksPerSection` | `BLOCKS_PER_SECTION` | `BLOCKS_PER_SECTION` | `BLOCKS_PER_SECTION` | `BLOCKS_PER_SECTION` | セクション1つに入るブロック数 | +| `clear_heightmaps` | `ClearHeightmaps()` | `clearHeightmaps()` | `clearHeightmaps()` | `clear_heightmaps()` | `clear_heightmaps()` | Heightmapsを削除し、Minecraftに再計算させる | | `compact` | `Compact()` | `compact()` | `compact()` | `compact()` | `compact()` | 使われていないパレット要素を全セクションから取り除く | | `copy` | `Copy()` | `copy()` | `copy()` | `copy()` | `clone()` | | | `data_version` | `DataVersion` | `dataVersion()` | `dataVersion()` | `data_version` | `data_version()` | チャンク構造のバージョン | -| `from_nbt` | `FromNbt()` | `fromNbt()` | `fromNbt()` | `from_nbt()` | `from_nbt()` | NBT からチャンクを読む | +| `from_nbt` | `FromNbt()` | `fromNbt()` | `fromNbt()` | `from_nbt()` | `from_nbt()` | NBTからチャンクを読む | | `get_biome` | `GetBiome()` | `getBiome()` | `getBiome()` | `get_biome()` | `get_biome()` | バイオームを取得する | | `get_block` | `GetBlock()` | `getBlock()` | `getBlock()` | `get_block()` | `get_block()` | ブロックを取得する | -| `invalidate_lighting` | `InvalidateLighting()` | `invalidateLighting()` | `invalidateLighting()` | `invalidate_lighting()` | `invalidate_lighting()` | isLightOn を 0 にし、光源の再計算を促す | +| `invalidate_lighting` | `InvalidateLighting()` | `invalidateLighting()` | `invalidateLighting()` | `invalidate_lighting()` | `invalidate_lighting()` | isLightOnを0にし、光源の再計算を促す | | `is_fully_generated` | `IsFullyGenerated` | `isFullyGenerated()` | `isFullyGenerated()` | `is_fully_generated` | `is_fully_generated()` | | | `is_modified` | `IsModified` | `isModified()` | `isModified()` | `is_modified` | `is_modified()` | このチャンクに変更が加わったか | | `min_section_y` | `MinSectionY` | `minSectionY()` | `minSectionY()` | `min_section_y` | `min_section_y()` | | @@ -80,48 +80,48 @@ | `section_ys` | `SectionYs` | `sectionYs()` | `sectionYs()` | `section_ys` | `section_ys()` | | | `set_biome` | `SetBiome()` | `setBiome()` | `setBiome()` | `set_biome()` | `set_biome()` | | | `set_block` | `SetBlock()` | `setBlock()` | `setBlock()` | `set_block()` | `set_block()` | ブロックを設定する | -| `status` | `Status` | `status()` | `status()` | `status` | `status()` | 生成段階(minecraft:full など) | -| `to_nbt` | `ToNbt()` | `toNbt()` | `toNbt()` | `to_nbt()` | `to_nbt()` | NBT へ書き戻す | +| `status` | `Status` | `status()` | `status()` | `status` | `status()` | 生成段階(minecraft:fullなど) | +| `to_nbt` | `ToNbt()` | `toNbt()` | `toNbt()` | `to_nbt()` | `to_nbt()` | NBTへ書き戻す | | `x` | `X` | `x()` | `x()` | `x` | `x()` | 絶対チャンクX座標 | | `z` | `Z` | `z()` | `z()` | `z` | `z()` | 絶対チャンクZ座標 | -| **ChunkSection** | `ChunkSection` | `ChunkSection` | `ChunkSection` | `ChunkSection` | `ChunkSection` | チャンクを Y 方向に 16 ブロックずつ区切った 16×16×16 の立方体 | +| **ChunkSection** | `ChunkSection` | `ChunkSection` | `ChunkSection` | `ChunkSection` | `ChunkSection` | チャンクをY方向に16ブロックずつ区切った16×16×16の立方体 | | `biomes` | `Biomes` | `biomes()` | `biomes()` | `biomes` | `biomes()` | | | `biomes_mut` | — | — | — | — | `biomes_mut()` | | | `block_states` | `BlockStates` | `blockStates()` | `blockStates()` | `block_states` | `block_states()` | | | `block_states_mut` | — | — | — | — | `block_states_mut()` | | | `compact` | `Compact()` | `compact()` | `compact()` | `compact()` | `compact()` | 使われていないパレット要素を取り除く | | `copy` | `Copy()` | `copy()` | `copy()` | `copy()` | `clone()` | | -| `from_nbt` | `FromNbt()` | `fromNbt()` | `fromNbt()` | `from_nbt()` | `from_nbt()` | NBT からセクションを読む | +| `from_nbt` | `FromNbt()` | `fromNbt()` | `fromNbt()` | `from_nbt()` | `from_nbt()` | NBTからセクションを読む | | `has_biomes` | `HasBiomes` | `hasBiomes()` | `hasBiomes()` | `has_biomes` | `has_biomes()` | バイオームを持つか | | `has_block_states` | `HasBlockStates` | `hasBlockStates()` | `hasBlockStates()` | `has_block_states` | `has_block_states()` | ブロック状態を持つか | | `raw` | `Raw` | `raw()` | `raw` | `raw` | `raw()` | | | `to_nbt` | `ToNbt()` | `toNbt()` | `toNbt()` | `to_nbt()` | `to_nbt()` | | | `y` | `Y` | `y()` | `y` | `y` | `y()` | | | **ChunkReadOptions** | `ChunkReadOptions` | `ChunkReadOptions` | `ChunkReadOptions` | `ChunkReadOptions` | `ChunkReadOptions` | チャンク読み込みのオプション | -| `lenient_bit_storage` | `LenientBitStorage` | `lenientBitStorage()` | `lenientBitStorage` | `lenient_bit_storage` | `lenient_bit_storage` | data の長さが期待値と違うとき、長さからビット幅を逆算して読むか | -| `on_version_mismatch` | `OnVersionMismatch` | `onVersionMismatch()` | `onVersionMismatch` | `on_version_mismatch` | `on_version_mismatch` | DataVersion が扱える形式より古いときの動作 | +| `lenient_bit_storage` | `LenientBitStorage` | `lenientBitStorage()` | `lenientBitStorage` | `lenient_bit_storage` | `lenient_bit_storage` | dataの長さが期待値と違うとき、長さからビット幅を逆算して読むか | +| `on_version_mismatch` | `OnVersionMismatch` | `onVersionMismatch()` | `onVersionMismatch` | `on_version_mismatch` | `on_version_mismatch` | DataVersionが扱える形式より古いときの動作 | | `on_warning` | `OnWarning` | `onWarning()` | `onWarning` | `on_warning` | `on_warning` | | | **ChunkWriteOptions** | `ChunkWriteOptions` | `ChunkWriteOptions` | `ChunkWriteOptions` | `ChunkWriteOptions` | `ChunkWriteOptions` | チャンク書き込みのオプション | -| `allow_foreign_data_version` | `AllowForeignDataVersion` | `allowForeignDataVersion()` | `allowForeignDataVersion` | `allow_foreign_data_version` | `allow_foreign_data_version` | 対象バージョン以外の DataVersion を持つチャンクの書き戻しを許すか | +| `allow_foreign_data_version` | `AllowForeignDataVersion` | `allowForeignDataVersion()` | `allowForeignDataVersion` | `allow_foreign_data_version` | `allow_foreign_data_version` | 扱える形式より古いDataVersionを持つチャンクの書き戻しを許すか | | `copy` | `Copy()` | `copy()` | `copy()` | `copy()` | `clone()` | | -| **VersionMismatchAction** | `VersionMismatchAction` | `VersionMismatchAction` | `VersionMismatchAction` | `VersionMismatchAction` | `VersionMismatchAction` | DataVersion が対象と違ったときの動作 | +| **VersionMismatchAction** | `VersionMismatchAction` | `VersionMismatchAction` | `VersionMismatchAction` | `VersionMismatchAction` | `VersionMismatchAction` | DataVersionが扱える形式より古いときの動作 | | `copy` | `Copy()` | `copy()` | `copy()` | `copy()` | `clone()` | | | `equals` | `Equals()` | `equals()` | `equals()` | `==` | `==` | | -| `error` | `Error` | `ERROR` | `Error` | `ERROR` | `Error` | UnsupportedDataVersion の例外にする | +| `error` | `Error` | `ERROR` | `Error` | `ERROR` | `Error` | UnsupportedDataVersionの例外にする | | `ignore` | `Ignore` | `IGNORE` | `Ignore` | `IGNORE` | `Ignore` | 何もしない | | `warn` | `Warn` | `WARN` | `Warn` | `WARN` | `Warn` | | | **BlockState** | `BlockState` | `BlockState` | `BlockState` | `BlockState` | `BlockState` | ブロックの状態 | | `copy` | `Copy()` | `copy()` | `copy()` | `copy()` | `clone()` | | | `equals` | `Equals()` | `equals()` | `equals()` | `==` | `==` | | -| `from_nbt` | `FromNbt()` | `fromNbt()` | `fromNbt()` | `from_nbt()` | `from_nbt()` | パレット要素の NBT から作る | +| `from_nbt` | `FromNbt()` | `fromNbt()` | `fromNbt()` | `from_nbt()` | `from_nbt()` | パレット要素のNBTから作る | | `name` | `Name` | `name()` | `name()` | `name` | `name()` | ブロックID(名前空間つき) | | `of` | `Of()` | `of()` | `of()` | `of()` | `of()` | | -| `parse` | `Parse()` | `parse()` | `parse()` | `parse()` | `parse()` | minecraft:oak_stairs[facing=north,half=top] 形式の文字列から作る | +| `parse` | `Parse()` | `parse()` | `parse()` | `parse()` | `parse()` | minecraft:oak_stairs[facing=north,half=top]形式の文字列から作る | | `properties` | `Properties` | `properties()` | `properties()` | `properties` | `properties()` | | | `property` | `Property()` | `property()` | `property()` | `property()` | `property()` | | -| `to_nbt` | `ToNbt()` | `toNbt()` | `toNbt()` | `to_nbt()` | `to_nbt()` | パレット要素の NBT へ変換する | -| `to_string` | `ToString()` | `toString()` | `toString()` | `==` | `to_string()` | minecraft:oak_stairs[facing=north,half=top] 形式の文字列を返す | -| `with` | `With()` | `with()` | `with()` | `with_property()` | `with()` | プロパティを 1 つ差し替えた新しい状態を返す | +| `to_nbt` | `ToNbt()` | `toNbt()` | `toNbt()` | `to_nbt()` | `to_nbt()` | パレット要素のNBTへ変換する | +| `to_string` | `ToString()` | `toString()` | `toString()` | `==` | `to_string()` | minecraft:oak_stairs[facing=north,half=top]形式の文字列を返す | +| `with` | `With()` | `with()` | `with()` | `with_property()` | `with()` | プロパティを1つ差し替えた新しい状態を返す | | **BlockPos** | `BlockPos` | `BlockPos` | `BlockPos` | `BlockPos` | `BlockPos` | ブロックの絶対座標 | | `chunk_pos` | `ChunkPos` | `chunkPos()` | `chunkPos()` | `chunk_pos` | `chunk_pos()` | この座標を含むチャンクの座標 | | `copy` | `Copy()` | `copy()` | `copy()` | `copy()` | `clone()` | | @@ -133,20 +133,20 @@ | `x` | `X` | `x()` | `x` | `x` | `x` | X座標 | | `y` | `Y` | `y()` | `y` | `y` | `y` | Y座標 | | `z` | `Z` | `z()` | `z` | `z` | `z` | Z座標 | -| **Cuboid** | `Cuboid` | `Cuboid` | `Cuboid` | `Cuboid` | `Cuboid` | ブロック座標の直方体な範囲 | +| **Cuboid** | `Cuboid` | `Cuboid` | `Cuboid` | `Cuboid` | `Cuboid` | ブロック座標の直方体の範囲 | | `copy` | `Copy()` | `copy()` | `copy()` | `copy()` | `clone()` | | | `equals` | `Equals()` | `equals()` | `equals()` | `==` | `==` | | -| `max_x` | `MaxX` | `maxX()` | `maxX` | `max_x` | `max_x` | X の最大値(含む) | -| `max_y` | `MaxY` | `maxY()` | `maxY` | `max_y` | `max_y` | Y の最大値(含む) | -| `max_z` | `MaxZ` | `maxZ()` | `maxZ` | `max_z` | `max_z` | Z の最大値(含む) | -| `min_x` | `MinX` | `minX()` | `minX` | `min_x` | `min_x` | X の最小値 | -| `min_y` | `MinY` | `minY()` | `minY` | `min_y` | `min_y` | Y の最小値 | -| `min_z` | `MinZ` | `minZ()` | `minZ` | `min_z` | `min_z` | Z の最小値 | +| `max_x` | `MaxX` | `maxX()` | `maxX` | `max_x` | `max_x` | Xの最大値(含む) | +| `max_y` | `MaxY` | `maxY()` | `maxY` | `max_y` | `max_y` | Yの最大値(含む) | +| `max_z` | `MaxZ` | `maxZ()` | `maxZ` | `max_z` | `max_z` | Zの最大値(含む) | +| `min_x` | `MinX` | `minX()` | `minX` | `min_x` | `min_x` | Xの最小値 | +| `min_y` | `MinY` | `minY()` | `minY` | `min_y` | `min_y` | Yの最小値 | +| `min_z` | `MinZ` | `minZ()` | `minZ` | `min_z` | `min_z` | Zの最小値 | | `of` | `Of()` | `of()` | `of()` | `of()` | `of()` | 両端の座標から作る | | `positions` | `Positions()` | `positions()` | `positions()` | `positions()` | `positions()` | 範囲内の座標を順に返す | -| `size_x` | `SizeX` | `sizeX()` | `sizeX()` | `size_x` | `size_x()` | X 方向の長さ | -| `size_y` | `SizeY` | `sizeY()` | `sizeY()` | `size_y` | `size_y()` | Y 方向の長さ | -| `size_z` | `SizeZ` | `sizeZ()` | `sizeZ()` | `size_z` | `size_z()` | Z 方向の長さ | +| `size_x` | `SizeX` | `sizeX()` | `sizeX()` | `size_x` | `size_x()` | X方向の長さ | +| `size_y` | `SizeY` | `sizeY()` | `sizeY()` | `size_y` | `size_y()` | Y方向の長さ | +| `size_z` | `SizeZ` | `sizeZ()` | `sizeZ()` | `size_z` | `size_z()` | Z方向の長さ | | `to_string` | `ToString()` | `toString()` | `toString()` | `==` | `to_string()` | | | `volume` | `Volume` | `volume()` | `volume()` | `volume` | `volume()` | 含まれるブロックの個数 | | **PalettedContainer** | `PalettedContainer` | `PalettedContainer` | `PalettedContainer` | `PalettedContainer` | `PalettedContainer` | パレットとビットストレージの組 | @@ -157,29 +157,28 @@ | `entry_count` | `EntryCount` | `entryCount()` | `entryCount()` | `entry_count` | `entry_count()` | | | `fill` | `Fill()` | `fill()` | `fill()` | `fill()` | `fill()` | | | `filled` | `Filled()` | `filled()` | `filled()` | `filled()` | `filled()` | 単一の値で埋めたコンテナを作る | -| `from_nbt` | `FromNbt()` | `fromNbt()` | `fromNbt()` | `from_nbt()` | `from_nbt()` | NBT から読み込む | +| `from_nbt` | `FromNbt()` | `fromNbt()` | `fromNbt()` | `from_nbt()` | `from_nbt()` | NBTから読み込む | | `get` | `Get()` | `get()` | `get()` | `get()` | `get()` | 添字の値を取り出す | | `min_bits` | `MinBits` | `minBits()` | `minBits()` | `min_bits` | `min_bits()` | | | `palette` | `Palette` | `palette()` | `palette()` | `palette` | `palette()` | | | `set` | `Set()` | `set()` | `set()` | `set()` | `set()` | | -| `to_nbt` | `ToNbt()` | `toNbt()` | `toNbt()` | `to_nbt()` | `to_nbt()` | NBT へ変換する | -| **BitStorage** | `BitStorage` | `BitStorage` | `BitStorage` | `BitStorage` | `BitStorage` | 添字を 64bit 整数の配列へ詰めた表現 | +| `to_nbt` | `ToNbt()` | `toNbt()` | `toNbt()` | `to_nbt()` | `to_nbt()` | NBTへ変換する | +| **BitStorage** | `BitStorage` | `BitStorage` | `BitStorage` | `BitStorage` | `BitStorage` | 添字を64bit整数の配列へ詰めた表現 | | `as_longs` | — | — | — | — | `as_longs()` | | -| `bits_per_entry` | `BitsPerEntry` | `bitsPerEntry()` | `bitsPerEntry()` | `bits_per_entry` | `bits_per_entry()` | 1 エントリあたりのビット数 | +| `bits_per_entry` | `BitsPerEntry` | `bitsPerEntry()` | `bitsPerEntry()` | `bits_per_entry` | `bits_per_entry()` | 1エントリあたりのビット数 | | `copy` | `Copy()` | `copy()` | `copy()` | `copy()` | `clone()` | | | `create` | `Create()` | `create()` | `create()` | `create()` | `create()` | すべてゼロで初期化した記憶域を作る | | `entry_count` | `EntryCount` | `entryCount()` | `entryCount()` | `entry_count` | `entry_count()` | | | `equals` | `Equals()` | `equals()` | `equals()` | `==` | `==` | | -| `from_longs` | `FromLongs()` | `fromLongs()` | `fromLongs()` | `from_longs()` | `from_longs()` | 既存の i64 配列から作る | +| `from_longs` | `FromLongs()` | `fromLongs()` | `fromLongs()` | `from_longs()` | `from_longs()` | 既存のi64配列から作る | | `get` | `Get()` | `get()` | `get()` | `get()` | `get()` | 添字の値を取り出す | | `into_longs` | — | — | — | — | `into_longs()` | | -| `long_count` | `LongCount()` | `longCount()` | `longCount()` | `long_count()` | `long_count()` | 必要な i64 の個数を求める | +| `long_count` | `LongCount()` | `longCount()` | `longCount()` | `long_count()` | `long_count()` | 必要なi64の個数を求める | | `resize` | `Resize()` | `resize()` | `resize()` | `resize()` | `resize()` | 別のビット幅へ詰め直した新しい記憶域を返す | | `set` | `Set()` | `set()` | `set()` | `set()` | `set()` | 添字の値を書き換える | | `to_longs` | `ToLongs()` | `toLongs()` | `toLongs()` | `to_longs()` | `to_longs()` | | -| `values_per_long` | `ValuesPerLong` | `valuesPerLong()` | `valuesPerLong()` | `values_per_long` | `values_per_long()` | 1 つの i64 に入るエントリ数 | +| `values_per_long` | `ValuesPerLong` | `valuesPerLong()` | `valuesPerLong()` | `values_per_long` | `values_per_long()` | 1つのi64に入るエントリ数 | -この表は実装から生成している。手で直さず、 -`python3 spec/tools/check_docs_sync.py --write` で更新すること。 +この表は実装から生成している。手で直さず、`python3 spec/tools/check_docs_sync.py --write`で更新すること。 diff --git a/docs/contributing/porting-guide.md b/docs/contributing/porting-guide.md index bfe6d44..3b7a1ba 100644 --- a/docs/contributing/porting-guide.md +++ b/docs/contributing/porting-guide.md @@ -1,25 +1,23 @@ # 新しい言語を追加する -本ライブラリは「1人が全言語へ同一の API を提供する」ことを価値としている。 -新しい言語を足すときも、その性質を壊さない手順で進める。 +本ライブラリは「1人が全言語へ同一のAPIを提供する」ことを価値としている。 +新しい言語を足すときも、その性質を損なわない手順で進める。 --- ## 0. 前提 -`docs/spec/` が唯一の正である。 +`docs/spec/`が唯一の正である。 既存の言語実装を写経するのではなく、仕様書を実装する。 -既存実装は「仕様書の解釈が合っているか」を照らし合わせる参考にとどめる。 +既存実装は「仕様書の解釈が合っているか」を照らし合わせるときの参考にとどめる。 -C# を基準実装としているので、迷ったら -[`csharp/src/SpringNBTLibrary/`](../../csharp/src/SpringNBTLibrary/) を見る -([adr/0002](../adr/0002-idiomatic-naming.md))。 +C#を基準実装としているので、迷ったら[`csharp/src/SpringNBTLibrary/`](../../csharp/src/SpringNBTLibrary/)を見る([adr/0002](../adr/0002-idiomatic-naming.md))。 --- ## 1. 進める順番 -レイヤ1 → レイヤ2 → レイヤ3 の順に、各レイヤを完成させてから次へ進む。 +レイヤ1 → レイヤ2 → レイヤ3の順に、各レイヤを完成させてから次へ進む。 | 順 | レイヤ | 仕様 | |---:|---|---| @@ -33,8 +31,7 @@ C# を基準実装としているので、迷ったら ## 2. 命名 -[命名変換規則](../spec/00-conventions.md#3-命名変換規則)に従い、 -その言語で自然な綴りにする。他言語の綴りをそのまま持ち込まない。 +[命名変換規則](../spec/00-conventions.md#3-命名変換規則)に従い、その言語で自然な綴りにする。他言語の綴りをそのまま持ち込まない。 ``` 論理名 read_file @@ -47,25 +44,24 @@ C# を基準実装としているので、迷ったら ## 3. 適合性検証ツールを実装する -これが移植で最初に効く成果物である。 -次のインターフェースを持つ CLI を用意する。 +これが移植で最初に役立つ成果物である。 +次のインターフェースを持つCLIを用意する。 ``` - decode <入力パス> <出力JSONパス> - encode <入力パス> <出力バイナリパス> - snbt <入力パス> <出力SNBTパス> + decode <入力パス> <出力JSONパス> [--format network] + encode <入力パス> <出力バイナリパス> [--format network] + snbt <入力パス> <出力SNBTパス> [--format network] + nbt-list <入力パス> <出力テキストパス> [--format network] region-list <入力.mcaパス> <出力テキストパス> - region-rewrite <入力.mcaパス> <出力ディレクトリ> + region-rewrite <入力.mcaパス> <出力.mcaパス> chunk-report <入力チャンクnbtパス> <出力テキストパス> chunk-edit <入力チャンクnbtパス> <出力nbtパス> version ``` -`spec/tools/run_conformance.py` の `LANGUAGES` に起動方法を足すと、 -既存言語との突き合わせが自動で始まる。 +`spec/tools/run_conformance.py`の`build_runners()`に、その言語のビルド方法と起動方法を持つ`Runner`を足し、`LANGUAGE_ORDER`に言語名を足すと、既存言語との比較が自動で始まる。 -実装が無い言語は自動的に検証対象から外れるので、 -1 コマンドずつ増やしながら進められる。 +実装が無い言語は自動的に検証対象から外れるので、1コマンドずつ増やしながら進められる。 --- @@ -75,56 +71,56 @@ C# を基準実装としているので、迷ったら | 箇所 | 内容 | |---|---| -| MUTF-8 | `U+0000` は `C0 80`。BMP 外は 4 バイトではなくサロゲートペア 2 つ(3+3 バイト) | -| 孤立サロゲート | UTF-8 に写せない文字列をどう保持するか。Rust は 2 形態の列挙にした([spec/10 2.3](../spec/10-nbt-binary.md)) | +| MUTF-8 | `U+0000`は`C0 80`。BMP外は4バイトではなくサロゲートペア2つ(3+3バイト) | +| 孤立サロゲート | UTF-8に写せない文字列をどう保持するか。Rustは2形態の列挙にした([spec/10 2.3](../spec/10-nbt-binary.md)) | | 小数の表記 | 言語の既定書式は使えない。[正準な10進表記](../spec/11-snbt.md#51-浮動小数点の正準10進表記)を実装する | -| `-0.0` | 符号を落とす言語がある(JavaScript の `toExponential` など) | -| `i64` | 64bit 整数を持たない言語では専用の型が要る([adr/0007](../adr/0007-typescript-bigint.md)) | -| 論理右シフト | `BitStorage` では符号なしとして扱う。算術シフトだと上位ビットが埋まる | -| 再帰の深さ | 深さ 512 を安全に扱えるか確かめる([spec/00 5.1](../spec/00-conventions.md)) | -| キーの並び | `Compound` は挿入順を保持する。ハッシュマップで実装すると書き戻しが一致しない | -| `data` と `palette` の順 | 実データは `data` が先([spec/31 4.1](../spec/31-paletted-container.md)) | +| `-0.0` | 符号を落とす言語がある(JavaScriptの`toExponential`など) | +| `i64` | 64bit整数を持たない言語では専用の型が要る([adr/0007](../adr/0007-typescript-bigint.md)) | +| 論理右シフト | `BitStorage`では符号なしとして扱う。算術シフトだと上位ビットが埋まる | +| 再帰の深さ | 深さ512を安全に扱えるか確かめる([spec/00 5.1](../spec/00-conventions.md)) | +| キーの並び | `Compound`は挿入順を保持する。ハッシュマップで実装すると書き戻しが一致しない | +| `data`と`palette`の順 | 実データは`data`が先([spec/31 4.1](../spec/31-paletted-container.md)) | --- ## 5. 単体テストを書く -適合性検証はバイト列の一致を見るが、API の振る舞いまでは見ない。 +適合性検証はバイト列の一致を見るが、APIの振る舞いまでは見ない。 既存言語のテストと同じ検証項目を用意する。 +テストファイルはレイヤごとに分ける。名前の付け方はその言語の慣習に合わせる(次はRustの例)。 ``` -tests/nbt NBT レイヤ -tests/anvil Anvil レイヤ -tests/world World レイヤ +rust/tests/nbt.rs NBT レイヤ +rust/tests/anvil.rs Anvil レイヤ +rust/tests/world.rs World レイヤ ``` -各言語のテストファイルを見比べれば、項目は対応が付くようにしてある。 +各言語のテストファイルは、見比べれば項目の対応が付くようにしてある。 --- ## 6. ドキュメントに載せる -1. `spec/tools/extract_api.py` に抽出器を足す - (その言語のソースから公開型・公開メンバを拾う) -2. `LANGUAGE_ORDER` と `LANGUAGE_LABELS` に言語を足す -3. `python3 spec/tools/check_docs_sync.py --write` で - [API 対応表](../api/overview.md)を再生成する -4. `docs/features.md` に列を足す -5. `docs/getting-started/<言語>.md` を書く +1. `spec/tools/extract_api.py`に抽出器を足し、`EXTRACTORS`に登録する(抽出器は、その言語のソースから公開型・公開メンバを拾う) +2. `spec/tools/extract_api.py`の`LANGUAGE_ORDER`と、`spec/tools/check_docs_sync.py`の`LANGUAGE_LABELS`に言語を足す +3. `python3 spec/tools/check_docs_sync.py --write`で[API対応表](../api/overview.md)を再生成する +4. `docs/features.md`に列を足し、`spec/tools/check_docs_sync.py`の`FEATURE_COLUMNS`にも同じ位置に言語を足す +5. `docs/getting-started/<言語>.md`を書く -5 が済むまで「対応した」とは言わない。 +5が済むまで「対応した」とは言わない。 実装があってもドキュメントから辿れなければ、利用者には存在しないのと同じである。 --- ## 7. 実ワールドで確かめる -最後に、自分の Minecraft ワールドで検証する。 -合成のテストベクタでは実データの網羅性に届かない。 +最後に、自分のMinecraftワールドで検証する。 +合成のテストベクタでは、実データほど網羅できない。 ```bash python3 spec/tools/scan_world.py "<ワールドのパス>" ``` -このツールは Python 実装を使うので、 -新しい言語では `run-conformance.sh` の実ワールドモードで確かめる。 +このツールが使うのはPython実装だけである。 +`run-conformance.sh`も実ワールドを扱わない(受け付ける引数は`--only`と`--generate-expect`だけ)。 +新しい言語では、実ワールドから取り出したファイルを3章の検証ツールに通し、既存言語の出力と比べて確かめる([spec/90の検証実績](../spec/90-conformance.md#検証実績))。 diff --git a/docs/contributing/release.md b/docs/contributing/release.md index a4e504e..9f78171 100644 --- a/docs/contributing/release.md +++ b/docs/contributing/release.md @@ -4,52 +4,45 @@ ## 1. バージョン番号 -`x.y.z` の各桁は次を表す。 +`x.y.z`の各桁は次を表す。 | 桁 | 上がるとき | 例 | |---|---|---| | `x` | ワールドの保存形式が変わったとき | 次元フォルダの構成が変わった | -| `y` | 機能を足した、または外したとき | LZ4 を読めるようにした | +| `y` | 機能を足した、または外したとき | LZ4を読めるようにした | | `z` | 不具合を直したとき | パレットの並び順を直した | -全言語で同じ番号を使う。ある言語だけ 1.1.0 ということはしない。 +全言語で同じ番号を使う。ある言語だけ1.1.0ということはしない。 -**Minecraft が更新されても、保存形式が変わらなければ `x` は上がらない。** -形式が同じなら新しいバージョンのワールドもそのまま扱えるので、 -ライブラリ側で対応することが無い([adr/0003](../adr/0003-version-policy.md))。 +**Minecraftが更新されても、保存形式が変わらなければ`x`は上がらない。** +形式が同じなら新しいバージョンのワールドもそのまま扱えるので、ライブラリ側で対応することが無い([adr/0003](../adr/0003-version-policy.md))。 -`x` が上がるのは、扱える形式の下限 -(`MIN_SUPPORTED_DATA_VERSION`)を引き上げるときである。 +`x`が上がるのは、扱える形式の下限(`MIN_SUPPORTED_DATA_VERSION`)を引き上げるときである。 それまで使えていたワールドが使えなくなるため、いちばん上の桁で知らせる。 -`x` を上げるときは `y` と `z` を 0 に戻す。 +`x`を上げるときは`y`と`z`を0に戻す。 --- -## 2. Minecraft の新バージョンが出たら +## 2. Minecraftの新バージョンが出たら **まず、形式が変わったかどうかを確かめる。** 変わっていなければ何もしなくてよい。 -1. 実ワールドを用意する。 新バージョンで新規ワールドを作り、 - ネザーとエンドにも行って各次元を生成させる +1. 実ワールドを用意する。新バージョンで新規ワールドを作り、ネザーとエンドにも行って各次元を生成させる 2. 走査する。 ```bash python3 spec/tools/scan_world.py "<新しいワールド>" --verbose ``` - ルート直下キー・セクションキー・`Status` の出現数が出るので、 - [spec/30](../spec/30-chunk-format.md) / [spec/40](../spec/40-world-layout.md) - の記述と突き合わせる -3. 差が無ければここで終わり。 そのバージョンのワールドはすでに扱える。 - 確認した記録として `TARGET_DATA_VERSION` を上げてもよいが、必須ではない + ルート直下キー・セクションキー・`Status`の出現数が出るので、[spec/30](../spec/30-chunk-format.md) / [spec/40](../spec/40-world-layout.md)の記述と照らし合わせる +3. 差が無ければここで終わり。そのバージョンのワールドはすでに扱える。確認した記録として`TARGET_DATA_VERSION`を上げてもよいが、必須ではない 形式が変わっていた場合だけ、次へ進む。 -4. 仕様書を直す。 推測ではなく実データを根拠に書き直す +4. 仕様書を直す。推測ではなく実データを根拠に書き直す 5. 実装を新しい形式へ合わせる -6. `MIN_SUPPORTED_DATA_VERSION` を新しい形式が入ったバージョンへ上げ、 - `TARGET_DATA_VERSION` も更新する。 +6. `MIN_SUPPORTED_DATA_VERSION`を新しい形式が入ったバージョンへ上げ、`TARGET_DATA_VERSION`も更新する。 ``` csharp/src/SpringNBTLibrary/SpringNbt.cs @@ -64,7 +57,7 @@ python3 spec/tools/build_testdata.py ``` 8. 全言語のテストと適合性検証を通す -9. バージョン番号の `x` を上げる +9. バージョン番号の`x`を上げる --- @@ -80,7 +73,7 @@ source spec/tools/env.sh (cd python && python -m pytest tests) (cd rust && cargo test) -# 全言語の挙動一致(このライブラリの核) +# 全言語の挙動一致(このライブラリでいちばん肝心な点) ./spec/run-conformance.sh # ドキュメントと実装の一致 @@ -95,8 +88,7 @@ python3 spec/tools/scan_world.py "<ワールドのパス>" ``` すべて通ることがリリースの条件である。 -1 つでも落ちている状態で出すと、「全言語で挙動が同じ」という -本ライブラリ唯一の売りが嘘になる。 +1つでも落ちている状態で出すと、「全言語で挙動が同じ」という本ライブラリ唯一の売りが嘘になる。 --- @@ -110,16 +102,26 @@ python3 spec/tools/scan_world.py "<ワールドのパス>" | Python | `python/pyproject.toml` | | Rust | `rust/Cargo.toml` | -あわせて `CHANGELOG.md` に変更点を書く。 +適合性検証ツールの`version`コマンドの出力にも版が直書きされている。 +言語間の比較には使わないが、表示がずれないよう合わせて上げる。 -**これはリリースの必須条件である。** 本文はここから組み立てるので、 -節が無いとワークフローが落ちて公開まで進まない。 +| 言語 | ファイル | +|---|---| +| C# | `csharp/tests/SpringNBTLibrary.Conformance/Program.cs` | +| Java | `java/src/test/java/io/github/scriptarts/springnbt/conformance/Conformance.java` | +| TypeScript | `typescript/src/conformance.ts` | +| Python | `python/src/spring_nbt_library/conformance.py` | +| Rust | `rust/examples/conformance.rs` | + +あわせて`CHANGELOG.md`に変更点を書く。 + +**これはリリースの必須条件である。** リリースの本文はここから組み立てるので、節が無いとワークフローが落ちて公開まで進まない。 --- ## 5. 公開 -GitHub Releases へビルド済みの成果物を置く。 +GitHub Releasesへビルド済みの成果物を置く。 利用者はそれを落として自分のプロジェクトへ組み込む。 ### 5.1 タグを打つと自動で公開される @@ -129,57 +131,49 @@ git tag -a v1.0.0 -m "v1.0.0" git push origin v1.0.0 ``` -`v` で始まるタグを push すると -[`release` ワークフロー](../../.github/workflows/release.yml)が動き、次を行う。 +`v`で始まるタグをpushすると[`release`ワークフロー](../../.github/workflows/release.yml)が動き、次を行う。 -1. 全対応言語のテストと[適合性検証](../spec/90-conformance.md)を通す - (落ちたら公開しない。壊れたものを配らないため) +1. 全対応言語のテストと[適合性検証](../spec/90-conformance.md)を通す(落ちたら公開しない。正しく動かないものを配らないため) 2. 言語ごとの成果物を組み立てる -3. `SHA256SUMS.txt` を作る -4. Releases を作って成果物を添付する +3. `SHA256SUMS.txt`を作る +4. Releasesを作って成果物を添付する ### 5.2 成果物 | 言語 | ファイル | 中身 | |---|---|---| -| C# | `SpringNBTLibrary-<版>-dotnet8.zip` | `.dll` と `.xml`(補完用)、LICENSE、README | +| C# | `SpringNBTLibrary.<版>.nupkg` | NuGetパッケージ(ローカルのソースとして使う) | +| C# | `SpringNBTLibrary-<版>-dotnet8.zip` | `.dll`と`.xml`(補完用)、LICENSE、README | | Java | `spring-nbt-library-<版>.jar` | | -| TypeScript | `spring-nbt-library-<版>.tgz` | ビルド済み JS と型定義 | -| Python | `spring_nbt_library-<版>-py3-none-any.whl` / `.tar.gz` | wheel と sdist | -| Rust | `spring-nbt-library-<版>.crate` | ソースの tar.gz | +| TypeScript | `spring-nbt-library-<版>.tgz` | ビルド済みJSと型定義 | +| Python | `spring_nbt_library-<版>-py3-none-any.whl` / `.tar.gz` | wheelとsdist | +| Rust | `spring-nbt-library-<版>.crate` | ソースのtar.gz | -Rust は git 参照が主な使い方になる。 Cargo は git リポジトリを直接 -依存にできるので、ファイルを落とす必要がない。`.crate` は -ネットワークに繋がらない環境向けの控えである。 +Rustはgit参照が主な使い方になる。Cargoはgitリポジトリを直接依存にできるので、ファイルを落とす必要がない。`.crate`はネットワークに繋がらない環境向けの控えである。 ### 5.3 タグを打つ前に試す -`release` ワークフローは手動でも起動できる。 -Actions の画面から `Run workflow` を選ぶと、 -成果物の組み立てまでを行い、Releases は作らずに artifacts として残す。 +`release`ワークフローは手動でも起動できる。 +Actionsの画面から`Run workflow`を選ぶと、成果物の組み立てまでを行い、Releasesは作らずにartifactsとして残す。 中身を確かめてからタグを打てる。 ### 5.4 リリースの本文 -本文は 2 つを継いで作る。 +本文は2つを継いで作る。 | 部分 | 出どころ | |---|---| -| 変更点 | `CHANGELOG.md` の `## [<版>]` の節 | +| 変更点 | `CHANGELOG.md`の`## [<版>]`の節 | | 案内文 | [`.github/release-common.md`](../../.github/release-common.md) | -組み立てるのは -[`spec/tools/release_notes.py`](../../spec/tools/release_notes.py) で、 -リリースのワークフローが呼ぶ。手元でも確かめられる。 +組み立てるのは[`spec/tools/release_notes.py`](../../spec/tools/release_notes.py)で、リリースのワークフローが呼ぶ。手元でも確かめられる。 ```bash python3 spec/tools/release_notes.py 1.1.0 ``` -変更点の実体を `CHANGELOG.md` 側に置いているのは、同じ内容を 2 か所へ -書かせないためである。**節が無ければ組み立てが失敗する**ので、 -変更点を書き忘れたまま公開することがない。 +変更点の実体を`CHANGELOG.md`側に置いているのは、同じ内容を2か所へ書かせないためである。**節が無ければ組み立てが失敗する**ので、変更点を書き忘れたまま公開することがない。 リポジトリ内への相対リンクは、そのタグを指す絶対URLへ書き換える。 リリースのページからは相対リンクを辿れないため。 diff --git a/docs/features.md b/docs/features.md index d4f383d..5683571 100644 --- a/docs/features.md +++ b/docs/features.md @@ -11,20 +11,18 @@ | ⏳ | 実装中 | | ❌ | 未対応 | -対応しているのは C# / Java / TypeScript / Python / Rust で、今後も増やします。 -対象は **26.1 で導入されたワールド形式**(DataVersion 4786 以降)です。 -動作は Java版 26.2 / DataVersion 4903 で確かめています。 +対応しているのはC# / Java / TypeScript / Python / Rustで、今後も増やします。 +対象は**26.1で導入されたワールド形式**(DataVersion 4786以降)です。 +動作はJava版26.2 / DataVersion 4903で確かめています。 形式が同じであれば、これより新しいバージョンのワールドもそのまま読み書きできます。 過去バージョンのワールド改変には対応していません(→ [07 バージョンポリシー](guide/07-version-policy.md))。 -この表は CI の `docs-sync` ジョブが各言語の公開APIと突き合わせていて、 -表と実装が食い違うとビルドが落ちます。 +この表は、CIの`lint`ワークフローが`spec/tools/check_docs_sync.py`で各言語の公開APIと照らし合わせています。 +表と実装が食い違うとCIが落ちます。 -実データでの検証: Java版 26.2 の実ワールドを走査し、`.dat` 23個 + チャンク 3,717個 の -すべてで読み込みとバイト一致の書き戻しが通りました。 -リージョンファイル 12 個は、開いて無変更で書き戻すと原本とバイト単位で一致します。 -代表ファイルは対応言語すべてに通していて、正規化JSON・SNBT・チャンク一覧・詰め直したバイト列が -全言語で一致することも確かめました(→ [90 適合性 2.4](spec/90-conformance.md#24-実ワールド走査))。 +実データでの検証: Java版26.2の実ワールドを走査し、`.dat` 23個 + チャンク3,717個のすべてで読み込みとバイト一致の書き戻しが通りました。 +リージョンファイル12個は、開いて無変更で書き戻すと原本とバイト単位で一致します。 +代表ファイルは対応言語すべてに通し、正規化JSON・SNBT・チャンク一覧・詰め直したバイト列が全言語で一致することも確かめました(→ [90 適合性 2.4](spec/90-conformance.md#24-実ワールド走査))。 --- @@ -34,23 +32,23 @@ |---|:--:|:--:|:--:|:--:|:--:|---|---| | 全13タグの読み込み | ✅ | ✅ | ✅ | ✅ | ✅ | [10](spec/10-nbt-binary.md#1-タグ型) | | | 全13タグの書き込み | ✅ | ✅ | ✅ | ✅ | ✅ | [10](spec/10-nbt-binary.md#5-正準書き出し-canonical-writer) | 出力は一意(正準ライタ) | -| MUTF-8 文字列(U+0000・補助文字・孤立サロゲート) | ✅ | ✅ | ✅ | ✅ | ✅ | [10](spec/10-nbt-binary.md#2-文字列-mutf-8) | | -| 圧縮 Gzip / Zlib / 無圧縮 | ✅ | ✅ | ✅ | ✅ | ✅ | [10](spec/10-nbt-binary.md#4-圧縮) | | +| MUTF-8文字列(U+0000・補助文字・孤立サロゲート) | ✅ | ✅ | ✅ | ✅ | ✅ | [10](spec/10-nbt-binary.md#2-文字列-mutf-8) | | +| 圧縮Gzip / Zlib / 無圧縮 | ✅ | ✅ | ✅ | ✅ | ✅ | [10](spec/10-nbt-binary.md#4-圧縮) | | | 圧縮方式の自動判定 | ✅ | ✅ | ✅ | ✅ | ✅ | [10](spec/10-nbt-binary.md#4-圧縮) | 読み込みの既定 | | Java形式(名前付きルート) | ✅ | ✅ | ✅ | ✅ | ✅ | [10](spec/10-nbt-binary.md#3-ファイル形式とルートタグ) | | -| Network形式(無名ルート 1.20.2+) | ✅ | ✅ | ✅ | ✅ | ✅ | [10](spec/10-nbt-binary.md#3-ファイル形式とルートタグ) | | -| ネスト深さ上限による保護 | ✅ | ✅ | ✅ | ✅ | ✅ | [00](spec/00-conventions.md#5-安全上限既定値) | 既定 512 | +| Network形式(無名ルート1.20.2+) | ✅ | ✅ | ✅ | ✅ | ✅ | [10](spec/10-nbt-binary.md#3-ファイル形式とルートタグ) | | +| ネスト深さ上限による保護 | ✅ | ✅ | ✅ | ✅ | ✅ | [00](spec/00-conventions.md#5-安全上限既定値) | 既定は512 | | 宣言長の先行検証 | ✅ | ✅ | ✅ | ✅ | ✅ | [10](spec/10-nbt-binary.md#12-長さフィールドの検証) | 巨大長宣言によるメモリ枯渇を防ぐ | | 展開後サイズの上限 | ✅ | ✅ | ✅ | ✅ | ✅ | [00](spec/00-conventions.md#5-安全上限既定値) | 既定は無制限 | -| 挿入順を保持する Compound | ✅ | ✅ | ✅ | ✅ | ✅ | [10](spec/10-nbt-binary.md#71-nbtcompound) | ラウンドトリップの前提 | +| 挿入順を保持するCompound | ✅ | ✅ | ✅ | ✅ | ✅ | [10](spec/10-nbt-binary.md#71-nbtcompound) | ラウンドトリップの前提 | | 型付き取得子(キー欠落と型不一致の区別) | ✅ | ✅ | ✅ | ✅ | ✅ | [10](spec/10-nbt-binary.md#71-nbtcompound) | `get_*` / `opt_*` | -| 型付き設定子 | ✅ | ✅ | ✅ | ✅ | ✅ | [10](spec/10-nbt-binary.md#71-nbtcompound) | `set_int(key, 42)` のように値を直に渡せる | -| 位置を指定した読み込み | ✅ | ✅ | ✅ | ✅ | ✅ | [10](spec/10-nbt-binary.md#31-連なった-nbt-を読む) | 連なった NBT を順に読み進める | +| 型付き設定子 | ✅ | ✅ | ✅ | ✅ | ✅ | [10](spec/10-nbt-binary.md#71-nbtcompound) | `set_int(key, 42)`のように値を直に渡せる | +| 位置を指定した読み込み | ✅ | ✅ | ✅ | ✅ | ✅ | [10](spec/10-nbt-binary.md#31-連なったnbtを読む) | 連なったNBTを順に読み進める | | SNBT パース | ✅ | ✅ | ✅ | ✅ | ✅ | [11](spec/11-snbt.md) | 1.21.5+ 構文 | -| SNBT 出力(1行 / 整形) | ✅ | ✅ | ✅ | ✅ | ✅ | [11](spec/11-snbt.md#5-出力) | | +| SNBT出力(1行 / 整形) | ✅ | ✅ | ✅ | ✅ | ✅ | [11](spec/11-snbt.md#5-出力) | | | 浮動小数点の正準10進表記 | ✅ | ✅ | ✅ | ✅ | ✅ | [11](spec/11-snbt.md#51-浮動小数点の正準10進表記) | 言語標準の書式に依存しない | -| SNBT の異種リスト | ❌ | ❌ | ❌ | ❌ | ❌ | [11](spec/11-snbt.md#4-リストの異種要素) | バイナリNBTへ写せないため受理しない([adr/0006](adr/0006-snbt-scope.md)) | -| SNBT の `\N{文字名}` エスケープ | ❌ | ❌ | ❌ | ❌ | ❌ | [11](spec/11-snbt.md#3-文字列とエスケープ) | 実装間で名前表が揃わない | +| SNBTの異種リスト | ❌ | ❌ | ❌ | ❌ | ❌ | [11](spec/11-snbt.md#4-リストの異種要素) | バイナリNBTへ写せないため受理しない([adr/0006](adr/0006-snbt-scope.md)) | +| SNBTの`\N{文字名}`エスケープ | ❌ | ❌ | ❌ | ❌ | ❌ | [11](spec/11-snbt.md#3-文字列とエスケープ) | 実装間で名前表が揃わない | ## レイヤ2: Anvil (.mca) @@ -62,40 +60,40 @@ | 解放セクタの再利用 | ✅ | ✅ | ✅ | ✅ | ✅ | [20](spec/20-anvil-region.md#4-セクタ確保) | 先頭から空きを探す | | チャンクの削除 | ✅ | ✅ | ✅ | ✅ | ✅ | [20](spec/20-anvil-region.md#4-セクタ確保) | | | タイムスタンプの読み書き | ✅ | ✅ | ✅ | ✅ | ✅ | [20](spec/20-anvil-region.md#22-タイムスタンプテーブル) | | -| 圧縮ID 1 (GZip) / 2 (Zlib) / 3 (無圧縮) | ✅ | ✅ | ✅ | ✅ | ✅ | [20](spec/20-anvil-region.md#31-圧縮方式id) | 書き込み既定は 2 | -| 圧縮ID 4 (LZ4) | 🔶 | 🔶 | 🔶 | 🔶 | 🔶 | [20](spec/20-anvil-region.md#31-圧縮方式id) | 読み込みのみ。書き込むと Zlib になる | +| 圧縮ID 1 (GZip) / 2 (Zlib) / 3 (無圧縮) | ✅ | ✅ | ✅ | ✅ | ✅ | [20](spec/20-anvil-region.md#31-圧縮方式id) | 書き込み既定は2 | +| 圧縮ID 4 (LZ4) | 🔶 | 🔶 | 🔶 | 🔶 | 🔶 | [20](spec/20-anvil-region.md#31-圧縮方式id) | 読み込みのみ。書き戻すとZlibになる(LZ4を指定した書き込みは`UNSUPPORTED_FEATURE`) | | 圧縮ID 127 (カスタム) | ❌ | ❌ | ❌ | ❌ | ❌ | [20](spec/20-anvil-region.md#31-圧縮方式id) | 方式が実装依存。生バイトAPIでのみ取得可 | -| 外部ファイル `.mcc` の読み込み | ✅ | ✅ | ✅ | ✅ | ✅ | [20](spec/20-anvil-region.md#32-外部ファイルへの退避-mcc) | | -| 外部ファイル `.mcc` への自動退避と復帰 | ✅ | ✅ | ✅ | ✅ | ✅ | [20](spec/20-anvil-region.md#32-外部ファイルへの退避-mcc) | 1MiB超で退避、縮めば内部へ戻す | +| 外部ファイル`.mcc`の読み込み | ✅ | ✅ | ✅ | ✅ | ✅ | [20](spec/20-anvil-region.md#32-外部ファイルへの退避-mcc) | | +| 外部ファイル`.mcc`への自動退避と復帰 | ✅ | ✅ | ✅ | ✅ | ✅ | [20](spec/20-anvil-region.md#32-外部ファイルへの退避-mcc) | 255セクタ(約1MiB)を超えたら退避、縮めば内部へ戻す | | 生バイトでのチャンク入出力 | ✅ | ✅ | ✅ | ✅ | ✅ | [20](spec/20-anvil-region.md#5-論理api) | 未対応圧縮方式でも取り出せる | -| 断片化解消 `optimize()` | ✅ | ✅ | ✅ | ✅ | ✅ | [20](spec/20-anvil-region.md#4-セクタ確保) | 添字順に詰め直す | +| 断片化解消`optimize()` | ✅ | ✅ | ✅ | ✅ | ✅ | [20](spec/20-anvil-region.md#4-セクタ確保) | 添字順に詰め直す | | セクタ重複・不正オフセットの検出 | ✅ | ✅ | ✅ | ✅ | ✅ | [20](spec/20-anvil-region.md#21-ロケーションテーブル) | 開いた時点で弾く | | ファイル長のセクタ整列検査 | ✅ | ✅ | ✅ | ✅ | ✅ | [20](spec/20-anvil-region.md#2-ファイル構造) | | | `region/` `entities/` `poi/` の横断アクセス | ✅ | ✅ | ✅ | ✅ | ✅ | [20](spec/20-anvil-region.md#5-論理api) | `RegionFolder` | -| 開いたリージョンの上限管理 | ✅ | ✅ | ✅ | ✅ | ✅ | [20](spec/20-anvil-region.md#51-開いたリージョンの上限) | 既定 8 件。溢れたら書き出して閉じる | +| 開いたリージョンの上限管理 | ✅ | ✅ | ✅ | ✅ | ✅ | [20](spec/20-anvil-region.md#51-開いたリージョンの上限) | 既定は8件。溢れたら書き出して閉じる | ## レイヤ3: World / Block | 機能 | C# | Java | TS | Py | Rust | 仕様 | 備考 | |---|:--:|:--:|:--:|:--:|:--:|---|---| | `level.dat` の読み込み | ✅ | ✅ | ✅ | ✅ | ✅ | [40](spec/40-world-layout.md#2-leveldat) | | -| `level.dat` の安全な書き込み | ✅ | ✅ | ✅ | ✅ | ✅ | [40](spec/40-world-layout.md#22-書き込み時の安全策) | 一時ファイル経由 + `level.dat_old` | -| `session.lock` の確認 | ✅ | ✅ | ❌ | 🔶 | ❌ | [40](spec/40-world-layout.md#31-言語による差異) | 起動中ワールドへの書き込みを防ぐ。排他ロックを扱える言語のみ。Py は POSIX のみ([adr/0008](adr/0008-session-lock.md)) | +| `level.dat`の安全な書き込み | ✅ | ✅ | ✅ | ✅ | ✅ | [40](spec/40-world-layout.md#22-書き込み時の安全策) | 一時ファイル経由 + `level.dat_old` | +| `session.lock`の確認 | ✅ | ✅ | ❌ | 🔶 | ❌ | [40](spec/40-world-layout.md#31-言語による差異) | 起動中ワールドへの書き込みを防ぐ。排他ロックを扱える言語のみ。PyはPOSIXのみ([adr/0008](adr/0008-session-lock.md)) | | 標準3次元 + カスタム次元の解決 | ✅ | ✅ | ✅ | ✅ | ✅ | [40](spec/40-world-layout.md#1-ディレクトリ構成) | | | チャンクの解釈(未知キーの保持つき) | ✅ | ✅ | ✅ | ✅ | ✅ | [30](spec/30-chunk-format.md#1-ルート要素) | 読んだ要素をすべて残す | | ブロックの取得・設定 | ✅ | ✅ | ✅ | ✅ | ✅ | [30](spec/30-chunk-format.md#22-座標とエントリ添字) | 絶対座標/チャンク相対の両方 | -| 置き換え時の付随データの掃除 | ✅ | ✅ | ✅ | ✅ | ✅ | [30](spec/30-chunk-format.md#24-ブロックを置き換えたときの掃除) | `block_entities` / `block_ticks` / `fluid_ticks` から同じ座標の要素を取り除く | +| 置き換え時の付随データの掃除 | ✅ | ✅ | ✅ | ✅ | ✅ | [30](spec/30-chunk-format.md#24-ブロックを置き換えたときの掃除) | `block_entities` / `block_ticks` / `fluid_ticks`から同じ座標の要素を取り除く | | バイオームの取得・設定(4×4×4) | ✅ | ✅ | ✅ | ✅ | ✅ | [30](spec/30-chunk-format.md#22-座標とエントリ添字) | | | `BlockState` の文字列表現 | ✅ | ✅ | ✅ | ✅ | ✅ | [30](spec/30-chunk-format.md#21-ブロック状態のパレット要素) | `minecraft:oak_stairs[facing=north]` | | ブロック座標と範囲の型 | ✅ | ✅ | ✅ | ✅ | ✅ | [30](spec/30-chunk-format.md#5-座標の型) | `BlockPos` / `Cuboid`。範囲内の走査つき | | チャンクの変更フラグ | ✅ | ✅ | ✅ | ✅ | ✅ | [30](spec/30-chunk-format.md#5-座標の型) | `is_modified`。書き戻す対象を決める | | パレット自動拡張とビット幅の再計算 | ✅ | ✅ | ✅ | ✅ | ✅ | [31](spec/31-paletted-container.md#4-書き込み時のビット幅再計算) | | -| 未使用パレットの掃除 `compact()` | ✅ | ✅ | ✅ | ✅ | ✅ | [31](spec/31-paletted-container.md#4-書き込み時のビット幅再計算) | 明示呼び出し時のみ | -| 非正準な BitStorage の救済読み | ✅ | ✅ | ✅ | ✅ | ✅ | [31](spec/31-paletted-container.md#3-読み込み時の検証) | `lenient_bit_storage` | -| `Heightmaps` の保持 | ✅ | ✅ | ✅ | ✅ | ✅ | [30](spec/30-chunk-format.md#3-heightmaps) | 値をそのまま残す | -| `Heightmaps` の再計算 | ❌ | ❌ | ❌ | ❌ | ❌ | [30](spec/30-chunk-format.md#3-heightmaps) | [adr/0004](adr/0004-defer-heightmap-recalc.md) | -| 光源の再計算 | ❌ | ❌ | ❌ | ❌ | ❌ | [30](spec/30-chunk-format.md#3-heightmaps) | 同上。`invalidate_lighting()` で無効化のみ提供 | -| `entities` / `poi` の型付きAPI | ❌ | ❌ | ❌ | ❌ | ❌ | [30](spec/30-chunk-format.md#4-entities--poi) | 生NBTとしてなら読み書き可 | +| 未使用パレットの掃除`compact()` | ✅ | ✅ | ✅ | ✅ | ✅ | [31](spec/31-paletted-container.md#4-書き込み時のビット幅再計算) | 明示呼び出し時のみ | +| 非正準なBitStorageの救済読み | ✅ | ✅ | ✅ | ✅ | ✅ | [31](spec/31-paletted-container.md#3-読み込み時の検証) | `lenient_bit_storage` | +| `Heightmaps`の保持 | ✅ | ✅ | ✅ | ✅ | ✅ | [30](spec/30-chunk-format.md#3-heightmaps) | 値をそのまま残す | +| `Heightmaps`の再計算 | ❌ | ❌ | ❌ | ❌ | ❌ | [30](spec/30-chunk-format.md#3-heightmaps) | [adr/0004](adr/0004-defer-heightmap-recalc.md) | +| 光源の再計算 | ❌ | ❌ | ❌ | ❌ | ❌ | [30](spec/30-chunk-format.md#3-heightmaps) | 同上。`invalidate_lighting()`で無効化のみ提供 | +| `entities` / `poi`の型付きAPI | ❌ | ❌ | ❌ | ❌ | ❌ | [30](spec/30-chunk-format.md#4-entities--poi) | 生NBTとしてなら読み書き可 | | チャンクの新規生成 | ❌ | ❌ | ❌ | ❌ | ❌ | — | ワールド生成は本ライブラリの範囲外 | | Bedrock版(LevelDB形式) | ❌ | ❌ | ❌ | ❌ | ❌ | — | 対象外 | @@ -103,11 +101,11 @@ | 機能 | C# | Java | TS | Py | Rust | 仕様 | 備考 | |---|:--:|:--:|:--:|:--:|:--:|---|---| -| 共通 `ErrorCode` による分類 | ✅ | ✅ | ✅ | ✅ | ✅ | [00](spec/00-conventions.md#4-エラー分類) | 全言語でコード集合が一致 | +| 共通`ErrorCode`による分類 | ✅ | ✅ | ✅ | ✅ | ✅ | [00](spec/00-conventions.md#4-エラー分類) | 全言語でコード集合が一致 | | 扱えない形式の検出(警告/エラー切替) | ✅ | ✅ | ✅ | ✅ | ✅ | [30](spec/30-chunk-format.md#6-バージョン検査) | | | 適合性検証ツール(CLI) | ✅ | ✅ | ✅ | ✅ | ✅ | [90](spec/90-conformance.md#23-クロス言語一致) | 全言語の出力を相互diff | -| 実ワールド走査ツール | — | — | — | ✅ | — | [90](spec/90-conformance.md#24-実ワールド走査) | `spec/tools/scan_world.py`。検証用のため Python のみ | -| ドキュメント同期検証 | — | — | — | ✅ | — | [adr/0009](adr/0009-static-api-extraction.md) | `spec/tools/check_docs_sync.py`。全言語のソースを静的解析して突き合わせる | +| 実ワールド走査ツール | — | — | — | ✅ | — | [90](spec/90-conformance.md#24-実ワールド走査) | `spec/tools/scan_world.py`。検証用のためPythonのみ | +| ドキュメント同期検証 | — | — | — | ✅ | — | [adr/0009](adr/0009-static-api-extraction.md) | `spec/tools/check_docs_sync.py`。全言語のソースを静的解析して照らし合わせる | --- @@ -118,12 +116,14 @@ | 箇所 | 差異 | 理由 | |---|---|---| -| `TAG_Long` の値型 | TypeScript のみ `bigint` | `number` では 2^53 超の整数を表せない([adr/0007](adr/0007-typescript-bigint.md)) | -| `TAG_String` の値型 | Rust のみ 2 形態の enum | `String` が UTF-8 に限られ、孤立サロゲートを保持できない([spec/10 2.3](spec/10-nbt-binary.md#23-各言語での保持方法)) | -| 整数の範囲検証 | Python / TypeScript は構築時に実行時検査 | 言語に整数幅が無いため、型では守れない | -| 例外 / 戻り値 | Rust のみ `Result` | 他4言語は例外。`ErrorCode` の集合は一致([adr/0005](adr/0005-unified-error-model.md)) | -| `set_block` の引数 | Rust のみ `IntoBlockState` トレイト | 他4言語はオーバーロードで `BlockState` と文字列の両方を受ける | -| Java の検査例外 | 使わない | シグネチャを他言語と揃えるため([adr/0005](adr/0005-unified-error-model.md)) | -| Python の再帰上限 | 読み書き時に一時的に引き上げる | 既定の上限 1000 が仕様の深さ上限 512 に届かないため | -| ストリーム入出力 | TypeScript には無い | Node のストリームは非同期しか無く、同期 API で揃えられないため。ファイルとバイト列の入口は同じ | -| `session.lock` の確認 | C# / Java / Python(POSIX) のみ実施 | 標準ライブラリでファイルの排他ロックを扱えない言語があるため([adr/0008](adr/0008-session-lock.md)) | +| `TAG_Long`の値型 | TypeScriptのみ`bigint` | `number`では2^53超の整数を表せない([adr/0007](adr/0007-typescript-bigint.md)) | +| `TAG_String`の値型 | Rustのみ2形態のenum | `String`がUTF-8に限られ、孤立サロゲートを保持できない([spec/10 2.3](spec/10-nbt-binary.md#23-各言語での保持方法)) | +| 整数の範囲検証 | Python / TypeScriptは構築時に実行時検査 | 言語に整数幅が無いため、型では守れない | +| 例外 / 戻り値 | Rustのみ`Result` | 他4言語は例外。`ErrorCode`の集合は一致([adr/0005](adr/0005-unified-error-model.md)) | +| `set_block`の引数 | Rustのみ`IntoBlockState`トレイト | 他4言語は`BlockState`と文字列の両方を受ける。C# / Javaはオーバーロード、TypeScript / Pythonは合併型の引数で表す | +| リストの範囲外の位置 | 各言語の標準の例外を使う。C# `ArgumentOutOfRangeException` / Java `IndexOutOfBoundsException` / Python `IndexError` / TypeScript `RangeError`。Rustは`get`が`None`、`insert`が`INVALID_ARGUMENT` | リストは各言語の標準のコレクションの約束事に合わせる。失敗したときに要素型は確定させない | +| `TAG_Float`のビットパターン | Python / TypeScriptは読み込んだビットを別に持つ | 値を倍精度で持つため、シグナリングNaNのビットが変わってしまう。値を変えていなければ読んだビットのまま書き戻す | +| Javaの検査例外 | 使わない | シグネチャを他言語と揃えるため([adr/0005](adr/0005-unified-error-model.md)) | +| Pythonの再帰上限 | 読み書き時に一時的に引き上げる | 既定の上限1000が仕様の深さ上限512に届かないため | +| ストリーム入出力 | TypeScriptには無い | Nodeのストリームは非同期しか無く、同期APIで揃えられないため。ファイルとバイト列の入出力は同じ | +| `session.lock`の確認 | C# / Java / Python(POSIX)のみ実施 | 標準ライブラリでファイルの排他ロックを扱えない言語があるため([adr/0008](adr/0008-session-lock.md)) | diff --git a/docs/getting-started/csharp.md b/docs/getting-started/csharp.md index 29228f3..afaef19 100644 --- a/docs/getting-started/csharp.md +++ b/docs/getting-started/csharp.md @@ -1,22 +1,21 @@ # はじめに(C#) -.NET 8 / C# 12 以上が必要です。 +.NET 8 / C# 12以上が必要です。 -C# は[基準実装](../adr/0002-idiomatic-naming.md)です。 +C#は[基準実装](../adr/0002-idiomatic-naming.md)です。 仕様の解釈に迷ったら、まずここを見てください。 ## 導入 -[Releases](https://github.com/ScriptArts/SpringNBTLibrary/releases) に 2 通り置いてあります。 +[Releases](https://github.com/ScriptArts/SpringNBTLibrary/releases)に2通り置いてあります。 どちらでも中身は同じです。 -### `.nupkg` を使う(おすすめ) +### `.nupkg`を使う(おすすめ) -NuGet へは公開していないので、落としたファイルを置いたフォルダを -ソースとして教えます。 +NuGetへは公開していないので、落としたファイルを置いたフォルダをソースとして教えます。 -1. `SpringNBTLibrary.<版>.nupkg` を、プロジェクト直下の `packages/` へ置きます -2. `.csproj` と同じ場所に `nuget.config` を作ります +1. `SpringNBTLibrary.<版>.nupkg`を、プロジェクト直下の`packages/`へ置きます +2. `.csproj`と同じ場所に`nuget.config`を作ります ```xml @@ -33,16 +32,15 @@ NuGet へは公開していないので、落としたファイルを置いた dotnet add package SpringNBTLibrary --version 1.0.0 ``` -ドキュメントの置き場所は NuGet が面倒を見るので、補完に説明が出ます。 -`nuget.config` をプロジェクトに置くのは、この設定をリポジトリで共有できるからです -(`dotnet nuget add source` は利用者ごとの設定を書き換えるので、 -他の人の手元では動きません)。 +ドキュメントの置き場所はNuGetが面倒を見るので、補完に説明が出ます。 +`nuget.config`をプロジェクトに置くのは、この設定をリポジトリで共有できるからです。 +`dotnet nuget add source`は利用者ごとの設定を書き換えるので、他の人の手元では動きません。 -### `.zip` を使う +### `.zip`を使う -dll を直接参照したいときはこちらです。 +dllを直接参照したいときはこちらです。 -1. `SpringNBTLibrary-<版>-dotnet8.zip` を展開します。中身は次のとおりです +1. `SpringNBTLibrary-<版>-dotnet8.zip`を展開します。中身は次のとおりです ``` SpringNBTLibrary.dll ライブラリ本体 @@ -50,9 +48,9 @@ dll を直接参照したいときはこちらです。 LICENSE / README.md ``` -2. プロジェクトの適当な場所(`lib/` など)へ dll と xml を隣り合わせで置きます。 - xml が無いと補完に説明が出ません -3. `.csproj` から参照します +2. プロジェクトの適当な場所(`lib/`など)へdllとxmlを隣り合わせで置きます。 + xmlが無いと補完に説明が出ません +3. `.csproj`から参照します ```xml @@ -62,21 +60,21 @@ dll を直接参照したいときはこちらです。 ``` -## NBT ファイルを読む +## NBTファイルを読む ```csharp using SpringNBTLibrary.Nbt; // 圧縮方式(Gzip / Zlib / 無圧縮)は自動で判定される NamedTag named = NbtIo.ReadFile("level.dat"); -NbtCompound data = ((NbtCompound)named.Tag).GetCompound("Data"); +NbtCompound data = named.Tag.GetCompound("Data"); Console.WriteLine(data.GetString("LevelName")); Console.WriteLine(data.GetInt("DataVersion")); ``` -型が違えば `UNEXPECTED_TAG_TYPE` の例外になります。 -キーが無いかもしれないときは `Opt*` を使います。 +型が違えば`UNEXPECTED_TAG_TYPE`の例外になります。 +キーが無いかもしれないときは`Opt*`を使います。 ```csharp // 無ければ null。あるが型違いなら例外 @@ -124,15 +122,15 @@ overworld.SetBlock(100, 64, -200, BlockState.Parse("minecraft:stone")); overworld.Flush(); // ここで初めてディスクへ書かれる ``` -> **Minecraft を終了してから実行すること。** -> 起動中のワールドへ書き込むとデータが壊れます。 -> C# 版は `session.lock` を確認して防いでいます([adr/0008](../adr/0008-session-lock.md))。 +> **Minecraftを終了してから実行してください。** +> 起動中のワールドへ書き込むとデータがおかしくなります。 +> C#版は`session.lock`を確認して防いでいます([adr/0008](../adr/0008-session-lock.md))。 -> ブロックを置き換えても Heightmaps と光源は再計算されません([adr/0004](../adr/0004-defer-heightmap-recalc.md))。 -> `Chunk.ClearHeightmaps()` / `InvalidateLighting()` でゲーム側に再計算させてください。 +> ブロックを置き換えてもHeightmapsと光源は再計算されません([adr/0004](../adr/0004-defer-heightmap-recalc.md))。 +> `Chunk.ClearHeightmaps()` / `InvalidateLighting()`でゲーム側に再計算させてください。 ## 次に読むもの -- [ガイド](../guide/01-nbt.md) — 目的別の使い方 -- [API 対応表](../api/overview.md) — 他言語版との対応 +- [ガイド](../guide/01-nbt.md): 目的別の使い方 +- [API対応表](../api/overview.md): 他言語版との対応 - [エラーと安全上限](../guide/06-errors-and-limits.md) diff --git a/docs/getting-started/java.md b/docs/getting-started/java.md index 16072fd..a705bf2 100644 --- a/docs/getting-started/java.md +++ b/docs/getting-started/java.md @@ -1,12 +1,12 @@ # はじめに(Java) -Java 21 (LTS) 以上が必要です。 +Java 21 (LTS)以上が必要です。 ## 導入 -[Releases](https://github.com/ScriptArts/SpringNBTLibrary/releases) から `spring-nbt-library-<版>.jar` を落とします。 +[Releases](https://github.com/ScriptArts/SpringNBTLibrary/releases)から`spring-nbt-library-<版>.jar`を落とします。 -Gradle なら jar を `libs/` へ置いて読み込みます。 +Gradleならjarを`libs/`へ置いて読み込みます。 ```groovy dependencies { @@ -14,7 +14,7 @@ dependencies { } ``` -Maven ならローカルリポジトリへ入れてから依存に書きます。 +Mavenならローカルリポジトリへ入れてから依存に書きます。 ```bash mvn install:install-file \ @@ -38,7 +38,7 @@ mvn install:install-file \ java -cp spring-nbt-library-1.0.0.jar:. YourApp ``` -## NBT ファイルを読む +## NBTファイルを読む ```java import io.github.scriptarts.springnbt.nbt.*; @@ -46,16 +46,16 @@ import java.nio.file.Path; // 圧縮方式(Gzip / Zlib / 無圧縮)は自動で判定される NamedTag named = NbtIo.readFile(Path.of("level.dat"), null); -NbtCompound data = ((NbtCompound) named.tag()).getCompound("Data"); +NbtCompound data = named.tag().getCompound("Data"); System.out.println(data.getString("LevelName")); System.out.println(data.getInt("DataVersion")); ``` -型が違えば `UNEXPECTED_TAG_TYPE` の例外になります。 -キーが無いかもしれないときは `opt*` を使います(無ければ `null`)。 +型が違えば`UNEXPECTED_TAG_TYPE`の例外になります。 +キーが無いかもしれないときは`opt*`を使います(無ければ`null`)。 -本ライブラリの例外 `SpringNbtException` は**非検査例外**です。 +本ライブラリの例外`SpringNbtException`は**非検査例外**です。 シグネチャを他言語版と揃えるため、検査例外は使いません([adr/0005](../adr/0005-unified-error-model.md))。 ## 書く @@ -99,15 +99,15 @@ try (MinecraftWorld world = MinecraftWorld.open(worldPath, options)) { } ``` -> **Minecraft を終了してから実行すること。** -> 起動中のワールドへ書き込むとデータが壊れます。 -> Java 版は `session.lock` を確認して防いでいます([adr/0008](../adr/0008-session-lock.md))。 +> **Minecraftを終了してから実行してください。** +> 起動中のワールドへ書き込むとデータがおかしくなります。 +> Java版は`session.lock`を確認して防いでいます([adr/0008](../adr/0008-session-lock.md))。 -> ブロックを置き換えても Heightmaps と光源は再計算されません([adr/0004](../adr/0004-defer-heightmap-recalc.md))。 -> `Chunk.clearHeightmaps()` / `invalidateLighting()` でゲーム側に再計算させてください。 +> ブロックを置き換えてもHeightmapsと光源は再計算されません([adr/0004](../adr/0004-defer-heightmap-recalc.md))。 +> `Chunk.clearHeightmaps()` / `invalidateLighting()`でゲーム側に再計算させてください。 ## 次に読むもの -- [ガイド](../guide/01-nbt.md) — 目的別の使い方 -- [API 対応表](../api/overview.md) — 他言語版との対応 +- [ガイド](../guide/01-nbt.md): 目的別の使い方 +- [API対応表](../api/overview.md): 他言語版との対応 - [エラーと安全上限](../guide/06-errors-and-limits.md) diff --git a/docs/getting-started/python.md b/docs/getting-started/python.md index 5117e0f..129c640 100644 --- a/docs/getting-started/python.md +++ b/docs/getting-started/python.md @@ -1,18 +1,18 @@ # はじめに(Python) -Python 3.10 以上が必要です。依存パッケージはありません。 +Python 3.10以上が必要です。依存パッケージはありません。 ## 導入 -[Releases](https://github.com/ScriptArts/SpringNBTLibrary/releases) から `spring_nbt_library-<版>-py3-none-any.whl` を落とします。 +[Releases](https://github.com/ScriptArts/SpringNBTLibrary/releases)から`spring_nbt_library-<版>-py3-none-any.whl`を落とします。 ```bash pip install spring_nbt_library-1.0.0-py3-none-any.whl ``` -wheel なのでビルドは不要です。 +wheelなのでビルドは不要です。 -## NBT ファイルを読む +## NBTファイルを読む ```python from spring_nbt_library.nbt import read_file @@ -25,8 +25,8 @@ print(data.get_string("LevelName")) print(data.get_int("DataVersion")) ``` -型が違えば `UNEXPECTED_TAG_TYPE` の例外になります。 -キーが無いかもしれないときは `opt_*` を使います(無ければ `None`)。 +型が違えば`UNEXPECTED_TAG_TYPE`の例外になります。 +キーが無いかもしれないときは`opt_*`を使います(無ければ`None`)。 ## 書く @@ -43,10 +43,12 @@ write_file("out.nbt", NamedTag("", root), NbtWriteOptions(compression=Compression.GZIP)) ``` -整数型の範囲は構築時に検査します。Python には整数の幅が無いので、 -型で守れないぶんを実行時に見ています。 +整数型の範囲は構築時に検査します。 +Pythonには整数の幅が無いので、型で守れないぶんを実行時に見ています。 ```python +from spring_nbt_library.nbt import NbtByte + NbtByte(300) # INVALID_ARGUMENT ``` @@ -67,7 +69,7 @@ with MinecraftWorld.open(world_path) as world: ## ブロックを書き換える ```python -from spring_nbt_library.world import MinecraftWorld, WorldOpenOptions +from spring_nbt_library.world import BlockState, MinecraftWorld, WorldOpenOptions with MinecraftWorld.open(world_path, WorldOpenOptions(writable=True)) as world: overworld = world.dimension("minecraft:overworld") @@ -75,11 +77,10 @@ with MinecraftWorld.open(world_path, WorldOpenOptions(writable=True)) as world: overworld.flush() # ここで初めてディスクへ書かれる ``` -> **Minecraft を終了してから実行すること。** -> Python 版が `session.lock` を確認するのは POSIX 環境だけです -> (Windows には `fcntl` がありません。[adr/0008](../adr/0008-session-lock.md))。 +> **Minecraftを終了してから実行してください。** +> Python版が`session.lock`を確認するのはPOSIX環境だけです(Windowsには`fcntl`がありません。[adr/0008](../adr/0008-session-lock.md))。 -> ブロックを置き換えても Heightmaps と光源は再計算されません([adr/0004](../adr/0004-defer-heightmap-recalc.md))。 +> ブロックを置き換えてもHeightmapsと光源は再計算されません([adr/0004](../adr/0004-defer-heightmap-recalc.md))。 ## 自分のワールドで検証する @@ -94,6 +95,6 @@ python3 spec/tools/scan_world.py "<ワールドのパス>" ## 次に読むもの -- [ガイド](../guide/01-nbt.md) — 目的別の使い方 -- [API 対応表](../api/overview.md) — 他言語版との対応 +- [ガイド](../guide/01-nbt.md): 目的別の使い方 +- [API対応表](../api/overview.md): 他言語版との対応 - [エラーと安全上限](../guide/06-errors-and-limits.md) diff --git a/docs/getting-started/rust.md b/docs/getting-started/rust.md index 92e9cb3..e77351b 100644 --- a/docs/getting-started/rust.md +++ b/docs/getting-started/rust.md @@ -1,20 +1,19 @@ # はじめに(Rust) -Rust 2021 edition、MSRV 1.75 です。 +Rust 2021 edition、MSRV 1.75です。 ## 導入 -Cargo.toml に git 参照を書きます。 +Cargo.tomlにgit参照を書きます。 ```toml [dependencies] spring-nbt-library = { git = "https://github.com/ScriptArts/SpringNBTLibrary", tag = "v1.0.0" } ``` -Cargo が自分で取得します。 +Cargoが自分で取得します。 -ネットワークに繋がらない環境なら、[Releases](https://github.com/ScriptArts/SpringNBTLibrary/releases) の -`spring-nbt-library-<版>.crate`(拡張子が違うだけの tar.gz)を展開してパス参照します。 +ネットワークに繋がらない環境なら、[Releases](https://github.com/ScriptArts/SpringNBTLibrary/releases)の`spring-nbt-library-<版>.crate`(拡張子が違うだけのtar.gz)を展開してパス参照します。 ```bash tar xzf spring-nbt-library-1.0.0.crate @@ -25,16 +24,16 @@ tar xzf spring-nbt-library-1.0.0.crate spring-nbt-library = { path = "spring-nbt-library-1.0.0" } ``` -## 例外ではなく `Result` +## 例外ではなく`Result` -Rust 版だけは例外ではなく `Result` を返します。 -`ErrorCode` の集合は他言語と完全に一致します([adr/0005](../adr/0005-unified-error-model.md))。 +Rust版だけは例外ではなく`Result`を返します。 +`ErrorCode`の集合は他言語と完全に一致します([adr/0005](../adr/0005-unified-error-model.md))。 ```rust use spring_nbt_library::error::{ErrorCode, Result}; ``` -## NBT ファイルを読む +## NBTファイルを読む ```rust use spring_nbt_library::nbt::{read_file, NbtReadOptions}; @@ -47,8 +46,8 @@ println!("{}", data.get_string("LevelName")?); println!("{}", data.get_int("DataVersion")?); ``` -型が違えば `ErrorCode::UnexpectedTagType` が返ります。 -キーが無いかもしれないときは `opt_*` を使います(無ければ `Ok(None)`)。 +型が違えば`ErrorCode::UnexpectedTagType`が返ります。 +キーが無いかもしれないときは`opt_*`を使います(無ければ`Ok(None)`)。 ## 書く @@ -64,15 +63,15 @@ let options = NbtWriteOptions { compression: Compression::Gzip, ..Default::defau write_file("out.nbt", &NamedTag::new("", root), &options)?; ``` -## 文字列は 2 形態 +## 文字列は2形態 -Rust の `String` は UTF-8 に限られるので、NBT に現れうる孤立サロゲートを保持できません。 -そこで `NbtString` を列挙にしています([spec/10 2.3](../spec/10-nbt-binary.md#23-各言語での保持方法))。 +Rustの`String`はUTF-8に限られるので、NBTに現れうる孤立サロゲートを保持できません。 +そこで`NbtString`を列挙にしています([spec/10 2.3](../spec/10-nbt-binary.md#23-各言語での保持方法))。 ```rust pub enum NbtString { Text(String), // 通常の文字列。実データはほぼすべてこちら - Surrogates(Vec), // UTF-8 に写せない UTF-16 コード単位の列 + Surrogates(Vec), // UTF-8に写せないUTF-16コード単位の列 } ``` @@ -96,6 +95,8 @@ world.close()?; ## ブロックを書き換える ```rust +use spring_nbt_library::world::{BlockState, MinecraftWorld, WorldOpenOptions}; + let options = WorldOpenOptions { writable: true, ..Default::default() }; let mut world = MinecraftWorld::open(world_path, options)?; @@ -108,22 +109,19 @@ if let Some(overworld) = world.dimension("minecraft:overworld")? { world.close()?; ``` -> **Minecraft を終了してから実行すること。** -> Rust 版は `std` にファイルロックが無いため -> `session.lock` は確認しません([adr/0008](../adr/0008-session-lock.md))。 -> 起動していないことは呼び出し側で担保してください。 +> **Minecraftを終了してから実行してください。** +> Rust版は`std`にファイルロックが無いため、`session.lock`は確認しません([adr/0008](../adr/0008-session-lock.md))。 +> 起動していないことは呼び出し側で保証してください。 -> ブロックを置き換えても Heightmaps と光源は再計算されません([adr/0004](../adr/0004-defer-heightmap-recalc.md))。 +> ブロックを置き換えてもHeightmapsと光源は再計算されません([adr/0004](../adr/0004-defer-heightmap-recalc.md))。 ## スタックの深さに注意 -NBT のネストは既定で深さ 512 まで許します。 -debug ビルドでは 1 段あたり約 8 KB 使うので、 -深いデータを扱うなら大きめのスタックを持つスレッドで走らせてください -([spec/00 5.1](../spec/00-conventions.md#51-深さ上限と実行スタック))。 +NBTのネストは既定で深さ512まで許します。 +debugビルドでは1段あたり約8 KB使うので、深いデータを扱うなら大きめのスタックを持つスレッドで走らせてください([spec/00 5.1](../spec/00-conventions.md#51-深さ上限と実行スタック))。 ## 次に読むもの -- [ガイド](../guide/01-nbt.md) — 目的別の使い方 -- [API 対応表](../api/overview.md) — 他言語版との対応 +- [ガイド](../guide/01-nbt.md): 目的別の使い方 +- [API対応表](../api/overview.md): 他言語版との対応 - [エラーと安全上限](../guide/06-errors-and-limits.md) diff --git a/docs/getting-started/typescript.md b/docs/getting-started/typescript.md index 03131b3..e6f8636 100644 --- a/docs/getting-started/typescript.md +++ b/docs/getting-started/typescript.md @@ -1,19 +1,20 @@ # はじめに(TypeScript) -Node.js 20 以上、TypeScript 5.7 以上が必要です。ESM 専用です。 +Node.js 20以上が必要です。ESM専用です。 +型定義を同梱しています。TypeScriptから使う場合は、ビルドに使っている5.7以上を推奨します。 ## 導入 -[Releases](https://github.com/ScriptArts/SpringNBTLibrary/releases) から `spring-nbt-library-<版>.tgz` を落とします。 +[Releases](https://github.com/ScriptArts/SpringNBTLibrary/releases)から`spring-nbt-library-<版>.tgz`を落とします。 ```bash npm install ./spring-nbt-library-1.0.0.tgz ``` -これで `node_modules` へ入り、通常のパッケージと同じように import できます。 +これで`node_modules`へ入り、通常のパッケージと同じようにimportできます。 型定義(`.d.ts`)も含まれているので補完が効きます。 -取り込み方は 2 通りあります。どちらでも構いません。 +取り込み方は2通りあります。どちらでも構いません。 ```ts // まとめて取る @@ -25,7 +26,7 @@ import { RegionFile } from "spring-nbt-library/anvil"; import { BlockState } from "spring-nbt-library/world"; ``` -`package.json` には次のように記録されます。 +`package.json`には次のように記録されます。 ```json { @@ -35,17 +36,17 @@ import { BlockState } from "spring-nbt-library/world"; } ``` -## `i64` は bigint +## `i64`はbigint -TypeScript の `number` は 2^53 を超える整数を表せません。 -`TAG_Long` と `TAG_Long_Array` だけ `bigint` を使います([adr/0007](../adr/0007-typescript-bigint.md))。 -他の整数型は `number` のままです。 +TypeScriptの`number`は2^53を超える整数を表せません。 +`TAG_Long`と`TAG_Long_Array`だけ`bigint`を使います([adr/0007](../adr/0007-typescript-bigint.md))。 +他の整数型は`number`のままです。 ```ts -compound.set("time", new NbtLong(449n)); // n を付ける +compound.set("time", new NbtLong(449n)); // nを付ける ``` -## NBT ファイルを読む +## NBTファイルを読む ```ts import { readFile, NbtCompound } from "spring-nbt-library"; @@ -58,8 +59,8 @@ console.log(data.getString("LevelName")); console.log(data.getInt("DataVersion")); ``` -型が違えば `UNEXPECTED_TAG_TYPE` の例外になります。 -キーが無いかもしれないときは `opt*` を使います(無ければ `undefined`)。 +型が違えば`UNEXPECTED_TAG_TYPE`の例外になります。 +キーが無いかもしれないときは`opt*`を使います(無ければ`undefined`)。 ## 書く @@ -96,6 +97,8 @@ try { ## ブロックを書き換える ```ts +import { BlockState, MinecraftWorld } from "spring-nbt-library"; + const world = MinecraftWorld.open(worldPath, { writable: true }); try { @@ -107,15 +110,14 @@ try { } ``` -> **Minecraft を終了してから実行すること。** -> TypeScript 版は Node にファイルロックの手段が無いため -> `session.lock` は確認しません([adr/0008](../adr/0008-session-lock.md))。 -> 起動中でないことは呼び出し側で担保することです。 +> **Minecraftを終了してから実行してください。** +> TypeScript版はNodeにファイルロックの手段が無いため、`session.lock`は確認しません([adr/0008](../adr/0008-session-lock.md))。 +> 起動していないことは呼び出し側で保証してください。 -> ブロックを置き換えても Heightmaps と光源は再計算されません([adr/0004](../adr/0004-defer-heightmap-recalc.md))。 +> ブロックを置き換えてもHeightmapsと光源は再計算されません([adr/0004](../adr/0004-defer-heightmap-recalc.md))。 ## 次に読むもの -- [ガイド](../guide/01-nbt.md) — 目的別の使い方 -- [API 対応表](../api/overview.md) — 他言語版との対応 +- [ガイド](../guide/01-nbt.md): 目的別の使い方 +- [API対応表](../api/overview.md): 他言語版との対応 - [エラーと安全上限](../guide/06-errors-and-limits.md) diff --git a/docs/guide/01-nbt.md b/docs/guide/01-nbt.md index 3e0edbe..1fc0023 100644 --- a/docs/guide/01-nbt.md +++ b/docs/guide/01-nbt.md @@ -1,19 +1,19 @@ -# 01. NBT の読み書き +# 01. NBTの読み書き -NBT (Named Binary Tag) は Minecraft のあらゆるデータの土台になる形式です。 -このレイヤは Minecraft のバージョンに一切依存しません。 +NBT(Named Binary Tag)は、Minecraftのあらゆるデータのもとになる形式です。 +このレイヤはMinecraftのバージョンに一切依存しません。 -> このガイドのコード例は基準実装の C# で示します。 -> 他の言語での綴りは [API 対応表](../api/nbt.md) を、 -> 最初の一歩は [はじめに](../getting-started/) を参照。 +> このガイドのコード例は基準実装のC#で示します。 +> 他の言語での綴りは[API対応表](../api/nbt.md)を、 +> 最初の一歩は[はじめに](../getting-started/)を参照してください。 --- -## 1. 13 種類のタグ +## 1. 13種類のタグ | ID | タグ | 値 | |---:|---|---| -| 0 | `TAG_End` | なし(Compound の終端) | +| 0 | `TAG_End` | なし(Compoundの終端) | | 1 | `TAG_Byte` | `i8` | | 2 | `TAG_Short` | `i16` | | 3 | `TAG_Int` | `i32` | @@ -21,17 +21,17 @@ NBT (Named Binary Tag) は Minecraft のあらゆるデータの土台になる | 5 | `TAG_Float` | `f32` | | 6 | `TAG_Double` | `f64` | | 7 | `TAG_Byte_Array` | `i8[]` | -| 8 | `TAG_String` | MUTF-8 の文字列 | +| 8 | `TAG_String` | MUTF-8の文字列 | | 9 | `TAG_List` | 同じ型のタグの列 | | 10 | `TAG_Compound` | 名前つきタグの集まり | | 11 | `TAG_Int_Array` | `i32[]` | | 12 | `TAG_Long_Array` | `i64[]` | -真偽値の専用型はありません。`TAG_Byte` の 0 / 1 で表します。 -`GetBool` はそれを読みやすく書くための糖衣です。 +真偽値の専用型はありません。`TAG_Byte`の0 / 1で表します。 +`GetBool`はそれを読みやすく書くための糖衣です。 数値はすべてビッグエンディアン、整数はすべて符号つきです。 -詳細は [spec/10](../spec/10-nbt-binary.md)。 +詳細は[spec/10](../spec/10-nbt-binary.md)。 --- @@ -39,28 +39,28 @@ NBT (Named Binary Tag) は Minecraft のあらゆるデータの土台になる ```csharp NamedTag named = NbtIo.ReadFile("level.dat"); -NbtCompound root = (NbtCompound)named.Tag; +NbtCompound root = named.Tag; ``` 圧縮方式(Gzip / Zlib / 無圧縮)は先頭バイトから自動判定します。 -指定したい場合だけ `NbtReadOptions.Compression` を設定します。 +指定したい場合だけ`NbtReadOptions.Compression`を設定します。 ### 型付き取得子 ```csharp int version = root.GetInt("DataVersion"); // 無い/型違いなら例外 -int? maybe = root.OptInt("DataVersion"); // 無ければ null、型違いなら例外 +int? maybe = root.OptInt("DataVersion"); // 無ければnull、型違いなら例外 ``` -「無い」と「型が違う」は区別しています。`Opt*` が許すのは前者だけです。 -型が違うのは常にプログラム側の想定違いなので、黙って握りつぶしません。 +「無い」と「型が違う」は区別しています。`Opt*`が許すのは前者だけです。 +型が違うのは常にプログラム側の想定違いなので、握りつぶすことはしません。 -### 連なった NBT を読む +### 連なったNBTを読む -`ReadBytes` は入力を 1 つの NBT として読みます。 +`ReadBytes`は入力を1つのNBTとして読みます。 後ろにバイトが残っていたらエラーです。読み違えを見逃さないためです。 -1 つのバイト列に NBT が複数並んでいるなら、位置を指定して読み進めます。 +1つのバイト列にNBTが複数並んでいるなら、位置を指定して読み進めます。 ```csharp int offset = 0; @@ -79,7 +79,7 @@ while (offset < bytes.Length) IReadOnlyList tags = NbtIo.ReadBytesAll(bytes); ``` -位置は渡したバイト列そのものを指すので、`ReadBytesAt` は圧縮されたデータを扱えません。 +位置は渡したバイト列そのものを指すので、`ReadBytesAt`は圧縮されたデータを扱えません。 ### 入れ子をたどる @@ -107,18 +107,17 @@ NbtIo.WriteFile("out.nbt", new NamedTag("", root)); ```csharp root.SetString("name", "SpringNBTLibrary"); root.SetInt("count", 42); -root.SetBool("enabled", true); // TAG_Byte の 0 / 1 になります +root.SetBool("enabled", true); // TAG_Byteの0 / 1になります root.SetByteArray("flags", new sbyte[] { 1, 0, 1 }); ``` -`NbtCompound` は挿入順を保持します。 -これは飾りではありません。触っていないデータを書き戻したときに -バイト単位で元と一致させるために必要です。 +`NbtCompound`は挿入順を保持します。 +これは飾りではありません。触っていないデータを書き戻したときに、バイト単位で元と一致させるために必要です。 -### リストは要素型が 1 つ +### リストは要素型が1つ -`TAG_List` は全要素が同じ型でなければなりません。 -違う型を足そうとすると `UNEXPECTED_TAG_TYPE` になります。 +`TAG_List`は全要素が同じ型でなければなりません。 +違う型を足そうとすると`UNEXPECTED_TAG_TYPE`になります。 ```csharp NbtList list = new NbtList(); @@ -126,9 +125,8 @@ list.Add(new NbtInt(1)); list.Add(new NbtString("x")); // 例外 ``` -空のリストは要素型 `TAG_End` で書き出します。 -ただし第三者のツールが別の要素型で空リストを書くことがあるので、 -読むときはそれも受け入れます。 +新しく作った空のリストは要素型`TAG_End`で書き出します。 +ただし、第三者のツールが別の要素型で空リストを書くことがあるので、読むときはそれも受け入れ、書き戻すときもその要素型を残します。 --- @@ -145,12 +143,12 @@ NbtIo.WriteFile("out.nbt", named, new NbtWriteOptions { Compression = Compression.Gzip }); ``` -### Java 形式と Network 形式 +### Java形式とNetwork形式 | 形式 | ルート | |---|---| -| `Java`(既定) | 名前つきの Compound | -| `Network` | 名前を持たない Compound(1.20.2 以降) | +| `Java`(既定) | 名前つきのCompound | +| `Network` | 名前を持たないCompound(1.20.2以降) | ```csharp NbtIo.ReadBytes(bytes, new NbtReadOptions { Format = NbtFormat.Network }); @@ -158,26 +156,25 @@ NbtIo.ReadBytes(bytes, new NbtReadOptions { Format = NbtFormat.Network }); --- -## 5. 文字列は MUTF-8 +## 5. 文字列はMUTF-8 -NBT の文字列は UTF-8 ではなく MUTF-8(Modified UTF-8)です。 -標準の UTF-8 と 2 点だけ違います。 +NBTの文字列はUTF-8ではなくMUTF-8(Modified UTF-8)です。 +標準のUTF-8と2点だけ違います。 -- `U+0000` を 1 バイトの `00` ではなく 2 バイトの `C0 80` で書く -- BMP 外の文字(絵文字など)を 4 バイトではなく - サロゲートペア 2 つ(3 バイト × 2)で書く +- `U+0000`を1バイトの`00`ではなく2バイトの`C0 80`で書く +- BMP外の文字(絵文字など)を4バイトではなくサロゲートペア2つ(3バイト × 2)で書く -ここを標準の UTF-8 のまま扱うと、絵文字を含む看板やアイテム名が壊れます。 -どの言語版も MUTF-8 として読み書きします。 +文字列を標準のUTF-8のまま扱うと、絵文字を含む看板やアイテム名がおかしくなります。 +どの言語版もMUTF-8として読み書きします。 -長さは 2 バイトの符号なし整数で表すので、1 つの文字列は 65535 バイトまでです。 +長さは2バイトの符号なし整数で表すので、1つの文字列は65535バイトまでです。 -詳細は [spec/10 2章](../spec/10-nbt-binary.md#2-文字列-mutf-8)。 +詳細は[spec/10 2章](../spec/10-nbt-binary.md#2-文字列-mutf-8)。 --- ## 6. 次に読むもの -- [02. SNBT](02-snbt.md) — 人が読める形式との相互変換 -- [03. リージョンファイル](03-anvil-region.md) — `.mca` を扱う -- [06. エラーと安全上限](06-errors-and-limits.md) — 壊れた入力への備え +- [02. SNBT](02-snbt.md)(人が読める形式との相互変換) +- [03. リージョンファイル](03-anvil-region.md)(`.mca`を扱う) +- [06. エラーと安全上限](06-errors-and-limits.md)(不正な入力への備え) diff --git a/docs/guide/02-snbt.md b/docs/guide/02-snbt.md index 86528ed..cf35a8e 100644 --- a/docs/guide/02-snbt.md +++ b/docs/guide/02-snbt.md @@ -1,14 +1,14 @@ # 02. SNBT -SNBT (Stringified NBT) は NBT を人が読める文字列で表したものです。 -コマンドの `/give` や `/data` で使われる記法です。 +SNBT(Stringified NBT)は、NBTを人が読める文字列で表したものです。 +コマンドの`/give`や`/data`で使われる記法です。 ``` {Name:"minecraft:oak_stairs",Count:1b,tag:{Damage:0}} ``` -> コード例は基準実装の C#。 -> 他言語での綴りは [API 対応表](../api/nbt.md)。 +> コード例は基準実装のC#で示します。 +> 他の言語での綴りは[API対応表](../api/nbt.md)を参照してください。 --- @@ -23,8 +23,8 @@ string oneLine = Snbt.Write(tag); string pretty = Snbt.WritePretty(tag); ``` -`WritePretty` は改行とインデントを入れます。 -`Write` は 1 行に詰める。どちらも全言語で 1 バイトも違わない出力になります。 +`WritePretty`は改行とインデントを入れます。 +`Write`は1行に詰めます。どちらも全言語で1バイトも違わない出力になります。 --- @@ -52,7 +52,7 @@ string pretty = Snbt.WritePretty(tag); --- -## 3. 1.21.5 以降の拡張構文 +## 3. 1.21.5以降の拡張構文 新しい構文にも対応しています。 @@ -69,9 +69,10 @@ true/false bool(TAG_Byte の 1 / 0 になる) ### 16進での接尾辞に注意 -`0xFFb` の `b` は 16 進の数字なのか `TAG_Byte` の印なのか区別できません。 -そこで 16 進・2 進リテラルでは `s` / `S` / `l` / `L` だけを型の接尾辞として認めます。 -`b` / `d` / `f` は数字として読みます([spec/11 2.1](../spec/11-snbt.md))。 +`0xFFb`の`b`は、16進の数字なのか`TAG_Byte`の印なのか区別できません。 +そこで、16進リテラルでは`s` / `S` / `l` / `L`だけを型の接尾辞として認めます。 +`b` / `d` / `f`は数字として読みます([spec/11 2.1](../spec/11-snbt.md))。 +2進リテラルの数字は`0`と`1`だけなので、どの接尾辞も使えます。 --- @@ -81,49 +82,51 @@ true/false bool(TAG_Byte の 1 / 0 になる) Infinity -Infinity NaN ``` -これらは SNBT で読み書きできるが、Minecraft 自身は受け付けないことがあります。 +これらはSNBTで読み書きできますが、Minecraft自身は受け付けないことがあります。 ワールドへ書き込む用途には使わないでください。 --- ## 5. 出力は常に同じ形になる -全言語で同じ入力から同じ文字列が出るよう、次を仕様で固定しています。 +全言語で同じ入力から同じ文字列が出るように、次の点を仕様で固定しています。 ### 小数の表記 -言語ごとの既定の書式(C# の `R`、Java の `Double.toString`、 -JavaScript の `toString`…)は微妙に違います。 -そこで元の値へ正確に戻る最短の 10 進表記を自前で求めている -([spec/11 5.1](../spec/11-snbt.md#51-浮動小数点の正準10進表記))。 +言語ごとの既定の書式(C#の`R`、Javaの`Double.toString`、JavaScriptの`toString`など)は微妙に違います。 +そこで、元の値へ正確に戻る最短の10進表記を自前で求めています([spec/11 5.1](../spec/11-snbt.md#51-浮動小数点の正準10進表記))。 ``` 0.1f -> 0.1f (0.10000000149011612f にはしない) --0.0 -> -0.0 (符号を落とさない) +-0.0 -> -0.0d (符号を落とさない) ``` ### 引用符とエスケープ -- 文字列は原則 `"` で囲む -- `"` を含み `'` を含まないときだけ `'` で囲む +- 文字列は常に`"`で囲む +- `"`と`\`は`\"`と`\\`にする +- `\b` `\t` `\n` `\f` `\r`はこの短縮形で書き、それ以外の制御文字は`\uXXXX`にする - 正しいサロゲートペア(絵文字など)はそのまま出す -- 孤立サロゲートだけを `\uXXXX` でエスケープする +- 孤立サロゲートは`\uXXXX`でエスケープする ### キーの引用 -`[A-Za-z0-9_.+-]` だけからなるキーは引用しない。それ以外は引用します。 +`[A-Za-z0-9_.+-]`だけからなるキーは引用せず、それ以外は引用します。 --- ## 6. 扱わないもの -SNBT にはコマンド用の拡張(`bool()`、`uuid()` など)があるが、 -バイナリ NBT へ一意に写せるものだけを対象にしている -([adr/0006](../adr/0006-snbt-scope.md))。 +対象は、バイナリNBTへ一意に写せる範囲だけです([adr/0006](../adr/0006-snbt-scope.md))。 + +- `[1, "a", {}]`のように型の違う要素が混じったリストは、バイナリNBTの`TAG_List`で表せないので受け付けません。パースすると`MALFORMED_DATA`になります +- 文字名によるエスケープ`\N{名前}`は、実装間で名前表が揃わないので扱いません。パースすると`UNSUPPORTED_FEATURE`になります + +コマンド用の拡張のうち`bool()`と`uuid()`は、バイナリNBTへ写せるので対応しています。 --- ## 7. 次に読むもの -- [01. NBT の読み書き](01-nbt.md) -- [仕様 11: SNBT](../spec/11-snbt.md) — EBNF 文法と正準表記の算法 +- [01. NBTの読み書き](01-nbt.md) +- [仕様11: SNBT](../spec/11-snbt.md)(EBNF文法と正準表記の算法) diff --git a/docs/guide/03-anvil-region.md b/docs/guide/03-anvil-region.md index 35df61b..e82dde4 100644 --- a/docs/guide/03-anvil-region.md +++ b/docs/guide/03-anvil-region.md @@ -1,12 +1,12 @@ # 03. リージョンファイル (.mca) -Anvil 形式のリージョンファイルは、32×32 = 1024 チャンクをまとめた入れ物です。 -`r...mca` という名前で、`region/` `entities/` `poi/` の各ディレクトリに置かれます。 +Anvil形式のリージョンファイルは、32×32 = 1024チャンクをまとめた入れ物です。 +`r...mca`という名前で、`region/`、`entities/`、`poi/`の各ディレクトリに置かれます。 -このレイヤはチャンクの中身を解釈しない。NBT として出し入れするだけです。 -中身を扱いたい場合は [04](04-world-and-level-dat.md) / [05](05-blocks-and-biomes.md) へ。 +このレイヤはチャンクの中身を解釈しません。NBTとして出し入れするだけです。 +中身を扱いたい場合は[04](04-world-and-level-dat.md)と[05](05-blocks-and-biomes.md)を参照してください。 -> コード例は基準実装の C#。他言語での綴りは [API 対応表](../api/anvil.md)。 +> コード例は基準実装のC#で示します。他の言語での綴りは[API対応表](../api/anvil.md)を参照してください。 --- @@ -44,12 +44,11 @@ region.WriteChunk(5, 12, chunk); region.Flush(); // ここでディスクへ反映される ``` -`Flush` を呼ぶまでディスクは変わりません。 +`Flush`を呼ぶまでディスクは変わりません。 リージョン全体をメモリ上のセクタ表として持ち、まとめて書き出す設計です。 この設計のおかげで、開いて何も変えずに書き戻すと元とバイト単位で一致します。 -触っていないチャンクの配置が動かないので、 -1 チャンクだけ書き換えるつもりが他を壊す、という事故が起きません。 +触っていないチャンクの配置は動きません。そのため、1チャンクだけ書き換えるつもりがほかのチャンクまで破損させてしまう、という失敗は起きません。 --- @@ -62,9 +61,9 @@ region.Flush(); // ここでディスクへ反映される +------+-------------+------------------------+ ``` -- 位置表 1024 個 × 4 バイト。上位 3 バイトが開始セクタ、下位 1 バイトがセクタ数 -- タイムスタンプ表 1024 個 × 4 バイト。Unix 秒 -- 位置とタイムスタンプがどちらも 0 なら、そのチャンクは存在しない +- ロケーションテーブルは1024個 × 4バイト。上位3バイトが開始セクタ、下位1バイトがセクタ数 +- タイムスタンプテーブルは1024個 × 4バイト。値はUnix秒 +- 位置とタイムスタンプがどちらも0なら、そのチャンクは存在しない チャンク本体は次の形をしています。 @@ -74,7 +73,7 @@ scheme: u8 圧縮方式 data: ... 圧縮された NBT ``` -詳細は [spec/20](../spec/20-anvil-region.md)。 +詳細は[spec/20](../spec/20-anvil-region.md)。 --- @@ -82,19 +81,19 @@ data: ... 圧縮された NBT | ID | 方式 | 本ライブラリ | |---:|---|---| -| 1 | GZip | ✅ | -| 2 | Zlib | ✅(書き込みの既定) | -| 3 | 無圧縮 | ✅ | -| 4 | LZ4 | 🔶 読み込みのみ | -| 127 | サードパーティ独自 | ❌ 生バイトでのみ取得可 | - -LZ4 は読めますが、書き出せません。 -LZ4 のチャンクを書き換えると、その1つだけ Zlib になります。 +| 1 | GZip | 対応 | +| 2 | Zlib | 対応(書き込みの既定) | +| 3 | 無圧縮 | 対応 | +| 4 | LZ4 | 読み込みのみ | +| 127 | サードパーティ独自 | 未対応(生バイトでのみ取得可) | + +LZ4は読めますが、書き出せません。 +LZ4のチャンクを書き換えると、そのチャンクだけZlibになります。 圧縮方式はチャンクごとに記録されるので、混在してもゲームは読めます。 -触っていないチャンクは生バイトのまま素通しするので、LZ4 のまま残ります。 +触っていないチャンクは生バイトのまま保持するので、LZ4のまま残ります。 -扱えない方式のチャンクを `ReadChunk` すると `UNSUPPORTED_FEATURE` になります。 -中身が要らない(別のリージョンへ丸ごと移すだけ、など)なら生バイトで扱えます。 +扱えない方式のチャンクを`ReadChunk`で読むと`UNSUPPORTED_FEATURE`になります。 +中身が要らない場合(別のリージョンへ丸ごと移すだけ、など)は、生バイトで扱えます。 ```csharp RawChunk raw = region.ReadChunkRaw(5, 12)!; @@ -105,13 +104,13 @@ other.WriteChunkRaw(5, 12, raw); // 展開せずそのまま移す ## 5. 大きすぎるチャンク(.mcc) -1 チャンクは位置表の都合で 255 セクタ(約 1MiB)までしか入りません。 -超えると Minecraft は本体を `c...mcc` という別ファイルへ出します。 +1チャンクはロケーションテーブルの都合で255セクタ(約1MiB)までしか入りません。 +これを超えると、Minecraftは本体を`c...mcc`という別ファイルへ出します。 本ライブラリはこれを自動で処理します。 -- 読むとき: `.mcc` があれば透過的に読む -- 書くとき: 1MiB を超えたら自動で `.mcc` へ出す。縮んだら本体へ戻して `.mcc` を消す +- 読むとき: `.mcc`があれば透過的に読む +- 書くとき: 255セクタに収まらなければ自動で`.mcc`へ出す。縮んだら本体へ戻して`.mcc`を消す 利用者が意識する必要はありません。 @@ -126,15 +125,14 @@ region.Optimize(); // 添字順に詰め直す region.Flush(); ``` -必要なとき以外は呼ばなくて構いません。ファイル全体が書き換わるので、 -無変更なら元と一致するという性質は失われます。 +必要なとき以外は呼ばなくて構いません。ファイル全体が書き換わるので、無変更なら元と一致するという性質は失われます。 --- ## 7. フォルダ単位で扱う -複数のリージョンにまたがる操作には `RegionFolder` を使います。 -`r.X.Z.mca` の名前解決とファイルの開閉をまとめて引き受けます。 +複数のリージョンにまたがる操作には`RegionFolder`を使います。 +`RegionFolder`は`r.X.Z.mca`の名前解決とファイルの開閉をまとめて引き受けます。 ```csharp using RegionFolder folder = RegionFolder.Open("dimensions/minecraft/overworld/region", @@ -151,13 +149,11 @@ foreach (ChunkPos pos in folder.ChunkPositions()) ## 8. 開いたリージョンはいくつまでか -`RegionFile` はファイル全体をメモリへ載せます。 -無変更で書き戻したときにバイト単位で一致させるための設計だが、 -そのぶん開いたまま貯めるとメモリを食います。 +`RegionFile`はファイル全体をメモリへ載せます。 +無変更で書き戻したときにバイト単位で一致させるための設計ですが、そのぶん開いたまま溜めるとメモリを食います。 -`RegionFolder` は開いたリージョンの数に上限を持ち(既定 8 件)、 -超えたら、最も長く使っていないものを書き出してから閉じます。 -数千リージョンあるワールドを端から走査しても破綻しません。 +`RegionFolder`は開いたリージョンの数に上限を持ち(既定8件)、上限を超えたら、最も長く使っていないものを書き出してから閉じます。 +数千リージョンあるワールドを端から走査しても問題は起きません。 ```csharp // 上限を変えたい場合 @@ -165,32 +161,30 @@ using RegionFolder folder = RegionFolder.Open(path, RegionFileMode.ReadWrite, maxCachedRegions: 32); ``` -このため `Region()` が返した参照は、 -別のリージョンへアクセスすると閉じられている場合があります。 +このため、`Region()`が返した参照は、別のリージョンへアクセスすると閉じられる場合があります。 ```csharp RegionFile? a = folder.Region(0, 0); folder.ReadChunk(9999, 9999); // 別のリージョンを開く -// a はもう閉じられているかもしれない +// aはもう閉じられているかもしれない ``` -参照を持ち回らず、必要なたびに取得してください。 -`ReadChunk` / `WriteChunk` 経由で使うぶんには意識しなくて構いません。 +参照を持ち回らず、必要になるたびに取得してください。 +`ReadChunk` / `WriteChunk`経由で使うぶんには意識しなくて構いません。 -## 9. 壊れたファイルの検出 +## 9. 破損したファイルの検出 -開いた時点で次を検査し、問題があれば `MALFORMED_DATA` にします。 +ファイルを開いた時点で次の点を検査し、問題があれば`MALFORMED_DATA`にします。 -- 位置表が指すセクタがファイルの外を指している -- 2 つのチャンクが同じセクタを使っている -- ファイル長が 4KiB の倍数でない +- ロケーションテーブルの指すセクタがファイルの外にある +- 2つのチャンクが同じセクタを使っている +- ファイル長が4KiBの倍数でない -黙って読み進めると、書き戻したときに別のチャンクを破壊しかねないため、 -先に止める方針をとっています。 +エラーを出さずに読み進めると、書き戻したときに別のチャンクを破損させかねません。そのため、先に止める方針をとっています。 --- ## 10. 次に読むもの -- [04. ワールドと level.dat](04-world-and-level-dat.md) -- [仕様 20: Anvil リージョン形式](../spec/20-anvil-region.md) +- [04. ワールドとlevel.dat](04-world-and-level-dat.md) +- [仕様20: Anvilリージョン形式](../spec/20-anvil-region.md) diff --git a/docs/guide/04-world-and-level-dat.md b/docs/guide/04-world-and-level-dat.md index 3d93179..dad9471 100644 --- a/docs/guide/04-world-and-level-dat.md +++ b/docs/guide/04-world-and-level-dat.md @@ -1,14 +1,14 @@ -# 04. ワールドと level.dat +# 04. ワールドとlevel.dat -ワールド全体を開き、`level.dat` と次元を扱います。 +ワールド全体を開き、`level.dat`と次元を扱います。 -> コード例は基準実装の C#。他言語での綴りは [API 対応表](../api/world.md)。 +> コード例は基準実装のC#で示します。他の言語での綴りは[API対応表](../api/world.md)を参照してください。 --- -## 1. 26.x のディレクトリ構成 +## 1. 26.xのディレクトリ構成 -1.21.x から大きく変わっています。 +1.21.xから大きく変わっています。 ``` <ワールド名>/ @@ -35,16 +35,16 @@ └─ data/minecraft/ ``` -変更点のまとめ +変更点をまとめると次のとおりです。 | 1.21.x | 26.x | |---|---| | `region/`(オーバーワールドは直下) | `dimensions/minecraft/overworld/region/` | | `DIM-1/` `DIM1/` | `dimensions/minecraft/the_nether/` `.../the_end/` | | `playerdata/` | `players/data/` | -| `level.dat` にゲームルール等を同梱 | `data/minecraft/*.dat` へ分離 | +| `level.dat`にゲームルール等を同梱 | `data/minecraft/*.dat`へ分離 | -この構成は[実際の 26.2 ワールドを走査して確かめた](../spec/40-world-layout.md)。 +この構成は、[実際の26.2ワールドを走査して確かめました](../spec/40-world-layout.md)。 --- @@ -58,20 +58,19 @@ Console.WriteLine(world.Level.VersionName); // 26.2 Console.WriteLine(world.Level.DataVersion); // 4903 ``` -既定は読み取り専用。 書き込むには明示的に許可します。 +既定は読み取り専用です。書き込むには明示的に許可します。 ```csharp WorldOpenOptions options = new WorldOpenOptions { Writable = true }; using MinecraftWorld world = MinecraftWorld.Open(worldPath, options); ``` -> 書き込みで開くと `session.lock` を確認する(C# / Java / Python(POSIX) のみ)。 -> TypeScript と Rust は確認しないので、 -> Minecraft が起動していないことを呼び出し側で担保すること([adr/0008](../adr/0008-session-lock.md))。 +> 書き込みで開くと`session.lock`を確認します(C# / Java / Python(POSIX)のみ)。 +> TypeScriptとRustは確認しないので、Minecraftが起動していないことを呼び出し側で確かめてください([adr/0008](../adr/0008-session-lock.md))。 --- -## 3. level.dat の中身 +## 3. level.datの中身 よく使う項目には名前つきの取得子があります。 @@ -81,7 +80,7 @@ LevelData level = world.Level; level.LevelName; // ワールド名 level.DataVersion; // 4903 level.VersionName; // "26.2" -level.Time; // 経過 tick +level.Time; // 経過tick level.GameType; // 0=サバイバル 1=クリエイティブ 2=アドベンチャー 3=スペクテイター level.SpawnPos; // [x, y, z] level.SpawnDimension; // "minecraft:overworld" @@ -89,7 +88,7 @@ level.Difficulty; // "normal" level.IsHardcore; ``` -それ以外は生の NBT を直接たどります。 +それ以外は生のNBTを直接たどります。 ```csharp NbtCompound data = level.Data; @@ -97,7 +96,7 @@ NbtCompound data = level.Data; ### 分離されたデータファイル -ゲームルールなどは `level.dat` にはもう入っていません。 +ゲームルールなどは`level.dat`にはもう入っていません。 ```csharp NbtCompound? rules = world.DataFile("game_rules"); @@ -118,11 +117,10 @@ foreach (string id in world.DimensionIds()) Dimension? overworld = world.Dimension("minecraft:overworld"); ``` -名前空間を省くと `minecraft:` を補う。`world.Dimension("overworld")` でも構いません。 -存在しない次元(まだ生成されていない、など)は `null` になります。 +名前空間を省くと`minecraft:`を補います。`world.Dimension("overworld")`でも構いません。 +存在しない次元(まだ生成されていない、など)は`null`になります。 -カスタム次元も同じように扱えます。データパックが作った -`dimensions/mypack/mydim/` は `"mypack:mydim"` で開けます。 +カスタム次元も同じように扱えます。データパックが作った`dimensions/mypack/mydim/`は`"mypack:mydim"`で開けます。 ### 次元の中身 @@ -134,7 +132,7 @@ foreach (ChunkPos pos in overworld.ChunkPositions()) } ``` -`entities/` と `poi/` は生の NBT として読めます。 +`entities/`と`poi/`は生のNBTとして読めます。 ```csharp RegionFolder? entities = overworld.EntityFolder; @@ -159,22 +157,21 @@ foreach (string uuid in world.PlayerIds()) ```csharp overworld.Flush(); // 変更したチャンクを書き戻す -world.SaveLevel(); // level.dat を書き戻す +world.SaveLevel(); // level.datを書き戻す ``` -`SaveLevel` は一時ファイルへ書いてから置き換えます。 -書き込み中に落ちても `level.dat` が壊れないようにするためで、 -直前の内容は `level.dat_old` へ退避されます。 +`SaveLevel`は一時ファイルへ書き、内容をディスクへ確実に書き出してから置き換えます。 +書き込み中に落ちても`level.dat`が破損しないようにするためです。直前の内容は`level.dat_old`へ退避されます。 -`using`(Java は try-with-resources、Python は `with`)を抜けるときに -`Close` が呼ばれ、書き込みモードなら自動で `Flush` されます。 +`using`(Javaはtry-with-resources、Pythonは`with`)を抜けるときに`Close`が呼ばれ、書き込みモードなら自動で`Flush`されます。 --- ## 7. バージョンが違うワールド -読み込みは寛容に、書き込みは対象バージョン固定にしています -([adr/0003](../adr/0003-version-policy.md))。詳しくは [07](07-version-policy.md)。 +読み込みは寛容にしています。 +書き込みを既定で止めるのは、形式の違う古いチャンク(DataVersion 4786未満)だけです。`DataVersion`は読んだ値のまま残します([adr/0003](../adr/0003-version-policy.md))。 +詳しくは[07](07-version-policy.md)。 --- @@ -182,4 +179,4 @@ world.SaveLevel(); // level.dat を書き戻す - [05. ブロックとバイオーム](05-blocks-and-biomes.md) - [07. バージョンポリシー](07-version-policy.md) -- [仕様 40: ワールドのディレクトリ構成](../spec/40-world-layout.md) +- [仕様40: ワールドのディレクトリ構成](../spec/40-world-layout.md) diff --git a/docs/guide/05-blocks-and-biomes.md b/docs/guide/05-blocks-and-biomes.md index 356f783..d66097f 100644 --- a/docs/guide/05-blocks-and-biomes.md +++ b/docs/guide/05-blocks-and-biomes.md @@ -2,7 +2,7 @@ チャンクの中身を読み書きします。3つのレイヤのいちばん上にあたります。 -> コード例は基準実装の C#。他言語での綴りは [API 対応表](../api/world.md)。 +> コード例は基準実装のC#で示します。他の言語での綴りは[API対応表](../api/world.md)を参照してください。 --- @@ -13,17 +13,16 @@ BlockState? block = overworld.GetBlock(100, 64, -200); Console.WriteLine(block); // minecraft:grass_block[snowy=false] ``` -座標は絶対ワールド座標。どのリージョンのどのチャンクのどのセクションか、 -という解決はすべて内部で行います。 +座標は絶対ワールド座標です。どのリージョンのどのチャンクのどのセクションか、という解決はすべて内部で行います。 -チャンクが存在しない、またはその高さにセクションが無ければ `null` になります。 +チャンクが存在しない、またはその高さにセクションが無ければ`null`になります。 「空気」と「そもそも無い」は別のものです。 チャンクを直接持っているなら、チャンク内相対座標(0〜15)でも読めます。 ```csharp Chunk chunk = overworld.Chunk(6, -13)!; -BlockState? block = chunk.GetBlock(4, 64, 10); // x, z は 0..15 +BlockState? block = chunk.GetBlock(4, 64, 10); // x, zは0..15 ``` --- @@ -40,17 +39,17 @@ stairs.Property("facing"); // "north" stairs.ToString(); // "minecraft:oak_stairs[facing=north,half=top]" ``` -- 名前空間を省くと `minecraft:` を補う(`Parse("stone")` → `minecraft:stone`) +- 名前空間を省くと`minecraft:`を補う(`Parse("stone")` → `minecraft:stone`) - プロパティは必ず名前の昇順で出力する。同じ状態なら同じ文字列になり、全言語で一致する -- プロパティの値はすべて文字列。`"false"` は真偽値ではなく文字列 +- プロパティの値はすべて文字列。`"false"`は真偽値ではなく文字列 ```csharp // 並び順が違っても同じブロックとして等しい BlockState.Parse("minecraft:oak_stairs[facing=north,half=top]") - == BlockState.Parse("minecraft:oak_stairs[half=top,facing=north]"); // true + .Equals(BlockState.Parse("minecraft:oak_stairs[half=top,facing=north]")); // true ``` -不変オブジェクトなので、一部だけ変えたいときは `With` を使います。 +不変オブジェクトなので、一部だけ変えたいときは`With`を使います。 ```csharp BlockState facingSouth = stairs.With("facing", "south"); @@ -65,8 +64,7 @@ overworld.SetBlock(100, 64, -200, "minecraft:stone"); overworld.Flush(); ``` -`BlockState` を渡すこともできます。同じ状態を何度も置くなら、 -一度解析したものを使い回すほうが速くなります。 +`BlockState`を渡すこともできます。同じ状態を何度も置くなら、一度解析したものを使い回すほうが速くなります。 ```csharp BlockState stone = BlockState.Parse("minecraft:stone"); @@ -78,29 +76,26 @@ overworld.SetBlock(100, 64, -200, stone); 利用者が意識する必要はありません。 チャンクの新規生成はしません。 -存在しない座標へ置こうとすると `INVALID_ARGUMENT` になります。 +存在しない座標へ置こうとすると`INVALID_ARGUMENT`になります。 ワールド生成はゲーム側の仕事です。 ### 付随データは自動で取り除かれる -チェストのあった座標に石を置くと、`block_entities` に残っていた -チェストの中身は自動で取り除かれます。 -`block_ticks` / `fluid_ticks` も同様です。 +チェストのあった座標に石を置くと、`block_entities`に残っていたチェストの中身は自動で取り除かれます。 +`block_ticks` / `fluid_ticks`も同様です。 ```csharp // この座標にチェストがあったなら、その中身ごと消える overworld.SetBlock(100, 64, -200, "minecraft:stone"); ``` -取り除かないと、ブロックと中身が食い違った状態が残り、 -Minecraft 側で警告やアイテムの復活といった予期しない挙動を招きます。 -しかも利用者からは見えにくい壊れ方をします(→ [spec/30 2.4](../spec/30-chunk-format.md#24-ブロックを置き換えたときの掃除))。 +取り除かないと、ブロックと中身が食い違った状態が残り、Minecraft側で警告やアイテムの復活といった予期しない挙動を招きます。 +しかも、利用者からは見えにくい形でおかしくなります(→ [spec/30 2.4](../spec/30-chunk-format.md#24-ブロックを置き換えたときの掃除))。 同じブロック状態を置き直したときは何も起きません。 状態が変わっていない以上、付随データを触る理由がないからです。 -逆に、新しいブロックが必要とする `block_entity`(置いたのがチェストなら空のチェスト)は -生成しません。必要なら生の NBT へ直接足してください。 +逆に、新しいブロックが必要とする`block_entity`(置いたのがチェストなら空のチェスト)は生成しません。必要なら生のNBTへ直接足してください。 ### 使わなくなったブロックの掃除 @@ -110,18 +105,19 @@ Minecraft 側で警告やアイテムの復活といった予期しない挙動 chunk.Compact(); // 未使用のパレット要素を捨て、必要ならビット幅を縮める ``` -`SetBlock` のたびに自動でやると著しく遅くなるので、明示的に呼んだときだけ行います。 +`SetBlock`のたびに自動でやると著しく遅くなるので、明示的に呼んだときだけ行います。 --- ### 範囲をまとめて書き換える -`Cuboid` は両端を指定した直方体の範囲です。`Positions()` で中の座標を順に返します。 +`Cuboid`は両端を指定した直方体の範囲です。`Positions()`は中の座標を順に返します。 ```csharp Cuboid area = Cuboid.Of(100, 64, -200, 110, 70, -190); -// 並びは Y、Z、X の順。X がいちばん内側で動くので、同じセクションを続けて触れる +// 並びはY、Z、Xの順 +// Xがいちばん内側で動くので、同じセクションを続けて触れる foreach (BlockPos pos in area.Positions()) { overworld.SetBlock(pos.X, pos.Y, pos.Z, "minecraft:stone"); @@ -130,7 +126,7 @@ foreach (BlockPos pos in area.Positions()) overworld.Flush(); ``` -`BlockPos` からはチャンク座標も引けます。 +`BlockPos`からはチャンク座標も引けます。 ```csharp BlockPos pos = new BlockPos(100, 64, -200); @@ -140,7 +136,7 @@ int localX = pos.LocalX; // 4 ### 書き戻される対象 -ブロックやバイオームを書き換えたチャンクには印が付き、`Flush` はそれだけを書き戻します。 +ブロックやバイオームを書き換えたチャンクには印が付き、`Flush`は印の付いたチャンクだけを書き戻します。 ```csharp Chunk chunk = overworld.Chunk(6, -13)!; @@ -149,24 +145,24 @@ chunk.IsModified; // true ``` 同じ状態を置き直しただけなら何も起きないので、印も付きません。 -`Raw` を直接いじったときは印が付かないので、自分で立ててください。 +`Raw`を直接いじったときは印が付かないので、自分で立ててください。 --- ## 4. バイオーム -バイオームは 4×4×4 ブロックが 1 単位です。1 ブロック単位では設定できません。 +バイオームは4×4×4ブロックが1単位です。1ブロック単位では設定できません。 ```csharp string? biome = overworld.GetBiome(100, 64, -200); overworld.SetBiome(100, 64, -200, "minecraft:desert"); ``` -`SetBiome(100, 64, -200, ...)` は、その座標を含む 4×4×4 の枠すべてを変えます。 +`SetBiome(100, 64, -200, ...)`は、その座標を含む4×4×4の枠すべてを変えます。 --- -## 5. Heightmaps と光源は再計算されない +## 5. Heightmapsと光源は再計算されない ブロックを置き換えても、`Heightmaps`(地表の高さ)と光源データは古いままです。 そのままだと、ゲームで見たときに影や明るさがおかしくなります。 @@ -174,11 +170,11 @@ overworld.SetBiome(100, 64, -200, "minecraft:desert"); 代わりに、ゲーム側へ再計算させます。 ```csharp -chunk.ClearHeightmaps(); // Heightmaps を消す -chunk.InvalidateLighting(); // isLightOn = false にする +chunk.ClearHeightmaps(); // Heightmapsを消す +chunk.InvalidateLighting(); // isLightOn = falseにする ``` -こうしておくと、Minecraft がそのチャンクを読み込むときに自分で作り直します。 +こうしておくと、Minecraftがそのチャンクを読み込むときに自分で作り直します。 ブロックを書き換えたら、どちらも呼んでおくのが安全です。 --- @@ -192,27 +188,26 @@ palette: [ air, stone, dirt ] 使われている種類の一覧 data: [ 0,0,1,1,2,0,... ] 各マスがパレットの何番か ``` -`data` は 64 ビット整数の配列に詰めます。 -1 つの値は必ず 1 つの整数に収まり、境界を跨ぎません。 -必要ビット数は `max(4, ceil(log2(パレット長)))` で決まります。 +`data`は64ビット整数の配列に詰めます。 +1つの値は必ず1つの整数に収まり、境界を跨ぎません。 +必要ビット数は`max(4, ceil(log2(パレット長)))`で決まります。 -| パレット長 | ビット幅 | `data` の長さ | +| パレット長 | ビット幅 | `data`の長さ | |---:|---:|---:| -| 1 | — | `data` を持たない | +| 1 | — | `data`を持たない | | 2〜16 | 4 | 256 | | 17〜32 | 5 | 342 | -| 33〜64 | 6 | 456 | +| 33〜64 | 6 | 410 | -この式は[実際の 26.2 ワールドの 167,088 個のコンテナすべてで一致を確認済み](../spec/31-paletted-container.md#21-実データで確認した組み合わせ)。 +この式は、[実際の26.2ワールドの167,088個のコンテナすべてで一致することを確認済みです](../spec/31-paletted-container.md#21-実データで確認した組み合わせ)。 -パレットの要素は元の NBT のまま保持しています。 -そのため触っていないブロックは、プロパティの並び順まで含めて -元どおりに書き戻されます。 +パレットの要素は元のNBTのまま保持しています。 +そのため触っていないブロックは、プロパティの並び順まで含めて元どおりに書き戻されます。 --- ## 7. 次に読むもの - [06. エラーと安全上限](06-errors-and-limits.md) -- [仕様 30: チャンク形式](../spec/30-chunk-format.md) -- [仕様 31: パレット付きコンテナ](../spec/31-paletted-container.md) +- [仕様30: チャンク形式](../spec/30-chunk-format.md) +- [仕様31: パレット付きコンテナ](../spec/31-paletted-container.md) diff --git a/docs/guide/06-errors-and-limits.md b/docs/guide/06-errors-and-limits.md index 083a07c..e136c50 100644 --- a/docs/guide/06-errors-and-limits.md +++ b/docs/guide/06-errors-and-limits.md @@ -1,7 +1,7 @@ # 06. エラーと安全上限 エラーの分類は全言語で完全に一致します。 -言語ごとに違うのは、それを例外で投げるか `Result` で返すかだけです。 +言語ごとに違うのは、エラーを例外で投げるか`Result`で返すかだけです。 --- @@ -11,22 +11,20 @@ |---|---|---| | `IO` | 入出力の失敗 | ファイルが無い、権限が無い | | `MALFORMED_DATA` | バイト列が仕様に反する | 未知のタグID、途中で入力が尽きた | -| `UNEXPECTED_TAG_TYPE` | 期待した型と違うタグを取り出した | `GetInt("x")` に String が入っていた | -| `UNSUPPORTED_FEATURE` | 仕様上は妥当だが本ビルドで扱えない | LZ4 依存を入れずに圧縮ID=4 を読んだ | -| `LIMIT_EXCEEDED` | 安全上限を超えた | ネスト深さ 512 超 | -| `INVALID_ARGUMENT` | 呼び出し側の引数が不正 | 座標がチャンク範囲外、`i8` に 300 を渡した | -| `UNSUPPORTED_DATA_VERSION` | 対象バージョン外のデータ | DataVersion 4903 以外を既定設定で書き戻そうとした | +| `UNEXPECTED_TAG_TYPE` | 期待した型と違うタグを取り出した | `GetInt("x")`にStringが入っていた | +| `UNSUPPORTED_FEATURE` | 仕様上は妥当だが本ビルドで扱えない | 圧縮ID=127のチャンクを`ReadChunk`で読んだ、圧縮ID=4(LZ4)を指定してチャンクを書こうとした | +| `LIMIT_EXCEEDED` | 安全上限を超えた | ネスト深さ512超 | +| `INVALID_ARGUMENT` | 呼び出し側の引数が不正 | 座標がチャンク範囲外、`i8`に300を渡した | +| `UNSUPPORTED_DATA_VERSION` | 扱える形式より古いデータ | DataVersion 4786未満のチャンクを既定設定で書き戻そうとした | ### 使い分けの考え方 -`MALFORMED_DATA` と `INVALID_ARGUMENT` の境目は -「誰の間違いか」で決まります。 +`MALFORMED_DATA`と`INVALID_ARGUMENT`の境目は「誰の間違いか」で決まります。 - ファイルの中身がおかしい → `MALFORMED_DATA` - 呼び出し側が渡した値がおかしい → `INVALID_ARGUMENT` -たとえば `BlockState.Parse("minecraft:stone[")` は -呼び出し側が渡した文字列の誤りなので `INVALID_ARGUMENT` になります。 +たとえば`BlockState.Parse("minecraft:stone[")`は、呼び出し側が渡した文字列の誤りなので`INVALID_ARGUMENT`になります。 --- @@ -34,11 +32,11 @@ | 言語 | 表現 | |---|---| -| C# | `SpringNbtException : Exception`(`Code` プロパティ、`InnerException` に原因) | -| Java | `SpringNbtException extends RuntimeException`(`code()`、`getCause()` に原因) | -| TypeScript | `SpringNbtError extends Error`(`code`、`cause` に原因) | -| Python | `SpringNbtError(Exception)`(`code` 属性、`__cause__` に原因) | -| Rust | `Result`(`Error` は `code()` と `message()` を持つ) | +| C# | `SpringNbtException : Exception`(`Code`プロパティ、`InnerException`に原因) | +| Java | `SpringNbtException extends RuntimeException`(`code()`、`getCause()`に原因) | +| TypeScript | `SpringNbtError extends Error`(`code`、`cause`に原因) | +| Python | `SpringNbtError(Exception)`(`code`属性、`__cause__`に原因) | +| Rust | `Result`(`Error`は`code()`と`message()`を持つ) | ```csharp try @@ -47,7 +45,7 @@ try } catch (SpringNbtException error) when (error.Code == ErrorCode.MalformedData) { - // 壊れたファイルとして扱う + // 破損したファイルとして扱う } ``` @@ -59,23 +57,22 @@ match read_file(path, &NbtReadOptions::default()) { } ``` -### Java は検査例外を使わない +### Javaは検査例外を使わない -`IOException` は `ErrorCode.IO` で包んで非検査例外として投げます。 +`IOException`は`ErrorCode.IO`で包んで非検査例外として投げます。 全言語でメソッドのシグネチャを揃えるためです([adr/0005](../adr/0005-unified-error-model.md))。 -原因の例外は `getCause()` から取れるので、情報は失われません。 +原因の例外は`getCause()`から取れるので、情報は失われません。 --- ## 3. 安全上限 -信用できない NBT ファイルを読むとき、 -悪意ある入力で無制限にメモリを確保させられないよう上限を設けています。 +信用できないNBTファイルを読むときに、悪意ある入力で無制限にメモリを確保させられないよう、上限を設けています。 | 項目 | 既定値 | 超過時 | |---|---|---| | ネスト深さ | 512 | `LIMIT_EXCEEDED` | -| 配列・リストの要素数 | 制限なし(宣言長 > 残り入力長 なら即エラー) | `MALFORMED_DATA` | +| 配列・リストの要素数 | 制限なし(宣言長 > 残り入力長なら即エラー) | `MALFORMED_DATA` | | 展開後の総バイト数 | 制限なし(設定可) | `LIMIT_EXCEEDED` | ```csharp @@ -88,51 +85,46 @@ NbtReadOptions options = new NbtReadOptions ### 宣言長は確保前に検証する -`TAG_Byte_Array` の長さに `0x7FFFFFFF` と書いてあっても、 -実際の残り入力がそれに足りなければ確保する前にエラーにします。 -「2GB 確保してから足りないと気づく」という動きはしません。 +`TAG_Byte_Array`の長さに`0x7FFFFFFF`と書いてあっても、実際の残り入力がそれに足りなければ、確保する前にエラーにします。 +「2GB確保してから足りないと気づく」という動きはしません。 --- -## 4. Rust のスタックに注意 +## 4. Rustのスタックに注意 -読み込み・書き出し・SNBT はいずれも再帰で実装しています。 -既定の深さ 512 はどの言語でも安全に扱えるが、Rust だけは事情があります。 +読み込み・書き出し・SNBTはいずれも再帰で実装しています。 +既定の深さ512はどの言語でも安全に扱えますが、Rustだけは事情があります。 -debug ビルドでは 1 段あたり約 8 KB を使います(release では約 1 KB)。 -512 段で 4 MB になり、既定のスレッドスタックによっては足りません。 +debugビルドでは1段あたり約8 KBを使います(releaseでは約1 KB)。 +512段で4 MBになり、既定のスレッドスタックによっては足りません。 深いデータを扱うなら、大きめのスタックを持つスレッドで走らせてください。 ```rust std::thread::Builder::new() .stack_size(32 * 1024 * 1024) - .spawn(|| { /* 深い NBT を読む */ })? + .spawn(|| { /* 深いNBTを読む */ })? .join() .unwrap(); ``` -Python も既定の再帰上限(1000)が深さ 512 に届かないが、 -これはライブラリ側で一時的に引き上げているので利用者の対応は不要です。 +Pythonも既定の再帰上限(1000)では深さ512に届きませんが、ライブラリ側で一時的に引き上げているので、利用者の対応は不要です。 -詳細は [spec/00 5.1](../spec/00-conventions.md#51-深さ上限と実行スタック)。 +詳細は[spec/00 5.1](../spec/00-conventions.md#51-深さ上限と実行スタック)。 --- -## 5. 壊れたデータに黙って合わせない +## 5. 破損したデータを推測で取り繕わない -壊れたデータを推測で修復することはしません。 +破損したデータを推測で修復することはしません。 -たとえばパレットの添字が範囲外を指しているとき、 -0 番目で代替すれば「読める」ようにはなります。 -しかしそれを書き戻すと、壊れたデータが -壊れていないように見える形で保存されてしまいます。 +たとえばパレットの添字が範囲外を指しているとき、0番目で代替すれば「読める」ようにはなります。 +しかし、それを書き戻すと、破損したデータが破損していないように見える形で保存されてしまいます。 -そこで明示的に `MALFORMED_DATA` を返します。 +そこで、明示的に`MALFORMED_DATA`を返します。 -例外は 1 つだけで、第三者ツールが書いた非正準な `BitStorage` を -救済するための `lenient_bit_storage` オプションがあります。 -これは明示的に有効にしたときだけ働きます。 +例外は1つだけで、第三者ツールが書いた非正準な`BitStorage`を救済するための`lenient_bit_storage`オプションがあります。 +このオプションは明示的に有効にしたときだけ働きます。 ```csharp ChunkReadOptions options = new ChunkReadOptions { LenientBitStorage = true }; @@ -143,4 +135,4 @@ ChunkReadOptions options = new ChunkReadOptions { LenientBitStorage = true }; ## 6. 次に読むもの - [07. バージョンポリシー](07-version-policy.md) -- [仕様 00: 共通規約](../spec/00-conventions.md) +- [仕様00: 共通規約](../spec/00-conventions.md) diff --git a/docs/guide/07-version-policy.md b/docs/guide/07-version-policy.md index 38a2258..55a79a9 100644 --- a/docs/guide/07-version-policy.md +++ b/docs/guide/07-version-policy.md @@ -1,10 +1,9 @@ # 07. バージョンポリシー -対応の判定はワールドの形式で行っています。Minecraft のバージョン番号は条件にしません -([adr/0003](../adr/0003-version-policy.md))。 +対応の判定はワールドの形式で行っています。Minecraftのバージョン番号は条件にしません([adr/0003](../adr/0003-version-policy.md))。 -いまの形式は **26.1(DataVersion 4786)** で入りました。 -次元とプレイヤーデータの置き場が変わり、`level.dat` の中身も分割されています。 +いまの形式は26.1(DataVersion 4786)で入りました。 +次元とプレイヤーデータの置き場が変わり、`level.dat`の中身も分割されています。 それ以降のバージョンなら、形式が変わらない限りそのまま読み書きできます。 ``` @@ -16,22 +15,22 @@ TARGET_DATA_VERSION = 4903 動作を確かめたバージョン(26.2 ## 1. 読み込み -`DataVersion` が **4786 以上なら何もしません。警告も出ません。** +`DataVersion`が**4786以上なら何もしません。警告も出ません。** 対象より新しいバージョンのワールドでも同じです。 形式が同じなら読めるので、知らせる必要がありません。 ```csharp -// DataVersion 5015 のチャンク(対象より新しい) +// DataVersion 5015のチャンク(対象より新しい) Chunk chunk = Chunk.FromNbt(nbt); // 警告なしで読める ``` -4786 未満、つまり形式そのものが違うワールドのときだけ、動作を選べます。 +4786未満、つまり形式そのものが違うワールドのときだけ、動作を選べます。 | 動作 | 意味 | |---|---| | `Warn`(既定) | 警告コールバックを呼んで続行する | -| `Error` | `UNSUPPORTED_DATA_VERSION` にする | +| `Error` | `UNSUPPORTED_DATA_VERSION`にする | | `Ignore` | 何もしない | ```csharp @@ -42,51 +41,47 @@ ChunkReadOptions options = new ChunkReadOptions }; ``` -構造そのものが想定と違えば(パレットが無い、`data` の長さが合わない、など)、 -バージョンに関わらず `MALFORMED_DATA` になります。黙って推測はしません。 +構造そのものが想定と違えば(パレットが無い、`data`の長さが合わない、など)、バージョンに関わらず`MALFORMED_DATA`になります。推測で読み進めることはしません。 -### NBT レイヤはバージョンに依存しない +### NBTレイヤはバージョンに依存しない -[レイヤ1(NBT)](01-nbt.md)と[レイヤ2(Anvil)](03-anvil-region.md)は -バージョン検査を一切しません。どのバージョンのファイルでも読み書きできます。 +[レイヤ1(NBT)](01-nbt.md)と[レイヤ2(Anvil)](03-anvil-region.md)はバージョン検査を一切しません。どのバージョンのファイルでも読み書きできます。 バージョンを見るのは[レイヤ3(World)](04-world-and-level-dat.md)だけです。 -古いワールドから NBT を取り出すだけなら、何の制約もなく使えます。 +古いワールドからNBTを取り出すだけなら、何の制約もなく使えます。 --- ## 2. 書き込み -**`DataVersion` は読んだ値をそのまま残します。** 書き換えません。 +**`DataVersion`は読んだ値をそのまま残します。** 書き換えません。 -書き換えてしまうと、そのワールドを開いたゲーム側が「このチャンクは新しい形式だ」と -判断してしまい、アップグレードの要否を誤ります。 +書き換えてしまうと、そのワールドを開いたゲーム側が「このチャンクは新しい形式だ」と判断してしまい、アップグレードの要否を誤ります。 ```csharp -// DataVersion 5015 のチャンクを書き戻す -chunk.ToNbt(); // 5015 のまま。許可も要らない +// DataVersion 5015のチャンクを書き戻す +chunk.ToNbt(); // 5015のままで、許可も要らない ``` -形式の違う古いチャンク(4786 未満)は、既定では書き戻せません。 +形式の違う古いチャンク(4786未満)は、既定では書き戻せません。 ```csharp -Chunk chunk = /* DataVersion 3700 のチャンク */; +Chunk chunk = /* DataVersion 3700のチャンク */; chunk.ToNbt(); // UNSUPPORTED_DATA_VERSION ``` -意図してやるなら明示的に許可します。このときも `DataVersion` は書き換えません。 +意図して書き戻すなら、明示的に許可します。このときも`DataVersion`は書き換えません。 ```csharp ChunkWriteOptions options = new ChunkWriteOptions { AllowForeignDataVersion = true }; -chunk.ToNbt(options); // 3700 のまま書き出される +chunk.ToNbt(options); // 3700のまま書き出される ``` -> 古い形式のワールドは、ディレクトリ構成も `level.dat` の中身も違います。 +> 古い形式のワールドは、ディレクトリ構成も`level.dat`の中身も違います。 > チャンク単体では書き戻せても、ワールド全体としては噛み合わないことがあります。 > 必ずバックアップを取ってから使ってください。 -安全な手順は「先に Minecraft で一度ワールドを開いて公式のアップグレードを済ませ、 -そのあとこのライブラリで編集する」です。 +安全な手順は「先にMinecraftで一度ワールドを開いて公式のアップグレードを済ませ、そのあとこのライブラリで編集する」です。 --- @@ -109,18 +104,17 @@ Console.WriteLine(SpringNbt.TargetDataVersion); // 4903 ## 4. 新しいバージョンが出たら -**形式が変わっていなければ、何もしなくてそのまま使えます。** +形式が変わっていなければ、何もしなくてもそのまま使えます。 ライブラリ側の更新も要りません。 形式が変わったときだけ、下限を上げて対応します。 -そのときは扱えなくなるワールドが出るので、 -[バージョン番号](../../README.md#バージョン番号)のいちばん上の桁も上がります。 +そのときは扱えなくなるワールドが出るので、[バージョン番号](../../README.md#バージョン番号)のいちばん上の桁も上がります。 -対応の手順は [リリース手順](../contributing/release.md) にまとめてあります。 +対応の手順は[リリース手順](../contributing/release.md)にまとめてあります。 --- ## 5. 次に読むもの - [adr/0003: ワールド形式で対応を判定する](../adr/0003-version-policy.md) -- [04. ワールドと level.dat](04-world-and-level-dat.md) +- [04. ワールドとlevel.dat](04-world-and-level-dat.md) diff --git a/docs/spec/00-conventions.md b/docs/spec/00-conventions.md index 22ad6d7..db0c81d 100644 --- a/docs/spec/00-conventions.md +++ b/docs/spec/00-conventions.md @@ -1,10 +1,10 @@ # 00. 共通規約 -本書は SpringNBTLibrary の全言語実装が従う共通規約を定める。 -各言語の実装は本書と本ディレクトリ配下の仕様書を**唯一の正**として実装される。 +本書はSpringNBTLibraryの全言語実装が従う共通規約を定める。 +各言語の実装は、本書と本ディレクトリ配下の仕様書を**唯一の拠り所**とする。 -- 対象: **26.1 で導入されたワールド形式**(DataVersion `4786` 以降) -- 検証: Minecraft **Java版 26.2**(DataVersion `4903` / Protocol `776`)の実データ +- 対象: 26.1で導入されたワールド形式(DataVersion `4786`以降) +- 検証: Minecraft Java版26.2(DataVersion `4903` / Protocol `776`)の実データ - それより古い形式のワールド改変は非サポート(→ [07 バージョンポリシー](../guide/07-version-policy.md)) --- @@ -13,22 +13,21 @@ | 用語 | 意味 | |---|---| -| タグ (Tag) | NBT の値の単位。13種の型を持つ | -| ルートタグ | ファイル/バイト列の最上位タグ。Java版では常に TAG_Compound | -| リージョン | 32×32 チャンクをまとめた `.mca` ファイル | -| セクタ (Sector) | リージョンファイル内の 4096 バイト単位のブロック | -| チャンク (Chunk) | 16×16 ブロック、Y方向はワールド全高の縦柱 | -| セクション (Section) | チャンクを Y方向に 16 ブロックずつ区切った 16×16×16 の立方体 | +| タグ (Tag) | NBTの値の単位。13種の型を持つ | +| ルートタグ | ファイル/バイト列の最上位タグ。Java版では常にTAG_Compound | +| リージョン | 32×32チャンクをまとめた`.mca`ファイル | +| セクタ (Sector) | リージョンファイル内の4096バイト単位のブロック | +| チャンク (Chunk) | 16×16ブロック、Y方向はワールド全高の縦柱 | +| セクション (Section) | チャンクをY方向に16ブロックずつ区切った16×16×16の立方体 | | パレット (Palette) | セクション内で使われるブロック状態/バイオームの一覧 | -| ビットストレージ (BitStorage) | パレット添字を 64bit 整数配列に詰めた表現 | +| ビットストレージ (BitStorage) | パレット添字を64bit整数配列に詰めた表現 | | 論理名 | 言語をまたいで同一の概念を指す名前。本仕様書での表記 | --- ## 2. バイト順と数値型 -NBT および Anvil のすべての多バイト数値は **ビッグエンディアン**、 -整数は **2の補数表現の符号付き**、浮動小数点は **IEEE 754** とする。 +NBTおよびAnvilのすべての多バイト数値はビッグエンディアン、整数は2の補数表現の符号付き、浮動小数点はIEEE 754とする。 論理型と各言語の対応は以下で固定する。 @@ -38,7 +37,7 @@ NBT および Anvil のすべての多バイト数値は **ビッグエンディ | `i16` | 16 | `short` | `short` | `number`(範囲検証あり) | `int`(範囲検証あり) | `i16` | | `i32` | 32 | `int` | `int` | `number`(範囲検証あり) | `int`(範囲検証あり) | `i32` | | `i64` | 64 | `long` | `long` | **`bigint`** | `int`(範囲検証あり) | `i64` | -| `f32` | 32 | `float` | `float` | `number`(f32 に丸めて保持) | `float`(f32 に丸めて保持) | `f32` | +| `f32` | 32 | `float` | `float` | `number`(f32に丸めて保持) | `float`(f32に丸めて保持) | `f32` | | `f64` | 64 | `double` | `double` | `number` | `float` | `f64` | 配列型は次のとおり。 @@ -51,21 +50,19 @@ NBT および Anvil のすべての多バイト数値は **ビッグエンディ **幅を持たない言語での注意点** -- **Python** は整数幅を持たないため、**構築時と書き込み時の双方で範囲検証**を行い、 - 範囲外なら `ErrorCode.INVALID_ARGUMENT` を送出する。 - `f32` は `struct.pack('>f', ...)` 相当の丸めを構築時に適用する -- **TypeScript** の `number` は倍精度で、`i32` までは正確だが `i64` を表せない。 - そのため **`i64` だけは `bigint` を使う**(→ [adr/0007](../adr/0007-typescript-bigint.md))。 - `i8` / `i16` / `i32` は `number` のまま範囲検証を行い、 - `f32` は `Math.fround` で丸める +- Pythonは整数幅を持たないため、**構築時と書き込み時の双方で範囲検証**を行い、範囲外なら`ErrorCode.INVALID_ARGUMENT`を送出する。 + `f32`は`struct.pack('>f', ...)`相当の丸めを構築時に適用する +- TypeScriptの`number`は倍精度で、`i32`までは正確だが`i64`を表せない。 + そのため、**`i64`だけは`bigint`を使う**(→ [adr/0007](../adr/0007-typescript-bigint.md))。 + `i8` / `i16` / `i32`は`number`のまま範囲検証を行い、`f32`は`Math.fround`で丸める --- ## 3. 命名変換規則 -論理名は **型名を `PascalCase`、メンバ名を `snake_case`、定数を `SCREAMING_SNAKE_CASE`** で表記する。 +論理名は**型名を`PascalCase`、メンバ名を`snake_case`、定数を`SCREAMING_SNAKE_CASE`** で表記する。 各言語実装はこの論理名から下表の規則で機械的に変換した名前を用いる。 -`spec/tools/check_docs_sync.py` はこの規則を使って全言語の公開APIを論理名へ写し、突き合わせる。 +`spec/tools/check_docs_sync.py`はこの規則を使って全言語の公開APIを論理名へ写し、比べる。 | 種別 | 論理名 | C# | Java | TypeScript | Python | Rust | |---|---|---|---|---|---|---| @@ -77,72 +74,69 @@ NBT および Anvil のすべての多バイト数値は **ビッグエンディ | 定数 | `TARGET_DATA_VERSION` | `TargetDataVersion` | `TARGET_DATA_VERSION` | `TARGET_DATA_VERSION` | `TARGET_DATA_VERSION` | `TARGET_DATA_VERSION` | | モジュール | `nbt` | `SpringNBTLibrary.Nbt` | `...springnbt.nbt` | `spring-nbt-library/nbt` | `spring_nbt_library.nbt` | `spring_nbt_library::nbt` | -真偽値を返す取得子は論理名を `is_*` / `has_*` で始める。 -C# は `Is*` / `Has*` のプロパティ、Java は `is*()` / `has*()` のメソッドとする。 +真偽値を返す取得子は、論理名を`is_*` / `has_*`で始める。 +C#は`Is*` / `Has*`のプロパティ、Javaは`is*()` / `has*()`のメソッドとする。 --- ## 4. エラー分類 -エラーは全言語で**同一の `ErrorCode`** を持つ単一の例外/エラー型で表す。 -コード集合が一致していることは docs-sync で検証される。 +エラーは全言語で**同一の`ErrorCode`** を持つ単一の例外/エラー型で表す。 +コード集合が一致していることはdocs-syncで検証される。 | `ErrorCode` | 意味 | 典型例 | |---|---|---| | `IO` | 下位の入出力失敗 | ファイルが無い、読み取り権限が無い | | `MALFORMED_DATA` | バイト列が仕様に反する | 未知のタグID、途中で入力が尽きた | -| `UNEXPECTED_TAG_TYPE` | 期待した型と違うタグを取り出した | `get_int("x")` に String が入っていた | -| `UNSUPPORTED_FEATURE` | 仕様上は妥当だが本ビルドで扱えない | LZ4 依存を入れずに圧縮ID=4 を読んだ | -| `LIMIT_EXCEEDED` | 安全上限を超えた | ネスト深さ 512 超、宣言長が入力長を超える | -| `INVALID_ARGUMENT` | 呼び出し側の引数が不正 | 座標がチャンク範囲外、`i8` に 300 を渡した | -| `UNSUPPORTED_DATA_VERSION` | 対象バージョン外のデータ | DataVersion 4903 以外を既定設定で書き戻そうとした | +| `UNEXPECTED_TAG_TYPE` | 期待した型と違うタグを取り出した | `get_int("x")`にStringが入っていた | +| `UNSUPPORTED_FEATURE` | 仕様上は妥当だが本ビルドで扱えない | 圧縮ID=127のチャンクを`read_chunk`で読んだ、圧縮ID=4(LZ4)を指定してチャンクを書こうとした、SNBTの`\N{...}`をパースした | +| `LIMIT_EXCEEDED` | 安全上限を超えた | ネスト深さ512超、展開後のサイズが`max_decompressed_size`を超えた | +| `INVALID_ARGUMENT` | 呼び出し側の引数が不正 | 座標がチャンク範囲外、`i8`に300を渡した | +| `UNSUPPORTED_DATA_VERSION` | 扱える形式より古いデータ | DataVersion 4786未満のチャンクを既定設定で書き戻そうとした | -各言語での表現: +各言語での表現は次のとおり。 | 言語 | 表現 | |---|---| -| C# | `SpringNbtException : Exception`(`ErrorCode Code` プロパティ、`InnerException` に原因) | -| Java | `SpringNbtException extends RuntimeException`(`ErrorCode code()`、`getCause()` に原因) | -| TypeScript | `SpringNbtError extends Error`(`code` プロパティ、`cause` に原因) | -| Python | `SpringNbtError(Exception)`(`code` 属性、`__cause__` に原因) | -| Rust | `pub struct Error { code: ErrorCode, message: String, source: Option<...> }` を `Result` で返す | +| C# | `SpringNbtException : Exception`(`ErrorCode Code`プロパティ、`InnerException`に原因) | +| Java | `SpringNbtException extends RuntimeException`(`ErrorCode code()`、`getCause()`に原因) | +| TypeScript | `SpringNbtError extends Error`(`code`プロパティ、`cause`に原因) | +| Python | `SpringNbtError(Exception)`(`code`属性、`__cause__`に原因) | +| Rust | `pub struct Error { code: ErrorCode, message: String, source: Option<...> }`を`Result`で返す | -`IO` は下位の入出力例外を必ず原因として保持し、握り潰さない。 -Java は検査例外を使わず、`IOException` を `ErrorCode.IO` でラップして送出する -(全言語でシグネチャを揃えるため。→ [adr/0005](../adr/0005-unified-error-model.md))。 +`IO`は下位の入出力例外を必ず原因として保持し、握り潰さない。 +Javaは検査例外を使わず、`IOException`を`ErrorCode.IO`でラップして送出する(全言語でシグネチャを揃えるため。→ [adr/0005](../adr/0005-unified-error-model.md))。 --- ## 5. 安全上限(既定値) 悪意ある/破損した入力で無制限にメモリを確保しないため、読み込み時に上限を設ける。 -全言語で同じ既定値を用い、`ReadOptions` で変更できる。 +全言語で同じ既定値を用い、`NbtReadOptions`で変更できる。 | 項目 | 既定値 | 超過時 | |---|---|---| | ネスト深さ | 512 | `LIMIT_EXCEEDED` | -| 配列・リストの要素数 | 制限なし(ただし宣言長 > 残り入力長 なら即エラー) | `MALFORMED_DATA` | -| 展開後の総バイト数 | 制限なし(`max_decompressed_size` で設定可) | `LIMIT_EXCEEDED` | +| 配列・リストの要素数 | 制限なし(ただし宣言長 > 残り入力長なら即エラー) | `MALFORMED_DATA` | +| 展開後の総バイト数 | 制限なし(`max_decompressed_size`で設定可) | `LIMIT_EXCEEDED` | -**宣言長の先行検証**: `TAG_Byte_Array` などの長さフィールドを読んだ時点で、 -残り入力バイト数から必要バイト数を満たせないと分かる場合は、確保する前にエラーとする。 +**宣言長の先行検証**: `TAG_Byte_Array`などの長さフィールドを読んだ時点で、残り入力バイト数が必要バイト数に満たないと分かる場合は、確保する前にエラーとする。 ### 5.1 深さ上限と実行スタック -読み込み・書き出し・SNBT の処理はいずれも**再帰**で実装されている。 -そのため深さ上限は「実行時スタックが尽きる前に必ず `LIMIT_EXCEEDED` を返す」値でなければならない。 -既定の 512 はどの言語でも通常の実行環境で安全に扱えるが、言語ごとに事情が違う。 +読み込み・書き出し・SNBTの処理は、いずれも再帰で実装されている。 +そのため深さ上限は、「実行時スタックが尽きる前に必ず`LIMIT_EXCEEDED`を返す」値でなければならない。 +既定の512はどの言語でも通常の実行環境で安全に扱えるが、言語ごとに事情が違う。 | 言語 | 事情 | 対応 | |---|---|---| -| C# / Java | 既定のスタック(1MiB 前後)で 512 段を扱える | 対応不要 | -| TypeScript | V8 の既定コールスタックで 512 段を扱える | 対応不要 | -| Python | **既定の再帰上限 1000 が 512 段に届かない** | 読み書きの間だけ再帰上限を一時的に引き上げる | -| Rust | release ビルドは 1 段あたり約 1 KB。**debug ビルドは約 8 KB** | 既定の 512 は release で安全。debug ビルドや小さいスレッドスタックでは注意 | +| C# / Java | 既定のスタック(1MiB前後)で512段を扱える | 対応不要 | +| TypeScript | V8の既定コールスタックで512段を扱える | 対応不要 | +| Python | **既定の再帰上限1000では512段に届かない** | 読み書きの間だけ再帰上限を一時的に引き上げる | +| Rust | releaseビルドは1段あたり約1 KB。**debugビルドは約8 KB** | 既定の512はreleaseで安全。debugビルドや小さいスレッドスタックでは注意 | -**`max_depth` を既定より大きくする場合**、Rust では実行スレッドのスタックを増やすこと -(`std::thread::Builder::stack_size(...)`、または環境変数 `RUST_MIN_STACK`)。 -Minecraft の実データのネストは 10 段程度なので、既定値を触る必要は通常ない。 +**`max_depth`を既定より大きくする場合**、Rustでは実行スレッドのスタックを増やすこと(`std::thread::Builder::stack_size(...)`、または環境変数`RUST_MIN_STACK`)。 +Minecraftの実データのネストは10段程度なので、既定値を触る必要は通常ない。 --- @@ -159,13 +153,13 @@ Minecraft の実データのネストは 10 段程度なので、既定値を触 } ``` -タグの表現: +タグの表現は次のとおり。 | 型 | JSON | |---|---| -| `byte` `short` `int` | `{"type":"int","value":42}`(JSON 数値) | +| `byte` `short` `int` | `{"type":"int","value":42}`(JSON数値) | | `long` | `{"type":"long","value":"-9223372036854775808"}`(**10進文字列**) | -| `float` | `{"type":"float","value":"0x3f800000"}`(**IEEE754 ビットパターンの16進**) | +| `float` | `{"type":"float","value":"0x3f800000"}`(**IEEE754ビットパターンの16進**) | | `double` | `{"type":"double","value":"0x3ff0000000000000"}`(同上) | | `byte_array` `int_array` | `{"type":"int_array","value":[1,2,3]}` | | `long_array` | `{"type":"long_array","value":["1","2"]}`(10進文字列の配列) | @@ -173,22 +167,22 @@ Minecraft の実データのネストは 10 段程度なので、既定値を触 | `list` | `{"type":"list","element_type":"int","value":[ ... ]}` | | `compound` | `{"type":"compound","value":[["key",{...}],["key2",{...}]]}` | -設計意図: +設計意図は次のとおり。 -- **float/double はビットパターン**で表す。10進表記は言語ごとの丸め・桁数差で不一致になるため -- **long は文字列**で表す。JSON 数値は倍精度に落ちる処理系があるため -- **compound はキーと値の組の配列**で表す。JSON オブジェクトだと挿入順の保持が処理系依存になるため -- **string は `mutf8` を必ず併記**する。孤立サロゲートなど UTF-8 に写せない値を厳密に比較するため +- **float/doubleはビットパターン**で表す。10進表記は言語ごとの丸め・桁数差で不一致になるため +- **longは文字列**で表す。JSON数値を倍精度に落とす処理系があるため +- **compoundはキーと値の組の配列**で表す。JSONオブジェクトだと挿入順の保持が処理系依存になるため +- **stringは`mutf8`を必ず併記**する。孤立サロゲートなどUTF-8に写せない値を厳密に比較するため -詳細と検証手順は [90 適合性](90-conformance.md) を参照。 +詳細と検証手順は[90 適合性](90-conformance.md)を参照。 --- ## 7. 関連文書 -- [10 NBT バイナリ形式](10-nbt-binary.md) +- [10 NBTバイナリ形式](10-nbt-binary.md) - [11 SNBT](11-snbt.md) -- [20 Anvil リージョン形式](20-anvil-region.md) +- [20 Anvilリージョン形式](20-anvil-region.md) - [30 チャンク形式](30-chunk-format.md) - [31 パレット付きコンテナ](31-paletted-container.md) - [40 ワールドのディレクトリ構成](40-world-layout.md) diff --git a/docs/spec/10-nbt-binary.md b/docs/spec/10-nbt-binary.md index 93d4a46..5837659 100644 --- a/docs/spec/10-nbt-binary.md +++ b/docs/spec/10-nbt-binary.md @@ -1,7 +1,7 @@ -# 10. NBT バイナリ形式 +# 10. NBTバイナリ形式 -NBT (Named Binary Tag) の読み書き仕様。**このレイヤは Minecraft のバージョンに一切依存しない。** -バージョン検査は [30 チャンク形式](30-chunk-format.md) 以降でのみ行う。 +NBT (Named Binary Tag)の読み書き仕様。**このレイヤはMinecraftのバージョンに一切依存しない。** +バージョン検査は[30 チャンク形式](30-chunk-format.md)以降でのみ行う。 前提: [00 共通規約](00-conventions.md)(ビッグエンディアン、エラー分類、安全上限) @@ -9,27 +9,32 @@ NBT (Named Binary Tag) の読み書き仕様。**このレイヤは Minecraft ## 1. タグ型 -| ID | 論理型名 | 型名 (Rust/C#/Java/Python) | ペイロード | +| ID | 論理型名 | 型名 (C#/Java/TypeScript/Python) | ペイロード | |---:|---|---|---| -| 0 | `End` | `NbtEnd` | なし | -| 1 | `Byte` | `NbtByte` | `i8` 1 バイト | -| 2 | `Short` | `NbtShort` | `i16` 2 バイト | -| 3 | `Int` | `NbtInt` | `i32` 4 バイト | -| 4 | `Long` | `NbtLong` | `i64` 8 バイト | -| 5 | `Float` | `NbtFloat` | `f32` 4 バイト (IEEE 754 binary32) | -| 6 | `Double` | `NbtDouble` | `f64` 8 バイト (IEEE 754 binary64) | -| 7 | `ByteArray` | `NbtByteArray` | `i32` 長さ + `i8` × 長さ | -| 8 | `String` | `NbtString` | `u16` バイト長 + MUTF-8 バイト列 | -| 9 | `List` | `NbtList` | `u8` 要素型ID + `i32` 個数 + ペイロード × 個数 | +| 0 | `End` | — | なし | +| 1 | `Byte` | `NbtByte` | `i8` 1バイト | +| 2 | `Short` | `NbtShort` | `i16` 2バイト | +| 3 | `Int` | `NbtInt` | `i32` 4バイト | +| 4 | `Long` | `NbtLong` | `i64` 8バイト | +| 5 | `Float` | `NbtFloat` | `f32` 4バイト (IEEE 754 binary32) | +| 6 | `Double` | `NbtDouble` | `f64` 8バイト (IEEE 754 binary64) | +| 7 | `ByteArray` | `NbtByteArray` | `i32`長さ + `i8` × 長さ | +| 8 | `String` | `NbtString` | `u16`バイト長 + MUTF-8バイト列 | +| 9 | `List` | `NbtList` | `u8`要素型ID + `i32`個数 + ペイロード × 個数 | | 10 | `Compound` | `NbtCompound` | 名前付きタグの並び + `TAG_End` (0x00) | -| 11 | `IntArray` | `NbtIntArray` | `i32` 長さ + `i32` × 長さ | -| 12 | `LongArray` | `NbtLongArray` | `i32` 長さ + `i64` × 長さ | +| 11 | `IntArray` | `NbtIntArray` | `i32`長さ + `i32` × 長さ | +| 12 | `LongArray` | `NbtLongArray` | `i32`長さ + `i64` × 長さ | -未知のタグID(13以上、または負値)を読んだ場合は `MALFORMED_DATA`。 +`End`は値として持てないため、対応する型は無い。 +Rustは型を分けず、`NbtTag`列挙の`NbtTag::Byte(i8)`のようなバリアントで表す。 +独立した型を持つのは`NbtString` / `NbtList` / `NbtCompound`だけである。 + +未知のタグID(13以上、または負値)を読んだ場合は`MALFORMED_DATA`。 ### 1.1 名前付きタグ -`TAG_Compound` の中身は「名前付きタグ」の並びで、各要素は次の3つ組で表される。 +`TAG_Compound`の中身は「名前付きタグ」の並びで、各要素はタグID・名前・ペイロードの3つからなる。 +名前はバイト長とMUTF-8のバイト列で表すので、並びは次の4項目になる。 ``` u8 タグID @@ -38,30 +43,30 @@ u16 名前のバイト長 -- タグIDが 0 (TAG_End) の場合は名前ご ... ペイロード ``` -`TAG_End` は 1 バイト `0x00` のみで、名前もペイロードも持たない。 +`TAG_End`は1バイトの`0x00`のみで、名前もペイロードも持たない。 ### 1.2 長さフィールドの検証 -`ByteArray` / `IntArray` / `LongArray` / `List` の長さは `i32`(符号付き)である。 +`ByteArray` / `IntArray` / `LongArray` / `List`の長さは`i32`(符号付き)である。 -- **負値** → `MALFORMED_DATA` -- 長さ × 要素バイト数が**残り入力バイト数を超える** → 確保する前に `MALFORMED_DATA` +- 負値 → `MALFORMED_DATA` +- 長さ × 要素バイト数が残り入力バイト数を超える → 確保する前に`MALFORMED_DATA` -この先行検証により、`0x7FFFFFFF` を宣言しただけの数バイトの入力でメモリを枯渇させられない。 +この先行検証により、`0x7FFFFFFF`を宣言しただけの数バイトの入力でメモリを枯渇させられない。 --- ## 2. 文字列: MUTF-8 -NBT の文字列は UTF-8 ではなく **Modified UTF-8 (MUTF-8)** である。 -Java の `DataInput.readUTF` / `DataOutput.writeUTF` と同じ符号化で、標準 UTF-8 と次の2点が異なる。 +NBTの文字列はUTF-8ではなく**Modified UTF-8 (MUTF-8)** である。 +Javaの`DataInput.readUTF` / `DataOutput.writeUTF`と同じ符号化で、標準UTF-8と次の2点が異なる。 -| 対象 | 標準 UTF-8 | MUTF-8 | +| 対象 | 標準UTF-8 | MUTF-8 | |---|---|---| | `U+0000` | `00`(1バイト) | `C0 80`(2バイト) | -| `U+10000`〜`U+10FFFF` | 4バイト1シーケンス | サロゲートペアに分解し **3バイト × 2**(CESU-8) | +| `U+10000`〜`U+10FFFF` | 4バイト1シーケンス | サロゲートペアに分解し、3バイト × 2(CESU-8) | -符号化規則: +符号化規則は次のとおり。 ``` U+0001 .. U+007F -> 0xxxxxxx (1 バイト) @@ -71,27 +76,25 @@ U+0800 .. U+FFFF -> 1110xxxx 10xxxxxx 10xxxxxx (3 バイト) U+10000 .. -> 上位/下位サロゲートへ分解し、各々を 3 バイトで符号化 ``` -長さフィールドは**符号化後のバイト数**を表す `u16` であり、最大 65535 バイト。 -文字数ではないことに注意する。書き込み時に 65535 を超えたら `INVALID_ARGUMENT`。 +長さフィールドは**符号化後のバイト数**を表す`u16`であり、最大65535バイト。 +文字数ではないことに注意する。書き込み時に65535を超えたら`INVALID_ARGUMENT`。 ### 2.1 復号時の扱い -- 孤立サロゲート(対にならない `U+D800`〜`U+DFFF`)は**そのまま保持する**。 - Minecraft の実データには通常現れないが、破損データを黙って変換すると - ラウンドトリップでバイトが変わってしまうため、失われないようにする。 -- 冗長な符号化(例: `U+0041` を 2 バイトで表現)は `MALFORMED_DATA` とする。 - ただし `U+0000` の `C0 80` は MUTF-8 として正当なので受理する。 -- 継続バイトが `10xxxxxx` でない、途中で入力が尽きた → `MALFORMED_DATA` +- 孤立サロゲート(対にならない`U+D800`〜`U+DFFF`)は**そのまま保持する**。 + Minecraftの実データには通常現れないが、破損データが知らないうちに変換されるとラウンドトリップでバイトが変わってしまうため、失われないようにする。 +- 冗長な符号化(例: `U+0041`を2バイトで表現)は`MALFORMED_DATA`とする。 + ただし`U+0000`の`C0 80`はMUTF-8として正当なので受理する。 +- 継続バイトが`10xxxxxx`でない、途中で入力が尽きた → `MALFORMED_DATA` -### 2.2 Compound のキー +### 2.2 Compoundのキー -`TAG_Compound` のキーも MUTF-8 の文字列だが、**値と違い孤立サロゲートを許さない**。 -UTF-8 へ写せないキーを読んだ場合は `MALFORMED_DATA` とする。 +`TAG_Compound`のキーもMUTF-8の文字列だが、**値と違い孤立サロゲートを許さない**。 +UTF-8へ写せないキーを読んだ場合は`MALFORMED_DATA`とする。 +APIで孤立サロゲートを含むキーを設定しようとした場合は`INVALID_ARGUMENT`とする(書き出すと読み戻せなくなるため)。 -Minecraft が書き出すキーは実際には ASCII の識別子のみであり、 -そこに孤立サロゲートが現れるのはデータ破損を意味する。 -またキーをすべての言語で「ただの文字列」として扱えることが、 -マップ型をそのまま使えるという実装上の大きな利点につながる。 +Minecraftが書き出すキーは実際にはASCIIの識別子のみであり、そこに孤立サロゲートが現れるのはデータ破損を意味する。 +また、キーをすべての言語で「ただの文字列」として扱えると、マップ型をそのまま使えるという実装上の大きな利点がある。 ### 2.3 各言語での保持方法 @@ -101,34 +104,34 @@ Minecraft が書き出すキーは実際には ASCII の識別子のみであり | Java | `String`(UTF-16) | そのまま保持できる | | TypeScript | `string`(UTF-16) | そのまま保持できる | | Python | `str`(コードポイント列) | サロゲートを単独の文字として保持できる | -| Rust | `String`(UTF-8) | UTF-8 に写せないため、専用の表現へ退避する | +| Rust | `String`(UTF-8) | UTF-8に写せないため、専用の表現へ退避する | -**Rust だけは UTF-8 の不変条件がある**ため、`NbtString` を +RustだけはUTF-8の不変条件があるため、`NbtString`を次の2形態の型として定義する。 ``` enum NbtString { - Text(String), // UTF-8 として表せる通常の文字列。実データはほぼすべてこちら - Surrogates(Vec), // UTF-8 に写せない UTF-16 コード単位の列 + Text(String), // UTF-8として表せる通常の文字列。実データはほぼすべてこちら + Surrogates(Vec), // UTF-8に写せないUTF-16コード単位の列 } ``` -という 2 形態の型として定義する。通常の入力では常に `Text` になる。 +通常の入力では常に`Text`になる。 --- ## 3. ファイル形式とルートタグ -`NbtFormat` は2種類。 +`NbtFormat`は2種類。 | 値 | 用途 | ルートの構造 | |---|---|---| -| `Java` | `level.dat`、チャンク、`.nbt` ファイル全般 | `u8` タグID(=10) + `u16` 名前長 + 名前 + Compound ペイロード | -| `Network` | 1.20.2 以降のネットワークプロトコル | `u8` タグID(=10) + Compound ペイロード(**名前なし**) | +| `Java` | `level.dat`、チャンク、`.nbt`ファイル全般 | `u8`タグID(=10) + `u16`名前長 + 名前 + Compoundペイロード | +| `Network` | 1.20.2以降のネットワークプロトコル | `u8`タグID(=10) + Compoundペイロード(**名前なし**) | -`Java` 形式のルート名は通常空文字列だが、値は保持し、書き込み時にそのまま出力する。 -ルートタグが `TAG_Compound` 以外だった場合は `MALFORMED_DATA`。 +`Java`形式のルート名は通常空文字列だが、値は保持し、書き込み時にそのまま出力する。 +ルートタグが`TAG_Compound`以外だった場合は`MALFORMED_DATA`。 -読み書きの入口は次の論理APIに統一する。 +読み書きに使うAPIは、次の論理APIに統一する。 ``` NbtIo.read_file(path, options) -> NamedTag @@ -139,14 +142,14 @@ NbtIo.write_bytes(named_tag, options) -> bytes NamedTag { name: String, tag: NbtCompound } ``` -`read_bytes` は入力を 1 つの NBT として読む。 -ルートタグの後にバイトが残っていたら `MALFORMED_DATA` とする。 -読み違えを黙って見逃さないためである。 +`read_bytes`は入力を1つのNBTとして読む。 +ルートタグの後にバイトが残っていたら`MALFORMED_DATA`とする。 +読み違えを見逃さず、エラーとして知らせるためである。 -### 3.1 連なった NBT を読む +### 3.1 連なったNBTを読む -1 つのバイト列に NBT が複数並んでいることがある。 -この形は `read_bytes` では読めないので、専用の入口を用意する。 +1つのバイト列にNBTが複数並んでいることがある。 +この形は`read_bytes`では読めないので、専用のAPIを用意する。 ``` NbtIo.read_bytes_at(bytes, offset, options) -> NbtReadResult @@ -155,9 +158,8 @@ NbtIo.read_bytes_all(bytes, options) -> NamedTag の一覧 NbtReadResult { tag: NamedTag, end: 整数 } ``` -`read_bytes_at` は `offset` から NBT を 1 つ読み、 -読み終わった直後の位置を `end` に入れて返す。 -`end` を次の `offset` として渡せば、順に読み進められる。 +`read_bytes_at`は`offset`からNBTを1つ読み、読み終わった直後の位置を`end`に入れて返す。 +`end`を次の`offset`として渡せば、順に読み進められる。 ``` offset = 0 @@ -167,42 +169,41 @@ while offset < len(bytes): offset = result.end ``` -`read_bytes_all` は入力を使い切るまで読み、読んだ順に並べて返す。 -空のバイト列は「0 個」であってエラーではない。 +`read_bytes_all`は入力を使い切るまで読み、読んだ順に並べて返す。 +空のバイト列は「0個」であってエラーではない。 | 条件 | 結果 | |---|---| -| `offset` が負、または入力長を超える | `INVALID_ARGUMENT` | -| `read_bytes_at` に圧縮を指定した | `INVALID_ARGUMENT` | +| `offset`が負、または入力長を超える | `INVALID_ARGUMENT` | +| `read_bytes_at`に圧縮を指定した | `INVALID_ARGUMENT` | | 途中で入力が尽きた | `MALFORMED_DATA` | -**`read_bytes_at` の位置は、渡したバイト列そのものを指す。** +**`read_bytes_at`の位置は、渡したバイト列そのものを指す。** 展開すると位置が変わってしまうので、圧縮されたデータは扱えない。 -`read_bytes_all` は位置を返さないので、 -入力全体に 1 回かかった圧縮であれば展開してから読む。 +`read_bytes_all`は位置を返さないので、入力全体に1回かかった圧縮であれば展開してから読む。 --- ## 4. 圧縮 -`Compression` は4値。 +`Compression`は4値。 | 値 | 内容 | 判定に使う先頭バイト | |---|---|---| -| `None` | 無圧縮 | `0x0A`(TAG_Compound のID) | +| `None` | 無圧縮 | `0x0A`(TAG_CompoundのID) | | `Gzip` | RFC 1952 | `1F 8B` | -| `Zlib` | RFC 1950 (zlib ラッパ付き deflate) | `78`(`78 01` / `78 9C` / `78 DA` など) | +| `Zlib` | RFC 1950 (zlibラッパ付きdeflate) | `78`(`78 01` / `78 9C` / `78 DA` など) | | `Auto` | 読み込み時のみ指定可。上記から自動判定 | — | -`Auto` は先頭 2 バイトで判定する。 +`Auto`は先頭2バイトで判定する。 1. `1F 8B` → `Gzip` -2. 先頭バイトの下位4bit が `8`(=deflate 圧縮法)かつ 先頭2バイトを `u16` ビッグエンディアンとして読んだ値が 31 の倍数 → `Zlib` -3. 先頭バイトが `0x0A` → `None` +2. 先頭バイトの下位4bitが`8`(=deflate圧縮法)、かつ先頭2バイトを`u16`ビッグエンディアンとして読んだ値が31の倍数 → `Zlib` +3. 先頭バイトが`0x0A` → `None` 4. いずれでもない → `MALFORMED_DATA` -書き込み時に `Auto` を指定した場合は `INVALID_ARGUMENT`。 -`level.dat` の既定は `Gzip`、リージョン内チャンクの既定は `Zlib`(→ [20](20-anvil-region.md))。 +書き込み時に`Auto`を指定した場合は`INVALID_ARGUMENT`。 +`level.dat`の既定は`Gzip`、リージョン内チャンクの既定は`Zlib`(→ [20](20-anvil-region.md))。 --- @@ -210,27 +211,26 @@ while offset < len(bytes): ラウンドトリップ検証を成立させるため、書き出しは**一意**でなければならない。 -- `NbtCompound` は**挿入順**で書き出す。ソートしない -- `NbtList` の要素型IDは、要素が1つ以上あればその型、空なら `End`(0) を書く -- `f32` / `f64` は NaN も含めビットパターンをそのまま書く(正規化しない) +- `NbtCompound`は**挿入順**で書き出す。ソートしない +- `NbtList`の要素型IDは、リストが持つ要素型をそのまま書く。要素が1つ以上あればその型、要素型が決まっていない空リストなら`End`(0)になる +- `f32` / `f64`はNaNも含めビットパターンをそのまま書く(正規化しない) - 圧縮は結果バイト列が実装依存になるため、**ラウンドトリップ検証は展開後のバイト列で行う** -空 `NbtList` の要素型については、読み込んだ値が `End` 以外(例: 実装によっては `Byte`) -だった場合も**読んだ値をそのまま保持して書き戻す**。Minecraft 側が生成した値を壊さないため。 +空の`NbtList`の要素型については、読み込んだ値が`End`以外(例: 実装によっては`Byte`)だった場合も**読んだ値をそのまま保持して書き戻す**。Minecraft側が生成した値を損なわないため。 --- ## 6. 読み書きオプション ``` -ReadOptions { +NbtReadOptions { format: NbtFormat = Java compression: Compression = Auto max_depth: i32 = 512 - max_decompressed_size: i64 = -1 // -1 は無制限 + max_decompressed_size: i64 = -1 // -1は無制限 } -WriteOptions { +NbtWriteOptions { format: NbtFormat = Java compression: Compression = Gzip } @@ -242,63 +242,59 @@ WriteOptions { ### 7.1 `NbtCompound` -- **挿入順を保持**するマップ(Rust: `IndexMap` 相当の自前実装 / C#: `OrderedDictionary` 相当 / Java: `LinkedHashMap` / Python: `dict`) +- **挿入順を保持**するマップ(Rust: `IndexMap`相当の自前実装 / C#: `OrderedDictionary`相当 / Java: `LinkedHashMap` / TypeScript: `Map` / Python: `dict`) - 同名キーの再設定は**位置を維持したまま値を置き換える** -- 型付き取得子 `get_int(key)` 等は、キーが無い場合と型が違う場合を区別する - - キーが無い → `None` / `null` を返す取得子(`opt_int`)と、エラーにする取得子(`get_int`)の両方を用意する - - 型が違う → 常に `UNEXPECTED_TAG_TYPE` -- 型付き設定子 `set_int(key, value)` 等を、取得子と対で用意する - - `set(key, NbtInt(42))` と書かずに済ませるための糖衣であり、動きは `set` と同じ - - 真偽値は専用型が無いので、`set_bool` は `TAG_Byte` の 0 / 1 として書く - - 対象は `byte` `short` `int` `long` `float` `double` `bool` `string` - `byte_array` `int_array` `long_array` の 11 種 +- 型付き取得子`get_int(key)`等は、キーが無い場合と型が違う場合を区別する + - キーが無い → `None` / `null`を返す取得子(`opt_int`)と、エラーにする取得子(`get_int`)の両方を用意する + - 型が違う → 常に`UNEXPECTED_TAG_TYPE` +- 型付き設定子`set_int(key, value)`等を、取得子と対で用意する + - `set(key, NbtInt(42))`と書かずに済ませるための糖衣であり、動きは`set`と同じ + - 真偽値は専用型が無いので、`set_bool`は`TAG_Byte`の0 / 1として書く + - 対象は`byte` `short` `int` `long` `float` `double` `bool` `string` `byte_array` `int_array` `long_array`の11種 ### 7.2 `NbtList` -- 要素型は1つに強制される。異なる型を追加しようとしたら `UNEXPECTED_TAG_TYPE` -- 空リストの要素型は `End` +- 要素型は1つに強制される。異なる型を追加しようとしたら`UNEXPECTED_TAG_TYPE` +- 空リストの要素型は`End` - 空リストに最初の要素を追加すると、要素型がその型に確定する - 全要素を削除しても、確定済みの要素型は**維持する**(読み書きの往復で型が消えないように) ### 7.3 等値比較と複製 どのタグ型も、値としての等値比較と深い複製を提供する。 -言語ごとの綴りは [API 対応表](../api/nbt.md) を参照。 +言語ごとの綴りは[API対応表](../api/nbt.md)を参照。 等値比較の規則は全言語で同じでなければならない。 | 対象 | 規則 | |---|---| | タグ型が違うもの同士 | 値が同じでも**等しくない**(`NbtInt(1)` ≠ `NbtShort(1)`) | -| `Float` / `Double` | 値ではなく**ビットパターン**で比べる。`NaN` 同士は等しく、`+0.0` と `-0.0` は等しくない | +| `Float` / `Double` | 値ではなく**ビットパターン**で比べる。`NaN`同士は等しく、`+0.0`と`-0.0`は等しくない | | 配列 | 長さと、同じ位置の要素がすべて一致すること | | `NbtList` | 要素型が一致し、同じ位置の要素がすべて一致すること | | `NbtCompound` | キーと値が**挿入順も含めて**一致すること | -`Float` / `Double` をビットパターンで比べるのは、 -正準書き出し(5章)がビットパターンをそのまま書くためである。 -値で比べると「等しいのに書き出すと違うバイト列になる」組み合わせが生じる。 +`Float` / `Double`をビットパターンで比べるのは、正準書き出し(5章)がビットパターンをそのまま書くためである。 +値で比べると、「等しいのに書き出すと違うバイト列になる」組み合わせが生じる。 -`NbtCompound` の並び順を等価性に含めるのも同じ理由で、 -順序が違えば書き出したバイト列も違う。 +`NbtCompound`の並び順を等価性に含めるのも同じ理由で、順序が違えば書き出したバイト列も違う。 複製は**深いコピー**とする。複製したタグをいくら書き換えても元に影響しない。 ### 7.4 人が読むための表現 -どの型も、デバッグ用の文字列表現を持つ -(C# `ToString` / Java `toString` / TypeScript `toString` / -Python `__repr__` / Rust `Display`)。 +どの型も、デバッグ用の文字列表現を持つ(C# `ToString` / Java `toString` / TypeScript `toString` / Python `__repr__` / Rust `Debug`)。 +Rustでは、`NbtString` / `NbtList` / `NbtCompound`が`Display`も実装する。 **中身の形式は決めない。** 言語ごとの作法に合わせてよい。 決めるのは「どの型にも用意する」ことだけである。 -機械が読む形式が要るなら [SNBT](11-snbt.md) を使う。 +機械が読む形式が要るなら[SNBT](11-snbt.md)を使う。 こちらは形式を定めており、全言語で同じ文字列になる。 --- ## 8. 関連 -- SNBT テキスト表現 → [11 SNBT](11-snbt.md) +- SNBTテキスト表現 → [11 SNBT](11-snbt.md) - 適合性テストベクタ → [90 適合性](90-conformance.md) diff --git a/docs/spec/11-snbt.md b/docs/spec/11-snbt.md index 54a0f44..037bdec 100644 --- a/docs/spec/11-snbt.md +++ b/docs/spec/11-snbt.md @@ -1,11 +1,10 @@ # 11. SNBT (Stringified NBT) -NBT のテキスト表現。コマンドやデバッグ出力で使われる形式で、 -本ライブラリはパースと出力の両方に対応する。 +NBTのテキスト表現。コマンドやデバッグ出力で使われる形式で、本ライブラリはパースと出力の両方に対応する。 -**1.21.5 で導入された拡張構文**を含む。記述は Java版 26.2 で確認した。 +**1.21.5で導入された拡張構文**を含む。記述はJava版26.2で確認した。 -前提: [10 NBT バイナリ形式](10-nbt-binary.md) +前提: [10 NBTバイナリ形式](10-nbt-binary.md) --- @@ -37,13 +36,13 @@ bare_string = ( "a".."z" | "A".."Z" | "0".."9" | "_" | "-" | "." | "+" )+ ; quoted_string = '"' { char | escape } '"' | "'" { char | escape } "'" ; ``` -数値リテラルの区切り文字 `_` は桁の間に置ける(`123_456` = `123456`)。 +数値リテラルの区切り文字`_`は桁の間に置ける(`123_456` = `123456`)。 --- ## 2. 型の決定 -| 記法 | NBT 型 | +| 記法 | NBT型 | |---|---| | `1b` `1B` | `Byte` | | `1s` `1S` | `Short` | @@ -59,16 +58,16 @@ quoted_string = '"' { char | escape } '"' | "'" { char | escape } "'" ; | `{}` | `Compound` | | その他 | `String` | -接尾辞のない整数は `Int`、小数点や指数を含む数は `Double`。 -`Int` の範囲を超える接尾辞なし整数は `MALFORMED_DATA`(暗黙に `Long` へ格上げしない)。 +接尾辞のない整数は`Int`、小数点や指数を含む数は`Double`。 +`Int`の範囲を超える接尾辞なし整数は`MALFORMED_DATA`(暗黙に`Long`へ格上げしない)。 -符号接尾辞 `u` / `s` は幅接尾辞の前に置く(`10ub` = 符号なしとして解釈した後に `Byte` へ格納)。 -`u` 付きの値が対象幅の符号なし最大値を超えたら `MALFORMED_DATA`。 +符号接尾辞`u` / `s`は幅接尾辞の前に置く(`10ub` = 符号なしとして解釈した後に`Byte`へ格納)。 +`u`付きの値が対象幅の符号なし最大値を超えたら`MALFORMED_DATA`。 ### 2.1 16進リテラルの接尾辞 -`0x` に続く `b` `B` `d` `D` `f` `F` は**16進数字として解釈する**。 -したがって **16進リテラルに使える幅接尾辞は `s` `S` `l` `L` だけ**とする。 +`0x`に続く`b` `B` `d` `D` `f` `F`は**16進数字として解釈する**。 +したがって**16進リテラルに使える幅接尾辞は`s` `S` `l` `L`だけ**とする。 ``` 0xFF -> Int 255 (FF は 16進数字) @@ -77,68 +76,71 @@ quoted_string = '"' { char | escape } '"' | "'" { char | escape } "'" ; 0xFFs -> Short 255 ``` -これは Minecraft の実装が明示していない曖昧点で、 -どちらに解釈しても文法上は成り立ってしまう。 -**黙って一方を選ぶと全言語で挙動がずれる**ため、ここで明示的に固定する。 -`Byte` / `Float` / `Double` を16進で書きたい場合は10進表記を使う。 +これはMinecraftの実装が明示していない曖昧な点で、どちらに解釈しても文法上は成り立ってしまう。 +明示しないまま一方を選ぶと全言語で挙動がずれるため、ここで明示的に固定する。 +`Byte` / `Float` / `Double`を16進で書きたい場合は10進表記を使う。 -2進リテラル (`0b`) の数字は `0` と `1` だけなので、この曖昧さは起きない。 -全ての幅接尾辞を使える。ただし `0b` 単体は「10進の 0 に `Byte` 接尾辞」と解釈する -(`0b` は真偽値の false として広く使われているため)。 +2進リテラル(`0b`)の数字は`0`と`1`だけなので、この曖昧さは起きない。 +全ての幅接尾辞を使える。ただし`0b`単体は「10進の0に`Byte`接尾辞」と解釈する(`0b`は真偽値のfalseとして広く使われているため)。 ### 2.2 特殊な浮動小数点値 -`Infinity` / `-Infinity` / `NaN` を受理する。幅接尾辞を付けられる。 -接尾辞が無ければ `Double`。 +`Infinity` / `-Infinity` / `NaN`を受理する。幅接尾辞を付けられる。 +接尾辞が無ければ`Double`。 -`bool(x)` は `x` が 0 以外なら `1b`、0 なら `0b` を返す。 -`uuid("...")` は UUID を `IntArray` 4 要素(上位から順)へ変換する。 +`bool(x)`は`x`が0以外なら`1b`、0なら`0b`を返す。 +`uuid("...")`はUUIDを`IntArray`の4要素(上位から順)へ変換する。 --- ## 3. 文字列とエスケープ -引用符は `"` と `'` のどちらも使える。文字列内では使っていない方の引用符をそのまま書ける。 +引用符は`"`と`'`のどちらも使える。文字列内では、使っていない方の引用符をそのまま書ける。 | エスケープ | 意味 | |---|---| | `\\` | `\` | | `\"` `\'` | 引用符 | -| `\b` `\s` `\t` `\n` `\f` `\r` | 制御文字(`\s` は空白) | -| `\xNN` | 8bit 値 | -| `\uNNNN` | UTF-16 コード単位 | +| `\b` `\s` `\t` `\n` `\f` `\r` | 制御文字(`\s`は空白) | +| `\xNN` | 8bit値 | +| `\uNNNN` | UTF-16コード単位 | | `\UNNNNNNNN` | コードポイント | -| `\N{名前}` | Unicode 文字名(例: `\N{SNOWMAN}`) | +| `\N{名前}` | Unicode文字名(例: `\N{SNOWMAN}`)。本ライブラリは受理しない | -出力側は `"` で囲み、次のものだけをエスケープする。 +読み込みでは、サロゲートについて次の規則に従う。 + +- `\U`でサロゲートの範囲(`U+D800`〜`U+DFFF`)を書いた場合は、`\u`で書いたときと同じく、そのコード単位を1つ置く +- `\U`で`U+10FFFF`を超える値を書いた場合は`MALFORMED_DATA`とする +- 結果の文字列で対になったサロゲートは、補助文字1文字と同じものとして扱う(`"😀"`と`"\U0001F600"`は同じ文字列) +- Compoundのキーに孤立サロゲートが残る場合は`MALFORMED_DATA`とする。バイナリのキーと同じ扱いである(→ [10 2.2章](10-nbt-binary.md#22-compoundのキー)) + +出力側は`"`で囲み、次のものだけをエスケープする。 | 対象 | 出力 | |---|---| | `"` `\` | `\"` `\\` | | `\b` `\t` `\n` `\f` `\r` | 同じ短縮形 | -| その他の制御文字 (`U+0000`〜`U+001F`, `U+007F`) | `\uXXXX`(小文字16進4桁) | -| **孤立サロゲート** (`U+D800`〜`U+DFFF` で対になっていないもの) | `\uXXXX` | +| その他の制御文字(`U+0000`〜`U+001F`, `U+007F`) | `\uXXXX`(小文字16進4桁) | +| **孤立サロゲート**(`U+D800`〜`U+DFFF`で対になっていないもの) | `\uXXXX` | **補助文字(正しいサロゲートペア)はエスケープせず、そのまま出す。** -UTF-16 を基本とする言語(C# / Java)では 2 つのコード単位に分かれて見えるため、 -素朴に「サロゲートならエスケープ」と実装すると -コードポイント単位の言語(Python / Rust)と出力が食い違う。 +UTF-16を基本とする言語(C# / Java / TypeScript)では、補助文字が2つのコード単位に分かれて見える。 +そのため素朴に「サロゲートならエスケープ」と実装すると、コードポイント単位の言語(Python / Rust)と出力が食い違う。 判定は必ず「対になっているか」で行う。 -`\N{...}` は入力としてのみ受理し、出力では使わない(実装間で名前表が揃わないため)。 +`\N{...}`はパースすると`UNSUPPORTED_FEATURE`を返し、出力でも使わない(実装間で名前表が揃わないため)。 --- ## 4. リストの異種要素 -1.21.5 以降、`[1, "a", {}]` のような異なる型の混在リストが SNBT で書ける。 -これは **`List` へ暗黙変換**され、各要素が -`{"": <値>}` のような包み方ではなく、Minecraft 側の規則に従って正規化される。 +1.21.5以降、`[1, "a", {}]`のような異なる型の混在リストがSNBTで書ける。 +Minecraftはこれをバイナリでは`List`として保存し、Compoundでない要素は空のキーを1つだけ持つCompound(`{"": <値>}`)に包む。 +ゲーム内ではこの包みを見えないように外して扱うので、異種リストがそのまま使えるように見える。 -本ライブラリは**バイナリ NBT の制約(リストは単一型)を優先**し、 -異種リストをパースした場合は `MALFORMED_DATA` を返す。 -コマンド文字列の解釈まで担うライブラリではないため、 -バイナリへ写せない表現は受理しない(→ [adr/0006](../adr/0006-snbt-scope.md))。 +本ライブラリは**バイナリNBTの制約(リストは単一型)を優先**し、異種リストをパースした場合は`MALFORMED_DATA`を返す。 +包みの付け外しはMinecraft内部の変換であり、再現すると、利用者が書いた`{"": <値>}`のCompoundと包みとを区別できなくなる。 +そのため、この表現は受理しない(→ [adr/0006](../adr/0006-snbt-scope.md))。 --- @@ -150,29 +152,27 @@ Snbt.write(tag) -> String -- 1行。空白なし Snbt.write_pretty(tag) -> String -- インデント 4、要素ごとに改行 ``` -出力規則: +出力規則は次のとおり。 -- `Compound` のキーは、`bare_string` として書ける場合は引用符なし。それ以外は `"` で囲む +- `Compound`のキーは、`bare_string`として書ける場合は引用符なし。それ以外は`"`で囲む - キーの順序は**挿入順**(ソートしない) -- `Float` / `Double` は下記の**正準10進表記**に接尾辞を付ける(`1.0f` / `1.0d`) -- `Double` は接尾辞 `d` を**必ず付ける**(省略すると読み戻し時に型が同じでも意図が読み取りにくいため) -- 特殊値は `Infinityd` / `-Infinityd` / `NaNd`(`Float` なら接尾辞 `f`) -- 型付き配列の要素は接尾辞付きで書く(`[B; 1b, 2b]` / `[L; 1L]`)。`IntArray` のみ接尾辞なし +- `Float` / `Double`は下記の正準10進表記に接尾辞を付ける(`1.0f` / `1.0d`) +- `Double`は接尾辞`d`を**必ず付ける**(省略すると、読み戻し時に型が同じでも意図が読み取りにくいため) +- 特殊値は`Infinityd` / `-Infinityd` / `NaNd`(`Float`なら接尾辞`f`) +- 型付き配列の要素は接尾辞付きで書く(`[B; 1b, 2b]` / `[L; 1L]`)。`IntArray`のみ接尾辞なし -型付き配列のパースでは、要素が `Byte` / `Short` / `Int` / `Long` のいずれでも受理し、 -対象の幅に収まるか検査したうえで格納する。Minecraft 自身が -`[I; 1, 2]` のように接尾辞なしで書き出すため、厳格にすると実データを読めない。 -範囲外の値は `MALFORMED_DATA`。 +型付き配列のパースでは、要素が`Byte` / `Short` / `Int` / `Long`のいずれでも受理し、対象の幅に収まるか検査したうえで格納する。 +Minecraft自身が`[I; 1, 2]`のように接尾辞なしで書き出すため、厳格にすると実データを読めない。 +範囲外の値は`MALFORMED_DATA`。 パースして出力し直した結果が入力と一致することは**保証しない**(空白や引用の揺れがあるため)。 -保証するのは「SNBT → NBT → SNBT → NBT」で NBT が一致することである。 +保証するのは、「SNBT → NBT → SNBT → NBT」でNBTが一致することである。 ### 5.1 浮動小数点の正準10進表記 -各言語の標準の数値書式(C# の `"R"`、Java の `Float.toString`、Python の `repr`、 -Rust の `{}`)は**互いに一致しない**。指数表記へ切り替わる閾値も、指数部の桁数も、 -`E` の大文字小文字も処理系ごとに違う。 -そのままでは SNBT 出力の言語間一致が成立しないため、次の手順を仕様として固定する。 +各言語の標準の数値書式(C#の`"R"`、Javaの`Float.toString`、Pythonの`repr`、Rustの`{}`、JavaScriptの`String(x)`)は互いに一致しない。 +指数表記へ切り替わる閾値も、指数部の桁数も、`E`の大文字小文字も処理系ごとに違う。 +そのままでは言語間でSNBTの出力が一致しないため、次の手順を仕様として固定する。 ``` canonical_decimal(値 v, f32 か f64 か): @@ -193,14 +193,14 @@ canonical_decimal(値 v, f32 か f64 か): digits[0] + "." + (digits[1..] があればそれ、無ければ "0") + "E" + exp10 ``` -- 小数部は**必ず 1 桁以上**書く(`1` ではなく `1.0`)。接尾辞なしでも小数と分かるようにするため -- 指数記号は**大文字 `E`**、指数はゼロ詰めしない(`1.0E20`、`1.0E-30`) -- 負値は先頭に `-` を付ける。`-0.0` は `-0.0` と書く -- 特殊値は `Infinity` / `-Infinity` / `NaN` +- 小数部は**必ず1桁以上**書く(`1`ではなく`1.0`)。接尾辞なしでも小数と分かるようにするため +- 指数記号は**大文字の`E`**、指数はゼロ詰めしない(`1.0E20`、`1.0E-30`) +- 負値は先頭に`-`を付ける。`-0.0`は`-0.0`と書く +- 特殊値は`Infinity` / `-Infinity` / `NaN` -例: +例は次のとおり。 -| 値 | f32 の出力 | f64 の出力 | +| 値 | f32の出力 | f64の出力 | |---|---|---| | 1 | `1.0f` | `1.0d` | | 0.75 | `0.75f` | `0.75d` | diff --git a/docs/spec/20-anvil-region.md b/docs/spec/20-anvil-region.md index 753294b..e46154e 100644 --- a/docs/spec/20-anvil-region.md +++ b/docs/spec/20-anvil-region.md @@ -1,14 +1,14 @@ -# 20. Anvil リージョン形式 +# 20. Anvilリージョン形式 -チャンクをまとめて格納する `.mca` ファイルの仕様。 +チャンクをまとめて格納する`.mca`ファイルの仕様。 -前提: [00 共通規約](00-conventions.md) / [10 NBT バイナリ形式](10-nbt-binary.md) +前提: [00 共通規約](00-conventions.md) / [10 NBTバイナリ形式](10-nbt-binary.md) --- ## 1. ファイル名と担当範囲 -`r...mca` の `X` / `Z` は**リージョン座標**で、1リージョンは 32×32 チャンクを担当する。 +`r...mca`の`X` / `Z`はリージョン座標で、1リージョンは32×32チャンクを担当する。 ``` regionX = chunkX >> 5 (算術シフト。負値も正しく動く) @@ -18,41 +18,39 @@ localZ = chunkZ & 31 index = localX + localZ * 32 (0..1023) ``` -`>>` は**算術右シフト**であること。C# の `int` / Java の `int` / Rust の `i32` / Python の `int` -はいずれも算術シフトになるが、実装時に論理シフトへ取り違えないこと。 +`>>`は**算術右シフト**であること。C#の`int` / Javaの`int` / TypeScriptの`number` / Rustの`i32` / Pythonの`int`はいずれも算術シフトになるが、実装時に誤って論理シフトにしないこと。 --- ## 2. ファイル構造 -ファイル全体は **4096 バイト(1セクタ)の倍数**でなければならない。 +ファイル全体は**4096バイト(1セクタ)の倍数**でなければならない。 | セクタ番号 | 内容 | |---:|---| -| 0 | ロケーションテーブル(4096 バイト) | -| 1 | タイムスタンプテーブル(4096 バイト) | -| 2 以降 | チャンクデータ | +| 0 | ロケーションテーブル(4096バイト) | +| 1 | タイムスタンプテーブル(4096バイト) | +| 2以降 | チャンクデータ | ### 2.1 ロケーションテーブル -1024 エントリ × 4 バイト。エントリ `index` はチャンク `(localX, localZ)` に対応する。 +1024エントリ × 4バイト。エントリ`index`はチャンク`(localX, localZ)`に対応する。 ``` バイト 0..2 : オフセット (u24, ビッグエンディアン) — チャンクデータの開始セクタ番号 バイト 3 : 長さ (u8) — チャンクデータが占めるセクタ数 ``` -**オフセットと長さが両方 0 のエントリは「チャンクが存在しない」**を意味する。 -オフセットが 2 未満(ヘッダ領域を指す)で 0 でない場合は `MALFORMED_DATA`。 +**オフセットと長さが両方0のエントリは、チャンクが存在しないことを意味する。** +オフセットが2未満(ヘッダ領域を指す)で0でない場合は`MALFORMED_DATA`。 -長さは `u8` なので最大 255 セクタ = 1,044,480 バイト。 -これを超えるチャンクは外部ファイルへ退避する(→ 4章)。 +長さは`u8`なので最大255セクタ = 1,044,480バイト。 +これを超えるチャンクは外部ファイルへ退避する(→ 3.2章)。 ### 2.2 タイムスタンプテーブル -1024 エントリ × 4 バイト。`i32` ビッグエンディアンの **Unix 秒**(最終更新時刻)。 -チャンクが存在しない場合は 0。本ライブラリは書き込み時に現在時刻を設定する -(`WriteOptions.preserve_timestamp` で元の値を保つこともできる)。 +1024エントリ × 4バイト。`i32`ビッグエンディアンの**Unix秒**(最終更新時刻)。 +チャンクが存在しない場合は0。本ライブラリは書き込み時に現在時刻を設定する。元の値を残したいときは、書き込んだあとに`set_timestamp`で設定し直す。 --- @@ -66,8 +64,8 @@ u8 compression -- 圧縮方式 ... data -- length - 1 バイト ``` -`length` は**必ず 1 以上**。0 以下は `MALFORMED_DATA`。 -`4 + length` が `セクタ数 × 4096` を超える場合も `MALFORMED_DATA`。 +`length`は**必ず1以上**。0以下は`MALFORMED_DATA`。 +`4 + length`が`セクタ数 × 4096`を超える場合も`MALFORMED_DATA`。 セクタ内の余りはゼロ埋めする。 ### 3.1 圧縮方式ID @@ -75,20 +73,19 @@ u8 compression -- 圧縮方式 | ID | 方式 | 本ライブラリの扱い | |---:|---|---| | 1 | GZip (RFC 1952) | 読み書き対応 | -| 2 | Zlib (RFC 1950) | 読み書き対応。**書き込みの既定** | +| 2 | Zlib (RFC 1950) | 読み書き対応。書き込みの既定 | | 3 | 無圧縮 | 読み書き対応 | -| 4 | LZ4(独自ヘッダ付きのブロック連結) | **読み込みのみ対応**。書き込みは `UNSUPPORTED_FEATURE` | -| 127 | サードパーティ製サーバのカスタム方式 | 読み込み時 `UNSUPPORTED_FEATURE`。生バイト API でのみ取得可 | +| 4 | LZ4(独自ヘッダ付きのブロック連結) | **読み込みのみ対応**。書き込みは`UNSUPPORTED_FEATURE` | +| 127 | サードパーティ製サーバのカスタム方式 | 読み込み時`UNSUPPORTED_FEATURE`。生バイトAPIでのみ取得可 | -上記以外のIDは `MALFORMED_DATA`。 +上記以外のIDは`MALFORMED_DATA`。 -### 3.1.1 圧縮ID 4 の中身 +#### 3.1.1 圧縮ID 4の中身 -**素の LZ4 ブロックでも LZ4 フレーム形式でもない。** -独自のヘッダを持つブロックの連結であり、 -これを知らずに LZ4 のデコーダへ直接渡しても読めない。 +**素のLZ4ブロックでもLZ4フレーム形式でもない。** +独自のヘッダを持つブロックの連結であり、これを知らずにLZ4のデコーダへ直接渡しても読めない。 -データは**ブロックの連結**で、各ブロックは 21 バイトのヘッダから始まる。 +データはブロックの連結で、各ブロックは21バイトのヘッダから始まる。 ``` +0 8 バイト マジック "LZ4Block" (0x4C 5A 34 42 6C 6F 63 6B) @@ -101,25 +98,23 @@ u8 compression -- 圧縮方式 +21 ... ブロック本体(圧縮後の長さぶん) ``` -**この形式だけリトルエンディアンである。** NBT 本体と Anvil のヘッダは -すべてビッグエンディアンなので、ここだけ例外になる。 +**この形式だけリトルエンディアンである。** NBT本体とAnvilのヘッダはすべてビッグエンディアンなので、ここだけ例外になる。 -読み込みの手順: +読み込みは次の手順で行う。 -1. 先頭 21 バイトを読み、マジックが `LZ4Block` でなければ `MALFORMED_DATA` -2. トークンの上位 4 ビットで分岐する - - `0x10`: 本体をそのまま出力へ足す。 - このとき圧縮後の長さと展開後の長さが**一致していなければ** `MALFORMED_DATA` - - `0x20`: 本体を LZ4 ブロック展開し、展開後の長さと一致するか確かめる +1. 先頭21バイトを読み、マジックが`LZ4Block`でなければ`MALFORMED_DATA` +2. トークンの上位4ビットで分岐する + - `0x10`: 本体をそのまま出力へ足す。このとき圧縮後の長さと展開後の長さが**一致していなければ**`MALFORMED_DATA` + - `0x20`: 本体をLZ4ブロックとして展開し、展開後の長さと一致するか確かめる - それ以外: `MALFORMED_DATA` 3. 次のブロックへ進む。入力を使い切るまで繰り返す -長さの検証(どちらかが負、または片方だけが 0)に反したら `MALFORMED_DATA`。 +圧縮後の長さと展開後の長さについて、どちらかが負、または片方だけが0なら`MALFORMED_DATA`。 -### 3.1.2 LZ4 ブロックの展開 +#### 3.1.2 LZ4ブロックの展開 -ブロック本体(トークンが `0x20` の場合)は素の LZ4 ブロック形式である。 -**シーケンスの並び**で、各シーケンスは次の形をとる。 +ブロック本体(トークンが`0x20`の場合)は素のLZ4ブロック形式で、シーケンスの並びになっている。 +各シーケンスは次の形をとる。 ``` +0 1 バイト トークン @@ -132,7 +127,7 @@ u8 compression -- 圧縮方式 0..n バイト マッチ長の追加バイト(下位 4 ビットが 15 のときだけ) ``` -追加バイトは **255 が続く限り読み進め、255 未満のバイトで終わる**。 +追加バイトは**255が続く限り読み進め、255未満のバイトで読み終える**。 読んだバイトの総和を基本の長さへ足す。 ``` @@ -143,20 +138,20 @@ u8 compression -- 圧縮方式 マッチは**出力済みのバイト列から**コピーする。 コピー開始位置は「現在の出力長 - オフセット」である。 -**コピーは 1 バイトずつ行うこと。** コピー元とコピー先は重なりうる。 -たとえばオフセット 1 でマッチ長 10 なら、直前の 1 バイトが 10 回並ぶ。 -まとめてコピーする実装(`memcpy` など)では結果が変わる。 +**コピーは1バイトずつ行うこと。** コピー元とコピー先は重なりうる。 +たとえばオフセット1でマッチ長10なら、直前の1バイトが10回並ぶ。 +まとめてコピーする実装(`memcpy`など)では結果が変わる。 展開の終わり方は次のとおり。 - リテラルを出力したあと入力が尽きていれば、そのシーケンスで終わり - 尽きていなければオフセットを読み、マッチをコピーして次のシーケンスへ進む -検証項目: +検証項目は次のとおり。 | 条件 | 結果 | |---|---| -| オフセットが `0` | `MALFORMED_DATA` | +| オフセットが`0` | `MALFORMED_DATA` | | オフセットが現在の出力長より大きい | `MALFORMED_DATA` | | 入力の終端を越えて読もうとした | `MALFORMED_DATA` | | 出力が展開後の長さを超えた | `MALFORMED_DATA` | @@ -164,22 +159,20 @@ u8 compression -- 圧縮方式 ### 3.2 外部ファイルへの退避 (`.mcc`) -圧縮方式IDの**最上位ビット (0x80) が立っている**場合、 -チャンクの実データはリージョンファイル内ではなく次のファイルに格納される。 +圧縮方式IDの**最上位ビット(0x80)が立っている**場合、チャンクの実データはリージョンファイル内ではなく次のファイルに格納される。 ``` <リージョンと同じディレクトリ>/c...mcc ``` -- `.mcc` の中身は**圧縮済みデータそのもの**。`length` も `compression` バイトも持たない -- 実際の圧縮方式は `compression & 0x7F` で得る -- このときリージョン内には 1 セクタだけが確保され、`length` は 1(`compression` バイトのみ)となる +- `.mcc`の中身は**圧縮済みデータそのもの**。`length`も`compression`バイトも持たない +- 実際の圧縮方式は`compression & 0x7F`で得る +- このときリージョン内には1セクタだけが確保され、`length`は1(`compression`バイトのみ)となる -`chunkX` / `chunkZ` は**絶対チャンク座標**であってリージョン内相対ではない。 +`chunkX` / `chunkZ`は**絶対チャンク座標**であり、リージョン内の相対座標ではない。 -**書き込み時の自動退避**: 圧縮後のデータが `255 * 4096 - 5` バイトを超える場合、 -本ライブラリは自動的に `.mcc` へ退避し、フラグを立てる。 -逆に `.mcc` にあったチャンクが縮んだ場合はリージョン内へ戻し、`.mcc` を削除する。 +**書き込み時の自動退避**: 圧縮後のデータが`255 * 4096 - 5`バイトを超える場合、本ライブラリは自動的に`.mcc`へ退避し、フラグを立てる。 +逆に`.mcc`にあったチャンクが縮んだ場合はリージョン内へ戻し、`.mcc`を削除する。 --- @@ -187,22 +180,22 @@ u8 compression -- 圧縮方式 書き込み時のセクタ管理規則。破損しやすい箇所なので厳密に定める。 -1. 起動時にロケーションテーブルを走査し、**使用中セクタの集合**を作る - - セクタ 0・1 は常に使用中(ヘッダ) - - 同じセクタを 2 つのチャンクが指していたら `MALFORMED_DATA` -2. チャンクを書くとき、必要セクタ数 `n = ceil((4 + length) / 4096)` を求める -3. 既存の割り当てが `n` セクタ**ちょうど**なら、その場に上書きする -4. そうでなければ、**先頭から探して `n` 連続で空いている領域**へ書く +1. 起動時にロケーションテーブルを走査し、使用中セクタの集合を作る + - セクタ0・1は常に使用中(ヘッダ) + - 同じセクタを2つのチャンクが指していたら`MALFORMED_DATA` +2. チャンクを書くとき、必要セクタ数`n = ceil((4 + length) / 4096)`を求める +3. 既存の割り当てが`n`セクタ**ちょうど**なら、その場に上書きする +4. そうでなければ、**先頭から探して`n`連続で空いている領域**へ書く - 見つからなければファイル末尾へ追記する - 元の割り当ては解放する 5. 書き終えたらロケーション/タイムスタンプの両テーブルを更新する -6. ファイル長を 4096 の倍数へ切り上げる(不足分はゼロ埋め) +6. ファイル長を4096の倍数へ切り上げる(不足分はゼロ埋め) -削除は、ロケーションエントリとタイムスタンプエントリを 0 にし、セクタを解放する。 -**ファイルの縮小は行わない**(次の書き込みで再利用される)。 +チャンクを削除するときは、ロケーションエントリとタイムスタンプエントリを0にし、セクタを解放する。 +**ファイルの縮小は行わない**(解放したセクタは次の書き込みで再利用される)。 -`optimize()` は全チャンクを読み出して新しいファイルへ隙間なく詰め直す。 -断片化した `.mca` のサイズを縮めたいときに使う。 +`optimize()`は全チャンクを読み出して新しいファイルへ隙間なく詰め直す。 +断片化した`.mca`のサイズを縮めたいときに使う。 --- @@ -230,7 +223,7 @@ RawChunk { compression: Compression, external: bool, data: bytes } ``` チャンク座標は**絶対座標**で受け取り、内部でリージョン内位置へ変換する。 -このリージョンの担当外の座標が渡されたら `INVALID_ARGUMENT`。 +このリージョンの担当外の座標が渡されたら`INVALID_ARGUMENT`。 ``` RegionFolder @@ -245,32 +238,29 @@ RegionFolder ### 5.1 開いたリージョンの上限 -`RegionFile` は**ファイル全体をメモリへ載せる**。 -無変更で書き戻したときにバイト単位で一致させるには、 -セクタの配置を丸ごと保持している必要があるためである。 +`RegionFile`は**ファイル全体をメモリへ載せる**。 +変更せずに書き戻したときにバイト単位で一致させるには、セクタの配置を丸ごと保持している必要があるためである。 -その代わり、開いたまま貯め続けるとメモリを使い切る。 -1 リージョンは理論上 1GiB(255 セクタ × 1024 チャンク)になりうるし、 -実データでも数 MB から数十 MB ある。数千リージョンあるワールドを -端から走査すれば破綻する。 +その代わり、リージョンを開いたまま貯め続けるとメモリを使い切る。 +1リージョンは理論上1GiB(255セクタ × 1024チャンク)になりうるし、実データでも数MBから数十MBある。 +数千リージョンあるワールドを端から走査すれば、処理を続けられなくなる。 -そこで `RegionFolder` は開いたリージョンの数に上限を持つ。 +そこで`RegionFolder`は開いたリージョンの数に上限を持つ。 | 項目 | 値 | |---|---| -| 既定の上限 | **8 件** | +| 既定の上限 | 8件 | | 上限に達したとき | 最も長く使われていないものを**書き出してから**閉じる | -| 上限が 1 未満 | `INVALID_ARGUMENT` | +| 上限が1未満 | `INVALID_ARGUMENT` | -**追い出す前に必ず書き出す。** 捨てると変更が失われる。 +**追い出す前に必ず書き出す。** 書き出さずに捨てると変更が失われる。 -この結果、`region()` が返した参照は -**別のリージョンへアクセスすると閉じられている場合がある**。 +この結果、`region()`が返した参照は、**別のリージョンへアクセスすると閉じられている場合がある**。 参照を保持せず、必要なたびに取得すること。 -`read_chunk` / `write_chunk` 経由で使うぶんには意識しなくてよい。 +`read_chunk` / `write_chunk`経由で使うぶんには意識しなくてよい。 -`RegionFolder` は `region/` `entities/` `poi/` のいずれか1つのディレクトリを表す。 -開いたリージョンファイルはキャッシュし、`close()` でまとめて閉じる。 +`RegionFolder`は`region/` `entities/` `poi/`のいずれか1つのディレクトリを表す。 +開いたリージョンファイルはキャッシュし、`close()`でまとめて閉じる。 --- diff --git a/docs/spec/30-chunk-format.md b/docs/spec/30-chunk-format.md index 096bb50..f58ce01 100644 --- a/docs/spec/30-chunk-format.md +++ b/docs/spec/30-chunk-format.md @@ -1,25 +1,24 @@ # 30. チャンク形式 -`.mca` 内に格納されるチャンク1つ分の NBT 構造。**ここから先は形式依存**である。 -本書は **26.1 で導入された形式(DataVersion 4786 以降)** を対象とし、 -記述は **Java版 26.2 (DataVersion 4903)** の実データで確認した。 +`.mca`内に格納されるチャンク1つ分のNBT構造。**ここから先は形式依存**である。 +本書は**26.1で導入された形式(DataVersion 4786以降)** を対象とし、記述はJava版26.2(DataVersion 4903)の実データで確認した。 -前提: [20 Anvil リージョン形式](20-anvil-region.md) / [31 パレット付きコンテナ](31-paletted-container.md) +前提: [20 Anvilリージョン形式](20-anvil-region.md) / [31 パレット付きコンテナ](31-paletted-container.md) --- ## 1. ルート要素 -ルートは名前なし(空文字列)の `TAG_Compound`。 -下表は **Java版 26.2 の実ワールド(3,481 チャンク)を走査して確認した**内容である。 -「出現」欄は、走査した full/未完成を含む全チャンクのうち何割に現れたかを示す。 +ルートは名前なし(空文字列)の`TAG_Compound`。 +下表はJava版26.2の実ワールド(3,481チャンク)を走査して確認した内容である。 +「出現」欄は、走査したチャンク(full/未完成の両方を含む)のすべてに現れたキーを「全部」、一部のチャンクにだけ現れたキーを「一部」と示す。 | キー | 型 | 出現 | 内容 | |---|---|---|---| -| `DataVersion` | `Int` | 全部 | チャンク構造のバージョン。26.1 は `4786`、26.2 は `4903` | +| `DataVersion` | `Int` | 全部 | チャンク構造のバージョン。26.1は`4786`、26.2は`4903` | | `xPos` | `Int` | 全部 | 絶対チャンクX座標 | | `zPos` | `Int` | 全部 | 絶対チャンクZ座標 | -| `yPos` | `Int` | 全部 | 最下段セクションのY位置。オーバーワールドは `-4` | +| `yPos` | `Int` | 全部 | 最下段セクションのY位置。オーバーワールドは`-4` | | `Status` | `String` | 全部 | 生成段階(→ 1.1) | | `LastUpdate` | `Long` | 全部 | 最終更新のゲーム内時刻 | | `InhabitedTime` | `Long` | 全部 | プレイヤー滞在時間(tick) | @@ -28,11 +27,11 @@ | `block_ticks` | `List` | 全部 | 予約されたブロック更新 | | `fluid_ticks` | `List` | 全部 | 予約された液体更新 | | `Heightmaps` | `Compound` | 全部 | 高さマップ(→ 3章) | -| `structures` | `Compound` | 全部 | `starts` と `References` を持つ | +| `structures` | `Compound` | 全部 | `starts`と`References`を持つ | | `PostProcessing` | `List` | 全部 | 生成後処理の待ち位置 | -| `isLightOn` | `Byte` | 一部 | 光源計算済みフラグ。実データでは `minecraft:full` のチャンクにのみ現れた | -| `entities` | `List` | 一部 | **生成途中のチャンクだけが持つ**。完成すると `entities/` のリージョンへ移る | -| `carving_mask` | `ByteArray` | 一部 | 洞窟の削り取り状況。`minecraft:carvers` 前後の段階でのみ現れる | +| `isLightOn` | `Byte` | 一部 | 光源計算済みフラグ。実データでは`minecraft:full`のチャンクにのみ現れた | +| `entities` | `List` | 一部 | **生成途中のチャンクだけが持つ**。完成すると`entities/`のリージョンへ移る | +| `carving_mask` | `ByteArray` | 一部 | 洞窟の削り取り状況。`minecraft:carvers`前後の段階でのみ現れる | ### 1.1 Status @@ -46,40 +45,40 @@ | `minecraft:initialize_light` | 光源の初期化まで済んだ段階 | | `minecraft:full` | 生成完了。ブロック操作の対象になるのはこれ | -**`minecraft:full` 以外のチャンクは構造が不完全**である。 -`set_block` は `minecraft:full` のチャンクに対してのみ行うのが安全で、 -それ以外は既定でエラーとする(`allow_incomplete_chunk` で明示的に許可できる)。 +**`minecraft:full`以外のチャンクは構造が不完全**である。 +`set_block`は`minecraft:full`のチャンクに対してのみ行うのが安全である。 +本ライブラリは生成段階を検査せず、それ以外のチャンクにもそのまま書き込む。 +生成済みかどうかは`is_fully_generated`で確かめられる。 本ライブラリは**上記以外のキーも含め、読んだ要素をすべて保持する**。 -未知のキーを落とさないことで、将来の追加要素があってもデータを壊さない。 +未知のキーを落とさないので、将来の追加要素があってもデータはおかしくならない。 -`Chunk` 型は解釈済みの値(`sections` など)と、元の `NbtCompound` の両方を持ち、 -書き出し時は「解釈して変更した部分だけを元の Compound に反映する」方式をとる。 +`Chunk`型は解釈済みの値(`sections`など)と元の`NbtCompound`の両方を持ち、書き出し時は「解釈した部分(各セクションの`block_states`と`biomes`)を元のCompoundへ書き戻し、解釈していないキーはそのまま残す」方式をとる。 --- ## 2. セクション -`sections` は `List`。1要素が 16×16×16 ブロックを表す。 +`sections`は`List`。1要素が16×16×16ブロックを表す。 | キー | 型 | 内容 | |---|---|---| -| `Y` | `Byte` | セクションのY位置。オーバーワールドは `-5`..`20` | -| `block_states` | `Compound` | ブロック状態のパレット付きコンテナ(4096 エントリ) | -| `biomes` | `Compound` | バイオームのパレット付きコンテナ(64 エントリ) | -| `BlockLight` | `ByteArray` | 2048 バイト。1ブロックあたり 4bit | -| `SkyLight` | `ByteArray` | 2048 バイト。1ブロックあたり 4bit | +| `Y` | `Byte` | セクションのY位置。オーバーワールドは`-5`..`20` | +| `block_states` | `Compound` | ブロック状態のパレット付きコンテナ(4096エントリ) | +| `biomes` | `Compound` | バイオームのパレット付きコンテナ(64エントリ) | +| `BlockLight` | `ByteArray` | 2048バイト。1ブロックあたり4bit | +| `SkyLight` | `ByteArray` | 2048バイト。1ブロックあたり4bit | -`Y` が `yPos - 1` や最上段+1 になっている**光源専用セクション**が存在しうる。 -これらは `block_states` を持たないため、ブロックAPIから見ると空セクションとして扱う。 -(26.2 の実ワールドでは観測されず、全セクションが `block_states` と `biomes` を持っていた。 -ただし将来・他の生成条件で現れうるため、`block_states` の有無は必ず確認する。) +`Y`が`yPos - 1`や最上段+1になっている光源専用セクションが存在しうる。 +光源専用セクションは`block_states`を持たないため、ブロックAPIでは空セクションとして扱う。 +(26.2の実ワールドでは観測されず、全セクションが`block_states`と`biomes`を持っていた。 +ただし将来や他の生成条件で現れうるため、`block_states`の有無は必ず確認する。) -`BlockLight` / `SkyLight` は**必要なセクションにしか無い**。 -実データでは 83,544 セクションのうち `BlockLight` が 5,103、`SkyLight` が 4,904 だった。 +`BlockLight` / `SkyLight`は**必要なセクションにしか無い**。 +実データでは83,544セクションのうち`BlockLight`が5,103、`SkyLight`が4,904だった。 -`sections` は `Y` の昇順に並んでいるが、**並び順に依存してはならない**。 -読み込み時に `Y` から索引を作る。 +`sections`は`Y`の昇順に並んでいるが、**並び順に依存してはならない**。 +読み込み時に`Y`から索引を作る。 ### 2.1 ブロック状態のパレット要素 @@ -87,9 +86,9 @@ { Name: "minecraft:oak_stairs", Properties: { facing: "north", half: "top", waterlogged: "false" } } ``` -- `Name` は必須の `String`。名前空間が省略されていたら `minecraft:` を補う -- `Properties` は任意の `Compound`。値はすべて `String`(数値や真偽値も文字列) -- `Properties` が空の場合、Minecraft はキー自体を出力しない。本ライブラリも同様にする +- `Name`は必須の`String`。名前空間が省略されていたら`minecraft:`を補う +- `Properties`は任意の`Compound`。値はすべて`String`(数値や真偽値も文字列) +- `Properties`が空の場合、Minecraftはキー自体を出力しない。本ライブラリも同様にする ### 2.1.1 ブロック状態の文字列表現 @@ -99,14 +98,12 @@ <名前空間>:<パス>[<キー>=<値>,<キー>=<値>,...] ``` -- **プロパティは必ず名前の昇順に並べて出力する。** これにより、同じブロック状態は - 内部の並び順に関係なく常に同じ文字列になり、全言語で一致する +- **プロパティは必ず名前の昇順に並べて出力する。** これにより、同じブロック状態は内部の並び順に関係なく常に同じ文字列になり、全言語で一致する - プロパティが空のときは角括弧ごと省略する -- 解析時は名前空間の省略を許し、`minecraft:` を補う -- 解析時のみ、`=` の前後と `,` の直後の空白を読み飛ばす。出力側は空白を入れない +- 解析時は名前空間の省略を許し、`minecraft:`を補う +- 解析時のみ、`=`の前後と`,`の直後の空白を読み飛ばす。出力側は空白を入れない -解析の失敗はすべて `INVALID_ARGUMENT`(呼び出し側が渡した文字列の誤りであり、 -ファイルの中身の破損ではないため)。 +解析の失敗はすべて`INVALID_ARGUMENT`(呼び出し側が渡した文字列の誤りであり、ファイルの中身の破損ではないため)。 | 入力 | 結果 | |---|---| @@ -115,13 +112,12 @@ | `minecraft:oak_stairs[]` | `minecraft:oak_stairs` | | `` (空文字列) | `INVALID_ARGUMENT` | | `minecraft:oak_stairs[facing=north` | `INVALID_ARGUMENT`(閉じ括弧が無い) | -| `minecraft:oak_stairs[]extra` | `INVALID_ARGUMENT`(`]` で終わっていない) | -| `minecraft:oak_stairs[facing]` | `INVALID_ARGUMENT`(`=` が無い) | +| `minecraft:oak_stairs[]extra` | `INVALID_ARGUMENT`(`]`で終わっていない) | +| `minecraft:oak_stairs[facing]` | `INVALID_ARGUMENT`(`=`が無い) | | `minecraft:oak_stairs[=north]` | `INVALID_ARGUMENT`(キーが空) | | `minecraft:oak_stairs[facing=north,facing=south]` | `INVALID_ARGUMENT`(キーの重複) | -キーの重複を後勝ちで受け入れないのは、どちらが採用されたのかが呼び出し側から -分からないまま書き込まれてしまうため。 +キーの重複を後勝ちで受け入れないのは、どちらが採用されたのかが呼び出し側から分からないまま書き込まれてしまうため。 ### 2.2 座標とエントリ添字 @@ -130,13 +126,12 @@ block index = (y & 15) * 256 + (z & 15) * 16 + (x & 15) biome index = ((y & 15) / 4) * 16 + ((z & 15) / 4) * 4 + ((x & 15) / 4) ``` -`& 15` により負の座標でも正しくセクション内相対値になる。 -セクションの選択は `sectionY = y >> 4`(算術右シフト)で行う。 +`& 15`により、負の座標でも正しくセクション内の相対値になる。 +セクションの選択は`sectionY = y >> 4`(算術右シフト)で行う。 ### 2.3 ブロックに紐づく付随データ -ブロックそのもの以外に、**特定の座標に結びついたデータ**が -チャンクのルート直下に 3 つある。 +ブロックそのもの以外に、特定の座標に結びついたデータがチャンクのルート直下に3つある。 | キー | 内容 | 主なキー | |---|---|---| @@ -144,10 +139,10 @@ biome index = ((y & 15) / 4) * 16 + ((z & 15) / 4) * 4 + ((x & 15) / 4) | `block_ticks` | ブロックのティック予約 | `i`(ブロックID), `x`, `y`, `z`, `t`, `p` | | `fluid_ticks` | 液体のティック予約 | 同上 | -いずれも `List` で、**座標は絶対ワールド座標の `Int`** で持つ。 -チャンク内相対ではないので、チャンク座標から換算して突き合わせる必要がある。 +いずれも`List`で、**座標は絶対ワールド座標の`Int`で持つ**。 +チャンク内の相対座標ではないので、チャンク座標から換算して比べる必要がある。 -実データの例(Java版 26.2): +Java版26.2の実データの例を示す。 ``` block_entities: {id:"minecraft:mob_spawner", x:-318, y:-40, z:-302, SpawnData:{...}, ...} @@ -156,31 +151,28 @@ block_ticks: {i:"minecraft:acacia_leaves", x:-93, y:75, z:-305, t:0, p:0} ### 2.4 ブロックを置き換えたときの掃除 -**ブロックを別の種類へ置き換えたら、その座標を指す 2.3 の要素をすべて取り除く。** +**ブロックを別の種類へ置き換えたら、その座標を指す2.3の要素をすべて取り除く。** -取り除かないと、たとえばチェストのあった座標に石を置いたとき -`block_entities` にチェストの中身が残る。ブロックと中身が食い違った状態になり、 -Minecraft 側で警告やアイテムの復活といった予期しない挙動を招く。 -しかも**利用者からは見えにくい**壊れ方をする。 +取り除かないと、たとえばチェストのあった座標に石を置いたとき、`block_entities`にチェストの中身が残る。 +ブロックと中身が食い違った状態になり、Minecraft側で警告やアイテムの復活といった予期しない挙動を招く。 +しかも、**利用者からは見えにくい**形でデータがおかしくなる。 | 状況 | 動作 | |---|---| -| 別の種類のブロックを置いた | その座標の要素を 3 つのリストすべてから取り除く | +| 別の種類のブロックを置いた | その座標の要素を3つのリストすべてから取り除く | | **同じブロック状態**を置き直した | 何もしない(状態が変わっていないので掃除する理由がない) | -| 要素が `x` `y` `z` を持たない | 対象か判断できないので触らない | +| 要素が`x` `y` `z`を持たない | 対象か判断できないので触らない | -新しいブロックが本来必要とする `block_entity`(置いたのがチェストなら空のチェスト)は -**生成しない**。どのブロックが block entity を必要とするかの判定には -ブロック定義テーブルが要り、それ自体が別の成果物になるため -([adr/0004](../adr/0004-defer-heightmap-recalc.md) と同じ理由)。 -必要なら利用者が生の NBT へ直接足す。 +新しいブロックが本来必要とする`block_entity`(置いたのがチェストなら空のチェスト)は**生成しない**。 +どのブロックがblock entityを必要とするかの判定にはブロック定義テーブルが要り、それ自体が別の成果物になるため([adr/0004](../adr/0004-defer-heightmap-recalc.md)と同じ理由)。 +必要なら利用者が生のNBTへ直接足す。 --- ## 3. Heightmaps -`Heightmaps` は `Compound` で、各値は `LongArray`。 -256 エントリ(16×16)を **9 ビット幅**の BitStorage(跨ぎなし)で詰めたもの。 +`Heightmaps`は`Compound`で、各値は`LongArray`。 +256エントリ(16×16)を**9ビット幅**のBitStorage(跨ぎなし)で詰めたもの。 ``` bits = 9 @@ -198,45 +190,43 @@ index = z * 16 + x | `OCEAN_FLOOR` | 移動を妨げる固体の最上位ブロック | **読み書き(そのまま保持)のみ対応し、再計算は行わない。** -どのブロックが「移動を妨げる」かの判定にはブロック定義テーブルが必要で、 -それ自体が別の大きな成果物になるため(→ [adr/0004](../adr/0004-defer-heightmap-recalc.md))。 +どのブロックが「移動を妨げる」かの判定にはブロック定義テーブルが必要で、それ自体が別の大きな成果物になるため(→ [adr/0004](../adr/0004-defer-heightmap-recalc.md))。 ブロックを改変した後は次のいずれかを推奨する。 -- `chunk.clear_heightmaps()` — `Heightmaps` を削除する。Minecraft が次回読み込み時に再計算する -- 何もしない — 見た目(草の生成やモブスポーン判定)が一時的にずれる可能性がある +- `chunk.clear_heightmaps()`で`Heightmaps`を削除する。Minecraftが次回読み込み時に再計算する +- 何もしない。見た目(草の生成やモブスポーン判定)が一時的にずれる可能性がある -同様に `chunk.invalidate_lighting()` は `isLightOn` を `0` にし、光源の再計算を促す。 +同様に`chunk.invalidate_lighting()`は`isLightOn`を`0`にし、光源の再計算を促す。 --- ## 4. entities / poi -`entities/` と `poi/` のリージョンファイルも同じ Anvil 形式だが、中身が異なる。 +`entities/`と`poi/`のリージョンファイルも同じAnvil形式だが、中身が異なる。 **entities**(`dimensions///entities/r.X.Z.mca`) | キー | 型 | 内容 | |---|---|---| | `DataVersion` | `Int` | | -| `Position` | `IntArray` | `[chunkX, chunkZ]` の 2 要素 | -| `Entities` | `List` | エンティティ。各要素が `id`(String) と `Pos`(List<Double>) を持つ | +| `Position` | `IntArray` | `[chunkX, chunkZ]`の2要素 | +| `Entities` | `List` | エンティティ。各要素が`id`(String)と`Pos`(List<Double>)を持つ | **poi**(`dimensions///poi/r.X.Z.mca`) | キー | 型 | 内容 | |---|---|---| | `DataVersion` | `Int` | | -| `Sections` | `Compound` | キーが**セクションYの10進文字列**(`"-1"` など)、値が下記 | +| `Sections` | `Compound` | キーが**セクションYの10進文字列**(`"-1"`など)、値が下記 | -`Sections` の各値: +`Sections`の各値は次の形をとる。 ``` { Valid: 1b, Records: [ { pos: [I; 279, -16, 182], free_tickets: 1, type: "minecraft:home" } ] } ``` -これらは**生の `NbtCompound` として読み書き**できるところまでを対象とし、 -型付きの API は提供しない。 +これらは**生の`NbtCompound`として読み書き**できるところまでを対象とし、型付きのAPIは提供しない。 --- @@ -258,10 +248,10 @@ Cuboid { min_x, min_y, min_z, max_x, max_y, max_z } positions() 範囲内の座標を順に返す ``` -`Cuboid` は**両端を含む**。`of(0, 0, 0, 0, 0, 0)` は 1 ブロックで `volume()` は 1。 +`Cuboid`は**両端を含む**。`of(0, 0, 0, 0, 0, 0)`は1ブロックで、`volume()`は1。 -`positions()` の並びは **Y、Z、X の順**とし、X をいちばん内側で動かす。 -X はチャンク内で連続するので、同じセクションを続けて触れる。 +`positions()`の並びは**Y、Z、Xの順**とし、Xをいちばん内側で動かす。 +Xはチャンク内で連続するので、同じセクションに続けてアクセスできる。 チャンク座標への変換は**算術右シフト**で行う。 論理右シフトでは負の座標が正しく求まらない。 @@ -270,9 +260,9 @@ X はチャンク内で連続するので、同じセクションを続けて触 ## 6. バージョン検査 -対応の判定はワールドの形式で行う。Minecraft のバージョン番号は条件にしない。 +対応の判定はワールドの形式で行う。Minecraftのバージョン番号は条件にしない。 -いまの形式は **26.1(DataVersion `4786`)** で入った。 +いまの形式は26.1(DataVersion `4786`)で入った。 これ以降のバージョンは、形式が変わらない限りそのまま読み書きできる。 ``` @@ -280,20 +270,18 @@ MIN_SUPPORTED_DATA_VERSION = 4786 扱える形式の下限 TARGET_DATA_VERSION = 4903 動作を確かめたバージョン(26.2) ``` -- 読み込み時、`DataVersion` が `4786` **以上なら何もしない** +- 読み込み時、`DataVersion`が`4786`**以上なら何もしない** - 対象より新しいバージョンでも警告を出さない - 形式が同じなら読めるので、知らせる必要がない -- `4786` 未満なら `ReadOptions.on_version_mismatch` に従う +- `4786`未満なら`ChunkReadOptions.on_version_mismatch`に従う - `Warn`(既定): 警告コールバックを呼んで続行する - `Error`: `UNSUPPORTED_DATA_VERSION` - `Ignore`: 何もしない -- 書き込み時、`DataVersion` は**読んだ値をそのまま残す** +- 書き込み時、`DataVersion`は**読んだ値をそのまま残す** - 書き換えると、そのワールドを開くゲーム側の判断を誤らせる - - `4786` 未満のチャンクは既定で `UNSUPPORTED_DATA_VERSION` - - `WriteOptions.allow_foreign_data_version = true` で明示的に許可できる。 - このときも `DataVersion` は書き換えない + - `4786`未満のチャンクは既定で`UNSUPPORTED_DATA_VERSION` + - `ChunkWriteOptions.allow_foreign_data_version = true`で明示的に許可できる。このときも`DataVersion`は書き換えない -**形式が変わったときだけ下限を上げる。** 上げたら、そのワールドを扱えなくなる -利用者が出るので、バージョン番号のいちばん上の桁も上げる。 +**形式が変わったときだけ下限を上げる。** 下限を上げると、そのワールドを扱えなくなる利用者が出るので、バージョン番号のいちばん上の桁も上げる。 -詳細は [07 バージョンポリシー](../guide/07-version-policy.md)。 +詳細は[07 バージョンポリシー](../guide/07-version-policy.md)。 diff --git a/docs/spec/31-paletted-container.md b/docs/spec/31-paletted-container.md index 9c3d933..f445df9 100644 --- a/docs/spec/31-paletted-container.md +++ b/docs/spec/31-paletted-container.md @@ -1,4 +1,4 @@ -# 31. パレット付きコンテナと BitStorage +# 31. パレット付きコンテナとBitStorage セクション内のブロック状態・バイオームを格納する形式。 本ライブラリで最も間違いやすい箇所なので、ビット演算を厳密に定める。 @@ -12,20 +12,20 @@ ``` block_states: { palette: [ {Name: "minecraft:stone"}, {Name:"minecraft:oak_stairs", Properties:{facing:"north"}} ], - data: [L; ...] // パレットが 1 要素のときは存在しない + data: [L; ...] // パレットが1要素のときは存在しない } ``` -`data` はパレットへの添字を 64bit 整数の配列へ詰めたもの。 +`data`はパレットへの添字を64bit整数の配列へ詰めたもの。 | 用途 | 要素数 | 添字の並び順 | ビット幅 | パレット上限 | |---|---:|---|---|---:| | `block_states` | 4096 | `y*256 + z*16 + x` | `max(4, ceil_log2(len))` | 4096 | | `biomes` | 64 | `y*16 + z*4 + x` | `max(1, ceil_log2(len))` | 64 | -`x` `y` `z` はセクション内の相対座標。ブロックは 0..15、バイオームは 0..3(1エントリが 4×4×4 ブロック)。 +`x` `y` `z`はセクション内の相対座標。ブロックは0..15、バイオームは0..3(1エントリが4×4×4ブロック)。 -`ceil_log2(n)` は「`n` 個の値を表すのに必要な最小ビット数」で、`n = 1` のとき **0** を返す。 +`ceil_log2(n)`は「`n`個の値を表すのに必要な最小ビット数」で、`n = 1`のとき**0**を返す。 ``` ceil_log2(1) = 0 @@ -37,17 +37,17 @@ ceil_log2(5) = 3 ceil_log2(4096) = 12 ``` -したがってブロックのビット幅は 4..12、バイオームのビット幅は 1..6 になる。 +したがってブロックのビット幅は4..12、バイオームのビット幅は1..6になる。 -**パレットが 1 要素のとき `data` は存在しない**(全エントリがその 1 種類)。 -逆に `data` があるのにパレットが 1 要素の場合は、`data` を無視せず読み、全要素が 0 であることを確認する。 +**パレットが1要素のとき`data`は存在しない**(全エントリがその1種類)。 +逆に`data`があるのにパレットが1要素の場合は、`data`を無視せず読み、全要素が0であることを確認する。 --- ## 2. ビット詰め(跨ぎなし) -1.16 以降、添字は **64bit 境界を跨がない**。 -1つの `i64` に入りきらない分は、その `i64` の残りビットを未使用のまま捨て、次の `i64` の最下位ビットから始める。 +1.16以降、添字は**64bit境界を跨がない**。 +1つの`i64`に入りきらない分は、その`i64`の残りビットを未使用のまま捨て、次の`i64`の最下位ビットから始める。 ``` values_per_long = 64 / bits (整数除算。切り捨て) @@ -65,25 +65,23 @@ entry i の位置: data[long_index] = (data[long_index] & ~mask) | ((value << bit_offset) & mask) ``` -`>>>` は**論理右シフト**であること。`i64` は符号付きなので、算術シフトを使うと -最上位ビット付近の値が壊れる。 +`>>>`は**論理右シフト**であること。`i64`は符号付きなので、算術シフトを使うと最上位ビット付近の値がおかしくなる。 | 言語 | 論理右シフトの書き方 | |---|---| -| Rust | `(v as u64) >> n` (`u64` へキャストしてからシフト) | +| Rust | `(v as u64) >> n`(`u64`へキャストしてからシフト) | | C# | `(long)((ulong)v >> n)` | | Java | `v >>> n` | +| TypeScript | `BigInt.asUintN(64, v) >> n`(`bigint`は`>>>`を使えないので、符号なし64bitへ直してからシフト) | | Python | `(v & 0xFFFFFFFFFFFFFFFF) >> n` | -Python は整数が無限精度なので、`i64` として扱う箇所では -**読み込み時に符号付き 64bit へ、書き込み時に符号なし 64bit へ**明示的に変換する。 +Pythonは整数が無限精度なので、`i64`として扱う箇所では**読み込み時に符号付き64bitへ、書き込み時に符号なし64bitへ**明示的に変換する。 ### 2.1 実データで確認した組み合わせ -Java版 26.2 の実ワールド(83,544 セクション = 167,088 個のコンテナ)を走査した結果、 -上式で求めた `long_count` は**すべて実際の `data` 長と一致した**。 +Java版26.2の実ワールド(83,544セクション = 167,088個のコンテナ)を走査した結果、上式で求めた`long_count`はすべて実際の`data`長と一致した。 -| コンテナ | パレット長 | bits | `data` の長さ | 出現数 | +| コンテナ | パレット長 | bits | `data`の長さ | 出現数 | |---|---:|---:|---|---:| | `biomes` | 1 | — | **無し** | 73,345 | | `block_states` | 1 | — | **無し** | 67,532 | @@ -93,11 +91,11 @@ Java版 26.2 の実ワールド(83,544 セクション = 167,088 個のコン | `biomes` | 3〜4 | 2 | 2 long | 735 | | `block_states` | 33〜64 | 6 | 410 long | 361 | -`spec/tools/scan_world.py` がこの検証を行う。仕様を変えたら必ず実ワールドで再確認すること。 +`spec/tools/scan_world.py`がこの検証を行う。仕様を変えたら必ず実ワールドで再確認すること。 ### 2.2 具体例 -ブロック(4096 エントリ)でパレットが 5 要素の場合: +ブロック(4096エントリ)でパレットが5要素の場合は次のようになる。 ``` bits = max(4, ceil_log2(5)) = max(4, 3) = 4 @@ -105,7 +103,7 @@ values_per_long = 64 / 4 = 16 long_count = ceil(4096 / 16) = 256 -- 端数なし。全ビット使用 ``` -パレットが 17 要素の場合: +パレットが17要素の場合は次のようになる。 ``` bits = max(4, ceil_log2(17)) = 5 @@ -113,28 +111,24 @@ values_per_long = 64 / 5 = 12 -- 60 ビット使用、上位 4 long_count = ceil(4096 / 12) = 342 ``` -最後の `i64` には `4096 - 341*12 = 4` 個しか入らないため、 -残り 8 スロット分のビットは**ゼロで埋める**。 +最後の`i64`には`4096 - 341*12 = 4`個しか入らないため、残り8スロット分のビットは**ゼロで埋める**。 --- ## 3. 読み込み時の検証 -1. `palette` が空 → `MALFORMED_DATA` -2. `data` の長さが上式の `long_count` と一致しない → `MALFORMED_DATA` - - ただし `ReadOptions.lenient_bit_storage = true` のときは、 - `data` の長さから `bits` を逆算して読む(第三者ツールが書いたデータの救済用) -3. 取り出した添字が `palette` の範囲外 → `MALFORMED_DATA` +1. `palette`が空 → `MALFORMED_DATA` +2. `data`の長さが上式の`long_count`と一致しない → `MALFORMED_DATA` + - ただし`ChunkReadOptions.lenient_bit_storage = true`のときは、`data`の長さから`bits`を逆算して読む(第三者ツールが書いたデータの救済用) +3. 取り出した添字が`palette`の範囲外 → `MALFORMED_DATA` -いずれも黙って 0 番目のパレット要素で代替してはならない。壊れたデータを -そうと分からない形で書き戻してしまうため。 +いずれの場合も、エラーを出さずに0番目のパレット要素で代替してはならない。破損したデータを、そうと分からない形で書き戻してしまうため。 --- ## 4. 書き込み時のビット幅再計算 -`set` でパレットに新しい要素を追加した結果ビット幅が変わる場合は、 -`data` 配列全体を新しいビット幅で詰め直す。 +`set`でパレットに新しい要素を追加した結果、ビット幅が変わる場合は、`data`配列全体を新しいビット幅で詰め直す。 ``` 新しい bits を求める @@ -142,28 +136,24 @@ long_count = ceil(4096 / 12) = 342 -> 変わったら long_count を求め直し、全 entry_count 個を読み直して新配列へ詰める ``` -**未使用パレットの掃除** (`compact()`): どのエントリからも参照されていない -パレット要素を取り除き、添字を振り直す。ビット幅が縮む場合は詰め直す。 -これは明示的に呼んだときだけ行い、`set` のたびに自動実行はしない -(大量の `set` を行う用途で著しく遅くなるため)。 +**未使用パレットの掃除**(`compact()`): どのエントリからも参照されていないパレット要素を取り除き、添字を振り直す。ビット幅が縮む場合は詰め直す。 +`compact()`は明示的に呼んだときだけ行い、`set`のたびに自動実行はしない(大量の`set`を行う用途で著しく遅くなるため)。 ### 4.1 キーの並び順 -compound へ書き出すときは **`data` を先、`palette` を後**に置く。 +compoundへ書き出すときは **`data`を先、`palette`を後**に置く。 -Minecraft 26.2 が実際に書き出したデータを走査したところ、 -`block_states` / `biomes` のどちらも例外なくこの順だった。 +Minecraft 26.2が実際に書き出したデータを走査したところ、`block_states` / `biomes`のどちらも例外なくこの順だった。 | コンテナ | キーの並び | 出現数 | |---|---|---| -| `block_states` | `palette` のみ(`data` 無し) | 67,532 | +| `block_states` | `palette`のみ(`data`無し) | 67,532 | | `block_states` | `data`, `palette` | 16,012 | -| `biomes` | `palette` のみ(`data` 無し) | 73,345 | +| `biomes` | `palette`のみ(`data`無し) | 73,345 | | `biomes` | `data`, `palette` | 10,199 | -NBT の compound は挿入順を保持するため、この並びを揃えないと -**内容が同じでもバイト列が変わる**。触っていないチャンクを書き戻しても -元と一致させるために、並び順まで仕様として固定する。 +NBTのcompoundは挿入順を保持するため、この並びを揃えないと**内容が同じでもバイト列が変わる**。 +触っていないチャンクを書き戻しても元と一致させるために、並び順まで仕様として固定する。 --- @@ -182,5 +172,5 @@ PalettedContainer from_nbt(nbt, entry_count, min_bits) -> PalettedContainer ``` -`min_bits` は `block_states` なら 4、`biomes` なら 1。 -座標から `index` への変換はセクション側([30](30-chunk-format.md))の責務とする。 +`min_bits`は`block_states`なら4、`biomes`なら1。 +座標から`index`への変換はセクション側([30](30-chunk-format.md))の責務とする。 diff --git a/docs/spec/40-world-layout.md b/docs/spec/40-world-layout.md index 64c4997..2bf75b2 100644 --- a/docs/spec/40-world-layout.md +++ b/docs/spec/40-world-layout.md @@ -1,10 +1,10 @@ # 40. ワールドのディレクトリ構成 -Java版のセーブデータのディレクトリ構成と `level.dat` の扱い。 +Java版のセーブデータのディレクトリ構成と`level.dat`の扱い。 -> **この章は Java版 26.2 の実データを走査して確認した内容に基づく。** -> 1.21.x までの構成(`region/` が直下、`DIM-1` / `DIM1`、`playerdata/`)とは**大きく異なる**。 -> 検証方法は [90 適合性](90-conformance.md) の「実ワールド走査」を参照。 +> **この章は、Java版26.2の実データを走査して確認した内容に基づく。** +> 1.21.xまでの構成(`region/`が直下、`DIM-1` / `DIM1`、`playerdata/`)とは大きく異なる。 +> 検証方法は[90 適合性](90-conformance.md)の「実ワールド走査」を参照。 --- @@ -48,7 +48,7 @@ Java版のセーブデータのディレクトリ構成と `level.dat` の扱い ### 1.1 次元の解決 -**標準の3次元も `dimensions/` の下に置かれる。** 特別扱いはない。 +**標準の3次元も`dimensions/`の下に置かれる。** 特別扱いはない。 | 次元ID | ディレクトリ | |---|---| @@ -57,9 +57,9 @@ Java版のセーブデータのディレクトリ構成と `level.dat` の扱い | `minecraft:the_end` | `dimensions/minecraft/the_end/` | | `:` | `dimensions///` | -つまり解決規則は `dimensions/<名前空間>/<パス>/` の**一本だけ**でよい。 +つまり、解決規則は`dimensions/<名前空間>/<パス>/`の一本だけでよい。 -> **1.21.x からの変更点** +> **1.21.xからの変更点** > > | 旧 | 新 | > |---|---| @@ -69,139 +69,129 @@ Java版のセーブデータのディレクトリ構成と `level.dat` の扱い > | `/playerdata/.dat` | `/players/data/.dat` | > | `/advancements/.json` | `/players/advancements/.json` | > | `/stats/.json` | `/players/stats/.json` | -> | `/data/*.dat`(雑多) | `/data/minecraft/<用途>.dat` に整理 | +> | `/data/*.dat`(雑多) | `/data/minecraft/<用途>.dat`に整理 | -生成されていない次元のディレクトリは存在しないことがある -(実データでは `the_nether` / `the_end` に `data/` だけがあり `region/` は無かった)。 -**ディレクトリの有無で判断し、無い場合は「チャンクが 1 つも無い次元」として扱う。** +生成されていない次元のディレクトリは存在しないことがある(実データでは`the_nether` / `the_end`に`data/`だけがあり、`region/`は無かった)。 +**ディレクトリの有無で判断し、無い場合は「チャンクが1つも無い次元」として扱う。** --- ## 2. level.dat -Gzip 圧縮された NBT。ルートは空名の `TAG_Compound` で、`Data` キーの下に本体がある。 +Gzip圧縮されたNBT。ルートは空名の`TAG_Compound`で、`Data`キーの下に本体がある。 -**26.x では大幅に軽量化されている。** ゲームルール・ワールド生成設定・天候・スコアボードなどは -`data/minecraft/` 配下の個別ファイルへ分離された。実データでは 430 バイトしかない。 +**26.xでは大幅に軽量化されている。** ゲームルール・ワールド生成設定・天候・スコアボードなどは、`data/minecraft/`配下の個別ファイルへ分離された。実データでは430バイトしかない。 | キー | 型 | 内容 | |---|---|---| | `Data.DataVersion` | `Int` | `4903` | -| `Data.version` | `Int` | NBT 保存形式のバージョン。`19133` | +| `Data.version` | `Int` | NBT保存形式のバージョン。`19133` | | `Data.LevelName` | `String` | ワールド名 | | `Data.Time` | `Long` | ワールドの経過時間(tick) | -| `Data.LastPlayed` | `Long` | 最終プレイ時刻(Unix ミリ秒) | +| `Data.LastPlayed` | `Long` | 最終プレイ時刻(Unixミリ秒) | | `Data.GameType` | `Int` | 0=サバイバル 1=クリエイティブ 2=アドベンチャー 3=スペクテイター | | `Data.allowCommands` | `Byte` | | | `Data.initialized` | `Byte` | | | `Data.WasModded` | `Byte` | | -| `Data.singleplayer_uuid` | `IntArray` | 4 要素 | +| `Data.singleplayer_uuid` | `IntArray` | 4要素 | | `Data.ServerBrands` | `List` | 例: `["vanilla"]` | | `Data.difficulty_settings` | `Compound` | `difficulty`(String) / `hardcore`(Byte) / `locked`(Byte) | | `Data.spawn` | `Compound` | `pos`(IntArray 3要素) / `pitch`(Float) / `yaw`(Float) / `dimension`(String) | | `Data.Version` | `Compound` | `Id`(Int) / `Name`(String) / `Series`(String) / `Snapshot`(Byte) | -| `Data.DataPacks` | `Compound` | `Enabled` / `Disabled`(どちらも `List`) | +| `Data.DataPacks` | `Compound` | `Enabled` / `Disabled`(どちらも`List`) | -> **1.21.x からの変更点** +> **1.21.xからの変更点** > > | 旧 | 新 | > |---|---| -> | `Data.SpawnX` / `SpawnY` / `SpawnZ` / `SpawnAngle` | `Data.spawn` の compound に統合 | -> | `Data.Difficulty` / `DifficultyLocked` / `hardcore` | `Data.difficulty_settings` に統合 | -> | `Data.GameRules` | `data/minecraft/game_rules.dat` へ分離 | -> | `Data.WorldGenSettings` | `data/minecraft/world_gen_settings.dat` へ分離 | -> | `Data.raining` / `thundering` / `rainTime` / `thunderTime` | `data/minecraft/weather.dat` へ分離 | -> | `Data.DayTime` | `data/minecraft/world_clocks.dat` へ分離(次元ごとの `total_ticks`) | -> | `Data.ScheduledEvents` | `data/minecraft/scheduled_events.dat` へ分離 | -> | `Data.BorderCenterX` ほか | 次元ごとの `data/minecraft/world_border.dat` へ分離 | -> | `Data.DragonFight` | `the_end` の `data/minecraft/ender_dragon_fight.dat` へ分離 | - -`Chunk` と同様、**未知のキーもすべて保持する**。 +> | `Data.SpawnX` / `SpawnY` / `SpawnZ` / `SpawnAngle` | `Data.spawn`のcompoundに統合 | +> | `Data.Difficulty` / `DifficultyLocked` / `hardcore` | `Data.difficulty_settings`に統合 | +> | `Data.GameRules` | `data/minecraft/game_rules.dat`へ分離 | +> | `Data.WorldGenSettings` | `data/minecraft/world_gen_settings.dat`へ分離 | +> | `Data.raining` / `thundering` / `rainTime` / `thunderTime` | `data/minecraft/weather.dat`へ分離 | +> | `Data.DayTime` | `data/minecraft/world_clocks.dat`へ分離(次元ごとの`total_ticks`) | +> | `Data.ScheduledEvents` | `data/minecraft/scheduled_events.dat`へ分離 | +> | `Data.BorderCenterX`ほか | 次元ごとの`data/minecraft/world_border.dat`へ分離 | +> | `Data.DragonFight` | `the_end`の`data/minecraft/ender_dragon_fight.dat`へ分離 | + +`Chunk`と同様、**未知のキーもすべて保持する**。 ### 2.1 分離されたデータファイル -`data/minecraft/*.dat` はどれも同じ形をしている。 +`data/minecraft/*.dat`はどれも同じ形をしている。 ``` { data: { …本体… }, DataVersion: 4903 } ``` -ルートは空名の `TAG_Compound`、その直下に `data` と `DataVersion` が並ぶ。 -`level.dat` だけが大文字の `Data` である点に注意。 +ルートは空名の`TAG_Compound`、その直下に`data`と`DataVersion`が並ぶ。 +`level.dat`だけが大文字の`Data`である点に注意。 -| ファイル | `data` の中身 | +| ファイル | `data`の中身 | |---|---| -| `game_rules.dat` | ゲームルール名(`minecraft:` 付きの名前空間つき)→ 値 | +| `game_rules.dat` | ゲームルール名(名前空間`minecraft:`つき)→ 値 | | `weather.dat` | `raining` / `thundering` / `rain_time` / `thunder_time` / `clear_weather_time` | | `world_clocks.dat` | 次元ID → `{ total_ticks: Long }` | | `world_gen_settings.dat` | `seed`(Long) / `generate_structures` / `bonus_chest` / `dimensions` | -| `random_sequences.dat` | `salt` と、シーケンス名 → `{ source: LongArray[2] }` | +| `random_sequences.dat` | `salt`と、シーケンス名 → `{ source: LongArray[2] }` | | `scheduled_events.dat` | `events`(List) | | `stopwatches.dat` | `stopwatches`(Compound) | | `world_border.dat`(次元ごと) | `center_x` / `center_z` / `size` / `damage_per_block` / `safe_zone` / `warning_blocks` / `warning_time` / `lerp_target` / `lerp_time` | | `chunk_tickets.dat`(次元ごと) | 読み込み維持チケット。未使用なら空 | -ゲームルール名が**名前空間つきになった**(旧 `doFireTick` → `minecraft:fire_spread_radius_around_player` のように、 -名前自体が変わっているものもある)ことに注意。 +ゲームルール名が名前空間つきになったことに注意。旧`doFireTick` → `minecraft:fire_spread_radius_around_player`のように、名前自体が変わっているものもある。 ### 2.2 書き込み時の安全策 -`level.dat` が壊れるとワールド全体が開けなくなるため、書き込みは次の順で行う。 +`level.dat`の中身がおかしくなるとワールド全体が開けなくなるため、書き込みは次の順で行う。 -1. 一時ファイル `level.dat.tmp` へ書く -2. `fsync` 相当でディスクへ確定させる -3. 既存の `level.dat` を `level.dat_old` へ移す -4. `level.dat.tmp` を `level.dat` へリネームする +1. 一時ファイル`level.dat.tmp`へ書き、fsyncで内容をディスクへ確実に書き出す +2. 既存の`level.dat`を`level.dat_old`へコピーする +3. `level.dat.tmp`を`level.dat`へリネームする -`players/data/.dat` も同じく `.dat_old` を持つので、同じ手順を使う。 +`players/data/.dat`も`.dat_old`を持つ。プレイヤーデータを書き込むAPIを設ける場合は、同じ手順を使う。 --- ## 3. session.lock -Minecraft は起動中このファイルを排他ロックする(実データでは 3 バイトだった)。 -ロックされたワールドへ書き込むとデータが壊れるため、本ライブラリは -**書き込みモードで開くときに確認する**。 +Minecraftは起動中、このファイルを排他ロックする(実データでは3バイトだった)。 +ロックされたワールドへ書き込むとデータがおかしくなるため、本ライブラリは**書き込みモードで開くときに確認する**。 -- ロックを取得できない → `IO`(メッセージで「Minecraft が起動中の可能性」を示す) -- `WorldOpenOptions.ignore_session_lock = true` で明示的に無視できる(自己責任) +- ロックを取得できない → `IO`(メッセージで「Minecraftが起動中の可能性」を示す) +- `WorldOpenOptions.ignore_session_lock = true`で明示的に無視できる(自己責任) -**ファイルの存在では判定できない。** `session.lock` は Minecraft を終了しても -残り続けるため、「ロックを取れるか」を試すしかない。 +**ファイルの存在では判定できない。** `session.lock`はMinecraftを終了しても残り続けるため、「ロックを取れるか」を試すしかない。 ### 3.1 言語による差異 -排他ロックを標準ライブラリで扱えるかは言語で分かれるため、 -**この確認だけは全言語で揃わない**(→ [adr/0008](../adr/0008-session-lock.md))。 +排他ロックを標準ライブラリで扱えるかは言語で分かれるため、**この確認だけは全言語で揃わない**(→ [adr/0008](../adr/0008-session-lock.md))。 | 言語 | 確認する | 手段 | |---|:--:|---| -| C# | ✅ | `FileStream` を `FileShare.None` で開く | -| Java | ✅ | `FileChannel.tryLock()` | -| Python | 🔶 | `fcntl.flock`。`fcntl` の無い環境(Windows)では確認しない | -| TypeScript | ❌ | Node に移植性のあるファイルロック API が無い | -| Rust | ❌ | `std` に無い。外部クレートが要る | +| C# | ○ | `FileStream`を`FileShare.None`で開く | +| Java | ○ | `FileChannel.tryLock()` | +| Python | △ | `fcntl.flock`。`fcntl`の無い環境(Windows)では確認しない | +| TypeScript | ✕ | Nodeに移植性のあるファイルロックAPIが無い | +| Rust | ✕ | `std`に無い。外部クレートが要る | -確認しない言語でも `ignore_session_lock` は受け取る(API の形を揃えるため。 -効果は無い)。**確認しない言語を使う場合、Minecraft が起動していないことは -呼び出し側で担保すること。** +確認しない言語でも`ignore_session_lock`は受け取る。APIの形を揃えるためで、効果は無い。**確認しない言語を使う場合、Minecraftが起動していないことは呼び出し側で保証すること。** -読み取り専用で開く場合はロックを取得しないが、 -起動中のワールドは書き込み途中の状態を読む可能性があることを警告する。 +読み取り専用で開く場合はロックを取得しないが、起動中のワールドでは書き込み途中の状態を読む可能性があることを警告する。 --- ## 4. 論理API ``` -World - open(dir, options) -> World +MinecraftWorld + open(dir, options) -> MinecraftWorld level() -> LevelData -- level.dat の内容 save_level() data_file(name) -> Option -- data/minecraft/.dat - dimension(dimension_id) -> Dimension - dimensions() -> Iterator -- 存在するものだけ - players() -> Iterator + dimension(dimension_id) -> Option + dimension_ids() -> Iterator -- 存在するものだけ + player_ids() -> Iterator player(uuid) -> Option close() @@ -219,9 +209,6 @@ Dimension set_biome(x, y, z, biome) ``` -`get_block` / `set_block` は**絶対ワールド座標**を取り、リージョン・チャンク・セクションを -内部で解決する。存在しないチャンクに対する `get_block` は `None`、 -`set_block` は `INVALID_ARGUMENT`(チャンクを生成する機能は持たない)。 +`get_block` / `set_block`は**絶対ワールド座標**を取り、リージョン・チャンク・セクションを内部で解決する。存在しないチャンクに対する`get_block`は`None`、`set_block`は`INVALID_ARGUMENT`(チャンクを生成する機能は持たない)。 -`region_folder()` などが `Option` を返すのは、 -**生成されていない次元にはディレクトリ自体が無い**ため。 +`dimension()`や`region_folder()`などが`Option`を返すのは、生成されていない次元にはディレクトリ自体が無いため。 diff --git a/docs/spec/90-conformance.md b/docs/spec/90-conformance.md index f87f342..053148e 100644 --- a/docs/spec/90-conformance.md +++ b/docs/spec/90-conformance.md @@ -9,82 +9,85 @@ ``` spec/testdata/ -├─ nbt/ 入力バイナリ (.nbt) と、対応する SNBT (.snbt) -├─ anvil/ .mca / .mcc -├─ world/ 最小ワールド一式 -└─ expect/ 正規化JSON の期待値(入力と同じ相対パス + .json) +├─ nbt/ 入力バイナリ (.nbt) +├─ concat/ NBTが複数連なったバイナリ (.nbt) +├─ anvil/ ベクタごとのディレクトリに .mca / .mcc +├─ world/ チャンク1つ分のNBT (.nbt) +└─ expect/ 正規化JSONの期待値(ベクタID + .json) ``` -`spec/testdata/manifest.json` が全ベクタの一覧と、各々が検証する内容を持つ。 +`spec/testdata/manifest.json`は、全ベクタの一覧と、各ベクタが検証する内容を持つ。 ```json { "vectors": [ { "id": "nbt/all_tags", + "kind": "nbt", "input": "nbt/all_tags.nbt", - "expect": "expect/nbt/all_tags.json", - "snbt": "nbt/all_tags.snbt", "format": "java", "compression": "none", - "roundtrip": true, - "description": "全13タグを1つずつ含む" + "description": "全13タグを含む。TAG_EndはCompoundの終端として現れる", + "expect": "expect/nbt/all_tags.json", + "roundtrip": true } ] } ``` -`roundtrip: false` のベクタは「読めるが、書き戻すとバイトが変わる」ことが -仕様上正しいもの(第三者ツールが書いた非正準なデータなど)を表す。 +読み込みが失敗することを確かめるベクタは、`expect`の代わりに`expect_error`(期待する`ErrorCode`)を持つ。 + +`roundtrip: false`のベクタは、「読めるが、書き戻すとバイトが変わる」ことが仕様上正しいもの(第三者ツールが書いた非正準なデータなど)を表す。 +読み込みが失敗するベクタと、連なったNBTのベクタも`roundtrip: false`になる。 --- -## 2. 検証の3種類 +## 2. 検証の種類 ### 2.1 デコード一致 -入力バイナリを読み、[00 共通規約 6章](00-conventions.md#6-正規化json適合性検証の中間表現)の -正規化JSON へ変換し、`expect/` のファイルと**文字列として完全一致**することを確認する。 +入力バイナリを読み、[00 共通規約 6章](00-conventions.md#6-正規化json適合性検証の中間表現)の正規化JSONへ変換し、`expect/`のファイルと**文字列として完全一致**することを確認する。 -JSON の出力規則(全言語で一致させるため厳密に定める): +全言語で一致させるため、JSONの出力規則を次のように厳密に定める。 - キーの順序は本仕様に書かれた順(`type` → `element_type` → `value` → `mutf8`) -- 区切りは `,` と `:`(**空白なし**) -- 非 ASCII 文字は `\uXXXX` へエスケープする +- 区切りは`,`と`:`(**空白なし**) +- 非ASCII文字は`\uXXXX`へエスケープする - 末尾に改行を1つ付ける +期待値ファイルを持つのは、読み込みに成功するNBTのベクタだけである。 +Anvil・World・連なったNBTのベクタは、2.3のクロス言語一致で確かめる。 + ### 2.2 ラウンドトリップ -`read(bytes) -> write() -> bytes'` で `bytes == bytes'` を確認する。 +`read(bytes) -> write() -> bytes'`で`bytes == bytes'`を確認する。 -- 圧縮ありのベクタは、**展開後のバイト列**で比較する(圧縮結果は zlib 実装のバージョンで変わるため) -- `roundtrip: false` のベクタはこの検証をスキップする +- 圧縮ありのベクタは、**展開後のバイト列**で比較する(圧縮結果はzlib実装のバージョンで変わるため) +- `roundtrip: false`のベクタはこの検証をスキップする ### 2.3 クロス言語一致 -`spec/run-conformance.sh` が全言語それぞれの CLI 検証ツールを起動し、 -同じ入力に対する出力(正規化JSON と再書き出しバイト列)を相互に diff する。 +`spec/run-conformance.sh`が全言語それぞれのCLI検証ツールを起動し、同じ入力に対する出力(正規化JSONと再書き出しバイト列)を相互にdiffする。 各言語は次のインターフェースを持つ検証ツールを提供する。 ``` - decode <入力パス> <出力JSONパス> - encode <入力パス> <出力バイナリパス> - snbt <入力パス> <出力SNBTパス> + decode <入力パス> <出力JSONパス> [--format network] + encode <入力パス> <出力バイナリパス> [--format network] + snbt <入力パス> <出力SNBTパス> [--format network] + nbt-list <入力パス> <出力テキストパス> [--format network] region-list <入力.mcaパス> <出力テキストパス> - region-rewrite <入力.mcaパス> <出力ディレクトリ> + region-rewrite <入力.mcaパス> <出力.mcaパス> chunk-report <入力チャンクnbtパス> <出力テキストパス> chunk-edit <入力チャンクnbtパス> <出力nbtパス> version ``` -`chunk-report` はチャンクの全 4096 ブロック(と 4×4×4 のバイオーム)を -1 つずつ読み出して種類ごとに数え上げる。パレットとビット詰めの取り出しを -端から端まで通すので、どこか 1 か所でも境界の扱いを誤れば集計値が変わる。 +`nbt-list`は連なったNBT([10 3.1](10-nbt-binary.md#31-連なったnbtを読む))を`read_bytes_all`と`read_bytes_at`の両方で読み、個数と各NBTの開始・終了位置を書き出す。 + +`chunk-report`は、チャンクの全4096ブロック(と4×4×4のバイオーム)を1つずつ読み出して種類ごとに数え上げる。パレットとビット詰めの取り出しを端から端まで通すので、どこか1か所でも境界の扱いを誤れば集計値が変わる。 -`chunk-edit` は決まった手順(パレット拡張・ビット幅の再計算・`compact()`・ -高さマップの無効化)でチャンクを編集し、無圧縮 NBT として書き出す。 -全言語で**バイト単位に一致**しなければならない。 +`chunk-edit`は決まった手順(パレット拡張・ビット幅の再計算・`compact()`・高さマップの無効化)でチャンクを編集し、無圧縮NBTとして書き出す。全言語で**バイト単位に一致**しなければならない。 | 言語 | 起動方法 | |---|---| @@ -94,52 +97,43 @@ JSON の出力規則(全言語で一致させるため厳密に定める): | Python | `python -m spring_nbt_library.conformance` | | Rust | `rust/target/release/examples/conformance` | -`spec/tools/run_conformance.py` が各言語のビルドと起動を引き受ける。 -**実装が無い言語は自動的に検証対象から外れる**ので、 -言語を 1 つずつ追加していく途中でも走らせられる。 +`spec/tools/run_conformance.py`が各言語のビルドと起動を引き受ける。実装が無い言語は自動的に検証対象から外れるので、言語を1つずつ追加していく途中でも走らせられる。 ---- +### 2.4 実ワールド走査 -## 2.4 実ワールド走査 - -合成ベクタだけでは、実際の Minecraft が書き出すデータの網羅性に届かない。 -`spec/tools/scan_world.py` が手元のワールドを丸ごと読んで、次を確認する。 +合成ベクタだけでは、実際のMinecraftが書き出すデータを網羅しきれない。 +`spec/tools/scan_world.py`が手元のワールドを丸ごと読んで、次を確認する。 ```bash python3 spec/tools/scan_world.py "<ワールドのパス>" --verbose ``` -やること: +`scan_world.py`は次の処理を行う。 -1. すべての `.dat` / `.nbt` を読み、ラウンドトリップ(読む→書く→バイト一致)を検証 -2. すべての `.mca` のヘッダを解析し、全チャンクを展開して NBT として読み、同じく検証 +1. すべての`.dat` / `.nbt`を読み、ラウンドトリップ(読む→書く→バイト一致)を検証 +2. すべての`.mca`のヘッダを解析し、全チャンクを展開してNBTとして読み、同じく検証 3. セクタの重複・不正オフセット・ファイル長の非整列を検出 -4. パレット長から求めたビット幅と、実際の `data` 長が一致するかを検証([31](31-paletted-container.md)) -5. ルート直下キー・セクションキー・`Status` の出現数を集計し、**仕様書とのズレを見つける** -6. World レイヤで全チャンクを解釈し直し、書き戻したバイト列が原本と一致するかを検証。 - さらに一部のチャンクは全ブロック・全バイオームを 1 つずつ読み出す - (`--block-sample` で件数を変えられる) +4. パレット長から求めたビット幅と、実際の`data`長が一致するかを検証([31](31-paletted-container.md)) +5. ルート直下キー・セクションキー・`Status`の出現数を集計し、仕様書とのズレを見つける +6. Worldレイヤで全チャンクを解釈し直し、書き戻したバイト列が原本と一致するかを検証。さらに一部のチャンクは全ブロック・全バイオームを1つずつ読み出す(`--block-sample`で件数を変えられる) -**このツールは一切書き込まない。** ただし Minecraft を終了させてから実行すること。 +**このツールは一切書き込まない。** ただし、Minecraftを終了させてから実行すること。 -ワールドのデータ自体はリポジトリに取り込まない(個人のセーブデータであるため)。 +ワールドのデータ自体は、個人のセーブデータであるためリポジトリに取り込まない。 検証したい人が自分のワールドを指定して走らせる形にしてある。 -### 検証実績 +#### 検証実績 | 日付 | 対象 | 結果 | |---|---|---| -| 2026-08-29 | Java版 26.2 の実ワールド(DataVersion 4903) | `.dat` 23個 + チャンク 3,717個 = **3,740件すべて読み込み・バイト一致のラウンドトリップ成功**。失敗 0 | -| 2026-08-29 | 同ワールドから抽出した代表14ファイル × 5言語 | 正規化JSON と SNBT が**全言語で完全一致**。書き戻したバイト列も**70件すべて原本と一致** | -| 2026-08-29 | 同ワールドのリージョンファイル 12個 × 5言語 | チャンク一覧と詰め直したバイト列が**全言語で完全一致**(3,717チャンク)。開いて無変更で書き戻すと**バイト単位で原本と一致** | -| 2026-08-29 | 同ワールドを World レイヤ(`MinecraftWorld` / `Chunk` / `PalettedContainer`)で解釈 | 3,481 チャンク・83,544 セクションを解釈し、NBT へ書き戻したバイト列が**全件原本と一致**。うち 40 チャンクは全ブロックを 1 つずつ読み出し(**393万ブロック**、79 種類)、バイオームも含めて成功。失敗 0 | +| 2026-08-29 | Java版26.2の実ワールド(DataVersion 4903) | `.dat` 23個 + チャンク3,717個 = 3,740件すべてで読み込みとバイト一致のラウンドトリップに成功。失敗0 | +| 2026-08-29 | 同ワールドから抽出した代表14ファイル × 5言語 | 正規化JSONとSNBTが全言語で完全一致。書き戻したバイト列も70件すべて原本と一致 | +| 2026-08-29 | 同ワールドのリージョンファイル12個 × 5言語 | チャンク一覧と詰め直したバイト列が全言語で完全一致(3,717チャンク)。開いて無変更で書き戻すと、バイト単位で原本と一致 | +| 2026-08-29 | 同ワールドをWorldレイヤ(`MinecraftWorld` / `Chunk` / `PalettedContainer`)で解釈 | 3,481チャンク・83,544セクションを解釈し、NBTへ書き戻したバイト列が全件原本と一致。うち40チャンクは全ブロックを1つずつ読み出し(393万ブロック、79種類)、バイオームも含めて成功。失敗0 | -パレット付きコンテナ([31](31-paletted-container.md))については、 -83,544 セクション = **167,088 個のコンテナすべて**で、 -パレット長から求めたビット幅と実際の `data` 長が一致することを確認した。 +パレット付きコンテナ([31](31-paletted-container.md))については、83,544セクション = 167,088個のコンテナすべてで、パレット長から求めたビット幅と実際の`data`長が一致することを確認した。 -この走査により、**[40 ワールドのディレクトリ構成](40-world-layout.md) が 26.x で大きく変わっていること** -(`dimensions/` への集約、`players/` への改名、`level.dat` からのデータ分離)が判明し、仕様書を全面的に書き直した。 +この走査で、[40 ワールドのディレクトリ構成](40-world-layout.md)が26.xで大きく変わっていること(`dimensions/`への集約、`players/`への改名、`level.dat`からのデータ分離)が分かり、仕様書を全面的に書き直した。 --- @@ -149,59 +143,75 @@ python3 spec/tools/scan_world.py "<ワールドのパス>" --verbose | ID | 検証内容 | |---|---| -| `nbt/hello_world` | 最小の Compound | -| `nbt/all_tags` | 全13タグを1つずつ | -| `nbt/nested_deep` | ネスト深さ 500(上限 512 の直下) | -| `nbt/nested_too_deep` | ネスト深さ 513 → `LIMIT_EXCEEDED` | -| `nbt/empty_list` | 空リスト(要素型 End) | -| `nbt/empty_list_typed` | 空リストだが要素型が Byte(第三者ツール由来) | +| `nbt/hello_world` | 最小のCompound。ルート名が空でない | +| `nbt/all_tags` | 全13タグ(TAG_EndはCompoundの終端として現れる) | +| `nbt/nested_deep` | ネスト深さ500(上限512の直下) | +| `nbt/nested_too_deep` | ネスト深さ600 → `LIMIT_EXCEEDED` | +| `nbt/empty_list` | 空リスト(要素型End) | +| `nbt/empty_list_typed` | 空リストだが要素型がByte(第三者ツール由来) | | `nbt/numeric_bounds` | 各整数型の最小値・最大値 | | `nbt/float_specials` | `+0.0` `-0.0` `Infinity` `-Infinity` `NaN` | -| `nbt/mutf8_nul` | `U+0000` を含む文字列 | +| `nbt/mutf8_nul` | `U+0000`を含む文字列 | | `nbt/mutf8_supplementary` | 補助文字(絵文字)を含む文字列 | | `nbt/mutf8_lone_surrogate` | 孤立サロゲート | -| `nbt/mutf8_max_length` | 65535 バイトちょうどの文字列 | +| `nbt/mutf8_max_length` | 65535バイトちょうどの文字列 | | `nbt/gzip` / `nbt/zlib` / `nbt/uncompressed` | 3種の圧縮を自動判定して読む | | `nbt/network_format` | 無名ルート(1.20.2+) | | `nbt/truncated` | 途中で切れた入力 → `MALFORMED_DATA` | -| `nbt/huge_declared_length` | 長さ `0x7FFFFFFF` の宣言 → `MALFORMED_DATA` | +| `nbt/huge_declared_length` | 長さ`0x7FFFFFFF`の宣言 → `MALFORMED_DATA` | | `nbt/unknown_tag_id` | タグID 13 → `MALFORMED_DATA` | +| `nbt/negative_length` | 長さフィールドが負値 → `MALFORMED_DATA` | +| `nbt/trailing_bytes` | ルートの後に余分なバイトがある → `MALFORMED_DATA` | +| `nbt/mutf8_lone_surrogate_key` | Compoundのキーが孤立サロゲート → `MALFORMED_DATA` | +| `nbt/mutf8_raw_nul` | MUTF-8に反する素の`0x00`を含む文字列 → `MALFORMED_DATA` | + +### 連なったNBT + +| ID | 検証内容 | +|---|---| +| `concat/three_tags` | 同じNBTが3つ連なっている | +| `concat/mixed` | 大きさの違うNBTが3つ連なっている | +| `concat/single` | NBTが1つだけ | ### Anvil | ID | 検証内容 | |---|---| -| `anvil/empty` | 全エントリ 0 のリージョン | +| `anvil/empty` | 全エントリ0のリージョン | | `anvil/single_chunk` | チャンク1つ | -| `anvil/fragmented` | 隙間のある配置。読み書き後も他チャンクを壊さない | -| `anvil/external_mcc` | `.mcc` へ退避されたチャンク | -| `anvil/mixed_compression` | 圧縮ID 1 / 2 / 3 が混在 | +| `anvil/fragmented` | 隙間のある配置。読み書き後も他チャンクのデータがおかしくならない | +| `anvil/external_mcc` | `.mcc`へ退避されたチャンク | +| `anvil/mixed_compression` | 圧縮ID 1 / 2 / 3が混在 | +| `anvil/lz4` | 圧縮ID 4(LZ4)。1ブロック / 2ブロック連結 / 無圧縮ブロック / 重なりのあるマッチ | +| `anvil/lz4_bad_magic` | LZ4Blockのマジックがおかしい → `MALFORMED_DATA` | | `anvil/bad_offset` | ヘッダ領域を指すオフセット → `MALFORMED_DATA` | | `anvil/overlapping_sectors` | 2チャンクが同じセクタを指す → `MALFORMED_DATA` | +| `anvil/unaligned_length` | ファイル長が4096の倍数でない → `MALFORMED_DATA` | +| `anvil/offset_out_of_file` | オフセットがファイル末尾より後ろを指す → `MALFORMED_DATA` | ### World / Block | ID | 検証内容 | |---|---| -| `world/minimal` | level.dat + region 1ファイルの最小ワールド | -| `world/palette_1` | パレット1要素(`data` なし)のセクション | -| `world/palette_5` | ビット幅 4、端数なし | -| `world/palette_17` | ビット幅 5、最後の long に端数 | -| `world/palette_grow` | `set_block` でビット幅が 4→5 へ拡張される | -| `world/palette_compact` | `compact()` で未使用要素が消えビット幅が縮む | -| `world/biome_rw` | 4×4×4 解像度のバイオーム読み書き | +| `world/palette_1` | パレット1要素(`data`なし)のセクション | +| `world/palette_5` | ビット幅4、端数なし | +| `world/palette_17` | ビット幅5、最後のlongに端数 | +| `world/multi_section` | セクション3個。バイオームも2種(ビット幅1) | +| `world/palette_unused` | 参照されていないパレット要素が2つある。`compact()`で減る | +| `world/block_entities` | `block_entities` / `block_ticks` / `fluid_ticks`を持つチャンク | +| `world/proto_chunk` | 生成途中のチャンク(`Status`が`minecraft:full`でない) | +| `world/palette_index_out_of_range` | 添字がパレットの範囲外を指す → `MALFORMED_DATA` | +| `world/bitstorage_wrong_length` | `data`の長さがパレット長から求めたビット幅と合わない → `MALFORMED_DATA` | --- ## 4. ベクタの生成方針 -期待値が生成器のバグを写してしまわないよう、次の順で作る。 +ライブラリのバグがそのまま期待値にならないよう、次の順で作る。 -1. **手書きの16進**で最小ベクタ(`hello_world`、`all_tags`)を定義する。 - `spec/tools/build_testdata.py` にバイト列を直接書き下し、 - 仕様書の記述だけを根拠に組み立てる -2. その最小ベクタで Rust 実装を検証する -3. 検証済みの Rust 実装を使って、大きなベクタ(`nested_deep` など)を生成する -4. 生成されたベクタを他3言語で読み、一致することを確認する +1. `spec/tools/build_testdata.py`が、入力ベクタ(NBT・連なったNBT・Anvil・World)と`manifest.json`をすべて生成する。このスクリプトはライブラリを使わず、仕様書の記述だけを根拠にした独立の最小NBTライタでバイト列を組み立てる。大きなベクタ(`nested_deep`など)もここで作る +2. `nbt/hello_world`の期待値(`expect/nbt/hello_world.json`)だけは手で書く +3. `spec/tools/run_conformance.py --generate-expect`で、基準実装(C#)の出力からNBTベクタの`expect/`を生成する。手書きの`hello_world`は上書きせず、基準実装の出力と一致するかを確かめる +4. 生成した期待値と全言語の出力を比べ、一致することを確認する(2章) -つまり **手書きベクタが信頼の起点**であり、生成器は増幅にのみ使う。 +つまり、**ライブラリを使わずに作った入力ベクタと手書きの期待値が信頼の起点**であり、基準実装の出力は期待値を増やすためにだけ使う。 diff --git a/java/README.md b/java/README.md index 3c43df7..c3859ea 100644 --- a/java/README.md +++ b/java/README.md @@ -1,19 +1,18 @@ -# SpringNBTLibrary(Java 版) +# SpringNBTLibrary(Java版) -Minecraft Java版 26.1 以降のワールド・NBTファイルを読み書きするライブラリです。 -Java 21 (LTS) 以上で動きます。 +Minecraft Java版26.1以降のワールド・NBTファイルを読み書きするライブラリです。 +Java 21 (LTS)以上で動きます。 -いまのワールド形式は 26.1 で入ったものです。 +いまのワールド形式は26.1で入ったものです。 それ以降のバージョンなら、形式が変わらない限りそのまま扱えます。 -C# / Java / TypeScript / Python / Rust 版があり、どれも同じ仕様書から作っています。 +C# / Java / TypeScript / Python / Rust版があり、どれも同じ仕様書から作っています。 同じ入力からは同じ結果が出ます([対応言語の一覧](../README.md#対応言語))。 -揃っているかは[適合性テスト](../docs/spec/90-conformance.md)で毎回確かめています。 +結果が揃っているかは[適合性テスト](../docs/spec/90-conformance.md)で毎回確かめています。 ## 導入 -[Releases](https://github.com/ScriptArts/SpringNBTLibrary/releases) から `spring-nbt-library-<版>.jar` を落として、 -クラスパスへ追加します。 +[Releases](https://github.com/ScriptArts/SpringNBTLibrary/releases)から`spring-nbt-library-<版>.jar`を落として、クラスパスへ追加します。 ```groovy dependencies { @@ -23,12 +22,12 @@ dependencies { ## 使い方 -- [はじめに(Java)](../docs/getting-started/java.md) — まずここから -- [ガイド](../docs/guide/01-nbt.md) — 目的別の使い方 -- [API 対応表](../docs/api/overview.md) — 他言語版との対応 -- [機能一覧](../docs/features.md) — 何ができて何ができないか +- [はじめに(Java)](../docs/getting-started/java.md): まずここから +- [ガイド](../docs/guide/01-nbt.md): 目的別の使い方 +- [API対応表](../docs/api/overview.md): 他言語版との対応 +- [機能一覧](../docs/features.md): 何ができて何ができないか -説明は [`docs/`](../docs/README.md) にまとめてあります。 +説明は[`docs/`](../docs/README.md)にまとめてあります。 ## ライセンス diff --git a/java/pom.xml b/java/pom.xml index 0f7aec7..4659d6f 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -10,7 +10,7 @@ jar SpringNBTLibrary - Minecraft Java Edition の NBT / Anvil ワールドデータを読み書きするライブラリ + Minecraft Java EditionのNBT / Anvilワールドデータを読み書きするライブラリ https://github.com/scriptarts/SpringNBTLibrary @@ -27,8 +27,7 @@ - + org.junit.jupiter diff --git a/java/src/main/java/io/github/scriptarts/springnbt/ErrorCode.java b/java/src/main/java/io/github/scriptarts/springnbt/ErrorCode.java index 5e23667..63f2ead 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/ErrorCode.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/ErrorCode.java @@ -2,9 +2,7 @@ /** * エラーの分類 - * 4言語すべてで同一の集合を持つ - * - *

仕様: {@code docs/spec/00-conventions.md} 4章 + * 全言語で同一の集合を持つ */ public enum ErrorCode { @@ -17,7 +15,7 @@ public enum ErrorCode { /** 期待した型と違うタグを取り出した */ UNEXPECTED_TAG_TYPE, - /** 仕様上は妥当だが、このビルドでは扱えない */ + /** 仕様上は妥当だが、このライブラリでは扱えない */ UNSUPPORTED_FEATURE, /** 安全上限を超えた */ @@ -26,7 +24,7 @@ public enum ErrorCode { /** 呼び出し側の引数が不正 */ INVALID_ARGUMENT, - /** 対象バージョン外のデータ */ + /** 扱える形式より古いデータ */ UNSUPPORTED_DATA_VERSION; /** diff --git a/java/src/main/java/io/github/scriptarts/springnbt/SpringNbt.java b/java/src/main/java/io/github/scriptarts/springnbt/SpringNbt.java index 19985e7..63ec61f 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/SpringNbt.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/SpringNbt.java @@ -1,25 +1,24 @@ package io.github.scriptarts.springnbt; /** - * SpringNBTLibrary のライブラリ全体に関わる定数 + * SpringNBTLibraryのライブラリ全体に関わる定数 * - *

仕様は {@code docs/spec/} を唯一の正とする - * 対象は 26.1 で導入されたワールド形式 (DataVersion 4786 以降) + * 対象は26.1で導入されたワールド形式 (DataVersion 4786以降) */ public final class SpringNbt { /** - * このライブラリが扱えるワールド形式の下限となる DataVersion (26.1) + * このライブラリが扱えるワールド形式の下限となるDataVersion (26.1) * - *

26.1 で次元とプレイヤーデータの置き場が変わり、いまの形式になった + *

26.1で次元とプレイヤーデータの置き場が変わり、いまの形式になった * これ以降のバージョンは、形式が同じであればそのまま読み書きできる * - *

これより古いワールドは構成そのものが違うので、 - * 読み込み時に {@code UNSUPPORTED_DATA_VERSION} の対象になる + *

これより古いワールドは構成そのものが違う + * そうしたチャンクは、読み込み時は既定で警告を出し、書き戻しは既定で{@code UNSUPPORTED_DATA_VERSION}にする */ public static final int MIN_SUPPORTED_DATA_VERSION = 4786; - /** 動作を確かめた Minecraft Java版の DataVersion (26.2) */ + /** 動作を確かめたMinecraft Java版のDataVersion (26.2) */ public static final int TARGET_DATA_VERSION = 4903; private SpringNbt() { diff --git a/java/src/main/java/io/github/scriptarts/springnbt/SpringNbtException.java b/java/src/main/java/io/github/scriptarts/springnbt/SpringNbtException.java index 09052b7..5227b2e 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/SpringNbtException.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/SpringNbtException.java @@ -2,12 +2,10 @@ /** * 本ライブラリが送出する例外 - * 分類は {@link #code()} で判別する + * 分類は{@link #code()}で判別する * - *

検査例外にしていないのは、4言語でメソッドのシグネチャを揃えるため - * 入出力エラーも {@link ErrorCode#IO} でラップして送出する - * - *

仕様: {@code docs/spec/00-conventions.md} 4章 / {@code docs/adr/0005-unified-error-model.md} + *

検査例外にしていないのは、全言語でメソッドのシグネチャを揃えるため + * 入出力エラーも{@link ErrorCode#IO}でラップして送出する */ public final class SpringNbtException extends RuntimeException { @@ -53,7 +51,7 @@ public String toString() { } /** - * {@link ErrorCode#MALFORMED_DATA} の例外を作る + * {@link ErrorCode#MALFORMED_DATA}の例外を作る * * @param message 説明 * @return 例外 @@ -63,7 +61,7 @@ public static SpringNbtException malformed(String message) { } /** - * {@link ErrorCode#INVALID_ARGUMENT} の例外を作る + * {@link ErrorCode#INVALID_ARGUMENT}の例外を作る * * @param message 説明 * @return 例外 @@ -73,7 +71,7 @@ public static SpringNbtException invalidArgument(String message) { } /** - * {@link ErrorCode#UNEXPECTED_TAG_TYPE} の例外を作る + * {@link ErrorCode#UNEXPECTED_TAG_TYPE}の例外を作る * * @param message 説明 * @return 例外 @@ -83,7 +81,7 @@ public static SpringNbtException unexpectedTagType(String message) { } /** - * {@link ErrorCode#LIMIT_EXCEEDED} の例外を作る + * {@link ErrorCode#LIMIT_EXCEEDED}の例外を作る * * @param message 説明 * @return 例外 @@ -93,7 +91,7 @@ public static SpringNbtException limitExceeded(String message) { } /** - * {@link ErrorCode#UNSUPPORTED_FEATURE} の例外を作る + * {@link ErrorCode#UNSUPPORTED_FEATURE}の例外を作る * * @param message 説明 * @return 例外 diff --git a/java/src/main/java/io/github/scriptarts/springnbt/anvil/ChunkCompression.java b/java/src/main/java/io/github/scriptarts/springnbt/anvil/ChunkCompression.java index d4d59f8..a71db36 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/anvil/ChunkCompression.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/anvil/ChunkCompression.java @@ -4,12 +4,10 @@ /** * リージョンファイル内でチャンクに使われる圧縮方式 - * {@link #id()} は仕様が定める圧縮方式IDと一致する + * {@link #id()}は仕様が定める圧縮方式IDと一致する * - *

NBT 層の {@link io.github.scriptarts.springnbt.nbt.Compression} とは別物であることに注意 - * あちらはファイル全体の圧縮を表し、こちらはリージョン内の 1 チャンクに付く 1 バイトのIDを表す - * - *

仕様: {@code docs/spec/20-anvil-region.md} 3.1章 + *

NBT層の{@link io.github.scriptarts.springnbt.nbt.Compression}とは違うものであることに注意 + * あちらはファイル全体の圧縮を表し、こちらはリージョン内の1チャンクに付く1バイトのIDを表す */ public enum ChunkCompression { @@ -23,7 +21,7 @@ public enum ChunkCompression { /** * Zlib (RFC 1950) * ID=2 - * Minecraft が実際に書き出す方式 + * Minecraftが実際に書き出す方式 */ ZLIB(2, "zlib"), @@ -34,9 +32,10 @@ public enum ChunkCompression { NONE(3, "none"), /** - * LZ4(ブロック形式) + * LZ4(独自ヘッダ付きのブロック連結) * ID=4 - * 任意依存 + * 読み込みのみ対応 + * 展開は自前で行い、外部の依存は使わない */ LZ4(4, "lz4"), @@ -74,14 +73,14 @@ public String asString() { } /** - * 圧縮方式IDから {@link ChunkCompression} を得る + * 圧縮方式IDから{@link ChunkCompression}を得る * * @param id 圧縮方式ID * @return 圧縮方式 * @throws SpringNbtException 未知のIDの場合 */ public static ChunkCompression fromId(int id) { - // 仕様が定めるのは 1・2・3・4・127 の 5 種類だけ + // 仕様が定めるのは1・2・3・4・127の5種類だけ for (ChunkCompression candidate : values()) { if (candidate.id == id) { return candidate; diff --git a/java/src/main/java/io/github/scriptarts/springnbt/anvil/ChunkPos.java b/java/src/main/java/io/github/scriptarts/springnbt/anvil/ChunkPos.java index ced86af..23cc279 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/anvil/ChunkPos.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/anvil/ChunkPos.java @@ -3,8 +3,6 @@ /** * チャンクの絶対座標 * - *

仕様: {@code docs/spec/20-anvil-region.md} 1章 - * * @param x 絶対チャンクX座標 * @param z 絶対チャンクZ座標 */ diff --git a/java/src/main/java/io/github/scriptarts/springnbt/anvil/Lz4.java b/java/src/main/java/io/github/scriptarts/springnbt/anvil/Lz4.java index a756969..e2fc05d 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/anvil/Lz4.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/anvil/Lz4.java @@ -5,28 +5,25 @@ import java.nio.charset.StandardCharsets; /** - * 圧縮方式ID 4 (LZ4) のチャンクを展開する + * 圧縮方式ID 4(LZ4)のチャンクを展開する * - *

素の LZ4 ブロックでも LZ4 フレーム形式でもなく、 - * 独自ヘッダを持つブロックの連結である + *

素のLZ4ブロックでもLZ4フレーム形式でもなく、独自ヘッダを持つブロックの連結である * *

書き込みには対応しない - * 書き戻すときは Zlib になる - * - *

仕様: {@code docs/spec/20-anvil-region.md} 3.1.1 / 3.1.2 + * NBTとして書き戻すときは既定でZlibになり、生バイトのまま書き戻すとLZ4のまま残る */ final class Lz4 { - /** ブロックの先頭に必ず置かれる 8 バイト */ + /** ブロックの先頭に必ず置かれる8バイト */ private static final byte[] MAGIC = "LZ4Block".getBytes(StandardCharsets.US_ASCII); /** ブロックヘッダの長さ */ private static final int HEADER_LENGTH = 21; - /** トークン上位 4 ビット: 本体が無圧縮 */ + /** トークン上位4ビット: 本体が無圧縮 */ private static final int METHOD_STORED = 0x10; - /** トークン上位 4 ビット: 本体が LZ4 圧縮 */ + /** トークン上位4ビット: 本体がLZ4圧縮 */ private static final int METHOD_COMPRESSED = 0x20; /** マッチの最小長 */ @@ -37,7 +34,7 @@ private Lz4() { } /** - * LZ4Block の連結を展開する + * LZ4Blockの連結を展開する * * @param payload 圧縮済みペイロード * @return 展開後のバイト列 @@ -54,14 +51,14 @@ static byte[] decompress(byte[] payload) { return output.toByteArray(); } - /** ブロックを 1 つ展開し、次のブロックの開始位置を返す */ + /** ブロックを1つ展開し、次のブロックの開始位置を返す */ private static int decompressBlock(byte[] payload, int position, ByteArrayOutputStream output) { if (position + HEADER_LENGTH > payload.length) { throw SpringNbtException.malformed( "LZ4: ブロックヘッダが足りない(" + (payload.length - position) + " バイト)"); } - // マジックが違えばそもそも LZ4Block ではない + // マジックが違えばそもそもLZ4Blockではない for (int index = 0; index < MAGIC.length; index++) { if (payload[position + index] != MAGIC[index]) { throw SpringNbtException.malformed("LZ4: ブロックが LZ4Block で始まっていない"); @@ -80,7 +77,7 @@ private static int decompressBlock(byte[] payload, int position, ByteArrayOutput } if (method == METHOD_STORED) { - // 無圧縮なら 2 つの長さは一致していなければならない + // 無圧縮なら2つの長さは一致していなければならない if (compressedLength != originalLength) { throw SpringNbtException.malformed( "LZ4: 無圧縮ブロックの長さが食い違う(" @@ -99,19 +96,19 @@ private static int decompressBlock(byte[] payload, int position, ByteArrayOutput return body + compressedLength; } - /** ヘッダに書かれた 2 つの長さが妥当か調べる */ + /** ヘッダに書かれた2つの長さが妥当か調べる */ private static void validateLengths(int compressedLength, int originalLength) { if (compressedLength < 0 || originalLength < 0) { throw SpringNbtException.malformed("LZ4: ブロックの長さが負値"); } - // 片方だけが 0 になることはない + // 片方だけが0になることはない if ((compressedLength == 0) != (originalLength == 0)) { throw SpringNbtException.malformed("LZ4: ブロックの長さが片方だけ 0"); } } - /** 素の LZ4 ブロックを展開する */ + /** 素のLZ4ブロックを展開する */ private static byte[] decompressRawBlock(byte[] source, int start, int length, int originalLength) { byte[] output = new byte[originalLength]; @@ -126,7 +123,7 @@ private static byte[] decompressRawBlock(byte[] source, int start, int length, int literalLength = token >> 4; - // 15 なら追加バイトで長さが続く + // 15なら追加バイトで長さが続く if (literalLength == 15) { literalLength += readLength(source, input, end); } @@ -151,7 +148,7 @@ private static byte[] decompressRawBlock(byte[] source, int start, int length, int matchLength = (token & 0x0F) + MIN_MATCH; - // 下位 4 ビットが 15 なら追加バイトで長さが続く + // 下位4ビットが15なら追加バイトで長さが続く if ((token & 0x0F) == 15) { matchLength += readLength(source, input, end); } @@ -167,11 +164,11 @@ private static byte[] decompressRawBlock(byte[] source, int start, int length, return output; } - /** 255 が続く形式の追加長さを読む */ + /** 255が続く形式の追加長さを読む */ private static int readLength(byte[] source, int[] input, int end) { int total = 0; - // 255 未満のバイトが出るまで足し続ける + // 255未満のバイトが出るまで足し続ける while (true) { if (input[0] >= end) { throw SpringNbtException.malformed("LZ4: 長さの追加バイトが途中で切れた"); @@ -211,7 +208,7 @@ private static int copyMatch(byte[] output, int written, int offset, int length) int from = written - offset; - // コピー元と先は重なりうるので 1 バイトずつ写す + // コピー元と先は重なりうるので1バイトずつ写す for (int index = 0; index < length; index++) { output[written + index] = output[from + index]; } @@ -219,7 +216,10 @@ private static int copyMatch(byte[] output, int written, int offset, int length) return written + length; } - /** リトルエンディアンの i32 を読む。この形式だけ他と逆になる */ + /** + * リトルエンディアンのi32を読む + * この形式だけ他と逆になる + */ private static int readInt32LittleEndian(byte[] source, int position) { return (source[position] & 0xFF) | ((source[position + 1] & 0xFF) << 8) diff --git a/java/src/main/java/io/github/scriptarts/springnbt/anvil/RawChunk.java b/java/src/main/java/io/github/scriptarts/springnbt/anvil/RawChunk.java index a2017ba..7349e67 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/anvil/RawChunk.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/anvil/RawChunk.java @@ -5,15 +5,12 @@ /** * リージョンファイルに格納されたままの、圧縮済みチャンクデータ * - *

本ライブラリが解釈できない圧縮方式(LZ4 未導入、カスタム方式)でも - * これなら取り出せる + *

本ライブラリが解釈できない圧縮方式(カスタム方式)のチャンクでも、これなら取り出せる * バックアップや別ツールへの受け渡しに使う * - *

仕様: {@code docs/spec/20-anvil-region.md} 5章 - * * @param compression この本体に使われている圧縮方式 * @param data 圧縮されたままの本体 - * @param external 外部ファイル {@code c.X.Z.mcc} に格納されていたか + * @param external 外部ファイル{@code c.X.Z.mcc}に格納されていたか */ public record RawChunk(ChunkCompression compression, byte[] data, boolean external) { diff --git a/java/src/main/java/io/github/scriptarts/springnbt/anvil/RegionFile.java b/java/src/main/java/io/github/scriptarts/springnbt/anvil/RegionFile.java index 9c742a1..4f7f5a3 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/anvil/RegionFile.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/anvil/RegionFile.java @@ -30,15 +30,13 @@ import java.util.zip.InflaterInputStream; /** - * Anvil のリージョンファイル ({@code r.X.Z.mca}) - * 32×32 チャンクを格納する + * Anvilのリージョンファイル ({@code r.X.Z.mca}) + * 32×32チャンクを格納する * *

ファイル全体をメモリに読み込んで扱う - * 実データのリージョンは数 MB 程度で、 - * この方が「触っていないチャンクのバイト配置をそのまま保つ」ことを保証しやすい - * 開いて何も変えずに {@link #flush()} すると、バイト単位で元と同じファイルになる - * - *

仕様: {@code docs/spec/20-anvil-region.md} + * 実データのリージョンは数MB程度で、この方が「触っていないチャンクのバイト配置をそのまま保つ」ことを保証しやすい + * 開いて何も変えずに{@link #flush()}すると、バイト単位で元と同じファイルになる + * ただし空のファイルは、8KiBのヘッダだけのファイルになる */ public final class RegionFile implements AutoCloseable { @@ -51,7 +49,7 @@ public final class RegionFile implements AutoCloseable { /** 1リージョンに入るチャンク数 */ private static final int CHUNK_COUNT = 1024; - /** 1チャンクが確保できるセクタ数の上限(長さフィールドが u8 のため) */ + /** 1チャンクが確保できるセクタ数の上限(長さフィールドがu8のため) */ private static final int MAX_SECTORS = 255; /** @@ -111,7 +109,7 @@ public int regionZ() { /** * リージョンファイルを開く * - * @param path {@code r.X.Z.mca} という名前のファイル + * @param path {@code r.X.Z.mca}という名前のファイル * 座標はファイル名から読み取る * @param mode 読み取り専用か読み書きか * @return 開いたリージョン @@ -157,7 +155,7 @@ public static RegionFile open(Path path) { /** ヘッダを解析し、ロケーションとタイムスタンプを取り込む */ private void parseHeader() { - // 空ファイルは「チャンクが 1 つも無いリージョン」として受け入れる + // 空ファイルは「チャンクが1つも無いリージョン」として受け入れる if (data.length == 0) { data = new byte[HEADER_SECTORS * SECTOR_SIZE]; return; @@ -176,7 +174,7 @@ private void parseHeader() { int totalSectors = data.length / SECTOR_SIZE; Map sectorOwner = new HashMap<>(); - // ロケーションテーブルの 1024 エントリを順に取り込む + // ロケーションテーブルの1024エントリを順に取り込む for (int index = 0; index < CHUNK_COUNT; index++) { long entry = readUnsigned(index * 4, 4); int offset = (int) (entry >>> 8); @@ -203,7 +201,7 @@ private void parseHeader() { "チャンク " + index + " の割り当てがファイル外へはみ出している"); } - // 同じセクタを 2 つのチャンクが指していたら、どちらかが壊れている + // 同じセクタを2つのチャンクが指していたら、どちらかがおかしくなっている for (int sector = offset; sector < offset + count; sector++) { Integer owner = sectorOwner.put(sector, index); @@ -248,7 +246,7 @@ private void ensureOpen() { * * @param chunkX 絶対チャンクX座標 * @param chunkZ 絶対チャンクZ座標 - * @return 存在すれば true + * @return 存在すればtrue */ public boolean hasChunk(int chunkX, int chunkZ) { ensureOpen(); @@ -264,7 +262,7 @@ public List chunkPositions() { ensureOpen(); List result = new ArrayList<>(); - // 添字の昇順に走査する(localZ が外、localX が内) + // 添字の昇順に走査する(localZが外、localXが内) for (int index = 0; index < CHUNK_COUNT; index++) { if (sectorCounts[index] == 0) { continue; @@ -279,8 +277,8 @@ public List chunkPositions() { } /** - * チャンクの最終更新時刻(Unix 秒) - * 存在しなければ 0 + * チャンクの最終更新時刻(Unix秒) + * タイムスタンプテーブルの値をそのまま返すため、チャンクが無くても0とは限らない * * @param chunkX 絶対チャンクX座標 * @param chunkZ 絶対チャンクZ座標 @@ -311,7 +309,7 @@ public void setTimestamp(int chunkX, int chunkZ, int value) { * @param chunkX 絶対チャンクX座標 * @param chunkZ 絶対チャンクZ座標 * @return チャンク - * 存在しなければ null + * 存在しなければnull */ public RawChunk readChunkRaw(int chunkX, int chunkZ) { ensureOpen(); @@ -339,7 +337,7 @@ public RawChunk readChunkRaw(int chunkX, int chunkZ) { ChunkCompression compression = ChunkCompression.fromId(schemeByte & 0x7F); if (external) { - // 最上位ビットが立っている場合、本体は c.X.Z.mcc にある + // 最上位ビットが立っている場合、本体はc.X.Z.mccにある return new RawChunk(compression, readExternalFile(chunkX, chunkZ), true); } @@ -348,12 +346,12 @@ public RawChunk readChunkRaw(int chunkX, int chunkZ) { } /** - * チャンクを NBT として読む + * チャンクをNBTとして読む * * @param chunkX 絶対チャンクX座標 * @param chunkZ 絶対チャンクZ座標 * @return チャンク - * 存在しなければ null + * 存在しなければnull */ public NbtCompound readChunk(int chunkX, int chunkZ) { RawChunk raw = readChunkRaw(chunkX, chunkZ); @@ -368,8 +366,8 @@ public NbtCompound readChunk(int chunkX, int chunkZ) { } /** - * チャンクを NBT として書き込む - * 圧縮方式は Zlib + * チャンクをNBTとして書き込む + * 圧縮方式はZlib * * @param chunkX 絶対チャンクX座標 * @param chunkZ 絶対チャンクZ座標 @@ -380,7 +378,7 @@ public void writeChunk(int chunkX, int chunkZ, NbtCompound tag) { } /** - * チャンクを NBT として、圧縮方式を指定して書き込む + * チャンクをNBTとして、圧縮方式を指定して書き込む * * @param chunkX 絶対チャンクX座標 * @param chunkZ 絶対チャンクZ座標 @@ -414,7 +412,7 @@ public void writeChunkRaw(int chunkX, int chunkZ, RawChunk raw) { int schemeByte; if (useExternal) { - // 1MiB を超えるチャンクは外部ファイルへ退避し、リージョンには目印だけ残す + // 255セクタ(約1MiB)に収まらないチャンクは外部ファイルへ退避し、リージョンには目印だけ残す writeExternalFile(chunkX, chunkZ, raw.data()); payload = new byte[0]; schemeByte = raw.compression().id() | 0x80; @@ -453,7 +451,7 @@ public void writeChunkRaw(int chunkX, int chunkZ, RawChunk raw) { * * @param chunkX 絶対チャンクX座標 * @param chunkZ 絶対チャンクZ座標 - * @return 削除できたら true + * @return 削除できたらtrue */ public boolean deleteChunk(int chunkX, int chunkZ) { ensureOpen(); @@ -476,7 +474,7 @@ public boolean deleteChunk(int chunkX, int chunkZ) { /** * 必要なセクタ数を確保し、開始セクタ番号を返す * - *

既存の割り当てがちょうど同じ大きさならその場を使い、 + *

既存の割り当てがちょうど同じ大きさなら、その場所をそのまま使う * そうでなければ先頭から空き領域を探し、無ければ末尾へ追加する */ private int allocateSectors(int index, int needed) { @@ -513,13 +511,13 @@ private int allocateSectors(int index, int needed) { /** * セクタの使用状況を作る - * {@code ignoreIndex} のチャンクは空きとして扱う + * {@code ignoreIndex}のチャンクは空きとして扱う */ private boolean[] buildSectorUsage(int ignoreIndex) { int totalSectors = data.length / SECTOR_SIZE; boolean[] used = new boolean[totalSectors]; - // ヘッダの 2 セクタは常に使用中 + // ヘッダの2セクタは常に使用中 for (int sector = 0; sector < HEADER_SECTORS && sector < totalSectors; sector++) { used[sector] = true; } @@ -618,6 +616,8 @@ public void flush() { } writeHeader(); + // 読み書きで開いたフォルダがまだ無ければ、書き出す前に作る + ensureDirectory(); try { Files.write(path, data); @@ -640,9 +640,9 @@ public byte[] toBytes() { return data.clone(); } - /** ロケーションテーブルとタイムスタンプテーブルを先頭 2 セクタへ書き戻す */ + /** ロケーションテーブルとタイムスタンプテーブルを先頭2セクタへ書き戻す */ private void writeHeader() { - // 位置表とタイムスタンプ表を、添字順に組み立て直す + // ロケーションテーブルとタイムスタンプテーブルを、添字順に組み立て直す for (int index = 0; index < CHUNK_COUNT; index++) { long entry = ((long) offsets[index] << 8) | (long) sectorCounts[index]; writeUnsigned(index * 4, entry, 4); @@ -706,8 +706,23 @@ private byte[] readExternalFile(int chunkX, int chunkZ) { } } + /** + * ファイルを置くディレクトリが無ければ作る + * + *

読むだけの操作で空のディレクトリができないよう、書き出す直前にだけ呼ぶ + */ + private void ensureDirectory() { + try { + Files.createDirectories(directory); + } catch (IOException error) { + throw new SpringNbtException(ErrorCode.IO, "ディレクトリを作れない: " + directory, error); + } + } + private void writeExternalFile(int chunkX, int chunkZ, byte[] payload) { Path external = externalPath(chunkX, chunkZ); + // 読み書きで開いたフォルダがまだ無ければ、書き出す前に作る + ensureDirectory(); try { Files.write(external, payload); @@ -760,7 +775,7 @@ static byte[] compress(byte[] plain, ChunkCompression compression) { } try (ByteArrayOutputStream destination = new ByteArrayOutputStream()) { - // ストリームを閉じてフッタを書かせてから toByteArray する必要がある + // ストリームを閉じてフッタを書かせてからtoByteArrayする必要がある try (OutputStream encoder = createEncoder(destination, compression)) { encoder.write(plain); } diff --git a/java/src/main/java/io/github/scriptarts/springnbt/anvil/RegionFileMode.java b/java/src/main/java/io/github/scriptarts/springnbt/anvil/RegionFileMode.java index e310a59..899db63 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/anvil/RegionFileMode.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/anvil/RegionFileMode.java @@ -5,7 +5,7 @@ public enum RegionFileMode { /** * 読み取り専用 - * 書き込み系の操作はエラーになる + * 書き込み系の操作はエラーになる({@code flush}は何もしない) */ READ_ONLY, diff --git a/java/src/main/java/io/github/scriptarts/springnbt/anvil/RegionFolder.java b/java/src/main/java/io/github/scriptarts/springnbt/anvil/RegionFolder.java index 9ee7186..7d6667b 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/anvil/RegionFolder.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/anvil/RegionFolder.java @@ -16,32 +16,27 @@ import java.util.Objects; /** - * リージョンファイルが並ぶディレクトリ 1 つ分 - * ({@code region/}、{@code entities/}、{@code poi/} のいずれか) + * リージョンファイルが並ぶディレクトリ1つ分 + * ({@code region/}、{@code entities/}、{@code poi/}のいずれか) * - *

開いたリージョンファイルはキャッシュし、{@link #close()} でまとめて閉じる + *

開いたリージョンファイルはキャッシュし、{@link #close()}でまとめて閉じる * チャンク座標からリージョンを解決するので、利用側はリージョンの存在を意識しなくてよい * - *

{@link RegionFile} はファイル全体をメモリへ載せるため、キャッシュには - * {@link #maxCachedRegions()} 件の上限がある - * 上限を超えると、最も長く使われていない - * ものから書き出して閉じる + *

{@link RegionFile}はファイル全体をメモリへ載せるため、キャッシュには{@link #maxCachedRegions()}件の上限がある + * 上限を超えると、最も長く使われていないものから書き出して閉じる * 大きなワールドを端から走査してもメモリを使い切らない * - *

このため {@link #region(int, int)} が返した参照は、 - * 別のリージョンへアクセスすると閉じられている場合がある + *

このため{@link #region(int, int)}が返した参照は、別のリージョンへアクセスすると閉じられている場合がある * 参照を保持せず、必要なたびに取得すること - * - *

仕様: {@code docs/spec/20-anvil-region.md} 5章 */ public final class RegionFolder implements AutoCloseable { /** * 同時に開いておくリージョンファイル数の既定の上限 * - *

1 リージョンは最大 255 セクタ × 1024 チャンク=理論上 1GiB になりうる - * 実データでは数 MB から数十 MB 程度 - * 8 件なら通常のワールドで数百 MB に収まる + *

1リージョンは最大255セクタ × 1024チャンク=理論上1GiBになりうる + * 実データでは数MBから数十MB程度 + * 8件なら通常のワールドで数百MBに収まる */ public static final int DEFAULT_MAX_CACHED_REGIONS = 8; @@ -111,7 +106,7 @@ public static RegionFolder open(Path directory, RegionFileMode mode) { * @param maxCachedRegions 同時に開いておくリージョンファイル数の上限 * @return 開いたフォルダ * @throws SpringNbtException 読み取り専用でディレクトリが存在しない場合、 - * または上限が 1 未満の場合 + * または上限が1未満の場合 */ public static RegionFolder open(Path directory, RegionFileMode mode, int maxCachedRegions) { Objects.requireNonNull(directory, "directory"); @@ -153,7 +148,7 @@ public List regionPositions() { } try (DirectoryStream stream = Files.newDirectoryStream(directory, "r.*.mca")) { - // r.X.Z.mca として解釈できるファイルだけを拾う + // r.X.Z.mcaとして解釈できるファイルだけを拾う for (Path path : stream) { RegionPos position = RegionPos.fromFileName(path.getFileName().toString()); @@ -177,7 +172,7 @@ public List regionPositions() { * @param regionX リージョンX座標 * @param regionZ リージョンZ座標 * @return リージョン - * 読み取り専用で存在しなければ null + * 読み取り専用で存在しなければnull */ public RegionFile region(int regionX, int regionZ) { ensureOpen(); @@ -191,7 +186,7 @@ public RegionFile region(int regionX, int regionZ) { Path path = directory.resolve(position.fileName()); - // 読み取り専用では、存在しないリージョンは「チャンクが無い」として null を返す + // 読み取り専用では、存在しないリージョンは「チャンクが無い」としてnullを返す if (!Files.exists(path) && mode == RegionFileMode.READ_ONLY) { return null; } @@ -212,7 +207,7 @@ private void touch(RegionPos position) { recentlyUsed.addLast(position); } - /** 新しく 1 件開けるよう、上限を下回るまで古いものを閉じる */ + /** 新しく1件開けるよう、上限を下回るまで古いものを閉じる */ private void evictUntilBelowLimit() { // 上限に達している間、いちばん長く使っていないものから閉じる while (cache.size() >= maxCachedRegions && !recentlyUsed.isEmpty()) { @@ -232,7 +227,7 @@ private void evictUntilBelowLimit() { * * @param chunkX 絶対チャンクX座標 * @param chunkZ 絶対チャンクZ座標 - * @return 存在すれば true + * @return 存在すればtrue */ public boolean hasChunk(int chunkX, int chunkZ) { RegionFile file = regionFor(chunkX, chunkZ); @@ -245,12 +240,12 @@ public boolean hasChunk(int chunkX, int chunkZ) { } /** - * チャンクを NBT として読む + * チャンクをNBTとして読む * * @param chunkX 絶対チャンクX座標 * @param chunkZ 絶対チャンクZ座標 * @return チャンク - * 存在しなければ null + * 存在しなければnull */ public NbtCompound readChunk(int chunkX, int chunkZ) { RegionFile file = regionFor(chunkX, chunkZ); @@ -263,7 +258,7 @@ public NbtCompound readChunk(int chunkX, int chunkZ) { } /** - * チャンクを NBT として書き込む + * チャンクをNBTとして書き込む * * @param chunkX 絶対チャンクX座標 * @param chunkZ 絶対チャンクZ座標 @@ -285,7 +280,7 @@ public void writeChunk(int chunkX, int chunkZ, NbtCompound tag) { * * @param chunkX 絶対チャンクX座標 * @param chunkZ 絶対チャンクZ座標 - * @return 削除できたら true + * @return 削除できたらtrue */ public boolean deleteChunk(int chunkX, int chunkZ) { RegionFile file = regionFor(chunkX, chunkZ); diff --git a/java/src/main/java/io/github/scriptarts/springnbt/anvil/RegionPos.java b/java/src/main/java/io/github/scriptarts/springnbt/anvil/RegionPos.java index 3db0607..0413183 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/anvil/RegionPos.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/anvil/RegionPos.java @@ -2,7 +2,7 @@ /** * リージョンの座標 - * 1リージョンは 32×32 チャンクを担当する + * 1リージョンは32×32チャンクを担当する * * @param x リージョンX座標 * @param z リージョンZ座標 @@ -19,16 +19,16 @@ public String fileName() { } /** - * {@code r.X.Z.mca} 形式のファイル名から座標を得る + * {@code r.X.Z.mca}形式のファイル名から座標を得る * * @param fileName ファイル名 * @return 座標 - * 解釈できなければ null + * 解釈できなければnull */ public static RegionPos fromFileName(String fileName) { String[] parts = fileName.split("\\."); - // "r" "" "" "mca" の 4 つに分かれるはず + // "r" "" "" "mca"の4つに分かれるはず if (parts.length != 4 || !parts[0].equals("r") || !parts[3].equals("mca")) { return null; } diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/CanonicalDecimal.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/CanonicalDecimal.java index 92bd1eb..c3a8819 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/CanonicalDecimal.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/CanonicalDecimal.java @@ -6,11 +6,8 @@ * 浮動小数点の正準10進表記 * *

各言語の標準の数値書式は互いに一致しない - * 指数表記へ切り替わる閾値も、 - * 指数部の桁数も、E の大文字小文字も処理系ごとに違う - * そのままでは SNBT 出力の言語間一致が成立しないため、書式をここで固定する - * - *

仕様: {@code docs/spec/11-snbt.md} 5.1章 + * 指数表記へ切り替わる閾値も、指数部の桁数も、Eの大文字小文字も処理系ごとに違う + * そのままではSNBTの出力が言語間で一致しないため、書式をここで固定する */ final class CanonicalDecimal { @@ -24,7 +21,7 @@ private CanonicalDecimal() { // ユーティリティクラス } - /** binary32 を正準10進表記へ変換する */ + /** binary32を正準10進表記へ変換する */ static String fromFloat(float value) { if (Float.isNaN(value)) { return "NaN"; @@ -40,7 +37,7 @@ static String fromFloat(float value) { int target = Float.floatToRawIntBits(value); - // 有効数字を 1 桁ずつ増やし、読み戻してビット一致する最短の表記を探す + // 有効数字を1桁ずつ増やし、読み戻したときにビットが一致する最短の表記を探す for (int precision = 1; precision <= 9; precision++) { String candidate = String.format(Locale.ROOT, "%." + (precision - 1) + "e", value); @@ -49,11 +46,11 @@ static String fromFloat(float value) { } } - // 9 桁あれば binary32 は必ず往復するので、ここへは来ない + // 9桁あればbinary32は必ず往復するので、ここへは来ない return format(String.format(Locale.ROOT, "%.8e", value)); } - /** binary64 を正準10進表記へ変換する */ + /** binary64を正準10進表記へ変換する */ static String fromDouble(double value) { if (Double.isNaN(value)) { return "NaN"; @@ -69,7 +66,7 @@ static String fromDouble(double value) { long target = Double.doubleToRawLongBits(value); - // 有効数字を 1 桁ずつ増やし、読み戻してビット一致する最短の表記を探す + // 有効数字を1桁ずつ増やし、読み戻したときにビットが一致する最短の表記を探す for (int precision = 1; precision <= 17; precision++) { String candidate = String.format(Locale.ROOT, "%." + (precision - 1) + "e", value); @@ -78,11 +75,11 @@ static String fromDouble(double value) { } } - // 17 桁あれば binary64 は必ず往復するので、ここへは来ない + // 17桁あればbinary64は必ず往復するので、ここへは来ない return format(String.format(Locale.ROOT, "%.16e", value)); } - /** 指数表記の文字列(例 {@code "7.5e-01"})から、仕様が定める正準表記を組み立てる */ + /** 指数表記の文字列(例{@code "7.5e-01"})から、仕様が定める正準表記を組み立てる */ private static String format(String exponential) { boolean negative = false; int index = 0; @@ -113,7 +110,7 @@ private static String format(String exponential) { return compose(negative, digits, exponent); } - /** {@code Integer.parseInt} は先頭の "+" を受け付けるが、環境差を避けるため明示的に外す */ + /** {@code Integer.parseInt}は先頭の"+"を受け付けるが、環境差を避けるため明示的に外す */ private static String stripPlus(String text) { if (text.startsWith("+")) { return text.substring(1); @@ -124,7 +121,7 @@ private static String stripPlus(String text) { /** * 末尾のゼロを取り除く - * すべてゼロなら "0" を残す + * すべてゼロなら"0"を残す */ private static String trimTrailingZeros(String digits) { int end = digits.length(); @@ -145,7 +142,7 @@ private static String compose(boolean negative, String digits, int exponent) { builder.append('-'); } - // 値が 0 のときは指数に関わらず 0.0 と書く + // 値が0のときは指数に関わらず0.0と書く if (digits.equals("0")) { builder.append("0.0"); return builder.toString(); @@ -156,7 +153,7 @@ private static String compose(boolean negative, String digits, int exponent) { builder.append(digits.charAt(0)); builder.append('.'); - // 2 桁目以降があれば小数点のうしろへ回す + // 2桁目以降があれば小数点のうしろへ回す if (digits.length() > 1) { builder.append(digits, 1, digits.length()); } else { @@ -169,8 +166,8 @@ private static String compose(boolean negative, String digits, int exponent) { } if (exponent >= 0) { - // 整数部は先頭 (exponent + 1) 桁 - // 足りなければゼロで右詰めする + // 整数部は先頭 (exponent + 1)桁 + // 足りなければ数字の右側をゼロで埋める int integerDigits = exponent + 1; // 整数部が数字の並びに収まるなら、そのまま切り出す @@ -179,7 +176,7 @@ private static String compose(boolean negative, String digits, int exponent) { } else { builder.append(digits); - // 数字が足りない分は 0 で埋めて桁を合わせる + // 数字が足りない分は0で埋めて桁を合わせる for (int i = digits.length(); i < integerDigits; i++) { builder.append('0'); } @@ -197,10 +194,10 @@ private static String compose(boolean negative, String digits, int exponent) { return builder.toString(); } - // 指数が負なら "0." に続けてゼロを詰めてから数字を置く + // 指数が負なら"0."に続けてゼロを詰めてから数字を置く builder.append("0."); - // 指数のぶんだけ 0.000... と 0 を並べる + // 指数の絶対値より1個少ない0を並べ、0.000...の形にする for (int i = 0; i < (-exponent) - 1; i++) { builder.append('0'); } diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/Compression.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/Compression.java index e9fcfd6..6b38c8d 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/Compression.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/Compression.java @@ -2,8 +2,6 @@ /** * 圧縮方式 - * - *

仕様: {@code docs/spec/10-nbt-binary.md} 4章 */ public enum Compression { diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/Mutf8.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/Mutf8.java index 2281fa3..0e91aa8 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/Mutf8.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/Mutf8.java @@ -3,25 +3,21 @@ import io.github.scriptarts.springnbt.SpringNbtException; /** - * Modified UTF-8 (MUTF-8) の符号化・復号 + * Modified UTF-8 (MUTF-8)の符号化・復号 * - *

標準 UTF-8 との違いは 2 点だけ + *

標準UTF-8との違いは2点だけ *

    - *
  • {@code U+0000} を {@code C0 80} の 2 バイトで表す
  • - *
  • {@code U+10000} 以上をサロゲートペアへ分解し、3 バイト × 2 で表す (CESU-8)
  • + *
  • {@code U+0000}を{@code C0 80}の2バイトで表す
  • + *
  • {@code U+10000}以上をサロゲートペアへ分解し、3バイト × 2で表す (CESU-8)
  • *
* - *

Java の {@link String} は UTF-16 コード単位の列なので、 - * サロゲートペアも孤立サロゲートもそのまま保持できる + *

Javaの{@link String}はUTF-16コード単位の列なので、サロゲートペアも孤立サロゲートもそのまま保持できる * - *

{@code DataInput.readUTF} と同じ符号化だが、 - * 65535 バイト超の扱いなど細部を仕様どおりに固定したいため自前で実装している - * - *

仕様: {@code docs/spec/10-nbt-binary.md} 2章 + *

{@code DataInput.readUTF}と同じ符号化だが、65535バイト超の扱いなど細部を仕様どおりに固定したいため自前で実装している */ public final class Mutf8 { - /** MUTF-8 の文字列が取りうる最大バイト長(長さフィールドが u16 のため) */ + /** MUTF-8の文字列が取りうる最大バイト長(長さフィールドがu16のため) */ public static final int MAX_BYTE_LENGTH = 65535; private Mutf8() { @@ -29,34 +25,34 @@ private Mutf8() { } /** - * MUTF-8 バイト列を文字列へ復号する + * MUTF-8バイト列を文字列へ復号する * * @param data バイト列 * @param offset 開始位置 * @param length 長さ * @return 復号した文字列 - * @throws SpringNbtException バイト列が MUTF-8 として不正な場合 + * @throws SpringNbtException バイト列がMUTF-8として不正な場合 */ public static String decode(byte[] data, int offset, int length) { StringBuilder builder = new StringBuilder(length); int index = offset; int end = offset + length; - // 先頭から 1 文字ずつ取り出す + // 先頭から1文字ずつ取り出す while (index < end) { int b0 = data[index] & 0xFF; if ((b0 & 0x80) == 0x00) { - // 1 バイト形式: 0xxxxxxx (U+0001..U+007F) + // 1バイト形式: 0xxxxxxx (U+0001..U+007F) if (b0 == 0x00) { - // 素の 0x00 は MUTF-8 では現れてはならない (C0 80 を使う) + // 素の0x00はMUTF-8では現れてはならない (C0 80を使う) throw SpringNbtException.malformed("MUTF-8: 素の 0x00 が現れた (U+0000 は C0 80 で表す)"); } builder.append((char) b0); index += 1; } else if ((b0 & 0xE0) == 0xC0) { - // 2 バイト形式: 110xxxxx 10xxxxxx + // 2バイト形式: 110xxxxx 10xxxxxx if (index + 1 >= end) { throw SpringNbtException.malformed("MUTF-8: 2バイト形式が途中で切れた"); } @@ -69,8 +65,8 @@ public static String decode(byte[] data, int offset, int length) { int value = ((b0 & 0x1F) << 6) | (b1 & 0x3F); - // C0 80 (U+0000) だけは正当 - // それ以外の 0x80 未満は冗長符号化 + // C0 80 (U+0000)だけは正当 + // それ以外の0x80未満は冗長符号化 if (value < 0x80 && !(b0 == 0xC0 && b1 == 0x80)) { throw SpringNbtException.malformed("MUTF-8: 冗長な2バイト符号化"); } @@ -78,7 +74,7 @@ public static String decode(byte[] data, int offset, int length) { builder.append((char) value); index += 2; } else if ((b0 & 0xF0) == 0xE0) { - // 3 バイト形式: 1110xxxx 10xxxxxx 10xxxxxx + // 3バイト形式: 1110xxxx 10xxxxxx 10xxxxxx if (index + 2 >= end) { throw SpringNbtException.malformed("MUTF-8: 3バイト形式が途中で切れた"); } @@ -92,7 +88,7 @@ public static String decode(byte[] data, int offset, int length) { int value = ((b0 & 0x0F) << 12) | ((b1 & 0x3F) << 6) | (b2 & 0x3F); - // 3 バイトで表すべき範囲は U+0800 以上 + // 3バイトで表すべき範囲はU+0800以上 if (value < 0x800) { throw SpringNbtException.malformed("MUTF-8: 冗長な3バイト符号化"); } @@ -100,7 +96,7 @@ public static String decode(byte[] data, int offset, int length) { builder.append((char) value); index += 3; } else { - // 4 バイト形式 (標準 UTF-8) や継続バイト単独は MUTF-8 では不正 + // 4バイト形式 (標準UTF-8)や継続バイト単独はMUTF-8では不正 throw SpringNbtException.malformed( String.format("MUTF-8: 不正な先頭バイト 0x%02X", b0)); } @@ -110,30 +106,29 @@ public static String decode(byte[] data, int offset, int length) { } /** - * MUTF-8 バイト列を文字列へ復号する + * MUTF-8バイト列を文字列へ復号する * * @param data バイト列 * @return 復号した文字列 - * @throws SpringNbtException バイト列が MUTF-8 として不正な場合 + * @throws SpringNbtException バイト列がMUTF-8として不正な場合 */ public static String decode(byte[] data) { return decode(data, 0, data.length); } /** - * 文字列を MUTF-8 バイト列へ符号化する + * 文字列をMUTF-8バイト列へ符号化する * - *

サロゲートは対になっているかどうかに関わらず 1 つずつ 3 バイトで符号化されるため、 - * 孤立サロゲートもそのまま往復できる + *

サロゲートは対になっているかどうかに関わらず1つずつ3バイトで符号化されるため、孤立サロゲートもそのまま往復できる * * @param text 文字列 - * @return MUTF-8 バイト列 + * @return MUTF-8バイト列 */ public static byte[] encode(String text) { byte[] buffer = new byte[byteLength(text)]; int position = 0; - // コード単位ごとに 1〜3 バイトへ展開する + // コード単位ごとに1〜3バイトへ展開する for (int index = 0; index < text.length(); index++) { char unit = text.charAt(index); @@ -141,7 +136,7 @@ public static byte[] encode(String text) { buffer[position] = (byte) unit; position += 1; } else if (unit == 0x0000 || unit <= 0x07FF) { - // U+0000 もこの経路で C0 80 になる + // U+0000もこの経路でC0 80になる buffer[position] = (byte) (0xC0 | ((unit >> 6) & 0x1F)); buffer[position + 1] = (byte) (0x80 | (unit & 0x3F)); position += 2; @@ -157,7 +152,7 @@ public static byte[] encode(String text) { } /** - * 文字列を MUTF-8 で符号化したときのバイト長を求める + * 文字列をMUTF-8で符号化したときのバイト長を求める * 実際に符号化はしない * * @param text 文字列 @@ -181,4 +176,32 @@ public static int byteLength(String text) { return length; } + + /** + * 対になっていないサロゲートを含むか + * + *

正しいサロゲートペアは1文字として数え、孤立したものだけを探す + * Compoundのキーには孤立サロゲートを許さないので、その検査に使う + */ + static boolean hasLoneSurrogate(String text) { + // コード単位を1つずつ見て、サロゲート対をまとめる + for (int index = 0; index < text.length(); index++) { + char c = text.charAt(index); + + // 上位サロゲートは、対になる下位サロゲートとまとめて1文字を成す + if (Character.isHighSurrogate(c)) { + // 対が揃っていなければ孤立サロゲート + if (index + 1 >= text.length() || !Character.isLowSurrogate(text.charAt(index + 1))) { + return true; + } + + index += 1; + } else if (Character.isLowSurrogate(c)) { + // 上位サロゲートを伴わない下位サロゲートは孤立している + return true; + } + } + + return false; + } } diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NamedTag.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NamedTag.java index a0a599c..dd7f23c 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NamedTag.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NamedTag.java @@ -5,7 +5,7 @@ /** * ルート名とルートタグの組 * - *

{@link NbtFormat#JAVA} ではルート名は通常空文字列だが、 + *

{@link NbtFormat#JAVA}ではルート名は通常空文字列だが、 * 読んだ値をそのまま保持し、書き出しでも同じ値を出力する * * @param name ルート名 diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtBinaryReader.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtBinaryReader.java index 29fd696..a9b6d05 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtBinaryReader.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtBinaryReader.java @@ -3,13 +3,11 @@ import io.github.scriptarts.springnbt.SpringNbtException; /** - * 展開済みのバイト列から NBT を読み出す + * 展開済みのバイト列からNBTを読み出す * *

入力全体をあらかじめメモリに持つ設計にしている * 「宣言された長さが残り入力長を超えていないか」を確保前に検査できるようにするため - * これがないと、長さ 0x7FFFFFFF を宣言しただけの数バイトの入力でメモリを枯渇させられる - * - *

仕様: {@code docs/spec/10-nbt-binary.md} + * これがないと、長さ0x7FFFFFFFを宣言しただけの数バイトの入力でメモリを枯渇させられる */ final class NbtBinaryReader { @@ -56,13 +54,13 @@ NamedTag readRoot(NbtFormat format) { } /** - * ルートタグを 1 つ読む + * ルートタグを1つ読む * 末尾の余りは見ない */ NamedTag readRootTag(NbtFormat format) { TagType type = TagType.fromId(readByteRaw() & 0xFF); - // Java版のファイル形式でもネットワーク形式でも、ルートは必ず TAG_Compound + // Java版のファイル形式でもネットワーク形式でも、ルートは必ずTAG_Compound if (type != TagType.COMPOUND) { throw SpringNbtException.malformed( "ルートタグは compound でなければならないが " + type.asString() + " だった"); @@ -83,7 +81,7 @@ NamedTag readRootTag(NbtFormat format) { /** 指定した型のペイロードを読む */ private NbtTag readPayload(TagType type, int depth) { - // 深さ上限は再帰する型に入る手前で検査する + // 深さ上限は型を問わず、ペイロードを読む前に検査する if (depth > maxDepth) { throw SpringNbtException.limitExceeded("ネストが深すぎる (上限 " + maxDepth + ")"); } @@ -105,11 +103,11 @@ private NbtTag readPayload(TagType type, int depth) { }; } - /** TAG_Compound のペイロード(名前付きタグの並び + TAG_End)を読む */ + /** TAG_Compoundのペイロード(名前付きタグの並び + TAG_End)を読む */ private NbtCompound readCompoundPayload(int depth) { NbtCompound compound = new NbtCompound(); - // TAG_End が現れるまで名前付きタグを読み続ける + // TAG_Endが現れるまで名前付きタグを読み続ける while (true) { TagType type = TagType.fromId(readByteRaw() & 0xFF); @@ -122,13 +120,13 @@ private NbtCompound readCompoundPayload(int depth) { } } - /** TAG_List のペイロードを読む */ + /** TAG_Listのペイロードを読む */ private NbtList readListPayload(int depth) { TagType elementType = TagType.fromId(readByteRaw() & 0xFF); int count = readLength(); if (elementType == TagType.END) { - // 要素型 End のリストは空でなければならない + // 要素型Endのリストは空でなければならない if (count != 0) { throw SpringNbtException.malformed( "要素型 End のリストに " + count + " 個の要素が宣言されている"); @@ -137,7 +135,7 @@ private NbtList readListPayload(int depth) { return new NbtList(TagType.END); } - // 1 要素の最小バイト数から、宣言された個数が入力に収まるかを先に検査する + // 1要素の最小バイト数から、宣言された個数が入力に収まるかを先に検査する ensureAvailable((long) count * minimumPayloadSize(elementType)); NbtList list = new NbtList(elementType); @@ -166,7 +164,7 @@ private int[] readIntArrayPayload() { int[] result = new int[count]; - // 4 バイトずつビッグエンディアンで読む + // 4バイトずつビッグエンディアンで読む for (int index = 0; index < count; index++) { result[index] = (int) readUnsigned(4); } @@ -180,7 +178,7 @@ private long[] readLongArrayPayload() { long[] result = new long[count]; - // 8 バイトずつビッグエンディアンで読む + // 8バイトずつビッグエンディアンで読む for (int index = 0; index < count; index++) { result[index] = readUnsigned(8); } @@ -188,7 +186,7 @@ private long[] readLongArrayPayload() { return result; } - /** MUTF-8 の文字列(u16 の長さ + 本体)を読む */ + /** MUTF-8の文字列(u16の長さ + 本体)を読む */ private String readString() { int length = (int) readUnsigned(2); ensureAvailable(length); @@ -205,7 +203,7 @@ private String readString() { private int readLength() { int length = (int) readUnsigned(4); - // 長さは i32 だが、負値は仕様上ありえない + // 長さはi32だが、負値は仕様上ありえない if (length < 0) { throw SpringNbtException.malformed("長さが負値: " + length); } @@ -248,8 +246,8 @@ private void ensureAvailable(long required) { /** * キーやルート名として使える文字列か検査する * - *

値と違い、キーには孤立サロゲートを許さない(仕様 10 の 2.2章) - * Minecraft が書き出すキーは ASCII の識別子のみで、 + *

値と違い、キーには孤立サロゲートを許さない + * Minecraftが書き出すキーはASCIIの識別子のみで、 * 孤立サロゲートが現れるのはデータ破損を意味する */ private static String requireUtf8Representable(String text, String role) { @@ -257,9 +255,9 @@ private static String requireUtf8Representable(String text, String role) { for (int index = 0; index < text.length(); index++) { char c = text.charAt(index); - // 上位サロゲートは、対になる下位サロゲートとまとめて 1 文字を成す + // 上位サロゲートは、対になる下位サロゲートとまとめて1文字を成す if (Character.isHighSurrogate(c)) { - // 対が揃っていれば 2 コード単位を消費する + // 対が揃っていれば2コード単位を消費する // 揃わなければ孤立サロゲート if (index + 1 < text.length() && Character.isLowSurrogate(text.charAt(index + 1))) { index += 1; @@ -284,13 +282,13 @@ private static int minimumPayloadSize(TagType type) { case SHORT -> 2; case INT, FLOAT -> 4; case LONG, DOUBLE -> 8; - // 長さフィールドの 4 バイトは必ずある + // 長さフィールドの4バイトは必ずある case BYTE_ARRAY, INT_ARRAY, LONG_ARRAY -> 4; - // 長さフィールドの 2 バイトは必ずある + // 長さフィールドの2バイトは必ずある case STRING -> 2; - // 要素型 1 バイト + 個数 4 バイト + // 要素型1バイト + 個数4バイト case LIST -> 5; - // 終端の TAG_End 1 バイトは必ずある + // 終端のTAG_End 1バイトは必ずある case COMPOUND, END -> 1; }; } diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtBinaryWriter.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtBinaryWriter.java index 2aff5bb..7d47be8 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtBinaryWriter.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtBinaryWriter.java @@ -5,12 +5,10 @@ import java.util.Map; /** - * NBT を展開済みのバイト列へ書き出す + * NBTを展開済みのバイト列へ書き出す * *

出力は一意でなければならない(ラウンドトリップ検証が成立するため) - * Compound は挿入順のまま、浮動小数点はビットパターンのまま書き出す - * - *

仕様: {@code docs/spec/10-nbt-binary.md} 5章 + * Compoundは挿入順のまま、浮動小数点はビットパターンのまま書き出す */ final class NbtBinaryWriter { @@ -35,7 +33,7 @@ private void writePayload(NbtTag tag) { case NbtShort value -> writeUnsigned(value.value(), 2); case NbtInt value -> writeUnsigned(value.value(), 4); case NbtLong value -> writeUnsigned(value.value(), 8); - // NaN や -0.0 を保つため、ビットパターンをそのまま書く + // NaNや-0.0を保つため、ビットパターンをそのまま書く case NbtFloat value -> writeUnsigned(Float.floatToRawIntBits(value.value()), 4); case NbtDouble value -> writeUnsigned(Double.doubleToRawLongBits(value.value()), 8); case NbtByteArray value -> writeByteArrayPayload(value.value()); @@ -76,7 +74,7 @@ private void writeByteArrayPayload(byte[] values) { private void writeIntArrayPayload(int[] values) { writeUnsigned(values.length, 4); - // 4 バイトずつビッグエンディアンで書く + // 4バイトずつビッグエンディアンで書く for (int value : values) { writeUnsigned(value, 4); } @@ -85,7 +83,7 @@ private void writeIntArrayPayload(int[] values) { private void writeLongArrayPayload(long[] values) { writeUnsigned(values.length, 4); - // 8 バイトずつビッグエンディアンで書く + // 8バイトずつビッグエンディアンで書く for (long value : values) { writeUnsigned(value, 8); } @@ -94,8 +92,8 @@ private void writeLongArrayPayload(long[] values) { private void writeString(String text) { byte[] encoded = Mutf8.encode(text); - // 長さフィールドは u16 - // キー名は素の String なのでここでも検査する + // 長さフィールドはu16 + // キー名は素のStringなのでここでも検査する if (encoded.length > Mutf8.MAX_BYTE_LENGTH) { throw SpringNbtException.invalidArgument( "文字列が長すぎる: MUTF-8 で " + encoded.length + " バイト (上限 " diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtByte.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtByte.java index 555cf6b..ca28981 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtByte.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtByte.java @@ -2,7 +2,7 @@ /** * TAG_Byte - * 8bit 符号付き整数 + * 8bit符号付き整数 */ public final class NbtByte implements NbtTag { diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtByteArray.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtByteArray.java index f4d233d..b59cc5e 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtByteArray.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtByteArray.java @@ -5,7 +5,7 @@ /** * TAG_Byte_Array - * 8bit 符号付き整数の配列 + * 8bit符号付き整数の配列 */ public final class NbtByteArray implements NbtTag { @@ -13,7 +13,7 @@ public final class NbtByteArray implements NbtTag { /** * 配列を指定して作る - * 渡した配列をそのまま保持する(コピーしない) + * 渡した配列をコピーせずにそのまま保持する * * @param value 配列 */ diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtCompound.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtCompound.java index 34d7765..5c9b01b 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtCompound.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtCompound.java @@ -11,21 +11,18 @@ * 挿入順を保持する、名前付きタグのマップ * *

既存キーへの再設定は位置を維持したまま値だけを置き換える - * ({@link LinkedHashMap} の既定の振る舞い) + * ({@link LinkedHashMap}の既定の振る舞い) * これにより読み込んだ順序が書き出しでも保たれ、ラウンドトリップが成立する * *

「キーが無い」と「型が違う」は区別する - * {@code opt*} はキーが無ければ {@code null} を返し、{@code get*} は例外を送出する - * どちらも型が違えば必ず {@link io.github.scriptarts.springnbt.ErrorCode#UNEXPECTED_TAG_TYPE} - * の例外になる - * - *

仕様: {@code docs/spec/10-nbt-binary.md} 7.1章 + * {@code opt*}はキーが無ければ{@code null}を返し、{@code get*}は例外を送出する + * どちらも型が違えば必ず{@link io.github.scriptarts.springnbt.ErrorCode#UNEXPECTED_TAG_TYPE}の例外になる */ public final class NbtCompound implements NbtTag, Iterable> { private final Map entries = new LinkedHashMap<>(); - /** 空の Compound を作る */ + /** 空のCompoundを作る */ public NbtCompound() { // 既定の状態で空 } @@ -57,7 +54,7 @@ public Set keys() { * キーが存在するか * * @param key キー - * @return 存在すれば true + * @return 存在すればtrue */ public boolean containsKey(String key) { return entries.containsKey(key); @@ -73,15 +70,21 @@ public boolean containsKey(String key) { public void set(String key, NbtTag value) { Objects.requireNonNull(key, "key"); Objects.requireNonNull(value, "value"); + + // 孤立サロゲートを含むキーは書き出すと読み戻せないので、ここで止める + if (Mutf8.hasLoneSurrogate(key)) { + throw SpringNbtException.invalidArgument("キーに孤立サロゲートは使えない"); + } + entries.put(key, value); } /** * キーに対応するタグを返す - * 存在しなければ null + * 存在しなければnull * * @param key キー - * @return タグ、または null + * @return タグ、またはnull */ public NbtTag opt(String key) { return entries.get(key); @@ -109,7 +112,7 @@ public NbtTag get(String key) { * キーを削除する * * @param key キー - * @return 削除できたら true + * @return 削除できたらtrue */ public boolean remove(String key) { return entries.remove(key) != null; @@ -151,7 +154,7 @@ public boolean equals(Object other) { var left = entries.entrySet().iterator(); var right = tag.entries.entrySet().iterator(); - // キーと値を挿入順に突き合わせる + // キーと値を挿入順に比較する // 順序も等価性の一部 while (left.hasNext()) { Map.Entry a = left.next(); @@ -178,11 +181,11 @@ public String toString() { // -- 型付き取得子 ------------------------------------------------------- /** - * TAG_Byte を取得する - * キーが無ければ null + * TAG_Byteを取得する + * キーが無ければnull * * @param key キー - * @return 値、または null + * @return 値、またはnull */ public Byte optByte(String key) { NbtByte tag = cast(key, NbtByte.class); @@ -195,7 +198,7 @@ public Byte optByte(String key) { } /** - * TAG_Byte を取得する + * TAG_Byteを取得する * キーが無ければ例外 * * @param key キー @@ -206,11 +209,11 @@ public byte getByte(String key) { } /** - * TAG_Short を取得する - * キーが無ければ null + * TAG_Shortを取得する + * キーが無ければnull * * @param key キー - * @return 値、または null + * @return 値、またはnull */ public Short optShort(String key) { NbtShort tag = cast(key, NbtShort.class); @@ -223,7 +226,7 @@ public Short optShort(String key) { } /** - * TAG_Short を取得する + * TAG_Shortを取得する * キーが無ければ例外 * * @param key キー @@ -234,11 +237,11 @@ public short getShort(String key) { } /** - * TAG_Int を取得する - * キーが無ければ null + * TAG_Intを取得する + * キーが無ければnull * * @param key キー - * @return 値、または null + * @return 値、またはnull */ public Integer optInt(String key) { NbtInt tag = cast(key, NbtInt.class); @@ -251,7 +254,7 @@ public Integer optInt(String key) { } /** - * TAG_Int を取得する + * TAG_Intを取得する * キーが無ければ例外 * * @param key キー @@ -262,11 +265,11 @@ public int getInt(String key) { } /** - * TAG_Long を取得する - * キーが無ければ null + * TAG_Longを取得する + * キーが無ければnull * * @param key キー - * @return 値、または null + * @return 値、またはnull */ public Long optLong(String key) { NbtLong tag = cast(key, NbtLong.class); @@ -279,7 +282,7 @@ public Long optLong(String key) { } /** - * TAG_Long を取得する + * TAG_Longを取得する * キーが無ければ例外 * * @param key キー @@ -290,11 +293,11 @@ public long getLong(String key) { } /** - * TAG_Float を取得する - * キーが無ければ null + * TAG_Floatを取得する + * キーが無ければnull * * @param key キー - * @return 値、または null + * @return 値、またはnull */ public Float optFloat(String key) { NbtFloat tag = cast(key, NbtFloat.class); @@ -307,7 +310,7 @@ public Float optFloat(String key) { } /** - * TAG_Float を取得する + * TAG_Floatを取得する * キーが無ければ例外 * * @param key キー @@ -318,11 +321,11 @@ public float getFloat(String key) { } /** - * TAG_Double を取得する - * キーが無ければ null + * TAG_Doubleを取得する + * キーが無ければnull * * @param key キー - * @return 値、または null + * @return 値、またはnull */ public Double optDouble(String key) { NbtDouble tag = cast(key, NbtDouble.class); @@ -335,7 +338,7 @@ public Double optDouble(String key) { } /** - * TAG_Double を取得する + * TAG_Doubleを取得する * キーが無ければ例外 * * @param key キー @@ -346,12 +349,12 @@ public double getDouble(String key) { } /** - * TAG_Byte を真偽値として取得する - * 0 以外が true - * キーが無ければ null + * TAG_Byteを真偽値として取得する + * 0以外がtrue + * キーが無ければnull * * @param key キー - * @return 値、または null + * @return 値、またはnull */ public Boolean optBool(String key) { Byte raw = optByte(key); @@ -364,8 +367,8 @@ public Boolean optBool(String key) { } /** - * TAG_Byte を真偽値として取得する - * 0 以外が true + * TAG_Byteを真偽値として取得する + * 0以外がtrue * キーが無ければ例外 * * @param key キー @@ -376,11 +379,11 @@ public boolean getBool(String key) { } /** - * TAG_String を取得する - * キーが無ければ null + * TAG_Stringを取得する + * キーが無ければnull * * @param key キー - * @return 値、または null + * @return 値、またはnull */ public String optString(String key) { NbtString tag = cast(key, NbtString.class); @@ -393,7 +396,7 @@ public String optString(String key) { } /** - * TAG_String を取得する + * TAG_Stringを取得する * キーが無ければ例外 * * @param key キー @@ -404,11 +407,11 @@ public String getString(String key) { } /** - * TAG_Byte_Array を取得する - * キーが無ければ null + * TAG_Byte_Arrayを取得する + * キーが無ければnull * * @param key キー - * @return 値、または null + * @return 値、またはnull */ public byte[] optByteArray(String key) { NbtByteArray tag = cast(key, NbtByteArray.class); @@ -421,7 +424,7 @@ public byte[] optByteArray(String key) { } /** - * TAG_Byte_Array を取得する + * TAG_Byte_Arrayを取得する * キーが無ければ例外 * * @param key キー @@ -432,11 +435,11 @@ public byte[] getByteArray(String key) { } /** - * TAG_Int_Array を取得する - * キーが無ければ null + * TAG_Int_Arrayを取得する + * キーが無ければnull * * @param key キー - * @return 値、または null + * @return 値、またはnull */ public int[] optIntArray(String key) { NbtIntArray tag = cast(key, NbtIntArray.class); @@ -449,7 +452,7 @@ public int[] optIntArray(String key) { } /** - * TAG_Int_Array を取得する + * TAG_Int_Arrayを取得する * キーが無ければ例外 * * @param key キー @@ -460,11 +463,11 @@ public int[] getIntArray(String key) { } /** - * TAG_Long_Array を取得する - * キーが無ければ null + * TAG_Long_Arrayを取得する + * キーが無ければnull * * @param key キー - * @return 値、または null + * @return 値、またはnull */ public long[] optLongArray(String key) { NbtLongArray tag = cast(key, NbtLongArray.class); @@ -477,7 +480,7 @@ public long[] optLongArray(String key) { } /** - * TAG_Long_Array を取得する + * TAG_Long_Arrayを取得する * キーが無ければ例外 * * @param key キー @@ -488,18 +491,18 @@ public long[] getLongArray(String key) { } /** - * TAG_List を取得する - * キーが無ければ null + * TAG_Listを取得する + * キーが無ければnull * * @param key キー - * @return 値、または null + * @return 値、またはnull */ public NbtList optList(String key) { return cast(key, NbtList.class); } /** - * TAG_List を取得する + * TAG_Listを取得する * キーが無ければ例外 * * @param key キー @@ -510,18 +513,18 @@ public NbtList getList(String key) { } /** - * TAG_Compound を取得する - * キーが無ければ null + * TAG_Compoundを取得する + * キーが無ければnull * * @param key キー - * @return 値、または null + * @return 値、またはnull */ public NbtCompound optCompound(String key) { return cast(key, NbtCompound.class); } /** - * TAG_Compound を取得する + * TAG_Compoundを取得する * キーが無ければ例外 * * @param key キー @@ -533,7 +536,7 @@ public NbtCompound getCompound(String key) { /** * キーに対応するタグを目的の型として取り出す - * キーが無ければ null、型が違えば例外 + * キーが無ければnull、型が違えば例外 */ private T cast(String key, Class expected) { NbtTag tag = entries.get(key); @@ -566,11 +569,11 @@ private T require(String key, Class expected) { } // -- 型付き設定子 ------------------------------------------------------- // - // set(key, new NbtInt(42)) と書かずに済むようにするための糖衣 - // 取得子の getInt と対になる + // set(key, new NbtInt(42))と書かずに済むようにするための糖衣 + // 取得子のgetIntと対になる /** - * TAG_Byte として設定する + * TAG_Byteとして設定する * * @param key キー * @param value 値 @@ -580,7 +583,7 @@ public void setByte(String key, byte value) { } /** - * TAG_Short として設定する + * TAG_Shortとして設定する * * @param key キー * @param value 値 @@ -590,7 +593,7 @@ public void setShort(String key, short value) { } /** - * TAG_Int として設定する + * TAG_Intとして設定する * * @param key キー * @param value 値 @@ -600,7 +603,7 @@ public void setInt(String key, int value) { } /** - * TAG_Long として設定する + * TAG_Longとして設定する * * @param key キー * @param value 値 @@ -610,7 +613,7 @@ public void setLong(String key, long value) { } /** - * TAG_Float として設定する + * TAG_Floatとして設定する * * @param key キー * @param value 値 @@ -620,7 +623,7 @@ public void setFloat(String key, float value) { } /** - * TAG_Double として設定する + * TAG_Doubleとして設定する * * @param key キー * @param value 値 @@ -630,14 +633,14 @@ public void setDouble(String key, double value) { } /** - * TAG_Byte として設定する - * true は 1、false は 0 + * TAG_Byteとして設定する + * trueは1、falseは0 * * @param key キー * @param value 値 */ public void setBool(String key, boolean value) { - // NBT に真偽値の専用型は無いので TAG_Byte の 0 / 1 で表す + // NBTに真偽値の専用型は無いので、TAG_Byteの0 / 1で表す if (value) { setByte(key, (byte) 1); } else { @@ -646,18 +649,18 @@ public void setBool(String key, boolean value) { } /** - * TAG_String として設定する + * TAG_Stringとして設定する * * @param key キー * @param value 値 - * @throws SpringNbtException MUTF-8 に符号化すると 65535 バイトを超える場合 + * @throws SpringNbtException MUTF-8に符号化すると65535バイトを超える場合 */ public void setString(String key, String value) { set(key, new NbtString(value)); } /** - * TAG_Byte_Array として設定する + * TAG_Byte_Arrayとして設定する * * @param key キー * @param value 値 @@ -667,7 +670,7 @@ public void setByteArray(String key, byte[] value) { } /** - * TAG_Int_Array として設定する + * TAG_Int_Arrayとして設定する * * @param key キー * @param value 値 @@ -677,7 +680,7 @@ public void setIntArray(String key, int[] value) { } /** - * TAG_Long_Array として設定する + * TAG_Long_Arrayとして設定する * * @param key キー * @param value 値 diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtDouble.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtDouble.java index c23e63f..5f82bd0 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtDouble.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtDouble.java @@ -47,7 +47,7 @@ public NbtTag copy() { @Override public boolean equals(Object other) { - // NaN や -0.0 を区別するため、値ではなくビットパターンで比較する + // NaNや-0.0を区別するため、値ではなくビットパターンで比較する return other instanceof NbtDouble tag && Double.doubleToRawLongBits(tag.value) == Double.doubleToRawLongBits(value); } diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtFloat.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtFloat.java index ea99d1f..0c17c91 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtFloat.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtFloat.java @@ -47,7 +47,7 @@ public NbtTag copy() { @Override public boolean equals(Object other) { - // NaN や -0.0 を区別するため、値ではなくビットパターンで比較する + // NaNや-0.0を区別するため、値ではなくビットパターンで比較する return other instanceof NbtFloat tag && Float.floatToRawIntBits(tag.value) == Float.floatToRawIntBits(value); } diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtFormat.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtFormat.java index 76be425..3644039 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtFormat.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtFormat.java @@ -1,21 +1,19 @@ package io.github.scriptarts.springnbt.nbt; /** - * NBT のルートタグの並び方 - * - *

仕様: {@code docs/spec/10-nbt-binary.md} 3章 + * NBTのルートタグの並び方 */ public enum NbtFormat { /** * ファイル形式 * ルートは「タグID + 名前長 + 名前 + ペイロード」の順に並ぶ - * {@code level.dat} やチャンクなど、保存されるデータはすべてこちら + * {@code level.dat}やチャンクなど、保存されるデータはすべてこちら */ JAVA, /** - * ネットワーク形式 (1.20.2 以降) + * ネットワーク形式 (1.20.2以降) * ルートに名前が付かない */ NETWORK diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtInt.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtInt.java index e575fd1..c955866 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtInt.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtInt.java @@ -2,7 +2,7 @@ /** * TAG_Int - * 32bit 符号付き整数 + * 32bit符号付き整数 */ public final class NbtInt implements NbtTag { diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtIntArray.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtIntArray.java index 13a6e7b..a90764e 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtIntArray.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtIntArray.java @@ -5,7 +5,7 @@ /** * TAG_Int_Array - * 32bit 符号付き整数の配列 + * 32bit符号付き整数の配列 */ public final class NbtIntArray implements NbtTag { @@ -13,7 +13,7 @@ public final class NbtIntArray implements NbtTag { /** * 配列を指定して作る - * 渡した配列をそのまま保持する(コピーしない) + * 渡した配列をコピーせずにそのまま保持する * * @param value 配列 */ diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtIo.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtIo.java index 1ff4b71..2706790 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtIo.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtIo.java @@ -19,9 +19,7 @@ import java.util.zip.InflaterInputStream; /** - * NBT のファイル・バイト列・ストリームからの読み書き - * - *

仕様: {@code docs/spec/10-nbt-binary.md} 3章・4章 + * NBTのファイル・バイト列・ストリームからの読み書き */ public final class NbtIo { @@ -30,11 +28,11 @@ private NbtIo() { } /** - * ファイルから NBT を読む + * ファイルからNBTを読む * * @param path ファイルパス * @param options 読み込みオプション - * null なら既定値 + * nullなら既定値 * @return 読み込んだルート * @throws SpringNbtException 読み込みに失敗した場合 */ @@ -53,11 +51,11 @@ public static NamedTag readFile(Path path, NbtReadOptions options) { } /** - * バイト列から NBT を読む + * バイト列からNBTを読む * * @param bytes バイト列 * @param options 読み込みオプション - * null なら既定値 + * nullなら既定値 * @return 読み込んだルート * @throws SpringNbtException 読み込みに失敗した場合 */ @@ -76,16 +74,17 @@ public static NamedTag readBytes(byte[] bytes, NbtReadOptions options) { } /** - * バイト列の指定した位置から NBT を 1 つ読む + * バイト列の指定した位置からNBTを1つ読む * - *

複数の NBT が連なっているデータを、先頭から順に読み進めるために使う - * 戻り値の {@code end} が次の開始位置になる + *

複数のNBTが連なっているデータを、先頭から順に読み進めるために使う + * 戻り値の{@code end}が次の開始位置になる * *

位置は渡したバイト列そのものを指すので、圧縮されたデータは扱えない * * @param bytes 入力 * @param offset 読み始める位置 - * @param options オプション。null なら既定 + * @param options オプション + * nullなら既定 * @return 読んだタグと、その直後の位置 * @throws SpringNbtException 読み込みに失敗した場合 */ @@ -106,15 +105,16 @@ public static NbtReadResult readBytesAt(byte[] bytes, int offset, NbtReadOptions } /** - * バイト列に連なっている NBT をすべて読む + * バイト列に連なっているNBTをすべて読む * *

入力を使い切るまで読み続ける * 空のバイト列なら空の一覧を返す * - *

圧縮は入力全体に 1 回かかっているものとして扱う + *

圧縮は入力全体に1回かかっているものとして扱う * * @param bytes 入力 - * @param options オプション。null なら既定 + * @param options オプション + * nullなら既定 * @return 読んだタグの一覧 * @throws SpringNbtException 読み込みに失敗した場合 */ @@ -124,7 +124,7 @@ public static List readBytesAll(byte[] bytes, NbtReadOptions options) NbtReadOptions effective = effectiveOptions(options); List tags = new ArrayList<>(); - // 空の入力は「0 個」であってエラーではない + // 空の入力は「0個」であってエラーではない if (bytes.length == 0) { return tags; } @@ -160,12 +160,12 @@ private static void requirePlainInput(NbtReadOptions options) { } /** - * ストリームから NBT を読む + * ストリームからNBTを読む * ストリームは最後まで読み切る * * @param stream 入力ストリーム * @param options 読み込みオプション - * null なら既定値 + * nullなら既定値 * @return 読み込んだルート * @throws SpringNbtException 読み込みに失敗した場合 */ @@ -180,12 +180,12 @@ public static NamedTag readStream(InputStream stream, NbtReadOptions options) { } /** - * NBT をファイルへ書き出す + * NBTをファイルへ書き出す * * @param path ファイルパス * @param tag 書き出すルート * @param options 書き込みオプション - * null なら既定値 + * nullなら既定値 * @throws SpringNbtException 書き込みに失敗した場合 */ public static void writeFile(Path path, NamedTag tag, NbtWriteOptions options) { @@ -201,11 +201,11 @@ public static void writeFile(Path path, NamedTag tag, NbtWriteOptions options) { } /** - * NBT をバイト列へ書き出す + * NBTをバイト列へ書き出す * * @param tag 書き出すルート * @param options 書き込みオプション - * null なら既定値 + * nullなら既定値 * @return バイト列 * @throws SpringNbtException 書き込みに失敗した場合 */ @@ -219,7 +219,7 @@ public static byte[] writeBytes(NamedTag tag, NbtWriteOptions options) { effective = options; } - // 書き込み時に AUTO は決められない + // 書き込み時にAUTOは決められない if (effective.compression() == Compression.AUTO) { throw SpringNbtException.invalidArgument("書き込みで Compression.AUTO は指定できない"); } @@ -229,12 +229,12 @@ public static byte[] writeBytes(NamedTag tag, NbtWriteOptions options) { } /** - * NBT をストリームへ書き出す + * NBTをストリームへ書き出す * * @param stream 出力ストリーム * @param tag 書き出すルート * @param options 書き込みオプション - * null なら既定値 + * nullなら既定値 * @throws SpringNbtException 書き込みに失敗した場合 */ public static void writeStream(OutputStream stream, NamedTag tag, NbtWriteOptions options) { @@ -263,13 +263,13 @@ public static Compression detectCompression(byte[] bytes) { throw SpringNbtException.malformed("入力が空で圧縮方式を判定できない"); } - // GZip は必ず 1F 8B で始まる + // GZipは必ず1F 8Bで始まる if (bytes.length >= 2 && (bytes[0] & 0xFF) == 0x1F && (bytes[1] & 0xFF) == 0x8B) { return Compression.GZIP; } if (bytes.length >= 2) { - // zlib ヘッダは「圧縮法が 8 (deflate)」かつ「先頭2バイトが 31 の倍数」 + // zlibヘッダは「圧縮法が8 (deflate)」かつ「先頭2バイトが31の倍数」 boolean isDeflate = (bytes[0] & 0x0F) == 0x08; int header = ((bytes[0] & 0xFF) << 8) | (bytes[1] & 0xFF); @@ -278,7 +278,7 @@ public static Compression detectCompression(byte[] bytes) { } } - // 無圧縮なら先頭は TAG_Compound のタグID + // 無圧縮なら先頭はTAG_CompoundのタグID if ((bytes[0] & 0xFF) == TagType.COMPOUND.id()) { return Compression.NONE; } @@ -290,7 +290,7 @@ public static Compression detectCompression(byte[] bytes) { /** 指定された方式で展開する */ private static byte[] decompress(byte[] bytes, NbtReadOptions options) { Compression method; - // AUTO なら先頭バイトから圧縮方式を見分ける + // AUTOなら先頭バイトから圧縮方式を見分ける if (options.compression() == Compression.AUTO) { method = detectCompression(bytes); } else { @@ -318,7 +318,7 @@ private static byte[] compress(byte[] plain, Compression method) { } try (ByteArrayOutputStream destination = new ByteArrayOutputStream()) { - // ストリームを閉じてフッタを書かせてから toByteArray する必要がある + // ストリームを閉じてフッタを書かせてからtoByteArrayする必要がある try (OutputStream encoder = createEncoder(destination, method)) { encoder.write(plain); } diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtList.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtList.java index 7d7cff8..3989357 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtList.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtList.java @@ -8,14 +8,11 @@ /** * TAG_List - * 要素型が 1 つに固定されたタグの列 + * 要素型が1つに固定されたタグの列 * - *

空リストの要素型は {@link TagType#END} + *

空リストの要素型は{@link TagType#END} * 最初の要素を追加した時点で型が確定する - * 全要素を削除しても確定済みの要素型は維持される - * (読み書きの往復で型が消えないようにするため) - * - *

仕様: {@code docs/spec/10-nbt-binary.md} 7.2章 + * 読み書きの往復で型が消えないよう、全要素を削除しても確定済みの要素型は維持される */ public final class NbtList implements NbtTag, Iterable { @@ -41,7 +38,7 @@ public NbtList(TagType elementType) { /** * 要素の型 - * 空で未確定なら {@link TagType#END} + * 空で未確定なら{@link TagType#END} * * @return 要素型 */ @@ -76,6 +73,8 @@ public NbtTag get(int index) { * @throws SpringNbtException 要素型と一致しない場合 */ public void set(int index, NbtTag item) { + // 要素型を確定させる前に位置を確かめ、失敗したときに状態を変えない + Objects.checkIndex(index, items.size()); ensureElementType(item); items.set(index, item); } @@ -99,6 +98,11 @@ public void add(NbtTag item) { * @throws SpringNbtException 要素型と一致しない場合 */ public void insert(int index, NbtTag item) { + // 要素型を確定させる前に位置を確かめ、失敗したときに状態を変えない + if (index < 0 || index > items.size()) { + throw new IndexOutOfBoundsException("位置が範囲外: " + index); + } + ensureElementType(item); items.add(index, item); } diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtLong.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtLong.java index 02d8f53..e66c024 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtLong.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtLong.java @@ -2,7 +2,7 @@ /** * TAG_Long - * 64bit 符号付き整数 + * 64bit符号付き整数 */ public final class NbtLong implements NbtTag { diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtLongArray.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtLongArray.java index fd146c3..7b9e357 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtLongArray.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtLongArray.java @@ -5,7 +5,7 @@ /** * TAG_Long_Array - * 64bit 符号付き整数の配列 + * 64bit符号付き整数の配列 */ public final class NbtLongArray implements NbtTag { @@ -13,7 +13,7 @@ public final class NbtLongArray implements NbtTag { /** * 配列を指定して作る - * 渡した配列をそのまま保持する(コピーしない) + * 渡した配列をコピーせずにそのまま保持する * * @param value 配列 */ diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtReadOptions.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtReadOptions.java index 664e6d1..8037506 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtReadOptions.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtReadOptions.java @@ -1,9 +1,7 @@ package io.github.scriptarts.springnbt.nbt; /** - * NBT 読み込みのオプション - * - *

仕様: {@code docs/spec/10-nbt-binary.md} 6章 / {@code docs/spec/00-conventions.md} 5章 + * NBT読み込みのオプション */ public final class NbtReadOptions { @@ -46,7 +44,7 @@ public NbtReadOptions setFormat(NbtFormat value) { /** * 圧縮方式 - * 既定は {@link Compression#AUTO} + * 既定は{@link Compression#AUTO} * * @return 圧縮方式 */ @@ -67,7 +65,7 @@ public NbtReadOptions setCompression(Compression value) { /** * ネストの深さ上限 - * 既定は 512 + * 既定は512 * * @return 深さ上限 */ diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtReadResult.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtReadResult.java index e0e5ca2..0156ed3 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtReadResult.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtReadResult.java @@ -5,9 +5,7 @@ * *

読んだタグと、その直後の位置を持つ * - *

続けて読むときは {@code end} を次の開始位置として渡す - * - *

仕様: {@code docs/spec/10-nbt-binary.md} 3.1章 + *

続けて読むときは{@code end}を次の開始位置として渡す * * @param tag 読んだタグ * @param end 読み終わった直後の位置 diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtShort.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtShort.java index 74b99d1..7344771 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtShort.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtShort.java @@ -2,7 +2,7 @@ /** * TAG_Short - * 16bit 符号付き整数 + * 16bit符号付き整数 */ public final class NbtShort implements NbtTag { diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtString.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtString.java index e744659..2fa28ae 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtString.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtString.java @@ -5,7 +5,7 @@ /** * TAG_String - * MUTF-8 で符号化される文字列 + * MUTF-8で符号化される文字列 */ public final class NbtString implements NbtTag { @@ -15,7 +15,7 @@ public final class NbtString implements NbtTag { * 値を指定して作る * * @param value 値 - * @throws SpringNbtException MUTF-8 に符号化すると 65535 バイトを超える場合 + * @throws SpringNbtException MUTF-8に符号化すると65535バイトを超える場合 */ public NbtString(String value) { this.value = validate(value); @@ -34,7 +34,7 @@ public String value() { * 値を設定する * * @param newValue 新しい値 - * @throws SpringNbtException MUTF-8 に符号化すると 65535 バイトを超える場合 + * @throws SpringNbtException MUTF-8に符号化すると65535バイトを超える場合 */ public void setValue(String newValue) { this.value = validate(newValue); @@ -65,13 +65,13 @@ public String toString() { return value; } - /** 長さフィールドが u16 のため、符号化後 65535 バイトを超える文字列は保持できない */ + /** 長さフィールドがu16のため、符号化後65535バイトを超える文字列は保持できない */ private static String validate(String candidate) { Objects.requireNonNull(candidate, "value"); int byteLength = Mutf8.byteLength(candidate); - // 長さフィールドは u16 - // 65535 を超えると書き出せない + // 長さフィールドはu16 + // 65535を超えると書き出せない if (byteLength > Mutf8.MAX_BYTE_LENGTH) { throw SpringNbtException.invalidArgument( "文字列が長すぎる: MUTF-8 で " + byteLength + " バイト (上限 " diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtTag.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtTag.java index 66fea85..f6f4e1a 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtTag.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtTag.java @@ -1,11 +1,9 @@ package io.github.scriptarts.springnbt.nbt; /** - * NBT のタグ + * NBTのタグ * - *

{@code sealed} なので {@code switch} のパターンマッチで網羅的に分岐できる - * - *

仕様: {@code docs/spec/10-nbt-binary.md} 1章 + *

{@code sealed}なので{@code switch}のパターンマッチで網羅的に分岐できる */ public sealed interface NbtTag permits NbtByte, NbtShort, NbtInt, NbtLong, NbtFloat, NbtDouble, diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtWriteOptions.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtWriteOptions.java index 72e9b44..dcd343f 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtWriteOptions.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/NbtWriteOptions.java @@ -1,9 +1,7 @@ package io.github.scriptarts.springnbt.nbt; /** - * NBT 書き込みのオプション - * - *

仕様: {@code docs/spec/10-nbt-binary.md} 6章 + * NBT書き込みのオプション */ public final class NbtWriteOptions { @@ -50,7 +48,7 @@ public NbtWriteOptions setFormat(NbtFormat value) { /** * 圧縮方式 - * 既定は {@link Compression#GZIP} + * 既定は{@link Compression#GZIP} * * @return 圧縮方式 */ diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/Snbt.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/Snbt.java index a7537be..70d449c 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/Snbt.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/Snbt.java @@ -5,12 +5,10 @@ import java.util.Objects; /** - * SNBT (Stringified NBT) のパースと出力 + * SNBT (Stringified NBT)のパースと出力 * - *

対応範囲は「バイナリ NBT へ損失なく写せる部分集合」 - * 1.21.5 以降の異種リスト({@code [1, "a"]})は受理しない - * - *

仕様: {@code docs/spec/11-snbt.md} / {@code docs/adr/0006-snbt-scope.md} + *

対応範囲は「バイナリNBTへ損失なく写せる部分集合」 + * 1.21.5以降の異種リスト({@code [1, "a"]})は受理しない */ public final class Snbt { @@ -21,9 +19,9 @@ private Snbt() { } /** - * SNBT 文字列をタグへ変換する + * SNBT文字列をタグへ変換する * - * @param text SNBT 文字列 + * @param text SNBT文字列 * @return タグ * @throws SpringNbtException 構文が不正な場合 */ @@ -33,11 +31,11 @@ public static NbtTag parse(String text) { } /** - * SNBT 文字列を Compound へ変換する + * SNBT文字列をCompoundへ変換する * - * @param text SNBT 文字列 + * @param text SNBT文字列 * @return Compound - * @throws SpringNbtException 構文が不正、またはルートが Compound でない場合 + * @throws SpringNbtException 構文が不正、またはルートがCompoundでない場合 */ public static NbtCompound parseCompound(String text) { NbtTag tag = parse(text); @@ -50,10 +48,10 @@ public static NbtCompound parseCompound(String text) { } /** - * タグを 1 行の SNBT へ変換する + * タグを1行のSNBTへ変換する * * @param tag タグ - * @return SNBT 文字列 + * @return SNBT文字列 */ public static String write(NbtTag tag) { Objects.requireNonNull(tag, "tag"); @@ -63,11 +61,11 @@ public static String write(NbtTag tag) { } /** - * タグを整形した SNBT へ変換する - * インデントは空白 4 個 + * タグを整形したSNBTへ変換する + * インデントは空白4個 * * @param tag タグ - * @return SNBT 文字列 + * @return SNBT文字列 */ public static String writePretty(NbtTag tag) { Objects.requireNonNull(tag, "tag"); @@ -78,7 +76,7 @@ public static String writePretty(NbtTag tag) { /** * タグを書き出す - * {@code depth} が負なら 1 行、0 以上なら整形して出力する + * {@code depth}が負なら1行、0以上なら整形して出力する */ private static void writeTag(StringBuilder builder, NbtTag tag, int depth) { switch (tag) { @@ -98,7 +96,7 @@ private static void writeTag(StringBuilder builder, NbtTag tag, int depth) { } private static void writeCompound(StringBuilder builder, NbtCompound compound, int depth) { - // 空の compound は改行もインデントも入れず {} と書く + // 空のcompoundは改行もインデントも入れず{}と書く if (compound.size() == 0) { builder.append("{}"); return; @@ -131,7 +129,7 @@ private static void writeCompound(StringBuilder builder, NbtCompound compound, i } private static void writeList(StringBuilder builder, NbtList list, int depth) { - // 空のリストは改行もインデントも入れず [] と書く + // 空のリストは改行もインデントも入れず[]と書く if (list.size() == 0) { builder.append("[]"); return; @@ -158,7 +156,7 @@ private static void writeList(StringBuilder builder, NbtList list, int depth) { private static void writeByteArray(StringBuilder builder, byte[] values) { builder.append("[B;"); - // 型付き配列は 1 行に収める + // 型付き配列は1行に収める // 要素には接尾辞を付ける for (int index = 0; index < values.length; index++) { if (index > 0) { @@ -174,7 +172,7 @@ private static void writeByteArray(StringBuilder builder, byte[] values) { private static void writeIntArray(StringBuilder builder, int[] values) { builder.append("[I;"); - // IntArray の要素は接尾辞なし + // IntArrayの要素は接尾辞なし for (int index = 0; index < values.length; index++) { if (index > 0) { builder.append(','); @@ -189,7 +187,7 @@ private static void writeIntArray(StringBuilder builder, int[] values) { private static void writeLongArray(StringBuilder builder, long[] values) { builder.append("[L;"); - // LongArray の要素には L 接尾辞を付ける + // LongArrayの要素にはL接尾辞を付ける for (int index = 0; index < values.length; index++) { if (index > 0) { builder.append(','); @@ -201,7 +199,7 @@ private static void writeLongArray(StringBuilder builder, long[] values) { builder.append(']'); } - /** 整形出力なら改行とインデントを、1 行出力なら何も入れない */ + /** 整形出力なら改行とインデントを、1行出力なら何も入れない */ private static void appendSeparator(StringBuilder builder, int depth) { if (depth < 0) { return; @@ -215,7 +213,7 @@ private static void appendSeparator(StringBuilder builder, int depth) { } } - /** 整形出力のときだけ深さを 1 段進める */ + /** 整形出力のときだけ深さを1段進める */ private static int nextDepth(int depth) { if (depth < 0) { return -1; @@ -256,7 +254,7 @@ private static String quoteString(String text) { StringBuilder builder = new StringBuilder(text.length() + 2); builder.append('"'); - // 1 文字ずつ見てエスケープが要るものだけ置き換える + // 1文字ずつ見てエスケープが要るものだけ置き換える for (int index = 0; index < text.length(); index++) { char c = text.charAt(index); @@ -278,7 +276,7 @@ private static String quoteString(String text) { builder.append(text.charAt(index + 1)); index += 1; } else if (c < 0x20 || c == 0x7F || Character.isSurrogate(c)) { - // 制御文字と孤立サロゲートは \\uXXXX で表す + // 制御文字と孤立サロゲートは\\uXXXXで表す builder.append(String.format("\\u%04x", (int) c)); } else { builder.append(c); diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/SnbtParser.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/SnbtParser.java index 647b2e1..2c6a4bc 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/SnbtParser.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/SnbtParser.java @@ -7,9 +7,7 @@ import java.util.UUID; /** - * SNBT (Stringified NBT) のパーサ - * - *

仕様: {@code docs/spec/11-snbt.md} + * SNBT (Stringified NBT)のパーサ */ final class SnbtParser { @@ -24,7 +22,7 @@ final class SnbtParser { } /** - * 入力全体を 1 つの値として読む + * 入力全体を1つの値として読む * 末尾に余りがあれば例外 */ NbtTag parseWhole() { @@ -68,13 +66,13 @@ private NbtCompound parseCompound() { NbtCompound compound = new NbtCompound(); skipWhitespace(); - // 空の Compound + // 空のCompound if (peek() == '}') { position += 1; return compound; } - // 要素を 1 つずつ読む + // 要素を1つずつ読む while (true) { skipWhitespace(); @@ -106,7 +104,7 @@ private NbtCompound parseCompound() { private NbtTag parseListOrArray() { expect('['); - // "[B;" のような型付き配列かどうかを先に判定する + // "[B;"のような型付き配列かどうかを先に判定する if (position + 1 < text.length() && text.charAt(position + 1) == ';') { char marker = text.charAt(position); @@ -129,7 +127,7 @@ private NbtList parseList() { return list; } - // 要素を 1 つずつ読む + // 要素を1つずつ読む while (true) { skipWhitespace(); @@ -141,7 +139,7 @@ private NbtList parseList() { NbtTag value = parseValue(); - // 異種リストはバイナリ NBT へ写せないため受理しない (adr/0006) + // 異種リストはバイナリNBTへ写せないため受理しない if (list.elementType() != TagType.END && list.elementType() != value.type()) { throw malformed("リストに異なる型が混在している: " + list.elementType().asString() + " と " + value.type().asString()); @@ -200,7 +198,7 @@ private NbtTag parseTypedArray(char marker) { if (marker == 'B') { byte[] result = new byte[values.size()]; - // 各要素が Byte の範囲に収まるか確認しながら詰める + // 各要素がByteの範囲に収まるか確認しながら詰める for (int i = 0; i < values.size(); i++) { long value = values.get(i); @@ -217,7 +215,7 @@ private NbtTag parseTypedArray(char marker) { if (marker == 'I') { int[] result = new int[values.size()]; - // 各要素が Int の範囲に収まるか確認しながら詰める + // 各要素がIntの範囲に収まるか確認しながら詰める for (int i = 0; i < values.size(); i++) { long value = values.get(i); @@ -258,8 +256,16 @@ private long toIntegral(NbtTag tag) { private String parseKey() { char c = peek(); + // 引用符で始まるキーは、エスケープを解いて読む if (c == '"' || c == '\'') { - return parseQuotedString(); + String quoted = parseQuotedString(); + + // バイナリの読み込みと同じく、キーには孤立サロゲートを許さない + if (Mutf8.hasLoneSurrogate(quoted)) { + throw malformed("Compound のキーが UTF-8 に写せない(孤立サロゲートを含む)"); + } + + return quoted; } String bare = readBareToken(); @@ -332,7 +338,7 @@ private long readHexDigits(int count) { long value = 0; - // 指定桁数ぶん 16進数字を読む + // 指定桁数ぶん16進数字を読む for (int i = 0; i < count; i++) { char c = text.charAt(position + i); int digit = hexDigitValue(c); @@ -349,7 +355,7 @@ private long readHexDigits(int count) { } private void appendCodePoint(StringBuilder builder, long codePoint) { - // Unicode のコードポイント範囲を外れていないか確認する + // Unicodeのコードポイント範囲を外れていないか確認する if (codePoint < 0 || codePoint > 0x10FFFF) { throw malformed(String.format("コードポイントが範囲外: U+%X", codePoint)); } @@ -357,7 +363,7 @@ private void appendCodePoint(StringBuilder builder, long codePoint) { builder.appendCodePoint((int) codePoint); } - /** Unicode 文字名によるエスケープ {@code \N{...}} を読む */ + /** Unicode文字名によるエスケープ{@code \N{...}}を読む */ private void appendNamedCharacter(StringBuilder builder) { expect('{'); int start = position; @@ -374,7 +380,7 @@ private void appendNamedCharacter(StringBuilder builder) { String name = text.substring(start, position); position += 1; - // 実装間で Unicode 文字名の表が揃わないため対応しない(C# / Rust には表が無い) + // 実装間でUnicode文字名の表が揃わないため対応しない(C# / TypeScript / Rustには表が無い) throw new SpringNbtException( ErrorCode.UNSUPPORTED_FEATURE, "文字名によるエスケープには対応していない: \\N{" + name + "}"); @@ -387,7 +393,7 @@ private NbtTag parseUnquoted() { throw malformed("値が来るべき位置に解釈できない文字がある: '" + peekOrNul() + "'"); } - // bool(...) / uuid(...) の関数呼び出し + // bool(...) / uuid(...)の関数呼び出し skipWhitespace(); if (peekOrNul() == '(' && (token.equals("bool") || token.equals("uuid"))) { return parseFunction(token); @@ -417,7 +423,7 @@ private NbtTag parseFunction(String name) { expect(')'); if (name.equals("bool")) { - // 0 以外を真とする + // 0以外を真とする if (toIntegral(argument) != 0) { return new NbtByte((byte) 1); } @@ -440,7 +446,7 @@ private NbtIntArray uuidToIntArray(NbtTag argument) { throw malformed("UUID として解釈できない: " + stringTag.value()); } - // UUID を上位から 32bit ずつ 4 要素の IntArray へ写す + // UUIDを上位から32bitずつ4要素のIntArrayへ写す long high = parsed.getMostSignificantBits(); long low = parsed.getLeastSignificantBits(); @@ -451,7 +457,7 @@ private NbtIntArray uuidToIntArray(NbtTag argument) { /** * 数値トークンを解釈する - * 数値として読めなければ null を返す(文字列として扱われる) + * 数値として読めなければnullを返す(文字列として扱われる) */ private NbtTag tryParseNumber(String token) { boolean negative = false; @@ -475,7 +481,7 @@ private NbtTag tryParseNumber(String token) { boolean isHex = isHexBody(body); // 幅接尾辞を末尾から剥がす - // 16進では b/d/f が数字と紛れるため s/l だけを認める + // 16進ではb/d/fが数字と紛れるためs/lだけを認める char last = body.charAt(body.length() - 1); boolean suffixAllowed; @@ -485,13 +491,13 @@ private NbtTag tryParseNumber(String token) { suffixAllowed = WIDTH_SUFFIXES.indexOf(last) >= 0; } - // 末尾 1 文字が型の印なら切り離す - // 1 文字だけの token は数字そのもの + // 末尾1文字が型の印なら切り離す + // 符号を除いた本体が1文字だけなら、接尾辞とみなさず剥がさない if (suffixAllowed && body.length() >= 2) { widthSuffix = Character.toLowerCase(last); body = body.substring(0, body.length() - 1); - // 符号接尾辞 u / s は幅接尾辞の手前に置かれる + // 符号接尾辞u / sは幅接尾辞の手前に置かれる if (body.length() >= 2) { char signChar = body.charAt(body.length() - 1); @@ -550,13 +556,13 @@ private static boolean isHexBody(String body) { } private static boolean isBinaryBody(String body) { - // 0b / 0B で始まり、続きがある場合だけ 2 進リテラルとみなす + // 0b / 0Bで始まり、続きがある場合だけ2進リテラルとみなす if (!(body.length() > 2 && body.charAt(0) == '0' && (body.charAt(1) == 'b' || body.charAt(1) == 'B'))) { return false; } - // 2進リテラルの本体は 0 と 1 だけ + // 2進リテラルの本体は0と1だけ for (int index = 2; index < body.length(); index++) { char c = body.charAt(index); @@ -581,7 +587,7 @@ private NbtTag makeFloating(double value, boolean negative, char widthSuffix) { return new NbtFloat((float) signed); } - // 接尾辞なしの小数は Double + // 接尾辞なしの小数はDouble if (widthSuffix == '\0' || widthSuffix == 'd') { return new NbtDouble(signed); } @@ -639,8 +645,8 @@ private NbtTag makeIntegral(long magnitude, boolean negative, case 'l' -> new NbtLong(value); case 'f' -> new NbtFloat((float) value); case 'd' -> new NbtDouble(value); - // 接尾辞なしの整数は Int - // 暗黙に Long へ格上げしない + // 接尾辞なしの整数はInt + // 暗黙にLongへ格上げしない default -> new NbtInt( (int) checkRange(value, Integer.MIN_VALUE, Integer.MAX_VALUE, "int")); }; @@ -657,7 +663,7 @@ private long checkUnsigned(long magnitude, long max) { private long toSigned(long magnitude, boolean negative) { if (negative) { - // Long.MIN_VALUE の絶対値は long に収まらないため個別に扱う + // Long.MIN_VALUEの絶対値はlongに収まらないため個別に扱う if (magnitude == Long.MIN_VALUE) { return Long.MIN_VALUE; } @@ -755,7 +761,7 @@ private char peek() { /** * 末尾でも例外にしない先読み - * 入力が尽きていれば NUL を返す + * 入力が尽きていればNULを返す */ private char peekOrNul() { if (position >= text.length()) { diff --git a/java/src/main/java/io/github/scriptarts/springnbt/nbt/TagType.java b/java/src/main/java/io/github/scriptarts/springnbt/nbt/TagType.java index f835c1c..2dcfc3c 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/nbt/TagType.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/nbt/TagType.java @@ -3,16 +3,14 @@ import io.github.scriptarts.springnbt.SpringNbtException; /** - * NBT のタグ型 - * {@link #id()} は仕様が定めるタグIDと一致する - * - *

仕様: {@code docs/spec/10-nbt-binary.md} 1章 + * NBTのタグ型 + * {@link #id()}は仕様が定めるタグIDと一致する */ public enum TagType { /** * TAG_End (0) - * Compound の終端を表す + * Compoundの終端を表す */ END(0, "end"), @@ -81,14 +79,14 @@ public String asString() { } /** - * タグIDから {@link TagType} を得る + * タグIDから{@link TagType}を得る * * @param id タグID * @return タグ型 * @throws SpringNbtException 未知のタグIDの場合 */ public static TagType fromId(int id) { - // 0..12 の範囲外はすべて不正なタグID + // 0..12の範囲外はすべて不正なタグID if (id < 0 || id >= BY_ID.length) { throw SpringNbtException.malformed("未知のタグID: " + id); } diff --git a/java/src/main/java/io/github/scriptarts/springnbt/world/BitStorage.java b/java/src/main/java/io/github/scriptarts/springnbt/world/BitStorage.java index e186a36..11f4f1f 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/world/BitStorage.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/world/BitStorage.java @@ -4,13 +4,11 @@ import java.util.Objects; /** - * 添字を 64bit 整数の配列へ詰めた表現 - * 1.16 以降の跨ぎなしパッキング + * 添字を64bit整数の配列へ詰めた表現 + * 1.16以降の跨ぎなしパッキング * - *

1 つの {@code long} に入りきらない分は、その {@code long} の残りビットを未使用のまま捨て、 - * 次の {@code long} の最下位ビットから始める - * - *

仕様: {@code docs/spec/31-paletted-container.md} 2章 + *

1つの{@code long}に入りきらない分は、その{@code long}の残りビットを未使用のまま捨て、 + * 次の{@code long}の最下位ビットから始める */ public final class BitStorage { @@ -25,7 +23,7 @@ private BitStorage(long[] data, int bitsPerEntry, int entryCount) { } /** - * 1 エントリあたりのビット数 + * 1エントリあたりのビット数 * * @return ビット数 */ @@ -35,7 +33,7 @@ public int bitsPerEntry() { /** * エントリ数 - * ブロックなら 4096、バイオームなら 64 + * ブロックなら4096、バイオームなら64 * * @return エントリ数 */ @@ -44,7 +42,7 @@ public int entryCount() { } /** - * 1 つの {@code long} に入るエントリ数 + * 1つの{@code long}に入るエントリ数 * * @return エントリ数 */ @@ -68,12 +66,13 @@ public static BitStorage create(int bitsPerEntry, int entryCount) { } /** - * 既存の {@code long} 配列から作る + * 既存の{@code long}配列から作る * - * @param data packed な配列 + * @param data packedな配列 * @param bitsPerEntry パレット長から求めたビット幅 * @param entryCount エントリ数 - * @param lenient true なら配列長からビット幅を逆算して読む(第三者ツール由来の救済) + * @param lenient trueなら、配列長が期待値と違う場合に配列長からビット幅を逆算して読む + * 第三者ツールが書いたデータの救済用 * @return 記憶域 * @throws SpringNbtException 配列長が期待値と一致しない場合 */ @@ -103,7 +102,7 @@ public static BitStorage fromLongs(long[] data, int bitsPerEntry, int entryCount } /** - * 必要な {@code long} の個数を求める + * 必要な{@code long}の個数を求める * * @param bitsPerEntry ビット幅 * @param entryCount エントリ数 @@ -156,7 +155,7 @@ public void set(int index, int value) { } /** - * packed な配列を返す + * packedな配列を返す * 内部の配列をそのまま返す(コピーしない) * * @return 配列 diff --git a/java/src/main/java/io/github/scriptarts/springnbt/world/BlockPos.java b/java/src/main/java/io/github/scriptarts/springnbt/world/BlockPos.java index fc0b0f1..4afaa35 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/world/BlockPos.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/world/BlockPos.java @@ -5,8 +5,6 @@ /** * ブロックの絶対座標 * - *

仕様: {@code docs/spec/30-chunk-format.md} 5章 - * * @param x X座標 * @param y Y座標 * @param z Z座標 diff --git a/java/src/main/java/io/github/scriptarts/springnbt/world/BlockState.java b/java/src/main/java/io/github/scriptarts/springnbt/world/BlockState.java index e6d9ec9..af541e7 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/world/BlockState.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/world/BlockState.java @@ -15,13 +15,9 @@ * 名前と、任意のプロパティの組 * *

プロパティは常に名前の昇順で保持する - * こうしておくと文字列表現が一意になり、 - * 全言語で同じ出力になる - * Minecraft が書き出した並び順は - * {@link PalettedContainer} がパレットを生の NBT のまま持つことで守られるので、 - * 触っていないブロックの並びが崩れることはない - * - *

仕様: {@code docs/spec/30-chunk-format.md} 2.1章 + * こうしておくと文字列表現が一意になり、全言語で同じ出力になる + * Minecraftが書き出した並び順は、{@link PalettedContainer}がパレットを生のNBTのまま持つことで守られる + * そのため、触っていないブロックの並びが崩れることはない */ public final class BlockState { @@ -32,9 +28,9 @@ public final class BlockState { * 名前とプロパティを指定して作る * * @param name ブロックID - * 名前空間が省略されていたら {@code minecraft:} を補う + * 名前空間が省略されていたら{@code minecraft:}を補う * @param properties プロパティ - * null なら空 + * nullなら空 */ public BlockState(String name, Map properties) { Objects.requireNonNull(name, "name"); @@ -79,14 +75,14 @@ public Map properties() { * * @param key プロパティ名 * @return 値 - * 無ければ null + * 無ければnull */ public String property(String key) { return properties.get(key); } /** - * プロパティを 1 つ差し替えた新しい状態を返す + * プロパティを1つ差し替えた新しい状態を返す * * @param key プロパティ名 * @param value 値 @@ -102,7 +98,7 @@ public BlockState with(String key, String value) { } /** - * {@code minecraft:oak_stairs[facing=north,half=top]} 形式の文字列から作る + * {@code minecraft:oak_stairs[facing=north,half=top]}形式の文字列から作る * * @param text 文字列 * @return ブロック状態 @@ -131,7 +127,7 @@ public static BlockState parse(String text) { return state; } - // "key=value" をカンマ区切りで読む + // "key=value"をカンマ区切りで読む for (String pair : body.split(",")) { int equals = pair.indexOf('='); @@ -157,11 +153,11 @@ public static BlockState parse(String text) { } /** - * パレット要素の NBT から作る + * パレット要素のNBTから作る * * @param nbt パレット要素 * @return ブロック状態 - * @throws SpringNbtException {@code Name} が無い、または {@code Properties} の値が文字列でない場合 + * @throws SpringNbtException {@code Name}が無い、または{@code Properties}の値が文字列でない場合 */ public static BlockState fromNbt(NbtCompound nbt) { Objects.requireNonNull(nbt, "nbt"); @@ -172,7 +168,7 @@ public static BlockState fromNbt(NbtCompound nbt) { return state; } - // Properties の値はすべて文字列(数値や真偽値も文字列で入る) + // Propertiesの値はすべて文字列(数値や真偽値も文字列で入る) for (Map.Entry entry : propertiesTag) { if (!(entry.getValue() instanceof NbtString text)) { throw SpringNbtException.unexpectedTagType("Properties の \"" + entry.getKey() @@ -186,10 +182,10 @@ public static BlockState fromNbt(NbtCompound nbt) { } /** - * パレット要素の NBT へ変換する + * パレット要素のNBTへ変換する * - *

プロパティが空なら {@code Properties} キー自体を出力しない - * Minecraft と同じ振る舞い + *

プロパティが空なら{@code Properties}キー自体を出力しない + * Minecraftと同じ振る舞い * * @return NBT */ @@ -212,7 +208,7 @@ public NbtCompound toNbt() { return result; } - /** 名前空間が省略されていたら {@code minecraft:} を補う */ + /** 名前空間が省略されていたら{@code minecraft:}を補う */ private static String normalize(String name) { if (name.indexOf(':') >= 0) { return name; @@ -234,7 +230,7 @@ public int hashCode() { } /** - * {@code minecraft:oak_stairs[facing=north,half=top]} 形式の文字列を返す + * {@code minecraft:oak_stairs[facing=north,half=top]}形式の文字列を返す */ @Override public String toString() { diff --git a/java/src/main/java/io/github/scriptarts/springnbt/world/Chunk.java b/java/src/main/java/io/github/scriptarts/springnbt/world/Chunk.java index da372d0..8032e80 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/world/Chunk.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/world/Chunk.java @@ -17,20 +17,18 @@ import java.util.TreeMap; /** - * チャンク 1 つ分 - * 地形の読み書きの入口 + * チャンク1つ分 + * 地形の読み書きはここから行う * - *

読んだ NBT をそのまま保持し、変更した部分だけを書き戻す - * 未知のキーを落とさないので、将来の追加要素があってもデータを壊さない - * - *

仕様: {@code docs/spec/30-chunk-format.md} + *

読んだNBTをそのまま保持し、書き戻すときに作り直すのはセクションだけにする + * 未知のキーを落とさないので、将来の追加要素があってもデータをおかしくしない */ public final class Chunk { - /** セクション 1 つに入るブロック数 */ + /** セクション1つに入るブロック数 */ public static final int BLOCKS_PER_SECTION = 4096; - /** セクション 1 つに入るバイオームのエントリ数(4×4×4 単位) */ + /** セクション1つに入るバイオームのエントリ数(4×4×4単位) */ public static final int BIOMES_PER_SECTION = 64; /** @@ -76,7 +74,7 @@ public int z() { /** * 最下段セクションのY位置 - * オーバーワールドは -4 + * オーバーワールドは-4 * * @return Y位置 */ @@ -85,7 +83,7 @@ public int minSectionY() { } /** - * 生成段階({@code minecraft:full} など) + * 生成段階({@code minecraft:full}など) * * @return 生成段階 */ @@ -97,7 +95,7 @@ public String status() { * 生成が完了しているか * ブロック改変の対象にしてよいのはこれだけ * - * @return 完了していれば true + * @return 完了していればtrue */ public boolean isFullyGenerated() { return "minecraft:full".equals(status()); @@ -107,12 +105,11 @@ public boolean isFullyGenerated() { * このチャンクに変更が加わったか * *

ブロックやバイオームを書き換えると立つ - * {@link Dimension#flush()} はこれが立っているチャンクだけを書き戻す + * {@link Dimension#flush()}はこれが立っているチャンクだけを書き戻す * - *

{@link #raw()} を直接いじった場合はここが立たないので、 - * 自分で {@link #setIsModified(boolean)} を呼ぶこと + *

{@link #raw()}を直接いじった場合は立たないので、自分で{@link #setIsModified(boolean)}を呼ぶこと * - * @return 変更があれば true + * @return 変更があればtrue */ public boolean isModified() { return modified; @@ -121,7 +118,7 @@ public boolean isModified() { /** * 変更の印を付け外しする * - * @param value 立てるなら true + * @param value 立てるならtrue */ public void setIsModified(boolean value) { modified = value; @@ -138,7 +135,7 @@ public Set sectionYs() { } /** - * 元の NBT + * 元のNBT * 解釈していないキーもここに残っている * * @return NBT @@ -148,11 +145,11 @@ public NbtCompound raw() { } /** - * NBT からチャンクを読む + * NBTからチャンクを読む * - * @param nbt チャンクの NBT + * @param nbt チャンクのNBT * @param options 読み込みオプション - * null なら既定値 + * nullなら既定値 * @return チャンク * @throws SpringNbtException 必須のキーが無い、または構造が想定と違う場合 */ @@ -174,7 +171,7 @@ public static Chunk fromNbt(NbtCompound nbt, ChunkReadOptions options) { return chunk; } - // 並び順に依存しないよう、Y から索引を作る + // 並び順に依存しないよう、Yから索引を作る for (NbtTag entry : sectionList) { if (!(entry instanceof NbtCompound sectionTag)) { throw SpringNbtException.unexpectedTagType( @@ -188,7 +185,7 @@ public static Chunk fromNbt(NbtCompound nbt, ChunkReadOptions options) { return chunk; } - /** DataVersion を検査し、オプションに従って警告またはエラーにする */ + /** DataVersionを検査し、オプションに従って警告またはエラーにする */ private void checkDataVersion(ChunkReadOptions options) { int version = dataVersion(); @@ -212,13 +209,13 @@ private void checkDataVersion(ChunkReadOptions options) { } /** - * NBT へ書き戻す - * 変更したセクションだけを反映し、他のキーはそのまま残す + * NBTへ書き戻す + * 全セクションを書き戻し、他のキーはそのまま残す * * @param options 書き込みオプション - * null なら既定値 + * nullなら既定値 * @return NBT - * @throws SpringNbtException DataVersion が対象と違い、かつ書き戻しが許可されていない場合 + * @throws SpringNbtException DataVersionが扱える形式より古く、かつ書き戻しが許可されていない場合 */ public NbtCompound toNbt(ChunkWriteOptions options) { ChunkWriteOptions effective; @@ -230,7 +227,7 @@ public NbtCompound toNbt(ChunkWriteOptions options) { int version = dataVersion(); - // 形式の違う古いチャンクは、書き戻すと壊しかねない + // 形式の違う古いチャンクは、書き戻すと使えなくなりかねない if (version < SpringNbt.MIN_SUPPORTED_DATA_VERSION && !effective.allowForeignDataVersion()) { throw new SpringNbtException(ErrorCode.UNSUPPORTED_DATA_VERSION, "DataVersion " + version + " のチャンクは書き戻せない(" @@ -239,7 +236,7 @@ public NbtCompound toNbt(ChunkWriteOptions options) { + " ChunkWriteOptions.setAllowForeignDataVersion(true)"); } - // DataVersion は読んだ値のまま残す + // DataVersionは読んだ値のまま残す // 書き換えると、そのワールドを開くゲーム側の判断を誤らせる if (sections.isEmpty()) { @@ -248,7 +245,7 @@ public NbtCompound toNbt(ChunkWriteOptions options) { NbtList sectionList = new NbtList(TagType.COMPOUND); - // Y の昇順で書き出す + // Yの昇順で書き出す for (ChunkSection section : sections.values()) { sectionList.add(section.toNbt()); } @@ -262,7 +259,7 @@ public NbtCompound toNbt(ChunkWriteOptions options) { * * @param sectionY セクションのY位置 * @return セクション - * 無ければ null + * 無ければnull */ public ChunkSection section(int sectionY) { return sections.get(sectionY); @@ -275,7 +272,7 @@ public ChunkSection section(int sectionY) { * @param y 絶対Y座標 * @param z チャンク内相対Z座標 (0..15) * @return ブロック - * セクションが無い、または block_states を持たない場合は null + * セクションが無い、またはblock_statesを持たない場合はnull */ public BlockState getBlock(int x, int y, int z) { checkLocalCoordinates(x, z); @@ -302,7 +299,7 @@ public BlockState getBlock(int x, int y, int z) { * @param y 絶対Y座標 * @param z チャンク内相対Z座標 (0..15) * @param state ブロック - * @throws SpringNbtException 対象のセクションが無い、または block_states を持たない場合 + * @throws SpringNbtException 対象のセクションが無い、またはblock_statesを持たない場合 */ public void setBlock(int x, int y, int z, String state) { Objects.requireNonNull(state, "state"); @@ -316,7 +313,7 @@ public void setBlock(int x, int y, int z, String state) { * @param y 絶対Y座標 * @param z チャンク内相対Z座標 (0..15) * @param state ブロック - * @throws SpringNbtException 対象のセクションが無い、または block_states を持たない場合 + * @throws SpringNbtException 対象のセクションが無い、またはblock_statesを持たない場合 */ public void setBlock(int x, int y, int z, BlockState state) { Objects.requireNonNull(state, "state"); @@ -331,7 +328,7 @@ public void setBlock(int x, int y, int z, BlockState state) { } // 同じ状態を置き直すだけなら、付随データを触る理由がない - // プロパティの並び順に左右されないよう、NBT ではなく BlockState として比べる + // プロパティの並び順に左右されないよう、NBTではなくBlockStateとして比べる BlockState current = getBlock(x, y, z); if (current != null && current.equals(state)) { @@ -346,14 +343,14 @@ public void setBlock(int x, int y, int z, BlockState state) { /** * その座標を指す付随データを取り除く * - *

{@code block_entities} / {@code block_ticks} / {@code fluid_ticks} の要素は - * いずれも {@code x} {@code y} {@code z} を絶対座標で持つ + *

{@code block_entities} / {@code block_ticks} / {@code fluid_ticks}の要素は + * いずれも{@code x} {@code y} {@code z}を絶対座標で持つ */ private void removeBlockData(int x, int y, int z) { int absoluteX = (x() * 16) + x; int absoluteZ = (z() * 16) + z; - // 3 つのリストは形が同じなので、まとめて同じ処理をかける + // 3つのリストは形が同じなので、まとめて同じ処理をかける for (String key : BLOCK_DATA_KEYS) { NbtList list = root.optList(key); @@ -390,13 +387,13 @@ private static boolean matchesPosition(NbtCompound entry, int x, int y, int z) { /** * バイオームを取得する - * 4×4×4 の単位なので、座標は自動的に丸められる + * 4×4×4の単位なので、座標は自動的に丸められる * * @param x チャンク内相対X座標 (0..15) * @param y 絶対Y座標 * @param z チャンク内相対Z座標 (0..15) * @return バイオームID - * セクションが無い場合は null + * セクションが無い場合はnull */ public String getBiome(int x, int y, int z) { checkLocalCoordinates(x, z); @@ -418,7 +415,7 @@ public String getBiome(int x, int y, int z) { /** * バイオームを設定する - * 4×4×4 の単位 + * 4×4×4の単位 * * @param x チャンク内相対X座標 (0..15) * @param y 絶対Y座標 @@ -442,18 +439,17 @@ public void setBiome(int x, int y, int z, String biome) { } /** - * {@code Heightmaps} を削除し、Minecraft に再計算させる + * {@code Heightmaps}を削除し、Minecraftに再計算させる * *

本ライブラリは高さマップを再計算しない * ブロックを改変したら呼ぶこと - * ({@code docs/adr/0004-defer-heightmap-recalc.md}) */ public void clearHeightmaps() { root.remove("Heightmaps"); modified = true; } - /** {@code isLightOn} を 0 にし、光源の再計算を促す */ + /** {@code isLightOn}を0にし、光源の再計算を促す */ public void invalidateLighting() { root.setByte("isLightOn", (byte) 0); modified = true; @@ -472,7 +468,7 @@ public void compact() { /** * セクション内のブロック添字 * - *

{@code & 15} により負のY座標でも正しく求まる + *

{@code & 15}により負のY座標でも正しく求まる * * @param x X座標 * @param y Y座標 @@ -485,7 +481,7 @@ public static int blockIndex(int x, int y, int z) { /** * セクション内のバイオーム添字 - * 1 エントリが 4×4×4 ブロック + * 1エントリが4×4×4ブロック * * @param x X座標 * @param y Y座標 @@ -497,7 +493,7 @@ public static int biomeIndex(int x, int y, int z) { } private static void checkLocalCoordinates(int x, int z) { - // チャンク内相対座標は 0..15 でなければならない + // チャンク内相対座標は0..15でなければならない if (x < 0 || x > 15 || z < 0 || z > 15) { throw SpringNbtException.invalidArgument( "チャンク内相対座標が範囲外: (" + x + ", " + z + ")。X も Z も 0..15 であること"); diff --git a/java/src/main/java/io/github/scriptarts/springnbt/world/ChunkReadOptions.java b/java/src/main/java/io/github/scriptarts/springnbt/world/ChunkReadOptions.java index f20200c..5195e7d 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/world/ChunkReadOptions.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/world/ChunkReadOptions.java @@ -4,8 +4,6 @@ /** * チャンク読み込みのオプション - * - *

仕様: {@code docs/spec/30-chunk-format.md} 5章 */ public final class ChunkReadOptions { @@ -23,7 +21,7 @@ public static ChunkReadOptions defaults() { } /** - * DataVersion が扱える形式より古いときの動作 + * DataVersionが扱える形式より古いときの動作 * * @return 動作 */ @@ -32,7 +30,7 @@ public VersionMismatchAction onVersionMismatch() { } /** - * DataVersion が扱える形式より古いときの動作を設定する + * DataVersionが扱える形式より古いときの動作を設定する * * @param value 動作 * @return このオブジェクト @@ -44,7 +42,7 @@ public ChunkReadOptions setOnVersionMismatch(VersionMismatchAction value) { /** * 警告の通知先 - * null なら何もしない + * nullなら何もしない * * @return 通知先 */ @@ -64,18 +62,18 @@ public ChunkReadOptions setOnWarning(Consumer value) { } /** - * data の長さが期待値と違うとき、長さからビット幅を逆算して読むか + * dataの長さが期待値と違うとき、長さからビット幅を逆算して読むか * - * @return 逆算するなら true + * @return 逆算するならtrue */ public boolean lenientBitStorage() { return lenientBitStorage; } /** - * data の長さから逆算するかを設定する + * dataの長さから逆算するかを設定する * - * @param value 逆算するなら true + * @param value 逆算するならtrue * @return このオブジェクト */ public ChunkReadOptions setLenientBitStorage(boolean value) { diff --git a/java/src/main/java/io/github/scriptarts/springnbt/world/ChunkSection.java b/java/src/main/java/io/github/scriptarts/springnbt/world/ChunkSection.java index 9d03c4f..88b09ed 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/world/ChunkSection.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/world/ChunkSection.java @@ -4,12 +4,10 @@ import java.util.Objects; /** - * チャンクを Y 方向に 16 ブロックずつ区切った 16×16×16 の立方体 + * チャンクをY方向に16ブロックずつ区切った16×16×16の立方体 * - *

{@code BlockLight} / {@code SkyLight} などの解釈していないキーは元の NBT に残り、 + *

{@code BlockLight} / {@code SkyLight}などの解釈していないキーは元のNBTに残り、 * 書き戻しでそのまま出力される - * - *

仕様: {@code docs/spec/30-chunk-format.md} 2章 */ public final class ChunkSection { @@ -26,7 +24,7 @@ private ChunkSection(NbtCompound raw, int y) { /** * セクションのY位置 - * オーバーワールドは -5..20 + * オーバーワールドは-5..20 * * @return Y位置 */ @@ -36,7 +34,7 @@ public int y() { /** * ブロック状態 - * 持たないセクション(光源専用)では null + * 持たないセクション(光源専用)ではnull * * @return ブロック状態 */ @@ -46,7 +44,7 @@ public PalettedContainer blockStates() { /** * バイオーム - * 持たないセクションでは null + * 持たないセクションではnull * * @return バイオーム */ @@ -57,7 +55,7 @@ public PalettedContainer biomes() { /** * ブロック状態を持つか * - * @return 持つなら true + * @return 持つならtrue */ public boolean hasBlockStates() { return blockStates != null; @@ -66,14 +64,14 @@ public boolean hasBlockStates() { /** * バイオームを持つか * - * @return 持つなら true + * @return 持つならtrue */ public boolean hasBiomes() { return biomes != null; } /** - * 元の NBT + * 元のNBT * 解釈していないキーもここに残っている * * @return NBT @@ -83,9 +81,9 @@ public NbtCompound raw() { } /** - * NBT からセクションを読む + * NBTからセクションを読む * - * @param nbt セクションの NBT + * @param nbt セクションのNBT * @param options 読み込みオプション * @return セクション */ @@ -94,7 +92,7 @@ public static ChunkSection fromNbt(NbtCompound nbt, ChunkReadOptions options) { ChunkSection section = new ChunkSection(nbt, nbt.getByte("Y")); NbtCompound blockStatesTag = nbt.optCompound("block_states"); - // 光源専用のセクションは block_states を持たない + // 光源専用のセクションはblock_statesを持たない if (blockStatesTag != null) { section.blockStates = PalettedContainer.fromNbt( blockStatesTag, Chunk.BLOCKS_PER_SECTION, 4, options.lenientBitStorage()); @@ -111,7 +109,7 @@ public static ChunkSection fromNbt(NbtCompound nbt, ChunkReadOptions options) { } /** - * NBT へ書き戻す + * NBTへ書き戻す * 解釈していないキーはそのまま残る * * @return NBT diff --git a/java/src/main/java/io/github/scriptarts/springnbt/world/ChunkWriteOptions.java b/java/src/main/java/io/github/scriptarts/springnbt/world/ChunkWriteOptions.java index a03bcc2..a498004 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/world/ChunkWriteOptions.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/world/ChunkWriteOptions.java @@ -15,22 +15,22 @@ public static ChunkWriteOptions defaults() { } /** - * 対象バージョン以外の DataVersion を持つチャンクの書き戻しを許すか + * 扱える形式より古いDataVersionを持つチャンクの書き戻しを許すか * - *

既定は false - * 古いワールドを黙って新形式で上書きし、 - * 利用者が気づかないうちに壊すことを防ぐため({@code docs/adr/0003-version-policy.md}) + *

既定はfalse + * エラーを出さずに古いワールドを新形式で上書きし、 + * 利用者が気づかないうちに使えなくすることを防ぐため * - * @return 許すなら true + * @return 許すならtrue */ public boolean allowForeignDataVersion() { return allowForeignDataVersion; } /** - * 対象バージョン以外の書き戻しを許すかを設定する + * 扱える形式より古いDataVersionを持つチャンクの書き戻しを許すかを設定する * - * @param value 許すなら true + * @param value 許すならtrue * @return このオブジェクト */ public ChunkWriteOptions setAllowForeignDataVersion(boolean value) { diff --git a/java/src/main/java/io/github/scriptarts/springnbt/world/Cuboid.java b/java/src/main/java/io/github/scriptarts/springnbt/world/Cuboid.java index c749272..5961340 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/world/Cuboid.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/world/Cuboid.java @@ -5,9 +5,10 @@ import java.util.Objects; /** - * ブロック座標の直方体な範囲 + * ブロック座標の直方体の範囲 * - *

両端を含む。{@code of(0, 0, 0, 0, 0, 0)} は 1 ブロック + *

両端を含む + * {@code of(0, 0, 0, 0, 0, 0)}は1ブロック * *

範囲内のブロックを順に処理したいときに使う */ @@ -23,7 +24,8 @@ public final class Cuboid { /** * 両端の座標から作る * - *

大小の順序は問わない。内部で小さいほうを最小に揃える + *

大小の順序は問わない + * 内部で小さいほうを最小に揃える * * @param first 端の座標 * @param second もう一方の端の座標 @@ -56,7 +58,7 @@ public static Cuboid of(int x1, int y1, int z1, int x2, int y2, int z2) { } /** - * X の最小値 + * Xの最小値 * * @return 座標 */ @@ -65,7 +67,7 @@ public int minX() { } /** - * Y の最小値 + * Yの最小値 * * @return 座標 */ @@ -74,7 +76,7 @@ public int minY() { } /** - * Z の最小値 + * Zの最小値 * * @return 座標 */ @@ -83,7 +85,7 @@ public int minZ() { } /** - * X の最大値(含む) + * Xの最大値(含む) * * @return 座標 */ @@ -92,7 +94,7 @@ public int maxX() { } /** - * Y の最大値(含む) + * Yの最大値(含む) * * @return 座標 */ @@ -101,7 +103,7 @@ public int maxY() { } /** - * Z の最大値(含む) + * Zの最大値(含む) * * @return 座標 */ @@ -110,7 +112,7 @@ public int maxZ() { } /** - * X 方向の長さ + * X方向の長さ * * @return 長さ */ @@ -119,7 +121,7 @@ public int sizeX() { } /** - * Y 方向の長さ + * Y方向の長さ * * @return 長さ */ @@ -128,7 +130,7 @@ public int sizeY() { } /** - * Z 方向の長さ + * Z方向の長さ * * @return 長さ */ @@ -151,7 +153,7 @@ public long volume() { * @param x X座標 * @param y Y座標 * @param z Z座標 - * @return 含まれれば true + * @return 含まれればtrue */ public boolean contains(int x, int y, int z) { return x >= minX && x <= maxX && y >= minY && y <= maxY && z >= minZ && z <= maxZ; @@ -160,7 +162,7 @@ public boolean contains(int x, int y, int z) { /** * 範囲内の座標を順に返す * - *

並びは Y、Z、X の順で、X がいちばん内側で動く + *

並びはY、Z、Xの順で、Xがいちばん内側で動く * * @return 座標の並び */ @@ -185,7 +187,7 @@ public BlockPos next() { BlockPos current = new BlockPos(x, y, z); x++; - // 内側から X が動くので、同じチャンクの並びを続けて触れる + // Xがいちばん内側で動くので、同じチャンクの並びを続けて扱える if (x > maxX) { x = minX; z++; diff --git a/java/src/main/java/io/github/scriptarts/springnbt/world/Dimension.java b/java/src/main/java/io/github/scriptarts/springnbt/world/Dimension.java index 2a21d05..c5cceb5 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/world/Dimension.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/world/Dimension.java @@ -17,13 +17,10 @@ import java.util.Set; /** - * ワールド内の次元 1 つ分 - * {@code region/} {@code entities/} {@code poi/} をまとめて扱う + * ワールド内の次元1つ分 + * {@code region/} {@code entities/} {@code poi/}をまとめて扱う * - *

ブロックの取得・設定は絶対ワールド座標で行い、 - * リージョン・チャンク・セクションの解決は内部で済ませる - * - *

仕様: {@code docs/spec/40-world-layout.md} 4章 + *

ブロックの取得・設定は絶対ワールド座標で行い、リージョン・チャンク・セクションの解決は内部で済ませる */ public final class Dimension implements AutoCloseable { @@ -53,7 +50,7 @@ public final class Dimension implements AutoCloseable { } /** - * 次元ID({@code minecraft:overworld} など) + * 次元ID({@code minecraft:overworld}など) * * @return 次元ID */ @@ -72,7 +69,7 @@ public Path directory() { /** * 地形のリージョンフォルダ - * 無ければ null + * 無ければnull * * @return フォルダ */ @@ -83,7 +80,7 @@ public RegionFolder regionFolder() { /** * エンティティのリージョンフォルダ - * 無ければ null + * 無ければnull * * @return フォルダ */ @@ -93,8 +90,8 @@ public RegionFolder entityFolder() { } /** - * POI のリージョンフォルダ - * 無ければ null + * POIのリージョンフォルダ + * 無ければnull * * @return フォルダ */ @@ -104,11 +101,13 @@ public RegionFolder poiFolder() { } /** - * {@code data/minecraft/.dat} を読む + * {@code data/minecraft/.dat}を読む + * + *

次元ごとの{@code world_border} / {@code raids} / {@code chunk_tickets}などが入る * * @param name ファイル名(拡張子なし) * @return NBT - * 存在しなければ null + * 存在しなければnull */ public NbtCompound dataFile(String name) { ensureOpen(); @@ -144,7 +143,7 @@ public List chunkPositions() { * @param chunkX 絶対チャンクX座標 * @param chunkZ 絶対チャンクZ座標 * @return チャンク - * 存在しなければ null + * 存在しなければnull */ public Chunk chunk(int chunkX, int chunkZ) { ensureOpen(); @@ -199,7 +198,7 @@ public void saveChunk(Chunk chunk) { * @param y Y座標 * @param z Z座標 * @return ブロック - * チャンクが無ければ null + * チャンクが無ければnull */ public BlockState getBlock(int x, int y, int z) { Chunk chunk = chunk(x >> 4, z >> 4); @@ -214,8 +213,8 @@ public BlockState getBlock(int x, int y, int z) { /** * 絶対座標でブロックを設定する * - *

変更したチャンクは記録され、{@link #flush()} でまとめて書き戻される - * 本ライブラリはチャンクを新規生成しないので、存在しない座標はエラーになる + *

変更したチャンクには印が付き、{@link #flush()}でまとめて書き戻される + * 本ライブラリはチャンクを新規生成しないので、チャンクが存在しない座標はエラーになる * * @param x X座標 * @param y Y座標 @@ -230,7 +229,7 @@ public void setBlock(int x, int y, int z, String state) { /** * 絶対座標でブロックを設定する * - *

変更したチャンクには印が付き、{@link #flush()} でまとめて書き戻される + *

変更したチャンクには印が付き、{@link #flush()}でまとめて書き戻される * * @param x 絶対X座標 * @param y 絶対Y座標 @@ -256,13 +255,13 @@ public void setBlock(int x, int y, int z, BlockState state) { /** * 絶対座標でバイオームを取得する - * 4×4×4 の単位 + * 4×4×4の単位 * * @param x X座標 * @param y Y座標 * @param z Z座標 * @return バイオームID - * チャンクが無ければ null + * チャンクが無ければnull */ public String getBiome(int x, int y, int z) { Chunk chunk = chunk(x >> 4, z >> 4); @@ -276,7 +275,7 @@ public String getBiome(int x, int y, int z) { /** * 絶対座標でバイオームを設定する - * 4×4×4 の単位 + * 4×4×4の単位 * * @param x X座標 * @param y Y座標 @@ -359,7 +358,7 @@ public void close() { /** * フォルダを遅延して開く - * 存在しなければ null のまま + * 存在しなければnullのまま */ private RegionFolder folder(RegionFolder slot, String name) { ensureOpen(); @@ -398,7 +397,7 @@ private void ensureOpen() { } } - /** チャンク座標を 1 つの long に詰めてキャッシュの鍵にする */ + /** チャンク座標を1つのlongに詰めてキャッシュのキーにする */ private static long chunkKey(int chunkX, int chunkZ) { return ((long) chunkX << 32) | (chunkZ & 0xFFFFFFFFL); } diff --git a/java/src/main/java/io/github/scriptarts/springnbt/world/LevelData.java b/java/src/main/java/io/github/scriptarts/springnbt/world/LevelData.java index 4eda422..d65d67c 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/world/LevelData.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/world/LevelData.java @@ -4,12 +4,9 @@ import io.github.scriptarts.springnbt.nbt.NbtCompound; /** - * {@code level.dat} の内容 + * {@code level.dat}の内容 * - *

26.x では大幅に軽量化されており、ゲームルールやワールド生成設定は - * {@code data/minecraft/} 配下の個別ファイルへ分離されている - * - *

仕様: {@code docs/spec/40-world-layout.md} 2章 + *

26.xでは大幅に軽量化されており、ゲームルールやワールド生成設定は{@code data/minecraft/}配下の個別ファイルへ分離されている */ public final class LevelData { @@ -24,8 +21,8 @@ public final class LevelData { } /** - * ルートの NBT - * {@code Data} を含む + * ルートのNBT + * {@code Data}を含む * * @return NBT */ @@ -34,7 +31,7 @@ public NbtCompound raw() { } /** - * {@code Data} の中身 + * {@code Data}の中身 * 実際の設定はここに入っている * * @return NBT @@ -81,7 +78,7 @@ public int gameType() { } /** - * スポーン地点の {@code [x, y, z]} + * スポーン地点の{@code [x, y, z]} * * @return 座標 */ @@ -99,7 +96,7 @@ public String spawnDimension() { } /** - * 難易度({@code normal} など) + * 難易度({@code normal}など) * * @return 難易度 */ @@ -110,14 +107,14 @@ public String difficulty() { /** * ハードコアか * - * @return ハードコアなら true + * @return ハードコアならtrue */ public boolean isHardcore() { return data.getCompound("difficulty_settings").getBool("hardcore"); } /** - * バージョン名({@code 26.2} など) + * バージョン名({@code 26.2}など) * * @return バージョン名 */ @@ -126,7 +123,7 @@ public String versionName() { } /** - * 書き出し用の {@link NamedTag} を作る + * 書き出し用の{@link NamedTag}を作る * * @return NamedTag */ diff --git a/java/src/main/java/io/github/scriptarts/springnbt/world/MinecraftWorld.java b/java/src/main/java/io/github/scriptarts/springnbt/world/MinecraftWorld.java index 626c3b8..2b089fb 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/world/MinecraftWorld.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/world/MinecraftWorld.java @@ -7,12 +7,14 @@ import io.github.scriptarts.springnbt.nbt.NbtIo; import java.io.IOException; import java.io.RandomAccessFile; +import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -21,12 +23,9 @@ import java.util.Objects; /** - * Minecraft Java版のセーブデータ 1 つ分 + * Minecraft Java版のセーブデータ1つ分 * - *

26.x では構成が大きく変わっており、標準の3次元も - * {@code dimensions/<名前空間>/<パス>/} の下に並ぶ - * - *

仕様: {@code docs/spec/40-world-layout.md} + *

26.xでは構成が大きく変わっており、標準の3次元も{@code dimensions/<名前空間>/<パス>/}の下に並ぶ */ public final class MinecraftWorld implements AutoCloseable { @@ -52,7 +51,7 @@ public Path directory() { } /** - * {@code level.dat} の内容 + * {@code level.dat}の内容 * * @return level.dat */ @@ -65,10 +64,10 @@ public LevelData level() { * * @param directory ワールドディレクトリ * @param options オプション - * null なら既定値 + * nullなら既定値 * @return ワールド - * @throws SpringNbtException ディレクトリや level.dat が無い場合、 - * または書き込みモードで session.lock を取得できない場合 + * @throws SpringNbtException ディレクトリやlevel.datが無い場合、 + * または書き込みモードでsession.lockを取得できない場合 */ public static MinecraftWorld open(Path directory, WorldOpenOptions options) { Objects.requireNonNull(directory, "directory"); @@ -90,7 +89,7 @@ public static MinecraftWorld open(Path directory, WorldOpenOptions options) { throw new SpringNbtException(ErrorCode.IO, "level.dat が無い: " + levelPath); } - // 書き込むなら、Minecraft が起動中でないことを先に確かめる + // 書き込むなら、Minecraftが起動中でないことを先に確かめる if (effective.writable() && !effective.ignoreSessionLock()) { checkSessionLock(directory); } @@ -108,7 +107,7 @@ public static MinecraftWorld open(Path directory) { return open(directory, null); } - /** {@code session.lock} を排他で開けるか確かめる */ + /** {@code session.lock}を排他で開けるか確かめる */ private static void checkSessionLock(Path directory) { Path lockPath = directory.resolve("session.lock"); @@ -134,14 +133,13 @@ private static void checkSessionLock(Path directory) { } /** - * {@code data/minecraft/.dat} を読む + * {@code data/minecraft/.dat}を読む * - *

26.x では {@code game_rules} / {@code weather} / {@code world_gen_settings} などが - * この形で {@code level.dat} から分離されている + *

26.xでは{@code game_rules} / {@code weather} / {@code world_gen_settings}などがこの形で{@code level.dat}から分離されている * * @param name ファイル名(拡張子なし) * @return NBT - * 存在しなければ null + * 存在しなければnull */ public NbtCompound dataFile(String name) { ensureOpen(); @@ -168,16 +166,16 @@ public List dimensionIds() { return found; } - // dimensions/<名前空間>/<パス>/ の 2 段を辿る + // dimensions/<名前空間>/<パス>/の2段を辿る try (DirectoryStream namespaces = Files.newDirectoryStream(root)) { - // 1 段目が名前空間 + // 1段目が名前空間 for (Path namespaceDir : namespaces) { if (!Files.isDirectory(namespaceDir)) { continue; } try (DirectoryStream paths = Files.newDirectoryStream(namespaceDir)) { - // 2 段目が次元のパス + // 2段目が次元のパス for (Path pathDir : paths) { // ディレクトリだけを次元として数える if (Files.isDirectory(pathDir)) { @@ -198,10 +196,10 @@ public List dimensionIds() { /** * 次元を得る * - * @param dimensionId {@code minecraft:overworld} のような名前空間つきのID - * 名前空間が省略されていたら {@code minecraft:} を補う + * @param dimensionId {@code minecraft:overworld}のような名前空間つきのID + * 名前空間が省略されていたら{@code minecraft:}を補う * @return 次元 - * ディレクトリが無ければ null + * ディレクトリが無ければnull */ public Dimension dimension(String dimensionId) { ensureOpen(); @@ -243,7 +241,7 @@ public List playerIds() { } try (DirectoryStream files = Files.newDirectoryStream(path, "*.dat")) { - // .dat の名前部分が UUID にあたる + // .datの名前部分がUUIDにあたる for (Path file : files) { String name = file.getFileName().toString(); found.add(name.substring(0, name.length() - 4)); @@ -261,7 +259,7 @@ public List playerIds() { * * @param uuid プレイヤーのUUID * @return NBT - * 存在しなければ null + * 存在しなければnull */ public NbtCompound player(String uuid) { ensureOpen(); @@ -276,10 +274,10 @@ public NbtCompound player(String uuid) { } /** - * {@code level.dat} を書き戻す + * {@code level.dat}を書き戻す * - *

壊れるとワールド全体が開けなくなるため、 - * 一時ファイルへ書いてから {@code level.dat_old} へ退避し、最後に置き換える + *

{@code level.dat}がおかしくなるとワールド全体が開けなくなるため、 + * 一時ファイルへ書いて内容をディスクへ確実に書き出してから、既存の{@code level.dat}を{@code level.dat_old}へ退避し、最後に置き換える */ public void saveLevel() { ensureOpen(); @@ -293,9 +291,22 @@ public void saveLevel() { Path backup = directory.resolve("level.dat_old"); try { - NbtIo.writeFile(temporary, level.toNamedTag(), null); + byte[] encoded = NbtIo.writeBytes(level.toNamedTag(), null); + + // 一時ファイルへ書き、置き換える前に内容をディスクへ確実に書き出す + try (FileChannel channel = FileChannel.open(temporary, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { + ByteBuffer buffer = ByteBuffer.wrap(encoded); + + // 全バイトを書き終えるまで書き続ける + while (buffer.hasRemaining()) { + channel.write(buffer); + } + + channel.force(true); + } - // 既存の level.dat は、置き換える前に level.dat_old へ退避する + // 既存のlevel.datは、置き換える前にlevel.dat_oldへ退避する if (Files.exists(path)) { Files.copy(path, backup, StandardCopyOption.REPLACE_EXISTING); } @@ -328,7 +339,7 @@ private void ensureOpen() { } } - /** 名前空間が省略されていたら {@code minecraft:} を補う */ + /** 名前空間が省略されていたら{@code minecraft:}を補う */ private static String normalizeDimensionId(String dimensionId) { if (dimensionId.indexOf(':') >= 0) { return dimensionId; diff --git a/java/src/main/java/io/github/scriptarts/springnbt/world/PalettedContainer.java b/java/src/main/java/io/github/scriptarts/springnbt/world/PalettedContainer.java index 0d82d3e..1e43eab 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/world/PalettedContainer.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/world/PalettedContainer.java @@ -14,11 +14,9 @@ * パレットとビットストレージの組 * セクション内のブロック状態やバイオームを格納する * - *

パレットの要素は生の {@link NbtTag} のまま持つ - * こうすると、触っていないブロックについては Minecraft が書き出したときの + *

パレットの要素は生の{@link NbtTag}のまま持つ + * こうすると、触っていないブロックについてはMinecraftが書き出したときの * プロパティの並び順まで含めてそのまま書き戻せる - * - *

仕様: {@code docs/spec/31-paletted-container.md} */ public final class PalettedContainer { @@ -35,7 +33,7 @@ private PalettedContainer(int entryCount, int minBits) { /** * エントリ数 - * ブロックなら 4096、バイオームなら 64 + * ブロックなら4096、バイオームなら64 * * @return エントリ数 */ @@ -45,7 +43,7 @@ public int entryCount() { /** * ビット幅の下限 - * ブロックなら 4、バイオームなら 1 + * ブロックなら4、バイオームなら1 * * @return 下限 */ @@ -65,7 +63,7 @@ public List palette() { /** * 現在のビット幅 - * パレットが 1 要素なら 0(記憶域を持たない) + * パレットが1要素なら0(記憶域を持たない) * * @return ビット幅 */ @@ -93,14 +91,14 @@ public static PalettedContainer filled(NbtTag value, int entryCount, int minBits } /** - * NBT から読み込む + * NBTから読み込む * - * @param nbt コンテナの NBT + * @param nbt コンテナのNBT * @param entryCount エントリ数 * @param minBits ビット幅の下限 - * @param lenientBitStorage data の長さが合わないとき、長さから逆算するか + * @param lenientBitStorage dataの長さが合わないとき、長さから逆算するか * @return コンテナ - * @throws SpringNbtException パレットが空、data の長さが合わない、添字が範囲外のいずれか + * @throws SpringNbtException パレットが空、dataの長さが合わない、添字がパレット範囲外のいずれか */ public static PalettedContainer fromNbt( NbtCompound nbt, int entryCount, int minBits, boolean lenientBitStorage) { @@ -112,7 +110,7 @@ public static PalettedContainer fromNbt( throw SpringNbtException.malformed("palette が無いか空"); } - // パレットの要素は生の NbtTag のまま持つ + // パレットの要素は生のNbtTagのまま持つ // 並び順まで元どおりに書き戻すため for (NbtTag entry : paletteTag) { result.palette.add(entry); @@ -121,7 +119,7 @@ public static PalettedContainer fromNbt( long[] data = nbt.optLongArray("data"); if (data == null) { - // パレットが 1 要素なら data は無くてよい + // パレットが1要素ならdataは無くてよい if (result.palette.size() != 1) { throw SpringNbtException.malformed( "palette が " + result.palette.size() + " 要素なのに data が無い"); @@ -134,7 +132,7 @@ public static PalettedContainer fromNbt( result.storage = BitStorage.fromLongs(data, bits, entryCount, lenientBitStorage); // 取り出した添字がパレットの範囲に収まっているか確かめる - // 黙って 0 番目で代替すると、壊れたデータをそうと分からない形で書き戻してしまう + // エラーを出さずに0番目で代替すると、おかしくなったデータをそうと分からない形で書き戻してしまう for (int index = 0; index < entryCount; index++) { int value = result.storage.get(index); @@ -148,7 +146,7 @@ public static PalettedContainer fromNbt( } /** - * NBT へ変換する + * NBTへ変換する * * @return NBT */ @@ -161,8 +159,8 @@ public NbtCompound toNbt() { paletteTag.add(entry); } - // パレットが 1 要素なら data は書かない - // Minecraft と同じ振る舞い + // パレットが1要素ならdataは書かない + // Minecraftと同じ振る舞い if (storage != null && palette.size() > 1) { result.set("data", new NbtLongArray(storage.toLongs())); } @@ -180,7 +178,7 @@ public NbtCompound toNbt() { public NbtTag get(int index) { checkIndex(index); - // 記憶域が無いということは、全エントリがパレットの 0 番目 + // 記憶域が無いということは、全エントリがパレットの0番目 if (storage == null) { return palette.get(0); } @@ -201,7 +199,7 @@ public void set(int index, NbtTag value) { int paletteIndex = indexOfOrAdd(value); - // 記憶域が無く、書き込む値も 0 番目なら何もしなくてよい + // 記憶域が無く、書き込む値も0番目なら何もしなくてよい if (storage == null && paletteIndex == 0) { return; } @@ -211,8 +209,8 @@ public void set(int index, NbtTag value) { } /** - * 全エントリを 1 つの値で埋める - * パレットもその 1 要素だけにする + * 全エントリを1つの値で埋める + * パレットもその1要素だけにする * * @param value 値 */ @@ -226,7 +224,7 @@ public void fill(NbtTag value) { /** * どのエントリからも参照されていないパレット要素を取り除き、添字を振り直す * - *

大量の {@code set} を行う用途で遅くならないよう、明示的に呼んだときだけ実行する + *

大量の{@code set}を行う用途で遅くならないよう、明示的に呼んだときだけ実行する */ public void compact() { if (storage == null) { @@ -270,7 +268,7 @@ public void compact() { palette.addAll(compacted); if (compacted.size() == 1) { - // 1 要素になったら記憶域を捨てる + // 1要素になったら記憶域を捨てる storage = null; } else { storage = rebuilt; @@ -282,7 +280,7 @@ public void compact() { * 無ければ末尾へ追加する */ private int indexOfOrAdd(NbtTag value) { - // パレットは高々 4096 要素なので線形探索で足りる + // パレットは高々4096要素なので線形探索で足りる for (int index = 0; index < palette.size(); index++) { if (palette.get(index).equals(value)) { return index; @@ -298,7 +296,7 @@ private void ensureStorage() { int required = Math.max(minBits, ceilLog2(palette.size())); if (storage == null) { - // これまで単一値だったので、全エントリが 0 番目のまま始まる + // これまで単一値だったので、全エントリが0番目のまま始まる storage = BitStorage.create(required, entryCount); return; } @@ -319,8 +317,8 @@ private void checkIndex(int index) { } /** - * {@code count} 個の値を表すのに必要な最小ビット数 - * 1 なら 0 + * {@code count}個の値を表すのに必要な最小ビット数 + * 1なら0 * * @param count 値の個数 * @return ビット数 @@ -328,7 +326,7 @@ private void checkIndex(int index) { public static int ceilLog2(int count) { int bits = 0; - // 1 を超える分だけシフトして数える + // 1を超える分だけシフトして数える while ((1 << bits) < count) { bits += 1; } diff --git a/java/src/main/java/io/github/scriptarts/springnbt/world/VersionMismatchAction.java b/java/src/main/java/io/github/scriptarts/springnbt/world/VersionMismatchAction.java index a8896ef..801056e 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/world/VersionMismatchAction.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/world/VersionMismatchAction.java @@ -1,6 +1,6 @@ package io.github.scriptarts.springnbt.world; -/** DataVersion が対象と違ったときの動作 */ +/** DataVersionが扱える形式より古いときの動作 */ public enum VersionMismatchAction { /** @@ -9,7 +9,7 @@ public enum VersionMismatchAction { */ WARN, - /** {@link io.github.scriptarts.springnbt.ErrorCode#UNSUPPORTED_DATA_VERSION} の例外にする */ + /** {@link io.github.scriptarts.springnbt.ErrorCode#UNSUPPORTED_DATA_VERSION}の例外にする */ ERROR, /** 何もしない */ diff --git a/java/src/main/java/io/github/scriptarts/springnbt/world/WorldOpenOptions.java b/java/src/main/java/io/github/scriptarts/springnbt/world/WorldOpenOptions.java index 34d612f..89328cd 100644 --- a/java/src/main/java/io/github/scriptarts/springnbt/world/WorldOpenOptions.java +++ b/java/src/main/java/io/github/scriptarts/springnbt/world/WorldOpenOptions.java @@ -21,7 +21,7 @@ public static WorldOpenOptions defaults() { * 読み書きで開くか * 既定は読み取り専用 * - * @return 読み書きなら true + * @return 読み書きならtrue */ public boolean writable() { return writable; @@ -30,7 +30,7 @@ public boolean writable() { /** * 読み書きで開くかを設定する * - * @param value 読み書きなら true + * @param value 読み書きならtrue * @return このオブジェクト */ public WorldOpenOptions setWritable(boolean value) { @@ -39,22 +39,22 @@ public WorldOpenOptions setWritable(boolean value) { } /** - * {@code session.lock} の確認を飛ばすか + * {@code session.lock}の確認を飛ばすか * - *

Minecraft が起動中のワールドへ書き込むとデータが壊れる + *

Minecraftが起動中のワールドへ書き込むとデータがおかしくなる * 既定では書き込みモードで開くときに必ず確認する * これを立てるのは自己責任 * - * @return 飛ばすなら true + * @return 飛ばすならtrue */ public boolean ignoreSessionLock() { return ignoreSessionLock; } /** - * {@code session.lock} の確認を飛ばすかを設定する + * {@code session.lock}の確認を飛ばすかを設定する * - * @param value 飛ばすなら true + * @param value 飛ばすならtrue * @return このオブジェクト */ public WorldOpenOptions setIgnoreSessionLock(boolean value) { diff --git a/java/src/test/java/io/github/scriptarts/springnbt/anvil/RegionFileTest.java b/java/src/test/java/io/github/scriptarts/springnbt/anvil/RegionFileTest.java index 87bc545..ab89e29 100644 --- a/java/src/test/java/io/github/scriptarts/springnbt/anvil/RegionFileTest.java +++ b/java/src/test/java/io/github/scriptarts/springnbt/anvil/RegionFileTest.java @@ -27,24 +27,22 @@ import org.junit.jupiter.params.provider.ValueSource; /** - * Anvil リージョンファイルの読み書き。 + * Anvilリージョンファイルの読み書き * - *

仕様: {@code docs/spec/20-anvil-region.md} - * - *

他言語版と同じ検証項目を持つ。 - * 共通テストベクタによる言語間比較は {@code spec/run-conformance.sh} が担当し、 - * ここでは API の振る舞いを直接確かめる。 + *

他言語版と同じ検証項目を持つ + * 共通テストベクタによる言語間比較は{@code spec/run-conformance.sh}が担当し、 + * ここではAPIの振る舞いを直接確かめる */ class RegionFileTest { @TempDir Path work; - /** 共通テストベクタのディレクトリ。 */ + /** 共通テストベクタのディレクトリ */ private static Path vectorDir(String name) { Path current = Path.of("").toAbsolutePath(); - // 実行ディレクトリからリポジトリ直下まで遡って spec/testdata を探す + // 実行ディレクトリからリポジトリ直下まで遡ってspec/testdataを探す while (current != null) { Path candidate = current.resolve("spec").resolve("testdata").resolve("anvil").resolve(name); @@ -58,7 +56,7 @@ private static Path vectorDir(String name) { throw new IllegalStateException("テストベクタが見つからない: anvil/" + name); } - /** ベクタを一時ディレクトリへ複製し、書き込みテストで原本を汚さないようにする。 */ + /** ベクタを一時ディレクトリへ複製し、書き込みテストで原本を汚さないようにする */ private Path copyVector(String name) throws IOException { Path source = vectorDir(name); Path destination = work.resolve(name); @@ -83,9 +81,31 @@ private static NbtCompound sampleChunk(int x, int z) { return chunk; } + @Test + void 読み書きのフォルダは書き出すときにだけ無いディレクトリを作る() { + Path missing = work.resolve("missing").resolve("region"); + + // 読むだけなら、無いディレクトリは作らない + try (RegionFolder folder = RegionFolder.open(missing, RegionFileMode.READ_WRITE)) { + assertNull(folder.readChunk(0, 0)); + } + + assertFalse(Files.exists(missing)); + + // 書き出すときに作る + try (RegionFolder folder = RegionFolder.open(missing, RegionFileMode.READ_WRITE)) { + folder.writeChunk(0, 0, sampleChunk(0, 0)); + } + + try (RegionFolder reopened = RegionFolder.open(missing)) { + assertEquals(0, reopened.readChunk(0, 0).getInt("xPos")); + } + } + @Test void キャッシュ上限を超えると古いリージョンから閉じる() { - // 上限 2 で 4 リージョンへ書く。古いものは閉じられるが内容は失われない + // 上限2で4リージョンへ書く + // 古いものは閉じられるが、内容は失われない try (RegionFolder folder = RegionFolder.open(work, RegionFileMode.READ_WRITE, 2)) { for (int region = 0; region < 4; region++) { folder.writeChunk(region * 32, 0, sampleChunk(region * 32, 0)); @@ -112,12 +132,16 @@ private static NbtCompound sampleChunk(int x, int z) { assertEquals(ErrorCode.INVALID_ARGUMENT, error.code()); } - /** 圧縮しても縮まないバイト列を作る。サイズの制御が効くようにするため。 */ + /** + * 圧縮しても縮まないバイト列を作る + * サイズを狙いどおりに制御できるようにするため + */ private static byte[] incompressible(int length) { byte[] result = new byte[length]; int state = 0x12345678; - // 線形合同法で疑似乱数を作る。テストの再現性を保つため固定の種を使う + // 線形合同法で疑似乱数を作る + // テストの再現性を保つため、固定の種を使う for (int index = 0; index < length; index++) { state = (state * 1664525) + 1013904223; result[index] = (byte) (state >>> 24); @@ -206,7 +230,7 @@ void readsEveryCompressionScheme() { @Test void readsLz4Chunks() { try (RegionFile region = RegionFile.open(vectorDir("lz4").resolve("r.0.0.mca"))) { - // 1 ブロック / 2 ブロック連結 / 無圧縮ブロック / 重なりのあるマッチ + // 1ブロック / 2ブロック連結 / 無圧縮ブロック / 重なりのあるマッチ for (int x = 0; x < 4; x++) { assertEquals(ChunkCompression.LZ4, region.readChunkRaw(x, 0).compression()); assertEquals(x, region.readChunk(x, 0).getInt("xPos")); @@ -227,12 +251,26 @@ void rejectsLz4BlockWithBrokenMagic() { } } + @Test + void truncatedGzipChunkIsMalformedData() throws IOException { + Path path = copyVector("lz4").resolve("r.0.0.mca"); + + try (RegionFile region = RegionFile.open(path, RegionFileMode.READ_WRITE)) { + // GZipのヘッダだけで本体が無い、途中で切れたチャンク + byte[] truncated = { 0x1F, (byte) 0x8B, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xFF }; + region.writeChunkRaw(0, 0, new RawChunk(ChunkCompression.GZIP, truncated)); + + SpringNbtException error = assertThrows(SpringNbtException.class, () -> region.readChunk(0, 0)); + assertEquals(ErrorCode.MALFORMED_DATA, error.code()); + } + } + @Test void writingLz4IsRejected() throws IOException { Path path = copyVector("lz4").resolve("r.0.0.mca"); try (RegionFile region = RegionFile.open(path, RegionFileMode.READ_WRITE)) { - // LZ4 は読み込みのみ対応なので、圧縮して書き出すことはできない + // LZ4は読み込みのみ対応なので、圧縮して書き出すことはできない NbtCompound chunk = region.readChunk(0, 0); SpringNbtException error = assertThrows( SpringNbtException.class, @@ -247,7 +285,8 @@ void untouchedLz4ChunksKeepTheirCompression() throws IOException { byte[] before = Files.readAllBytes(path); try (RegionFile region = RegionFile.open(path, RegionFileMode.READ_WRITE)) { - // 触らずに閉じるだけ。生バイトを素通しするので LZ4 のまま残る + // 触らずに閉じるだけ + // 生バイトに手を加えないので、LZ4のまま残る assertNotNull(region); } @@ -278,7 +317,7 @@ void brokenHeadersAreRejected(String vector) { @Test void chunkOutsideTheRegionIsRejected() { try (RegionFile region = RegionFile.open(vectorDir("empty").resolve("r.0.0.mca"))) { - // r.0.0 が担当するのは 0..31 の範囲だけ + // r.0.0が担当するのは0..31の範囲だけ assertEquals(ErrorCode.INVALID_ARGUMENT, codeOf(() -> region.hasChunk(32, 0))); } } @@ -295,7 +334,7 @@ void readOnlyRegionRejectsWrites() { @Test void openingAndFlushingWithoutChangesKeepsBytesIdentical() throws IOException { - // 触っていないチャンクの配置を保つことが、既存ワールドを壊さない前提になる + // 触っていないチャンクの配置を保つことが、既存ワールドをおかしくしないための前提になる Path path = copyVector("fragmented").resolve("r.0.0.mca"); byte[] original = Files.readAllBytes(path); @@ -347,7 +386,7 @@ void rewritingTheSameSizeKeepsTheChunkInPlace() throws IOException { void growingChunkIsRelocatedWithoutBreakingOthers() throws IOException { Path path = copyVector("fragmented").resolve("r.0.0.mca"); - // 5 セクタぶんになる大きなチャンクを作る + // 5セクタぶんになる大きなチャンクを作る NbtCompound big = sampleChunk(0, 0); big.set("filler", new NbtByteArray(incompressible(5 * RegionFile.SECTOR_SIZE))); @@ -357,7 +396,7 @@ void growingChunkIsRelocatedWithoutBreakingOthers() throws IOException { } try (RegionFile reopened = RegionFile.open(path)) { - // 動かした結果、他の 2 チャンクが壊れていないこと + // 動かした結果、他の2チャンクがおかしくなっていないこと assertEquals(3, reopened.chunkPositions().size()); assertEquals(5, reopened.readChunk(5, 3).getInt("xPos")); assertEquals(31, reopened.readChunk(31, 31).getInt("xPos")); @@ -430,7 +469,7 @@ void optimizeCompactsTheFile() throws IOException { void hugeChunkGoesToExternalFileAndComesBack() throws IOException { Path path = work.resolve("r.0.0.mca"); - // 1MiB を超えるよう、圧縮の効かないデータを詰める + // 1MiBを超えるよう、圧縮しても縮まないデータを詰める NbtCompound huge = sampleChunk(1, 2); huge.set("filler", new NbtByteArray(incompressible(1200 * 1024))); @@ -486,7 +525,7 @@ void folderResolvesChunksAcrossRegions() { folder.flush(); } - // 3 つの異なるリージョンへ振り分けられる + // 3つの異なるリージョンへ振り分けられる assertTrue(Files.exists(work.resolve("r.0.0.mca"))); assertTrue(Files.exists(work.resolve("r.-1.-1.mca"))); assertTrue(Files.exists(work.resolve("r.1.1.mca"))); diff --git a/java/src/test/java/io/github/scriptarts/springnbt/conformance/ChunkReport.java b/java/src/test/java/io/github/scriptarts/springnbt/conformance/ChunkReport.java index 6c7d99b..f7f3540 100644 --- a/java/src/test/java/io/github/scriptarts/springnbt/conformance/ChunkReport.java +++ b/java/src/test/java/io/github/scriptarts/springnbt/conformance/ChunkReport.java @@ -8,9 +8,7 @@ import java.util.TreeMap; /** - * チャンクの中身を、言語をまたいで文字列として完全一致する形へ写す。 - * - *

仕様: {@code docs/spec/90-conformance.md} 2.3章 + * チャンクの中身を、言語をまたいで文字列として完全一致する形へ写す */ final class ChunkReport { @@ -19,10 +17,10 @@ private ChunkReport() { } /** - * チャンクの全ブロック・全バイオームを走査して集計する。 + * チャンクの全ブロック・全バイオームを走査して集計する * *

パレットとビットストレージを端から端まで通すので、 - * ビット詰めの実装が 1 か所でもずれれば集計値が変わる。 + * ビット詰めの実装が1か所でもずれれば集計値が変わる */ static String describe(Chunk chunk) { StringBuilder builder = new StringBuilder(); @@ -53,7 +51,7 @@ static String describe(Chunk chunk) { .append(blockPalette).append(' ').append(blockBits).append(' ') .append(biomePalette).append(' ').append(biomeBits).append('\n'); - // 全ブロックを 1 つずつ読んで、状態の文字列表現ごとに数える + // 全ブロックを1つずつ読んで、状態の文字列表現ごとに数える for (int y = 0; y < 16; y++) { for (int z = 0; z < 16; z++) { for (int x = 0; x < 16; x++) { @@ -68,7 +66,7 @@ static String describe(Chunk chunk) { } } - // バイオームは 4×4×4 単位なので、4 ブロックおきに見る + // バイオームは4×4×4単位なので、4ブロックおきに見る for (int y = 0; y < 16; y += 4) { for (int z = 0; z < 16; z += 4) { for (int x = 0; x < 16; x += 4) { @@ -99,9 +97,10 @@ static String describe(Chunk chunk) { } /** - * 決まった手順でチャンクを編集する。全言語で同じ結果になるはず。 + * 決まった手順でチャンクを編集する + * 全言語で同じ結果になるはず * - *

パレット拡張・ビット幅の再計算・未使用要素の掃除を一通り通す。 + *

パレット拡張・ビット幅の再計算・未使用要素の掃除を一通り通す */ static void edit(Chunk chunk) { int baseY = chunk.minSectionY() * 16; @@ -113,7 +112,8 @@ static void edit(Chunk chunk) { chunk.setBlock(index % 16, baseY + (index / 16), index % 16, state); } - // プロパティ付きのブロックを、名前は同じで状態違いで置く + // プロパティ付きのブロックを置く + // 1つ目と2つ目はプロパティの並び順だけが違う同じ状態、3つ目は名前空間を省いて書いた別の状態 chunk.setBlock(1, baseY + 2, 1, BlockState.parse("minecraft:oak_stairs[facing=north,half=top]")); chunk.setBlock(2, baseY + 2, 2, BlockState.parse("minecraft:oak_stairs[half=top,facing=north]")); chunk.setBlock(3, baseY + 2, 3, BlockState.parse("oak_stairs[facing=south]")); @@ -125,7 +125,7 @@ static void edit(Chunk chunk) { // 使われなくなったパレット要素を掃除する chunk.compact(); - // 高さマップと光源は再計算しないので、無効化して Minecraft に任せる + // 高さマップと光源は再計算しないので、無効化してMinecraftに任せる chunk.clearHeightmaps(); chunk.invalidateLighting(); } diff --git a/java/src/test/java/io/github/scriptarts/springnbt/conformance/Conformance.java b/java/src/test/java/io/github/scriptarts/springnbt/conformance/Conformance.java index 337bb3e..0d20a83 100644 --- a/java/src/test/java/io/github/scriptarts/springnbt/conformance/Conformance.java +++ b/java/src/test/java/io/github/scriptarts/springnbt/conformance/Conformance.java @@ -22,12 +22,11 @@ import java.util.List; /** - * 適合性検証ツール。4言語すべてが同じインターフェースで同じ出力を出す。 + * 適合性検証ツール + * 全言語が同じインターフェースで同じ出力を出す * - *

{@code spec/run-conformance.sh} がこのツールを4言語ぶん起動し、 - * 出力を相互に diff することで「4言語が同一に振る舞う」ことを機械的に確かめる。 - * - *

仕様: {@code docs/spec/90-conformance.md} 2.3章 + *

{@code spec/run-conformance.sh}がこのツールを全言語ぶん起動し、 + * 出力を相互にdiffすることで「全言語が同一に振る舞う」ことを機械的に確かめる */ public final class Conformance { @@ -36,7 +35,7 @@ private Conformance() { } /** - * エントリポイント。 + * エントリポイント * * @param args コマンドライン引数 */ @@ -70,7 +69,7 @@ private static int run(String[] args) { } }; } catch (SpringNbtException error) { - // 4言語で同じ ErrorCode を出すことが検証対象なので、コードを機械可読な形で出す + // 言語間で同じErrorCodeを出すことが検証対象なので、コードを機械可読な形で出す System.err.print("ERROR " + error.code().asString() + " " + error.getMessage() + "\n"); return 1; } catch (IOException error) { @@ -79,7 +78,7 @@ private static int run(String[] args) { } } - /** 入力を読み、正規化JSON を書き出す。 */ + /** 入力を読み、正規化JSONを書き出す */ private static int runDecode(String[] args) throws IOException { if (args.length < 3) { System.err.println(usage()); @@ -93,7 +92,10 @@ private static int runDecode(String[] args) throws IOException { return 0; } - /** 入力を読み、無圧縮で書き直す。ラウンドトリップ検証に使う。 */ + /** + * 入力を読み、無圧縮で書き直す + * ラウンドトリップ検証に使う + */ private static int runEncode(String[] args) throws IOException { if (args.length < 3) { System.err.println(usage()); @@ -110,7 +112,7 @@ private static int runEncode(String[] args) throws IOException { return 0; } - /** 入力を読み、1行の SNBT を書き出す。 */ + /** 入力を読み、1行のSNBTを書き出す */ private static int runSnbt(String[] args) throws IOException { if (args.length < 3) { System.err.println(usage()); @@ -124,7 +126,7 @@ private static int runSnbt(String[] args) throws IOException { return 0; } - /** 連なった NBT を、位置を追いながら一覧として書き出す。 */ + /** 連なったNBTを、位置を追いながら一覧として書き出す */ private static int runNbtList(String[] args) throws IOException { if (args.length < 3) { System.err.println(usage()); @@ -158,7 +160,7 @@ private static int runNbtList(String[] args) throws IOException { return 0; } - /** リージョンの中身を一覧として書き出す。 */ + /** リージョンの中身を一覧として書き出す */ private static int runRegionList(String[] args) throws IOException { if (args.length < 3) { System.err.println(usage()); @@ -172,7 +174,7 @@ private static int runRegionList(String[] args) throws IOException { return 0; } - /** リージョンを読み直し、無圧縮で詰め直して書き出す。 */ + /** リージョンを読み直し、無圧縮で詰め直して書き出す */ private static int runRegionRewrite(String[] args) throws IOException { if (args.length < 3) { System.err.println(usage()); @@ -186,7 +188,7 @@ private static int runRegionRewrite(String[] args) throws IOException { return 0; } - /** チャンクの全ブロック・全バイオームを走査して集計を書き出す。 */ + /** チャンクの全ブロック・全バイオームを走査して集計を書き出す */ private static int runChunkReport(String[] args) throws IOException { if (args.length < 3) { System.err.println(usage()); @@ -197,7 +199,7 @@ private static int runChunkReport(String[] args) throws IOException { return 0; } - /** 決まった手順でチャンクを編集し、無圧縮で書き出す。 */ + /** 決まった手順でチャンクを編集し、無圧縮で書き出す */ private static int runChunkEdit(String[] args) throws IOException { if (args.length < 3) { System.err.println(usage()); @@ -213,19 +215,19 @@ private static int runChunkEdit(String[] args) throws IOException { return 0; } - /** チャンク NBT のファイルを読む。 */ + /** チャンクNBTのファイルを読む */ private static Chunk readChunkFile(String path) { NamedTag named = NbtIo.readFile(Path.of(path), null); - // 検証では DataVersion の違いを警告にせず、そのまま読む + // 検証では、DataVersionが扱える形式より古くても警告にせずそのまま読む ChunkReadOptions options = ChunkReadOptions.defaults() .setOnVersionMismatch(VersionMismatchAction.IGNORE); return Chunk.fromNbt(named.tag(), options); } - /** {@code --format network} が指定されていればネットワーク形式として読む。 */ + /** {@code --format network}が指定されていればネットワーク形式として読む */ private static NbtFormat parseFormat(String[] args) { - // 3 番目以降の引数からオプションを探す + // 3番目以降の引数からオプションを探す for (int index = 3; index < args.length - 1; index++) { if (args[index].equals("--format") && args[index + 1].equals("network")) { return NbtFormat.NETWORK; @@ -236,20 +238,20 @@ private static NbtFormat parseFormat(String[] args) { } /** - * 改行を変換せず、BOM も付けずに UTF-8 で書く。 + * 改行を変換せず、BOMも付けずにUTF-8で書く * - *

孤立サロゲートを含みうるが、標準の UTF-8 エンコーダは置換文字にしてしまうため、 - * 自前で符号化する。 + *

孤立サロゲートを含みうるが、標準のUTF-8エンコーダは置換文字にしてしまうため、 + * 自前で符号化する */ private static void writeTextFile(String path, String content) throws IOException { Files.write(Path.of(path), encodeUtf8KeepingSurrogates(content)); } - /** 孤立サロゲートを WTF-8(3バイト形式)として保持したまま UTF-8 へ符号化する。 */ + /** 孤立サロゲートをWTF-8(3バイト形式)として保持したままUTF-8へ符号化する */ private static byte[] encodeUtf8KeepingSurrogates(String text) { java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); - // コード単位を順に見て、正しいサロゲートペアだけ 1 文字として符号化する + // コード単位を順に見て、正しいサロゲートペアだけ1文字として符号化する for (int index = 0; index < text.length(); index++) { char unit = text.charAt(index); @@ -261,7 +263,7 @@ private static byte[] encodeUtf8KeepingSurrogates(String text) { .getBytes(StandardCharsets.UTF_8)); index += 1; } else if (Character.isSurrogate(unit)) { - // 孤立サロゲートは 3 バイト形式でそのまま書く + // 孤立サロゲートは3バイト形式でそのまま書く out.write(0xE0 | ((unit >> 12) & 0x0F)); out.write(0x80 | ((unit >> 6) & 0x3F)); out.write(0x80 | (unit & 0x3F)); diff --git a/java/src/test/java/io/github/scriptarts/springnbt/conformance/NormalizedJson.java b/java/src/test/java/io/github/scriptarts/springnbt/conformance/NormalizedJson.java index 41d981a..6cc7fec 100644 --- a/java/src/test/java/io/github/scriptarts/springnbt/conformance/NormalizedJson.java +++ b/java/src/test/java/io/github/scriptarts/springnbt/conformance/NormalizedJson.java @@ -19,13 +19,11 @@ import java.util.Map; /** - * NBT を、言語をまたいで文字列として完全一致する JSON へ写す。 + * NBTを、言語をまたいで文字列として完全一致するJSONへ写す * - *

浮動小数点をビットパターンで、64bit 整数を10進文字列で表すのが要。 - * 10進表記の丸めや JSON 数値の精度は処理系ごとに差が出るため、 - * そのまま出すと4言語の出力が一致しない。 - * - *

仕様: {@code docs/spec/00-conventions.md} 6章 / {@code docs/spec/90-conformance.md} + *

浮動小数点をビットパターンで、64bit整数を10進文字列で表すのが要 + * 10進表記の丸めやJSON数値の精度は処理系ごとに差が出るため、 + * そのまま出すと言語間で出力が一致しない */ final class NormalizedJson { @@ -33,7 +31,10 @@ private NormalizedJson() { // ユーティリティクラス } - /** ルートを含む全体を JSON 文字列へ変換する。末尾に改行を1つ付ける。 */ + /** + * ルートを含む全体をJSON文字列へ変換する + * 末尾に改行を1つ付ける + */ static String write(NamedTag named, NbtFormat format) { StringBuilder builder = new StringBuilder(); builder.append("{\"format\":"); @@ -58,7 +59,7 @@ private static void appendTag(StringBuilder builder, NbtTag tag) { builder.append("{\"type\":"); appendString(builder, tag.type().asString()); - // list だけは value の前に element_type が入る(仕様が定めるキー順) + // listだけはvalueの前にelement_typeが入る(仕様が定めるキー順) if (tag instanceof NbtList listTag) { builder.append(",\"element_type\":"); appendString(builder, listTag.elementType().asString()); @@ -70,7 +71,7 @@ private static void appendTag(StringBuilder builder, NbtTag tag) { case NbtByte value -> builder.append(value.value()); case NbtShort value -> builder.append(value.value()); case NbtInt value -> builder.append(value.value()); - // 64bit 整数は JSON 数値だと処理系によって精度が落ちるため10進文字列で表す + // 64bit整数はJSON数値だと処理系によって精度が落ちるため10進文字列で表す case NbtLong value -> appendString(builder, Long.toString(value.value())); case NbtFloat value -> appendString(builder, String.format("0x%08x", Float.floatToRawIntBits(value.value()))); @@ -79,7 +80,8 @@ private static void appendTag(StringBuilder builder, NbtTag tag) { case NbtString value -> { appendString(builder, value.value()); - // MUTF-8 のバイト列も併記する。孤立サロゲートなど UTF-8 に写せない値を厳密に比較するため + // MUTF-8のバイト列も併記する + // 孤立サロゲートなどUTF-8に写せない値を厳密に比較するため builder.append(",\"mutf8\":"); appendString(builder, toHex(Mutf8.encode(value.value()))); } @@ -111,7 +113,7 @@ private static void appendList(StringBuilder builder, NbtList list) { } private static void appendCompound(StringBuilder builder, NbtCompound compound) { - // JSON オブジェクトだと挿入順の保持が処理系依存になるため、組の配列で表す + // JSONオブジェクトだと挿入順の保持が処理系依存になるため、組の配列で表す builder.append('['); boolean first = true; @@ -162,7 +164,7 @@ private static void appendIntArray(StringBuilder builder, int[] values) { private static void appendLongArray(StringBuilder builder, long[] values) { builder.append('['); - // 64bit 整数は10進文字列の配列で表す + // 64bit整数は10進文字列の配列で表す for (int index = 0; index < values.length; index++) { if (index > 0) { builder.append(','); @@ -185,9 +187,10 @@ private static String toHex(byte[] bytes) { } /** - * JSON 文字列を書き出す。非 ASCII は必ず {@code \\uXXXX} へ逃がす。 + * JSON文字列を書き出す + * 非ASCIIは必ず{@code \\uXXXX}へエスケープする * - *

言語ごとに既定のエスケープ方針が違うため、ここで一律に固定しないと出力が一致しない。 + *

言語ごとに既定のエスケープ方針が違うため、ここで一律に固定しないと出力が一致しない */ private static void appendString(StringBuilder builder, String text) { builder.append('"'); @@ -204,7 +207,7 @@ private static void appendString(StringBuilder builder, String text) { case '\r' -> builder.append("\\r"); case '\t' -> builder.append("\\t"); default -> { - // ASCII の印字可能文字だけ生で出し、それ以外は \\uXXXX にする + // ASCIIの印字可能文字だけ生で出し、それ以外は\\uXXXXにする if (c >= 0x20 && c <= 0x7E) { builder.append(c); } else { diff --git a/java/src/test/java/io/github/scriptarts/springnbt/conformance/RegionReport.java b/java/src/test/java/io/github/scriptarts/springnbt/conformance/RegionReport.java index 038bfff..83dea48 100644 --- a/java/src/test/java/io/github/scriptarts/springnbt/conformance/RegionReport.java +++ b/java/src/test/java/io/github/scriptarts/springnbt/conformance/RegionReport.java @@ -15,9 +15,7 @@ import java.nio.file.Path; /** - * リージョンファイルの中身を、言語をまたいで文字列として完全一致する形へ写す。 - * - *

仕様: {@code docs/spec/90-conformance.md} 2.3章 + * リージョンファイルの中身を、言語をまたいで文字列として完全一致する形へ写す */ final class RegionReport { @@ -26,9 +24,10 @@ private RegionReport() { } /** - * 存在するチャンクを 1 行 1 チャンクで書き出す。並びはロケーションテーブルの添字順。 + * 存在するチャンクを1行1チャンクで書き出す + * 並びはロケーションテーブルの添字順 * - *

各行は「絶対X 絶対Z タイムスタンプ 圧縮方式 圧縮後バイト数 展開後バイト数 キー数」。 + *

各行は「絶対X 絶対Z タイムスタンプ 圧縮方式 圧縮後バイト数 展開後バイト数 ルート直下キー数」 */ static String list(RegionFile region) { StringBuilder builder = new StringBuilder(); @@ -70,10 +69,10 @@ static String list(RegionFile region) { } /** - * 全チャンクを読み直し、無圧縮で新しいリージョンへ詰め直して書き出す。 + * 全チャンクを読み直し、無圧縮で新しいリージョンへ詰め直して書き出す * - *

無圧縮にするのは、zlib の出力が処理系ごとに違い、 - * 圧縮したままでは言語間でバイトが一致しないため。 + *

無圧縮にするのは、zlibの出力が処理系ごとに違い、 + * 圧縮したままでは言語間でバイトが一致しないため */ static void rewrite(RegionFile source, Path outputPath) throws IOException { // 途中結果が残らないよう、書き出し先は必ず作り直す diff --git a/java/src/test/java/io/github/scriptarts/springnbt/nbt/NbtTest.java b/java/src/test/java/io/github/scriptarts/springnbt/nbt/NbtTest.java index 49090be..24b07bd 100644 --- a/java/src/test/java/io/github/scriptarts/springnbt/nbt/NbtTest.java +++ b/java/src/test/java/io/github/scriptarts/springnbt/nbt/NbtTest.java @@ -21,11 +21,11 @@ import org.junit.jupiter.params.provider.EnumSource; /** - * NBT レイヤの単体テスト。 + * NBTレイヤの単体テスト * - *

他言語版と同じ検証項目を持つ。 - * 共通テストベクタによる言語間比較は {@code spec/run-conformance.sh} が担当し、 - * ここでは API の振る舞いを直接確かめる。 + *

他言語版と同じ検証項目を持つ + * 共通テストベクタによる言語間比較は{@code spec/run-conformance.sh}が担当し、 + * ここではAPIの振る舞いを直接確かめる */ class NbtTest { @@ -37,7 +37,7 @@ private static NbtWriteOptions uncompressedWrite() { return NbtWriteOptions.uncompressed(); } - /** 仕様書どおりに組んだ最小の NBT。 */ + /** 仕様書どおりに組んだ最小のNBT */ private static byte[] helloWorldBytes() { ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -89,7 +89,7 @@ void nulIsTwoBytes() { @Test void supplementaryCharIsCesu8() { - // U+1F600 は UTF-16 で D83D DE00。MUTF-8 では 3 バイト × 2 になる + // U+1F600はUTF-16でD83D DE00、MUTF-8では3バイト × 2になる String sample = "\uD83D\uDE00"; byte[] expected = { (byte) 0xED, (byte) 0xA0, (byte) 0xBD, @@ -101,7 +101,7 @@ void supplementaryCharIsCesu8() { @Test void loneSurrogateSurvivesRoundtrip() { - // 対にならない上位サロゲート。UTF-8 には写せないが MUTF-8 では往復できる + // 対にならない上位サロゲートはUTF-8には写せないが、MUTF-8では往復できる String lone = "\uD83D"; byte[] expected = { (byte) 0xED, (byte) 0xA0, (byte) 0xBD }; assertArrayEquals(expected, Mutf8.encode(lone)); @@ -116,7 +116,7 @@ void invalidInputIsRejected() { new byte[] { (byte) 0xF0, (byte) 0x9F, (byte) 0x98, (byte) 0x80 }, new byte[] { (byte) 0xE3, (byte) 0x81 }); - // 素の 0x00 / 冗長符号化 / 4バイト形式 / 途中で切れた入力 のすべてを拒否する + // 素の0x00、冗長符号化、4バイト形式、途中で切れた入力のすべてを拒否する for (byte[] data : cases) { assertEquals(ErrorCode.MALFORMED_DATA, codeOf(() -> Mutf8.decode(data)), Arrays.toString(data)); @@ -143,6 +143,17 @@ void readsHandBuiltHelloWorld() { assertEquals("Bananrama", named.tag().getString("name")); } + @Test + void nanBitPatternsAreWrittenBackUnchanged() { + // ペイロード付き・負・シグナリングのNaNを、Float / Doubleの両方で持つCompound + byte[] original = { + (byte) 0x0A, (byte) 0x00, (byte) 0x00, (byte) 0x05, (byte) 0x00, (byte) 0x01, (byte) 0x61, (byte) 0x7F, (byte) 0xC0, (byte) 0x00, (byte) 0x01, (byte) 0x05, (byte) 0x00, (byte) 0x01, (byte) 0x62, (byte) 0xFF, (byte) 0xC0, (byte) 0x00, (byte) 0x00, (byte) 0x05, (byte) 0x00, (byte) 0x01, (byte) 0x63, (byte) 0x7F, (byte) 0x80, (byte) 0x00, (byte) 0x01, (byte) 0x06, (byte) 0x00, (byte) 0x01, (byte) 0x64, (byte) 0x7F, (byte) 0xF8, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x06, (byte) 0x00, (byte) 0x01, (byte) 0x65, (byte) 0xFF, (byte) 0xF8, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x01, (byte) 0x66, (byte) 0x7F, (byte) 0xF0, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, + }; + NamedTag named = NbtIo.readBytes(original, uncompressedRead()); + + assertArrayEquals(original, NbtIo.writeBytes(named, uncompressedWrite())); + } + @Test void writesBackTheSameBytes() { byte[] original = helloWorldBytes(); @@ -171,7 +182,7 @@ void floatSpecialsKeepTheirBitPattern() { byte[] encoded = NbtIo.writeBytes(new NamedTag("", root), uncompressedWrite()); NbtCompound decoded = NbtIo.readBytes(encoded, uncompressedRead()).tag(); - // -0.0 と +0.0 は == では区別できないので、ビットパターンで比較する + // -0.0と+0.0は == では区別できないので、ビットパターンで比較する assertEquals(Double.doubleToRawLongBits(-0.0), Double.doubleToRawLongBits(decoded.getDouble("negative_zero"))); assertTrue(Float.isNaN(decoded.getFloat("nan"))); @@ -216,12 +227,23 @@ void listRejectsMixedTypes() { codeOf(() -> list.add(new NbtString("x")))); } + @Test + void listRejectsOutOfRangePositionWithoutFixingElementType() { + NbtList list = new NbtList(); + + assertThrows(IndexOutOfBoundsException.class, () -> list.set(0, new NbtInt(1))); + assertThrows(IndexOutOfBoundsException.class, () -> list.insert(1, new NbtInt(1))); + + // 失敗した操作で要素型が確定していない + assertEquals(TagType.END, list.elementType()); + } + @Test void typedGetterDistinguishesMissingKeyFromWrongType() { NbtCompound root = new NbtCompound(); root.set("value", new NbtString("text")); - // キーが無い場合は null + // キーが無い場合はnull assertNull(root.optInt("missing")); // 型が違う場合はキーの有無に関わらず例外 @@ -229,6 +251,26 @@ void typedGetterDistinguishesMissingKeyFromWrongType() { assertEquals(ErrorCode.INVALID_ARGUMENT, codeOf(() -> root.getInt("missing"))); } + @Test + void brokenCompressedDataIsMalformedData() { + NamedTag named = NbtIo.readBytes(helloWorldBytes(), uncompressedRead()); + byte[] gzip = NbtIo.writeBytes(named, + NbtWriteOptions.defaults().setCompression(Compression.GZIP)); + byte[] truncated = java.util.Arrays.copyOf(gzip, gzip.length / 2); + byte[] brokenGzip = { + (byte) 0x1F, (byte) 0x8B, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, + }; + byte[] brokenZlib = { + 0x78, (byte) 0x9C, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, + }; + + // 途中で切れたGZip、中身の壊れたGZip、中身の壊れたZlibは、どれも仕様に反するデータとして扱う + assertEquals(ErrorCode.MALFORMED_DATA, codeOf(() -> NbtIo.readBytes(truncated, null))); + assertEquals(ErrorCode.MALFORMED_DATA, codeOf(() -> NbtIo.readBytes(brokenGzip, null))); + assertEquals(ErrorCode.MALFORMED_DATA, codeOf(() -> NbtIo.readBytes(brokenZlib, null))); + } + @ParameterizedTest @EnumSource(value = Compression.class, names = { "GZIP", "ZLIB", "NONE" }) void compressionIsDetectedAutomatically(Compression method) { @@ -238,7 +280,7 @@ void compressionIsDetectedAutomatically(Compression method) { assertEquals(method, NbtIo.detectCompression(encoded)); - // 既定の ReadOptions は AUTO なので、方式を指定しなくても読める + // 既定のReadOptionsはAUTOなので、方式を指定しなくても読める assertEquals("Bananrama", NbtIo.readBytes(encoded, null).tag().getString("name")); } @@ -252,7 +294,7 @@ void networkFormatHasNoRootName() { .setFormat(NbtFormat.NETWORK) .setCompression(Compression.NONE)); - // タグID + ペイロード のみで、名前長の 2 バイトが無い + // タグID + ペイロードのみで、名前長の2バイトが無い assertEquals(0x0A, encoded[0]); assertEquals(0x03, encoded[1]); @@ -276,7 +318,7 @@ void truncatedInputIsRejected() { @Test void hugeDeclaredLengthIsRejectedBeforeAllocating() { - // ルート直下に「長さ 0x7FFFFFFF の ByteArray」を宣言するだけの入力 + // ルート直下に「長さ0x7FFFFFFFのByteArray」を宣言するだけの入力 byte[] data = { 0x0A, 0x00, 0x00, 0x07, 0x00, 0x01, 0x61, @@ -338,7 +380,7 @@ void listKeepsElementTypeAfterClear() { @Test void loneSurrogateKeyIsRejected() { - // 値と違い、キーには孤立サロゲートを許さない(仕様 10 の 2.2章) + // 値と違い、キーには孤立サロゲートを許さない byte[] data = { 0x0A, 0x00, 0x00, 0x03, 0x00, 0x03, (byte) 0xED, (byte) 0xA0, (byte) 0xBD, @@ -374,7 +416,8 @@ void suffixDecidesType(String source, TagType expected) { @Test void hexSuffixRuleIsFixed() { - // 仕様 11 の 2.1: 16進では b/d/f を数字として読む。幅接尾辞は s/l のみ + // 16進ではb/d/fを数字として読む + // 幅接尾辞はs/lのみ assertEquals(new NbtInt(255), Snbt.parse("0xFF")); assertEquals(new NbtInt(4091), Snbt.parse("0xFFb")); assertEquals(new NbtLong(255), Snbt.parse("0xFFl")); @@ -383,7 +426,8 @@ void hexSuffixRuleIsFixed() { @Test void zeroByteLiteralIsNotBinary() { - // 0b は「10進の 0 に Byte 接尾辞」。真偽値の false として広く使われる形 + // 0bは「10進の0にByte接尾辞」 + // 真偽値のfalseとして広く使われる形 assertEquals(new NbtByte((byte) 0), Snbt.parse("0b")); assertEquals(new NbtInt(1), Snbt.parse("0b1")); assertEquals(new NbtByte((byte) 9), Snbt.parse("0b1001b")); @@ -417,7 +461,7 @@ void typedArrays() { assertEquals(new NbtIntArray(new int[] { 1, 2 }), Snbt.parse("[I; 1, 2]")); assertEquals(new NbtLongArray(new long[] { 1, 2 }), Snbt.parse("[L; 1L, 2L]")); - // 接尾辞なしでも範囲内なら受理する(Minecraft 自身がそう書き出すため) + // 接尾辞なしでも範囲内なら受理する(Minecraft自身がそう書き出すため) assertEquals(new NbtByteArray(new byte[] { 1, 2 }), Snbt.parse("[B; 1, 2]")); assertEquals(ErrorCode.MALFORMED_DATA, codeOf(() -> Snbt.parse("[B; 200]"))); } @@ -430,7 +474,7 @@ void trailingCommasAreAllowed() { @Test void heterogeneousListIsRejected() { - // 異種リストはバイナリ NBT へ写せないため受理しない (adr/0006) + // 異種リストはバイナリNBTへ写せないため受理しない assertEquals(ErrorCode.MALFORMED_DATA, codeOf(() -> Snbt.parse("[1, \"a\"]"))); } @@ -445,6 +489,28 @@ void escapeSequences() { codeOf(() -> Snbt.parse("\"\\N{SNOWMAN}\""))); } + @Test + void surrogateEscapesFollowTheSameRulesEverywhere() { + String lone = String.valueOf((char) 0xD800); + + // 8桁のUエスケープでサロゲートの範囲を書くと、4桁のuエスケープと同じく孤立サロゲートになる + assertEquals(new NbtString(lone), Snbt.parse("\"\\U0000D800\"")); + assertEquals(new NbtString(lone), Snbt.parse("\"\\uD800\"")); + + // 対になったサロゲートは補助文字1文字と同じ + assertEquals(new NbtString(new String(Character.toChars(0x1F600))), + Snbt.parse("\"\\uD83D\\uDE00\"")); + + // コードポイントの範囲外と、キーの孤立サロゲートは仕様に反する + assertEquals(ErrorCode.MALFORMED_DATA, codeOf(() -> Snbt.parse("\"\\U00110000\""))); + assertEquals(ErrorCode.MALFORMED_DATA, codeOf(() -> Snbt.parse("{\"\\uD800\":1}"))); + assertEquals(ErrorCode.MALFORMED_DATA, codeOf(() -> Snbt.parse("{\"\\U0000D800\":1}"))); + + // APIからも孤立サロゲートのキーは設定できない + NbtCompound compound = new NbtCompound(); + assertEquals(ErrorCode.INVALID_ARGUMENT, codeOf(() -> compound.set(lone, new NbtInt(1)))); + } + @Test void singleQuotedStringsWork() { assertEquals(new NbtString("say \"hi\""), Snbt.parse("'say \"hi\"'")); @@ -461,7 +527,7 @@ void functions() { @Test void snbtToNbtToSnbtToNbtIsStable() { - // 仕様 11 の 5章: 保証するのは「SNBT -> NBT -> SNBT -> NBT」で NBT が一致すること + // 保証するのは「SNBT -> NBT -> SNBT -> NBT」でNBTが一致すること String source = "{ name : 'Bananrama' , list : [ 1L , 2L ] , nested : { flag : true } , " + "bytes : [B; 1b, -2b] , ratio : 0.5f }"; @@ -548,6 +614,13 @@ void specialValues() { assertEquals("NaNf", Snbt.write(new NbtFloat(Float.NaN))); } + @Test + void floatOutOfRangeBecomesSignedInfinity() { + // binary32の範囲を超える値は、符号付きの無限大になる + assertEquals(new NbtFloat(Float.POSITIVE_INFINITY), Snbt.parse("1e39f")); + assertEquals(new NbtFloat(Float.NEGATIVE_INFINITY), Snbt.parse("-1e39f")); + } + @Test void everyFormattedValueParsesBackToTheSameBits() { double[] doubles = { @@ -575,7 +648,10 @@ void everyFormattedValueParsesBackToTheSameBits() { } } - /** 全13タグ型を含む Compound を作る。 */ + /** + * TAG_Endを除く12種のタグ型をすべて含むCompoundを作る + * TAG_EndはCompoundの終端として書き出されるので、往復させれば13種すべてを読み書きする + */ private static NbtCompound buildAllTags() { NbtCompound root = new NbtCompound(); root.set("byte", new NbtByte((byte) -128)); @@ -603,14 +679,14 @@ private static NbtCompound buildAllTags() { return root; } - /** 指定した深さまで Compound を入れ子にしたバイト列を作る。 */ + /** 指定した深さまでCompoundを入れ子にしたバイト列を作る */ private static byte[] buildNestedCompound(int depth) { ByteArrayOutputStream out = new ByteArrayOutputStream(); out.write(0x0A); out.write(0x00); out.write(0x00); - // ルート + (depth - 1) 段の入れ子 + // ルート + (depth - 1)段の入れ子 for (int index = 0; index < depth - 1; index++) { out.write(0x0A); out.write(0x00); @@ -734,7 +810,7 @@ void mirrorTheGetters() { assertArrayEquals(new int[] {1, -1}, root.getIntArray("ia")); assertArrayEquals(new long[] {1L, -1L}, root.getLongArray("la")); - // 真偽値は TAG_Byte の 0 / 1 として入る + // 真偽値はTAG_Byteの0 / 1として入る assertEquals(TagType.BYTE, root.get("t").type()); } diff --git a/java/src/test/java/io/github/scriptarts/springnbt/nbt/TagEqualityTest.java b/java/src/test/java/io/github/scriptarts/springnbt/nbt/TagEqualityTest.java index c9af2ff..de115f9 100644 --- a/java/src/test/java/io/github/scriptarts/springnbt/nbt/TagEqualityTest.java +++ b/java/src/test/java/io/github/scriptarts/springnbt/nbt/TagEqualityTest.java @@ -10,10 +10,8 @@ /** * タグの等値比較と深い複製 * - *

仕様: {@code docs/spec/10-nbt-binary.md} 7.3 - * *

規則は全言語で同じでなければならない - * 各言語の同名テストと突き合わせて読むこと + * 各言語の同名テストと見比べながら読むこと */ class TagEqualityTest { @@ -31,7 +29,7 @@ void sameTypeSameValueIsEqual() { @Test @DisplayName("型が違えば等しくない") void differentTagTypeIsNotEqual() { - // 値が同じでもタグの型が違えば別物 + // 値が同じでもタグの型が違えば等しくない assertFalse(new NbtInt(1).equals(new NbtShort((short) 1))); assertFalse(new NbtInt(1).equals(null)); } @@ -39,7 +37,7 @@ void differentTagTypeIsNotEqual() { @Test @DisplayName("浮動小数点はビットパターンで比べる") void floatsCompareByBitPattern() { - // NaN 同士は等しく、+0.0 と -0.0 は等しくない + // NaN同士は等しく、+0.0と-0.0は等しくない assertTrue(new NbtFloat(Float.NaN).equals(new NbtFloat(Float.NaN))); assertTrue(new NbtDouble(Double.NaN).equals(new NbtDouble(Double.NaN))); assertFalse(new NbtFloat(0.0f).equals(new NbtFloat(-0.0f))); @@ -63,7 +61,7 @@ void listComparesElementTypeAndOrder() { reversed.add(new NbtInt(1)); assertFalse(left.equals(reversed)); - // 空でも要素型が違えば別物 + // 空でも要素型が違えば等しくない assertFalse(new NbtList(TagType.INT).equals(new NbtList(TagType.BYTE))); } @@ -79,7 +77,7 @@ void compoundComparesInsertionOrder() { same.set("b", new NbtInt(2)); assertTrue(left.equals(same)); - // 中身は同じでも挿入順が違えば別物 + // 中身は同じでも挿入順が違えば等しくない NbtCompound reordered = new NbtCompound(); reordered.set("b", new NbtInt(2)); reordered.set("a", new NbtInt(1)); diff --git a/java/src/test/java/io/github/scriptarts/springnbt/world/BlockPosTest.java b/java/src/test/java/io/github/scriptarts/springnbt/world/BlockPosTest.java index 107e9c8..1bacda6 100644 --- a/java/src/test/java/io/github/scriptarts/springnbt/world/BlockPosTest.java +++ b/java/src/test/java/io/github/scriptarts/springnbt/world/BlockPosTest.java @@ -10,7 +10,9 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -/** ブロック座標と範囲。仕様: {@code docs/spec/30-chunk-format.md} 5章 */ +/** + * ブロック座標と範囲 + */ class BlockPosTest { @Test @@ -70,7 +72,7 @@ void walksEveryPositionInside() { assertEquals(box.volume(), positions.size()); assertEquals(2 * 3 * 4, positions.size()); - // 並びは Y、Z、X の順で、X がいちばん内側で動く + // 並びはY、Z、Xの順で、Xがいちばん内側で動く assertEquals(new BlockPos(0, 0, 0), positions.get(0)); assertEquals(new BlockPos(1, 0, 0), positions.get(1)); assertEquals(new BlockPos(0, 0, 1), positions.get(2)); @@ -85,7 +87,7 @@ void oneBlockBoxHasVolumeOne() { int count = 0; - // 1 つだけ返ることを数えて確かめる + // 1つだけ返ることを数えて確かめる for (BlockPos pos : box.positions()) { count++; } diff --git a/java/src/test/java/io/github/scriptarts/springnbt/world/WorldTest.java b/java/src/test/java/io/github/scriptarts/springnbt/world/WorldTest.java index 42f653c..a27eaf0 100644 --- a/java/src/test/java/io/github/scriptarts/springnbt/world/WorldTest.java +++ b/java/src/test/java/io/github/scriptarts/springnbt/world/WorldTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -11,6 +12,8 @@ import io.github.scriptarts.springnbt.ErrorCode; import io.github.scriptarts.springnbt.SpringNbt; import io.github.scriptarts.springnbt.SpringNbtException; +import io.github.scriptarts.springnbt.anvil.RegionFile; +import io.github.scriptarts.springnbt.anvil.RegionFileMode; import io.github.scriptarts.springnbt.nbt.Compression; import io.github.scriptarts.springnbt.nbt.NamedTag; import io.github.scriptarts.springnbt.nbt.NbtCompound; @@ -31,20 +34,17 @@ import org.junit.jupiter.params.provider.ValueSource; /** - * World / Block レイヤ。 + * World / Blockレイヤ * - *

仕様: {@code docs/spec/30-chunk-format.md} / {@code 31-paletted-container.md} / - * {@code 40-world-layout.md} - * - *

他言語版と同じ検証項目を持つ。 + *

他言語版と同じ検証項目を持つ */ class WorldTest { - /** 共通テストベクタ(world/*.nbt)のパス。 */ + /** 共通テストベクタ(world/*.nbt)のパス */ private static Path vectorPath(String name) { Path current = Path.of("").toAbsolutePath(); - // 実行ディレクトリからリポジトリ直下まで遡って spec/testdata を探す + // 実行ディレクトリからリポジトリ直下まで遡ってspec/testdataを探す while (current != null) { Path candidate = current.resolve("spec").resolve("testdata") .resolve("world").resolve(name + ".nbt"); @@ -59,7 +59,7 @@ private static Path vectorPath(String name) { throw new IllegalStateException("テストベクタが見つからない: world/" + name + ".nbt"); } - /** テストベクタをチャンクとして読む。 */ + /** テストベクタをチャンクとして読む */ private static Chunk loadChunk(String name) { return loadChunk(name, ChunkReadOptions.defaults()); } @@ -169,7 +169,8 @@ class BitStorageTest { void long境界を跨がずに詰める() { BitStorage storage = BitStorage.create(5, 4096); - // bits=5 なら 1 つの long に 12 個。12 個目は次の long の最下位から始まる + // bits=5なら1つのlongに12個 + // 添字12は次のlongの最下位から始まる storage.set(11, 31); storage.set(12, 1); long[] longs = storage.toLongs(); @@ -204,7 +205,7 @@ class BitStorageTest { @Test void 寛容モードなら長さからビット幅を逆算する() { - // 4096 エントリを 342 long で表せるのは bits=5 のときだけ + // 4096エントリを342 longで表せるのはbits=5のときだけ BitStorage storage = BitStorage.fromLongs(new long[342], 4, 4096, true); assertEquals(5, storage.bitsPerEntry()); } @@ -253,7 +254,7 @@ private NbtCompound blockEntry(String name) { void 値を足すとパレットとビット幅が広がる() { PalettedContainer container = PalettedContainer.filled(blockEntry("minecraft:air"), 4096, 4); - // パレットを 17 要素まで増やして bits=4 から 5 への拡張を起こす + // 17種のブロックを足してパレットを18要素にし、bits=4から5への拡張を起こす for (int index = 0; index < 17; index++) { container.set(index, blockEntry("minecraft:block_" + index)); } @@ -282,7 +283,7 @@ private NbtCompound blockEntry(String name) { container.compact(); - // 残るのは実際に使われている air と dirt の 2 つ + // 残るのは実際に使われているairとdirtの2つ assertEquals(2, container.palette().size()); assertEquals("minecraft:dirt", ((NbtCompound) container.get(0)).getString("Name")); } @@ -328,7 +329,8 @@ class ChunkTest { Chunk chunk = loadChunk("palette_17"); String[] head = {"minecraft:air", "minecraft:stone"}; - // ベクタの添字は (位置 * 11) % 17。パレット先頭 2 つだけ名前が違う + // ベクタの添字は (位置 * 11) % 17 + // パレットの先頭2つだけ名前が違う for (int position = 0; position < 4096; position++) { int paletteIndex = (position * 11) % 17; BlockState block = chunk.getBlock( @@ -383,7 +385,7 @@ class ChunkTest { chunk.setBlock(3, -60, 7, "minecraft:stone"); assertTrue(chunk.isModified()); - // 保存済みとして印を下ろせる + // 保存済みとして印を外せる chunk.setIsModified(false); assertFalse(chunk.isModified()); @@ -400,7 +402,7 @@ class ChunkTest { Chunk chunk = loadChunk("palette_1"); chunk.setBiome(0, -64, 0, "minecraft:desert"); - // 同じ 4×4×4 の枠内はまとめて変わる + // 同じ4×4×4の枠内はまとめて変わる assertEquals("minecraft:desert", chunk.getBiome(3, -61, 3)); assertEquals("minecraft:plains", chunk.getBiome(4, -64, 0)); } @@ -435,13 +437,13 @@ class ChunkTest { assertEquals(2, chunk.raw().getList("block_ticks").size()); assertEquals(1, chunk.raw().getList("fluid_ticks").size()); - // (0,-64,0) には chest と block_tick、(1,-64,1) には furnace と fluid_tick がある + // (0,-64,0)にはchestとblock_tick、(1,-64,1)にはfurnaceとfluid_tickがある chunk.setBlock(0, -64, 0, BlockState.parse("minecraft:stone")); chunk.setBlock(1, -64, 1, BlockState.parse("minecraft:stone")); NbtList entities = chunk.raw().getList("block_entities"); - // 触っていない (15,-50,15) の barrel だけが残る + // 触っていない(15,-50,15)のbarrelだけが残る assertEquals(1, entities.size()); assertEquals("minecraft:barrel", ((NbtCompound) entities.get(0)).getString("id")); @@ -466,8 +468,7 @@ class ChunkTest { @Test void 別のチャンクの同じ相対座標は消さない() { - // 付随データは絶対座標で持つので、チャンク座標を取り違えると - // 無関係な要素を消してしまう + // 付随データは絶対座標で持つので、チャンク座標を間違えると無関係な要素を消してしまう NamedTag named = NbtIo.readFile(vectorPath("block_entities"), null); NbtCompound root = (NbtCompound) named.tag(); root.set("xPos", new NbtInt(1)); @@ -476,7 +477,8 @@ class ChunkTest { Chunk chunk = Chunk.fromNbt(root, ChunkReadOptions.defaults()); chunk.setBlock(0, -64, 0, BlockState.parse("minecraft:stone")); - // このチャンクの (0,-64,0) は絶対座標 (16,-64,16)。どれとも一致しない + // このチャンクの(0,-64,0)は絶対座標(16,-64,16) + // どれとも一致しない assertEquals(3, chunk.raw().getList("block_entities").size()); } @@ -517,7 +519,7 @@ class ChunkTest { @Nested class DataVersionTest { - /** DataVersion だけを差し替えたチャンクを作る。 */ + /** DataVersionだけを差し替えたチャンクを作る */ private NbtCompound foreignChunk() { NamedTag named = NbtIo.readFile(vectorPath("palette_1"), null); NbtCompound root = (NbtCompound) named.tag(); @@ -574,11 +576,11 @@ private NbtCompound foreignChunk() { new ChunkReadOptions().setOnVersionMismatch(VersionMismatchAction.IGNORE)); ChunkWriteOptions write = new ChunkWriteOptions().setAllowForeignDataVersion(true); - // DataVersion は読んだ値のまま残す + // DataVersionは読んだ値のまま残す assertEquals(3953, chunk.toNbt(write).getInt("DataVersion")); } - /** 対象より新しい DataVersion を持つチャンクを作る。 */ + /** 対象より新しいDataVersionを持つチャンクを作る */ private NbtCompound newerChunk() { NamedTag named = NbtIo.readFile(vectorPath("palette_1"), null); NbtCompound root = (NbtCompound) named.tag(); @@ -593,7 +595,7 @@ private NbtCompound newerChunk() { .setOnVersionMismatch(VersionMismatchAction.WARN) .setOnWarning(warnings::add); - // 形式が同じであれば、新しいバージョンでも黙って読める + // 形式が同じであれば、新しいバージョンでも警告を出さずに読める Chunk chunk = Chunk.fromNbt(newerChunk(), options); assertEquals(5015, chunk.dataVersion()); assertTrue(warnings.isEmpty()); @@ -610,7 +612,7 @@ private NbtCompound newerChunk() { void 新しいバージョンのチャンクはそのまま書き戻せる() { Chunk chunk = Chunk.fromNbt(newerChunk(), null); - // 許可を出さなくても書き戻せて、DataVersion も変わらない + // 許可を出さなくても書き戻せて、DataVersionも変わらない assertEquals(5015, chunk.toNbt(null).getInt("DataVersion")); } @@ -642,5 +644,34 @@ class MinecraftWorldTest { () -> MinecraftWorld.open(work)); assertEquals(ErrorCode.IO, error.code()); } + + @Test + void 次元経由で読んだチャンクの警告も通知先へ届く() throws Exception { + Path region = work.resolve("dimensions").resolve("minecraft").resolve("overworld").resolve("region"); + Files.createDirectories(region); + + // DataVersionが扱える形式より古いチャンクを1つだけ持つワールドを作る + NbtCompound level = new NbtCompound(); + level.set("Data", new NbtCompound()); + NbtIo.writeFile(work.resolve("level.dat"), new NamedTag("", level), null); + + NbtCompound chunk = (NbtCompound) NbtIo.readFile(vectorPath("palette_1"), null).tag(); + chunk.set("DataVersion", new NbtInt(3953)); + + try (RegionFile file = RegionFile.open(region.resolve("r.0.0.mca"), RegionFileMode.READ_WRITE)) { + file.writeChunk(0, 0, chunk); + } + + List warnings = new ArrayList<>(); + WorldOpenOptions options = WorldOpenOptions.defaults() + .setChunkRead(ChunkReadOptions.defaults().setOnWarning(warnings::add)); + + try (MinecraftWorld world = MinecraftWorld.open(work, options)) { + assertNotNull(world.dimension(Dimension.OVERWORLD).chunk(0, 0)); + } + + // ワールドを開くときに渡した通知先が、次元経由の読み込みでも呼ばれる + assertEquals(1, warnings.size()); + } } } diff --git a/python/README.md b/python/README.md index 059d9f4..be4d14b 100644 --- a/python/README.md +++ b/python/README.md @@ -1,18 +1,18 @@ -# SpringNBTLibrary(Python 版) +# SpringNBTLibrary(Python版) -Minecraft Java版 26.1 以降のワールド・NBTファイルを読み書きするライブラリです。 -Python 3.10 以上で動きます。依存パッケージはありません。 +Minecraft Java版26.1以降のワールド・NBTファイルを読み書きするライブラリです。 +Python 3.10以上で動きます。依存パッケージはありません。 -いまのワールド形式は 26.1 で入ったものです。 +いまのワールド形式は26.1で入ったものです。 それ以降のバージョンなら、形式が変わらない限りそのまま扱えます。 -C# / Java / TypeScript / Python / Rust 版があり、どれも同じ仕様書から作っています。 +C# / Java / TypeScript / Python / Rust版があり、どれも同じ仕様書から作っています。 同じ入力からは同じ結果が出ます([対応言語の一覧](../README.md#対応言語))。 -揃っているかは[適合性テスト](../docs/spec/90-conformance.md)で毎回確かめています。 +結果が揃っているかは[適合性テスト](../docs/spec/90-conformance.md)で毎回確かめています。 ## 導入 -[Releases](https://github.com/ScriptArts/SpringNBTLibrary/releases) から wheel を落として入れます。 +[Releases](https://github.com/ScriptArts/SpringNBTLibrary/releases)からwheelを落として入れます。 ```bash pip install spring_nbt_library-1.0.0-py3-none-any.whl @@ -20,12 +20,12 @@ pip install spring_nbt_library-1.0.0-py3-none-any.whl ## 使い方 -- [はじめに(Python)](../docs/getting-started/python.md) — まずここから -- [ガイド](../docs/guide/01-nbt.md) — 目的別の使い方 -- [API 対応表](../docs/api/overview.md) — 他言語版との対応 -- [機能一覧](../docs/features.md) — 何ができて何ができないか +- [はじめに(Python)](../docs/getting-started/python.md): まずここから +- [ガイド](../docs/guide/01-nbt.md): 目的別の使い方 +- [API対応表](../docs/api/overview.md): 他言語版との対応 +- [機能一覧](../docs/features.md): 何ができて何ができないか -説明は [`docs/`](../docs/README.md) にまとめてあります。 +説明は[`docs/`](../docs/README.md)にまとめてあります。 ## ライセンス diff --git a/python/pyproject.toml b/python/pyproject.toml index 979681a..65cea4e 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "spring-nbt-library" version = "1.0.0" -description = "Minecraft Java Edition の NBT / Anvil ワールドデータを読み書きするライブラリ" +description = "Minecraft Java EditionのNBT / Anvilワールドデータを読み書きするライブラリ" readme = "README.md" requires-python = ">=3.10" license = "MIT" diff --git a/python/src/spring_nbt_library/__init__.py b/python/src/spring_nbt_library/__init__.py index 18f2084..319571e 100644 --- a/python/src/spring_nbt_library/__init__.py +++ b/python/src/spring_nbt_library/__init__.py @@ -1,20 +1,19 @@ -"""SpringNBTLibrary — Minecraft Java版の NBT / Anvil ワールドデータを読み書きするライブラリ +"""SpringNBTLibraryは、Minecraft Java版のNBT / Anvilワールドデータを読み書きするライブラリ -仕様は ``docs/spec/`` を唯一の正とする -対象は 26.1 で導入されたワールド形式 (DataVersion 4786 以降) +対象は26.1で導入されたワールド形式 (DataVersion 4786以降) """ from .errors import ErrorCode, SpringNbtError -#: このライブラリが扱えるワールド形式の下限となる DataVersion (26.1) -# -# 26.1 で次元とプレイヤーデータの置き場が変わり、いまの形式になった -# これ以降のバージョンは、形式が同じであればそのまま読み書きできる -# これより古いワールドは構成そのものが違うので、 -# 読み込み時に UNSUPPORTED_DATA_VERSION の対象になる +#: このライブラリが扱えるワールド形式の下限となるDataVersion (26.1) +#: +#: 26.1で次元とプレイヤーデータの置き場が変わり、いまの形式になった +#: これ以降のバージョンは、形式が同じであればそのまま読み書きできる +#: これより古いワールドは構成そのものが違う +#: そうしたチャンクは、読み込み時は既定で警告を出し、書き戻しは既定でUNSUPPORTED_DATA_VERSIONにする MIN_SUPPORTED_DATA_VERSION = 4786 -#: 動作を確かめた Minecraft Java版の DataVersion (26.2) +#: 動作を確かめたMinecraft Java版のDataVersion (26.2) TARGET_DATA_VERSION = 4903 __all__ = ["MIN_SUPPORTED_DATA_VERSION", "TARGET_DATA_VERSION", diff --git a/python/src/spring_nbt_library/anvil/__init__.py b/python/src/spring_nbt_library/anvil/__init__.py index b938f94..88b62a2 100644 --- a/python/src/spring_nbt_library/anvil/__init__.py +++ b/python/src/spring_nbt_library/anvil/__init__.py @@ -1,6 +1,4 @@ -"""Anvil のリージョンファイル (.mca) の読み書き - -仕様: ``docs/spec/20-anvil-region.md`` +"""Anvilのリージョンファイル(.mca)の読み書き """ from .folder import RegionFolder diff --git a/python/src/spring_nbt_library/anvil/folder.py b/python/src/spring_nbt_library/anvil/folder.py index a714ae5..5909416 100644 --- a/python/src/spring_nbt_library/anvil/folder.py +++ b/python/src/spring_nbt_library/anvil/folder.py @@ -1,20 +1,15 @@ -"""リージョンファイルが並ぶディレクトリ 1 つ分 +"""リージョンファイルが並ぶディレクトリ1つ分 -``region/``、``entities/``、``poi/`` のいずれかを表す -開いたリージョンファイルはキャッシュし、:meth:`RegionFolder.close` でまとめて閉じる +``region/``、``entities/``、``poi/``のいずれかを表す +開いたリージョンファイルはキャッシュし、:meth:`RegionFolder.close`でまとめて閉じる チャンク座標からリージョンを解決するので、利用側はリージョンの存在を意識しなくてよい -:class:`RegionFile` はファイル全体をメモリへ載せるため、キャッシュには -``max_cached_regions`` 件の上限がある -上限を超えると、最も長く使われていない -ものから書き出して閉じる +:class:`RegionFile`はファイル全体をメモリへ載せるため、キャッシュには``max_cached_regions``件の上限がある +上限を超えると、最も長く使われていないものから書き出して閉じる 大きなワールドを端から走査してもメモリを使い切らない -このため :meth:`RegionFolder.region` が返した参照は、 -**別のリージョンへアクセスすると閉じられている場合がある** +このため:meth:`RegionFolder.region`が返した参照は、**別のリージョンへアクセスすると閉じられている場合がある** 参照を保持せず、必要なたびに取得すること - -仕様: ``docs/spec/20-anvil-region.md`` 5章 """ from __future__ import annotations @@ -31,14 +26,14 @@ #: 同時に開いておくリージョンファイル数の既定の上限 #: -#: 1 リージョンは最大 255 セクタ × 1024 チャンク=理論上 1GiB になりうる -#: 実データでは数 MB から数十 MB 程度 -# 8 件なら通常のワールドで数百 MB に収まる +#: 1リージョンは最大255セクタ × 1024チャンク=理論上1GiBになりうる +#: 実データでは数MBから数十MB程度 +#: 8件なら通常のワールドで数百MBに収まる DEFAULT_MAX_CACHED_REGIONS = 8 class RegionFolder: - """リージョンフォルダ 1 つ分""" + """リージョンフォルダ1つ分""" def __init__(self, directory: str, mode: RegionFileMode, max_cached_regions: int = DEFAULT_MAX_CACHED_REGIONS) -> None: @@ -59,7 +54,10 @@ def cached_region_count(self) -> int: def open(directory: str, mode: RegionFileMode = RegionFileMode.READ_ONLY, max_cached_regions: int = DEFAULT_MAX_CACHED_REGIONS) -> "RegionFolder": - """リージョンフォルダを開く""" + """リージョンフォルダを開く + + :raises SpringNbtError: 読み取り専用でディレクトリが存在しない場合、または上限が1未満の場合 + """ if max_cached_regions < 1: raise SpringNbtError.invalid_argument( "max_cached_regions は 1 以上でなければならない: %d" % max_cached_regions) @@ -85,11 +83,11 @@ def region_positions(self) -> List[RegionPos]: found = [] - # r.X.Z.mca として解釈できるファイルだけを拾う + # r.X.Z.mcaとして解釈できるファイルだけを拾う for name in os.listdir(self.directory): position = RegionPos.from_file_name(name) - # r.X.Z.mca として解釈できるファイルだけを拾う + # r.X.Z.mcaとして解釈できるファイルだけを拾う if position is not None: found.append(position) @@ -99,20 +97,20 @@ def region_positions(self) -> List[RegionPos]: def region(self, region_x: int, region_z: int) -> Optional[RegionFile]: """リージョンファイルを取得する - 読み取り専用で存在しなければ None + 読み取り専用で存在しなければNone """ self._ensure_open() position = RegionPos(region_x, region_z) cached = self._cache.get(position) if cached is not None: - # 使ったものを末尾へ move して、最近使った順を保つ + # 使ったものを末尾へ移して、最近使った順を保つ self._cache.move_to_end(position) return cached path = os.path.join(self.directory, position.file_name()) - # 読み取り専用では、存在しないリージョンは「チャンクが無い」として None を返す + # 読み取り専用では、存在しないリージョンは「チャンクが無い」としてNoneを返す if not os.path.exists(path) and self._mode == RegionFileMode.READ_ONLY: return None @@ -125,7 +123,7 @@ def region(self, region_x: int, region_z: int) -> Optional[RegionFile]: return opened def _evict_until_below_limit(self) -> None: - """新しく 1 件開けるよう、上限を下回るまで古いものを閉じる""" + """新しく1件開けるよう、上限を下回るまで古いものを閉じる""" # 上限に達している間、いちばん長く使っていないものから閉じる while len(self._cache) >= self.max_cached_regions: _, oldest = self._cache.popitem(last=False) @@ -143,8 +141,8 @@ def has_chunk(self, chunk_x: int, chunk_z: int) -> bool: return file.has_chunk(chunk_x, chunk_z) def read_chunk(self, chunk_x: int, chunk_z: int) -> Optional[NbtCompound]: - """チャンクを NBT として読む - 存在しなければ None + """チャンクをNBTとして読む + 存在しなければNone """ file = self._region_for(chunk_x, chunk_z) @@ -154,7 +152,7 @@ def read_chunk(self, chunk_x: int, chunk_z: int) -> Optional[NbtCompound]: return file.read_chunk(chunk_x, chunk_z) def write_chunk(self, chunk_x: int, chunk_z: int, tag: NbtCompound) -> None: - """チャンクを NBT として書き込む""" + """チャンクをNBTとして書き込む""" file = self._region_for(chunk_x, chunk_z) if file is None: @@ -165,7 +163,7 @@ def write_chunk(self, chunk_x: int, chunk_z: int, tag: NbtCompound) -> None: def delete_chunk(self, chunk_x: int, chunk_z: int) -> bool: """チャンクを削除する - 削除できたら True + 削除できたらTrue """ file = self._region_for(chunk_x, chunk_z) diff --git a/python/src/spring_nbt_library/anvil/lz4.py b/python/src/spring_nbt_library/anvil/lz4.py index 1c4f2c0..9e311fe 100644 --- a/python/src/spring_nbt_library/anvil/lz4.py +++ b/python/src/spring_nbt_library/anvil/lz4.py @@ -1,12 +1,9 @@ -"""圧縮方式ID 4 (LZ4) のチャンクを展開する +"""圧縮方式ID 4(LZ4)のチャンクを展開する -素の LZ4 ブロックでも LZ4 フレーム形式でもなく、 -独自ヘッダを持つブロックの連結である +素のLZ4ブロックでもLZ4フレーム形式でもなく、独自ヘッダを持つブロックの連結である 書き込みには対応しない -書き戻すときは Zlib になる - -仕様: docs/spec/20-anvil-region.md 3.1.1 / 3.1.2 +NBTとして書き戻すときは既定でZlibになり、生バイトのまま書き戻すとLZ4のまま残る """ from __future__ import annotations @@ -17,16 +14,16 @@ __all__ = ["decompress_lz4"] -#: ブロックの先頭に必ず置かれる 8 バイト +#: ブロックの先頭に必ず置かれる8バイト MAGIC = b"LZ4Block" #: ブロックヘッダの長さ HEADER_LENGTH = 21 -#: トークン上位 4 ビット: 本体が無圧縮 +#: トークン上位4ビット: 本体が無圧縮 METHOD_STORED = 0x10 -#: トークン上位 4 ビット: 本体が LZ4 圧縮 +#: トークン上位4ビット: 本体がLZ4圧縮 METHOD_COMPRESSED = 0x20 #: マッチの最小長 @@ -34,9 +31,9 @@ def decompress_lz4(payload: bytes) -> bytes: - """LZ4Block の連結を展開する + """LZ4Blockの連結を展開する - :raises SpringNbtError: 形式に反する入力の場合 (MALFORMED_DATA) + :raises SpringNbtError: 形式に反する入力の場合(MALFORMED_DATA) """ blocks = [] position = 0 @@ -50,12 +47,12 @@ def decompress_lz4(payload: bytes) -> bytes: def _decompress_block(payload: bytes, position: int): - """ブロックを 1 つ展開し、中身と次のブロックの開始位置を返す""" + """ブロックを1つ展開し、中身と次のブロックの開始位置を返す""" if position + HEADER_LENGTH > len(payload): raise SpringNbtError.malformed( "LZ4: ブロックヘッダが足りない(%d バイト)" % (len(payload) - position)) - # マジックが違えばそもそも LZ4Block ではない + # マジックが違えばそもそもLZ4Blockではない if payload[position:position + len(MAGIC)] != MAGIC: raise SpringNbtError.malformed("LZ4: ブロックが LZ4Block で始まっていない") @@ -70,7 +67,7 @@ def _decompress_block(payload: bytes, position: int): raise SpringNbtError.malformed("LZ4: ブロック本体が入力からはみ出している") if method == METHOD_STORED: - # 無圧縮なら 2 つの長さは一致していなければならない + # 無圧縮なら2つの長さは一致していなければならない if compressed_length != original_length: raise SpringNbtError.malformed( "LZ4: 無圧縮ブロックの長さが食い違う(%d と %d)" @@ -78,7 +75,7 @@ def _decompress_block(payload: bytes, position: int): return payload[body:body + compressed_length], body + compressed_length - # 圧縮されているなら素の LZ4 ブロックとして展開する + # 圧縮されているなら素のLZ4ブロックとして展開する if method == METHOD_COMPRESSED: data = _decompress_raw_block( payload[body:body + compressed_length], original_length) @@ -88,17 +85,17 @@ def _decompress_block(payload: bytes, position: int): def _validate_lengths(compressed_length: int, original_length: int) -> None: - """ヘッダに書かれた 2 つの長さが妥当か調べる""" + """ヘッダに書かれた2つの長さが妥当か調べる""" if compressed_length < 0 or original_length < 0: raise SpringNbtError.malformed("LZ4: ブロックの長さが負値") - # 片方だけが 0 になることはない + # 片方だけが0になることはない if (compressed_length == 0) != (original_length == 0): raise SpringNbtError.malformed("LZ4: ブロックの長さが片方だけ 0") def _decompress_raw_block(source: bytes, original_length: int) -> bytes: - """素の LZ4 ブロックを展開する""" + """素のLZ4ブロックを展開する""" output = bytearray(original_length) position = 0 written = 0 @@ -110,7 +107,7 @@ def _decompress_raw_block(source: bytes, original_length: int) -> bytes: literal_length = token >> 4 - # 15 なら追加バイトで長さが続く + # 15なら追加バイトで長さが続く if literal_length == 15: extra, position = _read_length(source, position) literal_length += extra @@ -133,7 +130,7 @@ def _decompress_raw_block(source: bytes, original_length: int) -> bytes: match_length = (token & 0x0F) + MIN_MATCH - # 下位 4 ビットが 15 なら追加バイトで長さが続く + # 下位4ビットが15なら追加バイトで長さが続く if (token & 0x0F) == 15: extra, position = _read_length(source, position) match_length += extra @@ -148,10 +145,10 @@ def _decompress_raw_block(source: bytes, original_length: int) -> bytes: def _read_length(source: bytes, position: int): - """255 が続く形式の追加長さを読む""" + """255が続く形式の追加長さを読む""" total = 0 - # 255 未満のバイトが出るまで足し続ける + # 255未満のバイトが出るまで足し続ける while True: if position >= len(source): raise SpringNbtError.malformed("LZ4: 長さの追加バイトが途中で切れた") @@ -184,7 +181,7 @@ def _copy_match(output: bytearray, written: int, offset: int, length: int) -> in start = written - offset - # コピー元と先は重なりうるので 1 バイトずつ写す + # コピー元と先は重なりうるので1バイトずつ写す for index in range(length): output[written + index] = output[start + index] diff --git a/python/src/spring_nbt_library/anvil/region.py b/python/src/spring_nbt_library/anvil/region.py index 466aa6d..a5222ad 100644 --- a/python/src/spring_nbt_library/anvil/region.py +++ b/python/src/spring_nbt_library/anvil/region.py @@ -1,12 +1,10 @@ -"""Anvil のリージョンファイル (``r.X.Z.mca``) -32×32 チャンクを格納する +"""Anvilのリージョンファイル (``r.X.Z.mca``) +32×32チャンクを格納する ファイル全体をメモリに読み込んで扱う -実データのリージョンは数 MB 程度で、 -この方が「触っていないチャンクのバイト配置をそのまま保つ」ことを保証しやすい -開いて何も変えずに :meth:`RegionFile.flush` すると、バイト単位で元と同じファイルになる - -仕様: ``docs/spec/20-anvil-region.md`` +実データのリージョンは数MB程度で、この方が「触っていないチャンクのバイト配置をそのまま保つ」ことを保証しやすい +開いて何も変えずに:meth:`RegionFile.flush`すると、バイト単位で元と同じファイルになる +ただし空のファイルは、8KiBのヘッダだけのファイルになる """ from __future__ import annotations @@ -45,11 +43,11 @@ #: 1リージョンに入るチャンク数 _CHUNK_COUNT = 1024 -#: 1チャンクが確保できるセクタ数の上限(長さフィールドが u8 のため) +#: 1チャンクが確保できるセクタ数の上限(長さフィールドがu8のため) _MAX_SECTORS = 255 #: リージョン内に収められるペイロードの上限 -# 超えると外部ファイルへ退避する +#: 超えると外部ファイルへ退避する _MAX_INLINE_PAYLOAD = (_MAX_SECTORS * SECTOR_SIZE) - 5 _REGION_NAME = re.compile(r"^r\.(-?\d+)\.(-?\d+)\.mca$") @@ -58,30 +56,28 @@ class ChunkCompression(enum.Enum): """リージョンファイル内でチャンクに使われる圧縮方式 - NBT 層の :class:`~spring_nbt_library.nbt.Compression` とは別物であることに注意 - あちらはファイル全体の圧縮を表し、こちらはリージョン内の 1 チャンクに付く - 1 バイトのIDを表す - - 仕様: ``docs/spec/20-anvil-region.md`` 3.1章 + NBT層の:class:`~spring_nbt_library.nbt.Compression`とは違うものであることに注意 + あちらはファイル全体の圧縮を表し、こちらはリージョン内の1チャンクに付く1バイトのIDを表す """ #: GZip (RFC 1952) - # 実データではほぼ使われない + #: 実データではほぼ使われない GZIP = 1 #: Zlib (RFC 1950) - # Minecraft が実際に書き出す方式 + #: Minecraftが実際に書き出す方式 ZLIB = 2 #: 無圧縮 NONE = 3 - #: LZ4(ブロック形式) - # 任意依存 + #: LZ4(独自ヘッダ付きのブロック連結) + #: 読み込みのみ対応 + #: 展開は自前で行い、外部の依存は使わない LZ4 = 4 #: サードパーティ製サーバのカスタム方式 - # 中身は解釈できない + #: 中身は解釈できない CUSTOM = 127 def id(self) -> int: @@ -94,11 +90,11 @@ def as_string(self) -> str: @staticmethod def from_id(value: int) -> "ChunkCompression": - """圧縮方式IDから :class:`ChunkCompression` を得る + """圧縮方式IDから:class:`ChunkCompression`を得る :raises SpringNbtError: 未知のIDの場合 """ - # 仕様が定めるのは 1・2・3・4・127 の 5 種類だけ + # 仕様が定めるのは1・2・3・4・127の5種類だけ for candidate in ChunkCompression: if candidate.value == value: return candidate @@ -119,17 +115,17 @@ class RegionFileMode(enum.Enum): """リージョンファイルを開くときの動作""" #: 読み取り専用 - # 書き込み系の操作はエラーになる + #: 書き込み系の操作はエラーになる(flushは何もしない) READ_ONLY = "read_only" #: 読み書き - # ファイルが無ければ空のリージョンとして扱う + #: ファイルが無ければ空のリージョンとして扱う READ_WRITE = "read_write" class RegionPos: """リージョンの座標 - 1リージョンは 32×32 チャンクを担当する + 1リージョンは32×32チャンクを担当する """ __slots__ = ("x", "z") @@ -144,8 +140,8 @@ def file_name(self) -> str: @staticmethod def from_file_name(file_name: str) -> Optional["RegionPos"]: - """``r.X.Z.mca`` 形式のファイル名から座標を得る - 解釈できなければ None + """``r.X.Z.mca``形式のファイル名から座標を得る + 解釈できなければNone """ matched = _REGION_NAME.match(file_name) @@ -179,7 +175,7 @@ def __init__(self, x: int, z: int) -> None: def region(self) -> RegionPos: """このチャンクを含むリージョンの座標 - Python の ``>>`` は算術右シフトなので負の座標でも正しく求まる + Pythonの``>>``は算術右シフトなので負の座標でも正しく求まる """ return RegionPos(self.x >> 5, self.z >> 5) @@ -211,8 +207,7 @@ def __repr__(self) -> str: class RawChunk: """リージョンファイルに格納されたままの、圧縮済みチャンクデータ - 本ライブラリが解釈できない圧縮方式(LZ4 未導入、カスタム方式)でも - これなら取り出せる + 本ライブラリが解釈できない圧縮方式(カスタム方式)のチャンクでも、これなら取り出せる バックアップや別ツールへの受け渡しに使う """ @@ -230,14 +225,14 @@ def __repr__(self) -> str: class RegionFile: - """リージョンファイル 1 つ分""" + """リージョンファイル1つ分""" def __init__(self, path: str, mode: RegionFileMode, position: RegionPos, data: bytearray) -> None: self._path = path self._directory = os.path.dirname(path) - # パスが階層を含まない場合、.mcc の置き場としてカレントディレクトリを使う + # パスが階層を含まない場合、.mccの置き場としてカレントディレクトリを使う if self._directory == "": self._directory = "." @@ -258,7 +253,7 @@ def __init__(self, path: str, mode: RegionFileMode, position: RegionPos, def open(path: str, mode: RegionFileMode = RegionFileMode.READ_ONLY) -> "RegionFile": """リージョンファイルを開く - :param path: ``r.X.Z.mca`` という名前のファイル + :param path: ``r.X.Z.mca``という名前のファイル 座標はファイル名から読み取る :param mode: 読み取り専用か読み書きか """ @@ -293,7 +288,7 @@ def __exit__(self, exc_type, exc_value, traceback) -> None: def _parse_header(self) -> None: """ヘッダを解析し、ロケーションとタイムスタンプを取り込む""" - # 空ファイルは「チャンクが 1 つも無いリージョン」として受け入れる + # 空ファイルは「チャンクが1つも無いリージョン」として受け入れる if len(self._data) == 0: self._data = bytearray(_HEADER_SECTORS * SECTOR_SIZE) return @@ -310,7 +305,7 @@ def _parse_header(self) -> None: total_sectors = len(self._data) // SECTOR_SIZE sector_owner: Dict[int, int] = {} - # ロケーションテーブルの 1024 エントリを順に取り込む + # ロケーションテーブルの1024エントリを順に取り込む for index in range(_CHUNK_COUNT): entry = struct.unpack_from(">I", self._data, index * 4)[0] offset = entry >> 8 @@ -334,7 +329,7 @@ def _parse_header(self) -> None: raise SpringNbtError.malformed( "チャンク %d の割り当てがファイル外へはみ出している" % index) - # 同じセクタを 2 つのチャンクが指していたら、どちらかが壊れている + # 同じセクタを2つのチャンクが指していたら、どちらかがおかしくなっている for sector in range(offset, offset + count): owner = sector_owner.get(sector) @@ -349,7 +344,8 @@ def _parse_header(self) -> None: self._sector_counts[index] = count def _write_header(self) -> None: - """ロケーションテーブルとタイムスタンプテーブルを先頭 2 セクタへ書き戻す""" + """ロケーションテーブルとタイムスタンプテーブルを先頭2セクタへ書き戻す""" + # ロケーションテーブルとタイムスタンプテーブルを、添字順に組み立て直す for index in range(_CHUNK_COUNT): entry = (self._offsets[index] << 8) | self._sector_counts[index] struct.pack_into(">I", self._data, index * 4, entry) @@ -390,7 +386,7 @@ def chunk_positions(self) -> List[ChunkPos]: self._ensure_open() result = [] - # 添字の昇順に走査する(local_z が外、local_x が内) + # 添字の昇順に走査する(local_zが外、local_xが内) for index in range(_CHUNK_COUNT): if self._sector_counts[index] == 0: continue @@ -403,8 +399,8 @@ def chunk_positions(self) -> List[ChunkPos]: return result def timestamp(self, chunk_x: int, chunk_z: int) -> int: - """チャンクの最終更新時刻(Unix 秒) - 存在しなければ 0 + """チャンクの最終更新時刻(Unix秒) + タイムスタンプテーブルの値をそのまま返すため、チャンクが無くても0とは限らない """ self._ensure_open() return self._timestamps[self._index_of(chunk_x, chunk_z)] @@ -418,7 +414,7 @@ def set_timestamp(self, chunk_x: int, chunk_z: int, value: int) -> None: def read_chunk_raw(self, chunk_x: int, chunk_z: int) -> Optional[RawChunk]: """チャンクを圧縮されたまま取り出す - 存在しなければ None + 存在しなければNone """ self._ensure_open() index = self._index_of(chunk_x, chunk_z) @@ -442,14 +438,14 @@ def read_chunk_raw(self, chunk_x: int, chunk_z: int) -> Optional[RawChunk]: compression = ChunkCompression.from_id(scheme_byte & 0x7F) if external: - # 最上位ビットが立っている場合、本体は c.X.Z.mcc にある + # 最上位ビットが立っている場合、本体はc.X.Z.mccにある return RawChunk(compression, self._read_external_file(chunk_x, chunk_z), True) return RawChunk(compression, bytes(self._data[start + 5:start + 4 + length]), False) def read_chunk(self, chunk_x: int, chunk_z: int) -> Optional[NbtCompound]: - """チャンクを NBT として読む - 存在しなければ None + """チャンクをNBTとして読む + 存在しなければNone """ raw = self.read_chunk_raw(chunk_x, chunk_z) @@ -463,7 +459,7 @@ def read_chunk(self, chunk_x: int, chunk_z: int) -> Optional[NbtCompound]: def write_chunk(self, chunk_x: int, chunk_z: int, tag: NbtCompound, compression: ChunkCompression = ChunkCompression.ZLIB) -> None: - """チャンクを NBT として書き込む""" + """チャンクをNBTとして書き込む""" plain = write_nbt_bytes(NamedTag("", tag), NbtWriteOptions(compression=Compression.NONE)) self.write_chunk_raw( @@ -478,7 +474,7 @@ def write_chunk_raw(self, chunk_x: int, chunk_z: int, raw: RawChunk) -> None: use_external = len(raw.data) > _MAX_INLINE_PAYLOAD if use_external: - # 1MiB を超えるチャンクは外部ファイルへ退避し、リージョンには目印だけ残す + # 255セクタ(約1MiB)に収まらないチャンクは外部ファイルへ退避し、リージョンには目印だけ残す self._write_external_file(chunk_x, chunk_z, raw.data) payload = b"" scheme_byte = raw.compression.id() | 0x80 @@ -511,7 +507,7 @@ def write_chunk_raw(self, chunk_x: int, chunk_z: int, raw: RawChunk) -> None: def delete_chunk(self, chunk_x: int, chunk_z: int) -> bool: """チャンクを削除する - 削除できたら True + 削除できたらTrue """ self._ensure_open() self._ensure_writable() @@ -531,7 +527,7 @@ def delete_chunk(self, chunk_x: int, chunk_z: int) -> bool: def _allocate_sectors(self, index: int, needed: int) -> int: """必要なセクタ数を確保し、開始セクタ番号を返す - 既存の割り当てがちょうど同じ大きさならその場を使い、 + 既存の割り当てがちょうど同じ大きさなら、その場所をそのまま使う そうでなければ先頭から空き領域を探し、無ければ末尾へ追加する """ # 大きさが変わらないなら動かさない @@ -563,12 +559,12 @@ def _allocate_sectors(self, index: int, needed: int) -> int: def _build_sector_usage(self, ignore_index: int) -> List[bool]: """セクタの使用状況を作る - ``ignore_index`` のチャンクは空きとして扱う + ``ignore_index``のチャンクは空きとして扱う """ total_sectors = len(self._data) // SECTOR_SIZE used = [False] * total_sectors - # ヘッダの 2 セクタは常に使用中 + # ヘッダの2セクタは常に使用中 for sector in range(min(_HEADER_SECTORS, total_sectors)): used[sector] = True @@ -658,6 +654,8 @@ def flush(self) -> None: return self._write_header() + # 読み書きで開いたフォルダがまだ無ければ、書き出す前に作る + self._ensure_directory() try: with open(self._path, "wb") as handle: @@ -705,8 +703,19 @@ def _read_external_file(self, chunk_x: int, chunk_z: int) -> bytes: raise SpringNbtError( ErrorCode.IO, "外部チャンクファイルを読めない: %s" % external) from error + def _ensure_directory(self) -> None: + """ファイルを置くディレクトリが無ければ作る + 読むだけの操作で空のディレクトリができないよう、書き出す直前にだけ呼ぶ + """ + try: + os.makedirs(self._directory, exist_ok=True) + except OSError as error: + raise SpringNbtError(ErrorCode.IO, "ディレクトリを作れない: %s" % self._directory) from error + def _write_external_file(self, chunk_x: int, chunk_z: int, payload: bytes) -> None: external = self._external_path(chunk_x, chunk_z) + # 読み書きで開いたフォルダがまだ無ければ、書き出す前に作る + self._ensure_directory() try: with open(external, "wb") as handle: @@ -732,15 +741,16 @@ def _decompress_chunk(raw: RawChunk) -> bytes: if raw.compression == ChunkCompression.NONE: return raw.data - # GZip なら展開して返す + # GZipなら展開して返す if raw.compression == ChunkCompression.GZIP: try: return gzip.decompress(raw.data) - except OSError as error: + except (OSError, EOFError, zlib.error) as error: + # GZipでないデータ、途中で切れたデータ、中身の壊れたデータのどれも、仕様に反するデータとして扱う raise SpringNbtError( ErrorCode.MALFORMED_DATA, "チャンクの圧縮データを展開できない") from error - # Zlib なら展開して返す + # Zlibなら展開して返す if raw.compression == ChunkCompression.ZLIB: try: return zlib.decompress(raw.data) @@ -748,7 +758,7 @@ def _decompress_chunk(raw: RawChunk) -> bytes: raise SpringNbtError( ErrorCode.MALFORMED_DATA, "チャンクの圧縮データを展開できない") from error - # LZ4 は読み込みのみ対応 + # LZ4は読み込みのみ対応 if raw.compression == ChunkCompression.LZ4: return decompress_lz4(raw.data) @@ -763,7 +773,7 @@ def _compress_chunk(plain: bytes, compression: ChunkCompression) -> bytes: return plain if compression == ChunkCompression.GZIP: - # mtime を 0 に固定して、同じ入力から同じバイト列が出るようにする + # mtimeを0に固定して、同じ入力から同じバイト列が出るようにする return gzip.compress(plain, mtime=0) if compression == ChunkCompression.ZLIB: diff --git a/python/src/spring_nbt_library/conformance.py b/python/src/spring_nbt_library/conformance.py index 80580d2..f426154 100644 --- a/python/src/spring_nbt_library/conformance.py +++ b/python/src/spring_nbt_library/conformance.py @@ -1,10 +1,8 @@ """適合性検証ツール -4言語すべてが同じインターフェースで同じ出力を出す +全言語が同じインターフェースで同じ出力を出す -``spec/run-conformance.sh`` がこのツールを4言語ぶん起動し、 -出力を相互に diff することで「4言語が同一に振る舞う」ことを機械的に確かめる - -仕様: ``docs/spec/90-conformance.md`` 2.3章 +``spec/run-conformance.sh``がこのツールを全言語ぶん起動し、 +出力を相互にdiffすることで「全言語が同一に振る舞う」ことを機械的に確かめる """ from __future__ import annotations @@ -44,7 +42,7 @@ from .nbt import snbt as snbt_module def nbt_list(data: bytes, fmt) -> str: - """連なった NBT を、位置を追いながら一覧として書き出す""" + """連なったNBTを、位置を追いながら一覧として書き出す""" options = NbtReadOptions(fmt=fmt, compression=Compression.NONE) all_tags = read_bytes_all(data, options) lines = ["count %d" % len(all_tags)] @@ -80,11 +78,10 @@ def nbt_list(data: bytes, fmt) -> str: # --------------------------------------------------------------------------- # 正規化JSON # -# 浮動小数点をビットパターンで、64bit 整数を10進文字列で表すのが要 -# 10進表記の丸めや JSON 数値の精度は処理系ごとに差が出るため、 -# そのまま出すと4言語の出力が一致しない +# 浮動小数点をビットパターンで、64bit整数を10進文字列で表すのが要 +# 10進表記の丸めやJSON数値の精度は処理系ごとに差が出るため、 +# そのまま出すと言語間で出力が一致しない # -# 仕様: docs/spec/00-conventions.md 6章 # --------------------------------------------------------------------------- _JSON_ESCAPES = { @@ -99,15 +96,15 @@ def nbt_list(data: bytes, fmt) -> str: def _json_string(text: str) -> str: - """JSON 文字列を書き出す - 非 ASCII は必ず ``\\uXXXX`` へ逃がす + """JSON文字列を書き出す + 非ASCIIは必ず``\\uXXXX``へエスケープする - エスケープの単位は **UTF-16 コード単位** - C# / Java と桁数を揃えるため、補助文字はサロゲートペアの 2 つに分けて出す + エスケープの単位は**UTF-16コード単位** + C# / Javaと桁数を揃えるため、補助文字はサロゲートペアの2つに分けて出す """ parts = ['"'] - # コードポイントごとに、必要なら UTF-16 コード単位へ分解して書く + # コードポイントごとに、必要ならUTF-16コード単位へ分解して書く for character in text: # 決まった置き換えがある文字は、そのまま使う if character in _JSON_ESCAPES: @@ -116,7 +113,7 @@ def _json_string(text: str) -> str: code = ord(character) - # ASCII の印字可能文字だけ生で出す + # ASCIIの印字可能文字だけ生で出す if 0x20 <= code <= 0x7E: parts.append(character) elif code >= 0x10000: @@ -141,7 +138,7 @@ def _to_hex(data: bytes) -> str: def _json_tag(tag: NbtTag) -> str: parts = ['{"type":', _json_string(tag.type.as_string())] - # list だけは value の前に element_type が入る(仕様が定めるキー順) + # listだけはvalueの前にelement_typeが入る(仕様が定めるキー順) if isinstance(tag, NbtList): parts.append(',"element_type":') parts.append(_json_string(tag.element_type.as_string())) @@ -150,11 +147,11 @@ def _json_tag(tag: NbtTag) -> str: import struct - # 型ごとに、正規化 JSON での表し方を変える + # 型ごとに、正規化JSONでの表し方を変える if isinstance(tag, (NbtByte, NbtShort, NbtInt)): parts.append("%d" % tag.value) elif isinstance(tag, NbtLong): - # 64bit 整数は JSON 数値だと処理系によって精度が落ちるため10進文字列で表す + # 64bit整数はJSON数値だと処理系によって精度が落ちるため10進文字列で表す parts.append(_json_string("%d" % tag.value)) elif isinstance(tag, NbtFloat): parts.append(_json_string(_hex_bits(struct.pack(">f", tag.value)))) @@ -163,21 +160,21 @@ def _json_tag(tag: NbtTag) -> str: elif isinstance(tag, NbtString): parts.append(_json_string(tag.value)) - # MUTF-8 のバイト列も併記する - # 孤立サロゲートなど UTF-8 に写せない値を厳密に比較するため + # MUTF-8のバイト列も併記する + # 孤立サロゲートなどUTF-8に写せない値を厳密に比較するため parts.append(',"mutf8":') parts.append(_json_string(_to_hex(mutf8.encode(tag.value)))) elif isinstance(tag, (NbtByteArray, NbtIntArray)): parts.append("[" + ",".join("%d" % value for value in tag.value) + "]") elif isinstance(tag, NbtLongArray): - # 64bit 整数は10進文字列の配列で表す + # 64bit整数は10進文字列の配列で表す parts.append("[" + ",".join(_json_string("%d" % value) for value in tag.value) + "]") elif isinstance(tag, NbtList): parts.append("[" + ",".join(_json_tag(item) for item in tag) + "]") elif isinstance(tag, NbtCompound): - # JSON オブジェクトだと挿入順の保持が処理系依存になるため、組の配列で表す + # JSONオブジェクトだと挿入順の保持が処理系依存になるため、組の配列で表す entries = [] - # compound はキーと値の組の配列として写す + # compoundはキーと値の組の配列として写す # 挿入順を保つため for key, value in tag.items(): entries.append("[" + _json_string(key) + "," + _json_tag(value) + "]") @@ -190,7 +187,7 @@ def _json_tag(tag: NbtTag) -> str: def normalized_json(named: NamedTag, fmt: NbtFormat) -> str: - """ルートを含む全体を JSON 文字列へ変換する + """ルートを含む全体をJSON文字列へ変換する 末尾に改行を1つ付ける """ return "".join([ @@ -205,15 +202,14 @@ def normalized_json(named: NamedTag, fmt: NbtFormat) -> str: # --------------------------------------------------------------------------- # リージョンファイル # -# 仕様: docs/spec/90-conformance.md 2.3章 # --------------------------------------------------------------------------- def region_list(region: RegionFile) -> str: - """存在するチャンクを 1 行 1 チャンクで書き出す + """存在するチャンクを1行1チャンクで書き出す 並びはロケーションテーブルの添字順 - 各行は「絶対X 絶対Z タイムスタンプ 圧縮方式 圧縮後バイト数 展開後バイト数 キー数」 + 各行は「絶対X 絶対Z タイムスタンプ 圧縮方式 圧縮後バイト数 展開後バイト数 ルート直下キー数」 """ lines = ["region %d %d" % (region.region_x, region.region_z)] total = 0 @@ -247,7 +243,7 @@ def region_list(region: RegionFile) -> str: def region_rewrite(source: RegionFile, output_path: str) -> None: """全チャンクを読み直し、無圧縮で新しいリージョンへ詰め直して書き出す - 無圧縮にするのは、zlib の出力が処理系ごとに違い、 + 無圧縮にするのは、zlibの出力が処理系ごとに違い、 圧縮したままでは言語間でバイトが一致しないため """ # 途中結果が残らないよう、書き出し先は必ず作り直す @@ -271,9 +267,8 @@ def region_rewrite(source: RegionFile, output_path: str) -> None: # --------------------------------------------------------------------------- -# チャンク(World レイヤ) +# チャンク(Worldレイヤ) # -# 仕様: docs/spec/90-conformance.md 2.3章 # --------------------------------------------------------------------------- @@ -281,7 +276,7 @@ def chunk_describe(chunk: Chunk) -> str: """チャンクの全ブロック・全バイオームを走査して集計する パレットとビットストレージを端から端まで通すので、 - ビット詰めの実装が 1 か所でもずれれば集計値が変わる + ビット詰めの実装が1か所でもずれれば集計値が変わる """ lines = ["chunk %d %d %d %s" % (chunk.x, chunk.z, chunk.min_section_y, chunk.status)] blocks = {} @@ -308,7 +303,7 @@ def chunk_describe(chunk: Chunk) -> str: lines.append("section %d %d %d %d %d" % (section_y, block_palette, block_bits, biome_palette, biome_bits)) - # 全ブロックを 1 つずつ読んで、状態の文字列表現ごとに数える + # 全ブロックを1つずつ読んで、状態の文字列表現ごとに数える for y in range(16): for z in range(16): for x in range(16): @@ -320,7 +315,7 @@ def chunk_describe(chunk: Chunk) -> str: key = str(block) blocks[key] = blocks.get(key, 0) + 1 - # バイオームは 4×4×4 単位なので、4 ブロックおきに見る + # バイオームは4×4×4単位なので、4ブロックおきに見る for y in range(0, 16, 4): for z in range(0, 16, 4): for x in range(0, 16, 4): @@ -355,7 +350,8 @@ def chunk_edit(chunk: Chunk) -> None: state = BlockState.parse("minecraft:edited_%d[step=%d]" % (index, index)) chunk.set_block(index % 16, base_y + (index // 16), index % 16, state) - # プロパティ付きのブロックを、名前は同じで状態違いで置く + # プロパティ付きのブロックを置く + # 1つ目と2つ目はプロパティの並び順だけが違う同じ状態、3つ目は名前空間を省いて書いた別の状態 chunk.set_block(1, base_y + 2, 1, BlockState.parse("minecraft:oak_stairs[facing=north,half=top]")) chunk.set_block(2, base_y + 2, 2, BlockState.parse("minecraft:oak_stairs[half=top,facing=north]")) chunk.set_block(3, base_y + 2, 3, BlockState.parse("oak_stairs[facing=south]")) @@ -367,16 +363,16 @@ def chunk_edit(chunk: Chunk) -> None: # 使われなくなったパレット要素を掃除する chunk.compact() - # 高さマップと光源は再計算しないので、無効化して Minecraft に任せる + # 高さマップと光源は再計算しないので、無効化してMinecraftに任せる chunk.clear_heightmaps() chunk.invalidate_lighting() def read_chunk_file(path: str) -> Chunk: - """チャンク NBT のファイルを読む""" + """チャンクNBTのファイルを読む""" named = read_file(path) - # 検証では DataVersion の違いを警告にせず、そのまま読む + # 検証では、DataVersionが扱える形式より古くても警告にせずそのまま読む options = ChunkReadOptions(on_version_mismatch=VersionMismatchAction.IGNORE) return Chunk.from_nbt(named.tag, options) @@ -387,8 +383,8 @@ def read_chunk_file(path: str) -> Chunk: def _parse_format(args) -> NbtFormat: - """``--format network`` が指定されていればネットワーク形式として読む""" - # 3 番目以降の引数からオプションを探す + """``--format network``が指定されていればネットワーク形式として読む""" + # 3番目以降の引数からオプションを探す for index in range(3, len(args) - 1): if args[index] == "--format" and args[index + 1] == "network": return NbtFormat.NETWORK @@ -397,9 +393,9 @@ def _parse_format(args) -> NbtFormat: def _write_text_file(path: str, content: str) -> None: - """改行を変換せず、BOM も付けずに UTF-8 で書く + """改行を変換せず、BOMも付けずにUTF-8で書く - 孤立サロゲートを含みうるため ``surrogatepass`` で符号化する + 孤立サロゲートを含みうるため``surrogatepass``で符号化する """ with open(path, "wb") as handle: handle.write(content.encode("utf-8", "surrogatepass")) @@ -417,7 +413,7 @@ def main(argv=None) -> int: command = argv[0] - # version は入力ファイルを取らない + # versionは入力ファイルを取らない if command == "version": sys.stdout.write( "python spring-nbt-library 1.0.0 target_data_version=%d\n" % TARGET_DATA_VERSION) @@ -454,7 +450,7 @@ def main(argv=None) -> int: return 0 - # 連なった NBT を一覧にする + # 連なったNBTを一覧にする if command == "nbt-list": with open(argv[1], "rb") as handle: data = handle.read() @@ -462,7 +458,7 @@ def main(argv=None) -> int: _write_text_file(argv[2], nbt_list(data, fmt)) return 0 - # リージョン系は NBT の読み込みを経由しない + # リージョン系はNBTの読み込みを経由しない if command in ("region-list", "region-rewrite"): with RegionFile.open(argv[1], RegionFileMode.READ_ONLY) as region: # チャンク一覧を書き出す @@ -475,7 +471,7 @@ def main(argv=None) -> int: named = read_file(argv[1], NbtReadOptions(fmt=fmt)) - # 正規化 JSON を書き出す + # 正規化JSONを書き出す if command == "decode": _write_text_file(argv[2], normalized_json(named, fmt)) elif command == "encode": @@ -485,7 +481,7 @@ def main(argv=None) -> int: else: _write_text_file(argv[2], snbt_module.write(named.tag) + "\n") except SpringNbtError as error: - # 4言語で同じ ErrorCode を出すことが検証対象なので、コードを機械可読な形で出す + # 言語間で同じErrorCodeを出すことが検証対象なので、コードを機械可読な形で出す sys.stderr.write("ERROR %s %s\n" % (error.code.as_string(), error.message)) return 1 diff --git a/python/src/spring_nbt_library/errors.py b/python/src/spring_nbt_library/errors.py index 4bdb061..83f054a 100644 --- a/python/src/spring_nbt_library/errors.py +++ b/python/src/spring_nbt_library/errors.py @@ -1,6 +1,4 @@ -"""4言語で共通のエラーモデル - -仕様: ``docs/spec/00-conventions.md`` 4章 / ``docs/adr/0005-unified-error-model.md`` +"""全言語で共通のエラーモデル """ from __future__ import annotations @@ -12,7 +10,7 @@ class ErrorCode(enum.Enum): """エラーの分類 - 4言語すべてで同一の集合を持つ + 全言語で同一の集合を持つ """ #: 下位の入出力失敗 @@ -24,7 +22,7 @@ class ErrorCode(enum.Enum): #: 期待した型と違うタグを取り出した UNEXPECTED_TAG_TYPE = "UNEXPECTED_TAG_TYPE" - #: 仕様上は妥当だが、このビルドでは扱えない + #: 仕様上は妥当だが、このライブラリでは扱えない UNSUPPORTED_FEATURE = "UNSUPPORTED_FEATURE" #: 安全上限を超えた @@ -33,7 +31,7 @@ class ErrorCode(enum.Enum): #: 呼び出し側の引数が不正 INVALID_ARGUMENT = "INVALID_ARGUMENT" - #: 対象バージョン外のデータ + #: 扱える形式より古いデータ UNSUPPORTED_DATA_VERSION = "UNSUPPORTED_DATA_VERSION" def as_string(self) -> str: @@ -43,7 +41,7 @@ def as_string(self) -> str: class SpringNbtError(Exception): """本ライブラリが送出する例外 - 分類は :attr:`code` で判別する + 分類は:attr:`code`で判別する """ def __init__(self, code: ErrorCode, message: str) -> None: @@ -56,25 +54,25 @@ def __str__(self) -> str: @staticmethod def malformed(message: str) -> "SpringNbtError": - """``MALFORMED_DATA`` の例外を作る""" + """``MALFORMED_DATA``の例外を作る""" return SpringNbtError(ErrorCode.MALFORMED_DATA, message) @staticmethod def invalid_argument(message: str) -> "SpringNbtError": - """``INVALID_ARGUMENT`` の例外を作る""" + """``INVALID_ARGUMENT``の例外を作る""" return SpringNbtError(ErrorCode.INVALID_ARGUMENT, message) @staticmethod def unexpected_tag_type(message: str) -> "SpringNbtError": - """``UNEXPECTED_TAG_TYPE`` の例外を作る""" + """``UNEXPECTED_TAG_TYPE``の例外を作る""" return SpringNbtError(ErrorCode.UNEXPECTED_TAG_TYPE, message) @staticmethod def limit_exceeded(message: str) -> "SpringNbtError": - """``LIMIT_EXCEEDED`` の例外を作る""" + """``LIMIT_EXCEEDED``の例外を作る""" return SpringNbtError(ErrorCode.LIMIT_EXCEEDED, message) @staticmethod def unsupported_feature(message: str) -> "SpringNbtError": - """``UNSUPPORTED_FEATURE`` の例外を作る""" + """``UNSUPPORTED_FEATURE``の例外を作る""" return SpringNbtError(ErrorCode.UNSUPPORTED_FEATURE, message) diff --git a/python/src/spring_nbt_library/nbt/__init__.py b/python/src/spring_nbt_library/nbt/__init__.py index 9196db7..f34e734 100644 --- a/python/src/spring_nbt_library/nbt/__init__.py +++ b/python/src/spring_nbt_library/nbt/__init__.py @@ -1,8 +1,6 @@ """NBT (Named Binary Tag) の読み書き -このレイヤは Minecraft のバージョンに一切依存しない - -仕様: ``docs/spec/10-nbt-binary.md`` +このレイヤはMinecraftのバージョンに一切依存しない """ from . import canonical, mutf8, snbt diff --git a/python/src/spring_nbt_library/nbt/_recursion.py b/python/src/spring_nbt_library/nbt/_recursion.py index 058aaf1..0dd1e17 100644 --- a/python/src/spring_nbt_library/nbt/_recursion.py +++ b/python/src/spring_nbt_library/nbt/_recursion.py @@ -1,13 +1,12 @@ -"""Python の再帰上限を仕様の深さ上限に合わせるための補助 +"""Pythonの再帰上限を仕様の深さ上限に合わせるための補助 -Python の既定の再帰上限は 1000 で、仕様が定めるネスト深さ上限 512 に届かない -引き上げておかないと、ライブラリが ``LIMIT_EXCEEDED`` を返すより先に -``RecursionError`` が出てしまい、他の3言語と挙動が変わる +Pythonの既定の再帰上限1000のままでは、仕様が定めるネスト深さ上限512まで届かない +引き上げておかないと、ライブラリが``LIMIT_EXCEEDED``を返すより先に +``RecursionError``が出てしまい、他の4言語と挙動が変わる -他の3言語(C# / Java / Rust)は既定のスタックで深さ 512 を扱えるため、 -この調整が要るのは Python だけである - -仕様: ``docs/spec/00-conventions.md`` 5章 +他の4言語(C# / Java / TypeScript / Rust)は既定のスタックで深さ512を扱えるため、 +この調整が要るのはPythonだけである +ただしRustのdebugビルドは1段あたりのスタック消費が大きく、注意が要る """ from __future__ import annotations @@ -20,10 +19,10 @@ __all__ = ["DEFAULT_MAX_DEPTH", "headroom", "guard"] #: ネスト深さの既定上限 -# 仕様が定める値 +#: 仕様が定める値 DEFAULT_MAX_DEPTH = 512 -#: NBT のネスト 1 段あたりに消費する Python のスタックフレーム数(余裕を見た値) +#: NBTのネスト1段あたりに消費するPythonのスタックフレーム数(余裕を見た値) _FRAMES_PER_LEVEL = 6 #: 再帰上限に上乗せする余白 @@ -32,7 +31,7 @@ @contextlib.contextmanager def headroom(max_depth: int): - """仕様上の深さ上限まで再帰できるよう、一時的に Python の再帰上限を引き上げる""" + """仕様上の深さ上限まで再帰できるよう、一時的にPythonの再帰上限を引き上げる""" required = (max_depth * _FRAMES_PER_LEVEL) + _RECURSION_MARGIN previous = sys.getrecursionlimit() @@ -48,7 +47,7 @@ def headroom(max_depth: int): @contextlib.contextmanager def guard(max_depth: int, message: str): - """:func:`headroom` に加えて、なお底を突いた場合を LIMIT_EXCEEDED へ写す""" + """:func:`headroom`に加えて、なお底を突いた場合をLIMIT_EXCEEDEDへ写す""" with headroom(max_depth): try: yield diff --git a/python/src/spring_nbt_library/nbt/canonical.py b/python/src/spring_nbt_library/nbt/canonical.py index 9c264bd..5bce38c 100644 --- a/python/src/spring_nbt_library/nbt/canonical.py +++ b/python/src/spring_nbt_library/nbt/canonical.py @@ -1,11 +1,9 @@ """浮動小数点の正準10進表記 -各言語の標準の数値書式(C# の ``"R"``、Java の ``Float.toString``、 -Python の ``repr``、Rust の ``{}``)は互いに一致しない -指数表記へ切り替わる閾値も、指数部の桁数も、``E`` の大文字小文字も処理系ごとに違う -そのままでは SNBT 出力の言語間一致が成立しないため、書式をここで固定する - -仕様: ``docs/spec/11-snbt.md`` 5.1章 +各言語の標準の数値書式(C#の``"R"``、Javaの``Float.toString``、 +Pythonの``repr``、Rustの``{}``)は互いに一致しない +指数表記へ切り替わる閾値も、指数部の桁数も、``E``の大文字小文字も処理系ごとに違う +そのままではSNBTの出力が言語間で一致しないため、書式をここで固定する """ from __future__ import annotations @@ -23,7 +21,7 @@ def _special(value: float): - """特殊値なら文字列を、そうでなければ None を返す""" + """特殊値なら文字列を、そうでなければNoneを返す""" if math.isnan(value): return "NaN" @@ -38,7 +36,7 @@ def _special(value: float): def from_float(value: float) -> str: - """binary32 を正準10進表記へ変換する""" + """binary32を正準10進表記へ変換する""" special = _special(value) if special is not None: @@ -46,19 +44,19 @@ def from_float(value: float) -> str: target = struct.pack(">f", value) - # 有効数字を 1 桁ずつ増やし、読み戻してビット一致する最短の表記を探す + # 有効数字を1桁ずつ増やし、読み戻したときにビットが一致する最短の表記を探す for precision in range(1, 10): candidate = "%.*e" % (precision - 1, value) if struct.pack(">f", float(candidate)) == target: return _format(candidate) - # 9 桁あれば binary32 は必ず往復するので、ここへは来ない + # 9桁あればbinary32は必ず往復するので、ここへは来ない return _format("%.8e" % value) def from_double(value: float) -> str: - """binary64 を正準10進表記へ変換する""" + """binary64を正準10進表記へ変換する""" special = _special(value) if special is not None: @@ -66,19 +64,19 @@ def from_double(value: float) -> str: target = struct.pack(">d", value) - # 有効数字を 1 桁ずつ増やし、読み戻してビット一致する最短の表記を探す + # 有効数字を1桁ずつ増やし、読み戻したときにビットが一致する最短の表記を探す for precision in range(1, 18): candidate = "%.*e" % (precision - 1, value) if struct.pack(">d", float(candidate)) == target: return _format(candidate) - # 17 桁あれば binary64 は必ず往復するので、ここへは来ない + # 17桁あればbinary64は必ず往復するので、ここへは来ない return _format("%.16e" % value) def _format(exponential: str) -> str: - """指数表記の文字列(例 ``"7.5e-01"``)から、仕様が定める正準表記を組み立てる""" + """指数表記の文字列(例``"7.5e-01"``)から、仕様が定める正準表記を組み立てる""" negative = False index = 0 @@ -107,7 +105,7 @@ def _format(exponential: str) -> str: def _trim_trailing_zeros(digits: str) -> str: """末尾のゼロを取り除く - すべてゼロなら "0" を残す + すべてゼロなら"0"を残す """ end = len(digits) @@ -125,7 +123,7 @@ def _compose(negative: bool, digits: str, exponent: int) -> str: else: sign = "" - # 値が 0 のときは指数に関わらず 0.0 と書く + # 値が0のときは指数に関わらず0.0と書く if digits == "0": return sign + "0.0" @@ -139,8 +137,8 @@ def _compose(negative: bool, digits: str, exponent: int) -> str: return "%s%s.%sE%d" % (sign, digits[0], fraction, exponent) if exponent >= 0: - # 整数部は先頭 (exponent + 1) 桁 - # 足りなければゼロで右詰めする + # 整数部は先頭 (exponent + 1)桁 + # 足りなければ数字の右側をゼロで埋める integer_digits = exponent + 1 # 整数部が数字の並びに収まるなら、そのまま切り出す @@ -157,5 +155,5 @@ def _compose(negative: bool, digits: str, exponent: int) -> str: return "%s%s.%s" % (sign, integer_part, fraction) - # 指数が負なら "0." に続けてゼロを詰めてから数字を置く + # 指数が負なら"0."に続けてゼロを詰めてから数字を置く return "%s0.%s%s" % (sign, "0" * ((-exponent) - 1), digits) diff --git a/python/src/spring_nbt_library/nbt/io.py b/python/src/spring_nbt_library/nbt/io.py index 221bb5b..d7682c4 100644 --- a/python/src/spring_nbt_library/nbt/io.py +++ b/python/src/spring_nbt_library/nbt/io.py @@ -1,6 +1,4 @@ -"""NBT のファイル・バイト列・ストリームからの読み書き - -仕様: ``docs/spec/10-nbt-binary.md`` 3章〜6章 +"""NBTのファイル・バイト列・ストリームからの読み書き """ from __future__ import annotations @@ -50,14 +48,14 @@ class NbtFormat(enum.Enum): - """NBT のルートタグの並び方""" + """NBTのルートタグの並び方""" #: ファイル形式 - # ルートは「タグID + 名前長 + 名前 + ペイロード」の順 + #: ルートは「タグID + 名前長 + 名前 + ペイロード」の順 JAVA = "java" - #: ネットワーク形式 (1.20.2 以降) - # ルートに名前が付かない + #: ネットワーク形式 (1.20.2以降) + #: ルートに名前が付かない NETWORK = "network" @@ -65,9 +63,7 @@ class NbtReadResult: """位置を指定した読み込みの結果 読んだタグと、その直後の位置を持つ - 続けて読むときは end を次の開始位置として渡す - - 仕様: docs/spec/10-nbt-binary.md 3.1章 + 続けて読むときはendを次の開始位置として渡す """ __slots__ = ("tag", "end") @@ -90,7 +86,7 @@ class Compression(enum.Enum): ZLIB = "zlib" #: 先頭バイトから自動判定する - # 読み込み時のみ指定できる + #: 読み込み時のみ指定できる AUTO = "auto" @@ -108,7 +104,7 @@ def __repr__(self) -> str: class NbtReadOptions: - """NBT 読み込みのオプション""" + """NBT読み込みのオプション""" def __init__(self, fmt: NbtFormat = NbtFormat.JAVA, compression: Compression = Compression.AUTO, @@ -117,24 +113,24 @@ def __init__(self, fmt: NbtFormat = NbtFormat.JAVA, #: ルートタグの並び方 self.format = fmt #: 圧縮方式 - # 既定は自動判定 + #: 既定は自動判定 self.compression = compression #: ネストの深さ上限 self.max_depth = max_depth #: 展開後の総バイト数の上限 - # 負値なら無制限 + #: 負値なら無制限 self.max_decompressed_size = max_decompressed_size class NbtWriteOptions: - """NBT 書き込みのオプション""" + """NBT書き込みのオプション""" def __init__(self, fmt: NbtFormat = NbtFormat.JAVA, compression: Compression = Compression.GZIP) -> None: #: ルートタグの並び方 self.format = fmt #: 圧縮方式 - # 既定は GZip + #: 既定はGZip self.compression = compression @staticmethod @@ -154,7 +150,7 @@ def uncompressed() -> "NbtWriteOptions": class _Reader: - """展開済みのバイト列から NBT を読み出す + """展開済みのバイト列からNBTを読み出す 入力全体をあらかじめメモリに持つ設計にしている 「宣言された長さが残り入力長を超えていないか」を確保前に検査できるようにするため @@ -191,13 +187,13 @@ def read_root(self, fmt: NbtFormat) -> NamedTag: return tag def read_root_tag(self, fmt: NbtFormat) -> NamedTag: - """ルートタグを 1 つ読む + """ルートタグを1つ読む 形式によって名前の有無が変わる 末尾の余りは見ない """ tag_type = TagType.from_id(self._read_byte()) - # Java版のファイル形式でもネットワーク形式でも、ルートは必ず TAG_Compound + # Java版のファイル形式でもネットワーク形式でも、ルートは必ずTAG_Compound if tag_type != TagType.COMPOUND: raise SpringNbtError.malformed( "ルートタグは compound でなければならないが %s だった" % tag_type.as_string()) @@ -213,7 +209,7 @@ def read_root_tag(self, fmt: NbtFormat) -> NamedTag: return NamedTag(name, root) def _read_payload(self, tag_type: TagType, depth: int) -> NbtTag: - # 深さ上限は再帰する型に入る手前で検査する + # 深さ上限は型を問わず、ペイロードを読む前に検査する if depth > self._max_depth: raise SpringNbtError.limit_exceeded("ネストが深すぎる (上限 %d)" % self._max_depth) @@ -230,7 +226,8 @@ def _read_payload(self, tag_type: TagType, depth: int) -> NbtTag: return NbtLong(struct.unpack(">q", self._take(8))[0]) if tag_type == TagType.FLOAT: - return NbtFloat(struct.unpack(">f", self._take(4))[0]) + # NaNのビットパターンを保つため、読んだビットをそのまま渡す + return NbtFloat._from_bits(self._take(4)) if tag_type == TagType.DOUBLE: return NbtDouble(struct.unpack(">d", self._take(8))[0]) @@ -258,7 +255,7 @@ def _read_payload(self, tag_type: TagType, depth: int) -> NbtTag: def _read_compound_payload(self, depth: int) -> NbtCompound: compound = NbtCompound() - # TAG_End が現れるまで名前付きタグを読み続ける + # TAG_Endが現れるまで名前付きタグを読み続ける while True: tag_type = TagType.from_id(self._read_byte()) @@ -273,13 +270,13 @@ def _read_list_payload(self, depth: int) -> NbtList: count = self._read_length() if element_type == TagType.END: - # 要素型 End のリストは空でなければならない + # 要素型Endのリストは空でなければならない if count != 0: raise SpringNbtError.malformed("要素型 End のリストに %d 個の要素が宣言されている" % count) return NbtList(TagType.END) - # 1 要素の最小バイト数から、宣言された個数が入力に収まるかを先に検査する + # 1要素の最小バイト数から、宣言された個数が入力に収まるかを先に検査する self._ensure_available(count * _minimum_payload_size(element_type)) elements = [] @@ -314,7 +311,7 @@ def _read_string(self) -> str: def _read_length(self) -> int: length = struct.unpack(">i", self._take(4))[0] - # 長さは i32 だが、負値は仕様上ありえない + # 長さはi32だが、負値は仕様上ありえない if length < 0: raise SpringNbtError.malformed("長さが負値: %d" % length) @@ -344,19 +341,19 @@ def _ensure_available(self, required: int) -> None: def _require_utf8_representable(text: str, role: str) -> str: """キーやルート名として使える文字列か検査する - 値と違い、キーには孤立サロゲートを許さない(仕様 10 の 2.2章) - Minecraft が書き出すキーは ASCII の識別子のみで、 + 値と違い、キーには孤立サロゲートを許さない + Minecraftが書き出すキーはASCIIの識別子のみで、 孤立サロゲートが現れるのはデータ破損を意味する """ # 対にならないサロゲートが含まれていないか調べる index = 0 - # コード単位を 1 つずつ見て、サロゲート対をまとめる + # コード単位を1つずつ見て、サロゲート対をまとめる while index < len(text): code = ord(text[index]) - # 上位サロゲートは、対になる下位サロゲートとまとめて 1 文字を成す + # 上位サロゲートは、対になる下位サロゲートとまとめて1文字を成す if 0xD800 <= code <= 0xDBFF: - # 対が揃っていれば 2 コード単位を消費する + # 対が揃っていれば2コード単位を消費する # 揃わなければ孤立サロゲート if index + 1 < len(text) and 0xDC00 <= ord(text[index + 1]) <= 0xDFFF: index += 2 @@ -386,15 +383,15 @@ def _minimum_payload_size(tag_type: TagType) -> int: TagType.FLOAT: 4, TagType.LONG: 8, TagType.DOUBLE: 8, - # 長さフィールドの 4 バイトは必ずある + # 長さフィールドの4バイトは必ずある TagType.BYTE_ARRAY: 4, TagType.INT_ARRAY: 4, TagType.LONG_ARRAY: 4, - # 長さフィールドの 2 バイトは必ずある + # 長さフィールドの2バイトは必ずある TagType.STRING: 2, - # 要素型 1 バイト + 個数 4 バイト + # 要素型1バイト + 個数4バイト TagType.LIST: 5, - # 終端の TAG_End 1 バイトは必ずある + # 終端のTAG_End 1バイトは必ずある TagType.COMPOUND: 1, } @@ -405,17 +402,17 @@ def _minimum_payload_size(tag_type: TagType) -> int: class _Writer: - """NBT を展開済みのバイト列へ書き出す + """NBTを展開済みのバイト列へ書き出す 出力は一意でなければならない(ラウンドトリップ検証が成立するため) - Compound は挿入順のまま、浮動小数点はビットパターンのまま書き出す + Compoundは挿入順のまま、浮動小数点はビットパターンのまま書き出す """ def __init__(self) -> None: self._buffer = bytearray() def write_root(self, named: NamedTag, fmt: NbtFormat) -> bytes: - """ルートタグを 1 つ書き出す + """ルートタグを1つ書き出す 形式によって名前の有無が変わる """ self._buffer.append(TagType.COMPOUND.value) @@ -438,7 +435,8 @@ def _write_payload(self, tag: NbtTag) -> None: elif isinstance(tag, NbtLong): self._buffer += struct.pack(">q", tag.value) elif isinstance(tag, NbtFloat): - self._buffer += struct.pack(">f", tag.value) + # NaNや-0.0を保つため、値ではなくビットパターンを書く + self._buffer += tag._to_bits() elif isinstance(tag, NbtDouble): self._buffer += struct.pack(">d", tag.value) elif isinstance(tag, NbtByteArray): @@ -483,8 +481,8 @@ def _write_number_array(self, values, fmt: str) -> None: def _write_string(self, text: str) -> None: encoded = mutf8.encode(text) - # 長さフィールドは u16 - # キー名は素の str なのでここでも検査する + # 長さフィールドはu16 + # キー名は素のstrなのでここでも検査する if len(encoded) > mutf8.MAX_BYTE_LENGTH: raise SpringNbtError.invalid_argument( "文字列が長すぎる: MUTF-8 で %d バイト (上限 %d)" @@ -507,19 +505,19 @@ def detect_compression(data: bytes) -> Compression: if len(data) == 0: raise SpringNbtError.malformed("入力が空で圧縮方式を判定できない") - # GZip は必ず 1F 8B で始まる + # GZipは必ず1F 8Bで始まる if len(data) >= 2 and data[0] == 0x1F and data[1] == 0x8B: return Compression.GZIP if len(data) >= 2: - # zlib ヘッダは「圧縮法が 8 (deflate)」かつ「先頭2バイトが 31 の倍数」 + # zlibヘッダは「圧縮法が8 (deflate)」かつ「先頭2バイトが31の倍数」 is_deflate = (data[0] & 0x0F) == 0x08 header = (data[0] << 8) | data[1] if is_deflate and header % 31 == 0: return Compression.ZLIB - # 無圧縮なら先頭は TAG_Compound のタグID + # 無圧縮なら先頭はTAG_CompoundのタグID if data[0] == TagType.COMPOUND.value: return Compression.NONE @@ -527,19 +525,20 @@ def detect_compression(data: bytes) -> Compression: def _decompress(data: bytes, options: NbtReadOptions) -> bytes: - # AUTO なら先頭バイトから圧縮方式を見分ける + # AUTOなら先頭バイトから圧縮方式を見分ける if options.compression == Compression.AUTO: method = detect_compression(data) else: method = options.compression - # 方式ごとに圧縮する + # 方式ごとに展開する if method == Compression.NONE: plain = data elif method == Compression.GZIP: try: plain = gzip.decompress(data) - except OSError as error: + except (OSError, EOFError, zlib.error) as error: + # GZipでないデータ、途中で切れたデータ、中身の壊れたデータのどれも、仕様に反するデータとして扱う raise SpringNbtError(ErrorCode.MALFORMED_DATA, "GZip データを展開できない") from error elif method == Compression.ZLIB: try: @@ -562,7 +561,7 @@ def _compress(plain: bytes, method: Compression) -> bytes: return plain if method == Compression.GZIP: - # mtime を 0 に固定して、同じ入力から同じバイト列が出るようにする + # mtimeを0に固定して、同じ入力から同じバイト列が出るようにする return gzip.compress(plain, mtime=0) if method == Compression.ZLIB: @@ -572,12 +571,12 @@ def _compress(plain: bytes, method: Compression) -> bytes: # --------------------------------------------------------------------------- -# 公開 API +# 公開API # --------------------------------------------------------------------------- def read_bytes(data: bytes, options: Optional[NbtReadOptions] = None) -> NamedTag: - """バイト列から NBT を読む""" + """バイト列からNBTを読む""" if options is None: effective = _DEFAULT_READ else: @@ -585,17 +584,17 @@ def read_bytes(data: bytes, options: Optional[NbtReadOptions] = None) -> NamedTa reader = _Reader(_decompress(data, effective), effective.max_depth) - # Python は既定の再帰上限が仕様の深さ上限に届かないため、ここで引き上げる + # Pythonは既定の再帰上限が仕様の深さ上限に届かないため、ここで引き上げる with _recursion.guard(effective.max_depth, "ネストが深すぎて読み込めない"): return reader.read_root(effective.format) def read_bytes_at(data: bytes, offset: int, options: Optional[NbtReadOptions] = None) -> "NbtReadResult": - """バイト列の指定した位置から NBT を 1 つ読む + """バイト列の指定した位置からNBTを1つ読む - 複数の NBT が連なっているデータを、先頭から順に読み進めるために使う - 戻り値の end が次の開始位置になる + 複数のNBTが連なっているデータを、先頭から順に読み進めるために使う + 戻り値のendが次の開始位置になる 位置は渡したバイト列そのものを指すので、圧縮されたデータは扱えない @@ -610,7 +609,7 @@ def read_bytes_at(data: bytes, offset: int, reader = _Reader(data, effective.max_depth, offset) - # Python は既定の再帰上限が仕様の深さ上限に届かないため、ここで引き上げる + # Pythonは既定の再帰上限が仕様の深さ上限に届かないため、ここで引き上げる with _recursion.guard(effective.max_depth, "ネストが深すぎて読み込めない"): tag = reader.read_root_tag(effective.format) @@ -619,25 +618,25 @@ def read_bytes_at(data: bytes, offset: int, def read_bytes_all(data: bytes, options: Optional[NbtReadOptions] = None) -> List[NamedTag]: - """バイト列に連なっている NBT をすべて読む + """バイト列に連なっているNBTをすべて読む 入力を使い切るまで読み続ける 空のバイト列なら空の一覧を返す - 圧縮は入力全体に 1 回かかっているものとして扱う + 圧縮は入力全体に1回かかっているものとして扱う :raises SpringNbtError: 読み込みに失敗した場合 """ effective = _effective_read_options(options) tags: List[NamedTag] = [] - # 空の入力は「0 個」であってエラーではない + # 空の入力は「0個」であってエラーではない if len(data) == 0: return tags reader = _Reader(_decompress(data, effective), effective.max_depth) - # Python は既定の再帰上限が仕様の深さ上限に届かないため、ここで引き上げる + # Pythonは既定の再帰上限が仕様の深さ上限に届かないため、ここで引き上げる with _recursion.guard(effective.max_depth, "ネストが深すぎて読み込めない"): # 入力を使い切るまでルートタグを読み続ける while reader.has_more: @@ -662,7 +661,7 @@ def _require_plain_input(options: NbtReadOptions) -> None: def read_file(path: str, options: Optional[NbtReadOptions] = None) -> NamedTag: - """ファイルから NBT を読む""" + """ファイルからNBTを読む""" try: with open(path, "rb") as handle: data = handle.read() @@ -673,7 +672,7 @@ def read_file(path: str, options: Optional[NbtReadOptions] = None) -> NamedTag: def read_stream(stream, options: Optional[NbtReadOptions] = None) -> NamedTag: - """ストリームから NBT を読む + """ストリームからNBTを読む ストリームは最後まで読み切る """ try: @@ -685,13 +684,13 @@ def read_stream(stream, options: Optional[NbtReadOptions] = None) -> NamedTag: def write_bytes(named: NamedTag, options: Optional[NbtWriteOptions] = None) -> bytes: - """NBT をバイト列へ書き出す""" + """NBTをバイト列へ書き出す""" if options is None: effective = _DEFAULT_WRITE else: effective = options - # 書き込み時に Auto は決められない + # 書き込み時にAutoは決められない if effective.compression == Compression.AUTO: raise SpringNbtError.invalid_argument("書き込みで Compression.AUTO は指定できない") @@ -705,7 +704,7 @@ def write_bytes(named: NamedTag, options: Optional[NbtWriteOptions] = None) -> b def write_file(path: str, named: NamedTag, options: Optional[NbtWriteOptions] = None) -> None: - """NBT をファイルへ書き出す""" + """NBTをファイルへ書き出す""" data = write_bytes(named, options) try: @@ -716,7 +715,7 @@ def write_file(path: str, named: NamedTag, options: Optional[NbtWriteOptions] = def write_stream(stream, named: NamedTag, options: Optional[NbtWriteOptions] = None) -> None: - """NBT をストリームへ書き出す""" + """NBTをストリームへ書き出す""" data = write_bytes(named, options) try: diff --git a/python/src/spring_nbt_library/nbt/mutf8.py b/python/src/spring_nbt_library/nbt/mutf8.py index 7d842be..a64fc43 100644 --- a/python/src/spring_nbt_library/nbt/mutf8.py +++ b/python/src/spring_nbt_library/nbt/mutf8.py @@ -1,15 +1,12 @@ -"""Modified UTF-8 (MUTF-8) の符号化・復号 +"""Modified UTF-8 (MUTF-8)の符号化・復号 -標準 UTF-8 との違いは 2 点だけ +標準UTF-8との違いは2点だけ -* ``U+0000`` を ``C0 80`` の 2 バイトで表す -* ``U+10000`` 以上をサロゲートペアへ分解し、3 バイト × 2 で表す (CESU-8) +* ``U+0000``を``C0 80``の2バイトで表す +* ``U+10000``以上をサロゲートペアへ分解し、3バイト × 2で表す (CESU-8) -Python の :class:`str` はコードポイント単位だが、 -``surrogatepass`` 相当の扱いで孤立サロゲートも保持できるため、 -C# / Java と同じく文字列としてそのまま往復できる - -仕様: ``docs/spec/10-nbt-binary.md`` 2章 +Pythonの:class:`str`はコードポイント単位だが、``surrogatepass``相当の扱いで孤立サロゲートも保持できる +そのため、C# / Javaと同じく、MUTF-8のバイト列を文字列へ復号しても同じバイト列へ戻せる """ from __future__ import annotations @@ -18,19 +15,19 @@ __all__ = ["MAX_BYTE_LENGTH", "decode", "encode", "byte_length"] -#: MUTF-8 の文字列が取りうる最大バイト長(長さフィールドが u16 のため) +#: MUTF-8の文字列が取りうる最大バイト長(長さフィールドがu16のため) MAX_BYTE_LENGTH = 65535 def _to_utf16_units(text: str): - """文字列を UTF-16 コード単位の列へ落とす""" + """文字列をUTF-16コード単位の列へ落とす""" units = [] # コードポイントごとに、補助文字ならサロゲートペアへ分解する for character in text: code = ord(character) - # BMP 外の文字は、UTF-16 のサロゲート対へ分解して持つ + # BMP外の文字は、UTF-16のサロゲート対へ分解して持つ if code >= 0x10000: code -= 0x10000 units.append(0xD800 + (code >> 10)) @@ -42,7 +39,7 @@ def _to_utf16_units(text: str): def _from_utf16_units(units) -> str: - """UTF-16 コード単位の列を文字列へ戻す + """UTF-16コード単位の列を文字列へ戻す 孤立サロゲートはそのまま保持する """ result = [] @@ -52,7 +49,7 @@ def _from_utf16_units(units) -> str: while index < len(units): unit = units[index] - # サロゲート対が揃っていれば 1 つのコードポイントへ戻す + # サロゲート対が揃っていれば1つのコードポイントへ戻す if 0xD800 <= unit <= 0xDBFF and index + 1 < len(units) and 0xDC00 <= units[index + 1] <= 0xDFFF: low = units[index + 1] code = 0x10000 + ((unit - 0xD800) << 10) + (low - 0xDC00) @@ -66,27 +63,27 @@ def _from_utf16_units(units) -> str: def decode(data: bytes) -> str: - """MUTF-8 バイト列を文字列へ復号する + """MUTF-8バイト列を文字列へ復号する - :raises SpringNbtError: バイト列が MUTF-8 として不正な場合 + :raises SpringNbtError: バイト列がMUTF-8として不正な場合 """ units = [] index = 0 - # 先頭から 1 文字ずつ取り出す + # 先頭から1文字ずつ取り出す while index < len(data): b0 = data[index] if b0 & 0x80 == 0x00: - # 1 バイト形式: 0xxxxxxx (U+0001..U+007F) + # 1バイト形式: 0xxxxxxx (U+0001..U+007F) if b0 == 0x00: - # 素の 0x00 は MUTF-8 では現れてはならない (C0 80 を使う) + # 素の0x00はMUTF-8では現れてはならない (C0 80を使う) raise SpringNbtError.malformed("MUTF-8: 素の 0x00 が現れた (U+0000 は C0 80 で表す)") units.append(b0) index += 1 elif b0 & 0xE0 == 0xC0: - # 2 バイト形式: 110xxxxx 10xxxxxx + # 2バイト形式: 110xxxxx 10xxxxxx if index + 1 >= len(data): raise SpringNbtError.malformed("MUTF-8: 2バイト形式が途中で切れた") @@ -97,15 +94,15 @@ def decode(data: bytes) -> str: value = ((b0 & 0x1F) << 6) | (b1 & 0x3F) - # C0 80 (U+0000) だけは正当 - # それ以外の 0x80 未満は冗長符号化 + # C0 80 (U+0000)だけは正当 + # それ以外の0x80未満は冗長符号化 if value < 0x80 and not (b0 == 0xC0 and b1 == 0x80): raise SpringNbtError.malformed("MUTF-8: 冗長な2バイト符号化") units.append(value) index += 2 elif b0 & 0xF0 == 0xE0: - # 3 バイト形式: 1110xxxx 10xxxxxx 10xxxxxx + # 3バイト形式: 1110xxxx 10xxxxxx 10xxxxxx if index + 2 >= len(data): raise SpringNbtError.malformed("MUTF-8: 3バイト形式が途中で切れた") @@ -117,35 +114,34 @@ def decode(data: bytes) -> str: value = ((b0 & 0x0F) << 12) | ((b1 & 0x3F) << 6) | (b2 & 0x3F) - # 3 バイトで表すべき範囲は U+0800 以上 + # 3バイトで表すべき範囲はU+0800以上 if value < 0x800: raise SpringNbtError.malformed("MUTF-8: 冗長な3バイト符号化") units.append(value) index += 3 else: - # 4 バイト形式 (標準 UTF-8) や継続バイト単独は MUTF-8 では不正 + # 4バイト形式 (標準UTF-8)や継続バイト単独はMUTF-8では不正 raise SpringNbtError.malformed("MUTF-8: 不正な先頭バイト 0x%02X" % b0) return _from_utf16_units(units) def encode(text: str) -> bytes: - """文字列を MUTF-8 バイト列へ符号化する + """文字列をMUTF-8バイト列へ符号化する - サロゲートは対になっているかどうかに関わらず 1 つずつ 3 バイトで符号化されるため、 - 孤立サロゲートもそのまま往復できる + サロゲートは対になっているかどうかに関わらず1つずつ3バイトで符号化されるため、孤立サロゲートもそのまま往復できる """ out = bytearray() - # コード単位ごとに 1〜3 バイトへ展開する + # コード単位ごとに1〜3バイトへ展開する for unit in _to_utf16_units(text): - # U+0001..U+007F だけが 1 バイト - # U+0000 は 2 バイトになる + # U+0001..U+007Fだけが1バイト + # U+0000は2バイトになる if 0x0001 <= unit <= 0x007F: out.append(unit) elif unit == 0x0000 or unit <= 0x07FF: - # U+0000 もこの経路で C0 80 になる + # U+0000もこの経路でC0 80になる out.append(0xC0 | ((unit >> 6) & 0x1F)) out.append(0x80 | (unit & 0x3F)) else: @@ -157,15 +153,15 @@ def encode(text: str) -> bytes: def byte_length(text: str) -> int: - """文字列を MUTF-8 で符号化したときのバイト長を求める + """文字列をMUTF-8で符号化したときのバイト長を求める 実際に符号化はしない """ length = 0 # 各コード単位が何バイトになるかを数える for unit in _to_utf16_units(text): - # U+0001..U+007F だけが 1 バイト - # U+0000 は 2 バイトになる + # U+0001..U+007Fだけが1バイト + # U+0000は2バイトになる if 0x0001 <= unit <= 0x007F: length += 1 elif unit == 0x0000 or unit <= 0x07FF: diff --git a/python/src/spring_nbt_library/nbt/snbt.py b/python/src/spring_nbt_library/nbt/snbt.py index 4cdb4f2..eda81ca 100644 --- a/python/src/spring_nbt_library/nbt/snbt.py +++ b/python/src/spring_nbt_library/nbt/snbt.py @@ -1,9 +1,7 @@ -"""SNBT (Stringified NBT) のパースと出力 +"""SNBT (Stringified NBT)のパースと出力 -対応範囲は「バイナリ NBT へ損失なく写せる部分集合」 -1.21.5 以降の異種リスト(``[1, "a"]``)は受理しない - -仕様: ``docs/spec/11-snbt.md`` / ``docs/adr/0006-snbt-scope.md`` +対応範囲は「バイナリNBTへ損失なく写せる部分集合」 +1.21.5以降の異種リスト(``[1, "a"]``)は受理しない """ from __future__ import annotations @@ -28,6 +26,8 @@ NbtString, NbtTag, TagType, + _has_lone_surrogate, + _join_surrogate_pairs, ) __all__ = ["parse", "parse_compound", "write", "write_pretty"] @@ -76,7 +76,7 @@ def __init__(self, text: str) -> None: self._position = 0 def parse_whole(self) -> NbtTag: - """入力全体を 1 つのタグとして読む + """入力全体を1つのタグとして読む 末尾に余りがあればエラーにする """ value = self._parse_value() @@ -112,12 +112,12 @@ def _parse_compound(self) -> NbtCompound: compound = NbtCompound() self._skip_whitespace() - # 空の Compound + # 空のCompound if self._peek() == "}": self._position += 1 return compound - # 要素を 1 つずつ読む + # 要素を1つずつ読む while True: self._skip_whitespace() @@ -147,12 +147,12 @@ def _parse_compound(self) -> NbtCompound: def _parse_list_or_array(self) -> NbtTag: self._expect("[") - # "[B;" のような型付き配列かどうかを先に判定する + # "[B;"のような型付き配列かどうかを先に判定する if self._position + 1 < len(self._text) and self._text[self._position + 1] == ";": marker = self._text[self._position] - # [B; [I; [L; は型付き配列の印 - # ただの List と見分ける + # [B; [I; [L;は型付き配列の印 + # ただのListと見分ける if marker in "BIL": self._position += 2 return self._parse_typed_array(marker) @@ -168,7 +168,7 @@ def _parse_list(self) -> NbtList: self._position += 1 return result - # 要素を 1 つずつ読む + # 要素を1つずつ読む while True: self._skip_whitespace() @@ -179,7 +179,7 @@ def _parse_list(self) -> NbtList: value = self._parse_value() - # 異種リストはバイナリ NBT へ写せないため受理しない (adr/0006) + # 異種リストはバイナリNBTへ写せないため受理しない if result.element_type != TagType.END and result.element_type != value.type: raise self._malformed( "リストに異なる型が混在している: %s と %s" @@ -232,13 +232,13 @@ def _parse_typed_array(self, marker: str) -> NbtTag: else: raise self._malformed("配列の区切りが不正: '%s'" % following) - # 各要素が対象の幅に収まるか確認しながら詰める + # 各要素が対象の幅に収まるか確認してから詰める if marker == "B": self._check_elements(values, -128, 127, "ByteArray") return NbtByteArray(values) - # [I; は TAG_Int_Array - # 残りは TAG_Long_Array + # [I;はTAG_Int_Array + # 残りはTAG_Long_Array if marker == "I": self._check_elements(values, -2147483648, 2147483647, "IntArray") return NbtIntArray(values) @@ -247,7 +247,7 @@ def _parse_typed_array(self, marker: str) -> NbtTag: return NbtLongArray(values) def _check_elements(self, values, minimum: int, maximum: int, name: str) -> None: - # 範囲を確かめながら詰め直す + # 各要素が範囲に収まるか確かめる for value in values: if value < minimum or value > maximum: raise self._malformed("%s の要素が範囲外: %d" % (name, value)) @@ -264,8 +264,15 @@ def _to_integral(self, tag: NbtTag) -> int: def _parse_key(self) -> str: character = self._peek() + # 引用符で始まるキーは、エスケープを解いて読む if character in "\"'": - return self._parse_quoted_string() + quoted = _join_surrogate_pairs(self._parse_quoted_string()) + + # バイナリの読み込みと同じく、キーには孤立サロゲートを許さない + if _has_lone_surrogate(quoted): + raise self._malformed("Compound のキーが UTF-8 に写せない(孤立サロゲートを含む)") + + return quoted bare = self._read_bare_token() @@ -315,11 +322,11 @@ def _read_escape(self) -> str: if character == "u": return chr(self._read_hex_digits(4)) - # \U は 8 桁のコードポイント表記 + # \Uは8桁のコードポイント表記 if character == "U": code_point = self._read_hex_digits(8) - # Unicode のコードポイント範囲を外れていないか確認する + # Unicodeのコードポイント範囲を外れていないか確認する if code_point > 0x10FFFF: raise self._malformed("コードポイントが範囲外: U+%X" % code_point) @@ -345,7 +352,7 @@ def _read_hex_digits(self, count: int) -> int: return int(chunk, 16) def _read_named_character(self) -> str: - """Unicode 文字名によるエスケープ ``\\N{...}`` を読む""" + """Unicode文字名によるエスケープ``\\N{...}``を読む""" self._expect("{") start = self._position @@ -359,7 +366,7 @@ def _read_named_character(self) -> str: name = self._text[start:self._position] self._position += 1 - # 実装間で Unicode 文字名の表が揃わないため対応しない(C# / Rust には表が無い) + # 実装間でUnicode文字名の表が揃わないため対応しない(C# / TypeScript / Rustには表が無い) raise SpringNbtError( ErrorCode.UNSUPPORTED_FEATURE, "文字名によるエスケープには対応していない: \\N{%s}" % name) @@ -370,7 +377,7 @@ def _parse_unquoted(self) -> NbtTag: if len(token) == 0: raise self._malformed("値が来るべき位置に解釈できない文字がある: '%s'" % self._peek_or_empty()) - # bool(...) / uuid(...) の関数呼び出し + # bool(...) / uuid(...)の関数呼び出し self._skip_whitespace() if self._peek_or_empty() == "(" and token in ("bool", "uuid"): return self._parse_function(token) @@ -395,7 +402,7 @@ def _parse_function(self, name: str) -> NbtTag: self._expect(")") if name == "bool": - # 0 以外を真とする + # 0以外を真とする if self._to_integral(argument) != 0: return NbtByte(1) @@ -412,11 +419,11 @@ def _uuid_to_int_array(self, argument: NbtTag) -> NbtIntArray: except ValueError as error: raise self._malformed("UUID として解釈できない: %s" % argument.value) from error - # UUID を上位から 32bit ずつ 4 要素の IntArray へ写す + # UUIDを上位から32bitずつ4要素のIntArrayへ写す raw = parsed.bytes values = [] - # UUID の 16 バイトを、4 バイトずつ 4 つの i32 へ詰める + # UUIDの16バイトを、4バイトずつ4つのi32へ詰める for index in range(4): chunk = int.from_bytes(raw[index * 4:(index * 4) + 4], "big", signed=True) values.append(chunk) @@ -425,7 +432,7 @@ def _uuid_to_int_array(self, argument: NbtTag) -> NbtIntArray: def _try_parse_number(self, token: str) -> Optional[NbtTag]: """数値トークンを解釈する - 数値として読めなければ None(文字列として扱われる) + 数値として読めなければNoneを返す(文字列として扱われる) """ negative = False start = 0 @@ -446,27 +453,26 @@ def _try_parse_number(self, token: str) -> Optional[NbtTag]: is_hex = _is_hex_body(body) # 幅接尾辞を末尾から剥がす - # 16進では b/d/f が数字と紛れるため s/l だけを認める last = body[-1] - # 16 進では b/d/f が数字なので、型の印として使えるのは s と l だけ + # 16進ではb/d/fが数字と紛れるためs/lだけを認める if is_hex: suffix_allowed = last in "sSlL" else: suffix_allowed = last in _WIDTH_SUFFIXES - # 末尾 1 文字が型の印なら切り離す - # 1 文字だけの token は数字そのもの + # 末尾1文字が型の印なら切り離す + # 符号を除いた本体が1文字だけなら、接尾辞とみなさず剥がさない if suffix_allowed and len(body) >= 2: width_suffix = last.lower() body = body[:-1] - # 符号接尾辞 u / s は幅接尾辞の手前に置かれる + # 符号接尾辞u / sは幅接尾辞の手前に置かれる if len(body) >= 2: sign_char = body[-1] - # u / U は符号なしの印 - # 1.21.5 以降の拡張構文 + # u / Uは符号なしの印 + # 1.21.5以降の拡張構文 if sign_char in "uU": unsigned_suffix = True body = body[:-1] @@ -493,7 +499,7 @@ def _try_parse_number(self, token: str) -> Optional[NbtTag]: looks_floating = "." in body or "e" in body or "E" in body - # 小数点・指数・f/d の印があれば浮動小数点として読む + # 小数点・指数・f/dの印があれば浮動小数点として読む if looks_floating or width_suffix in ("f", "d"): try: parsed = float(body) @@ -514,7 +520,7 @@ def _make_floating(self, value: float, negative: bool, width_suffix: str) -> Nbt if width_suffix == "f": return NbtFloat(signed) - # 接尾辞なしの小数は Double + # 接尾辞なしの小数はDouble if width_suffix in ("", "d"): return NbtDouble(signed) @@ -578,8 +584,8 @@ def _make_integral(self, magnitude: int, negative: bool, if width_suffix == "d": return NbtDouble(float(value)) - # 接尾辞なしの整数は Int - # 暗黙に Long へ格上げしない + # 接尾辞なしの整数はInt + # 暗黙にLongへ格上げしない return NbtInt(self._check_range(value, -2147483648, 2147483647, "int")) def _check_unsigned(self, magnitude: int, maximum: int) -> int: @@ -647,7 +653,7 @@ def _is_binary_body(body: str) -> bool: if not (len(body) > 2 and body[0] == "0" and body[1] in "bB"): return False - # 2進リテラルの本体は 0 と 1 だけ + # 2進リテラルの本体は0と1だけ for character in body[2:]: if character not in "01": return False @@ -678,20 +684,15 @@ def _wrap_unsigned(magnitude: int, bits: int) -> int: return magnitude -# --------------------------------------------------------------------------- -# ライタ -# --------------------------------------------------------------------------- - - def parse(text: str) -> NbtTag: - """SNBT 文字列をタグへ変換する""" - # Python は既定の再帰上限が仕様の深さ上限に届かないため、ここで引き上げる + """SNBT文字列をタグへ変換する""" + # Pythonは既定の再帰上限が仕様の深さ上限に届かないため、ここで引き上げる with _recursion.guard(_recursion.DEFAULT_MAX_DEPTH, "SNBT のネストが深すぎる"): return _Parser(text).parse_whole() def parse_compound(text: str) -> NbtCompound: - """SNBT 文字列を Compound へ変換する""" + """SNBT文字列をCompoundへ変換する""" tag = parse(text) if isinstance(tag, NbtCompound): @@ -700,8 +701,13 @@ def parse_compound(text: str) -> NbtCompound: raise SpringNbtError.unexpected_tag_type("ルートが compound でない: %s" % tag.type.as_string()) +# --------------------------------------------------------------------------- +# ライタ +# --------------------------------------------------------------------------- + + def write(tag: NbtTag) -> str: - """タグを 1 行の SNBT へ変換する""" + """タグを1行のSNBTへ変換する""" parts = [] with _recursion.guard(_recursion.DEFAULT_MAX_DEPTH, "SNBT のネストが深すぎる"): @@ -711,8 +717,8 @@ def write(tag: NbtTag) -> str: def write_pretty(tag: NbtTag) -> str: - """タグを整形した SNBT へ変換する - インデントは空白 4 個 + """タグを整形したSNBTへ変換する + インデントは空白4個 """ parts = [] @@ -724,7 +730,7 @@ def write_pretty(tag: NbtTag) -> str: def _write_tag(parts, tag: NbtTag, depth: int) -> None: """タグを書き出す - ``depth`` が負なら 1 行、0 以上なら整形して出力する + ``depth``が負なら1行、0以上なら整形して出力する """ if isinstance(tag, NbtByte): parts.append("%db" % tag.value) @@ -755,7 +761,7 @@ def _write_tag(parts, tag: NbtTag, depth: int) -> None: def _write_compound(parts, compound: NbtCompound, depth: int) -> None: - # 空の compound は改行もインデントも入れず {} と書く + # 空のcompoundは改行もインデントも入れず{}と書く if len(compound) == 0: parts.append("{}") return @@ -765,7 +771,7 @@ def _write_compound(parts, compound: NbtCompound, depth: int) -> None: # 挿入順のまま「キー: 値」を並べる for key, value in compound.items(): - # 2 つ目以降の前に区切りのカンマを置く + # 2つ目以降の前に区切りのカンマを置く if not first: parts.append(",") @@ -785,7 +791,7 @@ def _write_compound(parts, compound: NbtCompound, depth: int) -> None: def _write_list(parts, value: NbtList, depth: int) -> None: - # 空のリストは改行もインデントも入れず [] と書く + # 空のリストは改行もインデントも入れず[]と書く if len(value) == 0: parts.append("[]") return @@ -795,7 +801,7 @@ def _write_list(parts, value: NbtList, depth: int) -> None: # 要素型は共通なので値だけを並べる for item in value: - # 2 つ目以降の前に区切りのカンマを置く + # 2つ目以降の前に区切りのカンマを置く if not first: parts.append(",") @@ -810,9 +816,9 @@ def _write_list(parts, value: NbtList, depth: int) -> None: def _write_typed_array(parts, marker: str, values, element_suffix: str) -> None: parts.append("[%s;" % marker) - # 型付き配列は 1 行に収める + # 型付き配列は1行に収める for index, value in enumerate(values): - # 2 つ目以降の前に区切りのカンマを置く + # 2つ目以降の前に区切りのカンマを置く if index > 0: parts.append(",") @@ -822,7 +828,7 @@ def _write_typed_array(parts, marker: str, values, element_suffix: str) -> None: def _append_separator(parts, depth: int) -> None: - """整形出力なら改行とインデントを、1 行出力なら何も入れない""" + """整形出力なら改行とインデントを、1行出力なら何も入れない""" if depth < 0: return @@ -831,7 +837,7 @@ def _append_separator(parts, depth: int) -> None: def _next_depth(depth: int) -> int: - """整形出力のときだけ深さを 1 段進める""" + """整形出力のときだけ深さを1段進める""" if depth < 0: return -1 @@ -875,16 +881,16 @@ def _quote_string(text: str) -> str: """文字列を二重引用符で囲み、必要な文字だけエスケープする""" parts = ['"'] - # 1 文字ずつ見てエスケープが要るものだけ置き換える + # 1文字ずつ見てエスケープが要るものだけ置き換える for character in text: - # 引用符と逆スラッシュだけを機械的に置き換える + # 引用符、逆スラッシュ、\b \t \n \f \rは対応するエスケープ表記に置き換える if character in _QUOTE_ESCAPES: parts.append(_QUOTE_ESCAPES[character]) continue code = ord(character) - # 制御文字とサロゲートは \uXXXX で表す + # 制御文字とサロゲートは\uXXXXで表す if code < 0x20 or code == 0x7F or 0xD800 <= code <= 0xDFFF: parts.append("\\u%04x" % code) else: diff --git a/python/src/spring_nbt_library/nbt/tag.py b/python/src/spring_nbt_library/nbt/tag.py index 157e12c..2a92cd1 100644 --- a/python/src/spring_nbt_library/nbt/tag.py +++ b/python/src/spring_nbt_library/nbt/tag.py @@ -1,11 +1,10 @@ -"""NBT のタグ型と値モデル - -仕様: ``docs/spec/10-nbt-binary.md`` 1章・7章 +"""NBTのタグ型と値モデル """ from __future__ import annotations import enum +import math import struct from typing import Dict, Iterator, List, Optional, Tuple @@ -31,7 +30,7 @@ class TagType(enum.Enum): - """NBT のタグ型 + """NBTのタグ型 値は仕様が定めるタグIDと一致する """ @@ -55,11 +54,11 @@ def as_string(self) -> str: @staticmethod def from_id(tag_id: int) -> "TagType": - """タグIDから :class:`TagType` を得る + """タグIDから:class:`TagType`を得る :raises SpringNbtError: 未知のタグIDの場合 """ - # 0..12 の範囲外はすべて不正なタグID + # 0..12の範囲外はすべて不正なタグID if tag_id < 0 or tag_id > TagType.LONG_ARRAY.value: raise SpringNbtError.malformed("未知のタグID: %d" % tag_id) @@ -83,8 +82,34 @@ def from_id(tag_id: int) -> "TagType": } +def _join_surrogate_pairs(text: str) -> str: + """UTF-16で対になるサロゲートを1文字へまとめる + + 他言語は文字列をUTF-16で持つので、対になったサロゲートと補助文字は同じ文字列になる + Pythonでも同じ文字列として比べられるよう、対になったものだけを合成し、孤立したものは残す + """ + # サロゲートを含む文字列だけを組み直す + for character in text: + if 0xD800 <= ord(character) <= 0xDFFF: + return mutf8._from_utf16_units(mutf8._to_utf16_units(text)) + + return text + + +def _has_lone_surrogate(text: str) -> bool: + """孤立サロゲートを含むか + 対になったサロゲートは:func:`_join_surrogate_pairs`で合成してある前提で、残ったものを探す + """ + # 合成したあとに残っているサロゲートは、どれも対になっていない + for character in text: + if 0xD800 <= ord(character) <= 0xDFFF: + return True + + return False + + def _check_range(value: int, minimum: int, maximum: int, type_name: str) -> int: - """Python の int には幅が無いため、構築時に範囲を検査する""" + """Pythonのintには幅が無いため、構築時に範囲を検査する""" if not isinstance(value, int) or isinstance(value, bool): raise SpringNbtError.invalid_argument("%s には整数を渡すこと: %r" % (type_name, value)) @@ -96,12 +121,12 @@ def _check_range(value: int, minimum: int, maximum: int, type_name: str) -> int: class NbtTag: - """NBT のタグ - 具象型は :class:`NbtByte` などの派生クラス + """NBTのタグ + 具象型は:class:`NbtByte`などの派生クラス """ #: このタグの型 - # 派生クラスが上書きする + #: 派生クラスが上書きする type: TagType = TagType.END def copy(self) -> "NbtTag": @@ -125,7 +150,8 @@ def value(self): @value.setter def value(self, new_value) -> None: """値を差し替える - 範囲外なら INVALID_ARGUMENT + 構築時と同じ検査と変換をかける + 整数型の範囲外、文字列の長さ超過、型違いはINVALID_ARGUMENT """ self._value = self._validate(new_value) @@ -133,7 +159,7 @@ def _validate(self, value): raise NotImplementedError def copy(self) -> "NbtTag": - """同じ値を持つ新しいタグを作る""" + """このタグの深いコピーを作る""" return type(self)(self._value) def __eq__(self, other) -> bool: @@ -151,7 +177,7 @@ def __repr__(self) -> str: class NbtByte(_ScalarTag): """TAG_Byte - 8bit 符号付き整数 + 8bit符号付き整数 """ type = TagType.BYTE @@ -162,7 +188,7 @@ def _validate(self, value): class NbtShort(_ScalarTag): """TAG_Short - 16bit 符号付き整数 + 16bit符号付き整数 """ type = TagType.SHORT @@ -173,7 +199,7 @@ def _validate(self, value): class NbtInt(_ScalarTag): """TAG_Int - 32bit 符号付き整数 + 32bit符号付き整数 """ type = TagType.INT @@ -184,7 +210,7 @@ def _validate(self, value): class NbtLong(_ScalarTag): """TAG_Long - 64bit 符号付き整数 + 64bit符号付き整数 """ type = TagType.LONG @@ -193,29 +219,86 @@ def _validate(self, value): return _check_range(value, -9223372036854775808, 9223372036854775807, "long") +def _round_to_float32(value) -> float: + """数値をbinary32へ最近接丸めする + binary32で表せない大きさの値は、他言語と同じく符号付きの無限大にする + """ + try: + return struct.unpack(">f", struct.pack(">f", float(value)))[0] + except OverflowError: + # 無限大へ丸まる値だけがここへ来る(floatに収まらない巨大な整数も含む) + if value > 0: + return math.inf + + return -math.inf + + class NbtFloat(_ScalarTag): """TAG_Float IEEE 754 binary32 + + Pythonのfloatはbinary64なので、シグナリングNaNはbinary32との変換で静かなNaNへ変わってしまう + 読み込んだときのビットパターンを覚えておき、値を変えていなければそのまま書き戻す """ + __slots__ = ("_bits",) + type = TagType.FLOAT + def __init__(self, value) -> None: + super().__init__(value) + self._bits = struct.pack(">f", self._value) + + @property + def value(self): + """保持している値""" + return self._value + + @value.setter + def value(self, new_value) -> None: + """値を差し替える + 構築時と同じくbinary32へ丸める + """ + self._value = self._validate(new_value) + + # 値を変えたら、読み込んだときのビットパターンはもう使わない + self._bits = struct.pack(">f", self._value) + + @classmethod + def _from_bits(cls, raw: bytes) -> "NbtFloat": + """binary32のビットパターンからタグを作る + 読んだビットを覚えておき、書き戻すときにそのまま使う + """ + tag = cls(struct.unpack(">f", raw)[0]) + tag._bits = bytes(raw) + return tag + + def _to_bits(self) -> bytes: + """書き出すbinary32のビットパターン""" + return self._bits + def _validate(self, value): if not isinstance(value, (int, float)) or isinstance(value, bool): raise SpringNbtError.invalid_argument("float には数値を渡すこと: %r" % (value,)) - # Python の float は binary64 なので、構築時に binary32 へ丸めて他言語と揃える - return struct.unpack(">f", struct.pack(">f", float(value)))[0] + # Pythonのfloatはbinary64なので、構築時にbinary32へ丸めて他言語と揃える + return _round_to_float32(value) + + def copy(self) -> "NbtTag": + """このタグの深いコピーを作る""" + result = NbtFloat(self._value) + result._bits = self._bits + return result def __eq__(self, other) -> bool: if type(other) is not type(self): return False - # NaN や -0.0 を区別するため、値ではなくビットパターンで比較する - return struct.pack(">f", other._value) == struct.pack(">f", self._value) + # NaNや-0.0を区別するため、値ではなくビットパターンで比較する + return other._bits == self._bits def __hash__(self) -> int: - return hash((self.type, struct.pack(">f", self._value))) + return hash((self.type, self._bits)) class NbtDouble(_ScalarTag): @@ -235,7 +318,7 @@ def __eq__(self, other) -> bool: if type(other) is not type(self): return False - # NaN や -0.0 を区別するため、値ではなくビットパターンで比較する + # NaNや-0.0を区別するため、値ではなくビットパターンで比較する return struct.pack(">d", other._value) == struct.pack(">d", self._value) def __hash__(self) -> int: @@ -244,7 +327,7 @@ def __hash__(self) -> int: class NbtString(_ScalarTag): """TAG_String - MUTF-8 で符号化される文字列 + MUTF-8で符号化される文字列 """ type = TagType.STRING @@ -253,10 +336,12 @@ def _validate(self, value): if not isinstance(value, str): raise SpringNbtError.invalid_argument("string には str を渡すこと: %r" % (value,)) + # 対になったサロゲートは、他言語と同じく補助文字1文字として持つ + value = _join_surrogate_pairs(value) length = mutf8.byte_length(value) - # 長さフィールドは u16 - # 65535 を超えると書き出せない + # 長さフィールドはu16 + # 65535を超えると書き出せない if length > mutf8.MAX_BYTE_LENGTH: raise SpringNbtError.invalid_argument( "文字列が長すぎる: MUTF-8 で %d バイト (上限 %d)" % (length, mutf8.MAX_BYTE_LENGTH)) @@ -270,7 +355,7 @@ class _ArrayTag(NbtTag): __slots__ = ("_value",) #: 要素が取りうる範囲 - # 派生クラスが上書きする + #: 派生クラスが上書きする _minimum = 0 _maximum = 0 _element_name = "" @@ -286,7 +371,7 @@ def value(self) -> List[int]: @value.setter def value(self, new_value) -> None: """値を差し替える - 範囲外なら INVALID_ARGUMENT + 範囲外ならINVALID_ARGUMENT """ self._value = self._validate(new_value) @@ -300,7 +385,7 @@ def _validate(self, value) -> List[int]: return result def copy(self) -> "NbtTag": - """同じ値を持つ新しいタグを作る""" + """このタグの深いコピーを作る""" return type(self)(list(self._value)) def __eq__(self, other) -> bool: @@ -318,7 +403,7 @@ def __repr__(self) -> str: class NbtByteArray(_ArrayTag): """TAG_Byte_Array - 8bit 符号付き整数の配列 + 8bit符号付き整数の配列 """ type = TagType.BYTE_ARRAY @@ -329,7 +414,7 @@ class NbtByteArray(_ArrayTag): class NbtIntArray(_ArrayTag): """TAG_Int_Array - 32bit 符号付き整数の配列 + 32bit符号付き整数の配列 """ type = TagType.INT_ARRAY @@ -340,7 +425,7 @@ class NbtIntArray(_ArrayTag): class NbtLongArray(_ArrayTag): """TAG_Long_Array - 64bit 符号付き整数の配列 + 64bit符号付き整数の配列 """ type = TagType.LONG_ARRAY @@ -351,9 +436,9 @@ class NbtLongArray(_ArrayTag): class NbtList(NbtTag): """TAG_List - 要素型が 1 つに固定されたタグの列 + 要素型が1つに固定されたタグの列 - 空リストの要素型は :attr:`TagType.END` + 空リストの要素型は:attr:`TagType.END` 最初の要素を追加した時点で型が確定する 全要素を削除しても確定済みの要素型は維持される """ @@ -364,16 +449,16 @@ def __init__(self, element_type: TagType = TagType.END, elements=None) -> None: self._element_type = element_type self._items: List[NbtTag] = [] - # 与えられた要素は 1 つずつ型検査しながら追加する + # 与えられた要素は1つずつ型検査しながら追加する if elements is not None: - # append を通すことで、要素型の検査も一緒にかかる + # appendを通すことで、要素型の検査も一緒にかかる for element in elements: self.append(element) @property def element_type(self) -> TagType: """要素の型 - 空で未確定なら :attr:`TagType.END` + 空で未確定なら:attr:`TagType.END` """ return self._element_type @@ -386,7 +471,7 @@ def append(self, item: NbtTag) -> None: self._items.append(item) def insert(self, index: int, item: NbtTag) -> None: - """指定位置に挿入する""" + """位置を指定して挿入する""" self._ensure_element_type(item) self._items.insert(index, item) @@ -397,7 +482,7 @@ def clear(self) -> None: self._items.clear() def copy(self) -> "NbtTag": - """同じ値を持つ新しいタグを作る""" + """このタグの深いコピーを作る""" copy = NbtList(self._element_type) # 要素も深くコピーする @@ -408,7 +493,7 @@ def copy(self) -> "NbtTag": def _ensure_element_type(self, item: NbtTag) -> None: """追加しようとしているタグが要素型と一致するか調べる""" - # TAG_End はリストの要素になれない + # TAG_Endはリストの要素になれない if item.type == TagType.END: raise SpringNbtError.unexpected_tag_type("TAG_End はリストの要素にできない") @@ -427,6 +512,10 @@ def __getitem__(self, index: int) -> NbtTag: return self._items[index] def __setitem__(self, index: int, item: NbtTag) -> None: + # 要素型を確定させる前に位置を確かめ、失敗したときに状態を変えない + if index < -len(self._items) or index >= len(self._items): + raise IndexError("位置が範囲外: %d" % index) + self._ensure_element_type(item) self._items[index] = item @@ -459,71 +548,71 @@ class NbtCompound(NbtTag): type = TagType.COMPOUND def __init__(self, entries=None) -> None: - # Python の dict は 3.7 以降で挿入順を保つ + # Pythonのdictは3.7以降で挿入順を保つ self._entries: Dict[str, NbtTag] = {} # 初期値が与えられたら、そのまま順に入れる if entries is not None: - # set を通すことで、挿入順が保たれる + # setを通すことで、挿入順が保たれる for key, value in entries: self.set(key, value) # -- 型付き設定子 ------------------------------------------------------- # - # set(key, NbtInt(42)) と書かずに済むようにするための糖衣 - # 取得子の get_int と対になる + # set(key, NbtInt(42))と書かずに済むようにするための糖衣 + # 取得子のget_intと対になる def set_byte(self, key: str, value: int) -> None: - """TAG_Byte として設定する""" + """TAG_Byteとして設定する""" self.set(key, NbtByte(value)) def set_short(self, key: str, value: int) -> None: - """TAG_Short として設定する""" + """TAG_Shortとして設定する""" self.set(key, NbtShort(value)) def set_int(self, key: str, value: int) -> None: - """TAG_Int として設定する""" + """TAG_Intとして設定する""" self.set(key, NbtInt(value)) def set_long(self, key: str, value: int) -> None: - """TAG_Long として設定する""" + """TAG_Longとして設定する""" self.set(key, NbtLong(value)) def set_float(self, key: str, value: float) -> None: - """TAG_Float として設定する""" + """TAG_Floatとして設定する""" self.set(key, NbtFloat(value)) def set_double(self, key: str, value: float) -> None: - """TAG_Double として設定する""" + """TAG_Doubleとして設定する""" self.set(key, NbtDouble(value)) def set_bool(self, key: str, value: bool) -> None: - """TAG_Byte として設定する - True は 1、False は 0 + """TAG_Byteとして設定する + Trueは1、Falseは0 """ - # NBT に真偽値の専用型は無いので TAG_Byte の 0 / 1 で表す + # NBTに真偽値の専用型は無いので、TAG_Byteの0 / 1で表す if value: self.set_byte(key, 1) else: self.set_byte(key, 0) def set_string(self, key: str, value: str) -> None: - """TAG_String として設定する + """TAG_Stringとして設定する - :raises SpringNbtError: MUTF-8 で 65535 バイトを超える場合 + :raises SpringNbtError: MUTF-8で65535バイトを超える場合 """ self.set(key, NbtString(value)) def set_byte_array(self, key: str, value: List[int]) -> None: - """TAG_Byte_Array として設定する""" + """TAG_Byte_Arrayとして設定する""" self.set(key, NbtByteArray(value)) def set_int_array(self, key: str, value: List[int]) -> None: - """TAG_Int_Array として設定する""" + """TAG_Int_Arrayとして設定する""" self.set(key, NbtIntArray(value)) def set_long_array(self, key: str, value: List[int]) -> None: - """TAG_Long_Array として設定する""" + """TAG_Long_Arrayとして設定する""" self.set(key, NbtLongArray(value)) def set(self, key: str, value: NbtTag) -> None: @@ -533,7 +622,13 @@ def set(self, key: str, value: NbtTag) -> None: if not isinstance(key, str): raise SpringNbtError.invalid_argument("キーには str を渡すこと: %r" % (key,)) - # TAG_End は Compound の終端マーカーなので値として持てない + key = _join_surrogate_pairs(key) + + # 孤立サロゲートを含むキーは書き出すと読み戻せないので、ここで止める + if _has_lone_surrogate(key): + raise SpringNbtError.invalid_argument("キーに孤立サロゲートは使えない") + + # TAG_EndはCompoundの終端マーカーなので値として持てない if value.type == TagType.END: raise SpringNbtError.unexpected_tag_type("TAG_End は Compound の値にできない") @@ -541,15 +636,15 @@ def set(self, key: str, value: NbtTag) -> None: def opt(self, key: str) -> Optional[NbtTag]: """キーに対応するタグを返す - 存在しなければ None + 存在しなければNone """ - return self._entries.get(key) + return self._entries.get(_join_surrogate_pairs(key)) def get(self, key: str) -> NbtTag: """キーに対応するタグを返す 存在しなければ例外 """ - found = self._entries.get(key) + found = self._entries.get(_join_surrogate_pairs(key)) if found is None: raise SpringNbtError.invalid_argument("キーが存在しない: %s" % key) @@ -558,8 +653,11 @@ def get(self, key: str) -> NbtTag: def remove(self, key: str) -> bool: """キーを削除する - 削除できたら True + 削除できたらTrue """ + key = _join_surrogate_pairs(key) + + # 存在するキーだけを削除する if key in self._entries: del self._entries[key] return True @@ -572,7 +670,7 @@ def clear(self) -> None: def contains_key(self, key: str) -> bool: """キーが存在するか""" - return key in self._entries + return _join_surrogate_pairs(key) in self._entries def keys(self): """挿入順のキー一覧""" @@ -583,7 +681,7 @@ def items(self) -> Iterator[Tuple[str, NbtTag]]: return iter(self._entries.items()) def copy(self) -> "NbtTag": - """同じ値を持つ新しいタグを作る""" + """このタグの深いコピーを作る""" copy = NbtCompound() # 挿入順のまま深くコピーする @@ -596,7 +694,7 @@ def __len__(self) -> int: return len(self._entries) def __contains__(self, key: str) -> bool: - return key in self._entries + return _join_surrogate_pairs(key) in self._entries def __getitem__(self, key: str) -> NbtTag: return self.get(key) @@ -605,7 +703,7 @@ def __setitem__(self, key: str, value: NbtTag) -> None: self.set(key, value) def __delitem__(self, key: str) -> None: - del self._entries[key] + del self._entries[_join_surrogate_pairs(key)] def __iter__(self) -> Iterator[str]: return iter(self._entries) @@ -626,12 +724,12 @@ def __repr__(self) -> str: # -- 型付き取得子 ------------------------------------------------------- # # 「キーが無い」と「型が違う」は区別する - # opt_* はキーが無ければ None を返し、get_* は例外を送出する - # どちらも型が違えば必ず UNEXPECTED_TAG_TYPE の例外になる + # opt_*はキーが無ければNoneを返し、get_*は例外を送出する + # どちらも型が違えば必ずUNEXPECTED_TAG_TYPEの例外になる def _cast(self, key: str, expected): """キーに対応するタグを目的の型として取り出す - キーが無ければ None + キーが無ければNone、型が違えば例外 """ tag = self._entries.get(key) @@ -647,7 +745,7 @@ def _cast(self, key: str, expected): def _require(self, key: str, expected): """キーに対応するタグを目的の型として取り出す - キーが無くても例外 + キーが無くても型が違っても例外 """ tag = self._cast(key, expected) @@ -657,8 +755,8 @@ def _require(self, key: str, expected): return tag def opt_byte(self, key: str) -> Optional[int]: - """TAG_Byte を取得する - キーが無ければ None + """TAG_Byteを取得する + キーが無ければNone """ tag = self._cast(key, NbtByte) @@ -668,14 +766,14 @@ def opt_byte(self, key: str) -> Optional[int]: return tag.value def get_byte(self, key: str) -> int: - """TAG_Byte を取得する + """TAG_Byteを取得する キーが無ければ例外 """ return self._require(key, NbtByte).value def opt_short(self, key: str) -> Optional[int]: - """TAG_Short を取得する - キーが無ければ None + """TAG_Shortを取得する + キーが無ければNone """ tag = self._cast(key, NbtShort) @@ -685,14 +783,14 @@ def opt_short(self, key: str) -> Optional[int]: return tag.value def get_short(self, key: str) -> int: - """TAG_Short を取得する + """TAG_Shortを取得する キーが無ければ例外 """ return self._require(key, NbtShort).value def opt_int(self, key: str) -> Optional[int]: - """TAG_Int を取得する - キーが無ければ None + """TAG_Intを取得する + キーが無ければNone """ tag = self._cast(key, NbtInt) @@ -702,14 +800,14 @@ def opt_int(self, key: str) -> Optional[int]: return tag.value def get_int(self, key: str) -> int: - """TAG_Int を取得する + """TAG_Intを取得する キーが無ければ例外 """ return self._require(key, NbtInt).value def opt_long(self, key: str) -> Optional[int]: - """TAG_Long を取得する - キーが無ければ None + """TAG_Longを取得する + キーが無ければNone """ tag = self._cast(key, NbtLong) @@ -719,14 +817,14 @@ def opt_long(self, key: str) -> Optional[int]: return tag.value def get_long(self, key: str) -> int: - """TAG_Long を取得する + """TAG_Longを取得する キーが無ければ例外 """ return self._require(key, NbtLong).value def opt_float(self, key: str) -> Optional[float]: - """TAG_Float を取得する - キーが無ければ None + """TAG_Floatを取得する + キーが無ければNone """ tag = self._cast(key, NbtFloat) @@ -736,14 +834,14 @@ def opt_float(self, key: str) -> Optional[float]: return tag.value def get_float(self, key: str) -> float: - """TAG_Float を取得する + """TAG_Floatを取得する キーが無ければ例外 """ return self._require(key, NbtFloat).value def opt_double(self, key: str) -> Optional[float]: - """TAG_Double を取得する - キーが無ければ None + """TAG_Doubleを取得する + キーが無ければNone """ tag = self._cast(key, NbtDouble) @@ -753,15 +851,15 @@ def opt_double(self, key: str) -> Optional[float]: return tag.value def get_double(self, key: str) -> float: - """TAG_Double を取得する + """TAG_Doubleを取得する キーが無ければ例外 """ return self._require(key, NbtDouble).value def opt_bool(self, key: str) -> Optional[bool]: - """TAG_Byte を真偽値として取得する - 0 以外が True - キーが無ければ None + """TAG_Byteを真偽値として取得する + 0以外がTrue + キーが無ければNone """ raw = self.opt_byte(key) @@ -771,15 +869,15 @@ def opt_bool(self, key: str) -> Optional[bool]: return raw != 0 def get_bool(self, key: str) -> bool: - """TAG_Byte を真偽値として取得する - 0 以外が True + """TAG_Byteを真偽値として取得する + 0以外がTrue キーが無ければ例外 """ return self.get_byte(key) != 0 def opt_string(self, key: str) -> Optional[str]: - """TAG_String を取得する - キーが無ければ None + """TAG_Stringを取得する + キーが無ければNone """ tag = self._cast(key, NbtString) @@ -789,14 +887,14 @@ def opt_string(self, key: str) -> Optional[str]: return tag.value def get_string(self, key: str) -> str: - """TAG_String を取得する + """TAG_Stringを取得する キーが無ければ例外 """ return self._require(key, NbtString).value def opt_byte_array(self, key: str) -> Optional[List[int]]: - """TAG_Byte_Array を取得する - キーが無ければ None + """TAG_Byte_Arrayを取得する + キーが無ければNone """ tag = self._cast(key, NbtByteArray) @@ -806,14 +904,14 @@ def opt_byte_array(self, key: str) -> Optional[List[int]]: return tag.value def get_byte_array(self, key: str) -> List[int]: - """TAG_Byte_Array を取得する + """TAG_Byte_Arrayを取得する キーが無ければ例外 """ return self._require(key, NbtByteArray).value def opt_int_array(self, key: str) -> Optional[List[int]]: - """TAG_Int_Array を取得する - キーが無ければ None + """TAG_Int_Arrayを取得する + キーが無ければNone """ tag = self._cast(key, NbtIntArray) @@ -823,14 +921,14 @@ def opt_int_array(self, key: str) -> Optional[List[int]]: return tag.value def get_int_array(self, key: str) -> List[int]: - """TAG_Int_Array を取得する + """TAG_Int_Arrayを取得する キーが無ければ例外 """ return self._require(key, NbtIntArray).value def opt_long_array(self, key: str) -> Optional[List[int]]: - """TAG_Long_Array を取得する - キーが無ければ None + """TAG_Long_Arrayを取得する + キーが無ければNone """ tag = self._cast(key, NbtLongArray) @@ -840,31 +938,31 @@ def opt_long_array(self, key: str) -> Optional[List[int]]: return tag.value def get_long_array(self, key: str) -> List[int]: - """TAG_Long_Array を取得する + """TAG_Long_Arrayを取得する キーが無ければ例外 """ return self._require(key, NbtLongArray).value def opt_list(self, key: str) -> Optional[NbtList]: - """TAG_List を取得する - キーが無ければ None + """TAG_Listを取得する + キーが無ければNone """ return self._cast(key, NbtList) def get_list(self, key: str) -> NbtList: - """TAG_List を取得する + """TAG_Listを取得する キーが無ければ例外 """ return self._require(key, NbtList) def opt_compound(self, key: str) -> Optional["NbtCompound"]: - """TAG_Compound を取得する - キーが無ければ None + """TAG_Compoundを取得する + キーが無ければNone """ return self._cast(key, NbtCompound) def get_compound(self, key: str) -> "NbtCompound": - """TAG_Compound を取得する + """TAG_Compoundを取得する キーが無ければ例外 """ return self._require(key, NbtCompound) diff --git a/python/src/spring_nbt_library/world/__init__.py b/python/src/spring_nbt_library/world/__init__.py index f7698e9..293a05f 100644 --- a/python/src/spring_nbt_library/world/__init__.py +++ b/python/src/spring_nbt_library/world/__init__.py @@ -1,6 +1,4 @@ """ワールド・チャンク・ブロックの読み書き - -仕様: ``docs/spec/30-chunk-format.md`` / ``docs/spec/40-world-layout.md`` """ from .bit_storage import BitStorage diff --git a/python/src/spring_nbt_library/world/bit_storage.py b/python/src/spring_nbt_library/world/bit_storage.py index 5e4164a..d9914e0 100644 --- a/python/src/spring_nbt_library/world/bit_storage.py +++ b/python/src/spring_nbt_library/world/bit_storage.py @@ -1,13 +1,11 @@ -"""添字を 64bit 整数の配列へ詰めた表現 -1.16 以降の**跨ぎなし**パッキング +"""添字を64bit整数の配列へ詰めた表現 +1.16以降の**跨ぎなし**パッキング -1 つの値に入りきらない分は、その値の残りビットを未使用のまま捨て、 +1つの値に入りきらない分は、その値の残りビットを未使用のまま捨て、 次の値の最下位ビットから始める -Python の整数は無限精度なので、i64 として扱う箇所では -読み込み時に符号付きへ、書き込み時に符号なしへ明示的に変換する - -仕様: ``docs/spec/31-paletted-container.md`` 2章 +Pythonの整数は無限精度なので、i64として扱う箇所では +値を取り出すときは符号なしへ、書き込むときは符号付きへ明示的に変換する """ from __future__ import annotations @@ -22,7 +20,7 @@ class BitStorage: - """packed な添字の並び""" + """packedな添字の並び""" __slots__ = ("_data", "bits_per_entry", "entry_count") @@ -33,7 +31,7 @@ def __init__(self, data: List[int], bits_per_entry: int, entry_count: int) -> No @property def values_per_long(self) -> int: - """1 つの 64bit 値に入るエントリ数""" + """1つの64bit値に入るエントリ数""" return 64 // self.bits_per_entry @staticmethod @@ -48,9 +46,10 @@ def create(bits_per_entry: int, entry_count: int) -> "BitStorage": @staticmethod def from_longs(data: List[int], bits_per_entry: int, entry_count: int, lenient: bool = False) -> "BitStorage": - """既存の 64bit 配列から作る + """既存の64bit配列から作る - :param lenient: True なら配列長からビット幅を逆算して読む(第三者ツール由来の救済) + :param lenient: Trueなら、配列長が期待値と違う場合に配列長からビット幅を逆算して読む + 第三者ツールが書いたデータの救済用 :raises SpringNbtError: 配列長が期待値と一致しない場合 """ expected = BitStorage.long_count(bits_per_entry, entry_count) @@ -74,7 +73,7 @@ def from_longs(data: List[int], bits_per_entry: int, entry_count: int, @staticmethod def long_count(bits_per_entry: int, entry_count: int) -> int: - """必要な 64bit 値の個数を求める""" + """必要な64bit値の個数を求める""" values_per_long = 64 // bits_per_entry return (entry_count + values_per_long - 1) // values_per_long @@ -87,7 +86,7 @@ def get(self, index: int) -> int: bit_offset = (index % per_long) * self.bits_per_entry mask = (1 << self.bits_per_entry) - 1 - # 符号付きのままシフトすると符号が伸びるので、符号なしへ直してから動かす + # 符号付きのままシフトすると上位ビットが伸びるので、符号なしへ直してから動かす return ((self._data[long_index] & _MASK64) >> bit_offset) & mask def set(self, index: int, value: int) -> None: @@ -109,7 +108,7 @@ def set(self, index: int, value: int) -> None: self._data[long_index] = _to_signed(updated) def to_longs(self) -> List[int]: - """packed な配列を返す + """packedな配列を返す 内部の配列をそのまま返す(コピーしない) """ return self._data @@ -131,7 +130,7 @@ def _check_index(self, index: int) -> None: def _to_signed(value: int) -> int: - """符号なし 64bit を符号付きへ読み替える""" + """符号なし64bitを符号付きへ読み替える""" if value >= (1 << 63): return value - (1 << 64) diff --git a/python/src/spring_nbt_library/world/block_pos.py b/python/src/spring_nbt_library/world/block_pos.py index 6f0413d..23afefd 100644 --- a/python/src/spring_nbt_library/world/block_pos.py +++ b/python/src/spring_nbt_library/world/block_pos.py @@ -1,6 +1,4 @@ """ブロックの絶対座標と、その範囲 - -仕様: docs/spec/30-chunk-format.md 5章 """ from __future__ import annotations @@ -61,10 +59,10 @@ def __repr__(self) -> str: class Cuboid: - """ブロック座標の直方体な範囲 + """ブロック座標の直方体の範囲 両端を含む - ``Cuboid.of(0, 0, 0, 0, 0, 0)`` は 1 ブロック + ``Cuboid.of(0, 0, 0, 0, 0, 0)``は1ブロック 範囲内のブロックを順に処理したいときに使う """ @@ -77,17 +75,17 @@ def __init__(self, first: BlockPos, second: BlockPos) -> None: 大小の順序は問わない 内部で小さいほうを最小に揃える """ - #: X の最小値 + #: Xの最小値 self.min_x = min(first.x, second.x) - #: Y の最小値 + #: Yの最小値 self.min_y = min(first.y, second.y) - #: Z の最小値 + #: Zの最小値 self.min_z = min(first.z, second.z) - #: X の最大値(含む) + #: Xの最大値(含む) self.max_x = max(first.x, second.x) - #: Y の最大値(含む) + #: Yの最大値(含む) self.max_y = max(first.y, second.y) - #: Z の最大値(含む) + #: Zの最大値(含む) self.max_z = max(first.z, second.z) @staticmethod @@ -97,17 +95,17 @@ def of(x1: int, y1: int, z1: int, x2: int, y2: int, z2: int) -> "Cuboid": @property def size_x(self) -> int: - """X 方向の長さ""" + """X方向の長さ""" return self.max_x - self.min_x + 1 @property def size_y(self) -> int: - """Y 方向の長さ""" + """Y方向の長さ""" return self.max_y - self.min_y + 1 @property def size_z(self) -> int: - """Z 方向の長さ""" + """Z方向の長さ""" return self.max_z - self.min_z + 1 @property @@ -124,9 +122,9 @@ def contains(self, x: int, y: int, z: int) -> bool: def positions(self) -> Iterator[BlockPos]: """範囲内の座標を順に返す - 並びは Y、Z、X の順で、X がいちばん内側で動く + 並びはY、Z、Xの順で、Xがいちばん内側で動く """ - # 内側から X が動くので、同じチャンクの並びを続けて触れる + # Xがいちばん内側で動くので、同じチャンクの並びを続けて扱える for y in range(self.min_y, self.max_y + 1): for z in range(self.min_z, self.max_z + 1): for x in range(self.min_x, self.max_x + 1): diff --git a/python/src/spring_nbt_library/world/block_state.py b/python/src/spring_nbt_library/world/block_state.py index 2432aee..978fb9c 100644 --- a/python/src/spring_nbt_library/world/block_state.py +++ b/python/src/spring_nbt_library/world/block_state.py @@ -2,13 +2,9 @@ 名前と、任意のプロパティの組 プロパティは**常に名前の昇順で保持する** -こうしておくと文字列表現が一意になり、 -全言語で同じ出力になる -Minecraft が書き出した並び順は -:class:`PalettedContainer` がパレットを生の NBT のまま持つことで守られるので、 -触っていないブロックの並びが崩れることはない - -仕様: ``docs/spec/30-chunk-format.md`` 2.1章 +こうしておくと文字列表現が一意になり、全言語で同じ出力になる +Minecraftが書き出した並び順は、:class:`PalettedContainer`がパレットを生のNBTのまま持つことで守られる +そのため、触っていないブロックの並びが崩れることはない """ from __future__ import annotations @@ -44,19 +40,19 @@ def properties(self) -> Mapping[str, str]: def property(self, key: str) -> Optional[str]: """プロパティを取得する - 無ければ None + 無ければNone """ return self._properties.get(key) def with_property(self, key: str, value: str) -> "BlockState": - """プロパティを 1 つ差し替えた新しい状態を返す""" + """プロパティを1つ差し替えた新しい状態を返す""" merged = dict(self._properties) merged[key] = value return BlockState(self.name, merged) @staticmethod def parse(text: str) -> "BlockState": - """``minecraft:oak_stairs[facing=north,half=top]`` 形式の文字列から作る + """``minecraft:oak_stairs[facing=north,half=top]``形式の文字列から作る :raises SpringNbtError: 形式が不正な場合 """ @@ -76,7 +72,7 @@ def parse(text: str) -> "BlockState": properties = {} if len(body) > 0: - # "key=value" をカンマ区切りで読む + # "key=value"をカンマ区切りで読む for pair in body.split(","): equals = pair.find("=") @@ -98,15 +94,15 @@ def parse(text: str) -> "BlockState": @staticmethod def from_nbt(nbt: NbtCompound) -> "BlockState": - """パレット要素の NBT から作る + """パレット要素のNBTから作る - :raises SpringNbtError: ``Name`` が無い、または ``Properties`` の値が文字列でない場合 + :raises SpringNbtError: ``Name``が無い、または``Properties``の値が文字列でない場合 """ properties_tag = nbt.opt_compound("Properties") properties = {} if properties_tag is not None: - # Properties の値はすべて文字列(数値や真偽値も文字列で入る) + # Propertiesの値はすべて文字列(数値や真偽値も文字列で入る) for key, value in properties_tag.items(): if not isinstance(value, NbtString): raise SpringNbtError.unexpected_tag_type( @@ -117,10 +113,10 @@ def from_nbt(nbt: NbtCompound) -> "BlockState": return BlockState(nbt.get_string("Name"), properties) def to_nbt(self) -> NbtCompound: - """パレット要素の NBT へ変換する + """パレット要素のNBTへ変換する - プロパティが空なら ``Properties`` キー自体を出力しない - Minecraft と同じ振る舞い + プロパティが空なら``Properties``キー自体を出力しない + Minecraftと同じ振る舞い """ result = NbtCompound() result.set("Name", NbtString(self.name)) @@ -147,7 +143,7 @@ def __hash__(self) -> int: return hash((self.name, tuple(sorted(self._properties.items())))) def __str__(self) -> str: - """``minecraft:oak_stairs[facing=north,half=top]`` 形式の文字列を返す""" + """``minecraft:oak_stairs[facing=north,half=top]``形式の文字列を返す""" if len(self._properties) == 0: return self.name @@ -160,7 +156,7 @@ def __repr__(self) -> str: def _normalize(name: str) -> str: - """名前空間が省略されていたら ``minecraft:`` を補う""" + """名前空間が省略されていたら``minecraft:``を補う""" if ":" in name: return name diff --git a/python/src/spring_nbt_library/world/chunk.py b/python/src/spring_nbt_library/world/chunk.py index 77344e8..d443cab 100644 --- a/python/src/spring_nbt_library/world/chunk.py +++ b/python/src/spring_nbt_library/world/chunk.py @@ -1,11 +1,8 @@ -"""チャンク 1 つ分 -地形の読み書きの入口 +"""チャンク1つ分 +地形の読み書きはここから行う -**読んだ NBT をそのまま保持し、変更した部分だけを書き戻す -** -未知のキーを落とさないので、将来の追加要素があってもデータを壊さない - -仕様: ``docs/spec/30-chunk-format.md`` +**読んだNBTをそのまま保持し、書き戻すときに作り直すのはセクションだけにする** +未知のキーを落とさないので、将来の追加要素があってもデータをおかしくしない """ from __future__ import annotations @@ -31,14 +28,14 @@ "block_index", ] -#: セクション 1 つに入るブロック数 +#: セクション1つに入るブロック数 BLOCKS_PER_SECTION = 4096 -#: セクション 1 つに入るバイオームのエントリ数(4×4×4 単位) +#: セクション1つに入るバイオームのエントリ数(4×4×4単位) BIOMES_PER_SECTION = 64 #: ブロックに紐づく付随データのキー -# ブロックを置き換えたら整合が崩れる +#: ブロックを置き換えたら整合が崩れる _BLOCK_DATA_KEYS = ("block_entities", "block_ticks", "fluid_ticks") @@ -56,13 +53,13 @@ def _matches_position(entry: NbtCompound, x: int, y: int, z: int) -> bool: class VersionMismatchAction(enum.Enum): - """DataVersion が対象と違ったときの動作""" + """DataVersionが扱える形式より古いときの動作""" #: 警告コールバックを呼んで続行する - # 既定 + #: 既定 WARN = "warn" - #: ``UNSUPPORTED_DATA_VERSION`` の例外にする + #: ``UNSUPPORTED_DATA_VERSION``の例外にする ERROR = "error" #: 何もしない @@ -71,20 +68,18 @@ class VersionMismatchAction(enum.Enum): class ChunkReadOptions: """チャンク読み込みのオプション - - 仕様: ``docs/spec/30-chunk-format.md`` 5章 """ def __init__(self, on_version_mismatch: VersionMismatchAction = VersionMismatchAction.WARN, on_warning: Optional[Callable[[str], None]] = None, lenient_bit_storage: bool = False) -> None: - #: DataVersion が対象と違うときの動作 + #: DataVersionが扱える形式より古いときの動作 self.on_version_mismatch = on_version_mismatch #: 警告の通知先 - # None なら何もしない + #: Noneなら何もしない self.on_warning = on_warning - #: data の長さが期待値と違うとき、長さからビット幅を逆算して読むか + #: dataの長さが期待値と違うとき、長さからビット幅を逆算して読むか self.lenient_bit_storage = lenient_bit_storage @@ -92,11 +87,11 @@ class ChunkWriteOptions: """チャンク書き込みのオプション""" def __init__(self, allow_foreign_data_version: bool = False) -> None: - #: 対象バージョン以外の DataVersion を持つチャンクの書き戻しを許すか + #: 扱える形式より古いDataVersionを持つチャンクの書き戻しを許すか #: - #: 既定は False - # 古いワールドを黙って新形式で上書きし、 - #: 利用者が気づかないうちに壊すことを防ぐため(``docs/adr/0003-version-policy.md``) + #: 既定はFalse + #: エラーを出さずに古いワールドを新形式で上書きし、 + #: 利用者が気づかないうちに使えなくすることを防ぐため self.allow_foreign_data_version = allow_foreign_data_version @@ -105,12 +100,10 @@ def __init__(self, allow_foreign_data_version: bool = False) -> None: class ChunkSection: - """チャンクを Y 方向に 16 ブロックずつ区切った 16×16×16 の立方体 + """チャンクをY方向に16ブロックずつ区切った16×16×16の立方体 - ``BlockLight`` / ``SkyLight`` などの解釈していないキーは元の NBT に残り、 + ``BlockLight`` / ``SkyLight``などの解釈していないキーは元のNBTに残り、 書き戻しでそのまま出力される - - 仕様: ``docs/spec/30-chunk-format.md`` 2章 """ __slots__ = ("raw", "y", "block_states", "biomes") @@ -133,11 +126,11 @@ def has_biomes(self) -> bool: @staticmethod def from_nbt(nbt: NbtCompound, options: ChunkReadOptions) -> "ChunkSection": - """NBT からセクションを読む""" + """NBTからセクションを読む""" section = ChunkSection(nbt, nbt.get_byte("Y")) block_states = nbt.opt_compound("block_states") - # 光源専用のセクションは block_states を持たない + # 光源専用のセクションはblock_statesを持たない if block_states is not None: section.block_states = PalettedContainer.from_nbt( block_states, BLOCKS_PER_SECTION, 4, options.lenient_bit_storage) @@ -152,7 +145,7 @@ def from_nbt(nbt: NbtCompound, options: ChunkReadOptions) -> "ChunkSection": return section def to_nbt(self) -> NbtCompound: - """NBT へ書き戻す + """NBTへ書き戻す 解釈していないキーはそのまま残る """ if self.block_states is not None: @@ -176,7 +169,7 @@ def compact(self) -> None: class Chunk: - """チャンク 1 つ分""" + """チャンク1つ分""" __slots__ = ("raw", "_sections", "_modified") @@ -203,13 +196,13 @@ def z(self) -> int: @property def min_section_y(self) -> int: """最下段セクションのY位置 - オーバーワールドは -4 + オーバーワールドは-4 """ return self.raw.get_int("yPos") @property def status(self) -> str: - """生成段階(``minecraft:full`` など)""" + """生成段階(``minecraft:full``など)""" return self.raw.get_string("Status") @property @@ -224,9 +217,9 @@ def is_modified(self) -> bool: """このチャンクに変更が加わったか ブロックやバイオームを書き換えると立つ - :meth:`Dimension.flush` はこれが立っているチャンクだけを書き戻す + :meth:`Dimension.flush`はこれが立っているチャンクだけを書き戻す - ``raw`` を直接いじった場合はここが立たないので、自分で True にすること + ``raw``を直接いじった場合は立たないので、自分でTrueにすること """ return self._modified @@ -244,7 +237,7 @@ def section_ys(self) -> List[int]: @staticmethod def from_nbt(nbt: NbtCompound, options: Optional[ChunkReadOptions] = None) -> "Chunk": - """NBT からチャンクを読む + """NBTからチャンクを読む :raises SpringNbtError: 必須のキーが無い、または構造が想定と違う場合 """ @@ -260,7 +253,7 @@ def from_nbt(nbt: NbtCompound, options: Optional[ChunkReadOptions] = None) -> "C if section_list is None: return chunk - # 並び順に依存しないよう、Y から索引を作る + # 並び順に依存しないよう、Yから索引を作る for entry in section_list: if not isinstance(entry, NbtCompound): raise SpringNbtError.unexpected_tag_type( @@ -272,7 +265,7 @@ def from_nbt(nbt: NbtCompound, options: Optional[ChunkReadOptions] = None) -> "C return chunk def _check_data_version(self, options: ChunkReadOptions) -> None: - """DataVersion を検査し、オプションに従って警告またはエラーにする""" + """DataVersionを検査し、オプションに従って警告またはエラーにする""" version = self.data_version # 形式が同じであれば、新しいバージョンでもそのまま読める @@ -291,10 +284,10 @@ def _check_data_version(self, options: ChunkReadOptions) -> None: options.on_warning(message) def to_nbt(self, options: Optional[ChunkWriteOptions] = None) -> NbtCompound: - """NBT へ書き戻す - 変更したセクションだけを反映し、他のキーはそのまま残す + """NBTへ書き戻す + 全セクションを書き戻し、他のキーはそのまま残す - :raises SpringNbtError: DataVersion が対象と違い、かつ書き戻しが許可されていない場合 + :raises SpringNbtError: DataVersionが扱える形式より古く、かつ書き戻しが許可されていない場合 """ if options is None: effective = _DEFAULT_WRITE @@ -303,7 +296,7 @@ def to_nbt(self, options: Optional[ChunkWriteOptions] = None) -> NbtCompound: version = self.data_version - # 形式の違う古いチャンクは、書き戻すと壊しかねない + # 形式の違う古いチャンクは、書き戻すと使えなくなりかねない if version < MIN_SUPPORTED_DATA_VERSION and not effective.allow_foreign_data_version: raise SpringNbtError( ErrorCode.UNSUPPORTED_DATA_VERSION, @@ -311,7 +304,7 @@ def to_nbt(self, options: Optional[ChunkWriteOptions] = None) -> NbtCompound: "許可するなら ChunkWriteOptions(allow_foreign_data_version=True)" % (version, MIN_SUPPORTED_DATA_VERSION)) - # DataVersion は読んだ値のまま残す + # DataVersionは読んだ値のまま残す # 書き換えると、そのワールドを開くゲーム側の判断を誤らせる if len(self._sections) == 0: @@ -319,7 +312,7 @@ def to_nbt(self, options: Optional[ChunkWriteOptions] = None) -> NbtCompound: section_list = NbtList(TagType.COMPOUND) - # Y の昇順で書き出す + # Yの昇順で書き出す for section_y in self.section_ys: section_list.append(self._sections[section_y].to_nbt()) @@ -328,7 +321,7 @@ def to_nbt(self, options: Optional[ChunkWriteOptions] = None) -> NbtCompound: def section(self, section_y: int) -> Optional[ChunkSection]: """Y位置からセクションを得る - 無ければ None + 無ければNone """ return self._sections.get(section_y) @@ -339,7 +332,7 @@ def get_block(self, x: int, y: int, z: int) -> Optional[BlockState]: :param y: 絶対Y座標 :param z: チャンク内相対Z座標 (0..15) :return: ブロック - セクションが無い、または block_states を持たない場合は None + セクションが無い、またはblock_statesを持たない場合はNone """ _check_local_coordinates(x, z) section = self.section(y >> 4) @@ -358,19 +351,15 @@ def get_block(self, x: int, y: int, z: int) -> Optional[BlockState]: def set_block(self, x: int, y: int, z: int, state: Union[BlockState, str]) -> None: """ブロックを設定する - ``minecraft:oak_stairs[facing=north]`` の形の文字列でも指定できる + ``minecraft:oak_stairs[facing=north]``の形の文字列でも指定できる 置き換えによって不整合になる付随データ(``block_entities`` / - ``block_ticks`` / ``fluid_ticks`` のうち、その座標を指すもの)は - 同時に取り除く - 残すとブロックと中身が食い違い、 - Minecraft 側で予期しない挙動になるため - - 仕様: ``docs/spec/30-chunk-format.md`` 2.4章 + ``block_ticks`` / ``fluid_ticks``のうち、その座標を指すもの)は同時に取り除く + 残すとブロックと中身が食い違い、Minecraft側で予期しない挙動になるため - :raises SpringNbtError: 対象のセクションが無い、または block_states を持たない場合 + :raises SpringNbtError: 対象のセクションが無い、またはblock_statesを持たない場合 """ - # 文字列で渡されたら BlockState へ直してから進む + # 文字列で渡されたらBlockStateへ直してから進む if isinstance(state, str): state = BlockState.parse(state) @@ -384,7 +373,7 @@ def set_block(self, x: int, y: int, z: int, state: Union[BlockState, str]) -> No "本ライブラリはセクションを新規生成しない" % (y, section_y)) # 同じ状態を置き直すだけなら、付随データを触る理由がない - # プロパティの並び順に左右されないよう、NBT ではなく BlockState として比べる + # プロパティの並び順に左右されないよう、NBTではなくBlockStateとして比べる current = self.get_block(x, y, z) if current is not None and current == state: @@ -397,13 +386,13 @@ def set_block(self, x: int, y: int, z: int, state: Union[BlockState, str]) -> No def _remove_block_data(self, x: int, y: int, z: int) -> None: """その座標を指す付随データを取り除く - ``block_entities`` / ``block_ticks`` / ``fluid_ticks`` の要素は - いずれも ``x`` ``y`` ``z`` を**絶対座標**で持つ + ``block_entities`` / ``block_ticks`` / ``fluid_ticks``の要素は + いずれも``x`` ``y`` ``z``を**絶対座標**で持つ """ absolute_x = (self.x * 16) + x absolute_z = (self.z * 16) + z - # 3 つのリストは形が同じなので、まとめて同じ処理をかける + # 3つのリストは形が同じなので、まとめて同じ処理をかける for key in _BLOCK_DATA_KEYS: values = self.raw.opt_list(key) @@ -421,7 +410,7 @@ def _remove_block_data(self, x: int, y: int, z: int) -> None: def get_biome(self, x: int, y: int, z: int) -> Optional[str]: """バイオームを取得する - 4×4×4 の単位なので、座標は自動的に丸められる + 4×4×4の単位なので、座標は自動的に丸められる """ _check_local_coordinates(x, z) section = self.section(y >> 4) @@ -439,7 +428,7 @@ def get_biome(self, x: int, y: int, z: int) -> Optional[str]: def set_biome(self, x: int, y: int, z: int, biome: str) -> None: """バイオームを設定する - 4×4×4 の単位 + 4×4×4の単位 """ _check_local_coordinates(x, z) section_y = y >> 4 @@ -453,17 +442,16 @@ def set_biome(self, x: int, y: int, z: int, biome: str) -> None: self._modified = True def clear_heightmaps(self) -> None: - """``Heightmaps`` を削除し、Minecraft に再計算させる + """``Heightmaps``を削除し、Minecraftに再計算させる 本ライブラリは高さマップを再計算しない ブロックを改変したら呼ぶこと - (``docs/adr/0004-defer-heightmap-recalc.md``) """ self.raw.remove("Heightmaps") self._modified = True def invalidate_lighting(self) -> None: - """``isLightOn`` を 0 にし、光源の再計算を促す""" + """``isLightOn``を0にし、光源の再計算を促す""" self.raw.set_byte("isLightOn", 0) self._modified = True @@ -476,20 +464,20 @@ def compact(self) -> None: def block_index(x: int, y: int, z: int) -> int: """セクション内のブロック添字 - ``& 15`` により負のY座標でも正しく求まる + ``& 15``により負のY座標でも正しく求まる """ return ((y & 15) * 256) + ((z & 15) * 16) + (x & 15) def biome_index(x: int, y: int, z: int) -> int: """セクション内のバイオーム添字 - 1 エントリが 4×4×4 ブロック + 1エントリが4×4×4ブロック """ return (((y & 15) // 4) * 16) + (((z & 15) // 4) * 4) + ((x & 15) // 4) def _check_local_coordinates(x: int, z: int) -> None: - # チャンク内相対座標は 0..15 でなければならない + # チャンク内相対座標は0..15でなければならない if x < 0 or x > 15 or z < 0 or z > 15: raise SpringNbtError.invalid_argument( "チャンク内相対座標が範囲外: (%d, %d)。X も Z も 0..15 であること" % (x, z)) diff --git a/python/src/spring_nbt_library/world/minecraft_world.py b/python/src/spring_nbt_library/world/minecraft_world.py index 3feb1ae..d8e0ffd 100644 --- a/python/src/spring_nbt_library/world/minecraft_world.py +++ b/python/src/spring_nbt_library/world/minecraft_world.py @@ -1,9 +1,6 @@ -"""Minecraft Java版のセーブデータ 1 つ分と、その中の次元 +"""Minecraft Java版のセーブデータ1つ分と、その中の次元 -26.x では構成が大きく変わっており、標準の3次元も -``dimensions/<名前空間>/<パス>/`` の下に並ぶ - -仕様: ``docs/spec/40-world-layout.md`` +26.xでは構成が大きく変わっており、標準の3次元も``dimensions/<名前空間>/<パス>/``の下に並ぶ """ from __future__ import annotations @@ -15,13 +12,13 @@ try: import fcntl except ImportError: - # Windows には fcntl が無い - # その場合 session.lock の確認は行わない + # Windowsにはfcntlが無い + # その場合session.lockの確認は行わない fcntl = None from ..anvil import ChunkPos, RegionFileMode, RegionFolder from ..errors import ErrorCode, SpringNbtError -from ..nbt import NamedTag, NbtCompound, read_file, write_file +from ..nbt import NamedTag, NbtCompound, read_file, write_bytes from .block_state import BlockState from .chunk import Chunk, ChunkReadOptions, ChunkWriteOptions @@ -35,14 +32,14 @@ def __init__(self, writable: bool = False, ignore_session_lock: bool = False, chunk_read: Optional[ChunkReadOptions] = None, chunk_write: Optional[ChunkWriteOptions] = None) -> None: #: 読み書きで開くか - # 既定は読み取り専用 + #: 既定は読み取り専用 self.writable = writable - #: ``session.lock`` の確認を飛ばすか + #: ``session.lock``の確認を飛ばすか #: - #: Minecraft が起動中のワールドへ書き込むとデータが壊れる + #: Minecraftが起動中のワールドへ書き込むとデータがおかしくなる #: 既定では書き込みモードで開くときに必ず確認する - # これを立てるのは自己責任 + #: これを立てるのは自己責任 self.ignore_session_lock = ignore_session_lock #: チャンク読み込みのオプション @@ -59,12 +56,9 @@ def __init__(self, writable: bool = False, ignore_session_lock: bool = False, class LevelData: - """``level.dat`` の内容 - - 26.x では大幅に軽量化されており、ゲームルールやワールド生成設定は - ``data/minecraft/`` 配下の個別ファイルへ分離されている + """``level.dat``の内容 - 仕様: ``docs/spec/40-world-layout.md`` 2章 + 26.xでは大幅に軽量化されており、ゲームルールやワールド生成設定は``data/minecraft/``配下の個別ファイルへ分離されている """ __slots__ = ("_root_name", "raw", "data") @@ -98,7 +92,7 @@ def game_type(self) -> int: @property def spawn_pos(self) -> List[int]: - """スポーン地点の ``[x, y, z]``""" + """スポーン地点の``[x, y, z]``""" return self.data.get_compound("spawn").get_int_array("pos") @property @@ -108,7 +102,7 @@ def spawn_dimension(self) -> str: @property def difficulty(self) -> str: - """難易度(``normal`` など)""" + """難易度(``normal``など)""" return self.data.get_compound("difficulty_settings").get_string("difficulty") @property @@ -118,22 +112,19 @@ def is_hardcore(self) -> bool: @property def version_name(self) -> str: - """バージョン名(``26.2`` など)""" + """バージョン名(``26.2``など)""" return self.data.get_compound("Version").get_string("Name") def to_named_tag(self) -> NamedTag: - """書き出し用の :class:`NamedTag` を作る""" + """書き出し用の:class:`NamedTag`を作る""" return NamedTag(self._root_name, self.raw) class Dimension: - """ワールド内の次元 1 つ分 - ``region/`` ``entities/`` ``poi/`` をまとめて扱う + """ワールド内の次元1つ分 + ``region/`` ``entities/`` ``poi/``をまとめて扱う - ブロックの取得・設定は**絶対ワールド座標**で行い、 - リージョン・チャンク・セクションの解決は内部で済ませる - - 仕様: ``docs/spec/40-world-layout.md`` 4章 + ブロックの取得・設定は**絶対ワールド座標**で行い、リージョン・チャンク・セクションの解決は内部で済ませる """ #: オーバーワールドの次元ID @@ -163,28 +154,30 @@ def __exit__(self, exc_type, exc_value, traceback) -> None: def region_folder(self) -> Optional[RegionFolder]: """地形のリージョンフォルダ - 無ければ None + 無ければNone """ self._regions = self._folder(self._regions, "region") return self._regions def entity_folder(self) -> Optional[RegionFolder]: """エンティティのリージョンフォルダ - 無ければ None + 無ければNone """ self._entities = self._folder(self._entities, "entities") return self._entities def poi_folder(self) -> Optional[RegionFolder]: - """POI のリージョンフォルダ - 無ければ None + """POIのリージョンフォルダ + 無ければNone """ self._poi = self._folder(self._poi, "poi") return self._poi def data_file(self, name: str) -> Optional[NbtCompound]: - """``data/minecraft/.dat`` を読む - 存在しなければ None + """``data/minecraft/.dat``を読む + 存在しなければNone + + 次元ごとの``world_border`` / ``raids`` / ``chunk_tickets``などが入る """ self._ensure_open() path = os.path.join(self.directory, "data", "minecraft", name + ".dat") @@ -206,7 +199,7 @@ def chunk_positions(self) -> List[ChunkPos]: def chunk(self, chunk_x: int, chunk_z: int) -> Optional[Chunk]: """チャンクを読む - 読み込んだチャンクはキャッシュされる + 読み込んだチャンクはキャッシュされ、次回は同じインスタンスが返る """ self._ensure_open() key = "%d,%d" % (chunk_x, chunk_z) @@ -244,7 +237,7 @@ def save_chunk(self, chunk: Chunk) -> None: def get_block(self, x: int, y: int, z: int) -> Optional[BlockState]: """絶対座標でブロックを取得する - チャンクが無ければ None + チャンクが無ければNone """ chunk = self.chunk(x >> 4, z >> 4) @@ -256,10 +249,10 @@ def get_block(self, x: int, y: int, z: int) -> Optional[BlockState]: def set_block(self, x: int, y: int, z: int, state: Union[BlockState, str]) -> None: """絶対座標でブロックを設定する - ``minecraft:oak_stairs[facing=north]`` の形の文字列でも指定できる + ``minecraft:oak_stairs[facing=north]``の形の文字列でも指定できる - 変更したチャンクには印が付き、:meth:`flush` でまとめて書き戻される - 本ライブラリはチャンクを新規生成しないので、存在しない座標はエラーになる + 変更したチャンクには印が付き、:meth:`flush`でまとめて書き戻される + 本ライブラリはチャンクを新規生成しないので、チャンクが存在しない座標はエラーになる """ self._ensure_writable() chunk_x = x >> 4 @@ -275,7 +268,7 @@ def set_block(self, x: int, y: int, z: int, state: Union[BlockState, str]) -> No def get_biome(self, x: int, y: int, z: int) -> Optional[str]: """絶対座標でバイオームを取得する - 4×4×4 の単位 + 4×4×4の単位 """ chunk = self.chunk(x >> 4, z >> 4) @@ -286,7 +279,7 @@ def get_biome(self, x: int, y: int, z: int) -> Optional[str]: def set_biome(self, x: int, y: int, z: int, biome: str) -> None: """絶対座標でバイオームを設定する - 4×4×4 の単位 + 4×4×4の単位 """ self._ensure_writable() chunk_x = x >> 4 @@ -341,7 +334,7 @@ def close(self) -> None: def _folder(self, slot: Optional[RegionFolder], name: str) -> Optional[RegionFolder]: """フォルダを遅延して開く - 存在しなければ None のまま + 存在しなければNoneのまま """ self._ensure_open() @@ -372,7 +365,7 @@ def _ensure_open(self) -> None: class MinecraftWorld: - """Minecraft Java版のセーブデータ 1 つ分""" + """Minecraft Java版のセーブデータ1つ分""" def __init__(self, directory: str, options: WorldOpenOptions, level: NamedTag) -> None: self.directory = directory @@ -386,7 +379,8 @@ def open(directory: str, options: Optional[WorldOpenOptions] = None) -> "MinecraftWorld": """ワールドを開く - :raises SpringNbtError: ディレクトリや ``level.dat`` が無い場合 + :raises SpringNbtError: ディレクトリや``level.dat``が無い場合、 + または書き込みモードで``session.lock``を取得できない場合(``fcntl``が使える環境のみ確かめる) """ if options is None: effective = WorldOpenOptions() @@ -401,7 +395,7 @@ def open(directory: str, if not os.path.exists(level_path): raise SpringNbtError(ErrorCode.IO, "level.dat が無い: %s" % level_path) - # 書き込みで開くときだけ、Minecraft が起動中でないかを確かめる + # 書き込むなら、Minecraftが起動中でないことを先に確かめる if effective.writable and not effective.ignore_session_lock: _check_session_lock(directory) @@ -414,11 +408,10 @@ def __exit__(self, exc_type, exc_value, traceback) -> None: self.close() def data_file(self, name: str) -> Optional[NbtCompound]: - """``data/minecraft/.dat`` を読む - 存在しなければ None + """``data/minecraft/.dat``を読む + 存在しなければNone - 26.x では ``game_rules`` / ``weather`` / ``world_gen_settings`` などが - この形で ``level.dat`` から分離されている + 26.xでは``game_rules`` / ``weather`` / ``world_gen_settings``などがこの形で``level.dat``から分離されている """ self._ensure_open() path = os.path.join(self.directory, "data", "minecraft", name + ".dat") @@ -438,14 +431,14 @@ def dimension_ids(self) -> List[str]: found = [] - # dimensions/<名前空間>/<パス>/ の 2 段を辿る + # dimensions/<名前空間>/<パス>/の2段を辿る for namespace_name in os.listdir(root): namespace_dir = os.path.join(root, namespace_name) if not os.path.isdir(namespace_dir): continue - # 2 段目が次元のパス + # 2段目が次元のパス for path_name in os.listdir(namespace_dir): # ディレクトリだけを次元として数える if os.path.isdir(os.path.join(namespace_dir, path_name)): @@ -457,7 +450,7 @@ def dimension_ids(self) -> List[str]: def dimension(self, dimension_id: str) -> Optional[Dimension]: """次元を得る - ディレクトリが無ければ None + ディレクトリが無ければNone """ self._ensure_open() normalized = _normalize_dimension_id(dimension_id) @@ -491,7 +484,7 @@ def player_ids(self) -> List[str]: def player(self, uuid: str) -> Optional[NbtCompound]: """プレイヤーデータを読む - 存在しなければ None + 存在しなければNone """ self._ensure_open() path = os.path.join(self.directory, "players", "data", uuid + ".dat") @@ -502,10 +495,10 @@ def player(self, uuid: str) -> Optional[NbtCompound]: return read_file(path).tag def save_level(self) -> None: - """``level.dat`` を書き戻す + """``level.dat``を書き戻す - 壊れるとワールド全体が開けなくなるため、 - 一時ファイルへ書いてから ``level.dat_old`` へ退避し、最後に置き換える + ``level.dat``がおかしくなるとワールド全体が開けなくなるため、 + 一時ファイルへ書いて内容をディスクへ確実に書き出してから、既存の``level.dat``を``level.dat_old``へ退避し、最後に置き換える """ self._ensure_open() @@ -516,13 +509,22 @@ def save_level(self) -> None: temporary = path + ".tmp" backup = path + "_old" - write_file(temporary, self.level.to_named_tag()) + encoded = write_bytes(self.level.to_named_tag()) - # 既存の level.dat は、置き換える前に level.dat_old へ退避する - if os.path.exists(path): - shutil.copyfile(path, backup) + try: + # 一時ファイルへ書き、置き換える前に内容をディスクへ確実に書き出す + with open(temporary, "wb") as handle: + handle.write(encoded) + handle.flush() + os.fsync(handle.fileno()) + + # 既存のlevel.datは、置き換える前にlevel.dat_oldへ退避する + if os.path.exists(path): + shutil.copyfile(path, backup) - os.replace(temporary, path) + os.replace(temporary, path) + except OSError as error: + raise SpringNbtError(ErrorCode.IO, "level.dat を書けない: %s" % path) from error def close(self) -> None: """開いている次元をすべて閉じる""" @@ -542,20 +544,18 @@ def _ensure_open(self) -> None: def _check_session_lock(directory: str) -> None: - """``session.lock`` を排他で開けるか確かめる + """``session.lock``を排他で開けるか確かめる - Minecraft は起動中このファイルのロックを保持し続ける + Minecraftは起動中このファイルのロックを保持し続ける ファイルの存在自体は起動していなくても残るので、 ロックが取れるかどうかで判定する - - 仕様: ``docs/spec/40-world-layout.md`` 3章 """ lock_path = os.path.join(directory, "session.lock") if not os.path.exists(lock_path): return - # ロックの手段が無い環境では確認できないので素通しする + # ロックの手段が無い環境では確認できないので、確認せずに進める if fcntl is None: return @@ -576,7 +576,7 @@ def _check_session_lock(directory: str) -> None: def _normalize_dimension_id(dimension_id: str) -> str: - """名前空間が省略されていたら ``minecraft:`` を補う""" + """名前空間が省略されていたら``minecraft:``を補う""" if ":" in dimension_id: return dimension_id diff --git a/python/src/spring_nbt_library/world/paletted_container.py b/python/src/spring_nbt_library/world/paletted_container.py index ac88d4d..890e4c1 100644 --- a/python/src/spring_nbt_library/world/paletted_container.py +++ b/python/src/spring_nbt_library/world/paletted_container.py @@ -1,11 +1,9 @@ """パレットとビットストレージの組 セクション内のブロック状態やバイオームを格納する -パレットの要素は**生の NbtTag のまま**持つ -こうすると、触っていないブロックについては Minecraft が書き出したときの +パレットの要素は**生のNbtTagのまま**持つ +こうすると、触っていないブロックについてはMinecraftが書き出したときの プロパティの並び順まで含めてそのまま書き戻せる - -仕様: ``docs/spec/31-paletted-container.md`` """ from __future__ import annotations @@ -20,12 +18,12 @@ def ceil_log2(count: int) -> int: - """``count`` 個の値を表すのに必要な最小ビット数 - 1 なら 0 + """``count``個の値を表すのに必要な最小ビット数 + 1なら0 """ bits = 0 - # 1 を超える分だけシフトして数える + # 1を超える分だけシフトして数える while (1 << bits) < count: bits += 1 @@ -53,7 +51,7 @@ def palette(self) -> Sequence[NbtTag]: @property def bits_per_entry(self) -> int: """現在のビット幅 - パレットが 1 要素なら 0(記憶域を持たない) + パレットが1要素なら0(記憶域を持たない) """ if self._storage is None: return 0 @@ -70,9 +68,9 @@ def filled(value: NbtTag, entry_count: int, min_bits: int) -> "PalettedContainer @staticmethod def from_nbt(nbt: NbtCompound, entry_count: int, min_bits: int, lenient_bit_storage: bool = False) -> "PalettedContainer": - """NBT から読み込む + """NBTから読み込む - :raises SpringNbtError: パレットが空、data の長さが合わない、添字が範囲外のいずれか + :raises SpringNbtError: パレットが空、dataの長さが合わない、添字がパレット範囲外のいずれか """ result = PalettedContainer(entry_count, min_bits) palette_tag = nbt.opt_list("palette") @@ -80,7 +78,7 @@ def from_nbt(nbt: NbtCompound, entry_count: int, min_bits: int, if palette_tag is None or len(palette_tag) == 0: raise SpringNbtError.malformed("palette が無いか空") - # パレットの要素は生の NbtTag のまま持つ + # パレットの要素は生のNbtTagのまま持つ # 並び順まで元どおりに書き戻すため for entry in palette_tag: result._palette.append(entry) @@ -88,7 +86,7 @@ def from_nbt(nbt: NbtCompound, entry_count: int, min_bits: int, data = nbt.opt_long_array("data") if data is None: - # パレットが 1 要素なら data は無くてよい + # パレットが1要素ならdataは無くてよい if len(result._palette) != 1: raise SpringNbtError.malformed( "palette が %d 要素なのに data が無い" % len(result._palette)) @@ -99,7 +97,7 @@ def from_nbt(nbt: NbtCompound, entry_count: int, min_bits: int, result._storage = BitStorage.from_longs(data, bits, entry_count, lenient_bit_storage) # 取り出した添字がパレットの範囲に収まっているか確かめる - # 黙って 0 番目で代替すると、壊れたデータをそうと分からない形で書き戻してしまう + # エラーを出さずに0番目で代替すると、おかしくなったデータをそうと分からない形で書き戻してしまう for index in range(entry_count): value = result._storage.get(index) @@ -111,7 +109,7 @@ def from_nbt(nbt: NbtCompound, entry_count: int, min_bits: int, return result def to_nbt(self) -> NbtCompound: - """NBT へ変換する""" + """NBTへ変換する""" result = NbtCompound() palette_tag = NbtList() @@ -119,8 +117,8 @@ def to_nbt(self) -> NbtCompound: for entry in self._palette: palette_tag.append(entry) - # パレットが 1 要素なら data は書かない - # Minecraft と同じ振る舞い + # パレットが1要素ならdataは書かない + # Minecraftと同じ振る舞い if self._storage is not None and len(self._palette) > 1: result.set("data", NbtLongArray(self._storage.to_longs())) @@ -131,7 +129,7 @@ def get(self, index: int) -> NbtTag: """添字の値を取り出す""" self._check_index(index) - # 記憶域が無いということは、全エントリがパレットの 0 番目 + # 記憶域が無いということは、全エントリがパレットの0番目 if self._storage is None: return self._palette[0] @@ -144,7 +142,7 @@ def set(self, index: int, value: NbtTag) -> None: self._check_index(index) palette_index = self._index_of_or_add(value) - # 記憶域が無く、書き込む値も 0 番目なら何もしなくてよい + # 記憶域が無く、書き込む値も0番目なら何もしなくてよい if self._storage is None and palette_index == 0: return @@ -152,8 +150,8 @@ def set(self, index: int, value: NbtTag) -> None: self._storage.set(index, palette_index) def fill(self, value: NbtTag) -> None: - """全エントリを 1 つの値で埋める - パレットもその 1 要素だけにする + """全エントリを1つの値で埋める + パレットもその1要素だけにする """ self._palette = [value] self._storage = None @@ -161,7 +159,7 @@ def fill(self, value: NbtTag) -> None: def compact(self) -> None: """どのエントリからも参照されていないパレット要素を取り除き、添字を振り直す - 大量の ``set`` を行う用途で遅くならないよう、明示的に呼んだときだけ実行する + 大量の``set``を行う用途で遅くならないよう、明示的に呼んだときだけ実行する """ if self._storage is None: return @@ -196,7 +194,7 @@ def compact(self) -> None: self._palette = compacted if len(compacted) == 1: - # 1 要素になったら記憶域を捨てる + # 1要素になったら記憶域を捨てる self._storage = None else: self._storage = rebuilt @@ -205,7 +203,7 @@ def _index_of_or_add(self, value: NbtTag) -> int: """パレット内の位置を返す 無ければ末尾へ追加する """ - # パレットは高々 4096 要素なので線形探索で足りる + # パレットは高々4096要素なので線形探索で足りる for index in range(len(self._palette)): if self._palette[index] == value: return index @@ -218,7 +216,7 @@ def _ensure_storage(self) -> None: required = max(self.min_bits, ceil_log2(len(self._palette))) if self._storage is None: - # これまで単一値だったので、全エントリが 0 番目のまま始まる + # これまで単一値だったので、全エントリが0番目のまま始まる self._storage = BitStorage.create(required, self.entry_count) return diff --git a/python/tests/test_anvil.py b/python/tests/test_anvil.py index 2bc2a7f..184d5e4 100644 --- a/python/tests/test_anvil.py +++ b/python/tests/test_anvil.py @@ -1,10 +1,8 @@ -"""Anvil リージョンファイルの読み書き。 +"""Anvilリージョンファイルの読み書き -仕様: docs/spec/20-anvil-region.md - -他言語版と同じ検証項目を持つ。 -共通テストベクタによる言語間比較は spec/run-conformance.sh が担当し、 -ここでは API の振る舞いを直接確かめる。 +他言語版と同じ検証項目を持つ +共通テストベクタによる言語間比較はspec/run-conformance.shが担当し、 +ここではAPIの振る舞いを直接確かめる """ from __future__ import annotations @@ -22,6 +20,7 @@ RegionFile, RegionFileMode, RegionFolder, + RawChunk, RegionPos, ) from spring_nbt_library.nbt import NbtByteArray, NbtCompound, NbtInt, NbtString @@ -31,7 +30,7 @@ def vector_dir(name: str) -> str: - """共通テストベクタのディレクトリ。""" + """共通テストベクタのディレクトリ""" path = os.path.join(VECTORS, name) if not os.path.isdir(path): @@ -42,12 +41,12 @@ def vector_dir(name: str) -> str: @pytest.fixture def work(tmp_path): - """テストごとの一時ディレクトリ。""" + """テストごとの一時ディレクトリ""" return str(tmp_path) def copy_vector(name: str, work: str) -> str: - """ベクタを一時ディレクトリへ複製し、書き込みテストで原本を汚さないようにする。""" + """ベクタを一時ディレクトリへ複製し、書き込みテストで原本を汚さないようにする""" destination = os.path.join(work, name) shutil.copytree(vector_dir(name), destination) return destination @@ -64,11 +63,15 @@ def sample_chunk(x: int, z: int) -> NbtCompound: def incompressible(length: int): - """圧縮しても縮まないバイト列を作る。サイズの制御が効くようにするため。""" + """圧縮しても縮まないバイト列を作る + + サイズを狙いどおりに制御できるようにするため + """ result = [] state = 0x12345678 - # 線形合同法で疑似乱数を作る。テストの再現性を保つため固定の種を使う + # 線形合同法で疑似乱数を作る + # テストの再現性を保つため、固定の種を使う for _ in range(length): state = ((state * 1664525) + 1013904223) & 0xFFFFFFFF value = state >> 24 @@ -151,7 +154,7 @@ def test_reads_every_compression_scheme(): def test_reads_lz4_chunks(): with RegionFile.open(os.path.join(vector_dir("lz4"), "r.0.0.mca")) as region: - # 1 ブロック / 2 ブロック連結 / 無圧縮ブロック / 重なりのあるマッチ + # 1ブロック / 2ブロック連結 / 無圧縮ブロック / 重なりのあるマッチ for x in range(4): assert region.read_chunk_raw(x, 0).compression == ChunkCompression.LZ4 assert region.read_chunk(x, 0).get_int("xPos") == x @@ -168,11 +171,25 @@ def test_rejects_lz4_block_with_broken_magic(): assert error.value.code == ErrorCode.MALFORMED_DATA +def test_truncated_gzip_chunk_is_malformed_data(tmp_path): + path = os.path.join(copy_vector("lz4", tmp_path), "r.0.0.mca") + + with RegionFile.open(path, RegionFileMode.READ_WRITE) as region: + # GZipのヘッダだけで本体が無い、途中で切れたチャンク + truncated = bytes.fromhex("1f8b08000000000000ff") + region.write_chunk_raw(0, 0, RawChunk(ChunkCompression.GZIP, truncated)) + + with pytest.raises(SpringNbtError) as error: + region.read_chunk(0, 0) + + assert error.value.code == ErrorCode.MALFORMED_DATA + + def test_writing_lz4_is_rejected(tmp_path): path = os.path.join(copy_vector("lz4", tmp_path), "r.0.0.mca") with RegionFile.open(path, RegionFileMode.READ_WRITE) as region: - # LZ4 は読み込みのみ対応なので、圧縮して書き出すことはできない + # LZ4は読み込みのみ対応なので、圧縮して書き出すことはできない chunk = region.read_chunk(0, 0) with pytest.raises(SpringNbtError) as error: @@ -187,7 +204,8 @@ def test_untouched_lz4_chunks_keep_their_compression(tmp_path): with open(path, "rb") as handle: before = handle.read() - # 触らずに閉じるだけ。生バイトを素通しするので LZ4 のまま残る + # 触らずに閉じるだけ + # 生バイトに手を加えないので、LZ4のまま残る with RegionFile.open(path, RegionFileMode.READ_WRITE): pass @@ -218,7 +236,7 @@ def test_broken_headers_are_rejected(vector): def test_chunk_outside_the_region_is_rejected(): with RegionFile.open(os.path.join(vector_dir("empty"), "r.0.0.mca")) as region: - # r.0.0 が担当するのは 0..31 の範囲だけ + # r.0.0が担当するのは0..31の範囲だけ with pytest.raises(SpringNbtError) as info: region.has_chunk(32, 0) @@ -239,7 +257,7 @@ def test_read_only_region_rejects_writes(): def test_opening_and_flushing_without_changes_keeps_bytes_identical(work): - # 触っていないチャンクの配置を保つことが、既存ワールドを壊さない前提になる + # 触っていないチャンクの配置を保つことが、既存ワールドをおかしくしないための前提になる directory = copy_vector("fragmented", work) path = os.path.join(directory, "r.0.0.mca") @@ -289,7 +307,7 @@ def test_growing_chunk_is_relocated_without_breaking_others(work): directory = copy_vector("fragmented", work) path = os.path.join(directory, "r.0.0.mca") - # 5 セクタぶんになる大きなチャンクを作る + # 5セクタぶんになる大きなチャンクを作る big = sample_chunk(0, 0) big.set("filler", NbtByteArray(incompressible(5 * SECTOR_SIZE))) @@ -298,7 +316,7 @@ def test_growing_chunk_is_relocated_without_breaking_others(work): region.flush() with RegionFile.open(path) as reopened: - # 動かした結果、他の 2 チャンクが壊れていないこと + # 動かした結果、他の2チャンクがおかしくなっていないこと assert len(reopened.chunk_positions()) == 3 assert reopened.read_chunk(5, 3).get_int("xPos") == 5 assert reopened.read_chunk(31, 31).get_int("xPos") == 31 @@ -361,7 +379,7 @@ def test_optimize_compacts_the_file(work): def test_huge_chunk_goes_to_external_file_and_comes_back(work): path = os.path.join(work, "r.0.0.mca") - # 1MiB を超えるよう、圧縮の効かないデータを詰める + # 1MiBを超えるよう、圧縮しても縮まないデータを詰める huge = sample_chunk(1, 2) huge.set("filler", NbtByteArray(incompressible(1200 * 1024))) @@ -411,7 +429,7 @@ def test_folder_resolves_chunks_across_regions(work): folder.write_chunk(40, 40, sample_chunk(40, 40)) folder.flush() - # 3 つの異なるリージョンへ振り分けられる + # 3つの異なるリージョンへ振り分けられる for name in ["r.0.0.mca", "r.-1.-1.mca", "r.1.1.mca"]: assert os.path.exists(os.path.join(work, name)) @@ -423,9 +441,27 @@ def test_folder_resolves_chunks_across_regions(work): assert not reopened.has_chunk(100, 100) +def test_read_write_folder_creates_missing_directory_only_when_writing(work): + missing = os.path.join(work, "missing", "region") + + # 読むだけなら、無いディレクトリは作らない + with RegionFolder.open(missing, RegionFileMode.READ_WRITE) as folder: + assert folder.read_chunk(0, 0) is None + + assert not os.path.exists(missing) + + # 書き出すときに作る + with RegionFolder.open(missing, RegionFileMode.READ_WRITE) as folder: + folder.write_chunk(0, 0, sample_chunk(0, 0)) + + with RegionFolder.open(missing) as reopened: + assert reopened.read_chunk(0, 0).get_int("xPos") == 0 + + def test_キャッシュ上限を超えると古いリージョンから閉じる(work): - """RegionFile はファイル全体をメモリへ載せるので、上限が要る。""" - # 上限 2 で 4 リージョンへ書く。古いものは閉じられるが内容は失われない + """RegionFileはファイル全体をメモリへ載せるので、上限が要る""" + # 上限2で4リージョンへ書く + # 古いものは閉じられるが、内容は失われない with RegionFolder.open(work, RegionFileMode.READ_WRITE, max_cached_regions=2) as folder: for region in range(4): folder.write_chunk(region * 32, 0, sample_chunk(region * 32, 0)) diff --git a/python/tests/test_block_pos.py b/python/tests/test_block_pos.py index 63da849..60440c9 100644 --- a/python/tests/test_block_pos.py +++ b/python/tests/test_block_pos.py @@ -1,6 +1,4 @@ -"""ブロック座標と範囲の単体テスト。 - -仕様: docs/spec/30-chunk-format.md 5章 +"""ブロック座標と範囲の単体テスト """ from __future__ import annotations @@ -51,7 +49,7 @@ def test_walks_every_position_inside(): assert len(positions) == box.volume assert len(positions) == 2 * 3 * 4 - # 並びは Y、Z、X の順で、X がいちばん内側で動く + # 並びはY、Z、Xの順で、Xがいちばん内側で動く assert positions[0] == BlockPos(0, 0, 0) assert positions[1] == BlockPos(1, 0, 0) assert positions[2] == BlockPos(0, 0, 1) diff --git a/python/tests/test_nbt.py b/python/tests/test_nbt.py index d64d60e..2a6b5e3 100644 --- a/python/tests/test_nbt.py +++ b/python/tests/test_nbt.py @@ -1,8 +1,8 @@ -"""NBT レイヤの単体テスト。 +"""NBTレイヤの単体テスト -C# 版 (csharp/tests/SpringNBTLibrary.Tests/) と同じ検証項目を持つ。 -共通テストベクタによる言語間比較は spec/run-conformance.sh が担当し、 -ここでは API の振る舞いを直接確かめる。 +C#版 (csharp/tests/SpringNBTLibrary.Tests/) と同じ検証項目を持つ +共通テストベクタによる言語間比較はspec/run-conformance.shが担当し、 +ここではAPIの振る舞いを直接確かめる """ from __future__ import annotations @@ -50,7 +50,7 @@ def uncompressed_write() -> NbtWriteOptions: def hello_world_bytes() -> bytes: - """仕様書どおりに組んだ最小の NBT。""" + """仕様書どおりに組んだ最小のNBT""" parts = [] # ルート: TAG_Compound、名前 "hello world" @@ -126,6 +126,14 @@ def test_reads_hand_built_hello_world(self): assert len(named.tag) == 1 assert named.tag.get_string("name") == "Bananrama" + def test_nan_bit_patterns_are_written_back_unchanged(self): + # ペイロード付き・負・シグナリングのNaNを、Float / Doubleの両方で持つCompound + original = bytes.fromhex( + "0a0000050001617fc0000105000162ffc00000050001637f800001060001647ff800000000000106000165fff8000000000000060001667ff000000000000100") + named = read_bytes(original, uncompressed_read()) + + assert write_bytes(named, uncompressed_write()) == original + def test_writes_back_the_same_bytes(self): original = hello_world_bytes() named = read_bytes(original, uncompressed_read()) @@ -149,7 +157,7 @@ def test_float_specials_keep_their_bit_pattern(self): encoded = write_bytes(NamedTag("", root), uncompressed_write()) decoded = read_bytes(encoded, uncompressed_read()).tag - # -0.0 と +0.0 は == では区別できないので、ビットパターンで比較する + # -0.0と+0.0は == では区別できないので、ビットパターンで比較する assert struct.pack(">d", decoded.get_double("negative_zero")) == struct.pack(">d", -0.0) assert decoded.get_float("nan") != decoded.get_float("nan") assert decoded.get_double("infinity") == float("inf") @@ -188,11 +196,20 @@ def test_list_rejects_mixed_types(self): assert info.value.code == ErrorCode.UNEXPECTED_TAG_TYPE + def test_list_rejects_out_of_range_position_without_fixing_element_type(self): + values = NbtList() + + with pytest.raises(IndexError): + values[0] = NbtInt(1) + + # 失敗した操作で要素型が確定していない + assert values.element_type == TagType.END + def test_typed_getter_distinguishes_missing_key_from_wrong_type(self): root = NbtCompound() root.set("value", NbtString("text")) - # キーが無い場合は None + # キーが無い場合はNone assert root.opt_int("missing") is None # 型が違う場合はキーの有無に関わらず例外 @@ -204,6 +221,20 @@ def test_typed_getter_distinguishes_missing_key_from_wrong_type(self): root.get_int("missing") assert missing.value.code == ErrorCode.INVALID_ARGUMENT + def test_broken_compressed_data_is_malformed_data(self): + named = read_bytes(hello_world_bytes(), uncompressed_read()) + gzip_bytes = write_bytes(named, NbtWriteOptions(compression=Compression.GZIP)) + truncated = gzip_bytes[:len(gzip_bytes) // 2] + broken_gzip = bytes.fromhex("1f8b0800000000000000ffffffffff") + broken_zlib = bytes.fromhex("789cffffffffffff") + + # 途中で切れたGZip、中身の壊れたGZip、中身の壊れたZlibは、どれも仕様に反するデータとして扱う + for data in (truncated, broken_gzip, broken_zlib): + with pytest.raises(SpringNbtError) as info: + read_bytes(data) + + assert info.value.code == ErrorCode.MALFORMED_DATA + @pytest.mark.parametrize("method", [Compression.GZIP, Compression.ZLIB, Compression.NONE]) def test_compression_is_detected_automatically(self, method): named = read_bytes(hello_world_bytes(), uncompressed_read()) @@ -211,7 +242,7 @@ def test_compression_is_detected_automatically(self, method): assert detect_compression(encoded) == method - # 既定の ReadOptions は AUTO なので、方式を指定しなくても読める + # 既定のReadOptionsはAUTOなので、方式を指定しなくても読める assert read_bytes(encoded).tag.get_string("name") == "Bananrama" def test_network_format_has_no_root_name(self): @@ -222,7 +253,7 @@ def test_network_format_has_no_root_name(self): NamedTag("ignored", root), NbtWriteOptions(fmt=NbtFormat.NETWORK, compression=Compression.NONE)) - # タグID + ペイロード のみで、名前長の 2 バイトが無い + # タグID + ペイロードのみで、名前長の2バイトが無い assert encoded[0] == 0x0A assert encoded[1] == 0x03 @@ -239,7 +270,7 @@ def test_truncated_input_is_rejected(self): assert info.value.code == ErrorCode.MALFORMED_DATA def test_huge_declared_length_is_rejected_before_allocating(self): - # ルート直下に「長さ 0x7FFFFFFF の ByteArray」を宣言するだけの入力 + # ルート直下に「長さ0x7FFFFFFFのByteArray」を宣言するだけの入力 data = (bytes([0x0A]) + bytes([0x00, 0x00]) + bytes([0x07]) + bytes([0x00, 0x01]) + b"a" + bytes([0x7F, 0xFF, 0xFF, 0xFF])) @@ -289,21 +320,23 @@ def test_write_rejects_auto_compression(self): (NbtLong, 9223372036854775808), ]) def test_integer_range_is_checked_on_construction(self, factory, value): - # Python の int には幅が無いため、構築時に範囲を検査する + # Pythonのintには幅が無いため、構築時に範囲を検査する with pytest.raises(SpringNbtError) as info: factory(value) assert info.value.code == ErrorCode.INVALID_ARGUMENT def test_float_is_rounded_to_binary32_on_construction(self): - # 他言語と同じ値になるよう、構築時に binary32 へ丸める + # 他言語と同じ値になるよう、構築時にbinary32へ丸める value = NbtFloat(0.1).value assert struct.pack(">f", value) == struct.pack(">f", 0.1) assert value != 0.1 def build_all_tags() -> NbtCompound: - """全13タグ型を含む Compound を作る。""" + """TAG_Endを除く12種のタグ型をすべて含むCompoundを作る + TAG_EndはCompoundの終端として書き出されるので、往復させれば13種すべてを読み書きする + """ root = NbtCompound() root.set("byte", NbtByte(-128)) root.set("short", NbtShort(32767)) @@ -330,10 +363,10 @@ def build_all_tags() -> NbtCompound: def build_nested_compound(depth: int) -> bytes: - """指定した深さまで Compound を入れ子にしたバイト列を作る。""" + """指定した深さまでCompoundを入れ子にしたバイト列を作る""" parts = [bytes([0x0A]), bytes([0x00, 0x00])] - # ルート + (depth - 1) 段の入れ子 + # ルート + (depth - 1)段の入れ子 for _ in range(depth - 1): parts.append(bytes([0x0A])) parts.append(bytes([0x00, 0x01])) @@ -379,14 +412,16 @@ def test_extended_integer_literals(self, source, expected): assert snbt.parse(source) == NbtInt(expected) def test_hex_suffix_rule_is_fixed(self): - # 仕様 11 の 2.1: 16進では b/d/f を数字として読む。幅接尾辞は s/l のみ + # 16進ではb/d/fを数字として読む + # 幅接尾辞はs/lのみ assert snbt.parse("0xFF") == NbtInt(255) assert snbt.parse("0xFFb") == NbtInt(4091) assert snbt.parse("0xFFl") == NbtLong(255) assert snbt.parse("0xFFs") == NbtShort(255) def test_zero_byte_literal_is_not_binary(self): - # 0b は「10進の 0 に Byte 接尾辞」。真偽値の false として広く使われる形 + # 0bは「10進の0にByte接尾辞」 + # 真偽値のfalseとして広く使われる形 assert snbt.parse("0b") == NbtByte(0) assert snbt.parse("0b1") == NbtInt(1) assert snbt.parse("0b1001b") == NbtByte(9) @@ -413,7 +448,7 @@ def test_typed_arrays(self): assert snbt.parse("[I; 1, 2]") == NbtIntArray([1, 2]) assert snbt.parse("[L; 1L, 2L]") == NbtLongArray([1, 2]) - # 接尾辞なしでも範囲内なら受理する(Minecraft 自身がそう書き出すため) + # 接尾辞なしでも範囲内なら受理する(Minecraft自身がそう書き出すため) assert snbt.parse("[B; 1, 2]") == NbtByteArray([1, 2]) def test_typed_array_range_is_checked(self): @@ -427,7 +462,7 @@ def test_trailing_commas_are_allowed(self): assert len(snbt.parse("[1,2,]")) == 2 def test_heterogeneous_list_is_rejected(self): - # 異種リストはバイナリ NBT へ写せないため受理しない (adr/0006) + # 異種リストはバイナリNBTへ写せないため受理しない with pytest.raises(SpringNbtError) as info: snbt.parse('[1, "a"]') @@ -440,6 +475,32 @@ def test_escape_sequences(self): assert snbt.parse('"\\s"') == NbtString(" ") assert snbt.parse('"\\U0001F600"') == NbtString("\U0001F600") + def test_surrogate_escapes_follow_the_same_rules_everywhere(self): + lone = chr(0xD800) + + # \Uでサロゲートの範囲を書くと、\uと同じく孤立サロゲートになる + assert snbt.parse('"\\U0000D800"') == NbtString(lone) + assert snbt.parse('"\\uD800"') == NbtString(lone) + + # 対になったサロゲートは補助文字1文字と同じ + assert snbt.parse('"\\uD83D\\uDE00"') == NbtString(chr(0x1F600)) + assert NbtString(chr(0xD83D) + chr(0xDE00)) == NbtString(chr(0x1F600)) + + # コードポイントの範囲外と、キーの孤立サロゲートは仕様に反する + for source in ('"\\U00110000"', '{"\\uD800":1}', '{"\\U0000D800":1}'): + with pytest.raises(SpringNbtError) as info: + snbt.parse(source) + + assert info.value.code == ErrorCode.MALFORMED_DATA + + # APIからも孤立サロゲートのキーは設定できない + compound = NbtCompound() + + with pytest.raises(SpringNbtError) as info: + compound.set(lone, NbtInt(1)) + + assert info.value.code == ErrorCode.INVALID_ARGUMENT + def test_named_character_escape_is_unsupported(self): with pytest.raises(SpringNbtError) as info: snbt.parse('"\\N{SNOWMAN}"') @@ -499,10 +560,10 @@ def test_parse_compound_rejects_non_compound_root(self): class TestCanonicalDecimal: - """仕様: docs/spec/11-snbt.md 5.1章 + """浮動小数点の正準10進表記 - ここが言語ごとにずれると SNBT 出力の言語間一致が崩れるため、 - 期待値は仕様の記述から手で書き下している。 + この表記が言語ごとにずれると、SNBT出力が言語間で一致しなくなるため、 + 期待値は仕様の記述から手で書き下している """ @pytest.mark.parametrize("value,expected", [ @@ -545,6 +606,20 @@ def test_special_values(self): assert snbt.write(NbtDouble(float("-inf"))) == "-Infinityd" assert snbt.write(NbtFloat(float("nan"))) == "NaNf" + def test_float_out_of_range_becomes_signed_infinity(self): + # binary32の範囲を超える値は、符号付きの無限大になる + assert snbt.parse("1e39f") == NbtFloat(float("inf")) + assert snbt.parse("-1e39f") == NbtFloat(float("-inf")) + assert NbtFloat(1e39).value == float("inf") + assert NbtFloat(-(10 ** 400)).value == float("-inf") + + # binary32の最大値へ丸まる値は無限大にしない + assert NbtFloat(3.4028235e38).value == struct.unpack(">f", bytes.fromhex("7f7fffff"))[0] + + values = NbtFloat(0.0) + values.value = 1e39 + assert values.value == float("inf") + def test_every_formatted_value_parses_back_to_the_same_bits(self): doubles = [0.0, -0.0, 1.0, -1.0, 0.1, 1.0 / 3.0, 1e300, 1e-300, 4903.0] @@ -564,9 +639,8 @@ def test_every_formatted_value_parses_back_to_the_same_bits(self): class TestTagEquality: """タグの等値比較と深い複製 - 仕様: docs/spec/10-nbt-binary.md 7.3 規則は全言語で同じでなければならない - 各言語の同名テストと突き合わせて読むこと + 各言語の同名テストと見比べながら読むこと """ def test_same_type_same_value_is_equal(self): @@ -578,13 +652,13 @@ def test_same_type_same_value_is_equal(self): assert NbtByteArray([1]) != NbtByteArray([1, 2]) def test_different_tag_type_is_not_equal(self): - # 値が同じでもタグの型が違えば別物 + # 値が同じでもタグの型が違えば等しくない assert NbtInt(1) != NbtShort(1) assert NbtInt(1) != 1 assert NbtInt(1) is not None def test_floats_compare_by_bit_pattern(self): - # NaN 同士は等しく、+0.0 と -0.0 は等しくない + # NaN同士は等しく、+0.0と-0.0は等しくない assert NbtFloat(float("nan")) == NbtFloat(float("nan")) assert NbtDouble(float("nan")) == NbtDouble(float("nan")) assert NbtFloat(0.0) != NbtFloat(-0.0) @@ -605,7 +679,7 @@ def test_list_compares_element_type_and_order(self): reversed_list.append(NbtInt(1)) assert left != reversed_list - # 空でも要素型が違えば別物 + # 空でも要素型が違えば等しくない assert NbtList(TagType.INT) != NbtList(TagType.BYTE) def test_compound_compares_insertion_order(self): @@ -618,7 +692,7 @@ def test_compound_compares_insertion_order(self): same.set("b", NbtInt(2)) assert left == same - # 中身は同じでも挿入順が違えば別物 + # 中身は同じでも挿入順が違えば等しくない reordered = NbtCompound() reordered.set("b", NbtInt(2)) reordered.set("a", NbtInt(1)) @@ -639,9 +713,7 @@ def test_copy_is_deep(self): class TestConcatenatedNbt: - """連なった NBT の読み込み - - 仕様: docs/spec/10-nbt-binary.md 3.1章 + """連なったNBTの読み込み """ def test_reads_concatenated_nbt_one_by_one(self): @@ -688,8 +760,6 @@ def test_rejects_compression_when_reading_at_offset(self): class TestTypedSetters: """型付き設定子 - - 仕様: docs/spec/10-nbt-binary.md 7.1章 """ def test_mirror_the_getters(self): @@ -720,7 +790,7 @@ def test_mirror_the_getters(self): assert root.get_int_array("ia") == [1, -1] assert root.get_long_array("la") == [1, -1] - # 真偽値は TAG_Byte の 0 / 1 として入る + # 真偽値はTAG_Byteの0 / 1として入る assert root.get("t").type == TagType.BYTE def test_keep_the_insertion_order(self): diff --git a/python/tests/test_world.py b/python/tests/test_world.py index 48fd9c4..29f3a7a 100644 --- a/python/tests/test_world.py +++ b/python/tests/test_world.py @@ -1,10 +1,7 @@ -"""World / Block レイヤ。 +"""World / Blockレイヤ -仕様: docs/spec/30-chunk-format.md / 31-paletted-container.md / 40-world-layout.md - -他言語版と同じ検証項目を持つ。 -共通テストベクタによる言語間比較は spec/run-conformance.sh が担当し、 -ここでは API の振る舞いを直接確かめる。 +他言語版と同じ検証項目を持つ +共通テストベクタによる言語間比較はspec/run-conformance.shが担当し、ここではAPIの振る舞いを直接確かめる """ from __future__ import annotations @@ -23,6 +20,7 @@ NbtWriteOptions, read_file, write_bytes, + write_file, ) from spring_nbt_library.world import ( BitStorage, @@ -33,15 +31,17 @@ MinecraftWorld, PalettedContainer, VersionMismatchAction, + WorldOpenOptions, ceil_log2, ) +from spring_nbt_library.anvil import RegionFile, RegionFileMode REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) VECTORS = os.path.join(REPO_ROOT, "spec", "testdata", "world") def vector_path(name: str) -> str: - """共通テストベクタ(world/*.nbt)のパス。""" + """共通テストベクタ(world/*.nbt)のパス""" path = os.path.join(VECTORS, name + ".nbt") if not os.path.isfile(path): @@ -51,19 +51,20 @@ def vector_path(name: str) -> str: def load_chunk(name: str, options: ChunkReadOptions = None) -> Chunk: - """テストベクタをチャンクとして読む。""" + """テストベクタをチャンクとして読む""" return Chunk.from_nbt(read_file(vector_path(name)).tag, options) def block_entry(name: str) -> NbtCompound: - """ブロックのパレット要素を作る。""" + """ブロックのパレット要素を作る""" entry = NbtCompound() entry.set("Name", NbtString(name)) return entry class TestBlockState: - """ブロック状態の文字列表現。仕様: docs/spec/30-chunk-format.md 2.1.1章""" + """ブロック状態の文字列表現 + """ def test_名前空間を省略するとminecraftが補われる(self): state = BlockState.parse("stone") @@ -120,7 +121,8 @@ def test_壊れた文字列はINVALID_ARGUMENT(self, text): class TestBitStorage: - """跨ぎなしのビット詰め。仕様: docs/spec/31-paletted-container.md 2章""" + """跨ぎなしのビット詰め + """ @pytest.mark.parametrize("bits,entry_count,expected", [ (4, 4096, 256), @@ -144,7 +146,8 @@ def test_書いた値をそのまま読み出せる(self): def test_long境界を跨がずに詰める(self): storage = BitStorage.create(5, 4096) - # bits=5 なら 1 つの long に 12 個。12 個目は次の long の最下位から始まる + # bits=5なら1つのlongに12個 + # 添字12は次のlongの最下位から始まる storage.set(11, 31) storage.set(12, 1) longs = storage.to_longs() @@ -172,7 +175,7 @@ def test_ビット幅に対して長さが合わない配列はMALFORMED_DATA(se assert error.value.code == ErrorCode.MALFORMED_DATA def test_寛容モードなら長さからビット幅を逆算する(self): - # 4096 エントリを 342 long で表せるのは bits=5 のときだけ + # 4096エントリを342 longで表せるのはbits=5のときだけ storage = BitStorage.from_longs([0] * 342, 4, 4096, lenient=True) assert storage.bits_per_entry == 5 @@ -194,7 +197,8 @@ def test_範囲外の添字はINVALID_ARGUMENT(self): class TestPalettedContainer: - """パレット付きコンテナ。仕様: docs/spec/31-paletted-container.md""" + """パレット付きコンテナ + """ @pytest.mark.parametrize("count,expected", [(1, 0), (2, 1), (4, 2), (5, 3), (17, 5)]) def test_必要ビット数はceil_log2(self, count, expected): @@ -211,7 +215,7 @@ def test_単一値のコンテナはdataを持たない(self): def test_値を足すとパレットとビット幅が広がる(self): container = PalettedContainer.filled(block_entry("minecraft:air"), 4096, 4) - # パレットを 17 要素まで増やして bits=4 から 5 への拡張を起こす + # 17種のブロックを足してパレットを18要素にし、bits=4から5への拡張を起こす for index in range(17): container.set(index, block_entry("minecraft:block_%d" % index)) @@ -234,7 +238,7 @@ def test_compactで未使用のパレット要素が消える(self): container.compact() - # 残るのは実際に使われている air と dirt の 2 つ + # 残るのは実際に使われているairとdirtの2つ assert len(container.palette) == 2 assert container.get(0).get_string("Name") == "minecraft:dirt" @@ -257,7 +261,8 @@ def test_範囲外の添字はINVALID_ARGUMENT(self): class TestChunk: - """チャンクの解釈。仕様: docs/spec/30-chunk-format.md""" + """チャンクの解釈 + """ def test_パレット1要素のチャンクを読める(self): chunk = load_chunk("palette_1") @@ -275,7 +280,8 @@ def test_ビット幅5のチャンクを端から端まで読める(self): chunk = load_chunk("palette_17") head = ["minecraft:air", "minecraft:stone"] - # ベクタの添字は (位置 * 11) % 17。パレット先頭 2 つだけ名前が違う + # ベクタの添字は (位置 * 11) % 17 + # パレットの先頭2つだけ名前が違う for position in range(4096): palette_index = (position * 11) % 17 block = chunk.get_block(position & 15, -64 + (position >> 8), (position >> 4) & 15) @@ -316,7 +322,7 @@ def test_変更したチャンクには印が付く(self): chunk.set_block(3, -60, 7, "minecraft:stone") assert chunk.is_modified is True - # 保存済みとして印を下ろせる + # 保存済みとして印を外せる chunk.is_modified = False assert chunk.is_modified is False @@ -331,7 +337,7 @@ def test_バイオームは4ブロック単位で効く(self): chunk = load_chunk("palette_1") chunk.set_biome(0, -64, 0, "minecraft:desert") - # 同じ 4×4×4 の枠内はまとめて変わる + # 同じ4×4×4の枠内はまとめて変わる assert chunk.get_biome(3, -61, 3) == "minecraft:desert" assert chunk.get_biome(4, -64, 0) == "minecraft:plains" @@ -361,13 +367,13 @@ def test_ブロックを置き換えると同じ座標の付随データが消 assert len(chunk.raw.get_list("block_ticks")) == 2 assert len(chunk.raw.get_list("fluid_ticks")) == 1 - # (0,-64,0) には chest と block_tick、(1,-64,1) には furnace と fluid_tick がある + # (0,-64,0)にはchestとblock_tick、(1,-64,1)にはfurnaceとfluid_tickがある chunk.set_block(0, -64, 0, BlockState.parse("minecraft:stone")) chunk.set_block(1, -64, 1, BlockState.parse("minecraft:stone")) entities = chunk.raw.get_list("block_entities") - # 触っていない (15,-50,15) の barrel だけが残る + # 触っていない(15,-50,15)のbarrelだけが残る assert len(entities) == 1 assert entities[0].get_string("id") == "minecraft:barrel" @@ -388,8 +394,7 @@ def test_同じブロックを置き直しても付随データは消えない(s assert len(chunk.raw.get_list("block_ticks")) == 2 def test_別のチャンクの同じ相対座標は消さない(self): - # 付随データは絶対座標で持つので、チャンク座標を取り違えると - # 無関係な要素を消してしまう + # 付随データは絶対座標で持つので、チャンク座標を間違えると無関係な要素を消してしまう root = read_file(vector_path("block_entities")).tag root.set("xPos", NbtInt(1)) root.set("zPos", NbtInt(1)) @@ -397,7 +402,8 @@ def test_別のチャンクの同じ相対座標は消さない(self): chunk = Chunk.from_nbt(root) chunk.set_block(0, -64, 0, BlockState.parse("minecraft:stone")) - # このチャンクの (0,-64,0) は絶対座標 (16,-64,16)。どれとも一致しない + # このチャンクの(0,-64,0)は絶対座標(16,-64,16) + # どれとも一致しない assert len(chunk.raw.get_list("block_entities")) == 3 def test_高さマップと光源を無効化できる(self): @@ -431,11 +437,12 @@ def test_チャンク内の相対座標が範囲外ならINVALID_ARGUMENT(self): class TestDataVersion: - """バージョンポリシー。仕様: docs/spec/30-chunk-format.md 5章""" + """バージョンポリシー + """ @staticmethod def foreign_chunk() -> NbtCompound: - """DataVersion だけを差し替えたチャンクを作る。""" + """DataVersionだけを差し替えたチャンクを作る""" root = read_file(vector_path("palette_1")).tag root.set("DataVersion", NbtInt(3953)) return root @@ -482,7 +489,7 @@ def test_許可すれば古いチャンクも元のバージョンのまま書 self.foreign_chunk(), ChunkReadOptions(on_version_mismatch=VersionMismatchAction.IGNORE)) - # DataVersion は読んだ値のまま残す + # DataVersionは読んだ値のまま残す written = chunk.to_nbt(ChunkWriteOptions(allow_foreign_data_version=True)) assert written.get_int("DataVersion") == 3953 @@ -492,7 +499,7 @@ def test_対象バージョンのチャンクはそのまま書き戻せる(self @staticmethod def newer_chunk() -> NbtCompound: - """対象より新しい DataVersion を持つチャンクを作る。""" + """対象より新しいDataVersionを持つチャンクを作る""" root = read_file(vector_path("palette_1")).tag root.set("DataVersion", NbtInt(5015)) return root @@ -503,7 +510,7 @@ def test_新しいバージョンのチャンクは警告を出さない(self): on_version_mismatch=VersionMismatchAction.WARN, on_warning=warnings.append) - # 形式が同じであれば、新しいバージョンでも黙って読める + # 形式が同じであれば、新しいバージョンでも警告を出さずに読める chunk = Chunk.from_nbt(self.newer_chunk(), options) assert chunk.data_version == 5015 assert warnings == [] @@ -515,12 +522,13 @@ def test_新しいバージョンのチャンクはERRORでも通る(self): def test_新しいバージョンのチャンクはそのまま書き戻せる(self): chunk = Chunk.from_nbt(self.newer_chunk()) - # 許可を出さなくても書き戻せて、DataVersion も変わらない + # 許可を出さなくても書き戻せて、DataVersionも変わらない assert chunk.to_nbt().get_int("DataVersion") == 5015 class TestMinecraftWorld: - """ワールドを開く。仕様: docs/spec/40-world-layout.md""" + """ワールドを開く + """ def test_存在しないディレクトリはIO(self, tmp_path): with pytest.raises(SpringNbtError) as error: @@ -533,3 +541,27 @@ def test_levelDatが無いディレクトリはIO(self, tmp_path): MinecraftWorld.open(str(tmp_path)) assert error.value.code == ErrorCode.IO + + def test_次元経由で読んだチャンクの警告も通知先へ届く(self, tmp_path): + region = tmp_path / "dimensions" / "minecraft" / "overworld" / "region" + region.mkdir(parents=True) + + # DataVersionが扱える形式より古いチャンクを1つだけ持つワールドを作る + level = NbtCompound() + level.set("Data", NbtCompound()) + write_file(str(tmp_path / "level.dat"), NamedTag("", level)) + + chunk = read_file(vector_path("palette_1")).tag + chunk.set("DataVersion", NbtInt(3953)) + + with RegionFile.open(str(region / "r.0.0.mca"), RegionFileMode.READ_WRITE) as file: + file.write_chunk(0, 0, chunk) + + warnings = [] + options = WorldOpenOptions(chunk_read=ChunkReadOptions(on_warning=warnings.append)) + + with MinecraftWorld.open(str(tmp_path), options) as world: + assert world.dimension("minecraft:overworld").chunk(0, 0) is not None + + # ワールドを開くときに渡した通知先が、次元経由の読み込みでも呼ばれる + assert len(warnings) == 1 diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 674719d..5854c12 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -4,7 +4,7 @@ version = "1.0.0" edition = "2021" rust-version = "1.75" license = "MIT" -description = "Minecraft Java Edition の NBT / Anvil ワールドデータを読み書きするライブラリ" +description = "Minecraft Java EditionのNBT / Anvilワールドデータを読み書きするライブラリ" repository = "https://github.com/scriptarts/SpringNBTLibrary" readme = "README.md" keywords = ["minecraft", "nbt", "anvil", "mca"] diff --git a/rust/README.md b/rust/README.md index ab28f31..8e8edc3 100644 --- a/rust/README.md +++ b/rust/README.md @@ -1,34 +1,34 @@ -# SpringNBTLibrary(Rust 版) +# SpringNBTLibrary(Rust版) -Minecraft Java版 26.1 以降のワールド・NBTファイルを読み書きするライブラリです。 -Rust 2021 edition、MSRV 1.75 です。 +Minecraft Java版26.1以降のワールド・NBTファイルを読み書きするライブラリです。 +Rust 2021 edition、MSRV 1.75です。 -いまのワールド形式は 26.1 で入ったものです。 +いまのワールド形式は26.1で入ったものです。 それ以降のバージョンなら、形式が変わらない限りそのまま扱えます。 -C# / Java / TypeScript / Python / Rust 版があり、どれも同じ仕様書から作っています。 +C# / Java / TypeScript / Python / Rust版があり、どれも同じ仕様書から作っています。 同じ入力からは同じ結果が出ます([対応言語の一覧](../README.md#対応言語))。 -揃っているかは[適合性テスト](../docs/spec/90-conformance.md)で毎回確かめています。 +結果が揃っているかは[適合性テスト](../docs/spec/90-conformance.md)で毎回確かめています。 ## 導入 -Cargo.toml に git 参照を書きます。 +Cargo.tomlにgit参照を書きます。 ```toml [dependencies] spring-nbt-library = { git = "https://github.com/ScriptArts/SpringNBTLibrary", tag = "v1.0.0" } ``` -ネットワークに繋がらない環境向けに、[Releases](https://github.com/ScriptArts/SpringNBTLibrary/releases) へ `.crate` も置いています。 +ネットワークに繋がらない環境向けに、[Releases](https://github.com/ScriptArts/SpringNBTLibrary/releases)へ`.crate`も置いています。 ## 使い方 -- [はじめに(Rust)](../docs/getting-started/rust.md) — まずここから -- [ガイド](../docs/guide/01-nbt.md) — 目的別の使い方 -- [API 対応表](../docs/api/overview.md) — 他言語版との対応 -- [機能一覧](../docs/features.md) — 何ができて何ができないか +- [はじめに(Rust)](../docs/getting-started/rust.md): まずここから +- [ガイド](../docs/guide/01-nbt.md): 目的別の使い方 +- [API対応表](../docs/api/overview.md): 他言語版との対応 +- [機能一覧](../docs/features.md): 何ができて何ができないか -説明は [`docs/`](../docs/README.md) にまとめてあります。 +説明は[`docs/`](../docs/README.md)にまとめてあります。 ## ライセンス diff --git a/rust/examples/conformance.rs b/rust/examples/conformance.rs index 693c396..03ee614 100644 --- a/rust/examples/conformance.rs +++ b/rust/examples/conformance.rs @@ -1,9 +1,8 @@ -//! 適合性検証ツール。4言語すべてが同じインターフェースで同じ出力を出す。 +//! 適合性検証ツール +//! 全言語が同じインターフェースで同じ出力を出す //! -//! `spec/run-conformance.sh` がこのツールを4言語ぶん起動し、 -//! 出力を相互に diff することで「4言語が同一に振る舞う」ことを機械的に確かめる。 -//! -//! 仕様: `docs/spec/90-conformance.md` 2.3章 +//! `spec/run-conformance.sh`がこのツールを全言語ぶん起動し、 +//! 出力を相互にdiffすることで「全言語が同一に振る舞う」ことを機械的に確かめる use std::process::ExitCode; @@ -48,7 +47,7 @@ fn main() -> ExitCode { | "chunk-report" | "chunk-edit" => match run(&args) { Ok(code) => code, Err(error) => { - // 4言語で同じ ErrorCode を出すことが検証対象なので、コードを機械可読な形で出す + // 言語間で同じErrorCodeを出すことが検証対象なので、コードを機械可読な形で出す eprint!("ERROR {} {}\n", error.code().as_str(), error.message()); ExitCode::FAILURE } @@ -60,7 +59,7 @@ fn main() -> ExitCode { } } -/// 連なった NBT を、位置を追いながら一覧として書き出す +/// 連なったNBTを、位置を追いながら一覧として書き出す fn nbt_list(data: &[u8], format: NbtFormat) -> Result { let options = NbtReadOptions { format, compression: Compression::None, ..NbtReadOptions::default() }; @@ -95,7 +94,7 @@ fn run(args: &[String]) -> Result { return Ok(ExitCode::from(2)); } - // チャンク系は World 層を通す + // チャンク系はWorld層を通す if args[0] == "chunk-report" || args[0] == "chunk-edit" { let mut chunk = read_chunk_file(&args[1])?; @@ -110,14 +109,14 @@ fn run(args: &[String]) -> Result { return Ok(ExitCode::SUCCESS); } - // 連なった NBT を一覧にする + // 連なったNBTを一覧にする if args[0] == "nbt-list" { let bytes = std::fs::read(&args[1])?; std::fs::write(&args[2], nbt_list(&bytes, parse_format(args))?)?; return Ok(ExitCode::SUCCESS); } - // リージョン系は NBT の読み込みを経由しない + // リージョン系はNBTの読み込みを経由しない if args[0] == "region-list" || args[0] == "region-rewrite" { let mut region = RegionFile::open(&args[1], RegionFileMode::ReadOnly)?; @@ -152,9 +151,9 @@ fn run(args: &[String]) -> Result { Ok(ExitCode::SUCCESS) } -/// `--format network` が指定されていればネットワーク形式として読む。 +/// `--format network`が指定されていればネットワーク形式として読む fn parse_format(args: &[String]) -> NbtFormat { - // 3 番目以降の引数からオプションを探す + // 3番目以降の引数からオプションを探す for index in 3..args.len().saturating_sub(1) { if args[index] == "--format" && args[index + 1] == "network" { return NbtFormat::Network; @@ -167,16 +166,15 @@ fn parse_format(args: &[String]) -> NbtFormat { // --------------------------------------------------------------------------- -// チャンク(World レイヤ) +// チャンク(Worldレイヤ) // -// 仕様: docs/spec/90-conformance.md 2.3章 // --------------------------------------------------------------------------- -/// チャンク NBT のファイルを読む。 +/// チャンクNBTのファイルを読む fn read_chunk_file(path: &str) -> Result { let named = read_file(path, &NbtReadOptions::default())?; - // 検証では DataVersion の違いを警告にせず、そのまま読む + // 検証では、DataVersionが扱える形式より古くても警告にせずそのまま読む let options = ChunkReadOptions { on_version_mismatch: VersionMismatchAction::Ignore, ..ChunkReadOptions::default() @@ -184,10 +182,10 @@ fn read_chunk_file(path: &str) -> Result { Chunk::from_nbt(named.tag, &options) } -/// チャンクの全ブロック・全バイオームを走査して集計する。 +/// チャンクの全ブロック・全バイオームを走査して集計する /// /// パレットとビットストレージを端から端まで通すので、 -/// ビット詰めの実装が 1 か所でもずれれば集計値が変わる。 +/// ビット詰めの実装が1か所でもずれれば集計値が変わる fn chunk_describe(chunk: &Chunk) -> Result { let mut out = format!( "chunk {} {} {} {}\n", @@ -225,7 +223,7 @@ fn chunk_describe(chunk: &Chunk) -> Result { "section {section_y} {block_palette} {block_bits} {biome_palette} {biome_bits}\n" )); - // 全ブロックを 1 つずつ読んで、状態の文字列表現ごとに数える + // 全ブロックを1つずつ読んで、状態の文字列表現ごとに数える for y in 0..16 { for z in 0..16 { for x in 0..16 { @@ -236,7 +234,7 @@ fn chunk_describe(chunk: &Chunk) -> Result { } } - // バイオームは 4×4×4 単位なので、4 ブロックおきに見る + // バイオームは4×4×4単位なので、4ブロックおきに見る for y in (0..16).step_by(4) { for z in (0..16).step_by(4) { for x in (0..16).step_by(4) { @@ -260,9 +258,10 @@ fn chunk_describe(chunk: &Chunk) -> Result { Ok(out) } -/// 決まった手順でチャンクを編集する。全言語で同じ結果になるはず。 +/// 決まった手順でチャンクを編集する +/// 全言語で同じ結果になるはず /// -/// パレット拡張・ビット幅の再計算・未使用要素の掃除を一通り通す。 +/// パレット拡張・ビット幅の再計算・未使用要素の掃除を一通り通す fn chunk_edit(chunk: &mut Chunk) -> Result<()> { let base_y = chunk.min_section_y()? * 16; @@ -272,7 +271,8 @@ fn chunk_edit(chunk: &mut Chunk) -> Result<()> { chunk.set_block(index % 16, base_y + (index / 16), index % 16, &state)?; } - // プロパティ付きのブロックを、名前は同じで状態違いで置く + // プロパティ付きのブロックを置く + // 1つ目と2つ目はプロパティの並び順だけが違う同じ状態、3つ目は名前空間を省いて書いた別の状態 chunk.set_block(1, base_y + 2, 1, &BlockState::parse("minecraft:oak_stairs[facing=north,half=top]")?)?; chunk.set_block(2, base_y + 2, 2, &BlockState::parse("minecraft:oak_stairs[half=top,facing=north]")?)?; chunk.set_block(3, base_y + 2, 3, &BlockState::parse("oak_stairs[facing=south]")?)?; @@ -284,7 +284,7 @@ fn chunk_edit(chunk: &mut Chunk) -> Result<()> { // 使われなくなったパレット要素を掃除する chunk.compact()?; - // 高さマップと光源は再計算しないので、無効化して Minecraft に任せる + // 高さマップと光源は再計算しないので、無効化してMinecraftに任せる chunk.clear_heightmaps(); chunk.invalidate_lighting(); Ok(()) @@ -293,12 +293,12 @@ fn chunk_edit(chunk: &mut Chunk) -> Result<()> { // --------------------------------------------------------------------------- // リージョンファイル // -// 仕様: docs/spec/90-conformance.md 2.3章 // --------------------------------------------------------------------------- -/// 存在するチャンクを 1 行 1 チャンクで書き出す。並びはロケーションテーブルの添字順。 +/// 存在するチャンクを1行1チャンクで書き出す +/// 並びはロケーションテーブルの添字順 /// -/// 各行は「絶対X 絶対Z タイムスタンプ 圧縮方式 圧縮後バイト数 展開後バイト数 キー数」。 +/// 各行は「絶対X 絶対Z タイムスタンプ 圧縮方式 圧縮後バイト数 展開後バイト数 ルート直下キー数」 fn region_list(region: &RegionFile) -> Result { let mut out = format!("region {} {}\n", region.region_x(), region.region_z()); let mut total = 0usize; @@ -336,10 +336,10 @@ fn region_list(region: &RegionFile) -> Result { Ok(out) } -/// 全チャンクを読み直し、無圧縮で新しいリージョンへ詰め直して書き出す。 +/// 全チャンクを読み直し、無圧縮で新しいリージョンへ詰め直して書き出す /// -/// 無圧縮にするのは、zlib の出力が処理系ごとに違い、 -/// 圧縮したままでは言語間でバイトが一致しないため。 +/// 無圧縮にするのは、zlibの出力が処理系ごとに違い、 +/// 圧縮したままでは言語間でバイトが一致しないため fn region_rewrite(source: &RegionFile, output_path: &str) -> Result<()> { // 途中結果が残らないよう、書き出し先は必ず作り直す if std::path::Path::new(output_path).exists() { @@ -369,11 +369,10 @@ fn region_rewrite(source: &RegionFile, output_path: &str) -> Result<()> { // --------------------------------------------------------------------------- // 正規化JSON // -// 浮動小数点をビットパターンで、64bit 整数を10進文字列で表すのが要。 -// 10進表記の丸めや JSON 数値の精度は処理系ごとに差が出るため、 -// そのまま出すと4言語の出力が一致しない。 +// 浮動小数点をビットパターンで、64bit整数を10進文字列で表すのが要 +// 10進表記の丸めやJSON数値の精度は処理系ごとに差が出るため、 +// そのまま出すと言語間で出力が一致しない // -// 仕様: docs/spec/00-conventions.md 6章 // --------------------------------------------------------------------------- fn normalized_json(named: &NamedTag, format: NbtFormat) -> String { @@ -396,7 +395,7 @@ fn append_tag(out: &mut String, tag: &NbtTag) { out.push_str("{\"type\":"); append_ascii(out, tag.tag_type().as_str()); - // list だけは value の前に element_type が入る(仕様が定めるキー順) + // listだけはvalueの前にelement_typeが入る(仕様が定めるキー順) if let NbtTag::List(list) = tag { out.push_str(",\"element_type\":"); append_ascii(out, list.element_type().as_str()); @@ -408,21 +407,22 @@ fn append_tag(out: &mut String, tag: &NbtTag) { NbtTag::Byte(value) => out.push_str(&value.to_string()), NbtTag::Short(value) => out.push_str(&value.to_string()), NbtTag::Int(value) => out.push_str(&value.to_string()), - // 64bit 整数は JSON 数値だと処理系によって精度が落ちるため10進文字列で表す + // 64bit整数はJSON数値だと処理系によって精度が落ちるため10進文字列で表す NbtTag::Long(value) => append_ascii(out, &value.to_string()), NbtTag::Float(value) => append_ascii(out, &format!("0x{:08x}", value.to_bits())), NbtTag::Double(value) => append_ascii(out, &format!("0x{:016x}", value.to_bits())), NbtTag::String(value) => { append_string(out, &value.to_utf16()); - // MUTF-8 のバイト列も併記する。孤立サロゲートなど UTF-8 に写せない値を厳密に比較するため + // MUTF-8のバイト列も併記する + // 孤立サロゲートなどUTF-8に写せない値を厳密に比較するため out.push_str(",\"mutf8\":"); append_ascii(out, &to_hex(&value.to_mutf8())); } NbtTag::ByteArray(values) => append_number_array(out, values), NbtTag::IntArray(values) => append_number_array(out, values), NbtTag::LongArray(values) => { - // 64bit 整数は10進文字列の配列で表す + // 64bit整数は10進文字列の配列で表す out.push('['); for (index, value) in values.iter().enumerate() { @@ -455,7 +455,7 @@ fn append_tag(out: &mut String, tag: &NbtTag) { } fn append_compound(out: &mut String, compound: &NbtCompound) { - // JSON オブジェクトだと挿入順の保持が処理系依存になるため、組の配列で表す + // JSONオブジェクトだと挿入順の保持が処理系依存になるため、組の配列で表す out.push('['); for (index, (key, value)) in compound.iter().enumerate() { @@ -497,15 +497,16 @@ fn to_hex(bytes: &[u8]) -> String { out } -/// ASCII だけの文字列を JSON 文字列として書く。 +/// ASCIIだけの文字列をJSON文字列として書く fn append_ascii(out: &mut String, text: &str) { append_string(out, &text.encode_utf16().collect::>()); } -/// JSON 文字列を書き出す。非 ASCII は必ず `\uXXXX` へ逃がす。 +/// JSON文字列を書き出す +/// 非ASCIIは必ず`\uXXXX`へエスケープする /// -/// エスケープの単位は UTF-16 コード単位。 -/// C# / Java と桁数を揃えるため、補助文字はサロゲートペアの 2 つに分けて出す。 +/// エスケープの単位はUTF-16コード単位 +/// C# / Javaと桁数を揃えるため、補助文字はサロゲートペアの2つに分けて出す fn append_string(out: &mut String, units: &[u16]) { out.push('"'); @@ -519,7 +520,7 @@ fn append_string(out: &mut String, units: &[u16]) { 0x0D => out.push_str("\\r"), 0x09 => out.push_str("\\t"), value => { - // ASCII の印字可能文字だけ生で出し、それ以外は \uXXXX にする + // ASCIIの印字可能文字だけ生で出し、それ以外は\uXXXXにする if (0x20..=0x7E).contains(&value) { out.push(value as u8 as char); } else { diff --git a/rust/src/anvil/folder.rs b/rust/src/anvil/folder.rs index f101cc6..bd0ab96 100644 --- a/rust/src/anvil/folder.rs +++ b/rust/src/anvil/folder.rs @@ -1,20 +1,15 @@ -//! リージョンファイルが並ぶディレクトリ 1 つ分 +//! リージョンファイルが並ぶディレクトリ1つ分 //! -//! `region/`、`entities/`、`poi/` のいずれかを表す -//! 開いたリージョンファイルはキャッシュし、[`RegionFolder::close`] でまとめて閉じる +//! `region/`、`entities/`、`poi/`のいずれかを表す +//! 開いたリージョンファイルはキャッシュし、[`RegionFolder::close`]でまとめて閉じる //! チャンク座標からリージョンを解決するので、利用側はリージョンの存在を意識しなくてよい //! -//! [`RegionFile`] はファイル全体をメモリへ載せるため、キャッシュには -//! [`RegionFolder::max_cached_regions`] 件の上限がある -//! 上限を超えると、 -//! 最も長く使われていないものから書き出して閉じる +//! [`RegionFile`]はファイル全体をメモリへ載せるため、キャッシュには[`RegionFolder::max_cached_regions`]件の上限がある +//! 上限を超えると、最も長く使われていないものから書き出して閉じる //! 大きなワールドを端から走査してもメモリを使い切らない //! -//! このため [`RegionFolder::region`] が返した参照は、 -//! **別のリージョンへアクセスすると閉じられている場合がある** +//! このため[`RegionFolder::region`]が返す参照は`&mut self`の借用に縛られ、別のリージョンへアクセスする前に手放す必要がある //! 参照を保持せず、必要なたびに取得すること -//! -//! 仕様: `docs/spec/20-anvil-region.md` 5章 use std::collections::{HashMap, VecDeque}; use std::path::{Path, PathBuf}; @@ -26,12 +21,12 @@ use super::region::{ChunkPos, RegionFile, RegionFileMode, RegionPos}; /// 同時に開いておくリージョンファイル数の既定の上限 /// -/// 1 リージョンは最大 255 セクタ × 1024 チャンク=理論上 1GiB になりうる -/// 実データでは数 MB から数十 MB 程度 -/// 8 件なら通常のワールドで数百 MB に収まる +/// 1リージョンは最大255セクタ × 1024チャンク=理論上1GiBになりうる +/// 実データでは数MBから数十MB程度 +/// 8件なら通常のワールドで数百MBに収まる pub const DEFAULT_MAX_CACHED_REGIONS: usize = 8; -/// リージョンフォルダ 1 つ分 +/// リージョンフォルダ1つ分 pub struct RegionFolder { directory: PathBuf, mode: RegionFileMode, @@ -117,11 +112,11 @@ impl RegionFolder { let mut found = Vec::new(); - // r.X.Z.mca として解釈できるファイルだけを拾う + // r.X.Z.mcaとして解釈できるファイルだけを拾う for entry in entries.flatten() { let name = entry.file_name().to_string_lossy().into_owned(); - // r.X.Z.mca として解釈できるファイルだけを拾う + // r.X.Z.mcaとして解釈できるファイルだけを拾う if let Some(position) = RegionPos::from_file_name(&name) { found.push(position); } @@ -133,7 +128,7 @@ impl RegionFolder { } /// リージョンファイルを取得する - /// 読み取り専用で存在しなければ `None` + /// 読み取り専用で存在しなければ`None` pub fn region(&mut self, region_x: i32, region_z: i32) -> Result> { self.ensure_open()?; let position = RegionPos::new(region_x, region_z); @@ -141,7 +136,7 @@ impl RegionFolder { if !self.cache.contains_key(&position) { let path = self.directory.join(position.file_name()); - // 読み取り専用では、存在しないリージョンは「チャンクが無い」として None を返す + // 読み取り専用では、存在しないリージョンは「チャンクが無い」としてNoneを返す if !path.exists() && self.mode == RegionFileMode::ReadOnly { return Ok(None); } @@ -160,7 +155,7 @@ impl RegionFolder { /// 使ったリージョンを、最近使った列の末尾へ移す fn touch(&mut self, position: RegionPos) { - // 同じ座標が列に残っていたら、先に取り除いてから末尾へ積む + // 既に列にあるなら、いったん外してから末尾へ積み直す if let Some(index) = self.recently_used.iter().position(|item| *item == position) { self.recently_used.remove(index); } @@ -168,7 +163,7 @@ impl RegionFolder { self.recently_used.push_back(position); } - /// 新しく 1 件開けるよう、上限を下回るまで古いものを閉じる + /// 新しく1件開けるよう、上限を下回るまで古いものを閉じる fn evict_until_below_limit(&mut self) -> Result<()> { // 上限に達している間、いちばん長く使っていないものから閉じる while self.cache.len() >= self.max_cached_regions { @@ -197,8 +192,8 @@ impl RegionFolder { } } - /// チャンクを NBT として読む - /// 存在しなければ `None` + /// チャンクをNBTとして読む + /// 存在しなければ`None` pub fn read_chunk(&mut self, chunk_x: i32, chunk_z: i32) -> Result> { let position = ChunkPos::new(chunk_x, chunk_z).region(); @@ -208,7 +203,7 @@ impl RegionFolder { } } - /// チャンクを NBT として書き込む + /// チャンクをNBTとして書き込む pub fn write_chunk( &mut self, chunk_x: i32, @@ -228,10 +223,10 @@ impl RegionFolder { } } - /// すでに組み立て済みの NBT をチャンクとして書き込む + /// すでに組み立て済みのNBTをチャンクとして書き込む /// - /// 既定の Zlib 圧縮を使う - /// World 層から呼ぶための入口 + /// 既定のZlib圧縮を使う + /// World層から呼ぶためのメソッド pub fn write_chunk_nbt( &mut self, chunk_x: i32, @@ -242,7 +237,7 @@ impl RegionFolder { } /// チャンクを削除する - /// 削除できたら `true` + /// 削除できたら`true` pub fn delete_chunk(&mut self, chunk_x: i32, chunk_z: i32) -> Result { let position = ChunkPos::new(chunk_x, chunk_z).region(); diff --git a/rust/src/anvil/lz4.rs b/rust/src/anvil/lz4.rs index c74bc07..bbbc81f 100644 --- a/rust/src/anvil/lz4.rs +++ b/rust/src/anvil/lz4.rs @@ -1,31 +1,28 @@ -//! 圧縮方式ID 4 (LZ4) のチャンクを展開する +//! 圧縮方式ID 4(LZ4)のチャンクを展開する //! -//! 素の LZ4 ブロックでも LZ4 フレーム形式でもなく、 -//! 独自ヘッダを持つブロックの連結である +//! 素のLZ4ブロックでもLZ4フレーム形式でもなく、独自ヘッダを持つブロックの連結である //! //! 書き込みには対応しない -//! 書き戻すときは Zlib になる -//! -//! 仕様: `docs/spec/20-anvil-region.md` 3.1.1 / 3.1.2 +//! NBTとして書き戻すときは既定でZlibになり、生バイトのまま書き戻すとLZ4のまま残る use crate::error::{Error, Result}; -/// ブロックの先頭に必ず置かれる 8 バイト +/// ブロックの先頭に必ず置かれる8バイト const MAGIC: &[u8; 8] = b"LZ4Block"; /// ブロックヘッダの長さ const HEADER_LENGTH: usize = 21; -/// トークン上位 4 ビット: 本体が無圧縮 +/// トークン上位4ビット: 本体が無圧縮 const METHOD_STORED: u8 = 0x10; -/// トークン上位 4 ビット: 本体が LZ4 圧縮 +/// トークン上位4ビット: 本体がLZ4圧縮 const METHOD_COMPRESSED: u8 = 0x20; /// マッチの最小長 const MIN_MATCH: usize = 4; -/// LZ4Block の連結を展開する +/// LZ4Blockの連結を展開する pub fn decompress(payload: &[u8]) -> Result> { let mut output: Vec = Vec::new(); let mut position = 0usize; @@ -38,7 +35,7 @@ pub fn decompress(payload: &[u8]) -> Result> { Ok(output) } -/// ブロックを 1 つ展開し、次のブロックの開始位置を返す +/// ブロックを1つ展開し、次のブロックの開始位置を返す fn decompress_block(payload: &[u8], position: usize, output: &mut Vec) -> Result { if position + HEADER_LENGTH > payload.len() { return Err(Error::malformed(format!( @@ -47,7 +44,7 @@ fn decompress_block(payload: &[u8], position: usize, output: &mut Vec) -> Re ))); } - // マジックが違えばそもそも LZ4Block ではない + // マジックが違えばそもそもLZ4Blockではない if &payload[position..position + MAGIC.len()] != MAGIC.as_slice() { return Err(Error::malformed("LZ4: ブロックが LZ4Block で始まっていない")); } @@ -67,7 +64,7 @@ fn decompress_block(payload: &[u8], position: usize, output: &mut Vec) -> Re } if method == METHOD_STORED { - // 無圧縮なら 2 つの長さは一致していなければならない + // 無圧縮なら2つの長さは一致していなければならない if compressed_length != original_length { return Err(Error::malformed(format!( "LZ4: 無圧縮ブロックの長さが食い違う({compressed_length} と {original_length})" @@ -86,13 +83,13 @@ fn decompress_block(payload: &[u8], position: usize, output: &mut Vec) -> Re Ok(body + compressed_length) } -/// ヘッダに書かれた 2 つの長さが妥当か調べる +/// ヘッダに書かれた2つの長さが妥当か調べる fn validate_lengths(compressed_length: i32, original_length: i32) -> Result<()> { if compressed_length < 0 || original_length < 0 { return Err(Error::malformed("LZ4: ブロックの長さが負値")); } - // 片方だけが 0 になることはない + // 片方だけが0になることはない if (compressed_length == 0) != (original_length == 0) { return Err(Error::malformed("LZ4: ブロックの長さが片方だけ 0")); } @@ -100,7 +97,7 @@ fn validate_lengths(compressed_length: i32, original_length: i32) -> Result<()> Ok(()) } -/// 素の LZ4 ブロックを展開する +/// 素のLZ4ブロックを展開する fn decompress_raw_block(source: &[u8], original_length: usize) -> Result> { let mut output: Vec = vec![0; original_length]; let mut input = 0usize; @@ -113,7 +110,7 @@ fn decompress_raw_block(source: &[u8], original_length: usize) -> Result let mut literal_length = (token >> 4) as usize; - // 15 なら追加バイトで長さが続く + // 15なら追加バイトで長さが続く if literal_length == 15 { literal_length += read_length(source, &mut input)?; } @@ -138,7 +135,7 @@ fn decompress_raw_block(source: &[u8], original_length: usize) -> Result let mut match_length = (token & 0x0F) as usize + MIN_MATCH; - // 下位 4 ビットが 15 なら追加バイトで長さが続く + // 下位4ビットが15なら追加バイトで長さが続く if (token & 0x0F) == 15 { match_length += read_length(source, &mut input)?; } @@ -155,11 +152,11 @@ fn decompress_raw_block(source: &[u8], original_length: usize) -> Result Ok(output) } -/// 255 が続く形式の追加長さを読む +/// 255が続く形式の追加長さを読む fn read_length(source: &[u8], input: &mut usize) -> Result { let mut total = 0usize; - // 255 未満のバイトが出るまで足し続ける + // 255未満のバイトが出るまで足し続ける loop { if *input >= source.len() { return Err(Error::malformed("LZ4: 長さの追加バイトが途中で切れた")); @@ -204,7 +201,7 @@ fn copy_match(output: &mut [u8], written: usize, offset: usize, length: usize) - let from = written - offset; - // コピー元と先は重なりうるので 1 バイトずつ写す + // コピー元と先は重なりうるので1バイトずつ写す for index in 0..length { output[written + index] = output[from + index]; } @@ -212,7 +209,7 @@ fn copy_match(output: &mut [u8], written: usize, offset: usize, length: usize) - Ok(written + length) } -/// リトルエンディアンの i32 を読む +/// リトルエンディアンのi32を読む /// この形式だけ他と逆になる fn read_i32_le(source: &[u8], position: usize) -> i32 { i32::from_le_bytes([ diff --git a/rust/src/anvil/mod.rs b/rust/src/anvil/mod.rs index 82f824e..389cb07 100644 --- a/rust/src/anvil/mod.rs +++ b/rust/src/anvil/mod.rs @@ -1,6 +1,4 @@ -//! Anvil のリージョンファイル (.mca) の読み書き -//! -//! 仕様: `docs/spec/20-anvil-region.md` +//! Anvilのリージョンファイル(.mca)の読み書き pub mod folder; mod lz4; diff --git a/rust/src/anvil/region.rs b/rust/src/anvil/region.rs index 9e4461f..d77f77d 100644 --- a/rust/src/anvil/region.rs +++ b/rust/src/anvil/region.rs @@ -1,12 +1,10 @@ -//! Anvil のリージョンファイル (`r.X.Z.mca`) -//! 32×32 チャンクを格納する +//! Anvilのリージョンファイル (`r.X.Z.mca`) +//! 32×32チャンクを格納する //! //! ファイル全体をメモリに読み込んで扱う -//! 実データのリージョンは数 MB 程度で、 -//! この方が「触っていないチャンクのバイト配置をそのまま保つ」ことを保証しやすい -//! 開いて何も変えずに [`RegionFile::flush`] すると、バイト単位で元と同じファイルになる -//! -//! 仕様: `docs/spec/20-anvil-region.md` +//! 実データのリージョンは数MB程度で、この方が「触っていないチャンクのバイト配置をそのまま保つ」ことを保証しやすい +//! 開いて何も変えずに[`RegionFile::flush`]すると、バイト単位で元と同じファイルになる +//! ただし空のファイルは、8KiBのヘッダだけのファイルになる use std::collections::HashMap; use std::fmt; @@ -32,7 +30,7 @@ const HEADER_SECTORS: usize = 2; /// 1リージョンに入るチャンク数 const CHUNK_COUNT: usize = 1024; -/// 1チャンクが確保できるセクタ数の上限(長さフィールドが u8 のため) +/// 1チャンクが確保できるセクタ数の上限(長さフィールドがu8のため) const MAX_SECTORS: usize = 255; /// リージョン内に収められるペイロードの上限 @@ -41,23 +39,21 @@ const MAX_INLINE_PAYLOAD: usize = (MAX_SECTORS * SECTOR_SIZE) - 5; /// リージョンファイル内でチャンクに使われる圧縮方式 /// -/// NBT 層の [`Compression`] とは別物であることに注意 -/// あちらはファイル全体の圧縮を表し、こちらはリージョン内の 1 チャンクに付く -/// 1 バイトのIDを表す -/// -/// 仕様: `docs/spec/20-anvil-region.md` 3.1章 +/// NBT層の[`Compression`]とは違うものであることに注意 +/// あちらはファイル全体の圧縮を表し、こちらはリージョン内の1チャンクに付く1バイトのIDを表す #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ChunkCompression { /// GZip (RFC 1952) /// 実データではほぼ使われない Gzip, /// Zlib (RFC 1950) - /// Minecraft が実際に書き出す方式 + /// Minecraftが実際に書き出す方式 Zlib, /// 無圧縮 None, - /// LZ4(ブロック形式) - /// 任意依存 + /// LZ4(独自ヘッダ付きのブロック連結) + /// 読み込みのみ対応 + /// 展開は自前で行い、外部の依存は使わない Lz4, /// サードパーティ製サーバのカスタム方式 /// 中身は解釈できない @@ -87,10 +83,10 @@ impl ChunkCompression { } } - /// 圧縮方式IDから [`ChunkCompression`] を得る + /// 圧縮方式IDから[`ChunkCompression`]を得る /// 未知のIDならエラー pub fn from_id(id: u8) -> Result { - // 仕様が定めるのは 1・2・3・4・127 の 5 種類だけ + // 仕様が定めるのは1・2・3・4・127の5種類だけ match id { 1 => Ok(ChunkCompression::Gzip), 2 => Ok(ChunkCompression::Zlib), @@ -109,7 +105,7 @@ impl ChunkCompression { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RegionFileMode { /// 読み取り専用 - /// 書き込み系の操作はエラーになる + /// 書き込み系の操作はエラーになる(`flush`は何もしない) ReadOnly, /// 読み書き /// ファイルが無ければ空のリージョンとして扱う @@ -117,7 +113,7 @@ pub enum RegionFileMode { } /// リージョンの座標 -/// 1リージョンは 32×32 チャンクを担当する +/// 1リージョンは32×32チャンクを担当する #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct RegionPos { /// リージョンX座標 @@ -137,12 +133,12 @@ impl RegionPos { format!("r.{}.{}.mca", self.x, self.z) } - /// `r.X.Z.mca` 形式のファイル名から座標を得る - /// 解釈できなければ `None` + /// `r.X.Z.mca`形式のファイル名から座標を得る + /// 解釈できなければ`None` pub fn from_file_name(file_name: &str) -> Option { let parts: Vec<&str> = file_name.split('.').collect(); - // "r" "" "" "mca" の 4 つに分かれるはず + // "r" "" "" "mca"の4つに分かれるはず if parts.len() != 4 || parts[0] != "r" || parts[3] != "mca" { return None; } @@ -178,7 +174,7 @@ impl ChunkPos { /// このチャンクを含むリージョンの座標 /// - /// Rust の `>>` は符号付き整数では算術右シフトなので、負の座標でも正しく求まる + /// Rustの`>>`は符号付き整数では算術右シフトなので、負の座標でも正しく求まる pub fn region(&self) -> RegionPos { RegionPos::new(self.x >> 5, self.z >> 5) } @@ -201,8 +197,7 @@ impl ChunkPos { /// リージョンファイルに格納されたままの、圧縮済みチャンクデータ /// -/// 本ライブラリが解釈できない圧縮方式(LZ4 未導入、カスタム方式)でも -/// これなら取り出せる +/// 本ライブラリが解釈できない圧縮方式(カスタム方式)のチャンクでも、これなら取り出せる /// バックアップや別ツールへの受け渡しに使う #[derive(Debug, Clone, PartialEq, Eq)] pub struct RawChunk { @@ -210,7 +205,7 @@ pub struct RawChunk { pub compression: ChunkCompression, /// 圧縮されたままの本体 pub data: Vec, - /// 外部ファイル `c.X.Z.mcc` に格納されていたか + /// 外部ファイル`c.X.Z.mcc`に格納されていたか pub external: bool, } @@ -221,7 +216,7 @@ impl RawChunk { } } -/// リージョンファイル 1 つ分 +/// リージョンファイル1つ分 pub struct RegionFile { path: PathBuf, directory: PathBuf, @@ -239,7 +234,7 @@ pub struct RegionFile { impl RegionFile { /// リージョンファイルを開く /// - /// `path` は `r.X.Z.mca` という名前でなければならない + /// `path`は`r.X.Z.mca`という名前でなければならない /// 座標はファイル名から読み取る pub fn open(path: impl AsRef, mode: RegionFileMode) -> Result { let path = path.as_ref(); @@ -315,7 +310,7 @@ impl RegionFile { /// ヘッダを解析し、ロケーションとタイムスタンプを取り込む fn parse_header(&mut self) -> Result<()> { - // 空ファイルは「チャンクが 1 つも無いリージョン」として受け入れる + // 空ファイルは「チャンクが1つも無いリージョン」として受け入れる if self.data.is_empty() { self.data = vec![0u8; HEADER_SECTORS * SECTOR_SIZE]; return Ok(()); @@ -342,7 +337,7 @@ impl RegionFile { let total_sectors = self.data.len() / SECTOR_SIZE; let mut sector_owner: HashMap = HashMap::new(); - // ロケーションテーブルの 1024 エントリを順に取り込む + // ロケーションテーブルの1024エントリを順に取り込む for index in 0..CHUNK_COUNT { let entry = self.read_unsigned(index * 4, 4); let offset = (entry >> 8) as usize; @@ -376,7 +371,7 @@ impl RegionFile { )); } - // 同じセクタを 2 つのチャンクが指していたら、どちらかが壊れている + // 同じセクタを2つのチャンクが指していたら、どちらかがおかしくなっている for sector in offset..offset + count { if let Some(owner) = sector_owner.insert(sector, index) { return Err(Error::new( @@ -393,9 +388,9 @@ impl RegionFile { Ok(()) } - /// ロケーションテーブルとタイムスタンプテーブルを先頭 2 セクタへ書き戻す + /// ロケーションテーブルとタイムスタンプテーブルを先頭2セクタへ書き戻す fn write_header(&mut self) { - // 位置表とタイムスタンプ表を、添字順に組み立て直す + // ロケーションテーブルとタイムスタンプテーブルを、添字順に組み立て直す for index in 0..CHUNK_COUNT { let entry = ((self.offsets[index] as u64) << 8) | (self.sector_counts[index] as u64); self.write_unsigned(index * 4, entry, 4); @@ -459,7 +454,7 @@ impl RegionFile { self.ensure_open()?; let mut result = Vec::new(); - // 添字の昇順に走査する(local_z が外、local_x が内) + // 添字の昇順に走査する(local_zが外、local_xが内) for index in 0..CHUNK_COUNT { if self.sector_counts[index] == 0 { continue; @@ -476,8 +471,8 @@ impl RegionFile { Ok(result) } - /// チャンクの最終更新時刻(Unix 秒) - /// 存在しなければ 0 + /// チャンクの最終更新時刻(Unix秒) + /// タイムスタンプテーブルの値をそのまま返すため、チャンクが無くても0とは限らない pub fn timestamp(&self, chunk_x: i32, chunk_z: i32) -> Result { self.ensure_open()?; let index = self.index_of(chunk_x, chunk_z)?; @@ -495,7 +490,7 @@ impl RegionFile { } /// チャンクを圧縮されたまま取り出す - /// 存在しなければ `None` + /// 存在しなければ`None` pub fn read_chunk_raw(&self, chunk_x: i32, chunk_z: i32) -> Result> { self.ensure_open()?; let index = self.index_of(chunk_x, chunk_z)?; @@ -526,7 +521,7 @@ impl RegionFile { let compression = ChunkCompression::from_id(scheme_byte & 0x7F)?; if external { - // 最上位ビットが立っている場合、本体は c.X.Z.mcc にある + // 最上位ビットが立っている場合、本体はc.X.Z.mccにある let payload = self.read_external_file(chunk_x, chunk_z)?; return Ok(Some(RawChunk { compression, data: payload, external: true })); } @@ -535,8 +530,8 @@ impl RegionFile { Ok(Some(RawChunk { compression, data: body, external: false })) } - /// チャンクを NBT として読む - /// 存在しなければ `None` + /// チャンクをNBTとして読む + /// 存在しなければ`None` pub fn read_chunk(&self, chunk_x: i32, chunk_z: i32) -> Result> { let raw = match self.read_chunk_raw(chunk_x, chunk_z)? { Some(value) => value, @@ -549,7 +544,7 @@ impl RegionFile { Ok(Some(read_bytes(&plain, &options)?.tag)) } - /// チャンクを NBT として書き込む + /// チャンクをNBTとして書き込む pub fn write_chunk( &mut self, chunk_x: i32, @@ -575,7 +570,7 @@ impl RegionFile { let scheme_byte: u8; if use_external { - // 1MiB を超えるチャンクは外部ファイルへ退避し、リージョンには目印だけ残す + // 255セクタ(約1MiB)に収まらないチャンクは外部ファイルへ退避し、リージョンには目印だけ残す self.write_external_file(chunk_x, chunk_z, &raw.data)?; payload = Vec::new(); scheme_byte = raw.compression.id() | 0x80; @@ -614,7 +609,7 @@ impl RegionFile { } /// チャンクを削除する - /// 削除できたら `true` + /// 削除できたら`true` pub fn delete_chunk(&mut self, chunk_x: i32, chunk_z: i32) -> Result { self.ensure_open()?; self.ensure_writable()?; @@ -635,7 +630,7 @@ impl RegionFile { /// 必要なセクタ数を確保し、開始セクタ番号を返す /// - /// 既存の割り当てがちょうど同じ大きさならその場を使い、 + /// 既存の割り当てがちょうど同じ大きさなら、その場所をそのまま使う /// そうでなければ先頭から空き領域を探し、無ければ末尾へ追加する fn allocate_sectors(&mut self, index: usize, needed: usize) -> usize { // 大きさが変わらないなら動かさない @@ -671,12 +666,12 @@ impl RegionFile { } /// セクタの使用状況を作る - /// `ignore_index` のチャンクは空きとして扱う + /// `ignore_index`のチャンクは空きとして扱う fn build_sector_usage(&self, ignore_index: usize) -> Vec { let total_sectors = self.data.len() / SECTOR_SIZE; let mut used = vec![false; total_sectors]; - // ヘッダの 2 セクタは常に使用中 + // ヘッダの2セクタは常に使用中 for sector in used.iter_mut().take(HEADER_SECTORS.min(total_sectors)) { *sector = true; } @@ -776,6 +771,8 @@ impl RegionFile { } self.write_header(); + // 読み書きで開いたフォルダがまだ無ければ、書き出す前に作る + self.ensure_directory()?; match std::fs::write(&self.path, &self.data) { Ok(()) => { @@ -861,8 +858,22 @@ impl RegionFile { }) } + /// ファイルを置くディレクトリが無ければ作る + /// 読むだけの操作で空のディレクトリができないよう、書き出す直前にだけ呼ぶ + fn ensure_directory(&self) -> Result<()> { + std::fs::create_dir_all(&self.directory).map_err(|error| { + Error::with_source( + ErrorCode::Io, + format!("ディレクトリを作れない: {}", self.directory.display()), + error, + ) + }) + } + fn write_external_file(&self, chunk_x: i32, chunk_z: i32, payload: &[u8]) -> Result<()> { let external = self.external_path(chunk_x, chunk_z); + // 読み書きで開いたフォルダがまだ無ければ、書き出す前に作る + self.ensure_directory()?; std::fs::write(&external, payload).map_err(|error| { Error::with_source( @@ -922,7 +933,7 @@ fn decompress_chunk(raw: &RawChunk) -> Result> { read_all(&mut decoder, &mut plain)?; Ok(plain) } - // LZ4 は読み込みのみ対応 + // LZ4は読み込みのみ対応 ChunkCompression::Lz4 => lz4::decompress(&raw.data), other => Err(Error::new( ErrorCode::UnsupportedFeature, diff --git a/rust/src/error.rs b/rust/src/error.rs index 784b422..06d0c11 100644 --- a/rust/src/error.rs +++ b/rust/src/error.rs @@ -1,11 +1,9 @@ -//! 4言語で共通のエラーモデル -//! -//! 仕様: `docs/spec/00-conventions.md` 4章 / `docs/adr/0005-unified-error-model.md` +//! 全言語で共通のエラーモデル use std::fmt; /// エラーの分類 -/// 4言語すべてで同一の集合を持つ +/// 全言語で同一の集合を持つ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ErrorCode { /// 下位の入出力失敗 @@ -14,13 +12,13 @@ pub enum ErrorCode { MalformedData, /// 期待した型と違うタグを取り出した UnexpectedTagType, - /// 仕様上は妥当だが、このビルドでは扱えない + /// 仕様上は妥当だが、このライブラリでは扱えない UnsupportedFeature, /// 安全上限を超えた LimitExceeded, /// 呼び出し側の引数が不正 InvalidArgument, - /// 対象バージョン外のデータ + /// 扱える形式より古いデータ UnsupportedDataVersion, } @@ -88,7 +86,7 @@ impl Error { Error::new(ErrorCode::LimitExceeded, message) } - /// 仕様上は妥当だが、このビルドでは扱えない + /// 仕様上は妥当だが、このライブラリでは扱えない pub fn unsupported_feature(message: impl Into) -> Self { Error::new(ErrorCode::UnsupportedFeature, message) } diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 56220c5..514f7c4 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,21 +1,20 @@ -//! SpringNBTLibrary — Minecraft Java版の NBT / Anvil ワールドデータを読み書きするライブラリ +//! SpringNBTLibraryは、Minecraft Java版のNBT / Anvilワールドデータを読み書きするライブラリ //! -//! 仕様は `docs/spec/` を唯一の正とする -//! 対象は 26.1 で導入されたワールド形式 (DataVersion 4786 以降) +//! 対象は26.1で導入されたワールド形式 (DataVersion 4786以降) #![forbid(unsafe_code)] #![warn(missing_docs)] -/// このライブラリが扱えるワールド形式の下限となる DataVersion (26.1) +/// このライブラリが扱えるワールド形式の下限となるDataVersion (26.1) /// -/// 26.1 で次元とプレイヤーデータの置き場が変わり、いまの形式になった +/// 26.1で次元とプレイヤーデータの置き場が変わり、いまの形式になった /// これ以降のバージョンは、形式が同じであればそのまま読み書きできる /// -/// これより古いワールドは構成そのものが違うので、 -/// 読み込み時に `UNSUPPORTED_DATA_VERSION` の対象になる +/// これより古いワールドは構成そのものが違う +/// そうしたチャンクは、読み込み時は既定で警告を出し、書き戻しは既定で`UNSUPPORTED_DATA_VERSION`にする pub const MIN_SUPPORTED_DATA_VERSION: i32 = 4786; -/// 動作を確かめた Minecraft Java版の DataVersion (26.2) +/// 動作を確かめたMinecraft Java版のDataVersion (26.2) pub const TARGET_DATA_VERSION: i32 = 4903; pub mod anvil; diff --git a/rust/src/nbt/canonical.rs b/rust/src/nbt/canonical.rs index 8d79230..326f09f 100644 --- a/rust/src/nbt/canonical.rs +++ b/rust/src/nbt/canonical.rs @@ -1,11 +1,9 @@ //! 浮動小数点の正準10進表記 //! -//! 各言語の標準の数値書式(C# の `"R"`、Java の `Float.toString`、 -//! Python の `repr`、Rust の `{}`)は互いに一致しない -//! 指数表記へ切り替わる閾値も、指数部の桁数も、`E` の大文字小文字も処理系ごとに違う -//! そのままでは SNBT 出力の言語間一致が成立しないため、書式をここで固定する -//! -//! 仕様: `docs/spec/11-snbt.md` 5.1章 +//! 各言語の標準の数値書式(C#の`"R"`、Javaの`Float.toString`、 +//! Pythonの`repr`、Rustの`{}`)は互いに一致しない +//! 指数表記へ切り替わる閾値も、指数部の桁数も、`E`の大文字小文字も処理系ごとに違う +//! そのままではSNBTの出力が言語間で一致しないため、書式をここで固定する /// 固定小数点表記を使う10進指数の下限 const MIN_FIXED_EXPONENT: i32 = -4; @@ -13,7 +11,7 @@ const MIN_FIXED_EXPONENT: i32 = -4; /// 固定小数点表記を使う10進指数の上限 const MAX_FIXED_EXPONENT: i32 = 16; -/// binary32 を正準10進表記へ変換する +/// binary32を正準10進表記へ変換する pub fn from_f32(value: f32) -> String { if value.is_nan() { return "NaN".to_string(); @@ -29,7 +27,7 @@ pub fn from_f32(value: f32) -> String { let target = value.to_bits(); - // 有効数字を 1 桁ずつ増やし、読み戻してビット一致する最短の表記を探す + // 有効数字を1桁ずつ増やし、読み戻したときにビットが一致する最短の表記を探す for precision in 1..=9usize { let candidate = format!("{:.*e}", precision - 1, value); @@ -43,11 +41,11 @@ pub fn from_f32(value: f32) -> String { } } - // 9 桁あれば binary32 は必ず往復するので、ここへは来ない + // 9桁あればbinary32は必ず往復するので、ここへは来ない format_exponential(&format!("{:.8e}", value)) } -/// binary64 を正準10進表記へ変換する +/// binary64を正準10進表記へ変換する pub fn from_f64(value: f64) -> String { if value.is_nan() { return "NaN".to_string(); @@ -63,7 +61,7 @@ pub fn from_f64(value: f64) -> String { let target = value.to_bits(); - // 有効数字を 1 桁ずつ増やし、読み戻してビット一致する最短の表記を探す + // 有効数字を1桁ずつ増やし、読み戻したときにビットが一致する最短の表記を探す for precision in 1..=17usize { let candidate = format!("{:.*e}", precision - 1, value); @@ -77,11 +75,11 @@ pub fn from_f64(value: f64) -> String { } } - // 17 桁あれば binary64 は必ず往復するので、ここへは来ない + // 17桁あればbinary64は必ず往復するので、ここへは来ない format_exponential(&format!("{:.16e}", value)) } -/// 指数表記の文字列(Rust の `{:e}` は `7.5e-1` 形式)から、仕様が定める正準表記を組み立てる +/// 指数表記の文字列(Rustの`{:e}`は`7.5e-1`形式)から、仕様が定める正準表記を組み立てる fn format_exponential(exponential: &str) -> String { let bytes: Vec = exponential.chars().collect(); let mut negative = false; @@ -111,7 +109,7 @@ fn format_exponential(exponential: &str) -> String { } /// 末尾のゼロを取り除く -/// すべてゼロなら "0" を残す +/// すべてゼロなら"0"を残す fn trim_trailing_zeros(digits: &str) -> String { let chars: Vec = digits.chars().collect(); let mut end = chars.len(); @@ -133,7 +131,7 @@ fn compose(negative: bool, digits: String, exponent: i32) -> String { result.push('-'); } - // 値が 0 のときは指数に関わらず 0.0 と書く + // 値が0のときは指数に関わらず0.0と書く if digits == "0" { result.push_str("0.0"); return result; @@ -144,7 +142,7 @@ fn compose(negative: bool, digits: String, exponent: i32) -> String { result.push_str(&digits[0..1]); result.push('.'); - // 2 桁目以降があれば小数点のうしろへ回す + // 2桁目以降があれば小数点のうしろへ回す if digits.len() > 1 { result.push_str(&digits[1..]); } else { @@ -157,8 +155,8 @@ fn compose(negative: bool, digits: String, exponent: i32) -> String { } if exponent >= 0 { - // 整数部は先頭 (exponent + 1) 桁 - // 足りなければゼロで右詰めする + // 整数部は先頭 (exponent + 1)桁 + // 足りなければ数字の右側をゼロで埋める let integer_digits = (exponent + 1) as usize; // 整数部が数字の並びに収まるなら、そのまま切り出す @@ -167,7 +165,7 @@ fn compose(negative: bool, digits: String, exponent: i32) -> String { } else { result.push_str(&digits); - // 数字が足りない分は 0 で埋めて桁を合わせる + // 数字が足りない分は0で埋めて桁を合わせる for _ in digits.len()..integer_digits { result.push('0'); } @@ -185,10 +183,10 @@ fn compose(negative: bool, digits: String, exponent: i32) -> String { return result; } - // 指数が負なら "0." に続けてゼロを詰めてから数字を置く + // 指数が負なら"0."に続けてゼロを詰めてから数字を置く result.push_str("0."); - // 指数のぶんだけ 0.000... と 0 を並べる + // 指数の絶対値より1個少ない0を並べ、0.000...の形にする for _ in 0..((-exponent) - 1) { result.push('0'); } diff --git a/rust/src/nbt/io.rs b/rust/src/nbt/io.rs index 789d6d1..3e85794 100644 --- a/rust/src/nbt/io.rs +++ b/rust/src/nbt/io.rs @@ -1,6 +1,4 @@ -//! NBT のファイル・バイト列・ストリームからの読み書き -//! -//! 仕様: `docs/spec/10-nbt-binary.md` 3章〜6章 +//! NBTのファイル・バイト列・ストリームからの読み書き use std::fmt; use std::io::{Read, Write}; @@ -14,22 +12,18 @@ use crate::error::{Error, ErrorCode, Result}; use crate::nbt::mutf8; use crate::nbt::tag::{NbtCompound, NbtList, NbtString, NbtTag, TagType}; -/// NBT のルートタグの並び方 -/// -/// 仕様: `docs/spec/10-nbt-binary.md` 3章 +/// NBTのルートタグの並び方 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NbtFormat { /// ファイル形式 /// ルートは「タグID + 名前長 + 名前 + ペイロード」の順に並ぶ Java, - /// ネットワーク形式 (1.20.2 以降) + /// ネットワーク形式 (1.20.2以降) /// ルートに名前が付かない Network, } /// 圧縮方式 -/// -/// 仕様: `docs/spec/10-nbt-binary.md` 4章 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Compression { /// 無圧縮 @@ -47,7 +41,7 @@ pub enum Compression { #[derive(Debug, Clone, PartialEq)] pub struct NamedTag { /// ルート名 - /// `Java` 形式では通常空文字列だが、読んだ値をそのまま保持する + /// `Java`形式では通常空文字列だが、読んだ値をそのまま保持する pub name: String, /// ルートタグ pub tag: NbtCompound, @@ -60,7 +54,7 @@ impl NamedTag { } } -/// NBT 読み込みのオプション +/// NBT読み込みのオプション #[derive(Debug, Clone, Copy)] pub struct NbtReadOptions { /// ルートタグの並び方 @@ -69,7 +63,7 @@ pub struct NbtReadOptions { /// 既定は自動判定 pub compression: Compression, /// ネストの深さ上限 - /// 既定は 512 + /// 既定は512 pub max_depth: i32, /// 展開後の総バイト数の上限 /// 負値なら無制限 @@ -87,13 +81,13 @@ impl Default for NbtReadOptions { } } -/// NBT 書き込みのオプション +/// NBT書き込みのオプション #[derive(Debug, Clone, Copy)] pub struct NbtWriteOptions { /// ルートタグの並び方 pub format: NbtFormat, /// 圧縮方式 - /// 既定は GZip + /// 既定はGZip pub compression: Compression, } @@ -117,9 +111,7 @@ impl NbtWriteOptions { /// 位置を指定した読み込みの結果 /// /// 読んだタグと、その直後の位置を持つ -/// 続けて読むときは `end` を次の開始位置として渡す -/// -/// 仕様: `docs/spec/10-nbt-binary.md` 3.1章 +/// 続けて読むときは`end`を次の開始位置として渡す #[derive(Debug, Clone, PartialEq)] pub struct NbtReadResult { /// 読んだタグ @@ -128,7 +120,7 @@ pub struct NbtReadResult { pub end: usize, } -/// 展開済みのバイト列から NBT を読み出す +/// 展開済みのバイト列からNBTを読み出す /// /// 入力全体をあらかじめメモリに持つ設計にしている /// 「宣言された長さが残り入力長を超えていないか」を確保前に検査できるようにするため @@ -170,12 +162,12 @@ impl<'a> Reader<'a> { Ok(tag) } - /// ルートタグを 1 つ読む + /// ルートタグを1つ読む /// 末尾の余りは見ない fn read_root_tag(&mut self, format: NbtFormat) -> Result { let tag_type = TagType::from_id(self.read_byte()?)?; - // Java版のファイル形式でもネットワーク形式でも、ルートは必ず TAG_Compound + // Java版のファイル形式でもネットワーク形式でも、ルートは必ずTAG_Compound if tag_type != TagType::Compound { return Err(Error::new( ErrorCode::MalformedData, @@ -207,7 +199,7 @@ impl<'a> Reader<'a> { } fn read_payload(&mut self, tag_type: TagType, depth: i32) -> Result { - // 深さ上限は再帰する型に入る手前で検査する + // 深さ上限は型を問わず、ペイロードを読む前に検査する if depth > self.max_depth { return Err(Error::new( ErrorCode::LimitExceeded, @@ -243,7 +235,7 @@ impl<'a> Reader<'a> { self.ensure_available(count as u64 * 4)?; let mut values = Vec::with_capacity(count); - // 4 バイトずつビッグエンディアンで読む + // 4バイトずつビッグエンディアンで読む for _ in 0..count { values.push(self.read_unsigned(4)? as i32); } @@ -255,7 +247,7 @@ impl<'a> Reader<'a> { self.ensure_available(count as u64 * 8)?; let mut values = Vec::with_capacity(count); - // 8 バイトずつビッグエンディアンで読む + // 8バイトずつビッグエンディアンで読む for _ in 0..count { values.push(self.read_unsigned(8)? as i64); } @@ -272,7 +264,7 @@ impl<'a> Reader<'a> { fn read_compound_payload(&mut self, depth: i32) -> Result { let mut compound = NbtCompound::new(); - // TAG_End が現れるまで名前付きタグを読み続ける + // TAG_Endが現れるまで名前付きタグを読み続ける loop { let tag_type = TagType::from_id(self.read_byte()?)?; @@ -300,7 +292,7 @@ impl<'a> Reader<'a> { let count = self.read_length()?; if element_type == TagType::End { - // 要素型 End のリストは空でなければならない + // 要素型Endのリストは空でなければならない if count != 0 { return Err(Error::new( ErrorCode::MalformedData, @@ -311,7 +303,7 @@ impl<'a> Reader<'a> { return Ok(NbtList::with_element_type(TagType::End)); } - // 1 要素の最小バイト数から、宣言された個数が入力に収まるかを先に検査する + // 1要素の最小バイト数から、宣言された個数が入力に収まるかを先に検査する self.ensure_available(count as u64 * minimum_payload_size(element_type))?; let mut list = NbtList::with_element_type(element_type); @@ -337,7 +329,7 @@ impl<'a> Reader<'a> { fn read_length(&mut self) -> Result { let length = self.read_unsigned(4)? as i32; - // 長さは i32 だが、負値は仕様上ありえない + // 長さはi32だが、負値は仕様上ありえない if length < 0 { return Err(Error::new( ErrorCode::MalformedData, @@ -394,13 +386,13 @@ fn minimum_payload_size(tag_type: TagType) -> u64 { TagType::Short => 2, TagType::Int | TagType::Float => 4, TagType::Long | TagType::Double => 8, - // 長さフィールドの 4 バイトは必ずある + // 長さフィールドの4バイトは必ずある TagType::ByteArray | TagType::IntArray | TagType::LongArray => 4, - // 長さフィールドの 2 バイトは必ずある + // 長さフィールドの2バイトは必ずある TagType::String => 2, - // 要素型 1 バイト + 個数 4 バイト + // 要素型1バイト + 個数4バイト TagType::List => 5, - // 終端の TAG_End 1 バイトは必ずある + // 終端のTAG_End 1バイトは必ずある TagType::Compound | TagType::End => 1, } } @@ -409,10 +401,10 @@ fn minimum_payload_size(tag_type: TagType) -> u64 { // 書き込み // --------------------------------------------------------------------------- -/// NBT を展開済みのバイト列へ書き出す +/// NBTを展開済みのバイト列へ書き出す /// /// 出力は一意でなければならない(ラウンドトリップ検証が成立するため) -/// Compound は挿入順のまま、浮動小数点はビットパターンのまま書き出す +/// Compoundは挿入順のまま、浮動小数点はビットパターンのまま書き出す struct Writer { buffer: Vec, } @@ -440,13 +432,13 @@ impl Writer { NbtTag::Short(value) => self.write_unsigned(*value as u16 as u64, 2), NbtTag::Int(value) => self.write_unsigned(*value as u32 as u64, 4), NbtTag::Long(value) => self.write_unsigned(*value as u64, 8), - // NaN や -0.0 を保つため、ビットパターンをそのまま書く + // NaNや-0.0を保つため、ビットパターンをそのまま書く NbtTag::Float(value) => self.write_unsigned(value.to_bits() as u64, 4), NbtTag::Double(value) => self.write_unsigned(value.to_bits(), 8), NbtTag::ByteArray(values) => { self.write_unsigned(values.len() as u64, 4); - // 1 バイトずつそのまま書く + // 1バイトずつそのまま書く for value in values { self.buffer.push(*value as u8); } @@ -457,7 +449,7 @@ impl Writer { NbtTag::IntArray(values) => { self.write_unsigned(values.len() as u64, 4); - // 4 バイトずつビッグエンディアンで書く + // 4バイトずつビッグエンディアンで書く for value in values { self.write_unsigned(*value as u32 as u64, 4); } @@ -465,7 +457,7 @@ impl Writer { NbtTag::LongArray(values) => { self.write_unsigned(values.len() as u64, 4); - // 8 バイトずつビッグエンディアンで書く + // 8バイトずつビッグエンディアンで書く for value in values { self.write_unsigned(*value as u64, 8); } @@ -502,8 +494,8 @@ impl Writer { fn write_string(&mut self, value: &NbtString) -> Result<()> { let encoded = value.to_mutf8(); - // 長さフィールドは u16 - // 65535 を超えると書き出せない + // 長さフィールドはu16 + // 65535を超えると書き出せない if encoded.len() > mutf8::MAX_BYTE_LENGTH { return Err(Error::new( ErrorCode::InvalidArgument, @@ -538,13 +530,13 @@ pub fn detect_compression(bytes: &[u8]) -> Result { )); } - // GZip は必ず 1F 8B で始まる + // GZipは必ず1F 8Bで始まる if bytes.len() >= 2 && bytes[0] == 0x1F && bytes[1] == 0x8B { return Ok(Compression::Gzip); } if bytes.len() >= 2 { - // zlib ヘッダは「圧縮法が 8 (deflate)」かつ「先頭2バイトが 31 の倍数」 + // zlibヘッダは「圧縮法が8 (deflate)」かつ「先頭2バイトが31の倍数」 let is_deflate = (bytes[0] & 0x0F) == 0x08; let header = ((bytes[0] as u32) << 8) | bytes[1] as u32; @@ -553,7 +545,7 @@ pub fn detect_compression(bytes: &[u8]) -> Result { } } - // 無圧縮なら先頭は TAG_Compound のタグID + // 無圧縮なら先頭はTAG_CompoundのタグID if bytes[0] == TagType::Compound.id() { return Ok(Compression::None); } @@ -650,19 +642,19 @@ fn compress(plain: Vec, method: Compression) -> Result> { } // --------------------------------------------------------------------------- -// 公開 API +// 公開API // --------------------------------------------------------------------------- -/// バイト列から NBT を読む +/// バイト列からNBTを読む pub fn read_bytes(bytes: &[u8], options: &NbtReadOptions) -> Result { let plain = decompress(bytes, options)?; Reader::new(&plain, options.max_depth).read_root(options.format) } -/// バイト列の指定した位置から NBT を 1 つ読む +/// バイト列の指定した位置からNBTを1つ読む /// -/// 複数の NBT が連なっているデータを、先頭から順に読み進めるために使う -/// 戻り値の `end` が次の開始位置になる +/// 複数のNBTが連なっているデータを、先頭から順に読み進めるために使う +/// 戻り値の`end`が次の開始位置になる /// /// 位置は渡したバイト列そのものを指すので、圧縮されたデータは扱えない pub fn read_bytes_at( @@ -685,16 +677,16 @@ pub fn read_bytes_at( Ok(NbtReadResult { tag, end: reader.position }) } -/// バイト列に連なっている NBT をすべて読む +/// バイト列に連なっているNBTをすべて読む /// /// 入力を使い切るまで読み続ける /// 空のバイト列なら空の一覧を返す /// -/// 圧縮は入力全体に 1 回かかっているものとして扱う +/// 圧縮は入力全体に1回かかっているものとして扱う pub fn read_bytes_all(bytes: &[u8], options: &NbtReadOptions) -> Result> { let mut tags: Vec = Vec::new(); - // 空の入力は「0 個」であってエラーではない + // 空の入力は「0個」であってエラーではない if bytes.is_empty() { return Ok(tags); } @@ -721,7 +713,7 @@ fn require_plain_input(options: &NbtReadOptions) -> Result<()> { Ok(()) } -/// ファイルから NBT を読む +/// ファイルからNBTを読む pub fn read_file(path: impl AsRef, options: &NbtReadOptions) -> Result { let raw = std::fs::read(path.as_ref()).map_err(|error| { // 下位の入出力エラーは情報を失わないよう原因として保持する @@ -735,7 +727,7 @@ pub fn read_file(path: impl AsRef, options: &NbtReadOptions) -> Result Result { let mut raw = Vec::new(); @@ -743,9 +735,9 @@ pub fn read_reader(source: &mut impl Read, options: &NbtReadOptions) -> Result Result> { - // 書き込み時に Auto は決められない + // 書き込み時にAutoは決められない if options.compression == Compression::Auto { return Err(Error::new( ErrorCode::InvalidArgument, @@ -757,7 +749,7 @@ pub fn write_bytes(named: &NamedTag, options: &NbtWriteOptions) -> Result, named: &NamedTag, @@ -774,7 +766,7 @@ pub fn write_file( }) } -/// NBT をライタへ書き出す +/// NBTをライタへ書き出す pub fn write_writer( destination: &mut impl Write, named: &NamedTag, diff --git a/rust/src/nbt/mod.rs b/rust/src/nbt/mod.rs index 372d126..b32e484 100644 --- a/rust/src/nbt/mod.rs +++ b/rust/src/nbt/mod.rs @@ -1,8 +1,6 @@ //! NBT (Named Binary Tag) の読み書き //! -//! このレイヤは Minecraft のバージョンに一切依存しない -//! -//! 仕様: `docs/spec/10-nbt-binary.md` +//! このレイヤはMinecraftのバージョンに一切依存しない pub mod canonical; pub mod io; diff --git a/rust/src/nbt/mutf8.rs b/rust/src/nbt/mutf8.rs index 9187bd5..ffbdce2 100644 --- a/rust/src/nbt/mutf8.rs +++ b/rust/src/nbt/mutf8.rs @@ -1,32 +1,29 @@ -//! Modified UTF-8 (MUTF-8) の符号化・復号 +//! Modified UTF-8 (MUTF-8)の符号化・復号 //! -//! 標準 UTF-8 との違いは 2 点だけ -//! - `U+0000` を `C0 80` の 2 バイトで表す -//! - `U+10000` 以上をサロゲートペアへ分解し、3 バイト × 2 で表す (CESU-8) -//! -//! 仕様: `docs/spec/10-nbt-binary.md` 2章 +//! 標準UTF-8との違いは2点だけ +//! - `U+0000`を`C0 80`の2バイトで表す +//! - `U+10000`以上をサロゲートペアへ分解し、3バイト × 2で表す (CESU-8) use crate::error::{Error, ErrorCode, Result}; -/// 長さフィールドが `u16` のため、1 つの文字列は符号化後 65535 バイトまで +/// 長さフィールドが`u16`のため、1つの文字列は符号化後65535バイトまで pub const MAX_BYTE_LENGTH: usize = 65535; -/// Rust の `char` へ写せない値も表せるよう、復号結果は UTF-16 コード単位の列で返す +/// Rustの`char`へ写せない値も表せるよう、復号結果はUTF-16コード単位の列で返す /// -/// 孤立サロゲートを含む文字列は `String` にできないため、 -/// 呼び出し側が `NbtString` としてどう保持するかを決められるようにしている +/// 孤立サロゲートを含む文字列は`String`にできないため、呼び出し側が`NbtString`としてどう保持するかを決められるようにしている pub fn decode_to_utf16(bytes: &[u8]) -> Result> { let mut units: Vec = Vec::with_capacity(bytes.len()); let mut i = 0usize; - // 先頭から 1 文字ずつ取り出す + // 先頭から1文字ずつ取り出す while i < bytes.len() { let b0 = bytes[i]; if b0 & 0x80 == 0x00 { - // 1 バイト形式: 0xxxxxxx (U+0001..U+007F) + // 1バイト形式: 0xxxxxxx (U+0001..U+007F) if b0 == 0x00 { - // 素の 0x00 は MUTF-8 では現れてはならない (C0 80 を使う) + // 素の0x00はMUTF-8では現れてはならない (C0 80を使う) return Err(Error::new( ErrorCode::MalformedData, "MUTF-8: 素の 0x00 が現れた (U+0000 は C0 80 で表す)", @@ -35,7 +32,7 @@ pub fn decode_to_utf16(bytes: &[u8]) -> Result> { units.push(b0 as u16); i += 1; } else if b0 & 0xE0 == 0xC0 { - // 2 バイト形式: 110xxxxx 10xxxxxx + // 2バイト形式: 110xxxxx 10xxxxxx if i + 1 >= bytes.len() { return Err(Error::new(ErrorCode::MalformedData, "MUTF-8: 2バイト形式が途中で切れた")); } @@ -44,15 +41,15 @@ pub fn decode_to_utf16(bytes: &[u8]) -> Result> { return Err(Error::new(ErrorCode::MalformedData, "MUTF-8: 2バイト形式の継続バイトが不正")); } let value = (((b0 & 0x1F) as u32) << 6) | ((b1 & 0x3F) as u32); - // C0 80 (U+0000) だけは正当 - // それ以外の 0x80 未満は冗長符号化 + // C0 80 (U+0000)だけは正当 + // それ以外の0x80未満は冗長符号化 if value < 0x80 && !(b0 == 0xC0 && b1 == 0x80) { return Err(Error::new(ErrorCode::MalformedData, "MUTF-8: 冗長な2バイト符号化")); } units.push(value as u16); i += 2; } else if b0 & 0xF0 == 0xE0 { - // 3 バイト形式: 1110xxxx 10xxxxxx 10xxxxxx + // 3バイト形式: 1110xxxx 10xxxxxx 10xxxxxx if i + 2 >= bytes.len() { return Err(Error::new(ErrorCode::MalformedData, "MUTF-8: 3バイト形式が途中で切れた")); } @@ -62,14 +59,14 @@ pub fn decode_to_utf16(bytes: &[u8]) -> Result> { return Err(Error::new(ErrorCode::MalformedData, "MUTF-8: 3バイト形式の継続バイトが不正")); } let value = (((b0 & 0x0F) as u32) << 12) | (((b1 & 0x3F) as u32) << 6) | ((b2 & 0x3F) as u32); - // 3 バイトで表すべき範囲は U+0800 以上 + // 3バイトで表すべき範囲はU+0800以上 if value < 0x800 { return Err(Error::new(ErrorCode::MalformedData, "MUTF-8: 冗長な3バイト符号化")); } units.push(value as u16); i += 3; } else { - // 4 バイト形式 (標準 UTF-8) や継続バイト単独は MUTF-8 では不正 + // 4バイト形式 (標準UTF-8)や継続バイト単独はMUTF-8では不正 return Err(Error::new( ErrorCode::MalformedData, format!("MUTF-8: 不正な先頭バイト 0x{b0:02X}"), @@ -80,21 +77,20 @@ pub fn decode_to_utf16(bytes: &[u8]) -> Result> { Ok(units) } -/// UTF-16 コード単位の列を MUTF-8 バイト列へ符号化する +/// UTF-16コード単位の列をMUTF-8バイト列へ符号化する /// -/// サロゲートは対になっているかどうかに関わらず 1 つずつ 3 バイトで符号化されるため、 -/// 孤立サロゲートもそのまま往復できる +/// サロゲートは対になっているかどうかに関わらず1つずつ3バイトで符号化されるため、孤立サロゲートもそのまま往復できる pub fn encode_from_utf16(units: &[u16]) -> Vec { let mut out: Vec = Vec::with_capacity(units.len() + units.len() / 2); - // コード単位ごとに 1〜3 バイトへ展開する + // コード単位ごとに1〜3バイトへ展開する for &unit in units { - // U+0001..U+007F だけが 1 バイト - // U+0000 は 2 バイトになる + // U+0001..U+007Fだけが1バイト + // U+0000は2バイトになる if unit >= 0x0001 && unit <= 0x007F { out.push(unit as u8); } else if unit == 0x0000 || unit <= 0x07FF { - // U+0000 もこの経路で C0 80 になる + // U+0000もこの経路でC0 80になる out.push(0xC0 | ((unit >> 6) as u8 & 0x1F)); out.push(0x80 | (unit as u8 & 0x3F)); } else { @@ -107,16 +103,16 @@ pub fn encode_from_utf16(units: &[u16]) -> Vec { out } -/// Rust の `str` を MUTF-8 バイト列へ符号化する +/// Rustの`str`をMUTF-8バイト列へ符号化する pub fn encode(text: &str) -> Vec { let units: Vec = text.encode_utf16().collect(); encode_from_utf16(&units) } -/// MUTF-8 バイト列を `String` へ復号する +/// MUTF-8バイト列を`String`へ復号する /// -/// 孤立サロゲートを含む入力は `String` にできないため `MALFORMED_DATA` になる -/// そのまま保持したい場合は [`decode_to_utf16`] を使う +/// 孤立サロゲートを含む入力は`String`にできないため`MALFORMED_DATA`になる +/// そのまま保持したい場合は[`decode_to_utf16`]を使う pub fn decode(bytes: &[u8]) -> Result { let units = decode_to_utf16(bytes)?; @@ -129,16 +125,16 @@ pub fn decode(bytes: &[u8]) -> Result { } } -/// MUTF-8 へ符号化したときのバイト数を数える +/// MUTF-8へ符号化したときのバイト数を数える /// /// 実際に符号化せずに長さだけを求める pub fn byte_length(text: &str) -> usize { let mut total = 0usize; - // コード単位ごとに 1〜3 バイトへ展開される + // 各コード単位が何バイトになるかを数える for unit in text.encode_utf16() { - // U+0001..U+007F だけが 1 バイト - // U+0000 は 2 バイトになる + // U+0001..U+007Fだけが1バイト + // U+0000は2バイトになる if unit >= 0x0001 && unit <= 0x007F { total += 1; } else if unit == 0x0000 || unit <= 0x07FF { @@ -151,8 +147,8 @@ pub fn byte_length(text: &str) -> usize { total } -/// UTF-16 コード単位の列を `String` へ変換する -/// 孤立サロゲートがあれば `None` +/// UTF-16コード単位の列を`String`へ変換する +/// 孤立サロゲートがあれば`None` pub fn utf16_to_string(units: &[u16]) -> Option { match String::from_utf16(units) { Ok(text) => Some(text), @@ -182,8 +178,8 @@ mod tests { #[test] fn supplementary_char_is_cesu8() { - // U+1F600 は UTF-16 では D83D DE00 - // MUTF-8 では 3 バイト × 2 になる + // U+1F600はUTF-16ではD83D DE00 + // MUTF-8では3バイト × 2になる let bytes = encode("\u{1F600}"); assert_eq!(bytes, vec![0xED, 0xA0, 0xBD, 0xED, 0xB8, 0x80]); let units = decode_to_utf16(&bytes).unwrap(); @@ -192,8 +188,8 @@ mod tests { #[test] fn lone_surrogate_survives_roundtrip() { - // 孤立した上位サロゲート D83D - // String にはできないが往復はできる + // 孤立した上位サロゲートD83D + // Stringにはできないが往復はできる let original: Vec = vec![0xD83D]; let bytes = encode_from_utf16(&original); assert_eq!(bytes, vec![0xED, 0xA0, 0xBD]); @@ -210,14 +206,14 @@ mod tests { #[test] fn decode_rejects_lone_surrogate() { - // 孤立サロゲートは String へ写せない + // 孤立サロゲートはStringへ写せない let err = decode(&[0xED, 0xA0, 0xBD]).unwrap_err(); assert_eq!(err.code(), ErrorCode::MalformedData); } #[test] fn byte_length_matches_encoded_length() { - // 1〜3 バイトになる各パターンで、数えた長さと符号化結果を突き合わせる + // 1〜3バイトになる各パターンで、数えた長さと符号化結果を比べる for text in ["", "Bananrama", "a\u{0000}b", "あいう", "\u{1F600}"] { assert_eq!(byte_length(text), encode(text).len(), "{text}"); } @@ -231,14 +227,14 @@ mod tests { #[test] fn overlong_two_byte_is_rejected() { - // U+0041 を 2 バイトで表した冗長符号化 + // U+0041を2バイトで表した冗長符号化 let err = decode_to_utf16(&[0xC1, 0x81]).unwrap_err(); assert_eq!(err.code(), ErrorCode::MalformedData); } #[test] fn four_byte_utf8_is_rejected() { - // 標準 UTF-8 の 4 バイト形式は MUTF-8 では不正 + // 標準UTF-8の4バイト形式はMUTF-8では不正 let err = decode_to_utf16(&[0xF0, 0x9F, 0x98, 0x80]).unwrap_err(); assert_eq!(err.code(), ErrorCode::MalformedData); } diff --git a/rust/src/nbt/snbt.rs b/rust/src/nbt/snbt.rs index cb9852f..63f57bd 100644 --- a/rust/src/nbt/snbt.rs +++ b/rust/src/nbt/snbt.rs @@ -1,9 +1,7 @@ -//! SNBT (Stringified NBT) のパースと出力 +//! SNBT (Stringified NBT)のパースと出力 //! -//! 対応範囲は「バイナリ NBT へ損失なく写せる部分集合」 -//! 1.21.5 以降の異種リスト(`[1, "a"]`)は受理しない -//! -//! 仕様: `docs/spec/11-snbt.md` / `docs/adr/0006-snbt-scope.md` +//! 対応範囲は「バイナリNBTへ損失なく写せる部分集合」 +//! 1.21.5以降の異種リスト(`[1, "a"]`)は受理しない use crate::error::{Error, ErrorCode, Result}; use crate::nbt::canonical; @@ -13,12 +11,12 @@ const INDENT_UNIT: &str = " "; const WIDTH_SUFFIXES: &str = "bBsSlLfFdD"; -/// SNBT 文字列をタグへ変換する +/// SNBT文字列をタグへ変換する pub fn parse(text: &str) -> Result { Parser::new(text).parse_whole() } -/// SNBT 文字列を Compound へ変換する +/// SNBT文字列をCompoundへ変換する pub fn parse_compound(text: &str) -> Result { match parse(text)? { NbtTag::Compound(compound) => Ok(compound), @@ -29,15 +27,15 @@ pub fn parse_compound(text: &str) -> Result { } } -/// タグを 1 行の SNBT へ変換する +/// タグを1行のSNBTへ変換する pub fn write(tag: &NbtTag) -> String { let mut out = String::new(); write_tag(&mut out, tag, -1); out } -/// タグを整形した SNBT へ変換する -/// インデントは空白 4 個 +/// タグを整形したSNBTへ変換する +/// インデントは空白4個 pub fn write_pretty(tag: &NbtTag) -> String { let mut out = String::new(); write_tag(&mut out, tag, 0); @@ -104,13 +102,13 @@ impl Parser { let mut compound = NbtCompound::new(); self.skip_whitespace(); - // 空の Compound + // 空のCompound if self.peek()? == '}' { self.position += 1; return Ok(compound); } - // 要素を 1 つずつ読む + // 要素を1つずつ読む loop { self.skip_whitespace(); @@ -145,12 +143,12 @@ impl Parser { fn parse_list_or_array(&mut self) -> Result { self.expect('[')?; - // "[B;" のような型付き配列かどうかを先に判定する + // "[B;"のような型付き配列かどうかを先に判定する if self.position + 1 < self.chars.len() && self.chars[self.position + 1] == ';' { let marker = self.chars[self.position]; - // [B; [I; [L; は型付き配列の印 - // ただの List と見分ける + // [B; [I; [L;は型付き配列の印 + // ただのListと見分ける if marker == 'B' || marker == 'I' || marker == 'L' { self.position += 2; return self.parse_typed_array(marker); @@ -170,7 +168,7 @@ impl Parser { return Ok(list); } - // 要素を 1 つずつ読む + // 要素を1つずつ読む loop { self.skip_whitespace(); @@ -182,7 +180,7 @@ impl Parser { let value = self.parse_value()?; - // 異種リストはバイナリ NBT へ写せないため受理しない (adr/0006) + // 異種リストはバイナリNBTへ写せないため受理しない if list.element_type() != TagType::End && list.element_type() != value.tag_type() { return Err(self.malformed(format!( "リストに異なる型が混在している: {} と {}", @@ -246,11 +244,11 @@ impl Parser { } } - // [B; は TAG_Byte_Array + // [B;はTAG_Byte_Array if marker == 'B' { let mut result: Vec = Vec::with_capacity(values.len()); - // 各要素が Byte の範囲に収まるか確認しながら詰める + // 各要素がByteの範囲に収まるか確認しながら詰める for value in &values { if *value < i8::MIN as i64 || *value > i8::MAX as i64 { return Err(self.malformed(format!("ByteArray の要素が範囲外: {value}"))); @@ -262,12 +260,12 @@ impl Parser { return Ok(NbtTag::ByteArray(result)); } - // [I; は TAG_Int_Array - // 残りは TAG_Long_Array + // [I;はTAG_Int_Array + // 残りはTAG_Long_Array if marker == 'I' { let mut result: Vec = Vec::with_capacity(values.len()); - // 各要素が Int の範囲に収まるか確認しながら詰める + // 各要素がIntの範囲に収まるか確認しながら詰める for value in &values { if *value < i32::MIN as i64 || *value > i32::MAX as i64 { return Err(self.malformed(format!("IntArray の要素が範囲外: {value}"))); @@ -321,9 +319,9 @@ impl Parser { Ok(bare) } - /// 引用符つき文字列を UTF-16 コード単位の列として読む + /// 引用符つき文字列をUTF-16コード単位の列として読む /// - /// `\uXXXX` で孤立サロゲートを書けるため、`String` ではなくコード単位で受ける + /// `\uXXXX`で孤立サロゲートを書けるため、`String`ではなくコード単位で受ける fn parse_quoted_string(&mut self) -> Result> { let quote = self.chars[self.position]; self.position += 1; @@ -376,31 +374,37 @@ impl Parser { _ => None, }; - // 1 文字で置き換えられるエスケープはここで片付く + // 1文字で置き換えられるエスケープはここで片付く if let Some(value) = simple { let mut buffer = [0u16; 2]; units.extend_from_slice(value.encode_utf16(&mut buffer)); return Ok(()); } - // \x は 2 桁の 16 進表記 + // \xは2桁の16進表記 if c == 'x' { units.push(self.read_hex_digits(2)? as u16); return Ok(()); } if c == 'u' { - // \uXXXX は UTF-16 コード単位を直接指定する + // \uXXXXはUTF-16コード単位を直接指定する // 孤立サロゲートもここで書ける units.push(self.read_hex_digits(4)? as u16); return Ok(()); } - // \U は 8 桁のコードポイント表記 + // \Uは8桁のコードポイント表記 if c == 'U' { let code_point = self.read_hex_digits(8)?; - // Unicode のコードポイント範囲を外れていないか確認する + // サロゲートの範囲は、\uXXXXと同じくそのコード単位を1つ置く + if (0xD800..=0xDFFF).contains(&code_point) { + units.push(code_point as u16); + return Ok(()); + } + + // Unicodeのコードポイント範囲外を弾く let value = match char::from_u32(code_point as u32) { Some(value) => value, None => { @@ -429,7 +433,7 @@ impl Parser { let mut value: u64 = 0; - // 指定桁数ぶん 16進数字を読む + // 指定桁数ぶん16進数字を読む for offset in 0..count { let c = self.chars[self.position + offset]; let digit = hex_digit_value(c); @@ -447,9 +451,9 @@ impl Parser { Ok(value) } - /// Unicode 文字名によるエスケープ `\N{...}` を読む + /// Unicode文字名によるエスケープ`\N{...}`を読む fn read_named_character(&mut self) -> Error { - // 実装間で Unicode 文字名の表が揃わないため対応しない(C# / Rust には表が無い) + // 実装間でUnicode文字名の表が揃わないため対応しない(C# / TypeScript / Rustには表が無い) let start = self.position; // 閉じ波括弧まで読み飛ばす @@ -461,7 +465,7 @@ impl Parser { Error::new( ErrorCode::UnsupportedFeature, - format!("文字名によるエスケープには対応していない: \\N{name}"), + format!("文字名によるエスケープには対応していない: \\N{{{name}}}"), ) } @@ -475,7 +479,7 @@ impl Parser { ))); } - // bool(...) / uuid(...) の関数呼び出し + // bool(...) / uuid(...)の関数呼び出し self.skip_whitespace(); if self.peek_or_nul() == '(' && (token == "bool" || token == "uuid") { return self.parse_function(&token); @@ -502,7 +506,7 @@ impl Parser { self.expect(')')?; if name == "bool" { - // 0 以外を真とする + // 0以外を真とする if self.to_integral(&argument)? != 0 { return Ok(NbtTag::Byte(1)); } @@ -536,7 +540,7 @@ impl Parser { let mut values: Vec = Vec::with_capacity(4); - // UUID を上位から 32bit ずつ 4 要素の IntArray へ写す + // UUIDを上位から32bitずつ4要素のIntArrayへ写す for index in 0..4 { let chunk = &hex[index * 8..(index + 1) * 8]; @@ -550,7 +554,7 @@ impl Parser { } /// 数値トークンを解釈する - /// 数値として読めなければ `None`(文字列として扱われる) + /// 数値として読めなければ`None`を返す(文字列として扱われる) fn try_parse_number(&self, token: &str) -> Result> { let chars: Vec = token.chars().collect(); let mut negative = false; @@ -574,7 +578,7 @@ impl Parser { let is_hex = is_hex_body(&body); // 幅接尾辞を末尾から剥がす - // 16進では b/d/f が数字と紛れるため s/l だけを認める + // 16進ではb/d/fが数字と紛れるためs/lだけを認める let last = body.chars().last().unwrap(); let suffix_allowed = if is_hex { last == 's' || last == 'S' || last == 'l' || last == 'L' @@ -582,18 +586,18 @@ impl Parser { WIDTH_SUFFIXES.contains(last) }; - // 末尾 1 文字が型の印なら切り離す - // 1 文字だけの token は数字そのもの + // 末尾1文字が型の印なら切り離す + // 符号を除いた本体が1文字だけなら、接尾辞とみなさず剥がさない if suffix_allowed && body.chars().count() >= 2 { width_suffix = last.to_ascii_lowercase(); body.pop(); - // 符号接尾辞 u / s は幅接尾辞の手前に置かれる + // 符号接尾辞u / sは幅接尾辞の手前に置かれる if body.chars().count() >= 2 { let sign_char = body.chars().last().unwrap(); - // u / U は符号なしの印 - // 1.21.5 以降の拡張構文 + // u / Uは符号なしの印 + // 1.21.5以降の拡張構文 if sign_char == 'u' || sign_char == 'U' { unsigned_suffix = true; body.pop(); @@ -646,7 +650,7 @@ impl Parser { return Ok(NbtTag::Float(signed as f32)); } - // 接尾辞なしの小数は Double + // 接尾辞なしの小数はDouble if width_suffix == '\0' || width_suffix == 'd' { return Ok(NbtTag::Double(signed)); } @@ -731,8 +735,8 @@ impl Parser { 'l' => Ok(NbtTag::Long(value)), 'f' => Ok(NbtTag::Float(value as f32)), 'd' => Ok(NbtTag::Double(value as f64)), - // 接尾辞なしの整数は Int - // 暗黙に Long へ格上げしない + // 接尾辞なしの整数はInt + // 暗黙にLongへ格上げしない _ => Ok(NbtTag::Int(self.check_range( value, i32::MIN as i64, @@ -754,7 +758,7 @@ impl Parser { fn to_signed(&self, magnitude: u64, negative: bool) -> Result { if negative { - // i64::MIN の絶対値は i64 に収まらないため個別に扱う + // i64::MINの絶対値はi64に収まらないため個別に扱う if magnitude == 9223372036854775808 { return Ok(i64::MIN); } @@ -808,7 +812,7 @@ impl Parser { } /// 末尾でもエラーにしない先読み - /// 入力が尽きていれば NUL を返す + /// 入力が尽きていればNULを返す fn peek_or_nul(&self) -> char { if self.position >= self.chars.len() { return '\0'; @@ -855,7 +859,7 @@ fn is_binary_body(body: &str) -> bool { return false; } - // 2進リテラルの本体は 0 と 1 だけ + // 2進リテラルの本体は0と1だけ for c in &chars[2..] { if *c != '0' && *c != '1' { return false; @@ -905,7 +909,7 @@ fn is_bare_char(c: char) -> bool { // --------------------------------------------------------------------------- /// タグを書き出す -/// `depth` が負なら 1 行、0 以上なら整形して出力する +/// `depth`が負なら1行、0以上なら整形して出力する fn write_tag(out: &mut String, tag: &NbtTag, depth: i32) { match tag { NbtTag::Byte(value) => { @@ -949,7 +953,7 @@ fn write_compound(out: &mut String, compound: &NbtCompound, depth: i32) { // 挿入順のまま「キー: 値」を並べる for (key, value) in compound.iter() { - // 2 つ目以降の前に区切りのカンマを置く + // 2つ目以降の前に区切りのカンマを置く if !first { out.push(','); } @@ -982,7 +986,7 @@ fn write_list(out: &mut String, list: &NbtList, depth: i32) { // 要素型は共通なので値だけを並べる for item in list.iter() { - // 2 つ目以降の前に区切りのカンマを置く + // 2つ目以降の前に区切りのカンマを置く if !first { out.push(','); } @@ -1006,9 +1010,9 @@ fn write_typed_array( out.push(marker); out.push(';'); - // 型付き配列は 1 行に収める + // 型付き配列は1行に収める for (index, value) in values.iter().enumerate() { - // 2 つ目以降の前に区切りのカンマを置く + // 2つ目以降の前に区切りのカンマを置く if index > 0 { out.push(','); } @@ -1020,7 +1024,7 @@ fn write_typed_array( out.push(']'); } -/// 整形出力なら改行とインデントを、1 行出力なら何も入れない +/// 整形出力なら改行とインデントを、1行出力なら何も入れない fn append_separator(out: &mut String, depth: i32) { if depth < 0 { return; @@ -1034,7 +1038,7 @@ fn append_separator(out: &mut String, depth: i32) { } } -/// 整形出力のときだけ深さを 1 段進める +/// 整形出力のときだけ深さを1段進める fn next_depth(depth: i32) -> i32 { if depth < 0 { return -1; @@ -1070,14 +1074,13 @@ fn is_bare_writable(text: &str) -> bool { /// 文字列を二重引用符で囲み、必要な文字だけエスケープする /// -/// UTF-16 コード単位で処理するのは、正しいサロゲートペアと孤立サロゲートを -/// 区別するため -/// C# / Java と同じ判定にしないと出力が食い違う +/// UTF-16コード単位で処理するのは、正しいサロゲートペアと孤立サロゲートを区別するため +/// C# / Javaと同じ判定にしないと出力が食い違う fn quote_string(units: &[u16]) -> String { let mut out = String::from("\""); let mut index = 0usize; - // 1 コード単位ずつ見てエスケープが要るものだけ置き換える + // 1コード単位ずつ見てエスケープが要るものだけ置き換える while index < units.len() { let unit = units[index]; @@ -1106,7 +1109,7 @@ fn quote_string(units: &[u16]) -> String { index += 1; } else if unit < 0x20 || unit == 0x7F || (0xD800..=0xDFFF).contains(&unit) { - // 制御文字と孤立サロゲートは \uXXXX で表す + // 制御文字と孤立サロゲートは\uXXXXで表す out.push_str(&format!("\\u{unit:04x}")); } else if let Some(value) = char::from_u32(unit as u32) { out.push(value); diff --git a/rust/src/nbt/tag.rs b/rust/src/nbt/tag.rs index 23b4c52..83c5c74 100644 --- a/rust/src/nbt/tag.rs +++ b/rust/src/nbt/tag.rs @@ -1,6 +1,4 @@ -//! NBT のタグ型と値モデル -//! -//! 仕様: `docs/spec/10-nbt-binary.md` 1章・7章 +//! NBTのタグ型と値モデル use std::collections::HashMap; use std::fmt; @@ -8,12 +6,12 @@ use std::fmt; use crate::error::{Error, ErrorCode, Result}; use crate::nbt::mutf8; -/// NBT のタグ型 +/// NBTのタグ型 /// 値は仕様が定めるタグIDと一致する #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum TagType { /// TAG_End (0) - /// Compound の終端を表す + /// Compoundの終端を表す End, /// TAG_Byte (1) Byte, @@ -80,7 +78,7 @@ impl TagType { } } - /// タグIDから [`TagType`] を得る + /// タグIDから[`TagType`]を得る /// 未知のIDならエラー pub fn from_id(id: u8) -> Result { match id { @@ -97,7 +95,7 @@ impl TagType { 10 => Ok(TagType::Compound), 11 => Ok(TagType::IntArray), 12 => Ok(TagType::LongArray), - // 0..12 の範囲外はすべて不正なタグID + // 0..12の範囲外はすべて不正なタグID other => Err(Error::new( ErrorCode::MalformedData, format!("未知のタグID: {other}"), @@ -106,18 +104,16 @@ impl TagType { } } -/// TAG_String の値 +/// TAG_Stringの値 /// -/// Rust の [`String`] は UTF-8 の不変条件を持つため、 -/// 孤立サロゲートを含む文字列をそのまま保持できない -/// 他の3言語(C# / Java / Python)は UTF-16 相当でそれを保持できてしまうので、 -/// ここだけ表現を分けて往復性を保っている +/// Rustの[`String`]はUTF-8の不変条件を持つため、孤立サロゲートを含む文字列をそのまま保持できない +/// 他の4言語(C# / Java / TypeScript / Python)はUTF-16相当でそれを保持できてしまうので、ここだけ表現を分けて往復性を保っている #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum NbtString { - /// UTF-8 として表せる通常の文字列 + /// UTF-8として表せる通常の文字列 /// 実データはほぼすべてこちら Text(String), - /// UTF-8 に写せない UTF-16 コード単位の列(孤立サロゲートを含む場合) + /// UTF-8に写せないUTF-16コード単位の列(孤立サロゲートを含む場合) Surrogates(Vec), } @@ -127,8 +123,8 @@ impl NbtString { NbtString::Text(text.into()) } - /// UTF-16 コード単位の列から作る - /// UTF-8 に写せなければ [`NbtString::Surrogates`] になる + /// UTF-16コード単位の列から作る + /// UTF-8に写せなければ[`NbtString::Surrogates`]になる pub fn from_utf16(units: Vec) -> NbtString { match String::from_utf16(&units) { Ok(text) => NbtString::Text(text), @@ -137,7 +133,7 @@ impl NbtString { } } - /// UTF-8 として表せる場合だけ文字列を返す + /// UTF-8として表せる場合だけ文字列を返す pub fn as_str(&self) -> Option<&str> { match self { NbtString::Text(text) => Some(text.as_str()), @@ -145,7 +141,7 @@ impl NbtString { } } - /// UTF-16 コード単位の列を返す + /// UTF-16コード単位の列を返す pub fn to_utf16(&self) -> Vec { match self { NbtString::Text(text) => text.encode_utf16().collect(), @@ -153,12 +149,12 @@ impl NbtString { } } - /// MUTF-8 バイト列を返す + /// MUTF-8バイト列を返す pub fn to_mutf8(&self) -> Vec { mutf8::encode_from_utf16(&self.to_utf16()) } - /// MUTF-8 で符号化したときのバイト長 + /// MUTF-8で符号化したときのバイト長 pub fn mutf8_len(&self) -> usize { self.to_mutf8().len() } @@ -176,9 +172,7 @@ impl From for NbtString { } } -/// NBT のタグ -/// -/// 仕様: `docs/spec/10-nbt-binary.md` 1章 +/// NBTのタグ #[derive(Debug, Clone)] pub enum NbtTag { /// TAG_Byte @@ -234,7 +228,7 @@ impl PartialEq for NbtTag { (NbtTag::Short(a), NbtTag::Short(b)) => a == b, (NbtTag::Int(a), NbtTag::Int(b)) => a == b, (NbtTag::Long(a), NbtTag::Long(b)) => a == b, - // NaN や -0.0 を区別するため、値ではなくビットパターンで比較する + // NaNや-0.0を区別するため、値ではなくビットパターンで比較する (NbtTag::Float(a), NbtTag::Float(b)) => a.to_bits() == b.to_bits(), (NbtTag::Double(a), NbtTag::Double(b)) => a.to_bits() == b.to_bits(), (NbtTag::ByteArray(a), NbtTag::ByteArray(b)) => a == b, @@ -249,12 +243,11 @@ impl PartialEq for NbtTag { } /// TAG_List -/// 要素型が 1 つに固定されたタグの列 +/// 要素型が1つに固定されたタグの列 /// -/// 空リストの要素型は [`TagType::End`] +/// 空リストの要素型は[`TagType::End`] /// 最初の要素を追加した時点で型が確定する -/// 全要素を削除しても確定済みの要素型は維持される -/// (読み書きの往復で型が消えないようにするため) +/// 読み書きの往復で型が消えないよう、全要素を削除しても確定済みの要素型は維持される #[derive(Debug, Clone, PartialEq)] pub struct NbtList { element_type: TagType, @@ -274,7 +267,7 @@ impl NbtList { } /// 要素の型 - /// 空で未確定なら [`TagType::End`] + /// 空で未確定なら[`TagType::End`] pub fn element_type(&self) -> TagType { self.element_type } @@ -303,7 +296,13 @@ impl NbtList { } /// 位置を指定して挿入する + /// 位置が要素数を超えていればエラー pub fn insert(&mut self, index: usize, item: NbtTag) -> Result<()> { + // 要素型を確定させる前に位置を確かめ、失敗したときに状態を変えない + if index > self.items.len() { + return Err(Error::invalid_argument(format!("位置が範囲外: {index}"))); + } + self.ensure_element_type(&item)?; self.items.insert(index, item); Ok(()) @@ -375,7 +374,7 @@ pub struct NbtCompound { } impl NbtCompound { - /// 空の Compound を作る + /// 空のCompoundを作る pub fn new() -> NbtCompound { NbtCompound { entries: Vec::new(), index: HashMap::new() } } @@ -423,7 +422,7 @@ impl NbtCompound { } /// キーに対応するタグを返す - /// 存在しなければ `None` + /// 存在しなければ`None` pub fn opt(&self, key: &str) -> Option<&NbtTag> { match self.index.get(key) { Some(&position) => Some(&self.entries[position].1), @@ -444,7 +443,7 @@ impl NbtCompound { } /// キーを削除する - /// 削除できたら `true` + /// 削除できたら`true` pub fn remove(&mut self, key: &str) -> bool { let position = match self.index.remove(key) { Some(position) => position, @@ -487,8 +486,8 @@ impl<'a> IntoIterator for &'a NbtCompound { /// 型付き取得子 /// /// 「キーが無い」と「型が違う」は区別する -/// `opt_*` はキーが無ければ `None` を返し、`get_*` はエラーにする -/// どちらも型が違えば必ず [`ErrorCode::UnexpectedTagType`] になる +/// `opt_*`はキーが無ければ`None`を返し、`get_*`はエラーにする +/// どちらも型が違えば必ず[`ErrorCode::UnexpectedTagType`]になる impl NbtCompound { fn wrong_type(key: &str, actual: TagType, expected: &str) -> Error { Error::new( @@ -507,8 +506,8 @@ impl NbtCompound { /// スカラ型の取得子を生やすためのマクロ /// -/// 6 種類ぶんを手で書くと差分が生まれやすいので、1 か所から展開する -/// 参照を返す取得子は寿命の注釈が要るため、マクロにせず個別に書く +/// 6種類ぶんを手で書くと差分が生まれやすいので、1か所から展開する +/// 参照を返す取得子は、配列型を`ref_accessors`で展開し、String / List / Compoundは個別に書く macro_rules! scalar_accessors { ($opt:ident, $get:ident, $variant:ident, $ret:ty, $label:literal) => { impl NbtCompound { @@ -541,8 +540,8 @@ scalar_accessors!(opt_double, get_double, Double, f64, "Double"); /// スカラ型の設定子を生やすためのマクロ /// -/// `set(key, NbtTag::Int(42))` と書かずに済むようにするための糖衣 -/// 取得子の `get_int` と対になる +/// `set(key, NbtTag::Int(42))`と書かずに済むようにするための糖衣 +/// 取得子の`get_int`と対になる macro_rules! scalar_setters { ($set:ident, $variant:ident, $arg:ty, $label:literal) => { impl NbtCompound { @@ -562,10 +561,10 @@ scalar_setters!(set_float, Float, f32, "Float"); scalar_setters!(set_double, Double, f64, "Double"); impl NbtCompound { - /// TAG_Byte として設定する - /// `true` は 1、`false` は 0 + /// TAG_Byteとして設定する + /// `true`は1、`false`は0 pub fn set_bool(&mut self, key: &str, value: bool) { - // NBT に真偽値の専用型は無いので TAG_Byte の 0 / 1 で表す + // NBTに真偽値の専用型は無いので、TAG_Byteの0 / 1で表す if value { self.set_byte(key, 1); } else { @@ -573,22 +572,22 @@ impl NbtCompound { } } - /// TAG_String として設定する + /// TAG_Stringとして設定する pub fn set_string(&mut self, key: &str, value: &str) { self.set(key, NbtTag::String(NbtString::from(value))); } - /// TAG_Byte_Array として設定する + /// TAG_Byte_Arrayとして設定する pub fn set_byte_array(&mut self, key: &str, value: Vec) { self.set(key, NbtTag::ByteArray(value)); } - /// TAG_Int_Array として設定する + /// TAG_Int_Arrayとして設定する pub fn set_int_array(&mut self, key: &str, value: Vec) { self.set(key, NbtTag::IntArray(value)); } - /// TAG_Long_Array として設定する + /// TAG_Long_Arrayとして設定する pub fn set_long_array(&mut self, key: &str, value: Vec) { self.set(key, NbtTag::LongArray(value)); } @@ -623,8 +622,8 @@ ref_accessors!(opt_int_array, get_int_array, IntArray, [i32], "Int_Array", as_sl ref_accessors!(opt_long_array, get_long_array, LongArray, [i64], "Long_Array", as_slice); impl NbtCompound { - /// TAG_String を [`NbtString`] のまま取得する - /// キーが無ければ `None` + /// TAG_Stringを[`NbtString`]のまま取得する + /// キーが無ければ`None` pub fn opt_string_tag(&self, key: &str) -> Result> { match self.opt(key) { None => Ok(None), @@ -633,7 +632,7 @@ impl NbtCompound { } } - /// TAG_String を [`NbtString`] のまま取得する + /// TAG_Stringを[`NbtString`]のまま取得する /// キーが無ければエラー pub fn get_string_tag(&self, key: &str) -> Result<&NbtString> { match self.opt_string_tag(key)? { @@ -642,8 +641,8 @@ impl NbtCompound { } } - /// TAG_List を取得する - /// キーが無ければ `None` + /// TAG_Listを取得する + /// キーが無ければ`None` pub fn opt_list(&self, key: &str) -> Result> { match self.opt(key) { None => Ok(None), @@ -652,7 +651,7 @@ impl NbtCompound { } } - /// TAG_List を取得する + /// TAG_Listを取得する /// キーが無ければエラー pub fn get_list(&self, key: &str) -> Result<&NbtList> { match self.opt_list(key)? { @@ -661,8 +660,8 @@ impl NbtCompound { } } - /// TAG_Compound を取得する - /// キーが無ければ `None` + /// TAG_Compoundを取得する + /// キーが無ければ`None` pub fn opt_compound(&self, key: &str) -> Result> { match self.opt(key) { None => Ok(None), @@ -671,7 +670,7 @@ impl NbtCompound { } } - /// TAG_Compound を取得する + /// TAG_Compoundを取得する /// キーが無ければエラー pub fn get_compound(&self, key: &str) -> Result<&NbtCompound> { match self.opt_compound(key)? { @@ -682,12 +681,11 @@ impl NbtCompound { } impl NbtCompound { - /// TAG_String を UTF-8 文字列として取得する - /// キーが無ければ `None` + /// TAG_StringをUTF-8文字列として取得する + /// キーが無ければ`None` /// /// 孤立サロゲートを含む文字列はエラーになる - /// その場合は - /// [`NbtCompound::opt_string_tag`] で [`NbtString`] のまま取り出す + /// その場合は[`NbtCompound::opt_string_tag`]で[`NbtString`]のまま取り出す pub fn opt_string(&self, key: &str) -> Result> { match self.opt_string_tag(key)? { None => Ok(None), @@ -701,7 +699,7 @@ impl NbtCompound { } } - /// TAG_String を UTF-8 文字列として取得する + /// TAG_StringをUTF-8文字列として取得する /// キーが無ければエラー pub fn get_string(&self, key: &str) -> Result<&str> { match self.opt_string(key)? { @@ -710,9 +708,9 @@ impl NbtCompound { } } - /// TAG_Byte を真偽値として取得する - /// 0 以外が `true` - /// キーが無ければ `None` + /// TAG_Byteを真偽値として取得する + /// 0以外が`true` + /// キーが無ければ`None` pub fn opt_bool(&self, key: &str) -> Result> { match self.opt_byte(key)? { Some(value) => Ok(Some(value != 0)), @@ -720,8 +718,8 @@ impl NbtCompound { } } - /// TAG_Byte を真偽値として取得する - /// 0 以外が `true` + /// TAG_Byteを真偽値として取得する + /// 0以外が`true` /// キーが無ければエラー pub fn get_bool(&self, key: &str) -> Result { Ok(self.get_byte(key)? != 0) @@ -732,7 +730,7 @@ impl NbtCompound { // 人が読むための表現 // // デバッグ用であり、中身の形式は仕様で決めていない -// SNBT が要るなら `snbt::write` を使う +// SNBTが要るなら`snbt::write`を使う // --------------------------------------------------------------------------- impl fmt::Display for NbtString { diff --git a/rust/src/world/bit_storage.rs b/rust/src/world/bit_storage.rs index ad53715..9cf586e 100644 --- a/rust/src/world/bit_storage.rs +++ b/rust/src/world/bit_storage.rs @@ -1,14 +1,12 @@ -//! 添字を 64bit 整数の配列へ詰めた表現 -//! 1.16 以降の**跨ぎなし**パッキング +//! 添字を64bit整数の配列へ詰めた表現 +//! 1.16以降の**跨ぎなし**パッキング //! -//! 1 つの `i64` に入りきらない分は、その `i64` の残りビットを未使用のまま捨て、 -//! 次の `i64` の最下位ビットから始める -//! -//! 仕様: `docs/spec/31-paletted-container.md` 2章 +//! 1つの`i64`に入りきらない分は、その`i64`の残りビットを未使用のまま捨て、 +//! 次の`i64`の最下位ビットから始める use crate::error::{Error, ErrorCode, Result}; -/// packed な添字の並び +/// packedな添字の並び #[derive(Debug, Clone, PartialEq, Eq)] pub struct BitStorage { data: Vec, @@ -17,18 +15,18 @@ pub struct BitStorage { } impl BitStorage { - /// 1 エントリあたりのビット数 + /// 1エントリあたりのビット数 pub fn bits_per_entry(&self) -> usize { self.bits_per_entry } /// エントリ数 - /// ブロックなら 4096、バイオームなら 64 + /// ブロックなら4096、バイオームなら64 pub fn entry_count(&self) -> usize { self.entry_count } - /// 1 つの `i64` に入るエントリ数 + /// 1つの`i64`に入るエントリ数 pub fn values_per_long(&self) -> usize { 64 / self.bits_per_entry } @@ -49,10 +47,9 @@ impl BitStorage { }) } - /// 既存の `i64` 配列から作る + /// 既存の`i64`配列から作る /// - /// `lenient` が `true` なら、配列長が期待値と違う場合に - /// 配列長からビット幅を逆算して読む + /// `lenient`が`true`なら、配列長が期待値と違う場合に配列長からビット幅を逆算して読む /// 第三者ツールが書いたデータの救済用 pub fn from_longs( data: Vec, @@ -93,7 +90,7 @@ impl BitStorage { )) } - /// 必要な `i64` の個数を求める + /// 必要な`i64`の個数を求める pub fn long_count(bits_per_entry: usize, entry_count: usize) -> usize { let values_per_long = 64 / bits_per_entry; entry_count.div_ceil(values_per_long) @@ -108,7 +105,7 @@ impl BitStorage { let bit_offset = (index % per_long) * self.bits_per_entry; let mask = (1u64 << self.bits_per_entry) - 1; - // 符号付きのままシフトすると符号が伸びるので、符号なしへ直してから動かす + // 符号付きのままシフトすると上位ビットが伸びるので、符号なしへ直してから動かす let unsigned = self.data[long_index] as u64; Ok(((unsigned >> bit_offset) & mask) as u32) } @@ -136,17 +133,17 @@ impl BitStorage { Ok(()) } - /// packed な配列を返す + /// packedな配列を返す pub fn as_longs(&self) -> &[i64] { &self.data } - /// packed な配列を複製して返す + /// packedな配列を複製して返す pub fn to_longs(&self) -> Vec { self.data.clone() } - /// packed な配列を取り出す + /// packedな配列を取り出す pub fn into_longs(self) -> Vec { self.data } diff --git a/rust/src/world/block_pos.rs b/rust/src/world/block_pos.rs index fe27452..2e4a53e 100644 --- a/rust/src/world/block_pos.rs +++ b/rust/src/world/block_pos.rs @@ -1,6 +1,4 @@ //! ブロックの絶対座標と、その範囲 -//! -//! 仕様: `docs/spec/30-chunk-format.md` 5章 use std::fmt; @@ -51,25 +49,25 @@ impl fmt::Display for BlockPos { } } -/// ブロック座標の直方体な範囲 +/// ブロック座標の直方体の範囲 /// /// 両端を含む -/// `Cuboid::of(0, 0, 0, 0, 0, 0)` は 1 ブロック +/// `Cuboid::of(0, 0, 0, 0, 0, 0)`は1ブロック /// /// 範囲内のブロックを順に処理したいときに使う #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Cuboid { - /// X の最小値 + /// Xの最小値 pub min_x: i32, - /// Y の最小値 + /// Yの最小値 pub min_y: i32, - /// Z の最小値 + /// Zの最小値 pub min_z: i32, - /// X の最大値(含む) + /// Xの最大値(含む) pub max_x: i32, - /// Y の最大値(含む) + /// Yの最大値(含む) pub max_y: i32, - /// Z の最大値(含む) + /// Zの最大値(含む) pub max_z: i32, } @@ -93,17 +91,17 @@ impl Cuboid { Cuboid::new(BlockPos::new(x1, y1, z1), BlockPos::new(x2, y2, z2)) } - /// X 方向の長さ + /// X方向の長さ pub fn size_x(&self) -> i32 { self.max_x - self.min_x + 1 } - /// Y 方向の長さ + /// Y方向の長さ pub fn size_y(&self) -> i32 { self.max_y - self.min_y + 1 } - /// Z 方向の長さ + /// Z方向の長さ pub fn size_z(&self) -> i32 { self.max_z - self.min_z + 1 } @@ -124,9 +122,9 @@ impl Cuboid { } /// 範囲内の座標を順に返す - /// 並びは Y、Z、X の順で、X がいちばん内側で動く + /// 並びはY、Z、Xの順で、Xがいちばん内側で動く pub fn positions(&self) -> impl Iterator + '_ { - // 内側から X が動くので、同じチャンクの並びを続けて触れる + // Xがいちばん内側で動くので、同じチャンクの並びを続けて扱える (self.min_y..=self.max_y).flat_map(move |y| { (self.min_z..=self.max_z) .flat_map(move |z| (self.min_x..=self.max_x).map(move |x| BlockPos::new(x, y, z))) diff --git a/rust/src/world/block_state.rs b/rust/src/world/block_state.rs index 8d151a4..d22c9bd 100644 --- a/rust/src/world/block_state.rs +++ b/rust/src/world/block_state.rs @@ -2,13 +2,9 @@ //! 名前と、任意のプロパティの組 //! //! プロパティは**常に名前の昇順で保持する** -//! こうしておくと文字列表現が一意になり、 -//! 全言語で同じ出力になる -//! Minecraft が書き出した並び順は -//! [`super::PalettedContainer`] がパレットを生の NBT のまま持つことで守られるので、 -//! 触っていないブロックの並びが崩れることはない -//! -//! 仕様: `docs/spec/30-chunk-format.md` 2.1章 +//! こうしておくと文字列表現が一意になり、全言語で同じ出力になる +//! Minecraftが書き出した並び順は、[`super::PalettedContainer`]がパレットを生のNBTのまま持つことで守られる +//! そのため、触っていないブロックの並びが崩れることはない use std::borrow::Cow; use std::collections::BTreeMap; @@ -24,14 +20,13 @@ pub struct BlockState { properties: BTreeMap, } -/// [`Chunk::set_block`](crate::world::Chunk::set_block) へ渡せるもの +/// [`Chunk::set_block`](crate::world::Chunk::set_block)へ渡せるもの /// -/// 他の言語ではオーバーロードで済むが、Rust には無いのでトレイトで受ける -/// `&BlockState` と `&str` の両方をそのまま渡せる +/// 他の言語ではオーバーロードで済むが、Rustには無いのでトレイトで受ける +/// `&BlockState`と`&str`の両方をそのまま渡せる /// -/// [`Cow`] を返すのは、すでに [`BlockState`] を持っているときに -/// 複製を作らないようにするためである -/// 1 ブロックずつ置く用途では、この複製が積み上がって効いてくる +/// [`Cow`]を返すのは、すでに[`BlockState`]を持っているときに複製を作らないようにするためである +/// 1ブロックずつ置く用途では、この複製が積み重なって無視できなくなる pub trait IntoBlockState { /// ブロック状態として借りる /// 文字列から作った場合だけ、その場で組み立てたものを返す @@ -53,7 +48,7 @@ impl IntoBlockState for str { impl BlockState { /// 名前とプロパティを指定して作る /// - /// 名前空間が省略されていたら `minecraft:` を補う + /// 名前空間が省略されていたら`minecraft:`を補う pub fn new(name: impl AsRef, properties: BTreeMap) -> BlockState { BlockState { name: normalize(name.as_ref()), properties } } @@ -75,19 +70,19 @@ impl BlockState { } /// プロパティを取得する - /// 無ければ `None` + /// 無ければ`None` pub fn property(&self, key: &str) -> Option<&str> { self.properties.get(key).map(|value| value.as_str()) } - /// プロパティを 1 つ差し替えた新しい状態を返す + /// プロパティを1つ差し替えた新しい状態を返す pub fn with(&self, key: impl Into, value: impl Into) -> BlockState { let mut result = self.clone(); result.properties.insert(key.into(), value.into()); result } - /// `minecraft:oak_stairs[facing=north,half=top]` 形式の文字列から作る + /// `minecraft:oak_stairs[facing=north,half=top]`形式の文字列から作る pub fn parse(text: &str) -> Result { let bracket = match text.find('[') { Some(position) => position, @@ -111,7 +106,7 @@ impl BlockState { let mut properties = BTreeMap::new(); if !body.is_empty() { - // "key=value" をカンマ区切りで読む + // "key=value"をカンマ区切りで読む for pair in body.split(',') { let equals = match pair.find('=') { Some(position) => position, @@ -147,13 +142,13 @@ impl BlockState { Ok(BlockState::new(&text[..bracket], properties)) } - /// パレット要素の NBT から作る + /// パレット要素のNBTから作る pub fn from_nbt(nbt: &NbtCompound) -> Result { let name = nbt.get_string("Name")?.to_string(); let mut properties = BTreeMap::new(); if let Some(properties_tag) = nbt.opt_compound("Properties")? { - // Properties の値はすべて文字列(数値や真偽値も文字列で入る) + // Propertiesの値はすべて文字列(数値や真偽値も文字列で入る) for (key, value) in properties_tag.iter() { match value { NbtTag::String(text) => match text.as_str() { @@ -183,10 +178,10 @@ impl BlockState { Ok(BlockState::new(name, properties)) } - /// パレット要素の NBT へ変換する + /// パレット要素のNBTへ変換する /// - /// プロパティが空なら `Properties` キー自体を出力しない - /// Minecraft と同じ振る舞い + /// プロパティが空なら`Properties`キー自体を出力しない + /// Minecraftと同じ振る舞い pub fn to_nbt(&self) -> NbtCompound { let mut result = NbtCompound::new(); result.set("Name", NbtTag::String(NbtString::new(self.name.clone()))); @@ -208,7 +203,7 @@ impl BlockState { } impl fmt::Display for BlockState { - /// `minecraft:oak_stairs[facing=north,half=top]` 形式の文字列を返す + /// `minecraft:oak_stairs[facing=north,half=top]`形式の文字列を返す fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.name)?; @@ -221,7 +216,7 @@ impl fmt::Display for BlockState { // 名前の昇順で並べるので、同じ状態なら必ず同じ文字列になる for (key, value) in &self.properties { - // 2 つ目以降の前に区切りのカンマを置く + // 2つ目以降の前に区切りのカンマを置く if !first { f.write_str(",")?; } @@ -234,7 +229,7 @@ impl fmt::Display for BlockState { } } -/// 名前空間が省略されていたら `minecraft:` を補う +/// 名前空間が省略されていたら`minecraft:`を補う fn normalize(name: &str) -> String { if name.contains(':') { return name.to_string(); diff --git a/rust/src/world/chunk.rs b/rust/src/world/chunk.rs index 65bbefd..976cad8 100644 --- a/rust/src/world/chunk.rs +++ b/rust/src/world/chunk.rs @@ -1,10 +1,8 @@ -//! チャンク 1 つ分 -//! 地形の読み書きの入口 +//! チャンク1つ分 +//! 地形の読み書きはここから行う //! -//! 読んだ NBT をそのまま保持し、変更した部分だけを書き戻す -//! 未知のキーを落とさないので、将来の追加要素があってもデータを壊さない -//! -//! 仕様: `docs/spec/30-chunk-format.md` +//! 読んだNBTをそのまま保持し、書き戻すときに作り直すのはセクションだけにする +//! 未知のキーを落とさないので、将来の追加要素があってもデータをおかしくしない use std::collections::BTreeMap; @@ -15,10 +13,10 @@ use crate::MIN_SUPPORTED_DATA_VERSION; use super::block_state::{BlockState, IntoBlockState}; use super::paletted_container::PalettedContainer; -/// セクション 1 つに入るブロック数 +/// セクション1つに入るブロック数 pub const BLOCKS_PER_SECTION: usize = 4096; -/// セクション 1 つに入るバイオームのエントリ数(4×4×4 単位) +/// セクション1つに入るバイオームのエントリ数(4×4×4単位) pub const BIOMES_PER_SECTION: usize = 64; /// ブロックに紐づく付随データのキー @@ -44,28 +42,26 @@ fn matches_position(entry: &NbtCompound, x: i32, y: i32, z: i32) -> bool { entry_x == x && entry_y == y && entry_z == z } -/// DataVersion が対象と違ったときの動作 +/// DataVersionが扱える形式より古いときの動作 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum VersionMismatchAction { /// 警告コールバックを呼んで続行する /// 既定 Warn, - /// [`ErrorCode::UnsupportedDataVersion`] の例外にする + /// [`ErrorCode::UnsupportedDataVersion`]の例外にする Error, /// 何もしない Ignore, } /// チャンク読み込みのオプション -/// -/// 仕様: `docs/spec/30-chunk-format.md` 5章 pub struct ChunkReadOptions { - /// DataVersion が扱える形式より古いときの動作 + /// DataVersionが扱える形式より古いときの動作 pub on_version_mismatch: VersionMismatchAction, /// 警告の通知先 - /// `None` なら何もしない + /// `None`なら何もしない pub on_warning: Option>, - /// data の長さが期待値と違うとき、長さからビット幅を逆算して読むか + /// dataの長さが期待値と違うとき、長さからビット幅を逆算して読むか pub lenient_bit_storage: bool, } @@ -91,17 +87,17 @@ impl std::fmt::Debug for ChunkReadOptions { /// チャンク書き込みのオプション #[derive(Debug, Clone, Copy, Default)] pub struct ChunkWriteOptions { - /// 対象バージョン以外の DataVersion を持つチャンクの書き戻しを許すか + /// 扱える形式より古いDataVersionを持つチャンクの書き戻しを許すか /// - /// 既定は `false` - /// 古いワールドを黙って新形式で上書きし、 - /// 利用者が気づかないうちに壊すことを防ぐため(`docs/adr/0003-version-policy.md`) + /// 既定は`false` + /// エラーを出さずに古いワールドを新形式で上書きし、 + /// 利用者が気づかないうちに使えなくすることを防ぐため pub allow_foreign_data_version: bool, } -/// チャンクを Y 方向に 16 ブロックずつ区切った 16×16×16 の立方体 +/// チャンクをY方向に16ブロックずつ区切った16×16×16の立方体 /// -/// `BlockLight` / `SkyLight` などの解釈していないキーは元の NBT に残り、 +/// `BlockLight` / `SkyLight`などの解釈していないキーは元のNBTに残り、 /// 書き戻しでそのまま出力される #[derive(Debug, Clone)] pub struct ChunkSection { @@ -113,13 +109,13 @@ pub struct ChunkSection { impl ChunkSection { /// セクションのY位置 - /// オーバーワールドは -5..20 + /// オーバーワールドは-5..20 pub fn y(&self) -> i32 { self.y } /// ブロック状態 - /// 持たないセクション(光源専用)では `None` + /// 持たないセクション(光源専用)では`None` pub fn block_states(&self) -> Option<&PalettedContainer> { self.block_states.as_ref() } @@ -130,7 +126,7 @@ impl ChunkSection { } /// バイオーム - /// 持たないセクションでは `None` + /// 持たないセクションでは`None` pub fn biomes(&self) -> Option<&PalettedContainer> { self.biomes.as_ref() } @@ -150,13 +146,13 @@ impl ChunkSection { self.biomes.is_some() } - /// 元の NBT + /// 元のNBT /// 解釈していないキーもここに残っている pub fn raw(&self) -> &NbtCompound { &self.raw } - /// NBT からセクションを読む + /// NBTからセクションを読む pub fn from_nbt(nbt: &NbtCompound, options: &ChunkReadOptions) -> Result { let mut section = ChunkSection { raw: nbt.clone(), @@ -165,7 +161,7 @@ impl ChunkSection { biomes: None, }; - // 光源専用のセクションは block_states を持たない + // 光源専用のセクションはblock_statesを持たない if let Some(block_states) = nbt.opt_compound("block_states")? { section.block_states = Some(PalettedContainer::from_nbt( block_states, @@ -188,7 +184,7 @@ impl ChunkSection { Ok(section) } - /// NBT へ書き戻す + /// NBTへ書き戻す /// 解釈していないキーはそのまま残る pub fn to_nbt(&self) -> Result { let mut result = self.raw.clone(); @@ -224,7 +220,7 @@ impl ChunkSection { } } -/// チャンク 1 つ分 +/// チャンク1つ分 #[derive(Debug, Clone)] pub struct Chunk { raw: NbtCompound, @@ -249,12 +245,12 @@ impl Chunk { } /// 最下段セクションのY位置 - /// オーバーワールドは -4 + /// オーバーワールドは-4 pub fn min_section_y(&self) -> Result { self.raw.get_int("yPos") } - /// 生成段階(`minecraft:full` など) + /// 生成段階(`minecraft:full`など) pub fn status(&self) -> Result<&str> { self.raw.get_string("Status") } @@ -271,11 +267,10 @@ impl Chunk { /// このチャンクに変更が加わったか /// /// ブロックやバイオームを書き換えると立つ - /// [`Dimension::flush`](crate::world::Dimension::flush) は + /// [`Dimension::flush`](crate::world::Dimension::flush)は /// これが立っているチャンクだけを書き戻す /// - /// [`Chunk::raw`] を直接いじった場合はここが立たないので、 - /// 自分で [`Chunk::set_is_modified`] を呼ぶこと + /// [`Chunk::raw`]を直接いじった場合は立たないので、自分で[`Chunk::set_is_modified`]を呼ぶこと pub fn is_modified(&self) -> bool { self.modified } @@ -291,13 +286,13 @@ impl Chunk { self.sections.keys().copied().collect() } - /// 元の NBT + /// 元のNBT /// 解釈していないキーもここに残っている pub fn raw(&self) -> &NbtCompound { &self.raw } - /// NBT からチャンクを読む + /// NBTからチャンクを読む pub fn from_nbt(nbt: NbtCompound, options: &ChunkReadOptions) -> Result { let mut chunk = Chunk { raw: nbt, sections: BTreeMap::new(), modified: false }; chunk.check_data_version(options)?; @@ -307,7 +302,7 @@ impl Chunk { None => return Ok(chunk), }; - // 並び順に依存しないよう、Y から索引を作る + // 並び順に依存しないよう、Yから索引を作る for entry in section_list.iter() { let section_tag = match entry { NbtTag::Compound(compound) => compound, @@ -329,7 +324,7 @@ impl Chunk { Ok(chunk) } - /// DataVersion を検査し、オプションに従って警告またはエラーにする + /// DataVersionを検査し、オプションに従って警告またはエラーにする fn check_data_version(&self, options: &ChunkReadOptions) -> Result<()> { let version = self.data_version()?; @@ -358,12 +353,12 @@ impl Chunk { Ok(()) } - /// NBT へ書き戻す - /// 変更したセクションだけを反映し、他のキーはそのまま残す + /// NBTへ書き戻す + /// 全セクションを書き戻し、他のキーはそのまま残す pub fn to_nbt(&self, options: &ChunkWriteOptions) -> Result { let version = self.data_version()?; - // 形式の違う古いチャンクは、書き戻すと壊しかねない + // 形式の違う古いチャンクは、書き戻すと使えなくなりかねない if version < MIN_SUPPORTED_DATA_VERSION && !options.allow_foreign_data_version { return Err(Error::new( ErrorCode::UnsupportedDataVersion, @@ -375,7 +370,7 @@ impl Chunk { )); } - // DataVersion は読んだ値のまま残す + // DataVersionは読んだ値のまま残す // 書き換えると、そのワールドを開くゲーム側の判断を誤らせる let mut result = self.raw.clone(); @@ -385,7 +380,7 @@ impl Chunk { let mut section_list = NbtList::with_element_type(TagType::Compound); - // Y の昇順で書き出す + // Yの昇順で書き出す for section in self.sections.values() { section_list.push(NbtTag::Compound(section.to_nbt()?))?; } @@ -395,7 +390,7 @@ impl Chunk { } /// Y位置からセクションを得る - /// 無ければ `None` + /// 無ければ`None` pub fn section(&self, section_y: i32) -> Option<&ChunkSection> { self.sections.get(§ion_y) } @@ -406,7 +401,7 @@ impl Chunk { } /// ブロックを取得する - /// X と Z はチャンク内相対 (0..15)、Y は絶対座標 + /// XとZはチャンク内相対 (0..15)、Yは絶対座標 pub fn get_block(&self, x: i32, y: i32, z: i32) -> Result> { check_local_coordinates(x, z)?; @@ -431,13 +426,12 @@ impl Chunk { /// ブロックを設定する /// - /// `&BlockState` のほか、`"minecraft:oak_stairs[facing=north]"` の形の - /// 文字列でも指定できる + /// `&BlockState`のほか、`"minecraft:oak_stairs[facing=north]"`の形の文字列でも指定できる pub fn set_block(&mut self, x: i32, y: i32, z: i32, state: &S) -> Result<()> where S: IntoBlockState + ?Sized, { - // すでに BlockState を持っているなら借りるだけで済ませる + // すでにBlockStateを持っているなら借りるだけで済ませる let borrowed = state.as_block_state()?; let state: &BlockState = &borrowed; check_local_coordinates(x, z)?; @@ -445,7 +439,7 @@ impl Chunk { let index = block_index(x, y, z); // 同じ状態を置き直すだけなら、付随データを触る理由がない - // プロパティの並び順に左右されないよう、NBT ではなく BlockState として比べる + // プロパティの並び順に左右されないよう、NBTではなくBlockStateとして比べる if let Some(current) = self.get_block(x, y, z)? { if ¤t == state { return Ok(()); @@ -477,13 +471,13 @@ impl Chunk { /// その座標を指す付随データを取り除く /// - /// `block_entities` / `block_ticks` / `fluid_ticks` の要素は - /// いずれも `x` `y` `z` を**絶対座標**で持つ + /// `block_entities` / `block_ticks` / `fluid_ticks`の要素は + /// いずれも`x` `y` `z`を**絶対座標**で持つ fn remove_block_data(&mut self, x: i32, y: i32, z: i32) -> Result<()> { let absolute_x = (self.x()? * 16) + x; let absolute_z = (self.z()? * 16) + z; - // 3 つのリストは形が同じなので、まとめて同じ処理をかける + // 3つのリストは形が同じなので、まとめて同じ処理をかける for key in BLOCK_DATA_KEYS { let filtered = match self.raw.opt_list(key)? { Some(list) if !list.is_empty() => { @@ -526,7 +520,7 @@ impl Chunk { } /// バイオームを取得する - /// 4×4×4 の単位なので、座標は自動的に丸められる + /// 4×4×4の単位なので、座標は自動的に丸められる pub fn get_biome(&self, x: i32, y: i32, z: i32) -> Result> { check_local_coordinates(x, z)?; @@ -556,7 +550,7 @@ impl Chunk { } /// バイオームを設定する - /// 4×4×4 の単位 + /// 4×4×4の単位 pub fn set_biome(&mut self, x: i32, y: i32, z: i32, biome: &str) -> Result<()> { check_local_coordinates(x, z)?; let section_y = y >> 4; @@ -581,17 +575,16 @@ impl Chunk { Ok(()) } - /// `Heightmaps` を削除し、Minecraft に再計算させる + /// `Heightmaps`を削除し、Minecraftに再計算させる /// /// 本ライブラリは高さマップを再計算しない /// ブロックを改変したら呼ぶこと - /// (`docs/adr/0004-defer-heightmap-recalc.md`) pub fn clear_heightmaps(&mut self) { self.raw.remove("Heightmaps"); self.modified = true; } - /// `isLightOn` を 0 にし、光源の再計算を促す + /// `isLightOn`を0にし、光源の再計算を促す pub fn invalidate_lighting(&mut self) { self.raw.set_byte("isLightOn", 0); self.modified = true; @@ -610,19 +603,19 @@ impl Chunk { /// セクション内のブロック添字 /// -/// `& 15` により負のY座標でも正しく求まる +/// `& 15`により負のY座標でも正しく求まる pub fn block_index(x: i32, y: i32, z: i32) -> usize { (((y & 15) * 256) + ((z & 15) * 16) + (x & 15)) as usize } /// セクション内のバイオーム添字 -/// 1 エントリが 4×4×4 ブロック +/// 1エントリが4×4×4ブロック pub fn biome_index(x: i32, y: i32, z: i32) -> usize { ((((y & 15) / 4) * 16) + (((z & 15) / 4) * 4) + ((x & 15) / 4)) as usize } fn check_local_coordinates(x: i32, z: i32) -> Result<()> { - // チャンク内相対座標は 0..15 でなければならない + // チャンク内相対座標は0..15でなければならない if !(0..=15).contains(&x) || !(0..=15).contains(&z) { return Err(Error::new( ErrorCode::InvalidArgument, diff --git a/rust/src/world/minecraft_world.rs b/rust/src/world/minecraft_world.rs index 4a991f8..e68dd87 100644 --- a/rust/src/world/minecraft_world.rs +++ b/rust/src/world/minecraft_world.rs @@ -1,17 +1,16 @@ -//! Minecraft Java版のセーブデータ 1 つ分と、その中の次元 +//! Minecraft Java版のセーブデータ1つ分と、その中の次元 //! -//! 26.x では構成が大きく変わっており、標準の3次元も -//! `dimensions/<名前空間>/<パス>/` の下に並ぶ -//! -//! 仕様: `docs/spec/40-world-layout.md` +//! 26.xでは構成が大きく変わっており、標準の3次元も`dimensions/<名前空間>/<パス>/`の下に並ぶ use std::collections::HashMap; +use std::io::Write; use std::path::{Path, PathBuf}; +use std::rc::Rc; use crate::anvil::{ChunkPos, RegionFileMode, RegionFolder}; use crate::error::{Error, ErrorCode, Result}; use crate::nbt::tag::NbtCompound; -use crate::nbt::{read_file, write_file, NamedTag, NbtReadOptions, NbtWriteOptions}; +use crate::nbt::{read_file, write_bytes, NamedTag, NbtReadOptions, NbtWriteOptions}; use super::block_state::{BlockState, IntoBlockState}; use super::chunk::{Chunk, ChunkReadOptions, ChunkWriteOptions}; @@ -22,15 +21,13 @@ pub struct WorldOpenOptions { /// 読み書きで開くか /// 既定は読み取り専用 pub writable: bool, - /// `session.lock` の確認を飛ばすか + /// `session.lock`の確認を飛ばすか /// - /// Rust 版はこの確認を行わない - /// 標準ライブラリだけでは - /// ファイルの排他ロックを扱えないため(`docs/adr/0008-session-lock.md`) - /// 他言語版との API を揃えるためにフィールドだけ用意してある + /// 標準ライブラリだけではファイルの排他ロックを扱えないため、Rust版はこの確認を行わない + /// 他言語版とAPIを揃えるためにフィールドだけ用意してある /// - /// Minecraft が起動中のワールドへ書き込むとデータが壊れるので、 - /// 起動していないことは呼び出し側で担保すること + /// Minecraftが起動中のワールドへ書き込むとデータがおかしくなるので、 + /// 起動していないことは呼び出し側で確かめること pub ignore_session_lock: bool, /// チャンク読み込みのオプション pub chunk_read: ChunkReadOptions, @@ -38,12 +35,9 @@ pub struct WorldOpenOptions { pub chunk_write: ChunkWriteOptions, } -/// `level.dat` の内容 -/// -/// 26.x では大幅に軽量化されており、ゲームルールやワールド生成設定は -/// `data/minecraft/` 配下の個別ファイルへ分離されている +/// `level.dat`の内容 /// -/// 仕様: `docs/spec/40-world-layout.md` 2章 +/// 26.xでは大幅に軽量化されており、ゲームルールやワールド生成設定は`data/minecraft/`配下の個別ファイルへ分離されている #[derive(Debug, Clone)] pub struct LevelData { root_name: String, @@ -51,13 +45,13 @@ pub struct LevelData { } impl LevelData { - /// ルートの NBT - /// `Data` を含む + /// ルートのNBT + /// `Data`を含む pub fn raw(&self) -> &NbtCompound { &self.raw } - /// `Data` の中身 + /// `Data`の中身 /// 実際の設定はここに入っている pub fn data(&self) -> Result<&NbtCompound> { self.raw.get_compound("Data") @@ -84,7 +78,7 @@ impl LevelData { self.data()?.get_int("GameType") } - /// スポーン地点の `[x, y, z]` + /// スポーン地点の`[x, y, z]` pub fn spawn_pos(&self) -> Result<&[i32]> { self.data()?.get_compound("spawn")?.get_int_array("pos") } @@ -94,7 +88,7 @@ impl LevelData { self.data()?.get_compound("spawn")?.get_string("dimension") } - /// 難易度(`normal` など) + /// 難易度(`normal`など) pub fn difficulty(&self) -> Result<&str> { self.data()? .get_compound("difficulty_settings")? @@ -108,29 +102,27 @@ impl LevelData { .get_bool("hardcore") } - /// バージョン名(`26.2` など) + /// バージョン名(`26.2`など) pub fn version_name(&self) -> Result<&str> { self.data()?.get_compound("Version")?.get_string("Name") } - /// 書き出し用の [`NamedTag`] を作る + /// 書き出し用の[`NamedTag`]を作る pub fn to_named_tag(&self) -> NamedTag { NamedTag::new(self.root_name.clone(), self.raw.clone()) } } -/// ワールド内の次元 1 つ分 -/// `region/` `entities/` `poi/` をまとめて扱う +/// ワールド内の次元1つ分 +/// `region/` `entities/` `poi/`をまとめて扱う /// -/// ブロックの取得・設定は**絶対ワールド座標**で行い、 -/// リージョン・チャンク・セクションの解決は内部で済ませる -/// -/// 仕様: `docs/spec/40-world-layout.md` 4章 +/// ブロックの取得・設定は**絶対ワールド座標**で行い、リージョン・チャンク・セクションの解決は内部で済ませる pub struct Dimension { id: String, directory: PathBuf, writable: bool, chunk_read: ChunkReadOptions, + warning: Option, chunk_write: ChunkWriteOptions, regions: Option, entities: Option, @@ -149,7 +141,12 @@ impl Dimension { /// エンドの次元ID pub const THE_END: &'static str = "minecraft:the_end"; - fn new(id: String, directory: PathBuf, options: &WorldOpenOptions) -> Dimension { + fn new( + id: String, + directory: PathBuf, + options: &WorldOpenOptions, + warning: Option, + ) -> Dimension { Dimension { id, directory, @@ -159,6 +156,7 @@ impl Dimension { on_warning: None, lenient_bit_storage: options.chunk_read.lenient_bit_storage, }, + warning, chunk_write: options.chunk_write, regions: None, entities: None, @@ -168,7 +166,7 @@ impl Dimension { } } - /// 次元ID(`minecraft:overworld` など) + /// 次元ID(`minecraft:overworld`など) pub fn id(&self) -> &str { &self.id } @@ -179,7 +177,7 @@ impl Dimension { } /// 地形のリージョンフォルダ - /// 無ければ `None` + /// 無ければ`None` pub fn region_folder(&mut self) -> Result> { self.ensure_open()?; @@ -191,7 +189,7 @@ impl Dimension { } /// エンティティのリージョンフォルダ - /// 無ければ `None` + /// 無ければ`None` pub fn entity_folder(&mut self) -> Result> { self.ensure_open()?; @@ -202,8 +200,8 @@ impl Dimension { Ok(self.entities.as_mut()) } - /// POI のリージョンフォルダ - /// 無ければ `None` + /// POIのリージョンフォルダ + /// 無ければ`None` pub fn poi_folder(&mut self) -> Result> { self.ensure_open()?; @@ -214,8 +212,10 @@ impl Dimension { Ok(self.poi.as_mut()) } - /// `data/minecraft/.dat` を読む - /// 存在しなければ `None` + /// `data/minecraft/.dat`を読む + /// 存在しなければ`None` + /// + /// 次元ごとの`world_border` / `raids` / `chunk_tickets`などが入る pub fn data_file(&self, name: &str) -> Result> { let path = self .directory @@ -239,7 +239,7 @@ impl Dimension { } /// チャンクを読む - /// 読み込んだチャンクはキャッシュされる + /// 読み込んだチャンクはキャッシュされ、次回は同じインスタンスへの参照が返る pub fn chunk(&mut self, chunk_x: i32, chunk_z: i32) -> Result> { self.ensure_open()?; let key = (chunk_x, chunk_z); @@ -255,9 +255,10 @@ impl Dimension { None => return Ok(None), }; + // 警告の通知先は、ワールドを開いたときに渡されたものを使う let options = ChunkReadOptions { on_version_mismatch: self.chunk_read.on_version_mismatch, - on_warning: None, + on_warning: forward_warning(&self.warning), lenient_bit_storage: self.chunk_read.lenient_bit_storage, }; self.chunk_cache.insert(key, Chunk::from_nbt(nbt, &options)?); @@ -274,7 +275,7 @@ impl Dimension { /// チャンクをその場で書き戻す /// - /// `flush` を待たずに反映したいときに使う + /// `flush`を待たずに反映したいときに使う pub fn save_chunk(&mut self, chunk: &Chunk) -> Result<()> { self.ensure_open()?; @@ -310,7 +311,7 @@ impl Dimension { } /// 絶対座標でブロックを取得する - /// チャンクが無ければ `None` + /// チャンクが無ければ`None` pub fn get_block(&mut self, x: i32, y: i32, z: i32) -> Result> { match self.chunk(x >> 4, z >> 4)? { Some(chunk) => chunk.get_block(x & 15, y, z & 15), @@ -320,11 +321,10 @@ impl Dimension { /// 絶対座標でブロックを設定する /// - /// `&BlockState` のほか、`"minecraft:oak_stairs[facing=north]"` の形の - /// 文字列でも指定できる + /// `&BlockState`のほか、`"minecraft:oak_stairs[facing=north]"`の形の文字列でも指定できる /// - /// 変更したチャンクには印が付き、[`Dimension::flush`] でまとめて書き戻される - /// 本ライブラリはチャンクを新規生成しないので、存在しない座標はエラーになる + /// 変更したチャンクには印が付き、[`Dimension::flush`]でまとめて書き戻される + /// 本ライブラリはチャンクを新規生成しないので、チャンクが存在しない座標はエラーになる pub fn set_block(&mut self, x: i32, y: i32, z: i32, state: &S) -> Result<()> where S: IntoBlockState + ?Sized, @@ -350,7 +350,7 @@ impl Dimension { } /// 絶対座標でバイオームを取得する - /// 4×4×4 の単位 + /// 4×4×4の単位 pub fn get_biome(&mut self, x: i32, y: i32, z: i32) -> Result> { match self.chunk(x >> 4, z >> 4)? { Some(chunk) => chunk.get_biome(x & 15, y, z & 15), @@ -359,7 +359,7 @@ impl Dimension { } /// 絶対座標でバイオームを設定する - /// 4×4×4 の単位 + /// 4×4×4の単位 pub fn set_biome(&mut self, x: i32, y: i32, z: i32, biome: &str) -> Result<()> { self.ensure_writable()?; let chunk_x = x >> 4; @@ -444,7 +444,7 @@ impl Dimension { } /// フォルダを開く - /// 存在しなければ `None` + /// 存在しなければ`None` fn open_folder(&self, name: &str) -> Result> { let path = self.directory.join(name); @@ -482,10 +482,28 @@ impl Dimension { } } -/// Minecraft Java版のセーブデータ 1 つ分 +/// 全次元で共有する警告の通知先 +type SharedWarning = Rc; + +/// 共有している警告の通知先を、チャンク読み込みのオプションへ渡せる形にする +/// +/// [`ChunkReadOptions`]の通知先は`Box`で持つので複製できない +/// 次元ごと・チャンクごとに渡すため、`Rc`で共有したものを包み直す +fn forward_warning(shared: &Option) -> Option> { + match shared { + Some(callback) => { + let callback = Rc::clone(callback); + Some(Box::new(move |message: &str| callback(message))) + } + None => None, + } +} + +/// Minecraft Java版のセーブデータ1つ分 pub struct MinecraftWorld { directory: PathBuf, options: WorldOpenOptions, + warning: Option, level: LevelData, dimensions: HashMap, closed: bool, @@ -493,7 +511,7 @@ pub struct MinecraftWorld { impl MinecraftWorld { /// ワールドを開く - pub fn open(directory: impl AsRef, options: WorldOpenOptions) -> Result { + pub fn open(directory: impl AsRef, mut options: WorldOpenOptions) -> Result { let directory = directory.as_ref().to_path_buf(); if !directory.is_dir() { @@ -514,9 +532,13 @@ impl MinecraftWorld { let named = read_file(&level_path, &NbtReadOptions::default())?; + // 警告の通知先は全次元で使うので、複製できる形にして持っておく + let warning = options.chunk_read.on_warning.take().map(Rc::from); + Ok(MinecraftWorld { directory, options, + warning, level: LevelData { root_name: named.name, raw: named.tag }, dimensions: HashMap::new(), closed: false, @@ -528,16 +550,15 @@ impl MinecraftWorld { &self.directory } - /// `level.dat` の内容 + /// `level.dat`の内容 pub fn level(&self) -> &LevelData { &self.level } - /// `data/minecraft/.dat` を読む - /// 存在しなければ `None` + /// `data/minecraft/.dat`を読む + /// 存在しなければ`None` /// - /// 26.x では `game_rules` / `weather` / `world_gen_settings` などが - /// この形で `level.dat` から分離されている + /// 26.xでは`game_rules` / `weather` / `world_gen_settings`などがこの形で`level.dat`から分離されている pub fn data_file(&self, name: &str) -> Result> { let path = self .directory @@ -561,7 +582,7 @@ impl MinecraftWorld { return Ok(found); } - // dimensions/<名前空間>/<パス>/ の 2 段を辿る + // dimensions/<名前空間>/<パス>/の2段を辿る for namespace in std::fs::read_dir(&root)?.flatten() { if !namespace.path().is_dir() { continue; @@ -569,7 +590,7 @@ impl MinecraftWorld { let namespace_name = namespace.file_name().to_string_lossy().into_owned(); - // 2 段目が次元のパス + // 2段目が次元のパス for entry in std::fs::read_dir(namespace.path())?.flatten() { // ディレクトリだけを次元として数える if entry.path().is_dir() { @@ -587,7 +608,7 @@ impl MinecraftWorld { } /// 次元を得る - /// ディレクトリが無ければ `None` + /// ディレクトリが無ければ`None` pub fn dimension(&mut self, dimension_id: &str) -> Result> { self.ensure_open()?; let normalized = normalize_dimension_id(dimension_id); @@ -604,7 +625,8 @@ impl MinecraftWorld { return Ok(None); } - let opened = Dimension::new(normalized.clone(), path, &self.options); + let opened = + Dimension::new(normalized.clone(), path, &self.options, self.warning.clone()); self.dimensions.insert(normalized.clone(), opened); } @@ -620,11 +642,11 @@ impl MinecraftWorld { return Ok(found); } - // .dat の名前部分が UUID にあたる + // .datの名前部分がUUIDにあたる for entry in std::fs::read_dir(&path)?.flatten() { let name = entry.file_name().to_string_lossy().into_owned(); - // .dat で終わるファイルだけを対象にする + // .datで終わるファイルだけを対象にする if let Some(stripped) = name.strip_suffix(".dat") { found.push(stripped.to_string()); } @@ -635,7 +657,7 @@ impl MinecraftWorld { } /// プレイヤーデータを読む - /// 存在しなければ `None` + /// 存在しなければ`None` pub fn player(&self, uuid: &str) -> Result> { let path = self .directory @@ -650,10 +672,10 @@ impl MinecraftWorld { Ok(Some(read_file(&path, &NbtReadOptions::default())?.tag)) } - /// `level.dat` を書き戻す + /// `level.dat`を書き戻す /// - /// 壊れるとワールド全体が開けなくなるため、 - /// 一時ファイルへ書いてから `level.dat_old` へ退避し、最後に置き換える + /// `level.dat`がおかしくなるとワールド全体が開けなくなるため、 + /// 一時ファイルへ書いて内容をディスクへ確実に書き出してから、既存の`level.dat`を`level.dat_old`へ退避し、最後に置き換える pub fn save_level(&mut self) -> Result<()> { self.ensure_open()?; @@ -668,8 +690,16 @@ impl MinecraftWorld { let temporary = self.directory.join("level.dat.tmp"); let backup = self.directory.join("level.dat_old"); - write_file(&temporary, &self.level.to_named_tag(), &NbtWriteOptions::default())?; + let encoded = write_bytes(&self.level.to_named_tag(), &NbtWriteOptions::default())?; + + // 一時ファイルへ書き、置き換える前に内容をディスクへ確実に書き出す + { + let mut file = std::fs::File::create(&temporary)?; + file.write_all(&encoded)?; + file.sync_all()?; + } + // 既存のlevel.datは、置き換える前にlevel.dat_oldへ退避する if path.exists() { std::fs::copy(&path, &backup)?; } @@ -703,7 +733,7 @@ impl MinecraftWorld { } } -/// 名前空間が省略されていたら `minecraft:` を補う +/// 名前空間が省略されていたら`minecraft:`を補う fn normalize_dimension_id(dimension_id: &str) -> String { if dimension_id.contains(':') { return dimension_id.to_string(); diff --git a/rust/src/world/mod.rs b/rust/src/world/mod.rs index f6f2a69..3d2fa53 100644 --- a/rust/src/world/mod.rs +++ b/rust/src/world/mod.rs @@ -1,6 +1,4 @@ //! ワールド・チャンク・ブロックの読み書き -//! -//! 仕様: `docs/spec/30-chunk-format.md` / `docs/spec/40-world-layout.md` pub mod bit_storage; pub mod block_pos; diff --git a/rust/src/world/paletted_container.rs b/rust/src/world/paletted_container.rs index 7af3214..a4b7f36 100644 --- a/rust/src/world/paletted_container.rs +++ b/rust/src/world/paletted_container.rs @@ -1,23 +1,21 @@ //! パレットとビットストレージの組 //! セクション内のブロック状態やバイオームを格納する //! -//! パレットの要素は**生の [`NbtTag`] のまま**持つ -//! こうすると、触っていないブロックについては Minecraft が書き出したときの +//! パレットの要素は**生の[`NbtTag`]のまま**持つ +//! こうすると、触っていないブロックについてはMinecraftが書き出したときの //! プロパティの並び順まで含めてそのまま書き戻せる -//! -//! 仕様: `docs/spec/31-paletted-container.md` use crate::error::{Error, ErrorCode, Result}; use crate::nbt::tag::{NbtCompound, NbtList, NbtTag}; use super::bit_storage::BitStorage; -/// `count` 個の値を表すのに必要な最小ビット数 -/// 1 なら 0 +/// `count`個の値を表すのに必要な最小ビット数 +/// 1なら0 pub fn ceil_log2(count: usize) -> usize { let mut bits = 0usize; - // 1 を超える分だけシフトして数える + // 1を超える分だけシフトして数える while (1usize << bits) < count { bits += 1; } @@ -36,13 +34,13 @@ pub struct PalettedContainer { impl PalettedContainer { /// エントリ数 - /// ブロックなら 4096、バイオームなら 64 + /// ブロックなら4096、バイオームなら64 pub fn entry_count(&self) -> usize { self.entry_count } /// ビット幅の下限 - /// ブロックなら 4、バイオームなら 1 + /// ブロックなら4、バイオームなら1 pub fn min_bits(&self) -> usize { self.min_bits } @@ -53,7 +51,7 @@ impl PalettedContainer { } /// 現在のビット幅 - /// パレットが 1 要素なら 0(記憶域を持たない) + /// パレットが1要素なら0(記憶域を持たない) pub fn bits_per_entry(&self) -> usize { match &self.storage { Some(storage) => storage.bits_per_entry(), @@ -71,7 +69,7 @@ impl PalettedContainer { } } - /// NBT から読み込む + /// NBTから読み込む pub fn from_nbt( nbt: &NbtCompound, entry_count: usize, @@ -90,7 +88,7 @@ impl PalettedContainer { let data = match nbt.opt_long_array("data")? { Some(values) => values.to_vec(), None => { - // パレットが 1 要素なら data は無くてよい + // パレットが1要素ならdataは無くてよい if palette.len() != 1 { return Err(Error::new( ErrorCode::MalformedData, @@ -111,7 +109,7 @@ impl PalettedContainer { let storage = BitStorage::from_longs(data, bits, entry_count, lenient_bit_storage)?; // 取り出した添字がパレットの範囲に収まっているか確かめる - // 黙って 0 番目で代替すると、壊れたデータをそうと分からない形で書き戻してしまう + // エラーを出さずに0番目で代替すると、おかしくなったデータをそうと分からない形で書き戻してしまう for index in 0..entry_count { let value = storage.get(index)? as usize; @@ -134,7 +132,7 @@ impl PalettedContainer { }) } - /// NBT へ変換する + /// NBTへ変換する pub fn to_nbt(&self) -> Result { let mut result = NbtCompound::new(); let mut palette_tag = NbtList::new(); @@ -144,10 +142,10 @@ impl PalettedContainer { palette_tag.push(entry.clone())?; } - // パレットが 1 要素なら data は書かない - // Minecraft と同じ振る舞い + // パレットが1要素ならdataは書かない + // Minecraftと同じ振る舞い if let Some(storage) = &self.storage { - // パレットが 1 要素なら data は書かない + // パレットが1要素ならdataは書かない if self.palette.len() > 1 { result.set("data", NbtTag::LongArray(storage.as_longs().to_vec())); } @@ -161,7 +159,7 @@ impl PalettedContainer { pub fn get(&self, index: usize) -> Result<&NbtTag> { self.check_index(index)?; - // 記憶域が無いということは、全エントリがパレットの 0 番目 + // 記憶域が無いということは、全エントリがパレットの0番目 match &self.storage { None => Ok(&self.palette[0]), Some(storage) => Ok(&self.palette[storage.get(index)? as usize]), @@ -174,7 +172,7 @@ impl PalettedContainer { self.check_index(index)?; let palette_index = self.index_of_or_add(value); - // 記憶域が無く、書き込む値も 0 番目なら何もしなくてよい + // 記憶域が無く、書き込む値も0番目なら何もしなくてよい if self.storage.is_none() && palette_index == 0 { return Ok(()); } @@ -187,8 +185,8 @@ impl PalettedContainer { } } - /// 全エントリを 1 つの値で埋める - /// パレットもその 1 要素だけにする + /// 全エントリを1つの値で埋める + /// パレットもその1要素だけにする pub fn fill(&mut self, value: NbtTag) { self.palette.clear(); self.palette.push(value); @@ -197,7 +195,7 @@ impl PalettedContainer { /// どのエントリからも参照されていないパレット要素を取り除き、添字を振り直す /// - /// 大量の `set` を行う用途で遅くならないよう、明示的に呼んだときだけ実行する + /// 大量の`set`を行う用途で遅くならないよう、明示的に呼んだときだけ実行する pub fn compact(&mut self) -> Result<()> { let storage = match &self.storage { Some(storage) => storage.clone(), @@ -240,7 +238,7 @@ impl PalettedContainer { self.palette = compacted; if single { - // 1 要素になったら記憶域を捨てる + // 1要素になったら記憶域を捨てる self.storage = None; } else { self.storage = Some(rebuilt); @@ -252,7 +250,7 @@ impl PalettedContainer { /// パレット内の位置を返す /// 無ければ末尾へ追加する fn index_of_or_add(&mut self, value: NbtTag) -> usize { - // パレットは高々 4096 要素なので線形探索で足りる + // パレットは高々4096要素なので線形探索で足りる for index in 0..self.palette.len() { if self.palette[index] == value { return index; @@ -269,7 +267,7 @@ impl PalettedContainer { match &self.storage { None => { - // これまで単一値だったので、全エントリが 0 番目のまま始まる + // これまで単一値だったので、全エントリが0番目のまま始まる self.storage = Some(BitStorage::create(required, self.entry_count)?); Ok(()) } diff --git a/rust/tests/anvil.rs b/rust/tests/anvil.rs index 6c7966e..0ed153c 100644 --- a/rust/tests/anvil.rs +++ b/rust/tests/anvil.rs @@ -1,21 +1,20 @@ -//! Anvil リージョンファイルの読み書き。 +//! Anvilリージョンファイルの読み書き //! -//! 仕様: `docs/spec/20-anvil-region.md` -//! -//! 他言語版と同じ検証項目を持つ。 -//! 共通テストベクタによる言語間比較は `spec/run-conformance.sh` が担当し、 -//! ここでは API の振る舞いを直接確かめる。 +//! 他言語版と同じ検証項目を持つ +//! 共通テストベクタによる言語間比較は`spec/run-conformance.sh`が担当し、 +//! ここではAPIの振る舞いを直接確かめる use std::path::{Path, PathBuf}; use spring_nbt_library::anvil::{ - ChunkCompression, ChunkPos, RegionFile, RegionFileMode, RegionFolder, RegionPos, SECTOR_SIZE, + ChunkCompression, ChunkPos, RawChunk, RegionFile, RegionFileMode, RegionFolder, RegionPos, + SECTOR_SIZE, }; use spring_nbt_library::error::ErrorCode; use spring_nbt_library::nbt::tag::{NbtCompound, NbtString, NbtTag}; use spring_nbt_library::TARGET_DATA_VERSION; -/// 共通テストベクタのディレクトリ。 +/// 共通テストベクタのディレクトリ fn vector_dir(name: &str) -> PathBuf { let path = Path::new(env!("CARGO_MANIFEST_DIR")) .parent() @@ -29,7 +28,8 @@ fn vector_dir(name: &str) -> PathBuf { path } -/// テストごとの一時ディレクトリ。破棄時に自動で片付ける。 +/// テストごとの一時ディレクトリ +/// 破棄時に自動で片付ける struct WorkDir { path: PathBuf, } @@ -49,7 +49,7 @@ impl WorkDir { self.path.join(name) } - /// ベクタを一時ディレクトリへ複製し、書き込みテストで原本を汚さないようにする。 + /// ベクタを一時ディレクトリへ複製し、書き込みテストで原本を汚さないようにする fn copy_vector(&self, name: &str) -> PathBuf { let source = vector_dir(name); let destination = self.path.join(name); @@ -80,12 +80,14 @@ fn sample_chunk(x: i32, z: i32) -> NbtCompound { chunk } -/// 圧縮しても縮まないバイト列を作る。サイズの制御が効くようにするため。 +/// 圧縮しても縮まないバイト列を作る +/// サイズを狙いどおりに制御できるようにするため fn incompressible(length: usize) -> Vec { let mut result = Vec::with_capacity(length); let mut state: u32 = 0x12345678; - // 線形合同法で疑似乱数を作る。テストの再現性を保つため固定の種を使う + // 線形合同法で疑似乱数を作る + // テストの再現性を保つため、固定の種を使う for _ in 0..length { state = state.wrapping_mul(1664525).wrapping_add(1013904223); result.push((state >> 24) as u8 as i8); @@ -200,7 +202,7 @@ fn reads_lz4_chunks() { let region = RegionFile::open(vector_dir("lz4").join("r.0.0.mca"), RegionFileMode::ReadOnly).unwrap(); - // 1 ブロック / 2 ブロック連結 / 無圧縮ブロック / 重なりのあるマッチ + // 1ブロック / 2ブロック連結 / 無圧縮ブロック / 重なりのあるマッチ for x in 0..4 { assert_eq!( region.read_chunk_raw(x, 0).unwrap().unwrap().compression, @@ -231,13 +233,27 @@ fn rejects_lz4_block_with_broken_magic() { assert_eq!(error.code(), ErrorCode::MalformedData); } +#[test] +fn truncated_gzip_chunk_is_malformed_data() { + let work = WorkDir::new("gziptruncated"); + let path = work.copy_vector("lz4").join("r.0.0.mca"); + let mut region = RegionFile::open(&path, RegionFileMode::ReadWrite).unwrap(); + + // GZipのヘッダだけで本体が無い、途中で切れたチャンク + let truncated = vec![0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff]; + region.write_chunk_raw(0, 0, &RawChunk::new(ChunkCompression::Gzip, truncated)).unwrap(); + + let error = region.read_chunk(0, 0).unwrap_err(); + assert_eq!(error.code(), ErrorCode::MalformedData); +} + #[test] fn writing_lz4_is_rejected() { let work = WorkDir::new("lz4write"); let path = work.copy_vector("lz4").join("r.0.0.mca"); let mut region = RegionFile::open(&path, RegionFileMode::ReadWrite).unwrap(); - // LZ4 は読み込みのみ対応なので、圧縮して書き出すことはできない + // LZ4は読み込みのみ対応なので、圧縮して書き出すことはできない let chunk = region.read_chunk(0, 0).unwrap().unwrap(); let error = region .write_chunk(0, 0, &chunk, ChunkCompression::Lz4) @@ -252,7 +268,8 @@ fn untouched_lz4_chunks_keep_their_compression() { let before = std::fs::read(&path).unwrap(); { - // 触らずに閉じるだけ。生バイトを素通しするので LZ4 のまま残る + // 触らずに閉じるだけ + // 生バイトに手を加えないので、LZ4のまま残る let mut region = RegionFile::open(&path, RegionFileMode::ReadWrite).unwrap(); region.close().unwrap(); } @@ -287,7 +304,7 @@ fn broken_headers_are_rejected() { ]; for vector in vectors { - // RegionFile は Debug を実装していないため、unwrap_err ではなく match で受ける + // RegionFileはDebugを実装していないため、unwrap_errではなくmatchで受ける match RegionFile::open(vector_dir(vector).join("r.0.0.mca"), RegionFileMode::ReadOnly) { Ok(_) => panic!("{vector}: エラーになるはずが成功した"), Err(error) => assert_eq!(error.code(), ErrorCode::MalformedData, "{vector}"), @@ -300,7 +317,7 @@ fn chunk_outside_the_region_is_rejected() { let region = RegionFile::open(vector_dir("empty").join("r.0.0.mca"), RegionFileMode::ReadOnly).unwrap(); - // r.0.0 が担当するのは 0..31 の範囲だけ + // r.0.0が担当するのは0..31の範囲だけ assert_eq!( region.has_chunk(32, 0).unwrap_err().code(), ErrorCode::InvalidArgument @@ -324,7 +341,7 @@ fn read_only_region_rejects_writes() { #[test] fn opening_and_flushing_without_changes_keeps_bytes_identical() { - // 触っていないチャンクの配置を保つことが、既存ワールドを壊さない前提になる + // 触っていないチャンクの配置を保つことが、既存ワールドをおかしくしないための前提になる let work = WorkDir::new("noop"); let path = work.copy_vector("fragmented").join("r.0.0.mca"); let original = std::fs::read(&path).unwrap(); @@ -392,7 +409,7 @@ fn growing_chunk_is_relocated_without_breaking_others() { let work = WorkDir::new("grow"); let path = work.copy_vector("fragmented").join("r.0.0.mca"); - // 5 セクタぶんになる大きなチャンクを作る + // 5セクタぶんになる大きなチャンクを作る let mut big = sample_chunk(0, 0); big.set("filler", NbtTag::ByteArray(incompressible(5 * SECTOR_SIZE))); @@ -405,7 +422,7 @@ fn growing_chunk_is_relocated_without_breaking_others() { let reopened = RegionFile::open(&path, RegionFileMode::ReadOnly).unwrap(); - // 動かした結果、他の 2 チャンクが壊れていないこと + // 動かした結果、他の2チャンクがおかしくなっていないこと assert_eq!(reopened.chunk_positions().unwrap().len(), 3); assert_eq!( reopened.read_chunk(5, 3).unwrap().unwrap().get_int("xPos").unwrap(), @@ -508,7 +525,7 @@ fn huge_chunk_goes_to_external_file_and_comes_back() { let work = WorkDir::new("mcc"); let path = work.join("r.0.0.mca"); - // 1MiB を超えるよう、圧縮の効かないデータを詰める + // 1MiBを超えるよう、圧縮しても縮まないデータを詰める let mut huge = sample_chunk(1, 2); huge.set("filler", NbtTag::ByteArray(incompressible(1200 * 1024))); @@ -576,6 +593,34 @@ fn timestamp_can_be_set_explicitly() { // RegionFolder // --------------------------------------------------------------------------- +#[test] +fn read_write_folder_creates_missing_directory_only_when_writing() { + let work = WorkDir::new("missingfolder"); + let missing = work.join("missing").join("region"); + + // 読むだけなら、無いディレクトリは作らない + { + let mut folder = RegionFolder::open(&missing, RegionFileMode::ReadWrite).unwrap(); + assert!(folder.read_chunk(0, 0).unwrap().is_none()); + folder.close().unwrap(); + } + + assert!(!missing.exists()); + + // 書き出すときに作る + { + let mut folder = RegionFolder::open(&missing, RegionFileMode::ReadWrite).unwrap(); + folder + .write_chunk(0, 0, &sample_chunk(0, 0), ChunkCompression::Zlib) + .unwrap(); + folder.close().unwrap(); + } + + let mut reopened = RegionFolder::open(&missing, RegionFileMode::ReadOnly).unwrap(); + let chunk = reopened.read_chunk(0, 0).unwrap().unwrap(); + assert_eq!(chunk.get_int("xPos").unwrap(), 0); +} + #[test] fn folder_resolves_chunks_across_regions() { let work = WorkDir::new("folder"); @@ -595,7 +640,7 @@ fn folder_resolves_chunks_across_regions() { folder.close().unwrap(); } - // 3 つの異なるリージョンへ振り分けられる + // 3つの異なるリージョンへ振り分けられる for name in ["r.0.0.mca", "r.-1.-1.mca", "r.1.1.mca"] { assert!(work.join(name).exists(), "{name} が作られていない"); } @@ -620,7 +665,8 @@ fn folder_resolves_chunks_across_regions() { fn キャッシュ上限を超えると古いリージョンから閉じる() { let work = WorkDir::new("lru"); - // 上限 2 で 4 リージョンへ書く。古いものは閉じられるが内容は失われない + // 上限2で4リージョンへ書く + // 古いものは閉じられるが、内容は失われない { let mut folder = RegionFolder::open_with_limit(&work.path, RegionFileMode::ReadWrite, 2).unwrap(); diff --git a/rust/tests/block_pos.rs b/rust/tests/block_pos.rs index 02ba5c8..c52a900 100644 --- a/rust/tests/block_pos.rs +++ b/rust/tests/block_pos.rs @@ -1,6 +1,4 @@ -//! ブロック座標と範囲の統合テスト。 -//! -//! 仕様: `docs/spec/30-chunk-format.md` 5章 +//! ブロック座標と範囲の統合テスト use spring_nbt_library::anvil::ChunkPos; use spring_nbt_library::world::{BlockPos, Cuboid}; @@ -52,7 +50,7 @@ fn walks_every_position_inside() { assert_eq!(positions.len() as i64, box_.volume()); assert_eq!(positions.len(), 2 * 3 * 4); - // 並びは Y、Z、X の順で、X がいちばん内側で動く + // 並びはY、Z、Xの順で、Xがいちばん内側で動く assert_eq!(positions[0], BlockPos::new(0, 0, 0)); assert_eq!(positions[1], BlockPos::new(1, 0, 0)); assert_eq!(positions[2], BlockPos::new(0, 0, 1)); diff --git a/rust/tests/nbt.rs b/rust/tests/nbt.rs index 0c5e951..f95a982 100644 --- a/rust/tests/nbt.rs +++ b/rust/tests/nbt.rs @@ -1,8 +1,8 @@ -//! NBT レイヤの統合テスト。 +//! NBTレイヤの統合テスト //! -//! 他言語版と同じ検証項目を持つ。 -//! 共通テストベクタによる言語間比較は `spec/run-conformance.sh` が担当し、 -//! ここでは API の振る舞いを直接確かめる。 +//! 他言語版と同じ検証項目を持つ +//! 共通テストベクタによる言語間比較は`spec/run-conformance.sh`が担当し、 +//! ここではAPIの振る舞いを直接確かめる use spring_nbt_library::error::ErrorCode; use spring_nbt_library::nbt::snbt; @@ -21,7 +21,7 @@ fn uncompressed_write() -> NbtWriteOptions { NbtWriteOptions::uncompressed() } -/// 仕様書どおりに組んだ最小の NBT。 +/// 仕様書どおりに組んだ最小のNBT fn hello_world_bytes() -> Vec { let mut bytes: Vec = Vec::new(); @@ -43,7 +43,8 @@ fn hello_world_bytes() -> Vec { bytes } -/// 全13タグ型を含む Compound を作る。 +/// TAG_Endを除く12種のタグ型をすべて含むCompoundを作る +/// TAG_EndはCompoundの終端として書き出されるので、往復させれば13種すべてを読み書きする fn build_all_tags() -> NbtCompound { let mut root = NbtCompound::new(); root.set("byte", NbtTag::Byte(-128)); @@ -70,11 +71,11 @@ fn build_all_tags() -> NbtCompound { root } -/// 指定した深さまで Compound を入れ子にしたバイト列を作る。 +/// 指定した深さまでCompoundを入れ子にしたバイト列を作る fn build_nested_compound(depth: usize) -> Vec { let mut bytes: Vec = vec![0x0A, 0x00, 0x00]; - // ルート + (depth - 1) 段の入れ子 + // ルート + (depth - 1)段の入れ子 for _ in 0..depth - 1 { bytes.extend_from_slice(&[0x0A, 0x00, 0x01, b'c']); } @@ -96,16 +97,16 @@ fn mutf8_roundtrips_and_rejects_invalid_input() { // ASCII assert_eq!(mutf8::encode("Bananrama"), b"Bananrama"); - // U+0000 は C0 80 の 2 バイトになる + // U+0000はC0 80の2バイトになる assert_eq!(mutf8::encode("a\u{0000}b"), vec![0x61, 0xC0, 0x80, 0x62]); - // 補助文字は CESU-8 になる + // 補助文字はCESU-8になる assert_eq!( mutf8::encode("\u{1F600}"), vec![0xED, 0xA0, 0xBD, 0xED, 0xB8, 0x80] ); - // 素の 0x00 / 冗長符号化 / 4バイト形式 / 途中で切れた入力 のすべてを拒否する + // 素の0x00、冗長符号化、4バイト形式、途中で切れた入力のすべてを拒否する let invalid: Vec> = vec![ vec![0x00], vec![0xC1, 0x81], @@ -121,13 +122,13 @@ fn mutf8_roundtrips_and_rejects_invalid_input() { #[test] fn lone_surrogate_is_kept_in_a_separate_representation() { - // Rust の String は UTF-8 に限られるため、専用の表現へ退避する + // RustのStringはUTF-8に限られるため、専用の表現へ退避する let value = NbtString::from_utf16(vec![0xD83D]); assert!(matches!(value, NbtString::Surrogates(_))); assert_eq!(value.as_str(), None); assert_eq!(value.to_mutf8(), vec![0xED, 0xA0, 0xBD]); - // 通常の文字列は Text になる + // 通常の文字列はTextになる let text = NbtString::from_utf16("あ".encode_utf16().collect()); assert_eq!(text.as_str(), Some("あ")); } @@ -145,6 +146,17 @@ fn reads_hand_built_hello_world() { assert_eq!(named.tag.get_string("name").unwrap(), "Bananrama"); } +#[test] +fn nan_bit_patterns_are_written_back_unchanged() { + // ペイロード付き・負・シグナリングのNaNを、Float / Doubleの両方で持つCompound + let original: Vec = vec![ + 0x0a, 0x00, 0x00, 0x05, 0x00, 0x01, 0x61, 0x7f, 0xc0, 0x00, 0x01, 0x05, 0x00, 0x01, 0x62, 0xff, 0xc0, 0x00, 0x00, 0x05, 0x00, 0x01, 0x63, 0x7f, 0x80, 0x00, 0x01, 0x06, 0x00, 0x01, 0x64, 0x7f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x06, 0x00, 0x01, 0x65, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x66, 0x7f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + ]; + let named = read_bytes(&original, &uncompressed_read()).unwrap(); + + assert_eq!(write_bytes(&named, &uncompressed_write()).unwrap(), original); +} + #[test] fn writes_back_the_same_bytes() { let original = hello_world_bytes(); @@ -175,7 +187,7 @@ fn float_specials_keep_their_bit_pattern() { let encoded = write_bytes(&NamedTag::new("", root), &uncompressed_write()).unwrap(); let decoded = read_bytes(&encoded, &uncompressed_read()).unwrap().tag; - // -0.0 と +0.0 は == では区別できないので、ビットパターンで比較する + // -0.0と+0.0は == では区別できないので、ビットパターンで比較する assert_eq!( decoded.get_double("negative_zero").unwrap().to_bits(), (-0.0f64).to_bits() @@ -239,6 +251,18 @@ fn list_rejects_mixed_types() { assert_eq!(error.code(), ErrorCode::UnexpectedTagType); } +#[test] +fn list_rejects_out_of_range_position_without_fixing_element_type() { + let mut list = NbtList::new(); + + let error = list.insert(1, NbtTag::Int(1)).unwrap_err(); + assert_eq!(error.code(), ErrorCode::InvalidArgument); + assert!(list.get(0).is_none()); + + // 失敗した操作で要素型が確定していない + assert_eq!(list.element_type(), TagType::End); +} + #[test] fn list_keeps_element_type_after_clear() { let mut list = NbtList::new(); @@ -255,7 +279,7 @@ fn typed_getter_distinguishes_missing_key_from_wrong_type() { let mut root = NbtCompound::new(); root.set("value", NbtTag::String(NbtString::new("text"))); - // キーが無い場合は None + // キーが無い場合はNone assert_eq!(root.opt_int("missing").unwrap(), None); // 型が違う場合はキーの有無に関わらずエラー @@ -269,11 +293,29 @@ fn typed_getter_distinguishes_missing_key_from_wrong_type() { ); } +#[test] +fn broken_compressed_data_is_malformed_data() { + let named = read_bytes(&hello_world_bytes(), &uncompressed_read()).unwrap(); + let options = NbtWriteOptions { format: NbtFormat::Java, compression: Compression::Gzip }; + let gzip = write_bytes(&named, &options).unwrap(); + let truncated = gzip[..gzip.len() / 2].to_vec(); + let broken_gzip: Vec = vec![ + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, + ]; + let broken_zlib: Vec = vec![0x78, 0x9c, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]; + + // 途中で切れたGZip、中身の壊れたGZip、中身の壊れたZlibは、どれも仕様に反するデータとして扱う + for data in [truncated, broken_gzip, broken_zlib] { + let error = read_bytes(&data, &NbtReadOptions::default()).unwrap_err(); + assert_eq!(error.code(), ErrorCode::MalformedData); + } +} + #[test] fn compression_is_detected_automatically() { let named = read_bytes(&hello_world_bytes(), &uncompressed_read()).unwrap(); - // 3 種の方式それぞれで、書き出した結果を方式指定なしで読み戻せること + // 3種の方式それぞれで、書き出した結果を方式指定なしで読み戻せること for method in [Compression::Gzip, Compression::Zlib, Compression::None] { let options = NbtWriteOptions { format: NbtFormat::Java, compression: method }; let encoded = write_bytes(&named, &options).unwrap(); @@ -294,7 +336,7 @@ fn network_format_has_no_root_name() { NbtWriteOptions { format: NbtFormat::Network, compression: Compression::None }; let encoded = write_bytes(&NamedTag::new("ignored", root), &write_options).unwrap(); - // タグID + ペイロード のみで、名前長の 2 バイトが無い + // タグID + ペイロードのみで、名前長の2バイトが無い assert_eq!(encoded[0], 0x0A); assert_eq!(encoded[1], 0x03); @@ -328,7 +370,7 @@ fn malformed_inputs_are_rejected() { ErrorCode::MalformedData ); - // 長さ 0x7FFFFFFF を宣言するだけの入力(確保前に弾く) + // 長さ0x7FFFFFFFを宣言するだけの入力(確保前に弾く) let huge = vec![0x0A, 0x00, 0x00, 0x07, 0x00, 0x01, b'a', 0x7F, 0xFF, 0xFF, 0xFF]; assert_eq!( read_bytes(&huge, &uncompressed_read()).unwrap_err().code(), @@ -354,10 +396,10 @@ fn malformed_inputs_are_rejected() { #[test] fn excessive_nesting_is_rejected() { - // 読み込みは再帰で行うため、深いネストはスタックを使う。 - // debug ビルドはフレームが太く、テストスレッド既定の 2 MiB では 512 段に届かない。 - // 実運用の release ビルドでは 2 MiB でも 2000 段を超えて扱えるが、 - // ここでは測定対象を「深さ上限の判定」に絞るため十分なスタックを与える。 + // 読み込みは再帰で行うため、深いネストはスタックを使う + // debugビルドはフレームが大きく、テストスレッド既定の2 MiBでは512段に届かない + // 実運用のreleaseビルドでは2 MiBでも2000段を超えて扱えるが、 + // ここでは測定対象を「深さ上限の判定」に絞るため、十分なスタックを与える let handle = std::thread::Builder::new() .stack_size(32 * 1024 * 1024) .spawn(|| { @@ -419,7 +461,8 @@ fn suffix_decides_type() { #[test] fn hex_suffix_rule_is_fixed() { - // 仕様 11 の 2.1: 16進では b/d/f を数字として読む。幅接尾辞は s/l のみ + // 16進ではb/d/fを数字として読む + // 幅接尾辞はs/lのみ assert_eq!(snbt::parse("0xFF").unwrap(), NbtTag::Int(255)); assert_eq!(snbt::parse("0xFFb").unwrap(), NbtTag::Int(4091)); assert_eq!(snbt::parse("0xFFl").unwrap(), NbtTag::Long(255)); @@ -428,7 +471,8 @@ fn hex_suffix_rule_is_fixed() { #[test] fn zero_byte_literal_is_not_binary() { - // 0b は「10進の 0 に Byte 接尾辞」。真偽値の false として広く使われる形 + // 0bは「10進の0にByte接尾辞」 + // 真偽値のfalseとして広く使われる形 assert_eq!(snbt::parse("0b").unwrap(), NbtTag::Byte(0)); assert_eq!(snbt::parse("0b1").unwrap(), NbtTag::Int(1)); assert_eq!(snbt::parse("0b1001b").unwrap(), NbtTag::Byte(9)); @@ -468,7 +512,7 @@ fn typed_arrays() { assert_eq!(snbt::parse("[I; 1, 2]").unwrap(), NbtTag::IntArray(vec![1, 2])); assert_eq!(snbt::parse("[L; 1L, 2L]").unwrap(), NbtTag::LongArray(vec![1, 2])); - // 接尾辞なしでも範囲内なら受理する(Minecraft 自身がそう書き出すため) + // 接尾辞なしでも範囲内なら受理する(Minecraft自身がそう書き出すため) assert_eq!(snbt::parse("[B; 1, 2]").unwrap(), NbtTag::ByteArray(vec![1, 2])); assert_eq!( snbt::parse("[B; 200]").unwrap_err().code(), @@ -485,7 +529,7 @@ fn trailing_commas_and_heterogeneous_lists() { other => panic!("リストではない: {other:?}"), } - // 異種リストはバイナリ NBT へ写せないため受理しない (adr/0006) + // 異種リストはバイナリNBTへ写せないため受理しない assert_eq!( snbt::parse("[1, \"a\"]").unwrap_err().code(), ErrorCode::MalformedData @@ -520,6 +564,27 @@ fn escape_sequences() { ); } +#[test] +fn surrogate_escapes_follow_the_same_rules_everywhere() { + let lone = NbtTag::String(NbtString::from_utf16(vec![0xD800])); + + // \Uでサロゲートの範囲を書くと、\uと同じく孤立サロゲートになる + assert_eq!(snbt::parse("\"\\U0000D800\"").unwrap(), lone); + assert_eq!(snbt::parse("\"\\uD800\"").unwrap(), lone); + + // 対になったサロゲートは補助文字1文字と同じ + assert_eq!( + snbt::parse("\"\\uD83D\\uDE00\"").unwrap(), + NbtTag::String(NbtString::new("\u{1F600}")) + ); + + // コードポイントの範囲外と、キーの孤立サロゲートは仕様に反する + // Rustのキーは`String`なので、APIから孤立サロゲートのキーを設定することはそもそもできない + for source in ["\"\\U00110000\"", "{\"\\uD800\":1}", "{\"\\U0000D800\":1}"] { + assert_eq!(snbt::parse(source).unwrap_err().code(), ErrorCode::MalformedData, "{source}"); + } +} + #[test] fn functions() { assert_eq!(snbt::parse("bool(5)").unwrap(), NbtTag::Byte(1)); @@ -537,7 +602,7 @@ fn functions() { #[test] fn snbt_to_nbt_to_snbt_to_nbt_is_stable() { - // 仕様 11 の 5章: 保証するのは「SNBT -> NBT -> SNBT -> NBT」で NBT が一致すること + // 保証するのは「SNBT -> NBT -> SNBT -> NBT」でNBTが一致すること let source = "{ name : 'Bananrama' , list : [ 1L , 2L ] , nested : { flag : true } , \ bytes : [B; 1b, -2b] , ratio : 0.5f }"; @@ -641,6 +706,13 @@ fn special_values() { assert_eq!(snbt::write(&NbtTag::Float(f32::NAN)), "NaNf"); } +#[test] +fn float_out_of_range_becomes_signed_infinity() { + // binary32の範囲を超える値は、符号付きの無限大になる + assert_eq!(snbt::parse("1e39f").unwrap(), NbtTag::Float(f32::INFINITY)); + assert_eq!(snbt::parse("-1e39f").unwrap(), NbtTag::Float(f32::NEG_INFINITY)); +} + #[test] fn every_formatted_value_parses_back_to_the_same_bits() { let doubles: Vec = vec![ @@ -694,9 +766,8 @@ fn every_formatted_value_parses_back_to_the_same_bits() { // --------------------------------------------------------------------------- // タグの等値比較と深い複製 // -// 仕様: docs/spec/10-nbt-binary.md 7.3 // 規則は全言語で同じでなければならない -// 各言語の同名テストと突き合わせて読むこと +// 各言語の同名テストと見比べながら読むこと // --------------------------------------------------------------------------- #[test] @@ -714,13 +785,13 @@ fn same_type_same_value_is_equal() { #[test] fn different_tag_type_is_not_equal() { - // 値が同じでもタグの型が違えば別物 + // 値が同じでもタグの型が違えば等しくない assert_ne!(NbtTag::Int(1), NbtTag::Short(1)); } #[test] fn floats_compare_by_bit_pattern() { - // NaN 同士は等しく、+0.0 と -0.0 は等しくない + // NaN同士は等しく、+0.0と-0.0は等しくない assert_eq!(NbtTag::Float(f32::NAN), NbtTag::Float(f32::NAN)); assert_eq!(NbtTag::Double(f64::NAN), NbtTag::Double(f64::NAN)); assert_ne!(NbtTag::Float(0.0), NbtTag::Float(-0.0)); @@ -743,7 +814,7 @@ fn list_compares_element_type_and_order() { reversed.push(NbtTag::Int(1)).unwrap(); assert_ne!(left, reversed); - // 空でも要素型が違えば別物 + // 空でも要素型が違えば等しくない assert_ne!( NbtList::with_element_type(TagType::Int), NbtList::with_element_type(TagType::Byte) @@ -761,7 +832,7 @@ fn compound_compares_insertion_order() { same.set("b", NbtTag::Int(2)); assert_eq!(left, same); - // 中身は同じでも挿入順が違えば別物 + // 中身は同じでも挿入順が違えば等しくない let mut reordered = NbtCompound::new(); reordered.set("b", NbtTag::Int(2)); reordered.set("a", NbtTag::Int(1)); @@ -787,16 +858,15 @@ fn clone_is_deep() { } // --------------------------------------------------------------------------- -// 連なった NBT の読み込み +// 連なったNBTの読み込み // -// 仕様: docs/spec/10-nbt-binary.md 3.1章 // --------------------------------------------------------------------------- #[test] fn reads_concatenated_nbt_one_by_one() { let mut joined: Vec = Vec::new(); - // 同じ NBT を 3 つ続けて並べる + // 同じNBTを3つ続けて並べる for _ in 0..3 { joined.extend_from_slice(&hello_world_bytes()); } @@ -850,7 +920,6 @@ fn rejects_compression_when_reading_at_offset() { // --------------------------------------------------------------------------- // 型付き設定子 // -// 仕様: docs/spec/10-nbt-binary.md 7.1章 // --------------------------------------------------------------------------- #[test] @@ -882,7 +951,7 @@ fn typed_setters_mirror_the_getters() { assert_eq!(root.get_int_array("ia").unwrap(), &[1, -1]); assert_eq!(root.get_long_array("la").unwrap(), &[1, -1]); - // 真偽値は TAG_Byte の 0 / 1 として入る + // 真偽値はTAG_Byteの0 / 1として入る assert_eq!(root.get("t").unwrap().tag_type(), TagType::Byte); } diff --git a/rust/tests/world.rs b/rust/tests/world.rs index 08903f4..514db28 100644 --- a/rust/tests/world.rs +++ b/rust/tests/world.rs @@ -1,10 +1,7 @@ -//! World / Block レイヤ。 +//! World / Blockレイヤ //! -//! 仕様: `docs/spec/30-chunk-format.md` / `31-paletted-container.md` / `40-world-layout.md` -//! -//! 他言語版と同じ検証項目を持つ。 -//! 共通テストベクタによる言語間比較は `spec/run-conformance.sh` が担当し、 -//! ここでは API の振る舞いを直接確かめる。 +//! 他言語版と同じ検証項目を持つ +//! 共通テストベクタによる言語間比較は`spec/run-conformance.sh`が担当し、ここではAPIの振る舞いを直接確かめる use std::cell::RefCell; use std::path::{Path, PathBuf}; @@ -13,15 +10,16 @@ use std::rc::Rc; use spring_nbt_library::error::{Error, ErrorCode}; use spring_nbt_library::nbt::tag::{NbtCompound, NbtString, NbtTag}; use spring_nbt_library::nbt::{ - read_file, write_bytes, Compression, NamedTag, NbtReadOptions, NbtWriteOptions, + read_file, write_bytes, write_file, Compression, NamedTag, NbtReadOptions, NbtWriteOptions, }; +use spring_nbt_library::anvil::{ChunkCompression, RegionFile, RegionFileMode}; use spring_nbt_library::world::{ ceil_log2, BitStorage, BlockState, Chunk, ChunkReadOptions, ChunkWriteOptions, MinecraftWorld, PalettedContainer, VersionMismatchAction, WorldOpenOptions, }; use spring_nbt_library::TARGET_DATA_VERSION; -/// 共通テストベクタ(world/*.nbt)のパス。 +/// 共通テストベクタ(world/*.nbt)のパス fn vector_path(name: &str) -> PathBuf { let path = Path::new(env!("CARGO_MANIFEST_DIR")) .parent() @@ -35,20 +33,20 @@ fn vector_path(name: &str) -> PathBuf { path } -/// テストベクタをチャンクとして読む。 +/// テストベクタをチャンクとして読む fn load_chunk(name: &str) -> Chunk { let named = read_file(vector_path(name), &NbtReadOptions::default()).expect("読めない"); Chunk::from_nbt(named.tag, &ChunkReadOptions::default()).expect("チャンクとして解釈できない") } -/// ブロックのパレット要素を作る。 +/// ブロックのパレット要素を作る fn block_entry(name: &str) -> NbtTag { let mut entry = NbtCompound::new(); entry.set("Name", NbtTag::String(NbtString::new(name))); NbtTag::Compound(entry) } -/// エラーが期待したコードであることを確かめる。 +/// エラーが期待したコードであることを確かめる fn assert_code(expected: ErrorCode, error: Error) { assert_eq!(expected, error.code(), "エラーコードが違う: {error}"); } @@ -132,7 +130,7 @@ fn 壊れた文字列はinvalid_argument() { "minecraft:oak_stairs[]extra", ]; - // 壊し方ごとに同じエラーコードになることを確かめる + // 崩し方ごとに同じエラーコードになることを確かめる for text in broken { match BlockState::parse(text) { Ok(_) => panic!("解釈が通ってしまった: {text}"), @@ -171,7 +169,8 @@ fn 書いた値をそのまま読み出せる() { fn long境界を跨がずに詰める() { let mut storage = BitStorage::create(5, 4096).unwrap(); - // bits=5 なら 1 つの long に 12 個。12 個目は次の long の最下位から始まる + // bits=5なら1つのlongに12個 + // 添字12は次のlongの最下位から始まる storage.set(11, 31).unwrap(); storage.set(12, 1).unwrap(); let longs = storage.as_longs(); @@ -207,7 +206,7 @@ fn ビット幅に対して長さが合わない配列はmalformed_data() { #[test] fn 寛容モードなら長さからビット幅を逆算する() { - // 4096 エントリを 342 long で表せるのは bits=5 のときだけ + // 4096エントリを342 longで表せるのはbits=5のときだけ let storage = BitStorage::from_longs(vec![0; 342], 4, 4096, true).unwrap(); assert_eq!(5, storage.bits_per_entry()); } @@ -259,7 +258,7 @@ fn 単一値のコンテナはdataを持たない() { fn 値を足すとパレットとビット幅が広がる() { let mut container = PalettedContainer::filled(block_entry("minecraft:air"), 4096, 4); - // パレットを 17 要素まで増やして bits=4 から 5 への拡張を起こす + // 17種のブロックを足してパレットを18要素にし、bits=4から5への拡張を起こす for index in 0..17 { container .set(index, block_entry(&format!("minecraft:block_{index}"))) @@ -291,7 +290,7 @@ fn compactで未使用のパレット要素が消える() { container.compact().unwrap(); - // 残るのは実際に使われている air と dirt の 2 つ + // 残るのは実際に使われているairとdirtの2つ assert_eq!(2, container.palette().len()); match container.get(0).unwrap() { @@ -354,7 +353,8 @@ fn ビット幅5のチャンクを端から端まで読める() { let chunk = load_chunk("palette_17"); let head = ["minecraft:air", "minecraft:stone"]; - // ベクタの添字は (位置 * 11) % 17。パレット先頭 2 つだけ名前が違う + // ベクタの添字は (位置 * 11) % 17 + // パレットの先頭2つだけ名前が違う for position in 0..4096i32 { let palette_index = ((position * 11) % 17) as usize; let block = chunk @@ -412,7 +412,7 @@ fn バイオームは4ブロック単位で効く() { let mut chunk = load_chunk("palette_1"); chunk.set_biome(0, -64, 0, "minecraft:desert").unwrap(); - // 同じ 4×4×4 の枠内はまとめて変わる + // 同じ4×4×4の枠内はまとめて変わる assert_eq!( Some("minecraft:desert".to_string()), chunk.get_biome(3, -61, 3).unwrap() @@ -465,14 +465,14 @@ fn ブロックを置き換えると同じ座標の付随データが消える() assert_eq!(2, chunk.raw().get_list("block_ticks").unwrap().len()); assert_eq!(1, chunk.raw().get_list("fluid_ticks").unwrap().len()); - // (0,-64,0) には chest と block_tick、(1,-64,1) には furnace と fluid_tick がある + // (0,-64,0)にはchestとblock_tick、(1,-64,1)にはfurnaceとfluid_tickがある let stone = BlockState::parse("minecraft:stone").unwrap(); chunk.set_block(0, -64, 0, &stone).unwrap(); chunk.set_block(1, -64, 1, &stone).unwrap(); let entities = chunk.raw().get_list("block_entities").unwrap(); - // 触っていない (15,-50,15) の barrel だけが残る + // 触っていない(15,-50,15)のbarrelだけが残る assert_eq!(1, entities.len()); match entities.get(0).unwrap() { @@ -507,8 +507,7 @@ fn 同じブロックを置き直しても付随データは消えない() { #[test] fn 別のチャンクの同じ相対座標は消さない() { - // 付随データは絶対座標で持つので、チャンク座標を取り違えると - // 無関係な要素を消してしまう + // 付随データは絶対座標で持つので、チャンク座標を間違えると無関係な要素を消してしまう let named = read_file(vector_path("block_entities"), &NbtReadOptions::default()).unwrap(); let mut root = named.tag; root.set("xPos", NbtTag::Int(1)); @@ -518,7 +517,8 @@ fn 別のチャンクの同じ相対座標は消さない() { let stone = BlockState::parse("minecraft:stone").unwrap(); chunk.set_block(0, -64, 0, &stone).unwrap(); - // このチャンクの (0,-64,0) は絶対座標 (16,-64,16)。どれとも一致しない + // このチャンクの(0,-64,0)は絶対座標(16,-64,16) + // どれとも一致しない assert_eq!(3, chunk.raw().get_list("block_entities").unwrap().len()); } @@ -572,10 +572,10 @@ fn チャンク内の相対座標が範囲外ならinvalid_argument() { } // --------------------------------------------------------------------------- -// DataVersion の扱い +// DataVersionの扱い // --------------------------------------------------------------------------- -/// DataVersion だけを差し替えたチャンクを作る。 +/// DataVersionだけを差し替えたチャンクを作る fn foreign_chunk() -> NbtCompound { let named = read_file(vector_path("palette_1"), &NbtReadOptions::default()).unwrap(); let mut root = named.tag; @@ -654,12 +654,12 @@ fn 許可すれば古いチャンクも元のバージョンのまま書き戻 allow_foreign_data_version: true, }; - // DataVersion は読んだ値のまま残す + // DataVersionは読んだ値のまま残す let written = chunk.to_nbt(&write).unwrap(); assert_eq!(3953, written.get_int("DataVersion").unwrap()); } -/// 対象より新しい DataVersion を持つチャンクを作る。 +/// 対象より新しいDataVersionを持つチャンクを作る fn newer_chunk() -> NbtCompound { let named = read_file(vector_path("palette_1"), &NbtReadOptions::default()).unwrap(); let mut root = named.tag; @@ -679,7 +679,7 @@ fn 新しいバージョンのチャンクは警告を出さない() { lenient_bit_storage: false, }; - // 形式が同じであれば、新しいバージョンでも黙って読める + // 形式が同じであれば、新しいバージョンでも警告を出さずに読める let chunk = Chunk::from_nbt(newer_chunk(), &read).unwrap(); assert_eq!(chunk.data_version().unwrap(), 5015); assert!(warnings.borrow().is_empty()); @@ -699,7 +699,7 @@ fn 新しいバージョンのチャンクはエラー設定でも通る() { fn 新しいバージョンのチャンクはそのまま書き戻せる() { let chunk = Chunk::from_nbt(newer_chunk(), &ChunkReadOptions::default()).unwrap(); - // 許可を出さなくても書き戻せて、DataVersion も変わらない + // 許可を出さなくても書き戻せて、DataVersionも変わらない let written = chunk.to_nbt(&ChunkWriteOptions::default()).unwrap(); assert_eq!(5015, written.get_int("DataVersion").unwrap()); } @@ -741,6 +741,52 @@ fn level_datが無いディレクトリはio() { } } +#[test] +fn 次元経由で読んだチャンクの警告も通知先へ届く() { + let work = std::env::temp_dir().join(format!("springnbt-warn-{}", std::process::id())); + let region = work.join("dimensions").join("minecraft").join("overworld").join("region"); + std::fs::create_dir_all(®ion).unwrap(); + + // DataVersionが扱える形式より古いチャンクを1つだけ持つワールドを作る + let mut level = NbtCompound::new(); + level.set("Data", NbtTag::Compound(NbtCompound::new())); + write_file(work.join("level.dat"), &NamedTag::new("", level), &NbtWriteOptions::default()) + .unwrap(); + + { + let mut file = RegionFile::open(region.join("r.0.0.mca"), RegionFileMode::ReadWrite).unwrap(); + file.write_chunk(0, 0, &foreign_chunk(), ChunkCompression::Zlib).unwrap(); + file.close().unwrap(); + } + + let warnings: Rc>> = Rc::new(RefCell::new(Vec::new())); + let sink = Rc::clone(&warnings); + let options = WorldOpenOptions { + chunk_read: ChunkReadOptions { + on_warning: Some(Box::new(move |message: &str| { + sink.borrow_mut().push(message.to_string()); + })), + ..ChunkReadOptions::default() + }, + ..WorldOpenOptions::default() + }; + + let found = { + let mut world = MinecraftWorld::open(&work, options).unwrap(); + let overworld = world.dimension("minecraft:overworld").unwrap().unwrap(); + let found = overworld.chunk(0, 0).unwrap().is_some(); + world.close().unwrap(); + found + }; + + // 判定の前に一時ディレクトリを片付ける + std::fs::remove_dir_all(&work).unwrap(); + + // ワールドを開くときに渡した通知先が、次元経由の読み込みでも呼ばれる + assert!(found); + assert_eq!(1, warnings.borrow().len()); +} + // --------------------------------------------------------------------------- // 文字列でのブロック指定と、変更の印 @@ -765,7 +811,7 @@ fn marks_the_chunk_as_modified() { chunk.set_block(3, -60, 7, "minecraft:stone").unwrap(); assert!(chunk.is_modified()); - // 保存済みとして印を下ろせる + // 保存済みとして印を外せる chunk.set_is_modified(false); assert!(!chunk.is_modified()); diff --git a/spec/run-conformance.sh b/spec/run-conformance.sh index 7f635a3..7c7fa3c 100755 --- a/spec/run-conformance.sh +++ b/spec/run-conformance.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# 4言語の適合性検証を走らせる。中身は spec/tools/run_conformance.py。 +# 全言語の適合性検証を走らせる。中身はspec/tools/run_conformance.py。 # # ./spec/run-conformance.sh 全言語を検証 # ./spec/run-conformance.sh --only csharp 言語を絞る @@ -8,7 +8,7 @@ set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -# ツールチェーンへ PATH を通す(未導入のものは検証対象から自動的に外れる) +# ツールチェーンへPATHを通す(未導入のものは検証対象から自動的に外れる) SPRINGNBT_ENV_QUIET=1 source "${repo_root}/spec/tools/env.sh" exec python3 "${repo_root}/spec/tools/run_conformance.py" "$@" diff --git a/spec/testdata/manifest.json b/spec/testdata/manifest.json index 02a45a4..66793cc 100644 --- a/spec/testdata/manifest.json +++ b/spec/testdata/manifest.json @@ -17,7 +17,7 @@ "input": "nbt/all_tags.nbt", "format": "java", "compression": "none", - "description": "全13タグを1つずつ含む", + "description": "全13タグを含む。TAG_EndはCompoundの終端として現れる", "expect": "expect/nbt/all_tags.json", "roundtrip": true }, diff --git a/spec/tools/build_testdata.py b/spec/tools/build_testdata.py index c1c8448..2af3351 100644 --- a/spec/tools/build_testdata.py +++ b/spec/tools/build_testdata.py @@ -1,12 +1,9 @@ #!/usr/bin/env python3 """共通テストベクタを生成する。 -仕様: docs/spec/90-conformance.md 4章 - -重要な設計方針: - このスクリプトは **ライブラリ本体を一切使わない**。 - 仕様書の記述だけを根拠に、独立した最小の NBT ライタを内蔵してバイト列を組み立てる。 - ライブラリでベクタを作ると、ライブラリのバグをそのまま期待値にしてしまうため。 +重要な設計方針として、このスクリプトは**ライブラリ本体を一切使わない**。 +仕様書の記述だけを根拠に、独立した最小のNBTライタを内蔵してバイト列を組み立てる。 +ライブラリでベクタを作ると、ライブラリのバグをそのまま期待値にしてしまうため。 使い方: python3 spec/tools/build_testdata.py @@ -21,7 +18,7 @@ import zlib # --------------------------------------------------------------------------- -# 独立した最小の NBT ライタ(仕様書の記述だけを根拠に実装する) +# 独立した最小のNBTライタ(仕様書の記述だけを根拠に実装する) # --------------------------------------------------------------------------- TAG_END = 0 @@ -40,14 +37,14 @@ def mutf8_encode(text: str) -> bytes: - """文字列を MUTF-8 へ符号化する。 + """文字列をMUTF-8へ符号化する。 - 標準 UTF-8 との違いは U+0000 が C0 80 になることと、 - 補助文字がサロゲートペアへ分解されて 3 バイト × 2 になること。 + 標準UTF-8との違いは、U+0000がC0 80になることと、 + 補助文字がサロゲートペアへ分解されて3バイト × 2になること。 """ out = bytearray() - # Python の str はコードポイント単位なので、まず UTF-16 コード単位へ落とす + # Pythonのstrはコードポイント単位なので、まずUTF-16コード単位へ落とす units = [] for ch in text: code = ord(ch) @@ -59,7 +56,7 @@ def mutf8_encode(text: str) -> bytes: else: units.append(code) - # コード単位ごとに 1〜3 バイトへ展開する + # コード単位ごとに1〜3バイトへ展開する for unit in units: if 0x0001 <= unit <= 0x007F: out.append(unit) @@ -75,7 +72,7 @@ def mutf8_encode(text: str) -> bytes: def write_string(text: str) -> bytes: - """u16 のバイト長を前置した MUTF-8 文字列。""" + """u16のバイト長を前置したMUTF-8文字列。""" encoded = mutf8_encode(text) if len(encoded) > 65535: raise ValueError("文字列が長すぎる: %d バイト" % len(encoded)) @@ -83,7 +80,7 @@ def write_string(text: str) -> bytes: class Tag: - """タグID とペイロードの組。""" + """タグIDとペイロードの組。""" def __init__(self, tag_id: int, payload: bytes) -> None: self.tag_id = tag_id @@ -111,7 +108,7 @@ def t_float(value: float) -> Tag: def t_float_bits(bits: int) -> Tag: - """ビットパターンを直接指定する。NaN の表現を固定したい場合に使う。""" + """ビットパターンを直接指定する。NaNの表現を固定したい場合に使う。""" return Tag(TAG_FLOAT, struct.pack(">I", bits)) @@ -135,7 +132,7 @@ def t_string(text: str) -> Tag: def t_string_raw(raw: bytes) -> Tag: - """MUTF-8 のバイト列を直接指定する。孤立サロゲートを書くために使う。""" + """MUTF-8のバイト列を直接指定する。孤立サロゲートを書くために使う。""" return Tag(TAG_STRING, struct.pack(">H", len(raw)) + raw) @@ -147,7 +144,7 @@ def t_list(element_id: int, elements: list) -> Tag: def t_compound(entries: list) -> Tag: - """entries は (名前, Tag) の並び。挿入順がそのまま出力順になる。""" + """entriesは (名前, Tag) の並び。挿入順がそのまま出力順になる。""" payload = b"" for name, tag in entries: payload += struct.pack(">B", tag.tag_id) @@ -194,9 +191,9 @@ def to_network(root: Tag) -> bytes: def write_if_changed(path, data): """内容が変わるときだけ書き出す。 - Gzip / Zlib の出力バイト列は zlib の実装とバージョンで変わる。 + Gzip / Zlibの出力バイト列はzlibの実装とバージョンで変わる。 毎回書き出すと、同じ内容なのに環境ごとに差分が出てしまい、 - 「テストベクタが生成器から再現できる」ことを CI で確かめられなくなる。 + 「テストベクタが生成器から再現できる」ことをCIで確かめられなくなる。 そこで**展開後の中身が同じなら書き換えない**。 圧縮方式そのものを変えたときはバイト列も中身も変わるので、ちゃんと更新される。 @@ -219,7 +216,7 @@ def write_if_changed(path, data): def same_after_decompression(existing, data): - """2 つのバイト列が、展開すると同じ内容になるか。 + """2つのバイト列が、展開すると同じ内容になるか。 どちらかが展開できない形式なら「違う」とみなす。 """ @@ -254,11 +251,11 @@ def region_contents(data): """リージョンファイルを、圧縮に依存しない「論理的な中身」へ均す。 位置表・タイムスタンプ表と、各チャンクの (圧縮方式ID, 展開後のバイト列) を並べる。 - 圧縮結果そのものは zlib の実装で変わるため、比較には使わない。 + 圧縮結果そのものはzlibの実装で変わるため、比較には使わない。 """ parts = [data[:2 * SECTOR]] - # 位置表の 1024 エントリを順に見て、存在するチャンクを取り出す + # 位置表の1024エントリを順に見て、存在するチャンクを取り出す for index in range(1024): offset, sectors = struct.unpack_from(">I", data, index * 4)[0] >> 8, data[index * 4 + 3] @@ -292,7 +289,7 @@ def region_contents(data): def add_concat_vector(vector_id, filename, data, description): - """連なった NBT のベクタを 1 件登録する。 + """連なったNBTのベクタを1件登録する。 期待値ファイルは持たず、言語間で出力が一致することだけを見る。 """ @@ -311,7 +308,7 @@ def add_concat_vector(vector_id, filename, data, description): def add_vector(vector_id, filename, data, description, fmt="java", compression="none", roundtrip=True, expect_error=None): - """ベクタを 1 件登録し、ファイルへ書き出す。""" + """ベクタを1件登録し、ファイルへ書き出す。""" write_if_changed(os.path.join(TESTDATA, filename), data) entry = { @@ -335,12 +332,15 @@ def add_vector(vector_id, filename, data, description, def hello_world_tag(): - """最小の NBT。ルート名 "hello world" の Compound に文字列がひとつ。""" + """最小のNBT。ルート名"hello world"のCompoundに文字列がひとつ。""" return t_compound([("name", t_string("Bananrama"))]) def all_tags_tag(): - """全13タグを1つずつ含む Compound。""" + """TAG_Endを除く12種のタグ型をすべて含むCompound。 + + TAG_EndはCompoundの終端として書き出されるので、バイト列には13種すべてが現れる。 + """ return t_compound([ ("byte", t_byte(-128)), ("short", t_short(32767)), @@ -361,7 +361,7 @@ def all_tags_tag(): def nested_compound(depth: int) -> Tag: - """指定した深さまで Compound を入れ子にする。ルートを含めた段数を depth とする。""" + """指定した深さまでCompoundを入れ子にする。ルートを含めた段数をdepthとする。""" tag = t_compound([]) # 内側から外側へ包んでいく @@ -383,7 +383,7 @@ def build_all(): add_vector( "nbt/all_tags", "nbt/all_tags.nbt", to_java_file("", all_tags), - "全13タグを1つずつ含む") + "全13タグを含む。TAG_EndはCompoundの終端として現れる") add_vector( "nbt/nested_deep", "nbt/nested_deep.nbt", @@ -456,7 +456,7 @@ def build_all(): to_java_file("", t_compound([("value", t_string("a" * 65535))])), "MUTF-8 で 65535 バイトちょうどの文字列。長さフィールドの上限") - # 同じ内容を3種の圧縮で用意し、自動判定が効くことを確かめる + # 同じ内容を3種の圧縮で用意し、自動判定が働くことを確かめる plain = to_java_file("hello world", hello) add_vector( "nbt/uncompressed", "nbt/uncompressed.nbt", plain, @@ -505,7 +505,7 @@ def build_all(): "nbt/trailing_bytes", "nbt/trailing_bytes.nbt", trailing, "ルートの後に余分なバイトがある", expect_error="MALFORMED_DATA") - # キーに孤立サロゲートを置いたベクタ。値と違いキーでは許さない(仕様 10 の 2.2章) + # キーに孤立サロゲートを置いたベクタ。値と違いキーでは許さない lone_key = (struct.pack(">B", TAG_COMPOUND) + write_string("") + struct.pack(">B", TAG_INT) + struct.pack(">H", 3) + bytes([0xED, 0xA0, 0xBD]) @@ -524,7 +524,7 @@ def build_all(): # --------------------------------------------------------------------------- -# Anvil リージョンファイル +# Anvilリージョンファイル # --------------------------------------------------------------------------- SECTOR = 4096 @@ -538,7 +538,7 @@ class RegionBuilder: """ def __init__(self): - # ヘッダ 2 セクタぶんをゼロで確保する + # ヘッダ2セクタぶんをゼロで確保する self.sectors = [bytearray(SECTOR), bytearray(SECTOR)] self.locations = [(0, 0)] * 1024 self.timestamps = [0] * 1024 @@ -548,7 +548,7 @@ def _index(self, local_x, local_z): def add_chunk(self, local_x, local_z, payload, compression=2, timestamp=1700000000, external=False, at_sector=None): - """チャンクを 1 つ置く。at_sector を指定すると配置先を固定できる。""" + """チャンクを1つ置く。at_sectorを指定すると配置先を固定できる。""" if external: body = b"" scheme = compression | 0x80 @@ -601,22 +601,21 @@ def build(self): # LZ4(圧縮方式ID 4)の符号化 # # 展開側はライブラリが実装するので、ここには符号化だけを置く。 -# 仕様: docs/spec/20-anvil-region.md 3.1.1 / 3.1.2 # --------------------------------------------------------------------------- -#: LZ4 ブロックのマッチはここから始まる長さ以上でなければならない。 +#: LZ4ブロックのマッチは、この長さ以上でなければならない。 LZ4_MIN_MATCH = 4 -#: ブロックの末尾 5 バイトは必ずリテラルにする。 +#: ブロックの末尾5バイトは必ずリテラルにする。 LZ4_LAST_LITERALS = 5 -#: 最後のマッチは末尾から 12 バイト以内で始めない。 +#: 最後のマッチは末尾から12バイト以内で始めない。 LZ4_MATCH_LIMIT = 12 def lz4_write_length(out, value): - """長さの追加ぶんを、255 が続く形式で書く。""" - # 255 を書けるだけ書き、最後に 255 未満のバイトで終える + """長さの追加ぶんを、255が続く形式で書く。""" + # 255を書けるだけ書き、最後に255未満のバイトで終える while value >= 255: out.append(255) value -= 255 @@ -625,11 +624,11 @@ def lz4_write_length(out, value): def lz4_emit(out, literals, offset, match_length): - """シーケンスを 1 つ書く。offset が None なら最後のシーケンス。""" + """シーケンスを1つ書く。offsetがNoneなら最後のシーケンス。""" token = (min(len(literals), 15) << 4) | min(match_length - LZ4_MIN_MATCH, 15) out.append(token) - # リテラル長が 15 以上なら追加バイトが続く + # リテラル長が15以上なら追加バイトが続く if len(literals) >= 15: lz4_write_length(out, len(literals) - 15) @@ -641,13 +640,13 @@ def lz4_emit(out, literals, offset, match_length): out += struct.pack("= 15: lz4_write_length(out, match_length - LZ4_MIN_MATCH - 15) def lz4_compress_block(data): - """LZ4 ブロック形式へ符号化する。 + """LZ4ブロック形式へ符号化する。 圧縮率は求めない。展開側の検証に必要な形(リテラル・マッチ・ 重なりのあるマッチ・長さの追加バイト)が出れば十分。 @@ -658,13 +657,13 @@ def lz4_compress_block(data): position = 0 limit = len(data) - LZ4_MATCH_LIMIT - # 4 バイト一致する直前の位置を探しながら進む + # 4バイト一致する直前の位置を探しながら進む while position < limit: key = bytes(data[position:position + LZ4_MIN_MATCH]) candidate = table.get(key) table[key] = position - # 参照できる範囲に候補が無ければ 1 バイト進む + # 参照できる範囲に候補が無ければ1バイト進む if candidate is None or position - candidate > 65535: position += 1 continue @@ -686,7 +685,7 @@ def lz4_compress_block(data): def lz4_block(payload, compressed=True): - """LZ4Block 形式のブロックを 1 つ作る。""" + """LZ4Block形式のブロックを1つ作る。""" if compressed: body = lz4_compress_block(payload) token = 0x26 @@ -694,7 +693,7 @@ def lz4_block(payload, compressed=True): body = payload token = 0x16 - # チェックサムは検証対象外なので 0 を入れる + # チェックサムは検証対象外なので0を入れる header = (b"LZ4Block" + bytes([token]) + struct.pack("= (1 << 63) else value for value in longs] @@ -914,10 +913,9 @@ def block_palette_entry(name, properties=None): def paletted_container(palette_entries, indices, min_bits): - """パレット付きコンテナを組み立てる。パレットが 1 要素なら data を書かない。 + """パレット付きコンテナを組み立てる。パレットが1要素ならdataを書かない。 - キーは実データと同じく `data` -> `palette` の順に置く - (docs/spec/31-paletted-container.md 4.1章)。 + キーは実データと同じく`data` -> `palette`の順に置く。 """ entries = [] @@ -930,7 +928,7 @@ def paletted_container(palette_entries, indices, min_bits): def make_section(y, block_palette, block_indices, biome_palette, biome_indices): - """セクションを 1 つ組み立てる。""" + """セクションを1つ組み立てる。""" return t_compound([ ("Y", t_byte(y)), ("block_states", paletted_container(block_palette, block_indices, 4)), @@ -939,7 +937,7 @@ def make_section(y, block_palette, block_indices, biome_palette, biome_indices): def block_entity(entity_id, x, y, z): - """ブロックエンティティ 1 件。座標は絶対座標で持つ(実データと同じ)。""" + """ブロックエンティティ1件。座標は絶対座標で持つ(実データと同じ)。""" return t_compound([ ("id", t_string(entity_id)), ("x", t_int(x)), @@ -949,7 +947,7 @@ def block_entity(entity_id, x, y, z): def block_tick(block_id, x, y, z): - """ブロックのティック予約 1 件。実データのキー名をなぞる。""" + """ブロックのティック予約1件。実データのキー名をなぞる。""" return t_compound([ ("i", t_string(block_id)), ("p", t_int(0)), @@ -962,7 +960,7 @@ def block_tick(block_id, x, y, z): def make_chunk(x, z, sections, status="minecraft:full", block_entities=None, block_ticks=None, fluid_ticks=None): - """チャンクの NBT を組み立てる。実データの構造をなぞる。""" + """チャンクのNBTを組み立てる。実データの構造をなぞる。""" entries = [ ("DataVersion", t_int(4903)), ("xPos", t_int(x)), @@ -977,7 +975,8 @@ def make_chunk(x, z, sections, status="minecraft:full", ])), ] - # 付随データは、空なら要素型 End の空リストになる(実データと同じ) + # block_entitiesは、空なら要素型Endの空リストになる(実データと同じ) + # block_ticksとfluid_ticksは、空ならキーごと書かない for key, values in (("block_entities", block_entities), ("block_ticks", block_ticks), ("fluid_ticks", fluid_ticks)): @@ -997,14 +996,14 @@ def make_chunk(x, z, sections, status="minecraft:full", def build_world(): - """World / Block のテストベクタを組み立てる。""" - # パレット 1 要素。data を持たない + """World / Blockのテストベクタを組み立てる。""" + # パレット1要素。dataを持たない add_vector( "world/palette_1", "world/palette_1.nbt", make_chunk(0, 0, [make_section(-4, [AIR], [0] * 4096, [PLAINS], [0] * 64)]), "パレット 1 要素のセクション(data 無し)") - # パレット 5 要素 -> bits=4、端数なし(256 long) + # パレット5要素 -> bits=4、端数なし(256 long) palette5 = [ AIR, STONE, block_palette_entry("minecraft:dirt"), @@ -1017,7 +1016,7 @@ def build_world(): make_chunk(0, 0, [make_section(-4, palette5, indices5, [PLAINS], [0] * 64)]), "パレット 5 要素 -> bits=4、data は 256 long(端数なし)") - # パレット 17 要素 -> bits=5、最後の long に端数(342 long) + # パレット17要素 -> bits=5、最後のlongに端数(342 long) palette17 = [AIR, STONE] + [ block_palette_entry("minecraft:stone", [("variant", "v%d" % index)]) for index in range(15) @@ -1028,7 +1027,7 @@ def build_world(): make_chunk(0, 0, [make_section(-4, palette17, indices17, [PLAINS], [0] * 64)]), "パレット 17 要素 -> bits=5、data は 342 long(最後に端数)") - # 複数セクション + バイオーム 2 種(bits=1) + # 複数セクション + バイオーム2種(bits=1) biome_palette = [PLAINS, t_string("minecraft:desert")] biome_indices = [(position // 8) % 2 for position in range(64)] add_vector( @@ -1040,7 +1039,7 @@ def build_world(): ]), "セクション 3 個。バイオームも 2 種(bits=1)") - # 未使用パレット要素を含む。compact() で減るはず + # 未使用パレット要素を含む。compact()で減るはず palette_unused = [AIR, STONE, block_palette_entry("minecraft:dirt"), block_palette_entry("minecraft:sand")] @@ -1053,14 +1052,14 @@ def build_world(): # ブロックに紐づく付随データを持つチャンク。 # ブロックを置き換えたとき、同じ座標の要素が取り除かれるかを見る。 - # チャンク (0,0) なので、絶対座標は x,z がそのまま。y は min_section_y*16 = -64 から + # チャンク (0,0) なので、絶対座標はx,zがそのまま。yはmin_section_y*16 = -64から add_vector( "world/block_entities", "world/block_entities.nbt", make_chunk( 0, 0, [make_section(-4, palette5, indices5, [PLAINS], [0] * 64)], block_entities=[ - # chunk_edit が置き換える座標にあるもの(消えるはず) + # chunk_editが置き換える座標にあるもの(消えるはず) block_entity("minecraft:chest", 0, -64, 0), block_entity("minecraft:furnace", 1, -64, 1), # 触らない座標にあるもの(残るはず) @@ -1104,7 +1103,7 @@ def build_world(): "添字がパレットの範囲外を指している", expect_error="MALFORMED_DATA") - # 異常系: data の長さがビット幅と合わない + # 異常系: dataの長さがビット幅と合わない wrong_length = t_compound([ ("data", t_long_array([0] * 100)), ("palette", t_list(TAG_COMPOUND, [AIR, STONE, block_palette_entry("minecraft:dirt")])), @@ -1127,7 +1126,7 @@ def build_world(): expect_error="MALFORMED_DATA") def write_manifest(): - """ベクタ一覧を manifest.json へ書き出す。""" + """ベクタ一覧をmanifest.jsonへ書き出す。""" manifest = { "comment": "spec/tools/build_testdata.py が生成する。手で編集しないこと", "vectors": VECTORS, @@ -1141,8 +1140,8 @@ def write_manifest(): def write_handwritten_expectations(): """手書きの期待値。生成器そのものを検証するための起点になる。 - ここだけは 90-conformance.md と 00-conventions.md の記述を見ながら手で書く。 - 他の期待値は検証済みの実装から生成するため、この 1 件が信頼の基点になる。 + ここだけは90-conformance.mdと00-conventions.mdの記述を見ながら手で書く。 + 他の期待値は検証済みの実装から生成するため、この1件が信頼の基点になる。 """ expected = ( '{"format":"java","root_name":"hello world","root":' diff --git a/spec/tools/check_comments.py b/spec/tools/check_comments.py index 4661a0c..5720df8 100644 --- a/spec/tools/check_comments.py +++ b/spec/tools/check_comments.py @@ -3,21 +3,25 @@ 全言語で同じ書きぶりを保つための道具。人手のレビューでは必ず抜けが出る。 -検査するもの: +次のものを検査する。 -1. **公開 API の doc コメント** — 型・公開メソッドの直前に、 - その言語の doc 形式のコメントがあるか +1. **公開APIのdocコメント**: 型・公開メソッドの直前に、 + その言語のdoc形式のコメントがあるか (C# `///

` / Java・TypeScript `/** */` / Python docstring / Rust `///`) -2. **条件分岐のコメント** — `if` の直前に説明のコメントがあるか -3. **ループのコメント** — `for` / `foreach` / `while` の直前に説明のコメントがあるか -4. **禁止記法** — 三項演算子、オプショナルチェーン、null 合体演算子 -5. **コメントの句点** — コメントに `。` を使わない - -ガード節(引数検証など、`throw` / `return` だけで終わる短い分岐)は -2 の対象から外す。書いても「不正なら弾く」としか書けず、かえって読みにくいため。 +2. **条件分岐のコメント**: `if`の直前かブロックの先頭に説明のコメントがあるか +3. **ループのコメント**: `for` / `foreach` / `while`の直前かブロックの先頭に説明のコメントがあるか +4. **禁止記法**: 三項演算子、オプショナルチェーン、null合体演算子 +5. **コメントの句点**: コメントに`。`を使わない +6. **仕様書への参照**: コメントに仕様書やADRの参照先(`docs/spec/…`、「3章」など)を書かない。 + 章立てを変えるたびにコードのコメントが古くなるため +7. **docコメントの整形**: 複数行のdocコメントを、本文を載せた`/**`の行で始めていないか + +ガード節(引数検証など、`throw` / `return`だけで終わる短い分岐)は +2の対象から外す。書いても「不正なら弾く」としか書けず、かえって読みにくいため。 +多重ループの内側は3の対象から外す。いちばん外側に書いた目的がそのまま当てはまるため。 使い方: - python3 spec/tools/check_comments.py # 違反があれば終了コード 1 + python3 spec/tools/check_comments.py # 違反があれば終了コード1 python3 spec/tools/check_comments.py --summary # 件数だけ出す python3 spec/tools/check_comments.py --language csharp @@ -35,7 +39,7 @@ LANGUAGE_ORDER = ["csharp", "java", "typescript", "python", "rust"] -#: 言語ごとの、検査対象のソースと doc コメントの形。 +#: 言語ごとの、検査対象のソースとdocコメントの形。 LANGUAGES = { "csharp": { "roots": ["csharp/src"], @@ -73,17 +77,17 @@ # 禁止記法 # --------------------------------------------------------------------------- -#: 三項演算子。`cond ? a : b`。C# の nullable(`int?`) や -#: TypeScript の省略可能引数(`x?: T`) と紛れないよう、`?` の前後に空白を要求する。 +#: 三項演算子。`cond ? a : b`。C#のnullable(`int?`)や +#: TypeScriptの省略可能引数(`x?: T`)と紛れないよう、`?`の前後に空白を要求する。 TERNARY = re.compile(r"[^?\s]\s+\?\s+[^?:]+?\s+:\s") #: オプショナルチェーン。`foo?.bar` OPTIONAL_CHAIN = re.compile(r"[A-Za-z0-9_\)\]]\?\.") -#: null 合体演算子。`a ?? b` / `a ??= b` +#: null合体演算子。`a ?? b` / `a ??= b` NULL_COALESCE = re.compile(r"\?\?=?") -#: Python の条件式。三項演算子に相当する +#: Pythonの条件式。三項演算子に相当する PY_CONDITIONAL = re.compile(r"\S\s+if\s+.+\s+else\s+\S") BANNED = [ @@ -97,7 +101,7 @@ # 構文の検出 # --------------------------------------------------------------------------- -#: 条件分岐の始まり。`else if` / `elif` は先頭の if に付いていればよいので除く。 +#: 条件分岐の始まり。`else if` / `elif`は先頭のifに付いていればよいので除く。 BRANCH = { "csharp": re.compile(r"^\s*if\s*\("), "java": re.compile(r"^\s*if\s*\("), @@ -115,7 +119,7 @@ "rust": re.compile(r"^\s*(for\s+|while\s+|loop\s*\{)"), } -#: 公開メソッド・関数の宣言。doc コメントを要求する。 +#: 公開メソッド・関数の宣言。docコメントを要求する。 PUBLIC_MEMBER = { "csharp": re.compile( r"^\s{4}public\s+(?!class|interface|enum|struct|record)" @@ -125,7 +129,7 @@ r"^\s{4}public\s+(?!class|interface|enum|record)" r"(?:static\s+|final\s+|abstract\s+|synchronized\s+)*" r"[A-Za-z0-9_<>,\[\]\?\. ]+\s+[A-Za-z0-9_]+\s*\("), - # 宣言は `{` で終わる。`foo(...);` のような呼び出しと区別する + # 宣言は`{`で終わる。`foo(...);`のような呼び出しと区別する "typescript": re.compile( r"^ (?!private|#|constructor|if|for|while|switch|return|throw|else|do|try|catch)" r"(?:static\s+|get\s+|set\s+)?[A-Za-z0-9_]+\s*[\(<].*\{\s*$"), @@ -142,9 +146,12 @@ "rust": re.compile(r"^pub\s+(?:struct|enum|trait)\s"), } -#: 本文を載せたまま閉じていない doc コメントの開始行。 -#: 複数行の doc コメントは本文の無い `/**` で始める決まりなので、 -#: この形は整形が壊れた跡とみなす。 +#: コメントに書かない、仕様書・ADRへの参照。 +SPEC_REFERENCE = re.compile(r"docs/(spec|adr)/|adr/[0-9]{4}|[0-9]章|仕様[0-9]+の") + +#: 本文を載せたまま閉じていないdocコメントの開始行。 +#: 複数行のdocコメントは本文の無い`/**`で始める決まりなので、 +#: この形は整形が崩れた跡とみなす。 BROKEN_DOC = re.compile(r"^\s*/\*\*\s*\S") #: ガード節とみなす本体。これだけで終わる分岐にはコメントを求めない。 @@ -153,7 +160,7 @@ class Finding: - """1 件の指摘。""" + """1件の指摘。""" def __init__(self, language: str, path: str, line: int, kind: str, text: str) -> None: self.language = language @@ -167,7 +174,7 @@ def __str__(self) -> str: def is_comment(line: str, language: str) -> bool: - """その行がコメント(doc 形式を含む)か。""" + """その行がコメント(doc形式を含む)か。""" stripped = line.strip() spec = LANGUAGES[language] @@ -177,7 +184,7 @@ def is_comment(line: str, language: str) -> bool: if stripped.startswith(spec["line_comment"]): return True - # doc コメントの途中の行(Java / TypeScript の ` * ...`)も含める + # docコメントの途中の行(Java / TypeScriptの` * ...`)も含める for prefix in spec["doc_prefixes"]: if stripped.startswith(prefix): return True @@ -186,9 +193,9 @@ def is_comment(line: str, language: str) -> bool: def has_doc_before(lines, index: int, language: str) -> bool: - """宣言行の直前に doc 形式のコメントがあるか。 + """宣言行の直前にdoc形式のコメントがあるか。 - 属性やアノテーション(`[Fact]` / `@Override` など)は読み飛ばす。 + 属性やアノテーション(`[Fact]` / `@Override`など)は読み飛ばす。 """ spec = LANGUAGES[language] position = index - 1 @@ -215,18 +222,18 @@ def has_doc_before(lines, index: int, language: str) -> bool: if stripped.startswith(prefix): return True - # Java / TypeScript は doc コメントの終わりが `*/` + # Java / TypeScriptはdocコメントの終わりが`*/` return stripped == "*/" def has_python_docstring(lines, index: int) -> bool: - """Python の宣言の次行以降に docstring があるか。 + """Pythonの宣言の次行以降にdocstringがあるか。 引数リストが複数行に渡ることがあるので、宣言が閉じる行まで読み飛ばす。 """ position = index - # 宣言が `:` で終わるまで進める(複数行のシグネチャに対応) + # 宣言が`:`で終わるまで進める(複数行のシグネチャに対応) while position < len(lines) and not lines[position].rstrip().endswith(":"): position += 1 @@ -259,7 +266,7 @@ def has_comment_before(lines, index: int, language: str) -> bool: position -= 1 continue - # Python は docstring がその前の説明にあたる + # Pythonはdocstringがその前の説明にあたる if language == "python" and '"""' in stripped: return True @@ -271,7 +278,7 @@ def has_comment_before(lines, index: int, language: str) -> bool: def has_comment_inside(lines, index: int, language: str) -> bool: """ブロックの中の先頭にコメントがあるか。 - `if (...) {` の次の行から書き始める書き方も、 + `if (...) {`の次の行から書き始める書き方も、 「この分岐で何をするか」を説明しているので認める。 """ position = index + 1 @@ -300,7 +307,7 @@ def has_explanation(lines, index: int, language: str) -> bool: def is_nested_loop(lines, index: int, language: str) -> bool: """直前の行もループの開始か。 - 多重ループは 1 つの処理単位なので、いちばん外側に目的が書いてあれば足りる。 + 多重ループは1つの処理単位なので、いちばん外側に目的が書いてあれば足りる。 内側にも同じ説明を重ねると、かえって読みにくくなる。 """ position = index - 1 @@ -318,7 +325,7 @@ def is_nested_loop(lines, index: int, language: str) -> bool: def is_guard(lines, index: int, language: str) -> bool: - """引数検証などのガード節か。本体が 1 文で、抜けるだけのもの。""" + """引数検証などのガード節か。本体が1文で、抜けるだけのもの。""" # 同じ行に本体がある場合(`if (x) return;`) tail = lines[index].split(")", 1) @@ -326,7 +333,7 @@ def is_guard(lines, index: int, language: str) -> bool: if len(tail[1].strip()) > 0: return True - # 次の 1〜2 行が抜けるだけなら、ガード節とみなす + # 次の1〜2行が抜けるだけなら、ガード節とみなす for offset in (1, 2): if index + offset >= len(lines): break @@ -342,9 +349,9 @@ def is_guard(lines, index: int, language: str) -> bool: def is_inherited(lines, index: int) -> bool: - """継承元の doc を引き継ぐ宣言か。 + """継承元のdocを引き継ぐ宣言か。 - `@Override` や `` が付いていれば、同じ説明を重ねて書く必要はない。 + `@Override`や``が付いていれば、同じ説明を重ねて書く必要はない。 """ position = index - 1 @@ -387,8 +394,23 @@ def source_files(language: str): return found +def check_comment_text(language: str, path: str, number: int, line: str): + """コメント1行の本文が規約に沿っているかを見て、指摘の一覧を返す。""" + findings = [] + + # コメントに句点は使わない。文が続くなら行を分ける + if "。" in line: + findings.append(Finding(language, path, number, "コメントに句点がある", line)) + + # 仕様書の章立ては変わるので、参照先をコメントに書かない + if SPEC_REFERENCE.search(line): + findings.append(Finding(language, path, number, "コメントに仕様書の参照がある", line)) + + return findings + + def check_language(language: str): - """1 言語分を検査して、指摘の一覧を返す。""" + """1言語分を検査して、指摘の一覧を返す。""" findings = [] for path in source_files(language): @@ -404,36 +426,33 @@ def check_language(language: str): number = index + 1 stripped = line.strip() - # Python の docstring も句点を使わない - if language == "python" and in_docstring and "。" in line: - findings.append(Finding(language, relative, number, - "コメントに句点がある", line)) + # Pythonのdocstringも、コメントと同じ規則で本文を見る + if language == "python" and in_docstring: + findings.extend(check_comment_text(language, relative, number, line)) if language == "python": - # 三重引用符の数が奇数なら、docstring の出入りが切り替わる + # 三重引用符の数が奇数なら、docstringの出入りが切り替わる if stripped.count('"""') % 2 == 1: in_docstring = not in_docstring - # 閉じ忘れた doc コメントは、この行だけで判別できる + # 閉じ忘れたdocコメントは、この行だけで判別できる if BROKEN_DOC.match(line) and not stripped.endswith("*/"): findings.append(Finding(language, relative, number, "doc コメントの整形が壊れている", line)) - # ブロックコメントの中は検査しない + # ブロックコメントは本文だけを見て、構文の検査はしない if stripped.startswith("/*"): + findings.extend(check_comment_text(language, relative, number, line)) in_block_comment = "*/" not in stripped continue if in_block_comment: + findings.extend(check_comment_text(language, relative, number, line)) in_block_comment = "*/" not in stripped continue if is_comment(line, language): - # コメントに句点は使わない。文が続くなら行を分ける - if "。" in line: - findings.append(Finding(language, relative, number, - "コメントに句点がある", line)) - + findings.extend(check_comment_text(language, relative, number, line)) continue # 1. 禁止記法 @@ -441,7 +460,7 @@ def check_language(language: str): if language in languages and pattern.search(line): findings.append(Finding(language, relative, number, "禁止記法: " + label, line)) - # 2. 公開型の doc コメント + # 2. 公開型のdocコメント if PUBLIC_TYPE[language].match(line): if language == "python": documented = has_python_docstring(lines, index) @@ -453,9 +472,9 @@ def check_language(language: str): continue - # 3. 公開メソッドの doc コメント + # 3. 公開メソッドのdocコメント if PUBLIC_MEMBER[language].match(line): - # 継承元の doc を引き継ぐものは、重ねて書かなくてよい + # 継承元のdocを引き継ぐものは、重ねて書かなくてよい if is_inherited(lines, index): continue @@ -471,7 +490,7 @@ def check_language(language: str): # 4. 条件分岐のコメント(ガード節と定型句は除く) if BRANCH[language].match(line): - # スクリプトの入口を示す定型句。説明することがない + # スクリプトの起点を示す定型句。説明することがない if stripped.startswith("if __name__ =="): continue @@ -482,7 +501,7 @@ def check_language(language: str): # 5. ループのコメント if LOOP[language].match(line): - # 多重ループの内側は、外側に付けた説明が 効いている + # 多重ループの内側には、外側に付けた説明が当てはまる if is_nested_loop(lines, index, language): continue diff --git a/spec/tools/check_docs_sync.py b/spec/tools/check_docs_sync.py index 0cb1ee4..db02208 100644 --- a/spec/tools/check_docs_sync.py +++ b/spec/tools/check_docs_sync.py @@ -1,24 +1,26 @@ #!/usr/bin/env python3 """ドキュメントと実装が食い違っていないかを機械で確かめる。 -1人で5言語ぶんの API を人手で揃え続けるのは必ず破綻する。 -このツールが CI でそれを守る。 +1人で5言語ぶんのAPIを人手で揃え続けると、必ずうまくいかなくなる。 +そこで、APIが揃っている状態をこのツールがCIで守る。 -やること: +このツールは次のことをする。 -1. **型の一致** — 全言語の公開型の論理名集合が一致するか。 - 言語の性質による差異は `extract_api.EXPECTED_TYPE_GAPS` に理由つきで登録し、 +1. **型の一致**: 全言語の公開型の論理名集合が一致するか。 + 言語の性質による差異は`extract_api.EXPECTED_TYPE_GAPS`に理由つきで登録し、 そこに無い差異が出たら失敗させる -2. **`docs/api/` の再生成** — 対応表は実装から生成する。 - 生成結果と現在のファイルが違えば失敗させる(`--write` で書き直せる) -3. **`docs/features.md` の照合** — ✅ が付いている機能に対応する型が +2. **メンバの一致**: 同じ型のメンバが全言語で揃っているか。 + 基準実装のC#にあるメンバは全言語に要る。C#に無いのに他の言語の多くが持つメンバは、 + C#の取りこぼしとして報告する。言語の性質による差異は`EXPECTED_MEMBER_GAPS`に理由つきで登録する +3. **`docs/api/`の再生成**: 対応表は実装から生成する。 + 生成結果と現在のファイルが違えば失敗させる(`--write`で書き直せる)。 + どのページにも載っていない公開型があっても失敗させる +4. **`docs/features.md`の照合**: ✅が付いている機能に対応する型が その言語に実在するか 使い方: - python3 spec/tools/check_docs_sync.py # 検査する(CI 用) - python3 spec/tools/check_docs_sync.py --write # docs/api/ を書き直す - -仕様: docs/spec/00-conventions.md 3章 / docs/adr/0009-static-api-extraction.md + python3 spec/tools/check_docs_sync.py # 検査する(CI用) + python3 spec/tools/check_docs_sync.py --write # docs/api/を書き直す """ from __future__ import annotations @@ -50,7 +52,7 @@ GENERATED_START = "" GENERATED_END = "" -#: `docs/api/<ファイル>.md` に、どのレイヤの型を載せるか。 +#: `docs/api/<ファイル>.md`に、どのレイヤの型を載せるか。 #: #: 型はソースのディレクトリではなく、利用者から見た役割で分ける。 API_PAGES = { @@ -84,7 +86,7 @@ }, } -#: `docs/api/` に載せない型。エラーモデルは `docs/guide/06` が本体。 +#: `docs/api/`に載せない型。エラーモデルは`docs/guide/06`が本体。 API_EXCLUDED_TYPES = {"SpringNbtError", "ErrorCode"} @@ -144,38 +146,37 @@ def check_types(apis, report: Report) -> None: #: 言語ごとに存在が変わるメンバと、その理由。メンバの一致検査から除く。 #: #: 「その言語では書きようがない」「言語の作法として別の形で提供している」ものだけを -#: 載せる。単に実装していないだけのものはここに逃がさず、実装する。 -#: 利用者から見て差が出るものは docs/features.md「言語ごとの差異」にも載せる。 +#: 載せる。単に実装していないだけのものはここに載せて済ませず、実装する。 +#: 利用者から見て差が出るものはdocs/features.md「言語ごとの差異」にも載せる。 EXPECTED_MEMBER_GAPS = { - # Node のストリームは非同期しか無い。このライブラリは全体を同期 API で - # 揃えているので、TypeScript ではファイルとバイト列の入口だけを提供する + # Nodeのストリームは非同期しか無い。このライブラリは全体を同期APIで + # 揃えているので、TypeScriptではファイルとバイト列での読み書きだけを提供する (MODULE_LEVEL, "read_stream"): ["typescript"], (MODULE_LEVEL, "write_stream"): ["typescript"], - # Rust ではタグ型を NbtTag 列挙のバリアントが表すので、 + # Rustではタグ型をNbtTag列挙のバリアントが表すので、 # 個々の型は自分の型を答える必要がない ("NbtCompound", "type"): ["rust"], ("NbtList", "type"): ["rust"], ("NbtString", "type"): ["rust"], - # Rust の NbtString は Text / Surrogates の 2 形態 + # RustのNbtStringはText / Surrogatesの2形態 # 孤立サロゲートを保持するため、単一の値では表せない - # (docs/spec/10-nbt-binary.md 2.3) ("NbtString", "value"): ["rust"], - # C# の NbtList は IList を実装するので、 - # インターフェースの要求として値指定の Remove を持つ - # 他言語は位置指定の remove_at だけを提供する + # C#のNbtListはIListを実装するので、 + # インターフェースの要求として値指定のRemoveを持つ + # 他言語は位置指定のremove_atだけを提供する ("NbtList", "remove"): ["java", "typescript", "python", "rust"], - # C# は添字子 list[i] で取り出すのが作法 + # C#は添字子list[i]で取り出すのが作法 ("NbtList", "get"): ["csharp"], - # TypeScript のオプションは素のオブジェクトなので、 - # { compression: Compression.None } をそのまま書く + # TypeScriptのオプションは素のオブジェクトなので、 + # { compression: Compression.None }をそのまま書く ("NbtWriteOptions", "uncompressed"): ["typescript"], - # C# / TypeScript では列挙の基底値へキャストするのが作法 + # C# / TypeScriptでは列挙の基底値へキャストするのが作法 ("ChunkCompression", "id"): ["csharp", "typescript"], } @@ -209,8 +210,8 @@ def has_member(api, type_name: str, member: str) -> bool: def check_members(apis, report: Report) -> None: """同じ型のメンバが全言語で揃っているかを見る。 - 論理APIの正は基準実装の C# とする(docs/adr/0002)。 - C# にあるものは全言語に要る。逆に、C# に無いのに他の言語の多くが + 論理APIの正は基準実装のC#とする。 + C#にあるものは全言語に要る。逆に、C#に無いのに他の言語の多くが 持っているものは、基準実装の取りこぼしとして報告する。 """ reference = apis["csharp"] @@ -275,11 +276,11 @@ def check_members(apis, report: Report) -> None: # --------------------------------------------------------------------------- -# 3. docs/api/ の生成 +# 3. docs/api/の生成 # --------------------------------------------------------------------------- def render_api_page(page_id: str, page, apis) -> str: - """1 ページぶんの対応表を組み立てる。""" + """1ページぶんの対応表を組み立てる。""" lines = [] lines.append("| 論理名 | " + " | ".join(LANGUAGE_LABELS[name] for name in LANGUAGE_ORDER) + " | 概要 |") @@ -353,7 +354,7 @@ def api_page_path(page_id: str) -> str: def sync_api_pages(apis, write: bool, report: Report) -> None: - """docs/api/*.md の生成部分を作り直し、書くか照合するかする。""" + """docs/api/*.mdの生成部分を作り直し、書くか照合するかする。""" for page_id, page in API_PAGES.items(): path = api_page_path(page_id) @@ -389,7 +390,7 @@ def sync_api_pages(apis, write: bool, report: Report) -> None: def check_api_coverage(apis, report: Report) -> None: - """docs/api/ のどのページにも載っていない公開型が無いかを見る。""" + """docs/api/のどのページにも載っていない公開型が無いかを見る。""" listed = set() for page in API_PAGES.values(): @@ -404,32 +405,32 @@ def check_api_coverage(apis, report: Report) -> None: # --------------------------------------------------------------------------- -# 4. docs/features.md の照合 +# 4. docs/features.mdの照合 # --------------------------------------------------------------------------- -#: features.md の行と、その機能を代表する型の対応。 +#: features.mdの行と、その機能を代表する型の対応。 #: -#: ✅ が付いている言語にその型が実在するかを確かめる。 +#: ✅が付いている言語にその型が実在するかを確かめる。 #: 表のすべての行を機械検証できるわけではないので、 #: 型の有無で判定できる機能だけを載せる。 FEATURE_TYPES = { "SNBT パース": MODULE_LEVEL, - "リージョンファイル": "RegionFile", + "リージョンの読み込み": "RegionFile", "`region/` `entities/` `poi/` の横断アクセス": "RegionFolder", "`level.dat` の読み込み": "MinecraftWorld", "標準3次元 + カスタム次元の解決": "Dimension", "チャンクの解釈": "Chunk", "`BlockState` の文字列表現": "BlockState", "パレット自動拡張とビット幅の再計算": "PalettedContainer", - "DataVersion 不一致時の警告/エラー切替": "VersionMismatchAction", + "扱えない形式の検出(警告/エラー切替)": "VersionMismatchAction", } -#: features.md の言語列の並び。表の見出しと同じ順。 +#: features.mdの言語列の並び。表の見出しと同じ順。 FEATURE_COLUMNS = ["csharp", "java", "typescript", "python", "rust"] def check_features(apis, report: Report) -> None: - """features.md で ✅ の機能に対応する型が実在するかを見る。""" + """features.mdで✅の機能に対応する型が実在するかを見る。""" path = os.path.join(REPO_ROOT, "docs", "features.md") with open(path, "r", encoding="utf-8") as handle: @@ -441,7 +442,7 @@ def check_features(apis, report: Report) -> None: cells = [cell.strip() for cell in line.split("|")[1:-1]] - # 機能名 + 5言語 + 仕様 + 備考 の形でなければ対象外 + # 機能名と5言語ぶんの列が無い行は対象外 if len(cells) < 6: continue diff --git a/spec/tools/check_links.py b/spec/tools/check_links.py index c93ee30..e069fd5 100644 --- a/spec/tools/check_links.py +++ b/spec/tools/check_links.py @@ -1,22 +1,20 @@ #!/usr/bin/env python3 """ドキュメント内のリンク切れを調べる。 -`docs/` は5系統に分かれていて相互に行き来する作りなので、 -ファイルを増やしたり動かしたりするとリンクが静かに死ぬ。 -読み手がそこで止まってしまうため、CI で検出する。 +`docs/`は6つのディレクトリに分かれていて相互に行き来する作りなので、 +ファイルを増やしたり動かしたりするとリンクが知らないうちに切れる。 +読み手がそこで止まってしまうため、CIで検出する。 -調べるもの: +次のことを調べる。 1. 相対リンクの指す先のファイルが実在するか -2. `#見出し` の指す見出しがそのファイルに実在するか +2. `#見出し`の指す見出しがそのファイルに実在するか 外部リンク(http/https)は対象外。ネットワークに依存すると -CI がその日の外部サイトの都合で落ちるようになるため。 +CIがその日の外部サイトの都合で落ちるようになるため。 使い方: python3 spec/tools/check_links.py - -仕様: docs/adr/0009-static-api-extraction.md """ from __future__ import annotations @@ -28,10 +26,10 @@ REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -#: `[表示](リンク先)` の形。画像も同じ形なので一緒に拾う。 +#: `[表示](リンク先)`の形。画像も同じ形なので一緒に拾う。 LINK = re.compile(r"\[[^\]]*\]\(([^)\s]+)\)") -#: Markdown の見出し。 +#: Markdownの見出し。 HEADING = re.compile(r"^(#{1,6})\s+(.*?)\s*#*$") #: コードブロックの区切り。この中のリンクは例示なので対象外。 @@ -42,9 +40,9 @@ def slugify(text: str) -> str: - """見出しを GitHub のアンカー名へ写す。 + """見出しをGitHubのアンカー名へ写す。 - GitHub は見出しを小文字にし、記号を落とし、空白をハイフンに変える。 + GitHubは見出しを小文字にし、記号を落とし、空白をハイフンに変える。 日本語はそのまま残る。 """ # インラインのコード・強調・リンクの装飾を落とす @@ -90,10 +88,10 @@ def collect_headings(path: str): def markdown_files(): - """検査対象の Markdown を列挙する。""" + """検査対象のMarkdownを列挙する。""" found = [] - # docs/ 配下は全部、言語ディレクトリは README だけを見る + # docs/配下は全部、言語ディレクトリはREADMEだけを見る for language in ("csharp", "java", "typescript", "python", "rust"): candidate = os.path.join(REPO_ROOT, language, "README.md") @@ -111,7 +109,7 @@ def markdown_files(): if filename.endswith(".md"): found.append(os.path.join(dirpath, filename)) - # リポジトリ直下の README なども対象にする(1 階層だけ) + # リポジトリ直下のREADMEなども対象にする(1階層だけ) for filename in os.listdir(REPO_ROOT): if filename.endswith(".md"): found.append(os.path.join(REPO_ROOT, filename)) diff --git a/spec/tools/env.sh b/spec/tools/env.sh index 52ac006..9bab26d 100755 --- a/spec/tools/env.sh +++ b/spec/tools/env.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash -# SpringNBTLibrary の開発用ツールチェーンへ PATH を通す。 +# SpringNBTLibraryの開発用ツールチェーンへPATHを通す。 # 使い方: source spec/tools/env.sh # -# Homebrew の keg-only な formula(dotnet@8 / openjdk@21 / python@3.12)は -# /opt/homebrew/bin へ symlink されないため、ここで明示的に前方に置く。 +# Homebrewのkeg-onlyなformula(dotnet@8 / openjdk@21 / python@3.12)は +# /opt/homebrew/binへsymlinkされないため、ここで明示的に前方に置く。 _brew_prefix="${HOMEBREW_PREFIX:-/opt/homebrew}" @@ -14,7 +14,7 @@ for _p in \ "${HOME}/.cargo/bin" \ "${_brew_prefix}/opt/rustup/bin" do - # 既に PATH に含まれている場合は重複追加しない + # 既にPATHに含まれている場合は重複追加しない case ":${PATH}:" in *":${_p}:"*) ;; *) PATH="${_p}:${PATH}" ;; @@ -22,7 +22,7 @@ do done export PATH -# Maven が使う JDK を明示する(システム既定の JDK が無い環境でも動くように) +# Mavenが使うJDKを明示する(システム既定のJDKが無い環境でも動くように) export JAVA_HOME="${_brew_prefix}/opt/openjdk@21" unset _brew_prefix _p diff --git a/spec/tools/extract_api.py b/spec/tools/extract_api.py index 78cff6b..4e62186 100644 --- a/spec/tools/extract_api.py +++ b/spec/tools/extract_api.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 """全言語のソースから公開APIを抜き出し、言語非依存の論理名へ写す。 -このライブラリの差別化要因は「1人の開発者が全言語へ同一の API を提供すること」 -にある。人手で5言語を揃え続けるのは必ず破綻するので、機械で突き合わせる。 +このライブラリの差別化要因は「1人の開発者が全言語へ同一のAPIを提供すること」にある。 +人手で5言語を揃え続けると必ずうまくいかなくなるので、機械で比べる。 -抽出はソースの静的解析(正規表現)で行う。リフレクションや `cargo public-api` -のような実行時/専用ツールを使わないのは、5言語ぶんのツールチェーンを -CI で揃える手間と、それ自体が壊れたときの原因切り分けの難しさを避けるため -(→ docs/adr/0009-static-api-extraction.md)。 +抽出はソースの静的解析(正規表現)で行う。 +リフレクションや`cargo public-api`のような実行時/専用ツールを使わないのは、 +5言語ぶんのツールチェーンをCIで揃える手間と、 +それ自体がおかしくなったときの原因切り分けの難しさを避けるため。 -出力する論理名は `docs/spec/00-conventions.md` 3章の命名変換規則に従う。 +出力する論理名は次の命名変換規則に従う。 型・列挙 PascalCase NbtCompound メンバ snake_case read_file @@ -17,10 +17,8 @@ 使い方: python3 spec/tools/extract_api.py # 言語ごとの一覧を出す - python3 spec/tools/extract_api.py --json # JSON で出す + python3 spec/tools/extract_api.py --json # JSONで出す python3 spec/tools/extract_api.py --language rust # 1言語だけ - -仕様: docs/spec/00-conventions.md 3章 """ from __future__ import annotations @@ -33,39 +31,39 @@ REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -#: 出力・比較の言語順。基準実装の C# を先頭に置く。 +#: 出力・比較の言語順。基準実装のC#を先頭に置く。 LANGUAGE_ORDER = ["csharp", "java", "typescript", "python", "rust"] #: モジュール直下の関数・定数をまとめる仮想の型名。 #: -#: C# と Java には自由関数が無いので静的クラス(`NbtIo` など)に置くが、 -#: TypeScript / Python / Rust ではモジュール関数になる。 +#: C#とJavaには自由関数が無いので静的クラス(`NbtIo`など)に置くが、 +#: TypeScript / Python / Rustではモジュール関数になる。 #: 論理APIとしては同じものなので、ここへ寄せて比較する。 MODULE_LEVEL = "(module)" -#: API 一覧から除く型。実装の都合で公開しているが、利用者向けの API ではない。 +#: API一覧から除く型。実装の都合で公開しているが、利用者向けのAPIではない。 INTERNAL_TYPES = { - # Rust の内部実装 + # Rustの内部実装 "Parser", "Reader", "Writer", - # Rust の trait 実装(impl Trait for Type の Trait 側を拾ってしまったもの) + # Rustのtrait実装(impl Trait for TypeのTrait側を拾ってしまったもの) "From", "IntoIterator", } #: 言語ごとに名前が変わる型を、論理名へ寄せる表。 #: -#: 例外型は C# / Java が `Exception`、TypeScript / Python / Rust が `Error` と -#: 言語の慣習で分かれる。論理的には同じ型なので `SpringNbtError` に統一する。 +#: 例外型はC# / Javaが`Exception`、TypeScript / Python / Rustが`Error`と +#: 言語の慣習で分かれる。論理的には同じ型なので`SpringNbtError`に統一する。 TYPE_ALIASES = { "SpringNbtException": "SpringNbtError", "Error": "SpringNbtError", - # C# の拡張メソッドの置き場は、論理的には元の型のメンバ + # C#の拡張メソッドの置き場は、論理的には元の型のメンバ "ChunkCompressionExtensions": "ChunkCompression", "ErrorCodeExtensions": "ErrorCode", "TagTypeExtensions": "TagType", - # C# / Java の静的クラスは、他言語ではモジュール関数 + # C# / Javaの静的クラスは、他言語ではモジュール関数 "NbtIo": MODULE_LEVEL, "Snbt": MODULE_LEVEL, "Mutf8": MODULE_LEVEL, @@ -74,17 +72,17 @@ #: 言語の作法で綴りが変わるメンバを、論理名へ寄せる表。 MEMBER_ALIASES = { - # Python の with は予約語 + # Pythonのwithは予約語 "with_property": "with", - # Rust の複製は Clone トレイトの clone() - # copy は別の意味を持つマーカートレイトなので、その名前は使えない + # Rustの複製はCloneトレイトのclone() + # copyは別の意味を持つマーカートレイトなので、その名前は使えない "clone": "copy", - # Rust には Stream 型が無く、読み書きは Read / Write トレイトで受ける + # RustにはStream型が無く、読み書きはRead / Writeトレイトで受ける "read_reader": "read_stream", "write_writer": "write_stream", - # Rust では &str を返す変換に as_str と名づける + # Rustでは&strを返す変換にas_strと名づける "as_str": "as_string", - # Rust の type は予約語 + # Rustのtypeは予約語 "tag_type": "type", } @@ -92,7 +90,7 @@ #: #: docs/features.md「言語ごとの差異」に対応する。 EXPECTED_TYPE_GAPS = { - # Rust では NbtTag 列挙のバリアントなので、独立した型にならない + # RustではNbtTag列挙のバリアントなので、独立した型にならない "NbtByte": ["rust"], "NbtShort": ["rust"], "NbtInt": ["rust"], @@ -102,16 +100,16 @@ "NbtByteArray": ["rust"], "NbtIntArray": ["rust"], "NbtLongArray": ["rust"], - # TypeScript は NbtTag を型の合併で表すので、基底クラスが無い + # TypeScriptはNbtTagを型の合併で表すので、基底クラスが無い "NbtTag": ["typescript"], - # 他の言語は set_block のオーバーロードで済むが、Rust には無いのでトレイトで受ける + # 他の言語はset_blockのオーバーロードで済むが、Rustには無いのでトレイトで受ける "IntoBlockState": ["csharp", "java", "typescript", "python"], } -#: API 一覧から除くメンバ。言語の作法として要るが、論理APIではない。 +#: API一覧から除くメンバ。言語の作法として要るが、論理APIではない。 #: #: 「言語ごとに綴りが変わるだけ」のものと「言語ごとに機能そのものが変わる」ものを -#: 混ぜないこと。等値比較や深い複製は前者なので、ここには入れず突合の対象にする。 +#: 混ぜないこと。等値比較や深い複製は前者なので、ここには入れず比較の対象にする。 INTERNAL_MEMBERS = { "hash_code", "dispose", @@ -120,7 +118,7 @@ "get_hash_code", "compare_to", "next", - "close", # 言語ごとに using / try-with-resources / with と作法が違う + "close", # 言語ごとにusing / try-with-resources / withと作法が違う "enter", "exit", "fmt", @@ -148,7 +146,7 @@ #: 抽出しないファイル。ライブラリの公開APIではないもの。 EXCLUDED_FILES = { - # 適合性検証用の CLI。テストの一部であってライブラリの API ではない + # 適合性検証用のCLI。テストの一部であってライブラリのAPIではない "conformance", "Conformance", "ChunkReport", @@ -166,10 +164,10 @@ class Api: - """1 言語ぶんの公開API。 + """1言語ぶんの公開API。 論理名(言語非依存)から、その言語での実際の名前へ引けるようにしておく。 - `docs/api/` の対応表はこの情報から生成する。 + `docs/api/`の対応表はこの情報から生成する。 """ def __init__(self, language: str) -> None: @@ -219,7 +217,7 @@ def add_member(self, type_name: str, member: str, actual: str = "", if member in INTERNAL_MEMBERS or len(member) == 0: return - # 制御構文を拾ってしまった場合に落とす。別名で寄せた with は残す + # 制御構文を拾ってしまった場合に落とす。別名で寄せたwithは残す if member in KEYWORDS and member not in MEMBER_ALIASES.values(): return @@ -293,7 +291,7 @@ def add_base(self, type_name: str, base_name: str) -> None: def resolve_inheritance(self) -> None: """継承元のメンバを継承先へ写し、内部専用の基底型を落とす。 - `_ScalarTag` のように利用者へ見せない基底へメンバをまとめている言語がある。 + `_ScalarTag`のように利用者へ見せない基底へメンバをまとめている言語がある。 論理APIとしては継承先が持っているのと同じなので、写してから基底を消す。 """ # 継承の連鎖をたどるため、変化がなくなるまで繰り返す @@ -319,13 +317,12 @@ def resolve_inheritance(self) -> None: self.type_names.pop(type_name, None) def merge_setters(self) -> None: - """`set_foo` と `foo` が両方ある型では、`set_foo` を落とす。 + """`set_foo`と`foo`が両方ある型では、`set_foo`を落とす。 - Java はオプション型に `setFoo()` を生やすが、他言語はプロパティへ - 直接代入する。論理的には同じ「設定子」なので、取得子の行にまとめる - (docs/spec/00-conventions.md 3章)。 + Javaはオプション型に`setFoo()`を生やすが、他言語はプロパティへ + 直接代入する。論理的には同じ「設定子」なので、取得子の行にまとめる。 - `set_block` のように対になる取得子が無いものは、独立したメソッドなので残す。 + `set_block`のように対になる取得子が無いものは、独立したメソッドなので残す。 """ for members in self.members.values(): # 対になる取得子があるものだけを設定子とみなす @@ -348,9 +345,9 @@ def to_json(self): def to_snake(name: str) -> str: - """PascalCase / camelCase を snake_case へ写す。 + """PascalCase / camelCaseをsnake_caseへ写す。 - 数字を含む語(`Mutf8` や `LZ4`)でも語の切れ目を保てるよう、 + 数字を含む語(`Mutf8`や`LZ4`)でも語の切れ目を保てるよう、 小文字または数字のあとの大文字を境界とみなす。 """ return _CAMEL_BOUNDARY.sub("_", name).lower() @@ -359,23 +356,22 @@ def to_snake(name: str) -> str: def normalize_member(name: str) -> str: """メンバ名を論理名(snake_case)へ写す。 - 接頭辞は落とさない。`get_int` のような「キーを指定して型付きで取り出す」 - 取得子は、どの言語でも論理名に `get` を含む - (docs/spec/00-conventions.md 3章の「取得子」は - `data_version` のような引数なしのアクセサを指す)。 + 接頭辞は落とさない。`get_int`のような「キーを指定して型付きで取り出す」 + 取得子は、どの言語でも論理名に`get`を含む + (命名変換規則でいう「取得子」は、`data_version`のような引数なしのアクセサを指す)。 """ return to_snake(name) def is_constant(name: str) -> bool: - """SCREAMING_SNAKE_CASE の定数か。""" + """SCREAMING_SNAKE_CASEの定数か。""" return name.isupper() and "_" in name or (name.isupper() and len(name) > 1) class DocCollector: """直前のドキュメントコメントを溜めておき、宣言行で受け渡す。 - 1 行形式(`/// 説明`)と + 1行形式(`/// 説明`)と 複数行形式(`/// ` / `/// 説明` / `/// `)の両方を扱う。 """ @@ -389,7 +385,7 @@ def __init__(self, inline, body, open_tag: str, close_tag: str) -> None: self.parts = [] def feed(self, line: str) -> bool: - """コメント行なら溜めて True を返す。宣言行なら False。""" + """コメント行なら溜めてTrueを返す。宣言行ならFalse。""" match = self.body.match(line) if match is None: @@ -398,7 +394,7 @@ def feed(self, line: str) -> bool: text = match.group(1).strip() inline_match = self.inline.search(line) - # 説明 が 1 行に収まっている場合 + # 説明が1行に収まっている場合 if inline_match is not None: self.pending = clean_doc(inline_match.group(1)) self.collecting = False @@ -411,7 +407,7 @@ def feed(self, line: str) -> bool: if text.startswith(self.close_tag): self.collecting = False - # 概要は 1 行目だけを使う 2 行目以降は補足なので表に載せない + # 概要は1行目だけを使う。2行目以降は補足なので表に載せない if len(self.parts) > 0: self.pending = clean_doc(self.parts[0]) else: @@ -431,7 +427,7 @@ def take(self) -> str: def split_parameters(text: str): - """`A a, B b` のような引数リストから、引数名だけを取り出す。""" + """`A a, B b`のような引数リストから、引数名だけを取り出す。""" names = [] for part in text.split(","): @@ -445,7 +441,7 @@ def split_parameters(text: str): def with_parens(line: str, name: str) -> str: - """宣言行を見て、メソッドなら `名前()`、プロパティなら `名前` を返す。""" + """宣言行を見て、メソッドなら`名前()`、プロパティなら`名前`を返す。""" tail = line[line.index(name) + len(name):] # 名前の直後が ( ならメソッド @@ -456,11 +452,11 @@ def with_parens(line: str, name: str) -> str: def clean_doc(text: str) -> str: - """ドキュメントコメントを 1 行の説明文へ均す。""" - # は B を残す。単に消すと説明文が意味を失う + """ドキュメントコメントを1行の説明文へ均す。""" + # はBを残す。単に消すと説明文が意味を失う text = re.sub(r'', lambda match: match.group(1).split(".")[-1], text) - # 残りのタグ(... など)は囲みだけ落として中身を残す + # 残りのタグ(...など)は囲みだけ落として中身を残す text = re.sub(r"<[^>]+>", "", text) text = text.replace("|", "\\|").strip() @@ -480,10 +476,10 @@ def clean_doc(text: str) -> str: r"readonly\s+|const\s+|new\s+|partial\s+)*" r"(?:[A-Za-z0-9_<>,\[\]\?\. ]+?)\s+([A-Za-z0-9_]+)\s*[\(\{=;]") -#: `class NbtByte : NbtTag` の継承元。 +#: `class NbtByte : NbtTag`の継承元。 CSHARP_BASE = re.compile(r":\s*([A-Za-z0-9_<>,\s\.]+?)\s*$") -#: 本体の `{` を次の行に置くプロパティ(`public string Value` だけの行)。 +#: 本体の`{`を次の行に置くプロパティ(`public string Value`だけの行)。 CSHARP_MEMBER_BLOCK = re.compile( r"^\s{4}public\s+(?:static\s+|virtual\s+|override\s+|abstract\s+|sealed\s+|" r"readonly\s+|new\s+)*" @@ -494,16 +490,16 @@ def clean_doc(text: str) -> str: CSHARP_ENUM_VALUE = re.compile(r"^\s{4}([A-Z][A-Za-z0-9_]*)\s*(?:=\s*[^,]+)?,?\s*$") -#: C# の XML ドキュメントコメントから概要を取り出す。 +#: C#のXMLドキュメントコメントから概要を取り出す。 CSHARP_SUMMARY_INLINE = re.compile(r"///\s*(.*?)") CSHARP_SUMMARY_BODY = re.compile(r"^\s*///\s?(.*)$") def extract_csharp() -> Api: - """C# のソースから公開APIを抜き出す。 + """C#のソースから公開APIを抜き出す。 - C# は基準実装なので、ここでだけ概要(``)も集める。 - `docs/api/` の説明文はこれを使う。 + C#は基準実装なので、ここでだけ概要(``)も集める。 + `docs/api/`の説明文はこれを使う。 """ api = Api("csharp") root = os.path.join(REPO_ROOT, "csharp", "src", "SpringNBTLibrary") @@ -513,7 +509,7 @@ def extract_csharp() -> Api: in_enum = False doc = DocCollector(CSHARP_SUMMARY_INLINE, CSHARP_SUMMARY_BODY, "", "") - # Foo.Bar.cs は partial の続き。型の概要は本体(Foo.cs)から採る + # Foo.Bar.csはpartialの続き。型の概要は本体(Foo.cs)から採る is_partial_continuation = os.path.basename(path).count(".") > 1 # ファイルを上から読み、型に入ったらそのメンバを拾う @@ -533,7 +529,7 @@ def extract_csharp() -> Api: api.add_type(current, summary) - # record は等値比較と文字列表現を自動で持つ + # recordは等値比較と文字列表現を自動で持つ if " record " in line: api.add_member(current, "equals", "Equals()") api.add_member(current, "to_string", "ToString()") @@ -551,7 +547,7 @@ def extract_csharp() -> Api: doc.take() continue - # enum の値は修飾子を持たないので別に拾う + # enumの値は修飾子を持たないので別に拾う if in_enum: enum_match = CSHARP_ENUM_VALUE.match(line) @@ -594,10 +590,10 @@ def extract_csharp() -> Api: r"^\s{4}public\s+(?:static\s+|final\s+|abstract\s+|synchronized\s+)*" r"(?:[A-Za-z0-9_<>,\[\]\?\. ]+?)\s+([A-Za-z0-9_]+)\s*[\(=;]") -#: `class NbtByte implements NbtTag` / `extends X` の継承元。 +#: `class NbtByte implements NbtTag` / `extends X`の継承元。 JAVA_BASE = re.compile(r"\b(?:extends|implements)\s+([A-Za-z0-9_<>,\s\.]+?)\s*(?:\{|$)") -#: interface のメソッド宣言。暗黙に public なので修飾子が無い。 +#: interfaceのメソッド宣言。暗黙にpublicなので修飾子が無い。 JAVA_INTERFACE_MEMBER = re.compile( r"^\s{4}(?:[A-Za-z0-9_<>,\[\]\?\.]+)\s+([a-z][A-Za-z0-9_]*)\s*\(") @@ -605,12 +601,12 @@ def extract_csharp() -> Api: JAVA_ENUM_VALUE = re.compile(r"^\s{4}([A-Z][A-Z0-9_]*)\s*(?:\([^)]*\))?\s*[,;]?\s*$") -#: record のコンポーネント。`record RawChunk(A a, B b)` は a() / b() が生える。 +#: recordのコンポーネント。`record RawChunk(A a, B b)`はa() / b()が生える。 JAVA_RECORD = re.compile(r"^public\s+record\s+[A-Za-z0-9_]+\s*\(([^)]*)\)") def extract_java() -> Api: - """Java のソースから公開APIを抜き出す。""" + """Javaのソースから公開APIを抜き出す。""" api = Api("java") root = os.path.join(REPO_ROOT, "java", "src", "main", "java") @@ -634,14 +630,14 @@ def extract_java() -> Api: api.add_type(current) - # record は括弧の中のコンポーネントがそのままアクセサになる + # recordは括弧の中のコンポーネントがそのままアクセサになる record_match = JAVA_RECORD.match(line) if record_match is not None: for component in split_parameters(record_match.group(1)): api.add_member(current, to_snake(component), component + "()") - # record は等値比較と文字列表現を自動で持つ + # recordは等値比較と文字列表現を自動で持つ api.add_member(current, "equals", "equals()") api.add_member(current, "to_string", "toString()") @@ -653,7 +649,7 @@ def extract_java() -> Api: if JAVA_CONSTRUCTOR.match(line) is not None: continue - # enum の値は public 修飾子を持たないので別に拾う + # enumの値はpublic修飾子を持たないので別に拾う enum_match = JAVA_ENUM_VALUE.match(line) if enum_match is not None: @@ -663,7 +659,7 @@ def extract_java() -> Api: member_match = JAVA_MEMBER.match(line) - # interface のメソッドは暗黙に public なので修飾子が無い + # interfaceのメソッドは暗黙にpublicなので修飾子が無い if member_match is None and in_interface: member_match = JAVA_INTERFACE_MEMBER.match(line) @@ -681,7 +677,7 @@ def extract_java() -> Api: TS_TYPE = re.compile( r"^export\s+(?:abstract\s+)?(?:class|interface|enum)\s+([A-Za-z0-9_]+)") -#: `export class X extends Y` の継承元。 +#: `export class X extends Y`の継承元。 TS_BASE = re.compile(r"\bextends\s+([A-Za-z0-9_]+)") #: 列挙のメソッド代わりに、型名を接頭辞に付けたモジュール関数を置いている型。 @@ -698,21 +694,21 @@ def extract_java() -> Api: TS_INTERFACE_FIELD = re.compile(r"^ ([A-Za-z0-9_]+)\??\s*:") -#: constructor(readonly foo: T, ...) の形で公開されるフィールド。 +#: constructor(readonly foo: T, ...)の形で公開されるフィールド。 TS_PARAMETER_PROPERTY = re.compile( r"^ (?:public\s+readonly\s+|readonly\s+|public\s+)([A-Za-z0-9_]+)\s*:") -#: `constructor(public value: Int8Array)` のように 1 行に収めた引数プロパティ。 +#: `constructor(public value: Int8Array)`のように1行に収めた引数プロパティ。 TS_INLINE_PARAMETER_PROPERTY = re.compile( r"^(?:public\s+readonly\s+|readonly\s+|public\s+)([A-Za-z0-9_]+)\s*:") #: クラス直下の公開フィールド(`readonly type = TagType.Byte as const;`)。 -#: `static readonly OVERWORLD = "..."` のような定数もここで拾う。 +#: `static readonly OVERWORLD = "..."`のような定数もここで拾う。 TS_CLASS_FIELD = re.compile( r"^ (?:public\s+)?(?:static\s+)?readonly\s+([A-Za-z0-9_]+)\s*=") def extract_typescript() -> Api: - """TypeScript のソースから公開APIを抜き出す。""" + """TypeScriptのソースから公開APIを抜き出す。""" api = Api("typescript") api.add_type(MODULE_LEVEL) root = os.path.join(REPO_ROOT, "typescript", "src") @@ -749,8 +745,8 @@ def extract_typescript() -> Api: logical = normalize_member(name) owner = MODULE_LEVEL - # TypeScript の列挙にはメソッドを持たせられないので、 - # `tagTypeAsString` のように型名を接頭辞に付けた関数で代用している + # TypeScriptの列挙にはメソッドを持たせられないので、 + # `tagTypeAsString`のように型名を接頭辞に付けた関数で代用している # 論理的にはその型のメンバなので、型のほうへ寄せる for type_name in TS_ENUM_HELPER_OWNERS: prefix = to_snake(type_name) + "_" @@ -783,7 +779,7 @@ def extract_typescript() -> Api: continue - # interface のフィールドはオプション構造体の設定項目にあたる + # interfaceのフィールドはオプション構造体の設定項目にあたる if in_interface: field_match = TS_INTERFACE_FIELD.match(line) @@ -793,8 +789,8 @@ def extract_typescript() -> Api: continue - # constructor の引数に readonly を付けたものは公開フィールドになる。 - # private constructor でも引数プロパティは公開なので、除外より先に見る + # constructorの引数にreadonlyを付けたものは公開フィールドになる。 + # private constructorでも引数プロパティは公開なので、除外より先に見る if line.startswith(" constructor") or line.startswith(" private constructor"): # 引数リストが同じ行に収まっている場合は、その場で引数プロパティを拾う if ")" in line: @@ -865,7 +861,7 @@ def extract_typescript() -> Api: def extract_python() -> Api: - """Python のソースから公開APIを抜き出す。""" + """Pythonのソースから公開APIを抜き出す。""" api = Api("python") api.add_type(MODULE_LEVEL) root = os.path.join(REPO_ROOT, "python", "src", "spring_nbt_library") @@ -875,7 +871,7 @@ def extract_python() -> Api: is_property = False for line in read_lines(path): - # @property の次に来る def は呼び出しカッコが要らない + # @propertyの次に来るdefは呼び出しカッコが要らない if line.strip() == "@property": is_property = True continue @@ -926,7 +922,7 @@ def extract_python() -> Api: method_match = PY_METHOD.match(line) - # 等値比較は __eq__ で書くのが Python の作法 + # 等値比較は__eq__で書くのがPythonの作法 if method_match is not None and method_match.group(1) in PY_DUNDER_MEMBERS: api.add_member(current, PY_DUNDER_MEMBERS[method_match.group(1)], "==") is_property = False @@ -945,14 +941,14 @@ def extract_python() -> Api: is_property = False - # __init__ で self に代入する属性も公開メンバとして数える + # __init__でselfに代入する属性も公開メンバとして数える attribute_match = PY_ATTRIBUTE.match(line) if attribute_match is not None: api.add_member(current, attribute_match.group(1)) continue - # クラス直下で定める値(type = TagType.BYTE など)も公開メンバ + # クラス直下で定める値(type = TagType.BYTEなど)も公開メンバ field_match = PY_CLASS_FIELD.match(line) if field_match is not None: @@ -977,15 +973,15 @@ def extract_python() -> Api: RS_FUNCTION = re.compile(r"^pub\s+fn\s+([A-Za-z0-9_]+)") RS_CONST = re.compile(r"^pub\s+const\s+([A-Z][A-Z0-9_]*)\s*:") -#: impl の中に置いた関連定数(`pub const OVERWORLD: &'static str = ...`)。 +#: implの中に置いた関連定数(`pub const OVERWORLD: &'static str = ...`)。 RS_ASSOCIATED_CONST = re.compile(r"^ pub\s+const\s+([A-Z][A-Z0-9_]*)\s*:") -#: `scalar_accessors!(opt_int, get_int, ...)` のように、マクロで生成するアクセサ。 +#: `scalar_accessors!(opt_int, get_int, ...)`のように、マクロで生成するアクセサ。 #: 静的解析ではマクロを展開できないので、呼び出し行から名前だけ拾う。 RS_ACCESSOR_MACRO = re.compile( r"^([a-z_]+_accessors)!\(\s*([a-z_0-9]+)\s*,\s*([a-z_0-9]+)\s*,") -#: `scalar_setters!(set_int, Int, i32, "Int")` のように、マクロで生成する設定子。 +#: `scalar_setters!(set_int, Int, i32, "Int")`のように、マクロで生成する設定子。 RS_SETTER_MACRO = re.compile(r"^([a-z_]+_setters)!\(\s*([a-z_0-9]+)\s*,") RS_DERIVE = re.compile(r"^#\[derive\(([^)]*)\)\]") @@ -993,20 +989,20 @@ def extract_python() -> Api: RS_TRAIT_IMPL_NAMED = re.compile( r"^impl(?:<[^>]*>)?\s+(?:[A-Za-z0-9_]+::)*([A-Za-z0-9_]+)(?:<[^>]*>)?\s+for\s+([A-Za-z0-9_]+)") -#: derive / trait 実装で得られる論理メンバと、その Rust での書き方。 +#: derive / trait実装で得られる論理メンバと、そのRustでの書き方。 #: -#: Rust では等値比較も複製もトレイトで与えるので、`pub fn` としては現れない。 -#: 他言語の `equals` / `copy` と同じ機能なので、ここで論理APIへ写す。 +#: Rustでは等値比較も複製もトレイトで与えるので、`pub fn`としては現れない。 +#: 他言語の`equals` / `copy`と同じ機能なので、ここで論理APIへ写す。 RS_TRAIT_MEMBERS = { "Clone": ("copy", "clone()"), "PartialEq": ("equals", "=="), - # Display を実装すると to_string() が生える。Debug は {:?} なので別物 + # Displayを実装するとto_string()が生える。Debugは{:?}なので、to_string()とは別のもの "Display": ("to_string", "to_string()"), } def extract_rust() -> Api: - """Rust のソースから公開APIを抜き出す。""" + """Rustのソースから公開APIを抜き出す。""" api = Api("rust") api.add_type(MODULE_LEVEL) root = os.path.join(REPO_ROOT, "rust", "src") @@ -1019,7 +1015,7 @@ def extract_rust() -> Api: for line in read_lines(path): derive_match = RS_DERIVE.match(line) - # derive は直後の型定義にかかるので、いったん覚えておく + # deriveは直後の型定義にかかるので、いったん覚えておく if derive_match is not None: derives = {name.strip() for name in derive_match.group(1).split(",")} continue @@ -1031,7 +1027,7 @@ def extract_rust() -> Api: in_enum = line.startswith("pub enum") api.add_type(current) - # derive で与えられる equals / copy を論理APIに数える + # deriveで与えられるequals / copyを論理APIに数える for trait, (member, actual) in RS_TRAIT_MEMBERS.items(): if trait in derives: api.add_member(current, member, actual) @@ -1042,7 +1038,7 @@ def extract_rust() -> Api: derives = set() trait_match = RS_TRAIT_IMPL_NAMED.match(line) - # 手書きの trait 実装からも equals / copy を拾う + # 手書きのtrait実装からもequals / copyを拾う if trait_match is not None and trait_match.group(1) in RS_TRAIT_MEMBERS: member, actual = RS_TRAIT_MEMBERS[trait_match.group(1)] api.add_member(trait_match.group(2), member, actual) @@ -1050,7 +1046,7 @@ def extract_rust() -> Api: in_enum = False continue - # そのほかの trait 実装は言語の作法なので論理APIに数えない + # そのほかのtrait実装は言語の作法なので論理APIに数えない if RS_TRAIT_IMPL.match(line) is not None: current = None in_enum = False @@ -1064,7 +1060,7 @@ def extract_rust() -> Api: api.add_type(current) continue - # マクロで生成するアクセサは NbtCompound のメンバとして数える + # マクロで生成するアクセサはNbtCompoundのメンバとして数える macro_match = RS_ACCESSOR_MACRO.match(line) if macro_match is not None: @@ -1113,7 +1109,7 @@ def extract_rust() -> Api: api.add_member(current, name, name + "()") continue - # impl の中の関連定数も公開APIに数える + # implの中の関連定数も公開APIに数える associated_match = RS_ASSOCIATED_CONST.match(line) if associated_match is not None: @@ -1151,7 +1147,7 @@ def source_files(root: str, suffix: str): stem = filename[:-len(suffix)] # 検証ツールと内部ヘルパ(_ 始まり)は公開APIではない。 - # ただし __init__.py はパッケージの入口なので対象に含める + # ただし__init__.pyはパッケージの起点なので対象に含める if stem in EXCLUDED_FILES: continue @@ -1165,7 +1161,7 @@ def source_files(root: str, suffix: str): def read_lines(path: str): - """ソースを 1 行ずつ返す。""" + """ソースを1行ずつ返す。""" with open(path, "r", encoding="utf-8") as handle: return handle.read().split("\n") diff --git a/spec/tools/release_notes.py b/spec/tools/release_notes.py index 8184ac2..fdd9b13 100644 --- a/spec/tools/release_notes.py +++ b/spec/tools/release_notes.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 """リリース本文を組み立てる。 -CHANGELOG.md からその版の節を抜き出し、毎回同じ案内文(`.github/release-common.md`) -と合わせて 1 つの本文にする。 +CHANGELOG.mdからその版の節を抜き出し、 +毎回同じ案内文(`.github/release-common.md`)と合わせて1つの本文にする。 -パッチノートの実体を CHANGELOG.md 側に置くのは、 -同じ内容を 2 か所へ書かせないため。 -CHANGELOG に節が無ければ失敗させるので、書き忘れたままリリースできない。 +パッチノートの実体をCHANGELOG.md側に置くのは、 +同じ内容を2か所へ書かせないため。 +CHANGELOGに節が無ければ失敗させるので、書き忘れたままリリースできない。 使い方: python3 spec/tools/release_notes.py 1.1.0 @@ -22,10 +22,10 @@ REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -#: 版ごとの節の見出し。`## [1.1.0]` の形。 +#: 版ごとの節の見出し。`## [1.1.0]`の形。 SECTION = re.compile(r"^## \[([^\]]+)\]\s*$") -#: 節の終わりとみなす見出し。版以外の `## 〜` でも区切る。 +#: 節の終わりとみなす見出し。版以外の`## 〜`でも区切る。 HEADING = re.compile(r"^## ") #: リポジトリ内への相対リンク。リリースのページからは辿れないので絶対URLへ直す。 @@ -41,7 +41,7 @@ def read_text(path: str) -> str: def extract_section(changelog: str, version: str): - """CHANGELOG から指定した版の節の中身を取り出す。無ければ None。""" + """CHANGELOGから指定した版の節の中身を取り出す。無ければNone。""" lines = changelog.split("\n") start = None diff --git a/spec/tools/run_conformance.py b/spec/tools/run_conformance.py index 80b86a5..f443642 100644 --- a/spec/tools/run_conformance.py +++ b/spec/tools/run_conformance.py @@ -1,20 +1,24 @@ #!/usr/bin/env python3 """全言語の適合性検証を走らせる。 -仕様: docs/spec/90-conformance.md - -やること: +次のことを行う。 1. 各言語の検証ツールを起動できるようにビルドする - 2. 全テストベクタについて、各言語で decode / encode / snbt を実行する - 3. decode の結果を expect/ の正規化JSON と突き合わせる - 4. encode の結果を展開後の入力バイト列と突き合わせる(ラウンドトリップ) - 5. 言語どうしの出力を相互に diff する + 2. NBTのテストベクタについて、各言語でdecode / encode / snbtを実行する + 3. decodeの結果をexpect/の正規化JSONと比べる + 4. encodeの結果を展開後の入力バイト列と比べる(ラウンドトリップ) + 5. 連なったNBTのテストベクタについて、各言語でnbt-listを実行する + 6. リージョンのテストベクタについて、各言語でregion-list / region-rewriteを実行し、 + 詰め直した結果がセクタ境界に揃っていることと、読み直しても中身が変わらないことを確かめる + 7. チャンクのテストベクタについて、各言語でchunk-report / chunk-editを実行し、 + 編集した結果を読み直せることを確かめる + 8. エラーになるはずのテストベクタでは、出力されたエラーコードを確かめる + 9. 言語どうしの出力を相互にdiffする 使い方: python3 spec/tools/run_conformance.py # 全言語を検証 python3 spec/tools/run_conformance.py --only csharp # 言語を絞る python3 spec/tools/run_conformance.py --generate-expect - 基準実装 (C#) の出力から expect/ を作り直す + 基準実装(C#)の出力からexpect/を作り直す """ from __future__ import annotations @@ -33,14 +37,14 @@ TESTDATA = os.path.join(REPO_ROOT, "spec", "testdata") MANIFEST = os.path.join(TESTDATA, "manifest.json") -# 基準実装。expect/ の生成元になる(docs/adr/0002-idiomatic-naming.md) +# 基準実装。expect/の生成元になる REFERENCE_LANGUAGE = "csharp" LANGUAGE_ORDER = ["csharp", "java", "typescript", "python", "rust"] class Runner: - """1 言語ぶんの検証ツールを起動する。""" + """1言語ぶんの検証ツールを起動する。""" def __init__(self, name, build_command, invoke_command): self.name = name @@ -64,10 +68,10 @@ def build(self): return True def run(self, args): - """検証ツールを起動し、(終了コード, 標準エラー) を返す。""" + """検証ツールを起動し、(終了コード, 標準エラー)を返す。""" environment = dict(os.environ) - # Python 版はインストールせずソースツリーから直接動かせるようにする + # Python版はインストールせずソースツリーから直接動かせるようにする if self.name == "python": environment["PYTHONPATH"] = os.path.join(REPO_ROOT, "python", "src") @@ -131,7 +135,7 @@ def build_runners(): def decompress(data, method): - """マニフェストの compression に従って展開する。""" + """マニフェストのcompressionに従って展開する。""" if method == "gzip": return gzip.decompress(data) @@ -175,7 +179,7 @@ def summary(self): def run_vector(runner, vector, workdir, report, outputs): - """1 言語 × 1 ベクタを検証する。ベクタの種別で扱いを分ける。""" + """1言語 × 1ベクタを検証する。ベクタの種別で扱いを分ける。""" if vector.get("kind") == "anvil": run_anvil_vector(runner, vector, workdir, report, outputs) return @@ -271,7 +275,7 @@ def run_vector(runner, vector, workdir, report, outputs): def run_concat_vector(runner, vector, workdir, report, outputs): - """連なった NBT のベクタを検証する。 + """連なったNBTのベクタを検証する。 期待値ファイルは持たず、言語間の一致だけを見る。 出力には位置を指定した読み込みの結果も含めるので、 @@ -295,10 +299,10 @@ def run_concat_vector(runner, vector, workdir, report, outputs): def run_world_vector(runner, vector, workdir, report, outputs): - """チャンク(World レイヤ)のベクタを検証する。 + """チャンク(Worldレイヤ)のベクタを検証する。 全ブロックを走査した集計と、決まった手順で編集した結果のバイト列を - 言語間で突き合わせる。パレットとビット詰めが 1 か所でもずれれば必ず出る。 + 言語間で比べる。パレットとビット詰めに1か所でもずれがあれば、必ず見つかる。 """ vector_id = vector["id"] input_path = os.path.join(TESTDATA, vector["input"]) @@ -344,7 +348,7 @@ def run_world_vector(runner, vector, workdir, report, outputs): read_bytes(edited_path) report.ok() - # 編集した結果をもう一度読めること。壊れた出力を作っていないかの確認 + # 編集した結果をもう一度読めること。おかしな出力を作っていないかの確認 reread_path = prefix + ".reread.txt" code, stderr = runner.run(["chunk-report", edited_path, reread_path]) @@ -359,18 +363,18 @@ def run_world_vector(runner, vector, workdir, report, outputs): def strip_storage_fields(listing): - """region-list の出力から「格納のしかた」に依る項目を落とす。 + """region-listの出力から「格納のしかた」に依る項目を落とす。 - 行の形は「絶対X 絶対Z タイムスタンプ 圧縮方式 圧縮後バイト数 展開後バイト数 キー数」。 + 行の形は「絶対X 絶対Z タイムスタンプ 圧縮方式 圧縮後バイト数 展開後バイト数 ルート直下キー数」。 詰め直すと圧縮方式と圧縮後バイト数は当然変わるので、 - 中身が保たれたかを見たいときはこの 2 つを外して比べる。 + 中身が保たれたかを見たいときはこの2つを外して比べる。 """ lines = [] for line in listing.splitlines(): fields = line.split(" ") - # チャンク行だけが 7 項目。region / total の行はそのまま残す + # チャンク行だけが7項目。region / totalの行はそのまま残す if len(fields) == 7: lines.append(" ".join([fields[0], fields[1], fields[2], fields[5], fields[6]])) else: @@ -382,15 +386,15 @@ def strip_storage_fields(listing): def run_anvil_vector(runner, vector, workdir, report, outputs): """リージョンファイルのベクタを検証する。 - NBT ベクタと違い期待値ファイルは持たず、言語間の一致だけを見る。 + NBTベクタと違い期待値ファイルは持たず、言語間の一致だけを見る。 セクタ確保のロジックは実装ごとに書き方が変わりやすいので、 - 「詰め直した結果のバイト列」が全言語で同じになることを要にする。 + 「詰め直した結果のバイト列」が全言語で同じになるかどうかを中心に見る。 """ vector_id = vector["id"] input_path = os.path.join(TESTDATA, vector["input"]) prefix = os.path.join(workdir, "%s_%s" % (runner.name, vector_id.replace("/", "_"))) - # 詰め直した結果も r.X.Z.mca でなければ座標を読み取れないので、専用のディレクトリへ置く + # 詰め直した結果もr.X.Z.mcaでなければ座標を読み取れないので、専用のディレクトリへ置く rewrite_dir = prefix + ".out" os.makedirs(rewrite_dir, exist_ok=True) rewritten_path = os.path.join(rewrite_dir, os.path.basename(input_path)) @@ -450,7 +454,7 @@ def run_anvil_vector(runner, vector, workdir, report, outputs): % (runner.name, vector_id, stderr.strip())) return - # 詰め直しでは圧縮方式が無圧縮に変わるため、その 2 項目を外して比べる + # 詰め直しでは圧縮方式が無圧縮に変わるため、その2項目を外して比べる if strip_storage_fields(read_text(relist_path)) != strip_storage_fields(read_text(list_path)): report.fail("%s / %s: 詰め直しの前後でチャンクの中身が変わった" % (runner.name, vector_id)) return @@ -459,7 +463,7 @@ def run_anvil_vector(runner, vector, workdir, report, outputs): def compare_languages(outputs, report): - """同じベクタに対する言語ごとの出力を相互に突き合わせる。""" + """同じベクタに対する言語ごとの出力を相互に比べる。""" for vector_id in sorted(outputs.keys()): for kind in sorted(outputs[vector_id].keys()): by_language = outputs[vector_id][kind] @@ -481,14 +485,14 @@ def compare_languages(outputs, report): def generate_expect(runner, vectors, report): - """基準実装の出力から expect/ を作り直す。""" + """基準実装の出力からexpect/を作り直す。""" generated = 0 for vector in vectors: if vector.get("expect_error") is not None: continue - # Anvil / World / 連結 のベクタは正規化JSON の期待値を持たない + # Anvil / World / 連結のベクタは正規化JSONの期待値を持たない if vector.get("kind") in ("anvil", "world", "concat"): continue diff --git a/spec/tools/scan_world.py b/spec/tools/scan_world.py index 9e67098..cb5624b 100644 --- a/spec/tools/scan_world.py +++ b/spec/tools/scan_world.py @@ -1,23 +1,22 @@ #!/usr/bin/env python3 """実ワールドを読み取り専用で走査し、実データ耐性を確かめる。 -合成のテストベクタだけでは、実際の Minecraft が書き出すデータの網羅性に届かない。 +合成のテストベクタだけでは、実際のMinecraftが書き出すデータを網羅できない。 このツールは手元のワールドを丸ごと読んで、次を確認する。 - 1. すべての `.dat` / `.nbt` を読み、ラウンドトリップ(読む→書く→バイト一致)を検証 - 2. すべての `.mca` のヘッダを解析し、全チャンクを展開して NBT として読み、同じく検証 + 1. すべての`.dat` / `.nbt`を読み、ラウンドトリップ(読む→書く→バイト一致)を検証 + 2. すべての`.mca`のヘッダを解析し、無変更で書き戻した結果が原本とバイト一致するかを検証。 + さらに全チャンクを展開してNBTとして読み、同じくラウンドトリップを検証 3. 使われている圧縮方式・チャンク構造の統計を出す(仕様書とのズレを見つけるため) - 4. World レイヤ(`MinecraftWorld` / `Chunk` / `PalettedContainer`)で解釈し直し、 - NBT へ書き戻したものが元と一致するか、ブロックが読み出せるかを検証 + 4. Worldレイヤ(`MinecraftWorld` / `Chunk` / `PalettedContainer`)で解釈し直し、 + NBTへ書き戻したものが元と一致するか、ブロックが読み出せるかを検証 **このツールは一切書き込まない。** 引数のワールドは読み取りのみで開く。 -とはいえ Minecraft が起動していない状態で実行すること(書き込み途中を読む可能性があるため)。 +とはいえMinecraftが起動していない状態で実行すること(書き込み途中を読む可能性があるため)。 使い方: python3 spec/tools/scan_world.py "<ワールドのパス>" python3 spec/tools/scan_world.py "<ワールドのパス>" --verbose - -仕様: docs/spec/90-conformance.md """ from __future__ import annotations @@ -234,9 +233,9 @@ def collect_chunk_stats(self, root: NbtCompound, kind: str) -> None: def check_paletted_container(self, section: NbtCompound, key: str, entry_count: int, min_bits: int) -> None: - """パレットの長さから求めたビット幅と、実際の data 長が一致するか確かめる。 + """パレットの長さから求めたビット幅と、実際のdata長が一致するか確かめる。 - 仕様 31 の計算式が実データに合っているかを直接検証する部分。 + ビット幅の計算式が実データに合っているかを直接検証する部分。 """ container = section.opt_compound(key) @@ -256,7 +255,7 @@ def check_paletted_container(self, section: NbtCompound, key: str, data = container.opt_long_array("data") if data is None: - # パレットが 1 要素なら data は無くてよい + # パレットが1要素ならdataは無くてよい if len(palette) != 1: self.failures.append( "%s: palette=%d なのに data が無い" % (key, len(palette))) @@ -334,14 +333,14 @@ def report(self) -> int: class WorldScanner: - """World レイヤを通してワールドを読み直し、解釈が正しいかを確かめる。 + """Worldレイヤを通してワールドを読み直し、解釈が正しいかを確かめる。 - Anvil レイヤの走査は「NBT として読めるか」までしか見ない。 - ここではさらに `Chunk` / `PalettedContainer` を通して、 + Anvilレイヤの走査は「NBTとして読めるか」までしか見ない。 + ここではさらに`Chunk` / `PalettedContainer`を通して、 パレットとビット詰めの解釈・再エンコードが元データと一致するかを確認する。 """ - #: 全ブロックを 1 つずつ読む対象のチャンク数の既定値。全チャンクだと時間がかかりすぎる。 + #: 全ブロックを1つずつ読む対象のチャンク数の既定値。全チャンクだと時間がかかりすぎる。 DEFAULT_BLOCK_SAMPLE = 40 def __init__(self, verbose: bool, block_sample: int) -> None: @@ -355,7 +354,7 @@ def __init__(self, verbose: bool, block_sample: int) -> None: def scan(self, directory: str) -> None: """ワールドを読み取り専用で開いて走査する。""" - # writable=False なので、この先どの経路でも書き込みは起きない + # writable=Falseなので、この先どの経路でも書き込みは起きない world = MinecraftWorld.open(directory, WorldOpenOptions(writable=False)) try: @@ -375,12 +374,12 @@ def scan(self, directory: str) -> None: world.close() def _scan_level(self, world: MinecraftWorld) -> None: - """level.dat と、そこから分離されたデータファイルを読む。""" + """level.datと、そこから分離されたデータファイルを読む。""" level = world.level self.stats["level_data_version=%d" % level.data_version] += 1 self.stats["level_version=%s" % level.version_name] += 1 - # 26.x で level.dat から分離された各ファイルが読めるか確かめる + # 26.xでlevel.datから分離された各ファイルが読めるか確かめる for name in ("game_rules", "weather", "world_gen_settings", "world_clocks", "random_sequences", "scoreboard", "custom_boss_events"): if world.data_file(name) is not None: @@ -393,12 +392,12 @@ def _scan_level(self, world: MinecraftWorld) -> None: self.stats["player_files"] += 1 def _scan_dimension(self, dimension_id: str, dimension) -> None: - """1 つの次元の全チャンクを World レイヤで解釈する。""" + """1つの次元の全チャンクをWorldレイヤで解釈する。""" positions = dimension.chunk_positions() self.stats["dimension_chunks"] += len(positions) sampled = 0 - # チャンクを 1 つずつ読み、再エンコードが元と一致するかを見る + # チャンクを1つずつ読み、再エンコードが元と一致するかを見る for position in positions: label = "%s (%d, %d)" % (dimension_id, position.x, position.z) folder = dimension.region_folder() @@ -430,12 +429,12 @@ def _scan_dimension(self, dimension_id: str, dimension) -> None: self.stats["block_sampled_chunks"] += sampled def _check_roundtrip(self, label: str, original: NbtCompound, chunk: Chunk) -> bool: - """World レイヤで読んで書き戻した結果が、元の NBT と一致するかを見る。 + """Worldレイヤで読んで書き戻した結果が、元のNBTと一致するかを見る。 セクションは毎回パレットとビット詰めを組み直して書き出されるので、 これが一致するならエンコード側の解釈も正しいことになる。 """ - # to_nbt は raw を書き換えるので、比較用のバイト列は先に取っておく + # to_nbtはrawを書き換えるので、比較用のバイト列は先に取っておく expected = write_bytes(NamedTag("", original), WRITE_OPTIONS) try: @@ -456,15 +455,15 @@ def _check_roundtrip(self, label: str, original: NbtCompound, chunk: Chunk) -> b return True def _walk_blocks(self, label: str, chunk: Chunk) -> None: - """チャンク内のブロックとバイオームを 1 つずつ読み出す。 + """チャンク内のブロックとバイオームを1つずつ読み出す。 ビット詰めの取り出しを端から端まで通すので、 - どこか 1 か所でも境界の扱いを誤っていれば例外か異常な値になる。 + どこか1か所でも境界の扱いを誤っていれば例外か異常な値になる。 """ for section_y in chunk.section_ys: base = section_y * 16 - # セクション内の 16×16×16 をすべて読む + # セクション内の16×16×16をすべて読む for y in range(16): for z in range(16): for x in range(16): @@ -473,7 +472,7 @@ def _walk_blocks(self, label: str, chunk: Chunk) -> None: if block is not None: self.blocks[block.name] += 1 - # バイオームは 4×4×4 単位なので 4 ブロックおきに読む + # バイオームは4×4×4単位なので4ブロックおきに読む for y in range(0, 16, 4): for z in range(0, 16, 4): for x in range(0, 16, 4): @@ -542,10 +541,10 @@ def _decompress_for_scan(raw): def ceil_log2(value: int) -> int: - """value 個の値を表すのに必要な最小ビット数。value == 1 なら 0。""" + """value個の値を表すのに必要な最小ビット数。value == 1なら0。""" bits = 0 - # 1 を超える分だけシフトして数える + # 1を超える分だけシフトして数える while (1 << bits) < value: bits += 1 @@ -605,7 +604,7 @@ def main() -> int: status = scanner.report() - # World レイヤでも読み直して、パレットとビット詰めの解釈を確かめる + # Worldレイヤでも読み直して、パレットとビット詰めの解釈を確かめる world_scanner = WorldScanner(args.verbose, args.block_sample) try: diff --git a/typescript/README.md b/typescript/README.md index 263a334..f97d3c5 100644 --- a/typescript/README.md +++ b/typescript/README.md @@ -1,18 +1,19 @@ -# SpringNBTLibrary(TypeScript 版) +# SpringNBTLibrary(TypeScript版) -Minecraft Java版 26.1 以降のワールド・NBTファイルを読み書きするライブラリです。 -Node.js 20 以上、TypeScript 5.7 以上で動きます。ESM 専用です。 +Minecraft Java版26.1以降のワールド・NBTファイルを読み書きするライブラリです。 +Node.js 20以上で動きます。ESM専用です。 +型定義を同梱しており、TypeScriptから使う場合は5.7以上を推奨します。 -いまのワールド形式は 26.1 で入ったものです。 +いまのワールド形式は26.1で入ったものです。 それ以降のバージョンなら、形式が変わらない限りそのまま扱えます。 -C# / Java / TypeScript / Python / Rust 版があり、どれも同じ仕様書から作っています。 +C# / Java / TypeScript / Python / Rust版があり、どれも同じ仕様書から作っています。 同じ入力からは同じ結果が出ます([対応言語の一覧](../README.md#対応言語))。 -揃っているかは[適合性テスト](../docs/spec/90-conformance.md)で毎回確かめています。 +結果が揃っているかは[適合性テスト](../docs/spec/90-conformance.md)で毎回確かめています。 ## 導入 -[Releases](https://github.com/ScriptArts/SpringNBTLibrary/releases) から `spring-nbt-library-<版>.tgz` を落として入れます。 +[Releases](https://github.com/ScriptArts/SpringNBTLibrary/releases)から`spring-nbt-library-<版>.tgz`を落として入れます。 ```bash npm install ./spring-nbt-library-1.0.0.tgz @@ -22,12 +23,12 @@ npm install ./spring-nbt-library-1.0.0.tgz ## 使い方 -- [はじめに(TypeScript)](../docs/getting-started/typescript.md) — まずここから -- [ガイド](../docs/guide/01-nbt.md) — 目的別の使い方 -- [API 対応表](../docs/api/overview.md) — 他言語版との対応 -- [機能一覧](../docs/features.md) — 何ができて何ができないか +- [はじめに(TypeScript)](../docs/getting-started/typescript.md): まずここから +- [ガイド](../docs/guide/01-nbt.md): 目的別の使い方 +- [API対応表](../docs/api/overview.md): 他言語版との対応 +- [機能一覧](../docs/features.md): 何ができて何ができないか -説明は [`docs/`](../docs/README.md) にまとめてあります。 +説明は[`docs/`](../docs/README.md)にまとめてあります。 ## ライセンス diff --git a/typescript/package.json b/typescript/package.json index 53949c1..6e8173b 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -1,7 +1,7 @@ { "name": "spring-nbt-library", "version": "1.0.0", - "description": "Minecraft Java Edition の NBT / Anvil ワールドデータを読み書きするライブラリ", + "description": "Minecraft Java EditionのNBT / Anvilワールドデータを読み書きするライブラリ", "license": "MIT", "type": "module", "main": "./dist/src/index.js", diff --git a/typescript/src/anvil/compression.ts b/typescript/src/anvil/compression.ts index 9a118c9..06254e9 100644 --- a/typescript/src/anvil/compression.ts +++ b/typescript/src/anvil/compression.ts @@ -1,10 +1,8 @@ /** * リージョンファイル内でチャンクに使われる圧縮方式 * - * NBT 層の `Compression` とは別物であることに注意 - * あちらはファイル全体の圧縮を表し、こちらはリージョン内の 1 チャンクに付く 1 バイトのIDを表す - * - * 仕様: `docs/spec/20-anvil-region.md` 3.1章 + * NBT層の`Compression`とは違うものであることに注意 + * あちらはファイル全体の圧縮を表し、こちらはリージョン内の1チャンクに付く1バイトのIDを表す */ import { SpringNbtError } from "../errors.js"; @@ -21,14 +19,15 @@ export enum ChunkCompression { Gzip = 1, /** * Zlib (RFC 1950) - * Minecraft が実際に書き出す方式 + * Minecraftが実際に書き出す方式 */ Zlib = 2, /** 無圧縮 */ None = 3, /** - * LZ4(ブロック形式) - * 任意依存 + * LZ4(独自ヘッダ付きのブロック連結) + * 読み込みのみ対応 + * 展開は自前で行い、外部の依存は使わない */ Lz4 = 4, /** @@ -58,12 +57,12 @@ export function chunkCompressionAsString(compression: ChunkCompression): string } /** - * 圧縮方式IDから {@link ChunkCompression} を得る + * 圧縮方式IDから{@link ChunkCompression}を得る * * @throws {SpringNbtError} 未知のIDの場合 */ export function chunkCompressionFromId(id: number): ChunkCompression { - // 仕様が定めるのは 1・2・3・4・127 の 5 種類だけ + // 仕様が定めるのは1・2・3・4・127の5種類だけ if (LABELS.has(id as ChunkCompression)) { return id as ChunkCompression; } @@ -79,7 +78,10 @@ export class RawChunk { readonly external: boolean = false, ) {} - /** 人が読むための表現。中身の形式は決めていない */ + /** + * 人が読むための表現 + * 中身の形式は決めていない + */ toString(): string { return `RawChunk(${chunkCompressionAsString(this.compression)}, ` + `${this.data.length} バイト, external=${this.external})`; diff --git a/typescript/src/anvil/index.ts b/typescript/src/anvil/index.ts index 83b8ce2..2a9084b 100644 --- a/typescript/src/anvil/index.ts +++ b/typescript/src/anvil/index.ts @@ -1,7 +1,5 @@ /** - * Anvil のリージョンファイル (.mca) の読み書き - * - * 仕様: `docs/spec/20-anvil-region.md` + * Anvilのリージョンファイル(.mca)の読み書き */ export { ChunkPos, RegionPos } from "./pos.js"; diff --git a/typescript/src/anvil/lz4.ts b/typescript/src/anvil/lz4.ts index 60ff4e6..313c184 100644 --- a/typescript/src/anvil/lz4.ts +++ b/typescript/src/anvil/lz4.ts @@ -1,27 +1,24 @@ /** - * 圧縮方式ID 4 (LZ4) のチャンクを展開する + * 圧縮方式ID 4(LZ4)のチャンクを展開する * - * 素の LZ4 ブロックでも LZ4 フレーム形式でもなく、 - * 独自ヘッダを持つブロックの連結である + * 素のLZ4ブロックでもLZ4フレーム形式でもなく、独自ヘッダを持つブロックの連結である * * 書き込みには対応しない - * 書き戻すときは Zlib になる - * - * 仕様: `docs/spec/20-anvil-region.md` 3.1.1 / 3.1.2 + * NBTとして書き戻すときは既定でZlibになり、生バイトのまま書き戻すとLZ4のまま残る */ import { SpringNbtError } from "../errors.js"; -/** ブロックの先頭に必ず置かれる 8 バイト */ +/** ブロックの先頭に必ず置かれる8バイト */ const MAGIC = new Uint8Array([0x4c, 0x5a, 0x34, 0x42, 0x6c, 0x6f, 0x63, 0x6b]); /** ブロックヘッダの長さ */ const HEADER_LENGTH = 21; -/** トークン上位 4 ビット: 本体が無圧縮 */ +/** トークン上位4ビット: 本体が無圧縮 */ const METHOD_STORED = 0x10; -/** トークン上位 4 ビット: 本体が LZ4 圧縮 */ +/** トークン上位4ビット: 本体がLZ4圧縮 */ const METHOD_COMPRESSED = 0x20; /** マッチの最小長 */ @@ -33,9 +30,9 @@ interface Cursor { } /** - * LZ4Block の連結を展開する + * LZ4Blockの連結を展開する * - * @throws {SpringNbtError} 形式に反する入力 (MALFORMED_DATA) + * @throws {SpringNbtError} 形式に反する入力(MALFORMED_DATA) */ export function decompressLz4(payload: Uint8Array): Uint8Array { const blocks: Uint8Array[] = []; @@ -62,7 +59,7 @@ export function decompressLz4(payload: Uint8Array): Uint8Array { return output; } -/** ブロックを 1 つ展開し、中身と次のブロックの開始位置を返す */ +/** ブロックを1つ展開し、中身と次のブロックの開始位置を返す */ function decompressBlock(payload: Uint8Array, position: number): { data: Uint8Array; next: number } { if (position + HEADER_LENGTH > payload.length) { throw SpringNbtError.malformed( @@ -70,7 +67,7 @@ function decompressBlock(payload: Uint8Array, position: number): { data: Uint8Ar ); } - // マジックが違えばそもそも LZ4Block ではない + // マジックが違えばそもそもLZ4Blockではない for (let index = 0; index < MAGIC.length; index++) { if (payload[position + index] !== MAGIC[index]) { throw SpringNbtError.malformed("LZ4: ブロックが LZ4Block で始まっていない"); @@ -90,7 +87,7 @@ function decompressBlock(payload: Uint8Array, position: number): { data: Uint8Ar } if (method === METHOD_STORED) { - // 無圧縮なら 2 つの長さは一致していなければならない + // 無圧縮なら2つの長さは一致していなければならない if (compressedLength !== originalLength) { throw SpringNbtError.malformed( `LZ4: 無圧縮ブロックの長さが食い違う(${compressedLength} と ${originalLength})`, @@ -113,19 +110,19 @@ function decompressBlock(payload: Uint8Array, position: number): { data: Uint8Ar ); } -/** ヘッダに書かれた 2 つの長さが妥当か調べる */ +/** ヘッダに書かれた2つの長さが妥当か調べる */ function validateLengths(compressedLength: number, originalLength: number): void { if (compressedLength < 0 || originalLength < 0) { throw SpringNbtError.malformed("LZ4: ブロックの長さが負値"); } - // 片方だけが 0 になることはない + // 片方だけが0になることはない if ((compressedLength === 0) !== (originalLength === 0)) { throw SpringNbtError.malformed("LZ4: ブロックの長さが片方だけ 0"); } } -/** 素の LZ4 ブロックを展開する */ +/** 素のLZ4ブロックを展開する */ function decompressRawBlock(source: Uint8Array, originalLength: number): Uint8Array { const output = new Uint8Array(originalLength); const cursor: Cursor = { position: 0 }; @@ -138,7 +135,7 @@ function decompressRawBlock(source: Uint8Array, originalLength: number): Uint8Ar let literalLength = token >> 4; - // 15 なら追加バイトで長さが続く + // 15なら追加バイトで長さが続く if (literalLength === 15) { literalLength += readLength(source, cursor); } @@ -163,7 +160,7 @@ function decompressRawBlock(source: Uint8Array, originalLength: number): Uint8Ar let matchLength = (token & 0x0f) + MIN_MATCH; - // 下位 4 ビットが 15 なら追加バイトで長さが続く + // 下位4ビットが15なら追加バイトで長さが続く if ((token & 0x0f) === 15) { matchLength += readLength(source, cursor); } @@ -180,11 +177,11 @@ function decompressRawBlock(source: Uint8Array, originalLength: number): Uint8Ar return output; } -/** 255 が続く形式の追加長さを読む */ +/** 255が続く形式の追加長さを読む */ function readLength(source: Uint8Array, cursor: Cursor): number { let total = 0; - // 255 未満のバイトが出るまで足し続ける + // 255未満のバイトが出るまで足し続ける while (true) { if (cursor.position >= source.length) { throw SpringNbtError.malformed("LZ4: 長さの追加バイトが途中で切れた"); @@ -229,7 +226,7 @@ function copyMatch(output: Uint8Array, written: number, offset: number, length: const from = written - offset; - // コピー元と先は重なりうるので 1 バイトずつ写す + // コピー元と先は重なりうるので1バイトずつ写す for (let index = 0; index < length; index++) { output[written + index] = output[from + index]; } diff --git a/typescript/src/anvil/pos.ts b/typescript/src/anvil/pos.ts index 4e14a41..b4e9031 100644 --- a/typescript/src/anvil/pos.ts +++ b/typescript/src/anvil/pos.ts @@ -1,12 +1,10 @@ /** * チャンクとリージョンの座標 - * - * 仕様: `docs/spec/20-anvil-region.md` 1章 */ /** * リージョンの座標 - * 1リージョンは 32×32 チャンクを担当する + * 1リージョンは32×32チャンクを担当する */ export class RegionPos { constructor( @@ -30,13 +28,13 @@ export class RegionPos { } /** - * `r.X.Z.mca` 形式のファイル名から座標を得る - * 解釈できなければ undefined + * `r.X.Z.mca`形式のファイル名から座標を得る + * 解釈できなければundefined */ static fromFileName(fileName: string): RegionPos | undefined { const parts = fileName.split("."); - // "r" "" "" "mca" の 4 つに分かれるはず + // "r" "" "" "mca"の4つに分かれるはず if (parts.length !== 4 || parts[0] !== "r" || parts[3] !== "mca") { return undefined; } @@ -49,7 +47,10 @@ export class RegionPos { return new RegionPos(Number.parseInt(parts[1], 10), Number.parseInt(parts[2], 10)); } - /** 人が読むための表現。中身の形式は決めていない */ + /** + * 人が読むための表現 + * 中身の形式は決めていない + */ toString(): string { return `RegionPos(${this.x}, ${this.z})`; } @@ -91,7 +92,10 @@ export class ChunkPos { return other.x === this.x && other.z === this.z; } - /** 人が読むための表現。中身の形式は決めていない */ + /** + * 人が読むための表現 + * 中身の形式は決めていない + */ toString(): string { return `ChunkPos(${this.x}, ${this.z})`; } diff --git a/typescript/src/anvil/regionFile.ts b/typescript/src/anvil/regionFile.ts index fc6bc97..fa44851 100644 --- a/typescript/src/anvil/regionFile.ts +++ b/typescript/src/anvil/regionFile.ts @@ -1,16 +1,14 @@ /** - * Anvil のリージョンファイル (`r.X.Z.mca`) - * 32×32 チャンクを格納する + * Anvilのリージョンファイル (`r.X.Z.mca`) + * 32×32チャンクを格納する * * ファイル全体をメモリに読み込んで扱う - * 実データのリージョンは数 MB 程度で、 - * この方が「触っていないチャンクのバイト配置をそのまま保つ」ことを保証しやすい - * 開いて何も変えずに `flush()` すると、バイト単位で元と同じファイルになる - * - * 仕様: `docs/spec/20-anvil-region.md` + * 実データのリージョンは数MB程度で、この方が「触っていないチャンクのバイト配置をそのまま保つ」ことを保証しやすい + * 開いて何も変えずに`flush()`すると、バイト単位で元と同じファイルになる + * ただし空のファイルは、8KiBのヘッダだけのファイルになる */ -import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { basename, dirname, join } from "node:path"; import { deflateSync, gunzipSync, gzipSync, inflateSync, constants as zlibConstants } from "node:zlib"; @@ -40,7 +38,7 @@ const HEADER_SECTORS = 2; /** 1リージョンに入るチャンク数 */ const CHUNK_COUNT = 1024; -/** 1チャンクが確保できるセクタ数の上限(長さフィールドが u8 のため) */ +/** 1チャンクが確保できるセクタ数の上限(長さフィールドがu8のため) */ const MAX_SECTORS = 255; /** @@ -53,7 +51,7 @@ const MAX_INLINE_PAYLOAD = MAX_SECTORS * SECTOR_SIZE - 5; export enum RegionFileMode { /** * 読み取り専用 - * 書き込み系の操作はエラーになる + * 書き込み系の操作はエラーになる(`flush`は何もしない) */ ReadOnly = "read_only", /** @@ -64,11 +62,11 @@ export enum RegionFileMode { } /** - * Anvil のリージョンファイル (`r.X.Z.mca`) - * 32×32 チャンクを格納する + * Anvilのリージョンファイル (`r.X.Z.mca`) + * 32×32チャンクを格納する * - * ファイル全体をメモリに読み込んで扱うので、開いて何も変えずに `flush()` すると - * バイト単位で元と同じファイルになる + * ファイル全体をメモリに読み込んで扱うので、開いて何も変えずに`flush()`すると、バイト単位で元と同じファイルになる + * ただし空のファイルは、8KiBのヘッダだけのファイルになる */ export class RegionFile { readonly #path: string; @@ -101,7 +99,7 @@ export class RegionFile { /** * リージョンファイルを開く * - * @param path `r.X.Z.mca` という名前のファイル + * @param path `r.X.Z.mca`という名前のファイル * 座標はファイル名から読み取る * @param mode 読み取り専用か読み書きか */ @@ -135,7 +133,7 @@ export class RegionFile { /** ヘッダを解析し、ロケーションとタイムスタンプを取り込む */ #parseHeader(): void { - // 空ファイルは「チャンクが 1 つも無いリージョン」として受け入れる + // 空ファイルは「チャンクが1つも無いリージョン」として受け入れる if (this.#data.length === 0) { this.#data = new Uint8Array(HEADER_SECTORS * SECTOR_SIZE); return; @@ -156,7 +154,7 @@ export class RegionFile { const totalSectors = this.#data.length / SECTOR_SIZE; const sectorOwner = new Map(); - // ロケーションテーブルの 1024 エントリを順に取り込む + // ロケーションテーブルの1024エントリを順に取り込む for (let index = 0; index < CHUNK_COUNT; index++) { const entry = this.#readUnsigned(index * 4, 4); const offset = Math.floor(entry / 256); @@ -184,7 +182,7 @@ export class RegionFile { throw SpringNbtError.malformed(`チャンク ${index} の割り当てがファイル外へはみ出している`); } - // 同じセクタを 2 つのチャンクが指していたら、どちらかが壊れている + // 同じセクタを2つのチャンクが指していたら、どちらかがおかしくなっている for (let sector = offset; sector < offset + count; sector++) { const owner = sectorOwner.get(sector); @@ -239,7 +237,7 @@ export class RegionFile { this.#ensureOpen(); const result: ChunkPos[] = []; - // 添字の昇順に走査する(localZ が外、localX が内) + // 添字の昇順に走査する(localZが外、localXが内) for (let index = 0; index < CHUNK_COUNT; index++) { if (this.#sectorCounts[index] === 0) { continue; @@ -254,8 +252,8 @@ export class RegionFile { } /** - * チャンクの最終更新時刻(Unix 秒) - * 存在しなければ 0 + * チャンクの最終更新時刻(Unix秒) + * タイムスタンプテーブルの値をそのまま返すため、チャンクが無くても0とは限らない */ timestamp(chunkX: number, chunkZ: number): number { this.#ensureOpen(); @@ -272,7 +270,7 @@ export class RegionFile { /** * チャンクを圧縮されたまま取り出す - * 存在しなければ undefined + * 存在しなければundefined */ readChunkRaw(chunkX: number, chunkZ: number): RawChunk | undefined { this.#ensureOpen(); @@ -302,7 +300,7 @@ export class RegionFile { const compression = chunkCompressionFromId(schemeByte & 0x7f); if (external) { - // 最上位ビットが立っている場合、本体は c.X.Z.mcc にある + // 最上位ビットが立っている場合、本体はc.X.Z.mccにある return new RawChunk(compression, this.#readExternalFile(chunkX, chunkZ), true); } @@ -310,8 +308,8 @@ export class RegionFile { } /** - * チャンクを NBT として読む - * 存在しなければ undefined + * チャンクをNBTとして読む + * 存在しなければundefined */ readChunk(chunkX: number, chunkZ: number): NbtCompound | undefined { const raw = this.readChunkRaw(chunkX, chunkZ); @@ -323,7 +321,7 @@ export class RegionFile { return readBytes(decompressChunk(raw), { compression: Compression.None }).tag; } - /** チャンクを NBT として書き込む */ + /** チャンクをNBTとして書き込む */ writeChunk( chunkX: number, chunkZ: number, @@ -346,7 +344,7 @@ export class RegionFile { let schemeByte: number; if (useExternal) { - // 1MiB を超えるチャンクは外部ファイルへ退避し、リージョンには目印だけ残す + // 255セクタ(約1MiB)に収まらないチャンクは外部ファイルへ退避し、リージョンには目印だけ残す this.#writeExternalFile(chunkX, chunkZ, raw.data); payload = new Uint8Array(0); schemeByte = raw.compression | 0x80; @@ -382,7 +380,7 @@ export class RegionFile { /** * チャンクを削除する - * 削除できたら true + * 削除できたらtrue */ deleteChunk(chunkX: number, chunkZ: number): boolean { this.#ensureOpen(); @@ -405,7 +403,7 @@ export class RegionFile { /** * 必要なセクタ数を確保し、開始セクタ番号を返す * - * 既存の割り当てがちょうど同じ大きさならその場を使い、 + * 既存の割り当てがちょうど同じ大きさなら、その場所をそのまま使う * そうでなければ先頭から空き領域を探し、無ければ末尾へ追加する */ #allocateSectors(index: number, needed: number): number { @@ -442,13 +440,13 @@ export class RegionFile { /** * セクタの使用状況を作る - * `ignoreIndex` のチャンクは空きとして扱う + * `ignoreIndex`のチャンクは空きとして扱う */ #buildSectorUsage(ignoreIndex: number): boolean[] { const totalSectors = this.#data.length / SECTOR_SIZE; const used = new Array(totalSectors).fill(false); - // ヘッダの 2 セクタは常に使用中 + // ヘッダの2セクタは常に使用中 for (let sector = 0; sector < HEADER_SECTORS && sector < totalSectors; sector++) { used[sector] = true; } @@ -543,6 +541,8 @@ export class RegionFile { } this.#writeHeader(); + // 読み書きで開いたフォルダがまだ無ければ、書き出す前に作る + this.#ensureDirectory(); try { writeFileSync(this.#path, this.#data); @@ -563,9 +563,9 @@ export class RegionFile { return this.#data.slice(); } - /** ロケーションテーブルとタイムスタンプテーブルを先頭 2 セクタへ書き戻す */ + /** ロケーションテーブルとタイムスタンプテーブルを先頭2セクタへ書き戻す */ #writeHeader(): void { - // 位置表とタイムスタンプ表を、添字順に組み立て直す + // ロケーションテーブルとタイムスタンプテーブルを、添字順に組み立て直す for (let index = 0; index < CHUNK_COUNT; index++) { this.#writeUnsigned(index * 4, this.#offsets[index] * 256 + this.#sectorCounts[index], 4); this.#writeUnsigned(SECTOR_SIZE + index * 4, this.#timestamps[index] >>> 0, 4); @@ -642,8 +642,22 @@ export class RegionFile { } } + /** + * ファイルを置くディレクトリが無ければ作る + * 読むだけの操作で空のディレクトリができないよう、書き出す直前にだけ呼ぶ + */ + #ensureDirectory(): void { + try { + mkdirSync(this.#directory, { recursive: true }); + } catch (error) { + throw new SpringNbtError(ErrorCode.Io, `ディレクトリを作れない: ${this.#directory}`, { cause: error }); + } + } + #writeExternalFile(chunkX: number, chunkZ: number, payload: Uint8Array): void { const external = this.#externalPath(chunkX, chunkZ); + // 読み書きで開いたフォルダがまだ無ければ、書き出す前に作る + this.#ensureDirectory(); try { writeFileSync(external, payload); diff --git a/typescript/src/anvil/regionFolder.ts b/typescript/src/anvil/regionFolder.ts index 1e8c62b..950f6d4 100644 --- a/typescript/src/anvil/regionFolder.ts +++ b/typescript/src/anvil/regionFolder.ts @@ -1,21 +1,16 @@ /** - * リージョンファイルが並ぶディレクトリ 1 つ分 - * (`region/`、`entities/`、`poi/` のいずれか) + * リージョンファイルが並ぶディレクトリ1つ分 + * (`region/`、`entities/`、`poi/`のいずれか) * - * 開いたリージョンファイルはキャッシュし、`close()` でまとめて閉じる + * 開いたリージョンファイルはキャッシュし、`close()`でまとめて閉じる * チャンク座標からリージョンを解決するので、利用側はリージョンの存在を意識しなくてよい * - * `RegionFile` はファイル全体をメモリへ載せるため、キャッシュには - * `maxCachedRegions` 件の上限がある - * 上限を超えると、最も長く使われていない - * ものから書き出して閉じる + * `RegionFile`はファイル全体をメモリへ載せるため、キャッシュには`maxCachedRegions`件の上限がある + * 上限を超えると、最も長く使われていないものから書き出して閉じる * 大きなワールドを端から走査してもメモリを使い切らない * - * このため `region()` が返した参照は、 - * **別のリージョンへアクセスすると閉じられている場合がある** + * このため`region()`が返した参照は、**別のリージョンへアクセスすると閉じられている場合がある** * 参照を保持せず、必要なたびに取得すること - * - * 仕様: `docs/spec/20-anvil-region.md` 5章 */ import { existsSync, readdirSync } from "node:fs"; @@ -29,15 +24,15 @@ import { RegionFile, RegionFileMode } from "./regionFile.js"; /** * 同時に開いておくリージョンファイル数の既定の上限 * - * 1 リージョンは最大 255 セクタ × 1024 チャンク=理論上 1GiB になりうる - * 実データでは数 MB から数十 MB 程度 - * 8 件なら通常のワールドで数百 MB に収まる + * 1リージョンは最大255セクタ × 1024チャンク=理論上1GiBになりうる + * 実データでは数MBから数十MB程度 + * 8件なら通常のワールドで数百MBに収まる */ export const DEFAULT_MAX_CACHED_REGIONS = 8; /** - * リージョンファイルが並ぶディレクトリ 1 つ分 - * (`region/`、`entities/`、`poi/` のいずれか) + * リージョンファイルが並ぶディレクトリ1つ分 + * (`region/`、`entities/`、`poi/`のいずれか) * * チャンク座標からリージョンを解決するので、利用側はリージョンの存在を意識しなくてよい */ @@ -66,7 +61,11 @@ export class RegionFolder { return this.#cache.size; } - /** リージョンフォルダを開く */ + /** + * リージョンフォルダを開く + * + * @throws {SpringNbtError} 読み取り専用でディレクトリが存在しない場合、または上限が1未満の場合 + */ static open( directory: string, mode: RegionFileMode = RegionFileMode.ReadOnly, @@ -95,7 +94,7 @@ export class RegionFolder { const found: RegionPos[] = []; - // r.X.Z.mca として解釈できるファイルだけを拾う + // r.X.Z.mcaとして解釈できるファイルだけを拾う for (const name of readdirSync(this.directory)) { const position = RegionPos.fromFileName(name); @@ -118,7 +117,7 @@ export class RegionFolder { /** * リージョンファイルを取得する - * 読み取り専用で存在しなければ undefined + * 読み取り専用で存在しなければundefined */ region(regionX: number, regionZ: number): RegionFile | undefined { this.#ensureOpen(); @@ -132,7 +131,7 @@ export class RegionFolder { const path = join(this.directory, position.fileName); - // 読み取り専用では、存在しないリージョンは「チャンクが無い」として undefined を返す + // 読み取り専用では、存在しないリージョンは「チャンクが無い」としてundefinedを返す if (!existsSync(path) && this.#mode === RegionFileMode.ReadOnly) { return undefined; } @@ -158,7 +157,7 @@ export class RegionFolder { this.#recentlyUsed.push(key); } - /** 新しく 1 件開けるよう、上限を下回るまで古いものを閉じる */ + /** 新しく1件開けるよう、上限を下回るまで古いものを閉じる */ #evictUntilBelowLimit(): void { // 上限に達している間、いちばん長く使っていないものから閉じる while (this.#cache.size >= this.maxCachedRegions && this.#recentlyUsed.length > 0) { @@ -191,8 +190,8 @@ export class RegionFolder { } /** - * チャンクを NBT として読む - * 存在しなければ undefined + * チャンクをNBTとして読む + * 存在しなければundefined */ readChunk(chunkX: number, chunkZ: number): NbtCompound | undefined { const file = this.#regionFor(chunkX, chunkZ); @@ -204,7 +203,7 @@ export class RegionFolder { return file.readChunk(chunkX, chunkZ); } - /** チャンクを NBT として書き込む */ + /** チャンクをNBTとして書き込む */ writeChunk(chunkX: number, chunkZ: number, tag: NbtCompound): void { const file = this.#regionFor(chunkX, chunkZ); @@ -219,7 +218,7 @@ export class RegionFolder { /** * チャンクを削除する - * 削除できたら true + * 削除できたらtrue */ deleteChunk(chunkX: number, chunkZ: number): boolean { const file = this.#regionFor(chunkX, chunkZ); diff --git a/typescript/src/conformance.ts b/typescript/src/conformance.ts index 5c0c5c6..dd6c623 100644 --- a/typescript/src/conformance.ts +++ b/typescript/src/conformance.ts @@ -2,10 +2,8 @@ * 適合性検証ツール * 全言語が同じインターフェースで同じ出力を出す * - * `spec/run-conformance.sh` がこのツールを言語ぶん起動し、 - * 出力を相互に diff することで「全言語が同一に振る舞う」ことを機械的に確かめる - * - * 仕様: `docs/spec/90-conformance.md` 2.3章 + * `spec/run-conformance.sh`がこのツールを全言語ぶん起動し、 + * 出力を相互にdiffすることで「全言語が同一に振る舞う」ことを機械的に確かめる */ import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs"; @@ -39,7 +37,7 @@ import { } from "./nbt/index.js"; import { NbtCompound as NbtCompoundClass } from "./nbt/index.js"; -/** 連なった NBT を、位置を追いながら一覧として書き出す。 */ +/** 連なったNBTを、位置を追いながら一覧として書き出す */ function nbtList(raw: Buffer, format: NbtFormat): string { const bytes = new Uint8Array(raw); const options = { format, compression: Compression.None }; @@ -76,11 +74,10 @@ const USAGE = `使い方: // --------------------------------------------------------------------------- // 正規化JSON // -// 浮動小数点をビットパターンで、64bit 整数を10進文字列で表すのが要 -// 10進表記の丸めや JSON 数値の精度は処理系ごとに差が出るため、 +// 浮動小数点をビットパターンで、64bit整数を10進文字列で表すのが要 +// 10進表記の丸めやJSON数値の精度は処理系ごとに差が出るため、 // そのまま出すと言語間で出力が一致しない // -// 仕様: docs/spec/00-conventions.md 6章 // --------------------------------------------------------------------------- const JSON_ESCAPES = new Map([ @@ -94,16 +91,16 @@ const JSON_ESCAPES = new Map([ ]); /** - * JSON 文字列を書き出す - * 非 ASCII は必ず `\uXXXX` へ逃がす + * JSON文字列を書き出す + * 非ASCIIは必ず`\uXXXX`へエスケープする * - * エスケープの単位は UTF-16 コード単位 + * エスケープの単位はUTF-16コード単位 * 言語ごとに既定のエスケープ方針が違うため、ここで一律に固定しないと出力が一致しない */ function jsonString(text: string): string { let result = '"'; - // コード単位ごとに、JSON として安全な形へ直す + // コード単位ごとに、JSONとして安全な形へ直す for (let index = 0; index < text.length; index++) { const unit = text.charCodeAt(index); const escaped = JSON_ESCAPES.get(unit); @@ -113,7 +110,7 @@ function jsonString(text: string): string { continue; } - // ASCII の印字可能文字だけ生で出し、それ以外は \uXXXX にする + // ASCIIの印字可能文字だけ生で出し、それ以外は\uXXXXにする if (unit >= 0x20 && unit <= 0x7e) { result += text[index]; } else { @@ -139,7 +136,7 @@ function doubleBitsHex(value: number): string { function toHex(bytes: Uint8Array): string { let result = ""; - // 1 バイトずつ 16 進 2 桁へ直す + // 1バイトずつ16進2桁へ直す for (const value of bytes) { result += value.toString(16).padStart(2, "0"); } @@ -150,7 +147,7 @@ function toHex(bytes: Uint8Array): string { function jsonTag(tag: NbtTag): string { let result = `{"type":${jsonString(tagTypeAsString(tag.type))}`; - // list だけは value の前に element_type が入る(仕様が定めるキー順) + // listだけはvalueの前にelement_typeが入る(仕様が定めるキー順) if (tag.type === TagType.List) { result += `,"element_type":${jsonString(tagTypeAsString((tag as NbtList).elementType))}`; } @@ -164,7 +161,7 @@ function jsonTag(tag: NbtTag): string { result += `${tag.value}`; break; case TagType.Long: - // 64bit 整数は JSON 数値だと処理系によって精度が落ちるため10進文字列で表す + // 64bit整数はJSON数値だと処理系によって精度が落ちるため10進文字列で表す result += jsonString(`${tag.value}`); break; case TagType.Float: @@ -176,8 +173,8 @@ function jsonTag(tag: NbtTag): string { case TagType.String: result += jsonString(tag.value); - // MUTF-8 のバイト列も併記する - // 孤立サロゲートなど UTF-8 に写せない値を厳密に比較するため + // MUTF-8のバイト列も併記する + // 孤立サロゲートなどUTF-8に写せない値を厳密に比較するため result += `,"mutf8":${jsonString(toHex(mutf8.encode(tag.value)))}`; break; case TagType.ByteArray: @@ -185,7 +182,7 @@ function jsonTag(tag: NbtTag): string { result += `[${Array.from(tag.value).join(",")}]`; break; case TagType.LongArray: { - // 64bit 整数は10進文字列の配列で表す + // 64bit整数は10進文字列の配列で表す const items: string[] = []; // 配列の各要素を文字列として並べる @@ -208,10 +205,10 @@ function jsonTag(tag: NbtTag): string { break; } case TagType.Compound: { - // JSON オブジェクトだと挿入順の保持が処理系依存になるため、組の配列で表す + // JSONオブジェクトだと挿入順の保持が処理系依存になるため、組の配列で表す const items: string[] = []; - // compound はキーと値の組の配列として写す + // compoundはキーと値の組の配列として写す // 挿入順を保つため for (const [key, value] of (tag as NbtCompound).entries()) { items.push(`[${jsonString(key)},${jsonTag(value)}]`); @@ -228,7 +225,7 @@ function jsonTag(tag: NbtTag): string { } /** - * ルートを含む全体を JSON 文字列へ変換する + * ルートを含む全体をJSON文字列へ変換する * 末尾に改行を1つ付ける */ function normalizedJson(named: NamedTag, format: NbtFormat): string { @@ -243,14 +240,13 @@ function normalizedJson(named: NamedTag, format: NbtFormat): string { // --------------------------------------------------------------------------- // リージョンファイル // -// 仕様: docs/spec/90-conformance.md 2.3章 // --------------------------------------------------------------------------- /** - * 存在するチャンクを 1 行 1 チャンクで書き出す + * 存在するチャンクを1行1チャンクで書き出す * 並びはロケーションテーブルの添字順 * - * 各行は「絶対X 絶対Z タイムスタンプ 圧縮方式 圧縮後バイト数 展開後バイト数 キー数」 + * 各行は「絶対X 絶対Z タイムスタンプ 圧縮方式 圧縮後バイト数 展開後バイト数 ルート直下キー数」 */ function regionList(region: RegionFile): string { const lines: string[] = [`region ${region.regionX} ${region.regionZ}`]; @@ -289,7 +285,7 @@ function regionList(region: RegionFile): string { /** * 全チャンクを読み直し、無圧縮で新しいリージョンへ詰め直して書き出す * - * 無圧縮にするのは、zlib の出力が処理系ごとに違い、 + * 無圧縮にするのは、zlibの出力が処理系ごとに違い、 * 圧縮したままでは言語間でバイトが一致しないため */ function regionRewrite(source: RegionFile, outputPath: string): void { @@ -318,16 +314,15 @@ function regionRewrite(source: RegionFile, outputPath: string): void { // --------------------------------------------------------------------------- -// チャンク(World レイヤ) +// チャンク(Worldレイヤ) // -// 仕様: docs/spec/90-conformance.md 2.3章 // --------------------------------------------------------------------------- /** * チャンクの全ブロック・全バイオームを走査して集計する * * パレットとビットストレージを端から端まで通すので、 - * ビット詰めの実装が 1 か所でもずれれば集計値が変わる + * ビット詰めの実装が1か所でもずれれば集計値が変わる */ function chunkDescribe(chunk: Chunk): string { const lines: string[] = [ @@ -358,7 +353,7 @@ function chunkDescribe(chunk: Chunk): string { `section ${sectionY} ${blockPalette} ${blockBits} ${biomePalette} ${biomeBits}`, ); - // 全ブロックを 1 つずつ読んで、状態の文字列表現ごとに数える + // 全ブロックを1つずつ読んで、状態の文字列表現ごとに数える for (let y = 0; y < 16; y++) { for (let z = 0; z < 16; z++) { for (let x = 0; x < 16; x++) { @@ -374,7 +369,7 @@ function chunkDescribe(chunk: Chunk): string { } } - // バイオームは 4×4×4 単位なので、4 ブロックおきに見る + // バイオームは4×4×4単位なので、4ブロックおきに見る for (let y = 0; y < 16; y += 4) { for (let z = 0; z < 16; z += 4) { for (let x = 0; x < 16; x += 4) { @@ -418,7 +413,8 @@ function chunkEdit(chunk: Chunk): void { chunk.setBlock(index % 16, baseY + Math.floor(index / 16), index % 16, state); } - // プロパティ付きのブロックを、名前は同じで状態違いで置く + // プロパティ付きのブロックを置く + // 1つ目と2つ目はプロパティの並び順だけが違う同じ状態、3つ目は名前空間を省いて書いた別の状態 chunk.setBlock(1, baseY + 2, 1, BlockState.parse("minecraft:oak_stairs[facing=north,half=top]")); chunk.setBlock(2, baseY + 2, 2, BlockState.parse("minecraft:oak_stairs[half=top,facing=north]")); chunk.setBlock(3, baseY + 2, 3, BlockState.parse("oak_stairs[facing=south]")); @@ -430,14 +426,14 @@ function chunkEdit(chunk: Chunk): void { // 使われなくなったパレット要素を掃除する chunk.compact(); - // 高さマップと光源は再計算しないので、無効化して Minecraft に任せる + // 高さマップと光源は再計算しないので、無効化してMinecraftに任せる chunk.clearHeightmaps(); chunk.invalidateLighting(); } /** * 集計表から現在の件数を取り出す - * まだ無ければ 0 + * まだ無ければ0 */ function countOf(counts: Map, key: string): number { const current = counts.get(key); @@ -449,9 +445,9 @@ function countOf(counts: Map, key: string): number { return current; } -/** チャンク NBT のファイルを読む */ +/** チャンクNBTのファイルを読む */ function readChunkFile(path: string): Chunk { - // 検証では DataVersion の違いを警告にせず、そのまま読む + // 検証では、DataVersionが扱える形式より古くても警告にせずそのまま読む return Chunk.fromNbt(readFile(path).tag, { onVersionMismatch: VersionMismatchAction.Ignore, }); @@ -461,9 +457,9 @@ function readChunkFile(path: string): Chunk { // コマンド // --------------------------------------------------------------------------- -/** `--format network` が指定されていればネットワーク形式として読む */ +/** `--format network`が指定されていればネットワーク形式として読む */ function parseFormat(args: string[]): NbtFormat { - // 3 番目以降の引数からオプションを探す + // 3番目以降の引数からオプションを探す for (let index = 3; index < args.length - 1; index++) { if (args[index] === "--format" && args[index + 1] === "network") { return NbtFormat.Network; @@ -474,15 +470,15 @@ function parseFormat(args: string[]): NbtFormat { } /** - * 改行を変換せず、BOM も付けずに UTF-8 で書く + * 改行を変換せず、BOMも付けずにUTF-8で書く * - * 孤立サロゲートを含みうるが、標準の UTF-8 エンコーダは置換文字にしてしまうため、 + * 孤立サロゲートを含みうるが、標準のUTF-8エンコーダは置換文字にしてしまうため、 * 自前で符号化する(WTF-8) */ function writeTextFile(path: string, content: string): void { const bytes: number[] = []; - // コード単位を順に見て、正しいサロゲートペアだけ 1 文字として符号化する + // コード単位を順に見て、正しいサロゲートペアだけ1文字として符号化する for (let index = 0; index < content.length; index++) { const unit = content.charCodeAt(index); @@ -506,7 +502,7 @@ function writeTextFile(path: string, content: string): void { bytes.push(0xc0 | (unit >> 6)); bytes.push(0x80 | (unit & 0x3f)); } else { - // 孤立サロゲートもこの経路で 3 バイト形式のまま書く + // 孤立サロゲートもこの経路で3バイト形式のまま書く bytes.push(0xe0 | (unit >> 12)); bytes.push(0x80 | ((unit >> 6) & 0x3f)); bytes.push(0x80 | (unit & 0x3f)); @@ -596,7 +592,7 @@ function main(argv: string[]): number { } } catch (error) { if (error instanceof SpringNbtError) { - // 言語間で同じ ErrorCode を出すことが検証対象なので、コードを機械可読な形で出す + // 言語間で同じErrorCodeを出すことが検証対象なので、コードを機械可読な形で出す process.stderr.write(`ERROR ${error.code} ${error.message}\n`); return 1; } diff --git a/typescript/src/errors.ts b/typescript/src/errors.ts index cf43a76..7c66ea9 100644 --- a/typescript/src/errors.ts +++ b/typescript/src/errors.ts @@ -1,7 +1,5 @@ /** - * 4言語で共通のエラーモデル - * - * 仕様: `docs/spec/00-conventions.md` 4章 / `docs/adr/0005-unified-error-model.md` + * 全言語で共通のエラーモデル */ /** @@ -18,7 +16,7 @@ export enum ErrorCode { /** 期待した型と違うタグを取り出した */ UnexpectedTagType = "UNEXPECTED_TAG_TYPE", - /** 仕様上は妥当だが、このビルドでは扱えない */ + /** 仕様上は妥当だが、このライブラリでは扱えない */ UnsupportedFeature = "UNSUPPORTED_FEATURE", /** 安全上限を超えた */ @@ -27,7 +25,7 @@ export enum ErrorCode { /** 呼び出し側の引数が不正 */ InvalidArgument = "INVALID_ARGUMENT", - /** 対象バージョン外のデータ */ + /** 扱える形式より古いデータ */ UnsupportedDataVersion = "UNSUPPORTED_DATA_VERSION", } @@ -38,7 +36,7 @@ export function errorCodeAsString(code: ErrorCode): string { /** * 本ライブラリが送出する例外 - * 分類は {@link SpringNbtError.code} で判別する + * 分類は{@link SpringNbtError.code}で判別する */ export class SpringNbtError extends Error { /** エラーの分類 */ @@ -54,27 +52,27 @@ export class SpringNbtError extends Error { return `[${this.code}] ${this.message}`; } - /** {@link ErrorCode.MalformedData} の例外を作る */ + /** {@link ErrorCode.MalformedData}の例外を作る */ static malformed(message: string): SpringNbtError { return new SpringNbtError(ErrorCode.MalformedData, message); } - /** {@link ErrorCode.InvalidArgument} の例外を作る */ + /** {@link ErrorCode.InvalidArgument}の例外を作る */ static invalidArgument(message: string): SpringNbtError { return new SpringNbtError(ErrorCode.InvalidArgument, message); } - /** {@link ErrorCode.UnexpectedTagType} の例外を作る */ + /** {@link ErrorCode.UnexpectedTagType}の例外を作る */ static unexpectedTagType(message: string): SpringNbtError { return new SpringNbtError(ErrorCode.UnexpectedTagType, message); } - /** {@link ErrorCode.LimitExceeded} の例外を作る */ + /** {@link ErrorCode.LimitExceeded}の例外を作る */ static limitExceeded(message: string): SpringNbtError { return new SpringNbtError(ErrorCode.LimitExceeded, message); } - /** {@link ErrorCode.UnsupportedFeature} の例外を作る */ + /** {@link ErrorCode.UnsupportedFeature}の例外を作る */ static unsupportedFeature(message: string): SpringNbtError { return new SpringNbtError(ErrorCode.UnsupportedFeature, message); } diff --git a/typescript/src/index.ts b/typescript/src/index.ts index a9df78c..78d3680 100644 --- a/typescript/src/index.ts +++ b/typescript/src/index.ts @@ -1,22 +1,21 @@ /** - * SpringNBTLibrary — Minecraft Java版の NBT / Anvil ワールドデータを読み書きするライブラリ + * SpringNBTLibraryは、Minecraft Java版のNBT / Anvilワールドデータを読み書きするライブラリ * - * 仕様は `docs/spec/` を唯一の正とする - * 対象は 26.1 で導入されたワールド形式 (DataVersion 4786 以降) + * 対象は26.1で導入されたワールド形式 (DataVersion 4786以降) */ /** - * このライブラリが扱えるワールド形式の下限となる DataVersion (26.1) + * このライブラリが扱えるワールド形式の下限となるDataVersion (26.1) * - * 26.1 で次元とプレイヤーデータの置き場が変わり、いまの形式になった + * 26.1で次元とプレイヤーデータの置き場が変わり、いまの形式になった * これ以降のバージョンは、形式が同じであればそのまま読み書きできる * - * これより古いワールドは構成そのものが違うので、 - * 読み込み時に UNSUPPORTED_DATA_VERSION の対象になる + * これより古いワールドは構成そのものが違う + * そうしたチャンクは、読み込み時は既定で警告を出し、書き戻しは既定でUNSUPPORTED_DATA_VERSIONにする */ export const MIN_SUPPORTED_DATA_VERSION = 4786; -/** 動作を確かめた Minecraft Java版の DataVersion (26.2) */ +/** 動作を確かめたMinecraft Java版のDataVersion (26.2) */ export const TARGET_DATA_VERSION = 4903; export { ErrorCode, SpringNbtError, errorCodeAsString } from "./errors.js"; @@ -28,7 +27,7 @@ export * as anvil from "./anvil/index.js"; export * as world from "./world/index.js"; // よく使う型はトップレベルからも直接取れるようにする -// 3 つのレイヤで名前が衝突しないことは check_docs_sync が保証する +// 3つのレイヤで名前が衝突しないことはcheck_docs_syncが保証する export * from "./nbt/index.js"; export * from "./anvil/index.js"; export * from "./world/index.js"; diff --git a/typescript/src/nbt/canonical.ts b/typescript/src/nbt/canonical.ts index 5917a81..2149f8a 100644 --- a/typescript/src/nbt/canonical.ts +++ b/typescript/src/nbt/canonical.ts @@ -1,12 +1,10 @@ /** * 浮動小数点の正準10進表記 * - * 各言語の標準の数値書式(C# の `"R"`、Java の `Float.toString`、 - * Python の `repr`、Rust の `{}`、JavaScript の `String(x)`)は互いに一致しない - * 指数表記へ切り替わる閾値も、指数部の桁数も、`E` の大文字小文字も処理系ごとに違う - * そのままでは SNBT 出力の言語間一致が成立しないため、書式をここで固定する - * - * 仕様: `docs/spec/11-snbt.md` 5.1章 + * 各言語の標準の数値書式(C#の`"R"`、Javaの`Float.toString`、 + * Pythonの`repr`、Rustの`{}`、JavaScriptの`String(x)`)は互いに一致しない + * 指数表記へ切り替わる閾値も、指数部の桁数も、`E`の大文字小文字も処理系ごとに違う + * そのままではSNBTの出力が言語間で一致しないため、書式をここで固定する */ /** 固定小数点表記を使う10進指数の下限 */ @@ -17,7 +15,7 @@ const MAX_FIXED_EXPONENT = 16; const scratch = new DataView(new ArrayBuffer(8)); -/** 特殊値なら文字列を、そうでなければ undefined を返す */ +/** 特殊値なら文字列を、そうでなければundefinedを返す */ function special(value: number): string | undefined { if (Number.isNaN(value)) { return "NaN"; @@ -34,19 +32,19 @@ function special(value: number): string | undefined { return undefined; } -/** binary32 のビットパターンを取り出す */ +/** binary32のビットパターンを取り出す */ function floatBits(value: number): number { scratch.setFloat32(0, value, false); return scratch.getUint32(0, false); } -/** binary64 のビットパターンを取り出す */ +/** binary64のビットパターンを取り出す */ function doubleBits(value: number): bigint { scratch.setFloat64(0, value, false); return scratch.getBigUint64(0, false); } -/** binary32 を正準10進表記へ変換する */ +/** binary32を正準10進表記へ変換する */ export function fromFloat(value: number): string { const specialText = special(value); @@ -57,7 +55,7 @@ export function fromFloat(value: number): string { const target = floatBits(value); const negative = isNegative(value); - // 有効数字を 1 桁ずつ増やし、読み戻してビット一致する最短の表記を探す + // 有効数字を1桁ずつ増やし、読み戻したときにビットが一致する最短の表記を探す for (let precision = 1; precision <= 9; precision++) { const candidate = value.toExponential(precision - 1); @@ -66,11 +64,11 @@ export function fromFloat(value: number): string { } } - // 9 桁あれば binary32 は必ず往復するので、ここへは来ない + // 9桁あればbinary32は必ず往復するので、ここへは来ない return formatExponential(value.toExponential(8), negative); } -/** binary64 を正準10進表記へ変換する */ +/** binary64を正準10進表記へ変換する */ export function fromDouble(value: number): string { const specialText = special(value); @@ -81,7 +79,7 @@ export function fromDouble(value: number): string { const target = doubleBits(value); const negative = isNegative(value); - // 有効数字を 1 桁ずつ増やし、読み戻してビット一致する最短の表記を探す + // 有効数字を1桁ずつ増やし、読み戻したときにビットが一致する最短の表記を探す for (let precision = 1; precision <= 17; precision++) { const candidate = value.toExponential(precision - 1); @@ -90,16 +88,15 @@ export function fromDouble(value: number): string { } } - // 17 桁あれば binary64 は必ず往復するので、ここへは来ない + // 17桁あればbinary64は必ず往復するので、ここへは来ない return formatExponential(value.toExponential(16), negative); } /** * 符号が負かどうかを判定する * - * `-0` を `< 0` では判定できないため、ビットの符号で見る - * JavaScript の `toExponential` は `-0` の符号を落とすので、 - * ここで別に持っておかないと `-0.0d` が `0.0d` になり他言語と食い違う + * `-0`を`< 0`では判定できないため、ビットの符号で見る + * JavaScriptの`toExponential`は`-0`の符号を落とすので、ここで別に持っておかないと`-0.0d`が`0.0d`になり他言語と食い違う */ function isNegative(value: number): boolean { if (value < 0) { @@ -109,7 +106,7 @@ function isNegative(value: number): boolean { return Object.is(value, -0); } -/** 指数表記の文字列(例 `"7.5e-1"`)から、仕様が定める正準表記を組み立てる */ +/** 指数表記の文字列(例`"7.5e-1"`)から、仕様が定める正準表記を組み立てる */ function formatExponential(exponential: string, negative: boolean): string { let index = 0; @@ -137,7 +134,7 @@ function formatExponential(exponential: string, negative: boolean): string { /** * 末尾のゼロを取り除く - * すべてゼロなら "0" を残す + * すべてゼロなら"0"を残す */ function trimTrailingZeros(digits: string): string { let end = digits.length; @@ -158,7 +155,7 @@ function compose(negative: boolean, digits: string, exponent: number): string { sign = "-"; } - // 値が 0 のときは指数に関わらず 0.0 と書く + // 値が0のときは指数に関わらず0.0と書く if (digits === "0") { return `${sign}0.0`; } @@ -175,8 +172,8 @@ function compose(negative: boolean, digits: string, exponent: number): string { } if (exponent >= 0) { - // 整数部は先頭 (exponent + 1) 桁 - // 足りなければゼロで右詰めする + // 整数部は先頭 (exponent + 1)桁 + // 足りなければ数字の右側をゼロで埋める const integerDigits = exponent + 1; let integerPart: string; @@ -195,6 +192,6 @@ function compose(negative: boolean, digits: string, exponent: number): string { return `${sign}${integerPart}.${fraction}`; } - // 指数が負なら "0." に続けてゼロを詰めてから数字を置く + // 指数が負なら"0."に続けてゼロを詰めてから数字を置く return `${sign}0.${"0".repeat(-exponent - 1)}${digits}`; } diff --git a/typescript/src/nbt/index.ts b/typescript/src/nbt/index.ts index f5a6eaa..7352945 100644 --- a/typescript/src/nbt/index.ts +++ b/typescript/src/nbt/index.ts @@ -1,9 +1,7 @@ /** * NBT (Named Binary Tag) の読み書き * - * このレイヤは Minecraft のバージョンに一切依存しない - * - * 仕様: `docs/spec/10-nbt-binary.md` + * このレイヤはMinecraftのバージョンに一切依存しない */ export * as mutf8 from "./mutf8.js"; diff --git a/typescript/src/nbt/io.ts b/typescript/src/nbt/io.ts index e9ed38f..37bf176 100644 --- a/typescript/src/nbt/io.ts +++ b/typescript/src/nbt/io.ts @@ -1,7 +1,5 @@ /** - * NBT のファイル・バイト列からの読み書き - * - * 仕様: `docs/spec/10-nbt-binary.md` 3章〜6章 + * NBTのファイル・バイト列からの読み書き */ import { readFileSync, writeFileSync } from "node:fs"; @@ -20,7 +18,6 @@ import { NbtByteArray, NbtCompound, NbtDouble, - NbtFloat, NbtInt, NbtIntArray, NbtList, @@ -30,11 +27,13 @@ import { NbtString, NbtTag, TagType, + float32BitsOf, + float32FromBits, tagTypeAsString, tagTypeFromId, } from "./tag.js"; -/** NBT のルートタグの並び方 */ +/** NBTのルートタグの並び方 */ export enum NbtFormat { /** * ファイル形式 @@ -42,7 +41,7 @@ export enum NbtFormat { */ Java = "java", /** - * ネットワーク形式 (1.20.2 以降) + * ネットワーク形式 (1.20.2以降) * ルートに名前が付かない */ Network = "network", @@ -70,47 +69,50 @@ export class NamedTag { readonly tag: NbtCompound, ) {} - /** 人が読むための表現。中身の形式は決めていない */ + /** + * 人が読むための表現 + * 中身の形式は決めていない + */ toString(): string { return `NamedTag("${this.name}", ${this.tag})`; } } -/** NBT 読み込みのオプション */ +/** NBT読み込みのオプション */ export interface NbtReadOptions { /** * ルートタグの並び方 - * 既定は {@link NbtFormat.Java} + * 既定は{@link NbtFormat.Java} */ format?: NbtFormat; /** * 圧縮方式 - * 既定は {@link Compression.Auto} + * 既定は{@link Compression.Auto} */ compression?: Compression; /** * ネストの深さ上限 - * 既定は 512 + * 既定は512 */ maxDepth?: number; /** * 展開後の総バイト数の上限 * 負値なら無制限 - * 既定は -1 + * 既定は-1 */ maxDecompressedSize?: number; } -/** NBT 書き込みのオプション */ +/** NBT書き込みのオプション */ export interface NbtWriteOptions { /** * ルートタグの並び方 - * 既定は {@link NbtFormat.Java} + * 既定は{@link NbtFormat.Java} */ format?: NbtFormat; /** * 圧縮方式 - * 既定は {@link Compression.Gzip} + * 既定は{@link Compression.Gzip} */ compression?: Compression; } @@ -119,9 +121,7 @@ export interface NbtWriteOptions { * 位置を指定した読み込みの結果 * * 読んだタグと、その直後の位置を持つ - * 続けて読むときは `end` を次の開始位置として渡す - * - * 仕様: `docs/spec/10-nbt-binary.md` 3.1章 + * 続けて読むときは`end`を次の開始位置として渡す */ export interface NbtReadResult { /** 読んだタグ */ @@ -137,7 +137,7 @@ const DEFAULT_MAX_DEPTH = 512; // --------------------------------------------------------------------------- /** - * 展開済みのバイト列から NBT を読み出す + * 展開済みのバイト列からNBTを読み出す * * 入力全体をあらかじめメモリに持つ設計にしている * 「宣言された長さが残り入力長を超えていないか」を確保前に検査できるようにするため @@ -184,13 +184,13 @@ class Reader { } /** - * ルートタグを 1 つ読む + * ルートタグを1つ読む * 形式によって名前の有無が変わる */ readRootTag(format: NbtFormat): NamedTag { const type = tagTypeFromId(this.#readByte()); - // Java版のファイル形式でもネットワーク形式でも、ルートは必ず TAG_Compound + // Java版のファイル形式でもネットワーク形式でも、ルートは必ずTAG_Compound if (type !== TagType.Compound) { throw SpringNbtError.malformed( `ルートタグは compound でなければならないが ${tagTypeAsString(type)} だった`, @@ -211,7 +211,7 @@ class Reader { } #readPayload(type: TagType, depth: number): NbtTag { - // 深さ上限は再帰する型に入る手前で検査する + // 深さ上限は型を問わず、ペイロードを読む前に検査する if (depth > this.#maxDepth) { throw SpringNbtError.limitExceeded(`ネストが深すぎる (上限 ${this.#maxDepth})`); } @@ -226,7 +226,8 @@ class Reader { case TagType.Long: return new NbtLong(this.#take(8, (offset) => this.#view.getBigInt64(offset, false))); case TagType.Float: - return new NbtFloat(this.#take(4, (offset) => this.#view.getFloat32(offset, false))); + // NaNのビットパターンを保つため、値ではなくビットとして読む + return float32FromBits(this.#take(4, (offset) => this.#view.getInt32(offset, false))); case TagType.Double: return new NbtDouble(this.#take(8, (offset) => this.#view.getFloat64(offset, false))); case TagType.ByteArray: @@ -249,7 +250,7 @@ class Reader { #readCompoundPayload(depth: number): NbtCompound { const compound = new NbtCompound(); - // TAG_End が現れるまで名前付きタグを読み続ける + // TAG_Endが現れるまで名前付きタグを読み続ける for (;;) { const type = tagTypeFromId(this.#readByte()); @@ -267,7 +268,7 @@ class Reader { const count = this.#readLength(); if (elementType === TagType.End) { - // 要素型 End のリストは空でなければならない + // 要素型Endのリストは空でなければならない if (count !== 0) { throw SpringNbtError.malformed(`要素型 End のリストに ${count} 個の要素が宣言されている`); } @@ -275,7 +276,7 @@ class Reader { return new NbtList(TagType.End); } - // 1 要素の最小バイト数から、宣言された個数が入力に収まるかを先に検査する + // 1要素の最小バイト数から、宣言された個数が入力に収まるかを先に検査する this.#ensureAvailable(count * minimumPayloadSize(elementType)); const list = new NbtList(elementType); @@ -309,7 +310,7 @@ class Reader { const result = new Int32Array(count); - // 4 バイトずつビッグエンディアンで読む + // 4バイトずつビッグエンディアンで読む for (let index = 0; index < count; index++) { result[index] = this.#view.getInt32(this.#position + index * 4, false); } @@ -324,7 +325,7 @@ class Reader { const result = new BigInt64Array(count); - // 8 バイトずつビッグエンディアンで読む + // 8バイトずつビッグエンディアンで読む for (let index = 0; index < count; index++) { result[index] = this.#view.getBigInt64(this.#position + index * 8, false); } @@ -333,7 +334,7 @@ class Reader { return result; } - /** MUTF-8 の文字列(u16 の長さ + 本体)を読む */ + /** MUTF-8の文字列(u16の長さ + 本体)を読む */ #readString(): string { const length = this.#take(2, (offset) => this.#view.getUint16(offset, false)); this.#ensureAvailable(length); @@ -350,7 +351,7 @@ class Reader { #readLength(): number { const length = this.#take(4, (offset) => this.#view.getInt32(offset, false)); - // 長さは i32 だが、負値は仕様上ありえない + // 長さはi32だが、負値は仕様上ありえない if (length < 0) { throw SpringNbtError.malformed(`長さが負値: ${length}`); } @@ -402,18 +403,18 @@ function minimumPayloadSize(type: TagType): number { case TagType.Long: case TagType.Double: return 8; - // 長さフィールドの 4 バイトは必ずある + // 長さフィールドの4バイトは必ずある case TagType.ByteArray: case TagType.IntArray: case TagType.LongArray: return 4; - // 長さフィールドの 2 バイトは必ずある + // 長さフィールドの2バイトは必ずある case TagType.String: return 2; - // 要素型 1 バイト + 個数 4 バイト + // 要素型1バイト + 個数4バイト case TagType.List: return 5; - // 終端の TAG_End 1 バイトは必ずある + // 終端のTAG_End 1バイトは必ずある default: return 1; } @@ -422,8 +423,8 @@ function minimumPayloadSize(type: TagType): number { /** * キーやルート名として使える文字列か検査する * - * 値と違い、キーには孤立サロゲートを許さない(仕様 10 の 2.2章) - * Minecraft が書き出すキーは ASCII の識別子のみで、 + * 値と違い、キーには孤立サロゲートを許さない + * Minecraftが書き出すキーはASCIIの識別子のみで、 * 孤立サロゲートが現れるのはデータ破損を意味する */ function requireUtf8Representable(text: string, role: string): string { @@ -456,17 +457,17 @@ function requireUtf8Representable(text: string, role: string): string { // --------------------------------------------------------------------------- /** - * NBT を展開済みのバイト列へ書き出す + * NBTを展開済みのバイト列へ書き出す * * 出力は一意でなければならない(ラウンドトリップ検証が成立するため) - * Compound は挿入順のまま、浮動小数点はビットパターンのまま書き出す + * Compoundは挿入順のまま、浮動小数点はビットパターンのまま書き出す */ class Writer { #chunks: Uint8Array[] = []; #scratch = new DataView(new ArrayBuffer(8)); /** - * ルートタグを 1 つ書き出す + * ルートタグを1つ書き出す * 形式によって名前の有無が変わる */ writeRoot(named: NamedTag, format: NbtFormat): Uint8Array { @@ -496,8 +497,8 @@ class Writer { this.#writeScalar(8, (view) => view.setBigInt64(0, tag.value, false)); break; case TagType.Float: - // NaN や -0.0 を保つため、そのまま書く - this.#writeScalar(4, (view) => view.setFloat32(0, tag.value, false)); + // NaNや-0.0を保つため、値ではなくビットパターンを書く + this.#writeScalar(4, (view) => view.setInt32(0, float32BitsOf(tag), false)); break; case TagType.Double: this.#writeScalar(8, (view) => view.setFloat64(0, tag.value, false)); @@ -518,7 +519,7 @@ class Writer { case TagType.IntArray: this.#writeScalar(4, (view) => view.setInt32(0, tag.value.length, false)); - // 4 バイトずつビッグエンディアンで書く + // 4バイトずつビッグエンディアンで書く for (const value of tag.value) { this.#writeScalar(4, (view) => view.setInt32(0, value, false)); } @@ -527,7 +528,7 @@ class Writer { case TagType.LongArray: this.#writeScalar(4, (view) => view.setInt32(0, tag.value.length, false)); - // 8 バイトずつビッグエンディアンで書く + // 8バイトずつビッグエンディアンで書く for (const value of tag.value) { this.#writeScalar(8, (view) => view.setBigInt64(0, value, false)); } @@ -562,8 +563,8 @@ class Writer { #writeString(text: string): void { const encoded = mutf8.encode(text); - // 長さフィールドは u16 - // キー名は素の string なのでここでも検査する + // 長さフィールドはu16 + // キー名は素のstringなのでここでも検査する if (encoded.length > mutf8.MAX_BYTE_LENGTH) { throw SpringNbtError.invalidArgument( `文字列が長すぎる: MUTF-8 で ${encoded.length} バイト (上限 ${mutf8.MAX_BYTE_LENGTH})`, @@ -586,7 +587,7 @@ class Writer { #concat(): Uint8Array { let total = 0; - // 先に全体長を数えてから 1 回で確保する + // 先に全体長を数えてから1回で確保する for (const chunk of this.#chunks) { total += chunk.length; } @@ -594,7 +595,7 @@ class Writer { const result = new Uint8Array(total); let offset = 0; - // 溜めた断片を 1 つの配列へ連結する + // 溜めた断片を1つの配列へ連結する for (const chunk of this.#chunks) { result.set(chunk, offset); offset += chunk.length; @@ -618,13 +619,13 @@ export function detectCompression(bytes: Uint8Array): Compression { throw SpringNbtError.malformed("入力が空で圧縮方式を判定できない"); } - // GZip は必ず 1F 8B で始まる + // GZipは必ず1F 8Bで始まる if (bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b) { return Compression.Gzip; } if (bytes.length >= 2) { - // zlib ヘッダは「圧縮法が 8 (deflate)」かつ「先頭2バイトが 31 の倍数」 + // zlibヘッダは「圧縮法が8 (deflate)」かつ「先頭2バイトが31の倍数」 const isDeflate = (bytes[0] & 0x0f) === 0x08; const header = (bytes[0] << 8) | bytes[1]; @@ -633,7 +634,7 @@ export function detectCompression(bytes: Uint8Array): Compression { } } - // 無圧縮なら先頭は TAG_Compound のタグID + // 無圧縮なら先頭はTAG_CompoundのタグID if (bytes[0] === TagType.Compound) { return Compression.None; } @@ -699,7 +700,7 @@ function compress(plain: Uint8Array, method: Compression): Uint8Array { } // --------------------------------------------------------------------------- -// 公開 API +// 公開API // --------------------------------------------------------------------------- /** 省略された項目を既定値で埋める */ @@ -758,7 +759,7 @@ function fillWriteOptions(options?: NbtWriteOptions): Required return filled; } -/** バイト列から NBT を読む */ +/** バイト列からNBTを読む */ export function readBytes(bytes: Uint8Array, options?: NbtReadOptions): NamedTag { const effective = fillReadOptions(options); const plain = decompress(bytes, effective); @@ -766,10 +767,10 @@ export function readBytes(bytes: Uint8Array, options?: NbtReadOptions): NamedTag } /** - * バイト列の指定した位置から NBT を 1 つ読む + * バイト列の指定した位置からNBTを1つ読む * - * 複数の NBT が連なっているデータを、先頭から順に読み進めるために使う - * 戻り値の `end` が次の開始位置になる + * 複数のNBTが連なっているデータを、先頭から順に読み進めるために使う + * 戻り値の`end`が次の開始位置になる * * 位置は渡したバイト列そのものを指すので、圧縮されたデータは扱えない * @@ -795,12 +796,12 @@ export function readBytesAt( } /** - * バイト列に連なっている NBT をすべて読む + * バイト列に連なっているNBTをすべて読む * * 入力を使い切るまで読み続ける * 空のバイト列なら空の一覧を返す * - * 圧縮は入力全体に 1 回かかっているものとして扱う + * 圧縮は入力全体に1回かかっているものとして扱う * * @throws {SpringNbtError} 読み込みに失敗した場合 */ @@ -808,7 +809,7 @@ export function readBytesAll(bytes: Uint8Array, options?: NbtReadOptions): Named const effective = fillReadOptions(options); const tags: NamedTag[] = []; - // 空の入力は「0 個」であってエラーではない + // 空の入力は「0個」であってエラーではない if (bytes.length === 0) { return tags; } @@ -833,7 +834,7 @@ function requirePlainInput(options: Required): void { } } -/** ファイルから NBT を読む */ +/** ファイルからNBTを読む */ export function readFile(path: string, options?: NbtReadOptions): NamedTag { let raw: Uint8Array; @@ -847,11 +848,11 @@ export function readFile(path: string, options?: NbtReadOptions): NamedTag { return readBytes(raw, options); } -/** NBT をバイト列へ書き出す */ +/** NBTをバイト列へ書き出す */ export function writeBytes(named: NamedTag, options?: NbtWriteOptions): Uint8Array { const effective = fillWriteOptions(options); - // 書き込み時に Auto は決められない + // 書き込み時にAutoは決められない if (effective.compression === Compression.Auto) { throw SpringNbtError.invalidArgument("書き込みで Compression.Auto は指定できない"); } @@ -860,7 +861,7 @@ export function writeBytes(named: NamedTag, options?: NbtWriteOptions): Uint8Arr return compress(plain, effective.compression); } -/** NBT をファイルへ書き出す */ +/** NBTをファイルへ書き出す */ export function writeFile(path: string, named: NamedTag, options?: NbtWriteOptions): void { const bytes = writeBytes(named, options); diff --git a/typescript/src/nbt/mutf8.ts b/typescript/src/nbt/mutf8.ts index e585a19..fc57414 100644 --- a/typescript/src/nbt/mutf8.ts +++ b/typescript/src/nbt/mutf8.ts @@ -1,46 +1,43 @@ /** - * Modified UTF-8 (MUTF-8) の符号化・復号 + * Modified UTF-8 (MUTF-8)の符号化・復号 * - * 標準 UTF-8 との違いは 2 点だけ - * - `U+0000` を `C0 80` の 2 バイトで表す - * - `U+10000` 以上をサロゲートペアへ分解し、3 バイト × 2 で表す (CESU-8) + * 標準UTF-8との違いは2点だけ + * - `U+0000`を`C0 80`の2バイトで表す + * - `U+10000`以上をサロゲートペアへ分解し、3バイト × 2で表す (CESU-8) * - * JavaScript の `string` は UTF-16 コード単位の列なので、 - * サロゲートペアも孤立サロゲートもそのまま保持できる(C# / Java と同じ性質) - * - * 仕様: `docs/spec/10-nbt-binary.md` 2章 + * JavaScriptの`string`はUTF-16コード単位の列なので、サロゲートペアも孤立サロゲートもそのまま保持できる(C# / Javaと同じ性質) */ import { SpringNbtError } from "../errors.js"; -/** MUTF-8 の文字列が取りうる最大バイト長(長さフィールドが u16 のため) */ +/** MUTF-8の文字列が取りうる最大バイト長(長さフィールドがu16のため) */ export const MAX_BYTE_LENGTH = 65535; /** - * MUTF-8 バイト列を文字列へ復号する + * MUTF-8バイト列を文字列へ復号する * - * @throws {SpringNbtError} バイト列が MUTF-8 として不正な場合 + * @throws {SpringNbtError} バイト列がMUTF-8として不正な場合 */ export function decode(data: Uint8Array, offset = 0, length = data.length - offset): string { const units: number[] = []; let index = offset; const end = offset + length; - // 先頭から 1 文字ずつ取り出す + // 先頭から1文字ずつ取り出す while (index < end) { const b0 = data[index]; if ((b0 & 0x80) === 0x00) { - // 1 バイト形式: 0xxxxxxx (U+0001..U+007F) + // 1バイト形式: 0xxxxxxx (U+0001..U+007F) if (b0 === 0x00) { - // 素の 0x00 は MUTF-8 では現れてはならない (C0 80 を使う) + // 素の0x00はMUTF-8では現れてはならない (C0 80を使う) throw SpringNbtError.malformed("MUTF-8: 素の 0x00 が現れた (U+0000 は C0 80 で表す)"); } units.push(b0); index += 1; } else if ((b0 & 0xe0) === 0xc0) { - // 2 バイト形式: 110xxxxx 10xxxxxx + // 2バイト形式: 110xxxxx 10xxxxxx if (index + 1 >= end) { throw SpringNbtError.malformed("MUTF-8: 2バイト形式が途中で切れた"); } @@ -53,8 +50,8 @@ export function decode(data: Uint8Array, offset = 0, length = data.length - offs const value = ((b0 & 0x1f) << 6) | (b1 & 0x3f); - // C0 80 (U+0000) だけは正当 - // それ以外の 0x80 未満は冗長符号化 + // C0 80 (U+0000)だけは正当 + // それ以外の0x80未満は冗長符号化 if (value < 0x80 && !(b0 === 0xc0 && b1 === 0x80)) { throw SpringNbtError.malformed("MUTF-8: 冗長な2バイト符号化"); } @@ -62,7 +59,7 @@ export function decode(data: Uint8Array, offset = 0, length = data.length - offs units.push(value); index += 2; } else if ((b0 & 0xf0) === 0xe0) { - // 3 バイト形式: 1110xxxx 10xxxxxx 10xxxxxx + // 3バイト形式: 1110xxxx 10xxxxxx 10xxxxxx if (index + 2 >= end) { throw SpringNbtError.malformed("MUTF-8: 3バイト形式が途中で切れた"); } @@ -76,7 +73,7 @@ export function decode(data: Uint8Array, offset = 0, length = data.length - offs const value = ((b0 & 0x0f) << 12) | ((b1 & 0x3f) << 6) | (b2 & 0x3f); - // 3 バイトで表すべき範囲は U+0800 以上 + // 3バイトで表すべき範囲はU+0800以上 if (value < 0x800) { throw SpringNbtError.malformed("MUTF-8: 冗長な3バイト符号化"); } @@ -84,14 +81,14 @@ export function decode(data: Uint8Array, offset = 0, length = data.length - offs units.push(value); index += 3; } else { - // 4 バイト形式 (標準 UTF-8) や継続バイト単独は MUTF-8 では不正 + // 4バイト形式 (標準UTF-8)や継続バイト単独はMUTF-8では不正 throw SpringNbtError.malformed( `MUTF-8: 不正な先頭バイト 0x${b0.toString(16).toUpperCase().padStart(2, "0")}`, ); } } - // コード単位が多いと fromCharCode の引数上限に触れるため、分割して連結する + // コード単位が多いとfromCharCodeの引数の上限を超えるため、分割して連結する let result = ""; const chunkSize = 8192; @@ -104,16 +101,15 @@ export function decode(data: Uint8Array, offset = 0, length = data.length - offs } /** - * 文字列を MUTF-8 バイト列へ符号化する + * 文字列をMUTF-8バイト列へ符号化する * - * サロゲートは対になっているかどうかに関わらず 1 つずつ 3 バイトで符号化されるため、 - * 孤立サロゲートもそのまま往復できる + * サロゲートは対になっているかどうかに関わらず1つずつ3バイトで符号化されるため、孤立サロゲートもそのまま往復できる */ export function encode(text: string): Uint8Array { const buffer = new Uint8Array(byteLength(text)); let position = 0; - // コード単位ごとに 1〜3 バイトへ展開する + // コード単位ごとに1〜3バイトへ展開する for (let index = 0; index < text.length; index++) { const unit = text.charCodeAt(index); @@ -121,7 +117,7 @@ export function encode(text: string): Uint8Array { buffer[position] = unit; position += 1; } else if (unit === 0x0000 || unit <= 0x07ff) { - // U+0000 もこの経路で C0 80 になる + // U+0000もこの経路でC0 80になる buffer[position] = 0xc0 | ((unit >> 6) & 0x1f); buffer[position + 1] = 0x80 | (unit & 0x3f); position += 2; @@ -137,7 +133,7 @@ export function encode(text: string): Uint8Array { } /** - * 文字列を MUTF-8 で符号化したときのバイト長を求める + * 文字列をMUTF-8で符号化したときのバイト長を求める * 実際に符号化はしない */ export function byteLength(text: string): number { diff --git a/typescript/src/nbt/snbt.ts b/typescript/src/nbt/snbt.ts index 3baaf74..287fb88 100644 --- a/typescript/src/nbt/snbt.ts +++ b/typescript/src/nbt/snbt.ts @@ -1,10 +1,8 @@ /** - * SNBT (Stringified NBT) のパースと出力 + * SNBT (Stringified NBT)のパースと出力 * - * 対応範囲は「バイナリ NBT へ損失なく写せる部分集合」 - * 1.21.5 以降の異種リスト(`[1, "a"]`)は受理しない - * - * 仕様: `docs/spec/11-snbt.md` / `docs/adr/0006-snbt-scope.md` + * 対応範囲は「バイナリNBTへ損失なく写せる部分集合」 + * 1.21.5以降の異種リスト(`[1, "a"]`)は受理しない */ import { ErrorCode, SpringNbtError } from "../errors.js"; @@ -24,6 +22,7 @@ import { NbtString, NbtTag, TagType, + hasLoneSurrogate, tagTypeAsString, } from "./tag.js"; @@ -88,7 +87,7 @@ function isBinaryBody(body: string): boolean { return false; } - // 2進リテラルの本体は 0 と 1 だけ + // 2進リテラルの本体は0と1だけ for (let index = 2; index < body.length; index++) { if (body[index] !== "0" && body[index] !== "1") { return false; @@ -107,12 +106,12 @@ class Parser { #position = 0; constructor(text: string) { - // サロゲートペアを 2 コード単位のまま扱うため、コードポイント単位に分けない + // サロゲートペアを2コード単位のまま扱うため、コードポイント単位に分けない this.#chars = text.split(""); } /** - * 入力全体を 1 つのタグとして読む + * 入力全体を1つのタグとして読む * 末尾に余りがあればエラーにする */ parseWhole(): NbtTag { @@ -156,13 +155,13 @@ class Parser { const compound = new NbtCompound(); this.#skipWhitespace(); - // 空の Compound + // 空のCompound if (this.#peek() === "}") { this.#position += 1; return compound; } - // 要素を 1 つずつ読む + // 要素を1つずつ読む for (;;) { this.#skipWhitespace(); @@ -194,7 +193,7 @@ class Parser { #parseListOrArray(): NbtTag { this.#expect("["); - // "[B;" のような型付き配列かどうかを先に判定する + // "[B;"のような型付き配列かどうかを先に判定する if (this.#position + 1 < this.#chars.length && this.#chars[this.#position + 1] === ";") { const marker = this.#chars[this.#position]; @@ -217,7 +216,7 @@ class Parser { return list; } - // 要素を 1 つずつ読む + // 要素を1つずつ読む for (;;) { this.#skipWhitespace(); @@ -229,7 +228,7 @@ class Parser { const value = this.#parseValue(); - // 異種リストはバイナリ NBT へ写せないため受理しない (adr/0006) + // 異種リストはバイナリNBTへ写せないため受理しない if (list.elementType !== TagType.End && list.elementType !== value.type) { throw this.#malformed( `リストに異なる型が混在している: ${tagTypeAsString(list.elementType)} と ` + @@ -290,7 +289,7 @@ class Parser { if (marker === "B") { const result = new Int8Array(values.length); - // 各要素が Byte の範囲に収まるか確認しながら詰める + // 各要素がByteの範囲に収まるか確認しながら詰める for (let index = 0; index < values.length; index++) { const value = values[index]; @@ -307,7 +306,7 @@ class Parser { if (marker === "I") { const result = new Int32Array(values.length); - // 各要素が Int の範囲に収まるか確認しながら詰める + // 各要素がIntの範囲に収まるか確認しながら詰める for (let index = 0; index < values.length; index++) { const value = values[index]; @@ -343,8 +342,16 @@ class Parser { #parseKey(): string { const c = this.#peek(); + // 引用符で始まるキーは、エスケープを解いて読む if (c === '"' || c === "'") { - return this.#parseQuotedString(); + const quoted = this.#parseQuotedString(); + + // バイナリの読み込みと同じく、キーには孤立サロゲートを許さない + if (hasLoneSurrogate(quoted)) { + throw this.#malformed("Compound のキーが UTF-8 に写せない(孤立サロゲートを含む)"); + } + + return quoted; } const bare = this.#readBareToken(); @@ -403,7 +410,7 @@ class Parser { } if (c === "u") { - // \uXXXX は UTF-16 コード単位を直接指定する + // \uXXXXはUTF-16コード単位を直接指定する // 孤立サロゲートもここで書ける return String.fromCharCode(this.#readHexDigits(4)); } @@ -411,11 +418,12 @@ class Parser { if (c === "U") { const codePoint = this.#readHexDigits(8); - // Unicode のコードポイント範囲を外れていないか確認する + // Unicodeのコードポイント範囲を外れていないか確認する if (codePoint > 0x10ffff) { throw this.#malformed(`コードポイントが範囲外: U+${codePoint.toString(16).toUpperCase()}`); } + // サロゲートの範囲は、\uXXXXと同じくそのコード単位を1つ置く(String.fromCodePointはそう扱う) return String.fromCodePoint(codePoint); } @@ -433,7 +441,7 @@ class Parser { let value = 0; - // 指定桁数ぶん 16進数字を読む + // 指定桁数ぶん16進数字を読む for (let offset = 0; offset < count; offset++) { const c = this.#chars[this.#position + offset]; const digit = hexDigitValue(c); @@ -449,7 +457,7 @@ class Parser { return value; } - /** Unicode 文字名によるエスケープ `\N{...}` を読む */ + /** Unicode文字名によるエスケープ`\N{...}`を読む */ #readNamedCharacter(): SpringNbtError { const start = this.#position; @@ -460,10 +468,10 @@ class Parser { const name = this.#chars.slice(start, this.#position).join(""); - // 実装間で Unicode 文字名の表が揃わないため対応しない + // 実装間でUnicode文字名の表が揃わないため対応しない return new SpringNbtError( ErrorCode.UnsupportedFeature, - `文字名によるエスケープには対応していない: \\N${name}`, + `文字名によるエスケープには対応していない: \\N{${name}}`, ); } @@ -474,7 +482,7 @@ class Parser { throw this.#malformed(`値が来るべき位置に解釈できない文字がある: '${this.#peekOrEmpty()}'`); } - // bool(...) / uuid(...) の関数呼び出し + // bool(...) / uuid(...)の関数呼び出し this.#skipWhitespace(); if (this.#peekOrEmpty() === "(" && (token === "bool" || token === "uuid")) { return this.#parseFunction(token); @@ -504,7 +512,7 @@ class Parser { this.#expect(")"); if (name === "bool") { - // 0 以外を真とする + // 0以外を真とする if (this.#toIntegral(argument) !== 0n) { return new NbtByte(1); } @@ -528,7 +536,7 @@ class Parser { const result = new Int32Array(4); - // UUID を上位から 32bit ずつ 4 要素の IntArray へ写す + // UUIDを上位から32bitずつ4要素のIntArrayへ写す for (let index = 0; index < 4; index++) { result[index] = Number.parseInt(hex.slice(index * 8, (index + 1) * 8), 16) | 0; } @@ -538,7 +546,7 @@ class Parser { /** * 数値トークンを解釈する - * 数値として読めなければ undefined(文字列として扱われる) + * 数値として読めなければundefinedを返す(文字列として扱われる) */ #tryParseNumber(token: string): NbtTag | undefined { let negative = false; @@ -562,7 +570,7 @@ class Parser { const last = body[body.length - 1]; // 幅接尾辞を末尾から剥がす - // 16進では b/d/f が数字と紛れるため s/l だけを認める + // 16進ではb/d/fが数字と紛れるためs/lだけを認める let suffixAllowed: boolean; if (hexBody) { suffixAllowed = last === "s" || last === "S" || last === "l" || last === "L"; @@ -574,7 +582,7 @@ class Parser { widthSuffix = last.toLowerCase(); body = body.slice(0, -1); - // 符号接尾辞 u / s は幅接尾辞の手前に置かれる + // 符号接尾辞u / sは幅接尾辞の手前に置かれる if (body.length >= 2) { const signChar = body[body.length - 1]; @@ -636,7 +644,7 @@ class Parser { return new NbtFloat(signed); } - // 接尾辞なしの小数は Double + // 接尾辞なしの小数はDouble if (widthSuffix === "" || widthSuffix === "d") { return new NbtDouble(signed); } @@ -668,7 +676,7 @@ class Parser { magnitude = magnitude * radixBig + BigInt(digit); - // 符号なし 64bit を超えたらその場で打ち切る + // 符号なし64bitを超えたらその場で打ち切る if (magnitude > 0xffffffffffffffffn) { throw this.#malformed(`整数が大きすぎる: ${digits}`); } @@ -730,8 +738,8 @@ class Parser { return new NbtDouble(Number(value)); } - // 接尾辞なしの整数は Int - // 暗黙に Long へ格上げしない + // 接尾辞なしの整数はInt + // 暗黙にLongへ格上げしない return new NbtInt(Number(this.#checkRange(value, -2147483648n, 2147483647n, "int"))); } @@ -808,12 +816,12 @@ class Parser { } } -/** SNBT 文字列をタグへ変換する */ +/** SNBT文字列をタグへ変換する */ export function parse(text: string): NbtTag { return new Parser(text).parseWhole(); } -/** SNBT 文字列を Compound へ変換する */ +/** SNBT文字列をCompoundへ変換する */ export function parseCompound(text: string): NbtCompound { const tag = parse(text); @@ -828,7 +836,7 @@ export function parseCompound(text: string): NbtCompound { // ライタ // --------------------------------------------------------------------------- -/** タグを 1 行の SNBT へ変換する */ +/** タグを1行のSNBTへ変換する */ export function write(tag: NbtTag): string { const parts: string[] = []; writeTag(parts, tag, -1); @@ -836,8 +844,8 @@ export function write(tag: NbtTag): string { } /** - * タグを整形した SNBT へ変換する - * インデントは空白 4 個 + * タグを整形したSNBTへ変換する + * インデントは空白4個 */ export function writePretty(tag: NbtTag): string { const parts: string[] = []; @@ -847,7 +855,7 @@ export function writePretty(tag: NbtTag): string { /** * タグを書き出す - * `depth` が負なら 1 行、0 以上なら整形して出力する + * `depth`が負なら1行、0以上なら整形して出力する */ function writeTag(parts: string[], tag: NbtTag, depth: number): void { switch (tag.type) { @@ -956,7 +964,7 @@ function writeTypedArray( ): void { parts.push(`[${marker};`); - // 型付き配列は 1 行に収める + // 型付き配列は1行に収める for (let index = 0; index < values.length; index++) { if (index > 0) { parts.push(","); @@ -968,7 +976,7 @@ function writeTypedArray( parts.push("]"); } -/** 整形出力なら改行とインデントを、1 行出力なら何も入れない */ +/** 整形出力なら改行とインデントを、1行出力なら何も入れない */ function appendSeparator(parts: string[], depth: number): void { if (depth < 0) { return; @@ -978,7 +986,7 @@ function appendSeparator(parts: string[], depth: number): void { parts.push(INDENT_UNIT.repeat(depth)); } -/** 整形出力のときだけ深さを 1 段進める */ +/** 整形出力のときだけ深さを1段進める */ function nextDepth(depth: number): number { if (depth < 0) { return -1; @@ -1027,15 +1035,14 @@ const QUOTE_ESCAPES = new Map([ /** * 文字列を二重引用符で囲み、必要な文字だけエスケープする * - * UTF-16 コード単位で処理するのは、正しいサロゲートペアと孤立サロゲートを - * 区別するため - * ここを誤ると Python / Rust と出力が食い違う + * UTF-16コード単位で処理するのは、正しいサロゲートペアと孤立サロゲートを区別するため + * ここを誤るとPython / Rustと出力が食い違う */ function quoteString(text: string): string { let result = '"'; let index = 0; - // 1 コード単位ずつ見てエスケープが要るものだけ置き換える + // 1コード単位ずつ見てエスケープが要るものだけ置き換える while (index < text.length) { const unit = text.charCodeAt(index); const escaped = QUOTE_ESCAPES.get(unit); @@ -1058,7 +1065,7 @@ function quoteString(text: string): string { } if (unit < 0x20 || unit === 0x7f || (unit >= 0xd800 && unit <= 0xdfff)) { - // 制御文字と孤立サロゲートは \uXXXX で表す + // 制御文字と孤立サロゲートは\uXXXXで表す result += `\\u${unit.toString(16).padStart(4, "0")}`; } else { result += text[index]; diff --git a/typescript/src/nbt/tag.ts b/typescript/src/nbt/tag.ts index e4d9d17..6217b58 100644 --- a/typescript/src/nbt/tag.ts +++ b/typescript/src/nbt/tag.ts @@ -1,20 +1,18 @@ /** - * NBT のタグ型と値モデル - * - * 仕様: `docs/spec/10-nbt-binary.md` 1章・7章 + * NBTのタグ型と値モデル */ import { SpringNbtError } from "../errors.js"; import * as mutf8 from "./mutf8.js"; /** - * NBT のタグ型 + * NBTのタグ型 * 値は仕様が定めるタグIDと一致する */ export enum TagType { /** * TAG_End (0) - * Compound の終端を表す + * Compoundの終端を表す */ End = 0, /** TAG_Byte (1) */ @@ -65,12 +63,12 @@ export function tagTypeAsString(type: TagType): string { } /** - * タグIDから {@link TagType} を得る + * タグIDから{@link TagType}を得る * * @throws {SpringNbtError} 未知のタグIDの場合 */ export function tagTypeFromId(id: number): TagType { - // 0..12 の範囲外はすべて不正なタグID + // 0..12の範囲外はすべて不正なタグID if (id < 0 || id > TagType.LongArray) { throw SpringNbtError.malformed(`未知のタグID: ${id}`); } @@ -79,10 +77,10 @@ export function tagTypeFromId(id: number): TagType { } /** - * Number 型で保持する整数の範囲を検査する + * Number型で保持する整数の範囲を検査する * - * JavaScript の number は倍精度なので、i32 までは正確に扱えるが幅の情報を持たない - * 構築時に検査しておかないと、書き出しで黙って切り詰められる + * JavaScriptのnumberは倍精度なので、i32までは正確に扱えるが幅の情報を持たない + * 構築時に検査しておかないと、書き出しで知らないうちに切り詰められる */ function checkRange(value: number, minimum: number, maximum: number, typeName: string): number { if (!Number.isInteger(value)) { @@ -102,8 +100,8 @@ function checkRange(value: number, minimum: number, maximum: number, typeName: s const BIT_VIEW = new DataView(new ArrayBuffer(8)); /** - * binary32 のビットパターンを取り出す - * NaN や -0.0 を値ではなくビットで区別するために使う + * binary32のビットパターンを取り出す + * NaNや-0.0を値ではなくビットで区別するために使う */ function floatBits(value: number): number { BIT_VIEW.setFloat32(0, value); @@ -111,8 +109,8 @@ function floatBits(value: number): number { } /** - * binary64 のビットパターンを取り出す - * NaN や -0.0 を値ではなくビットで区別するために使う + * binary64のビットパターンを取り出す + * NaNや-0.0を値ではなくビットで区別するために使う */ function doubleBits(value: number): bigint { BIT_VIEW.setFloat64(0, value); @@ -128,7 +126,7 @@ function arrayEquals( return false; } - // 同じ位置の要素同士を突き合わせる + // 同じ位置の要素同士を比較する for (let index = 0; index < left.length; index++) { if (left[index] !== right[index]) { return false; @@ -140,7 +138,7 @@ function arrayEquals( /** * TAG_Byte - * 8bit 符号付き整数 + * 8bit符号付き整数 */ export class NbtByte { readonly type = TagType.Byte as const; @@ -157,7 +155,7 @@ export class NbtByte { /** * 値を差し替える - * 範囲外なら INVALID_ARGUMENT + * 範囲外ならINVALID_ARGUMENT */ set value(newValue: number) { this.#value = checkRange(newValue, -128, 127, "byte"); @@ -168,7 +166,10 @@ export class NbtByte { return new NbtByte(this.#value); } - /** 人が読むための表現。中身の形式は決めていない */ + /** + * 人が読むための表現 + * 中身の形式は決めていない + */ toString(): string { return `${this.#value}b`; } @@ -181,7 +182,7 @@ export class NbtByte { /** * TAG_Short - * 16bit 符号付き整数 + * 16bit符号付き整数 */ export class NbtShort { readonly type = TagType.Short as const; @@ -198,7 +199,7 @@ export class NbtShort { /** * 値を差し替える - * 範囲外なら INVALID_ARGUMENT + * 範囲外ならINVALID_ARGUMENT */ set value(newValue: number) { this.#value = checkRange(newValue, -32768, 32767, "short"); @@ -209,7 +210,10 @@ export class NbtShort { return new NbtShort(this.#value); } - /** 人が読むための表現。中身の形式は決めていない */ + /** + * 人が読むための表現 + * 中身の形式は決めていない + */ toString(): string { return `${this.#value}s`; } @@ -222,7 +226,7 @@ export class NbtShort { /** * TAG_Int - * 32bit 符号付き整数 + * 32bit符号付き整数 */ export class NbtInt { readonly type = TagType.Int as const; @@ -239,7 +243,7 @@ export class NbtInt { /** * 値を差し替える - * 範囲外なら INVALID_ARGUMENT + * 範囲外ならINVALID_ARGUMENT */ set value(newValue: number) { this.#value = checkRange(newValue, -2147483648, 2147483647, "int"); @@ -250,7 +254,10 @@ export class NbtInt { return new NbtInt(this.#value); } - /** 人が読むための表現。中身の形式は決めていない */ + /** + * 人が読むための表現 + * 中身の形式は決めていない + */ toString(): string { return `${this.#value}`; } @@ -266,11 +273,11 @@ const LONG_MAX = 9223372036854775807n; /** * TAG_Long - * 64bit 符号付き整数 + * 64bit符号付き整数 * - * 値は `bigint` で保持する - * JavaScript の `number` は倍精度なので、 - * 2^53 を超える整数を正確に表せず、そのまま使うとワールドのシード値などが壊れる + * 値は`bigint`で保持する + * JavaScriptの`number`は倍精度なので、2^53を超える整数を正確に表せない + * そのまま使うとワールドのシード値などがおかしくなる */ export class NbtLong { readonly type = TagType.Long as const; @@ -287,7 +294,7 @@ export class NbtLong { /** * 値を差し替える - * 範囲外なら INVALID_ARGUMENT + * 範囲外ならINVALID_ARGUMENT */ set value(newValue: bigint) { this.#value = NbtLong.#check(newValue); @@ -298,7 +305,10 @@ export class NbtLong { return new NbtLong(this.#value); } - /** 人が読むための表現。中身の形式は決めていない */ + /** + * 人が読むための表現 + * 中身の形式は決めていない + */ toString(): string { return `${this.#value}L`; } @@ -321,6 +331,15 @@ export class NbtLong { } } +/** + * 読み込んだときのbinary32のビットパターン + * + * numberはbinary64なので、シグナリングNaNはbinary32との変換で静かなNaNへ変わってしまう + * 読んだビットをここに覚えておき、値を変えていなければそのまま書き戻す + * 値を差し替えたタグは表から外す + */ +const readFloatBits = new WeakMap(); + /** * TAG_Float * IEEE 754 binary32 @@ -330,7 +349,7 @@ export class NbtFloat { #value: number; constructor(value: number) { - // 他言語と同じ値になるよう、構築時に binary32 へ丸める + // 他言語と同じ値になるよう、構築時にbinary32へ丸める this.#value = Math.fround(value); } @@ -341,31 +360,103 @@ export class NbtFloat { /** * 値を差し替える - * 範囲外なら INVALID_ARGUMENT + * 構築時と同じくbinary32へ丸める */ set value(newValue: number) { this.#value = Math.fround(newValue); + + // 値を変えたら、読み込んだときのビットパターンはもう使わない + readFloatBits.delete(this); } /** このタグの深いコピーを作る */ copy(): NbtFloat { - return new NbtFloat(this.#value); + const result = new NbtFloat(this.#value); + const bits = readFloatBits.get(this); + + // 読み込んだビットパターンを持っていれば、複製にも引き継ぐ + if (bits !== undefined) { + readFloatBits.set(result, bits); + } + + return result; } - /** 人が読むための表現。中身の形式は決めていない */ + /** + * 人が読むための表現 + * 中身の形式は決めていない + */ toString(): string { return `${this.#value}f`; } /** * 同じ型で同じ値か - * NaN や -0.0 を区別するため、値ではなくビットパターンで比べる + * NaNや-0.0を区別するため、値ではなくビットパターンで比べる */ equals(other: unknown): boolean { - return other instanceof NbtFloat && floatBits(other.value) === floatBits(this.#value); + return other instanceof NbtFloat && float32BitsOf(other) === float32BitsOf(this); + } +} + +/** + * binary32のビットパターンからタグを作る + * 読み込んだビットを覚えておき、書き戻すときにそのまま使う(ライブラリ内部用) + */ +function float32FromBits(bits: number): NbtFloat { + BIT_VIEW.setInt32(0, bits); + const tag = new NbtFloat(BIT_VIEW.getFloat32(0)); + readFloatBits.set(tag, bits | 0); + return tag; +} + +/** + * タグのbinary32のビットパターンを返す + * 読み込んだときのビットを覚えていればそれを、無ければ値から求める(ライブラリ内部用) + */ +function float32BitsOf(tag: NbtFloat): number { + const bits = readFloatBits.get(tag); + + // 読み込んだままのタグは、読んだときのビットを使う + if (bits !== undefined) { + return bits; + } + + return floatBits(tag.value); +} + +/** + * 対になっていないサロゲートを含むか(ライブラリ内部用) + * + * 正しいサロゲートペアは1文字として数え、孤立したものだけを探す + * Compoundのキーには孤立サロゲートを許さないので、その検査に使う + */ +function hasLoneSurrogate(text: string): boolean { + // コード単位を1つずつ見て、サロゲート対をまとめる + for (let index = 0; index < text.length; index++) { + const unit = text.charCodeAt(index); + + // 上位サロゲートは、対になる下位サロゲートとまとめて1文字を成す + if (unit >= 0xd800 && unit <= 0xdbff) { + const next = text.charCodeAt(index + 1); + + // 対が揃っていなければ孤立サロゲート + if (!(next >= 0xdc00 && next <= 0xdfff)) { + return true; + } + + index += 1; + } else if (unit >= 0xdc00 && unit <= 0xdfff) { + // 上位サロゲートを伴わない下位サロゲートは孤立している + return true; + } } + + return false; } +export { float32BitsOf, float32FromBits, hasLoneSurrogate }; + /** * TAG_Double * IEEE 754 binary64 @@ -380,14 +471,17 @@ export class NbtDouble { return new NbtDouble(this.value); } - /** 人が読むための表現。中身の形式は決めていない */ + /** + * 人が読むための表現 + * 中身の形式は決めていない + */ toString(): string { return `${this.value}d`; } /** * 同じ型で同じ値か - * NaN や -0.0 を区別するため、値ではなくビットパターンで比べる + * NaNや-0.0を区別するため、値ではなくビットパターンで比べる */ equals(other: unknown): boolean { return other instanceof NbtDouble && doubleBits(other.value) === doubleBits(this.value); @@ -396,7 +490,7 @@ export class NbtDouble { /** * TAG_String - * MUTF-8 で符号化される文字列 + * MUTF-8で符号化される文字列 */ export class NbtString { readonly type = TagType.String as const; @@ -413,7 +507,7 @@ export class NbtString { /** * 値を差し替える - * 範囲外なら INVALID_ARGUMENT + * MUTF-8で65535バイトを超えるとINVALID_ARGUMENT */ set value(newValue: string) { this.#value = NbtString.#check(newValue); @@ -424,7 +518,10 @@ export class NbtString { return new NbtString(this.#value); } - /** 人が読むための表現。中身の形式は決めていない */ + /** + * 人が読むための表現 + * 中身の形式は決めていない + */ toString(): string { return this.#value; } @@ -437,8 +534,8 @@ export class NbtString { static #check(value: string): string { const length = mutf8.byteLength(value); - // 長さフィールドは u16 - // 65535 を超えると書き出せない + // 長さフィールドはu16 + // 65535を超えると書き出せない if (length > mutf8.MAX_BYTE_LENGTH) { throw SpringNbtError.invalidArgument( `文字列が長すぎる: MUTF-8 で ${length} バイト (上限 ${mutf8.MAX_BYTE_LENGTH})`, @@ -451,7 +548,7 @@ export class NbtString { /** * TAG_Byte_Array - * 8bit 符号付き整数の配列 + * 8bit符号付き整数の配列 */ export class NbtByteArray { readonly type = TagType.ByteArray as const; @@ -463,7 +560,10 @@ export class NbtByteArray { return new NbtByteArray(this.value.slice()); } - /** 人が読むための表現。中身の形式は決めていない */ + /** + * 人が読むための表現 + * 中身の形式は決めていない + */ toString(): string { return `[B; ${this.value.length} 要素]`; } @@ -476,7 +576,7 @@ export class NbtByteArray { /** * TAG_Int_Array - * 32bit 符号付き整数の配列 + * 32bit符号付き整数の配列 */ export class NbtIntArray { readonly type = TagType.IntArray as const; @@ -488,7 +588,10 @@ export class NbtIntArray { return new NbtIntArray(this.value.slice()); } - /** 人が読むための表現。中身の形式は決めていない */ + /** + * 人が読むための表現 + * 中身の形式は決めていない + */ toString(): string { return `[I; ${this.value.length} 要素]`; } @@ -501,7 +604,7 @@ export class NbtIntArray { /** * TAG_Long_Array - * 64bit 符号付き整数の配列 + * 64bit符号付き整数の配列 */ export class NbtLongArray { readonly type = TagType.LongArray as const; @@ -513,7 +616,10 @@ export class NbtLongArray { return new NbtLongArray(this.value.slice()); } - /** 人が読むための表現。中身の形式は決めていない */ + /** + * 人が読むための表現 + * 中身の形式は決めていない + */ toString(): string { return `[L; ${this.value.length} 要素]`; } @@ -526,12 +632,11 @@ export class NbtLongArray { /** * TAG_List - * 要素型が 1 つに固定されたタグの列 + * 要素型が1つに固定されたタグの列 * - * 空リストの要素型は {@link TagType.End} + * 空リストの要素型は{@link TagType.End} * 最初の要素を追加した時点で型が確定する - * 全要素を削除しても確定済みの要素型は維持される - * (読み書きの往復で型が消えないようにするため) + * 読み書きの往復で型が消えないよう、全要素を削除しても確定済みの要素型は維持される */ export class NbtList { readonly type = TagType.List as const; @@ -541,9 +646,9 @@ export class NbtList { constructor(elementType: TagType = TagType.End, elements?: Iterable) { this.#elementType = elementType; - // 与えられた要素は 1 つずつ型検査しながら追加する + // 与えられた要素は1つずつ型検査しながら追加する if (elements !== undefined) { - // add を通すことで、要素型の検査も一緒にかかる + // addを通すことで、要素型の検査も一緒にかかる for (const element of elements) { this.add(element); } @@ -552,7 +657,7 @@ export class NbtList { /** * 要素の型 - * 空で未確定なら {@link TagType.End} + * 空で未確定なら{@link TagType.End} */ get elementType(): TagType { return this.#elementType; @@ -563,13 +668,24 @@ export class NbtList { return this.#items.length; } - /** 位置を指定して取り出す */ + /** + * 位置を指定して取り出す + * + * @throws RangeError 位置が範囲外の場合 + */ get(index: number): NbtTag { + this.#checkIndex(index, this.#items.length - 1); return this.#items[index]; } - /** 位置を指定して置き換える */ + /** + * 位置を指定して置き換える + * + * @throws RangeError 位置が範囲外の場合 + */ set(index: number, item: NbtTag): void { + // 要素型を確定させる前に位置を確かめ、失敗したときに状態を変えない + this.#checkIndex(index, this.#items.length - 1); this.#ensureElementType(item); this.#items[index] = item; } @@ -580,17 +696,36 @@ export class NbtList { this.#items.push(item); } - /** 位置を指定して挿入する */ + /** + * 位置を指定して挿入する + * + * @throws RangeError 位置が範囲外の場合 + */ insert(index: number, item: NbtTag): void { + // 要素型を確定させる前に位置を確かめ、失敗したときに状態を変えない + this.#checkIndex(index, this.#items.length); this.#ensureElementType(item); this.#items.splice(index, 0, item); } - /** 位置を指定して削除する */ + /** + * 位置を指定して削除する + * + * @throws RangeError 位置が範囲外の場合 + */ removeAt(index: number): void { + this.#checkIndex(index, this.#items.length - 1); this.#items.splice(index, 1); } + /** 位置が0以上`max`以下の整数であることを確かめる */ + #checkIndex(index: number, max: number): void { + // 配列の範囲外へ書くと穴が空くので、他言語と同じく例外にする + if (!Number.isInteger(index) || index < 0 || index > max) { + throw new RangeError(`位置が範囲外: ${index}`); + } + } + /** * 全要素を削除する * 確定済みの要素型は維持する @@ -611,7 +746,10 @@ export class NbtList { return result; } - /** 人が読むための表現。中身の形式は決めていない */ + /** + * 人が読むための表現 + * 中身の形式は決めていない + */ toString(): string { return `[${tagTypeAsString(this.#elementType)}; ${this.#items.length} 要素]`; } @@ -662,7 +800,7 @@ export class NbtList { * 挿入順を保持する、名前付きタグのマップ * * 既存キーへの再設定は位置を維持したまま値だけを置き換える - * (`Map` の既定の振る舞い) + * (`Map`の既定の振る舞い) */ export class NbtCompound { readonly type = TagType.Compound as const; @@ -693,50 +831,55 @@ export class NbtCompound { * 既存キーなら位置を維持して値だけ置き換える */ set(key: string, value: NbtTag): void { + // 孤立サロゲートを含むキーは書き出すと読み戻せないので、ここで止める + if (hasLoneSurrogate(key)) { + throw SpringNbtError.invalidArgument("キーに孤立サロゲートは使えない"); + } + this.#entries.set(key, value); } // -- 型付き設定子 ------------------------------------------------------- // - // set(key, new NbtInt(42)) と書かずに済むようにするための糖衣 - // 取得子の getInt と対になる + // set(key, new NbtInt(42))と書かずに済むようにするための糖衣 + // 取得子のgetIntと対になる - /** TAG_Byte として設定する */ + /** TAG_Byteとして設定する */ setByte(key: string, value: number): void { this.set(key, new NbtByte(value)); } - /** TAG_Short として設定する */ + /** TAG_Shortとして設定する */ setShort(key: string, value: number): void { this.set(key, new NbtShort(value)); } - /** TAG_Int として設定する */ + /** TAG_Intとして設定する */ setInt(key: string, value: number): void { this.set(key, new NbtInt(value)); } - /** TAG_Long として設定する */ + /** TAG_Longとして設定する */ setLong(key: string, value: bigint): void { this.set(key, new NbtLong(value)); } - /** TAG_Float として設定する */ + /** TAG_Floatとして設定する */ setFloat(key: string, value: number): void { this.set(key, new NbtFloat(value)); } - /** TAG_Double として設定する */ + /** TAG_Doubleとして設定する */ setDouble(key: string, value: number): void { this.set(key, new NbtDouble(value)); } /** - * TAG_Byte として設定する - * true は 1、false は 0 + * TAG_Byteとして設定する + * trueは1、falseは0 */ setBool(key: string, value: boolean): void { - // NBT に真偽値の専用型は無いので TAG_Byte の 0 / 1 で表す + // NBTに真偽値の専用型は無いので、TAG_Byteの0 / 1で表す if (value) { this.setByte(key, 1); } else { @@ -745,31 +888,31 @@ export class NbtCompound { } /** - * TAG_String として設定する - * MUTF-8 で 65535 バイトを超えると INVALID_ARGUMENT + * TAG_Stringとして設定する + * MUTF-8で65535バイトを超えるとINVALID_ARGUMENT */ setString(key: string, value: string): void { this.set(key, new NbtString(value)); } - /** TAG_Byte_Array として設定する */ + /** TAG_Byte_Arrayとして設定する */ setByteArray(key: string, value: Int8Array): void { this.set(key, new NbtByteArray(value)); } - /** TAG_Int_Array として設定する */ + /** TAG_Int_Arrayとして設定する */ setIntArray(key: string, value: Int32Array): void { this.set(key, new NbtIntArray(value)); } - /** TAG_Long_Array として設定する */ + /** TAG_Long_Arrayとして設定する */ setLongArray(key: string, value: BigInt64Array): void { this.set(key, new NbtLongArray(value)); } /** * キーに対応するタグを返す - * 存在しなければ undefined + * 存在しなければundefined */ opt(key: string): NbtTag | undefined { return this.#entries.get(key); @@ -791,7 +934,7 @@ export class NbtCompound { /** * キーを削除する - * 削除できたら true + * 削除できたらtrue */ remove(key: string): boolean { return this.#entries.delete(key); @@ -814,7 +957,10 @@ export class NbtCompound { return result; } - /** 人が読むための表現。中身の形式は決めていない */ + /** + * 人が読むための表現 + * 中身の形式は決めていない + */ toString(): string { return `{${this.#entries.size} 要素}`; } @@ -830,7 +976,7 @@ export class NbtCompound { const right = other.entries(); - // 挿入順にキーと値を突き合わせる + // キーと値を挿入順に比較する for (const [key, value] of this.#entries) { const pair = right.next(); @@ -849,19 +995,19 @@ export class NbtCompound { // -- 型付き取得子 ------------------------------------------------------- // // 「キーが無い」と「型が違う」は区別する - // opt* はキーが無ければ undefined を返し、get* は例外を送出する - // どちらも型が違えば必ず UNEXPECTED_TAG_TYPE の例外になる + // opt*はキーが無ければundefinedを返し、get*は例外を送出する + // どちらも型が違えば必ずUNEXPECTED_TAG_TYPEの例外になる /** - * TAG_Byte を取得する - * キーが無ければ undefined + * TAG_Byteを取得する + * キーが無ければundefined */ optByte(key: string): number | undefined { return this.#optScalar(key, TagType.Byte) as number | undefined; } /** - * TAG_Byte を取得する + * TAG_Byteを取得する * キーが無ければ例外 */ getByte(key: string): number { @@ -869,15 +1015,15 @@ export class NbtCompound { } /** - * TAG_Short を取得する - * キーが無ければ undefined + * TAG_Shortを取得する + * キーが無ければundefined */ optShort(key: string): number | undefined { return this.#optScalar(key, TagType.Short) as number | undefined; } /** - * TAG_Short を取得する + * TAG_Shortを取得する * キーが無ければ例外 */ getShort(key: string): number { @@ -885,15 +1031,15 @@ export class NbtCompound { } /** - * TAG_Int を取得する - * キーが無ければ undefined + * TAG_Intを取得する + * キーが無ければundefined */ optInt(key: string): number | undefined { return this.#optScalar(key, TagType.Int) as number | undefined; } /** - * TAG_Int を取得する + * TAG_Intを取得する * キーが無ければ例外 */ getInt(key: string): number { @@ -901,15 +1047,15 @@ export class NbtCompound { } /** - * TAG_Long を取得する - * キーが無ければ undefined + * TAG_Longを取得する + * キーが無ければundefined */ optLong(key: string): bigint | undefined { return this.#optScalar(key, TagType.Long) as bigint | undefined; } /** - * TAG_Long を取得する + * TAG_Longを取得する * キーが無ければ例外 */ getLong(key: string): bigint { @@ -917,15 +1063,15 @@ export class NbtCompound { } /** - * TAG_Float を取得する - * キーが無ければ undefined + * TAG_Floatを取得する + * キーが無ければundefined */ optFloat(key: string): number | undefined { return this.#optScalar(key, TagType.Float) as number | undefined; } /** - * TAG_Float を取得する + * TAG_Floatを取得する * キーが無ければ例外 */ getFloat(key: string): number { @@ -933,15 +1079,15 @@ export class NbtCompound { } /** - * TAG_Double を取得する - * キーが無ければ undefined + * TAG_Doubleを取得する + * キーが無ければundefined */ optDouble(key: string): number | undefined { return this.#optScalar(key, TagType.Double) as number | undefined; } /** - * TAG_Double を取得する + * TAG_Doubleを取得する * キーが無ければ例外 */ getDouble(key: string): number { @@ -949,9 +1095,9 @@ export class NbtCompound { } /** - * TAG_Byte を真偽値として取得する - * 0 以外が true - * キーが無ければ undefined + * TAG_Byteを真偽値として取得する + * 0以外がtrue + * キーが無ければundefined */ optBool(key: string): boolean | undefined { const raw = this.optByte(key); @@ -964,8 +1110,8 @@ export class NbtCompound { } /** - * TAG_Byte を真偽値として取得する - * 0 以外が true + * TAG_Byteを真偽値として取得する + * 0以外がtrue * キーが無ければ例外 */ getBool(key: string): boolean { @@ -973,15 +1119,15 @@ export class NbtCompound { } /** - * TAG_String を取得する - * キーが無ければ undefined + * TAG_Stringを取得する + * キーが無ければundefined */ optString(key: string): string | undefined { return this.#optScalar(key, TagType.String) as string | undefined; } /** - * TAG_String を取得する + * TAG_Stringを取得する * キーが無ければ例外 */ getString(key: string): string { @@ -989,15 +1135,15 @@ export class NbtCompound { } /** - * TAG_Byte_Array を取得する - * キーが無ければ undefined + * TAG_Byte_Arrayを取得する + * キーが無ければundefined */ optByteArray(key: string): Int8Array | undefined { return this.#optScalar(key, TagType.ByteArray) as Int8Array | undefined; } /** - * TAG_Byte_Array を取得する + * TAG_Byte_Arrayを取得する * キーが無ければ例外 */ getByteArray(key: string): Int8Array { @@ -1005,15 +1151,15 @@ export class NbtCompound { } /** - * TAG_Int_Array を取得する - * キーが無ければ undefined + * TAG_Int_Arrayを取得する + * キーが無ければundefined */ optIntArray(key: string): Int32Array | undefined { return this.#optScalar(key, TagType.IntArray) as Int32Array | undefined; } /** - * TAG_Int_Array を取得する + * TAG_Int_Arrayを取得する * キーが無ければ例外 */ getIntArray(key: string): Int32Array { @@ -1021,15 +1167,15 @@ export class NbtCompound { } /** - * TAG_Long_Array を取得する - * キーが無ければ undefined + * TAG_Long_Arrayを取得する + * キーが無ければundefined */ optLongArray(key: string): BigInt64Array | undefined { return this.#optScalar(key, TagType.LongArray) as BigInt64Array | undefined; } /** - * TAG_Long_Array を取得する + * TAG_Long_Arrayを取得する * キーが無ければ例外 */ getLongArray(key: string): BigInt64Array { @@ -1037,15 +1183,15 @@ export class NbtCompound { } /** - * TAG_List を取得する - * キーが無ければ undefined + * TAG_Listを取得する + * キーが無ければundefined */ optList(key: string): NbtList | undefined { return this.#castTag(key, TagType.List) as NbtList | undefined; } /** - * TAG_List を取得する + * TAG_Listを取得する * キーが無ければ例外 */ getList(key: string): NbtList { @@ -1053,15 +1199,15 @@ export class NbtCompound { } /** - * TAG_Compound を取得する - * キーが無ければ undefined + * TAG_Compoundを取得する + * キーが無ければundefined */ optCompound(key: string): NbtCompound | undefined { return this.#castTag(key, TagType.Compound) as NbtCompound | undefined; } /** - * TAG_Compound を取得する + * TAG_Compoundを取得する * キーが無ければ例外 */ getCompound(key: string): NbtCompound { @@ -1070,7 +1216,7 @@ export class NbtCompound { /** * キーに対応するタグを目的の型として取り出す - * キーが無ければ undefined、型が違えば例外 + * キーが無ければundefined、型が違えば例外 */ #castTag(key: string, expected: TagType): NbtTag | undefined { const tag = this.#entries.get(key); @@ -1110,10 +1256,8 @@ export class NbtCompound { } /** - * NBT のタグ - * 判別可能な合併型なので、`switch (tag.type)` で網羅的に分岐できる - * - * 仕様: `docs/spec/10-nbt-binary.md` 1章 + * NBTのタグ + * 判別可能な合併型なので、`switch (tag.type)`で網羅的に分岐できる */ export type NbtTag = | NbtByte diff --git a/typescript/src/world/bitStorage.ts b/typescript/src/world/bitStorage.ts index 90e2967..f94e730 100644 --- a/typescript/src/world/bitStorage.ts +++ b/typescript/src/world/bitStorage.ts @@ -1,20 +1,18 @@ /** - * 添字を 64bit 整数の配列へ詰めた表現 - * 1.16 以降の**跨ぎなし**パッキング + * 添字を64bit整数の配列へ詰めた表現 + * 1.16以降の**跨ぎなし**パッキング * - * 1 つの `bigint` に入りきらない分は、その値の残りビットを未使用のまま捨て、 + * 1つの`bigint`に入りきらない分は、その値の残りビットを未使用のまま捨て、 * 次の値の最下位ビットから始める - * - * 仕様: `docs/spec/31-paletted-container.md` 2章 */ import { SpringNbtError } from "../errors.js"; /** - * 添字を 64bit 整数の配列へ詰めた表現 - * 1.16 以降の**跨ぎなし**パッキング + * 添字を64bit整数の配列へ詰めた表現 + * 1.16以降の**跨ぎなし**パッキング * - * 1 つの `bigint` に入りきらない分は、その値の残りビットを未使用のまま捨て、 + * 1つの`bigint`に入りきらない分は、その値の残りビットを未使用のまま捨て、 * 次の値の最下位ビットから始める */ export class BitStorage { @@ -28,20 +26,20 @@ export class BitStorage { this.#entryCount = entryCount; } - /** 1 エントリあたりのビット数 */ + /** 1エントリあたりのビット数 */ get bitsPerEntry(): number { return this.#bitsPerEntry; } /** * エントリ数 - * ブロックなら 4096、バイオームなら 64 + * ブロックなら4096、バイオームなら64 */ get entryCount(): number { return this.#entryCount; } - /** 1 つの 64bit 値に入るエントリ数 */ + /** 1つの64bit値に入るエントリ数 */ get valuesPerLong(): number { return Math.floor(64 / this.#bitsPerEntry); } @@ -60,9 +58,10 @@ export class BitStorage { } /** - * 既存の 64bit 配列から作る + * 既存の64bit配列から作る * - * @param lenient true なら配列長からビット幅を逆算して読む(第三者ツール由来の救済) + * @param lenient trueなら、配列長が期待値と違う場合に配列長からビット幅を逆算して読む + * 第三者ツールが書いたデータの救済用 */ static fromLongs( data: BigInt64Array, @@ -95,7 +94,7 @@ export class BitStorage { ); } - /** 必要な 64bit 値の個数を求める */ + /** 必要な64bit値の個数を求める */ static longCount(bitsPerEntry: number, entryCount: number): number { const valuesPerLong = Math.floor(64 / bitsPerEntry); return Math.ceil(entryCount / valuesPerLong); @@ -138,7 +137,7 @@ export class BitStorage { } /** - * packed な配列を返す + * packedな配列を返す * 内部の配列をそのまま返す(コピーしない) */ toLongs(): BigInt64Array { diff --git a/typescript/src/world/blockPos.ts b/typescript/src/world/blockPos.ts index bfeed45..b8d64f7 100644 --- a/typescript/src/world/blockPos.ts +++ b/typescript/src/world/blockPos.ts @@ -1,7 +1,5 @@ /** * ブロックの絶対座標と、その範囲 - * - * 仕様: `docs/spec/30-chunk-format.md` 5章 */ import { ChunkPos } from "../anvil/index.js"; @@ -44,17 +42,17 @@ export class BlockPos { ); } - /** `(x, y, z)` の形の文字列にする */ + /** `(x, y, z)`の形の文字列にする */ toString(): string { return `(${this.x}, ${this.y}, ${this.z})`; } } /** - * ブロック座標の直方体な範囲 + * ブロック座標の直方体の範囲 * * 両端を含む - * `Cuboid.of(0, 0, 0, 0, 0, 0)` は 1 ブロック + * `Cuboid.of(0, 0, 0, 0, 0, 0)`は1ブロック * * 範囲内のブロックを順に処理したいときに使う */ @@ -68,7 +66,8 @@ export class Cuboid { /** * 両端の座標から作る - * 大小の順序は問わない。内部で小さいほうを最小に揃える + * 大小の順序は問わない + * 内部で小さいほうを最小に揃える */ constructor(first: BlockPos, second: BlockPos) { this.minX = Math.min(first.x, second.x); @@ -84,17 +83,17 @@ export class Cuboid { return new Cuboid(new BlockPos(x1, y1, z1), new BlockPos(x2, y2, z2)); } - /** X 方向の長さ */ + /** X方向の長さ */ get sizeX(): number { return this.maxX - this.minX + 1; } - /** Y 方向の長さ */ + /** Y方向の長さ */ get sizeY(): number { return this.maxY - this.minY + 1; } - /** Z 方向の長さ */ + /** Z方向の長さ */ get sizeZ(): number { return this.maxZ - this.minZ + 1; } @@ -118,10 +117,10 @@ export class Cuboid { /** * 範囲内の座標を順に返す - * 並びは Y、Z、X の順で、X がいちばん内側で動く + * 並びはY、Z、Xの順で、Xがいちばん内側で動く */ *positions(): IterableIterator { - // 内側から X が動くので、同じチャンクの並びを続けて触れる + // Xがいちばん内側で動くので、同じチャンクの並びを続けて扱える for (let y = this.minY; y <= this.maxY; y++) { for (let z = this.minZ; z <= this.maxZ; z++) { for (let x = this.minX; x <= this.maxX; x++) { diff --git a/typescript/src/world/blockState.ts b/typescript/src/world/blockState.ts index 0e07c74..ab329dd 100644 --- a/typescript/src/world/blockState.ts +++ b/typescript/src/world/blockState.ts @@ -3,13 +3,9 @@ * 名前と、任意のプロパティの組 * * プロパティは**常に名前の昇順で保持する** - * こうしておくと文字列表現が一意になり、 - * 全言語で同じ出力になる - * Minecraft が書き出した並び順は - * `PalettedContainer` がパレットを生の NBT のまま持つことで守られるので、 - * 触っていないブロックの並びが崩れることはない - * - * 仕様: `docs/spec/30-chunk-format.md` 2.1章 + * こうしておくと文字列表現が一意になり、全言語で同じ出力になる + * Minecraftが書き出した並び順は、`PalettedContainer`がパレットを生のNBTのまま持つことで守られる + * そのため、触っていないブロックの並びが崩れることはない */ import { SpringNbtError } from "../errors.js"; @@ -20,8 +16,7 @@ import { NbtCompound, NbtString, TagType } from "../nbt/index.js"; * 名前と、任意のプロパティの組 * * プロパティは**常に名前の昇順で保持する** - * こうしておくと文字列表現が一意になり、 - * 全言語で同じ出力になる + * こうしておくと文字列表現が一意になり、全言語で同じ出力になる */ export class BlockState { readonly #name: string; @@ -56,20 +51,20 @@ export class BlockState { /** * プロパティを取得する - * 無ければ undefined + * 無ければundefined */ property(key: string): string | undefined { return this.#properties.get(key); } - /** プロパティを 1 つ差し替えた新しい状態を返す */ + /** プロパティを1つ差し替えた新しい状態を返す */ with(key: string, value: string): BlockState { const entries = [...this.#properties]; entries.push([key, value]); return new BlockState(this.#name, entries); } - /** `minecraft:oak_stairs[facing=north,half=top]` 形式の文字列から作る */ + /** `minecraft:oak_stairs[facing=north,half=top]`形式の文字列から作る */ static parse(text: string): BlockState { const bracket = text.indexOf("["); @@ -90,7 +85,7 @@ export class BlockState { const seen = new Set(); if (body.length > 0) { - // "key=value" をカンマ区切りで読む + // "key=value"をカンマ区切りで読む for (const pair of body.split(",")) { const equals = pair.indexOf("="); @@ -117,14 +112,14 @@ export class BlockState { return new BlockState(text.slice(0, bracket), entries); } - /** パレット要素の NBT から作る */ + /** パレット要素のNBTから作る */ static fromNbt(nbt: NbtCompound): BlockState { const entries: Array<[string, string]> = []; const seen = new Set(); const propertiesTag = nbt.optCompound("Properties"); if (propertiesTag !== undefined) { - // Properties の値はすべて文字列(数値や真偽値も文字列で入る) + // Propertiesの値はすべて文字列(数値や真偽値も文字列で入る) for (const [key, value] of propertiesTag.entries()) { if (value.type !== TagType.String) { throw SpringNbtError.unexpectedTagType( @@ -140,10 +135,10 @@ export class BlockState { } /** - * パレット要素の NBT へ変換する + * パレット要素のNBTへ変換する * - * プロパティが空なら `Properties` キー自体を出力しない - * Minecraft と同じ振る舞い + * プロパティが空なら`Properties`キー自体を出力しない + * Minecraftと同じ振る舞い */ toNbt(): NbtCompound { const result = new NbtCompound(); @@ -180,7 +175,7 @@ export class BlockState { return true; } - /** `minecraft:oak_stairs[facing=north,half=top]` 形式の文字列を返す */ + /** `minecraft:oak_stairs[facing=north,half=top]`形式の文字列を返す */ toString(): string { if (this.#properties.size === 0) { return this.#name; @@ -219,7 +214,7 @@ export class BlockState { } } -/** 名前空間が省略されていたら `minecraft:` を補う */ +/** 名前空間が省略されていたら`minecraft:`を補う */ function normalize(name: string): string { if (name.includes(":")) { return name; diff --git a/typescript/src/world/chunk.ts b/typescript/src/world/chunk.ts index 6c6e616..382ba8e 100644 --- a/typescript/src/world/chunk.ts +++ b/typescript/src/world/chunk.ts @@ -1,11 +1,9 @@ /** - * チャンク 1 つ分 - * 地形の読み書きの入口 + * チャンク1つ分 + * 地形の読み書きはここから行う * - * 読んだ NBT をそのまま保持し、変更した部分だけを書き戻す - * 未知のキーを落とさないので、将来の追加要素があってもデータを壊さない - * - * 仕様: `docs/spec/30-chunk-format.md` + * 読んだNBTをそのまま保持し、書き戻すときに作り直すのはセクションだけにする + * 未知のキーを落とさないので、将来の追加要素があってもデータをおかしくしない */ import { MIN_SUPPORTED_DATA_VERSION } from "../index.js"; @@ -22,10 +20,10 @@ import { import { BlockState } from "./blockState.js"; import { PalettedContainer } from "./palettedContainer.js"; -/** セクション 1 つに入るブロック数 */ +/** セクション1つに入るブロック数 */ export const BLOCKS_PER_SECTION = 4096; -/** セクション 1 つに入るバイオームのエントリ数(4×4×4 単位) */ +/** セクション1つに入るバイオームのエントリ数(4×4×4単位) */ export const BIOMES_PER_SECTION = 64; /** @@ -48,14 +46,14 @@ function matchesPosition(entry: NbtCompound, x: number, y: number, z: number): b return entryX === x && entryY === y && entryZ === z; } -/** DataVersion が対象と違ったときの動作 */ +/** DataVersionが扱える形式より古いときの動作 */ export enum VersionMismatchAction { /** * 警告コールバックを呼んで続行する * 既定 */ Warn = "warn", - /** `UNSUPPORTED_DATA_VERSION` の例外にする */ + /** `UNSUPPORTED_DATA_VERSION`の例外にする */ Error = "error", /** 何もしない */ Ignore = "ignore", @@ -64,32 +62,32 @@ export enum VersionMismatchAction { /** チャンク読み込みのオプション */ export interface ChunkReadOptions { /** - * DataVersion が扱える形式より古いときの動作 - * 既定は `Warn` + * DataVersionが扱える形式より古いときの動作 + * 既定は`Warn` */ onVersionMismatch?: VersionMismatchAction; /** 警告の通知先 */ onWarning?: (message: string) => void; - /** data の長さが期待値と違うとき、長さからビット幅を逆算して読むか */ + /** dataの長さが期待値と違うとき、長さからビット幅を逆算して読むか */ lenientBitStorage?: boolean; } /** チャンク書き込みのオプション */ export interface ChunkWriteOptions { /** - * 対象バージョン以外の DataVersion を持つチャンクの書き戻しを許すか + * 扱える形式より古いDataVersionを持つチャンクの書き戻しを許すか * - * 既定は false - * 古いワールドを黙って新形式で上書きし、 - * 利用者が気づかないうちに壊すことを防ぐため(`docs/adr/0003-version-policy.md`) + * 既定はfalse + * エラーを出さずに古いワールドを新形式で上書きし、 + * 利用者が気づかないうちに使えなくすることを防ぐため */ allowForeignDataVersion?: boolean; } /** - * チャンクを Y 方向に 16 ブロックずつ区切った 16×16×16 の立方体 + * チャンクをY方向に16ブロックずつ区切った16×16×16の立方体 * - * `BlockLight` / `SkyLight` などの解釈していないキーは元の NBT に残り、 + * `BlockLight` / `SkyLight`などの解釈していないキーは元のNBTに残り、 * 書き戻しでそのまま出力される */ export class ChunkSection { @@ -103,7 +101,7 @@ export class ChunkSection { /** * ブロック状態 - * 持たないセクション(光源専用)では undefined + * 持たないセクション(光源専用)ではundefined */ get blockStates(): PalettedContainer | undefined { return this.#blockStates; @@ -111,7 +109,7 @@ export class ChunkSection { /** * バイオーム - * 持たないセクションでは undefined + * 持たないセクションではundefined */ get biomes(): PalettedContainer | undefined { return this.#biomes; @@ -127,12 +125,12 @@ export class ChunkSection { return this.#biomes !== undefined; } - /** NBT からセクションを読む */ + /** NBTからセクションを読む */ static fromNbt(nbt: NbtCompound, lenientBitStorage: boolean): ChunkSection { const section = new ChunkSection(nbt, nbt.getByte("Y")); const blockStates = nbt.optCompound("block_states"); - // 光源専用のセクションは block_states を持たない + // 光源専用のセクションはblock_statesを持たない if (blockStates !== undefined) { section.#blockStates = PalettedContainer.fromNbt( blockStates, @@ -157,7 +155,7 @@ export class ChunkSection { } /** - * NBT へ書き戻す + * NBTへ書き戻す * 解釈していないキーはそのまま残る */ toNbt(): NbtCompound { @@ -185,11 +183,11 @@ export class ChunkSection { } /** - * チャンク 1 つ分 - * 地形の読み書きの入口 + * チャンク1つ分 + * 地形の読み書きはここから行う * - * 読んだ NBT をそのまま保持し、変更した部分だけを書き戻す - * 未知のキーを落とさないので、将来の追加要素があってもデータを壊さない + * 読んだNBTをそのまま保持し、書き戻すときに作り直すのはセクションだけにする + * 未知のキーを落とさないので、将来の追加要素があってもデータをおかしくしない */ export class Chunk { readonly #sections = new Map(); @@ -214,13 +212,13 @@ export class Chunk { /** * 最下段セクションのY位置 - * オーバーワールドは -4 + * オーバーワールドは-4 */ get minSectionY(): number { return this.raw.getInt("yPos"); } - /** 生成段階(`minecraft:full` など) */ + /** 生成段階(`minecraft:full`など) */ get status(): string { return this.raw.getString("Status"); } @@ -237,9 +235,9 @@ export class Chunk { * このチャンクに変更が加わったか * * ブロックやバイオームを書き換えると立つ - * `Dimension.flush()` はこれが立っているチャンクだけを書き戻す + * `Dimension.flush()`はこれが立っているチャンクだけを書き戻す * - * `raw` を直接いじった場合はここが立たないので、自分で true にすること + * `raw`を直接いじった場合は立たないので、自分でtrueにすること */ get isModified(): boolean { return this.#modified; @@ -258,7 +256,7 @@ export class Chunk { return [...this.#sections.keys()].sort((left, right) => left - right); } - /** NBT からチャンクを読む */ + /** NBTからチャンクを読む */ static fromNbt(nbt: NbtCompound, options?: ChunkReadOptions): Chunk { const chunk = new Chunk(nbt); chunk.#checkDataVersion(options); @@ -275,7 +273,7 @@ export class Chunk { return chunk; } - // 並び順に依存しないよう、Y から索引を作る + // 並び順に依存しないよう、Yから索引を作る for (const entry of sectionList) { if (entry.type !== TagType.Compound) { throw SpringNbtError.unexpectedTagType("sections の要素が compound でない"); @@ -288,7 +286,7 @@ export class Chunk { return chunk; } - /** DataVersion を検査し、オプションに従って警告またはエラーにする */ + /** DataVersionを検査し、オプションに従って警告またはエラーにする */ #checkDataVersion(options?: ChunkReadOptions): void { const version = this.dataVersion; @@ -319,8 +317,8 @@ export class Chunk { } /** - * NBT へ書き戻す - * 変更したセクションだけを反映し、他のキーはそのまま残す + * NBTへ書き戻す + * 全セクションを書き戻し、他のキーはそのまま残す */ toNbt(options?: ChunkWriteOptions): NbtCompound { const version = this.dataVersion; @@ -330,7 +328,7 @@ export class Chunk { allowForeign = true; } - // 形式の違う古いチャンクは、書き戻すと壊しかねない + // 形式の違う古いチャンクは、書き戻すと使えなくなりかねない if (version < MIN_SUPPORTED_DATA_VERSION && !allowForeign) { throw new SpringNbtError( ErrorCode.UnsupportedDataVersion, @@ -340,7 +338,7 @@ export class Chunk { ); } - // DataVersion は読んだ値のまま残す + // DataVersionは読んだ値のまま残す // 書き換えると、そのワールドを開くゲーム側の判断を誤らせる if (this.#sections.size === 0) { @@ -349,7 +347,7 @@ export class Chunk { const sectionList = new NbtList(TagType.Compound); - // Y の昇順で書き出す + // Yの昇順で書き出す for (const sectionY of this.sectionYs) { sectionList.add(this.#sections.get(sectionY)!.toNbt()); } @@ -360,7 +358,7 @@ export class Chunk { /** * Y位置からセクションを得る - * 無ければ undefined + * 無ければundefined */ section(sectionY: number): ChunkSection | undefined { return this.#sections.get(sectionY); @@ -393,10 +391,10 @@ export class Chunk { /** * ブロックを設定する * - * `minecraft:oak_stairs[facing=north]` の形の文字列でも指定できる + * `minecraft:oak_stairs[facing=north]`の形の文字列でも指定できる */ setBlock(x: number, y: number, z: number, state: BlockState | string): void { - // 文字列で渡されたら BlockState へ直してから進む + // 文字列で渡されたらBlockStateへ直してから進む if (typeof state === "string") { this.setBlock(x, y, z, BlockState.parse(state)); return; @@ -429,14 +427,14 @@ export class Chunk { /** * その座標を指す付随データを取り除く * - * `block_entities` / `block_ticks` / `fluid_ticks` の要素は - * いずれも `x` `y` `z` を**絶対座標**で持つ + * `block_entities` / `block_ticks` / `fluid_ticks`の要素は + * いずれも`x` `y` `z`を**絶対座標**で持つ */ #removeBlockData(x: number, y: number, z: number): void { const absoluteX = this.x * 16 + x; const absoluteZ = this.z * 16 + z; - // 3 つのリストは形が同じなので、まとめて同じ処理をかける + // 3つのリストは形が同じなので、まとめて同じ処理をかける for (const key of BLOCK_DATA_KEYS) { const list = this.raw.optList(key); @@ -458,7 +456,7 @@ export class Chunk { /** * バイオームを取得する - * 4×4×4 の単位なので、座標は自動的に丸められる + * 4×4×4の単位なので、座標は自動的に丸められる */ getBiome(x: number, y: number, z: number): string | undefined { checkLocalCoordinates(x, z); @@ -479,7 +477,7 @@ export class Chunk { /** * バイオームを設定する - * 4×4×4 の単位 + * 4×4×4の単位 */ setBiome(x: number, y: number, z: number, biome: string): void { checkLocalCoordinates(x, z); @@ -498,18 +496,17 @@ export class Chunk { } /** - * `Heightmaps` を削除し、Minecraft に再計算させる + * `Heightmaps`を削除し、Minecraftに再計算させる * * 本ライブラリは高さマップを再計算しない * ブロックを改変したら呼ぶこと - * (`docs/adr/0004-defer-heightmap-recalc.md`) */ clearHeightmaps(): void { this.raw.remove("Heightmaps"); this.#modified = true; } - /** `isLightOn` を 0 にし、光源の再計算を促す */ + /** `isLightOn`を0にし、光源の再計算を促す */ invalidateLighting(): void { this.raw.setByte("isLightOn", 0); this.#modified = true; @@ -527,7 +524,7 @@ export class Chunk { /** * セクション内のブロック添字 * - * `& 15` により負のY座標でも正しく求まる + * `& 15`により負のY座標でも正しく求まる */ export function blockIndex(x: number, y: number, z: number): number { return (y & 15) * 256 + (z & 15) * 16 + (x & 15); @@ -535,14 +532,14 @@ export function blockIndex(x: number, y: number, z: number): number { /** * セクション内のバイオーム添字 - * 1 エントリが 4×4×4 ブロック + * 1エントリが4×4×4ブロック */ export function biomeIndex(x: number, y: number, z: number): number { return Math.floor((y & 15) / 4) * 16 + Math.floor((z & 15) / 4) * 4 + Math.floor((x & 15) / 4); } function checkLocalCoordinates(x: number, z: number): void { - // チャンク内相対座標は 0..15 でなければならない + // チャンク内相対座標は0..15でなければならない if (x < 0 || x > 15 || z < 0 || z > 15) { throw SpringNbtError.invalidArgument( `チャンク内相対座標が範囲外: (${x}, ${z})。X も Z も 0..15 であること`, diff --git a/typescript/src/world/index.ts b/typescript/src/world/index.ts index 2486228..9d9718b 100644 --- a/typescript/src/world/index.ts +++ b/typescript/src/world/index.ts @@ -1,7 +1,5 @@ /** * ワールド・チャンク・ブロックの読み書き - * - * 仕様: `docs/spec/30-chunk-format.md` / `docs/spec/40-world-layout.md` */ export { BlockState } from "./blockState.js"; diff --git a/typescript/src/world/minecraftWorld.ts b/typescript/src/world/minecraftWorld.ts index 96e4e15..0f7442d 100644 --- a/typescript/src/world/minecraftWorld.ts +++ b/typescript/src/world/minecraftWorld.ts @@ -1,18 +1,25 @@ /** - * Minecraft Java版のセーブデータ 1 つ分と、その中の次元 + * Minecraft Java版のセーブデータ1つ分と、その中の次元 * - * 26.x では構成が大きく変わっており、標準の3次元も - * `dimensions/<名前空間>/<パス>/` の下に並ぶ - * - * 仕様: `docs/spec/40-world-layout.md` + * 26.xでは構成が大きく変わっており、標準の3次元も`dimensions/<名前空間>/<パス>/`の下に並ぶ */ -import { copyFileSync, existsSync, readdirSync, renameSync, statSync } from "node:fs"; +import { + closeSync, + copyFileSync, + existsSync, + fsyncSync, + openSync, + readdirSync, + renameSync, + statSync, + writeFileSync, +} from "node:fs"; import { basename, join } from "node:path"; import { ChunkPos, RegionFileMode, RegionFolder } from "../anvil/index.js"; import { ErrorCode, SpringNbtError } from "../errors.js"; -import { NamedTag, NbtCompound, readFile, writeFile } from "../nbt/index.js"; +import { NamedTag, NbtCompound, readFile, writeBytes } from "../nbt/index.js"; import { Chunk, ChunkReadOptions, ChunkWriteOptions } from "./chunk.js"; import { BlockState } from "./blockState.js"; @@ -24,15 +31,13 @@ export interface WorldOpenOptions { */ writable?: boolean; /** - * `session.lock` の確認を飛ばすか + * `session.lock`の確認を飛ばすか * - * TypeScript 版はこの確認を行わない - * Node には移植性のある - * ファイルロックの手段が無いため(`docs/adr/0008-session-lock.md`) - * 他言語版との API を揃えるためにフィールドだけ用意してある + * Nodeには移植性のあるファイルロックの手段が無いため、TypeScript版はこの確認を行わない + * 他言語版とAPIを揃えるためにフィールドだけ用意してある * - * Minecraft が起動中のワールドへ書き込むとデータが壊れるので、 - * 起動していないことは呼び出し側で担保すること + * Minecraftが起動中のワールドへ書き込むとデータがおかしくなるので、 + * 起動していないことは呼び出し側で確かめること */ ignoreSessionLock?: boolean; /** チャンク読み込みのオプション */ @@ -42,10 +47,9 @@ export interface WorldOpenOptions { } /** - * `level.dat` の内容 + * `level.dat`の内容 * - * 26.x では大幅に軽量化されており、ゲームルールやワールド生成設定は - * `data/minecraft/` 配下の個別ファイルへ分離されている + * 26.xでは大幅に軽量化されており、ゲームルールやワールド生成設定は`data/minecraft/`配下の個別ファイルへ分離されている */ export class LevelData { readonly raw: NbtCompound; @@ -81,7 +85,7 @@ export class LevelData { return this.data.getInt("GameType"); } - /** スポーン地点の `[x, y, z]` */ + /** スポーン地点の`[x, y, z]` */ get spawnPos(): Int32Array { return this.data.getCompound("spawn").getIntArray("pos"); } @@ -91,7 +95,7 @@ export class LevelData { return this.data.getCompound("spawn").getString("dimension"); } - /** 難易度(`normal` など) */ + /** 難易度(`normal`など) */ get difficulty(): string { return this.data.getCompound("difficulty_settings").getString("difficulty"); } @@ -101,23 +105,22 @@ export class LevelData { return this.data.getCompound("difficulty_settings").getBool("hardcore"); } - /** バージョン名(`26.2` など) */ + /** バージョン名(`26.2`など) */ get versionName(): string { return this.data.getCompound("Version").getString("Name"); } - /** 書き出し用の `NamedTag` を作る */ + /** 書き出し用の`NamedTag`を作る */ toNamedTag(): NamedTag { return new NamedTag(this.#rootName, this.raw); } } /** - * ワールド内の次元 1 つ分 - * `region/` `entities/` `poi/` をまとめて扱う + * ワールド内の次元1つ分 + * `region/` `entities/` `poi/`をまとめて扱う * - * ブロックの取得・設定は**絶対ワールド座標**で行い、 - * リージョン・チャンク・セクションの解決は内部で済ませる + * ブロックの取得・設定は**絶対ワールド座標**で行い、リージョン・チャンク・セクションの解決は内部で済ませる */ export class Dimension { /** オーバーワールドの次元ID */ @@ -151,7 +154,7 @@ export class Dimension { /** * 地形のリージョンフォルダ - * 無ければ undefined + * 無ければundefined */ get regionFolder(): RegionFolder | undefined { this.#regions = this.#folder(this.#regions, "region"); @@ -160,7 +163,7 @@ export class Dimension { /** * エンティティのリージョンフォルダ - * 無ければ undefined + * 無ければundefined */ get entityFolder(): RegionFolder | undefined { this.#entities = this.#folder(this.#entities, "entities"); @@ -168,8 +171,8 @@ export class Dimension { } /** - * POI のリージョンフォルダ - * 無ければ undefined + * POIのリージョンフォルダ + * 無ければundefined */ get poiFolder(): RegionFolder | undefined { this.#poi = this.#folder(this.#poi, "poi"); @@ -177,8 +180,10 @@ export class Dimension { } /** - * `data/minecraft/.dat` を読む - * 存在しなければ undefined + * `data/minecraft/.dat`を読む + * 存在しなければundefined + * + * 次元ごとの`world_border` / `raids` / `chunk_tickets`などが入る */ dataFile(name: string): NbtCompound | undefined { this.#ensureOpen(); @@ -205,7 +210,7 @@ export class Dimension { /** * チャンクを読む - * 読み込んだチャンクはキャッシュされる + * 読み込んだチャンクはキャッシュされ、次回は同じインスタンスが返る */ chunk(chunkX: number, chunkZ: number): Chunk | undefined { this.#ensureOpen(); @@ -249,7 +254,7 @@ export class Dimension { /** * 絶対座標でブロックを取得する - * チャンクが無ければ undefined + * チャンクが無ければundefined */ getBlock(x: number, y: number, z: number): BlockState | undefined { const chunk = this.chunk(x >> 4, z >> 4); @@ -264,10 +269,10 @@ export class Dimension { /** * 絶対座標でブロックを設定する * - * `minecraft:oak_stairs[facing=north]` の形の文字列でも指定できる + * `minecraft:oak_stairs[facing=north]`の形の文字列でも指定できる * - * 変更したチャンクには印が付き、`flush()` でまとめて書き戻される - * 本ライブラリはチャンクを新規生成しないので、存在しない座標はエラーになる + * 変更したチャンクには印が付き、`flush()`でまとめて書き戻される + * 本ライブラリはチャンクを新規生成しないので、チャンクが存在しない座標はエラーになる */ setBlock(x: number, y: number, z: number, state: BlockState | string): void { this.#ensureWritable(); @@ -286,7 +291,7 @@ export class Dimension { /** * 絶対座標でバイオームを取得する - * 4×4×4 の単位 + * 4×4×4の単位 */ getBiome(x: number, y: number, z: number): string | undefined { const chunk = this.chunk(x >> 4, z >> 4); @@ -300,7 +305,7 @@ export class Dimension { /** * 絶対座標でバイオームを設定する - * 4×4×4 の単位 + * 4×4×4の単位 */ setBiome(x: number, y: number, z: number, biome: string): void { this.#ensureWritable(); @@ -373,7 +378,7 @@ export class Dimension { /** * フォルダを遅延して開く - * 存在しなければ undefined のまま + * 存在しなければundefinedのまま */ #folder(slot: RegionFolder | undefined, name: string): RegionFolder | undefined { this.#ensureOpen(); @@ -412,9 +417,9 @@ export class Dimension { } /** - * Minecraft Java版のセーブデータ 1 つ分 + * Minecraft Java版のセーブデータ1つ分 * - * 26.x では標準の3次元も `dimensions/<名前空間>/<パス>/` の下に並ぶ + * 26.xでは構成が大きく変わっており、標準の3次元も`dimensions/<名前空間>/<パス>/`の下に並ぶ */ export class MinecraftWorld { readonly #dimensions = new Map(); @@ -441,7 +446,7 @@ export class MinecraftWorld { throw new SpringNbtError(ErrorCode.Io, `level.dat が無い: ${levelPath}`); } - // 書き込むなら、Minecraft が起動中でないことを先に確かめる + // 書き込むなら、Minecraftが起動中でないことを先に確かめる if (options.writable === true && options.ignoreSessionLock !== true) { checkSessionLock(directory); } @@ -450,11 +455,10 @@ export class MinecraftWorld { } /** - * `data/minecraft/.dat` を読む - * 存在しなければ undefined + * `data/minecraft/.dat`を読む + * 存在しなければundefined * - * 26.x では `game_rules` / `weather` / `world_gen_settings` などが - * この形で `level.dat` から分離されている + * 26.xでは`game_rules` / `weather` / `world_gen_settings`などがこの形で`level.dat`から分離されている */ dataFile(name: string): NbtCompound | undefined { this.#ensureOpen(); @@ -478,7 +482,7 @@ export class MinecraftWorld { const found: string[] = []; - // dimensions/<名前空間>/<パス>/ の 2 段を辿る + // dimensions/<名前空間>/<パス>/の2段を辿る for (const namespaceName of readdirSync(root)) { const namespaceDir = join(root, namespaceName); @@ -486,7 +490,7 @@ export class MinecraftWorld { continue; } - // 2 段目が次元のパス + // 2段目が次元のパス for (const pathName of readdirSync(namespaceDir)) { // ディレクトリだけを次元として数える if (statSync(join(namespaceDir, pathName)).isDirectory()) { @@ -502,7 +506,7 @@ export class MinecraftWorld { /** * 次元を得る - * ディレクトリが無ければ undefined + * ディレクトリが無ければundefined */ dimension(dimensionId: string): Dimension | undefined { this.#ensureOpen(); @@ -541,9 +545,9 @@ export class MinecraftWorld { const found: string[] = []; - // .dat の名前部分が UUID にあたる + // .datの名前部分がUUIDにあたる for (const name of readdirSync(path)) { - // .dat 以外のファイルは対象外 + // .dat以外のファイルは対象外 if (name.endsWith(".dat")) { found.push(basename(name, ".dat")); } @@ -555,7 +559,7 @@ export class MinecraftWorld { /** * プレイヤーデータを読む - * 存在しなければ undefined + * 存在しなければundefined */ player(uuid: string): NbtCompound | undefined { this.#ensureOpen(); @@ -569,10 +573,10 @@ export class MinecraftWorld { } /** - * `level.dat` を書き戻す + * `level.dat`を書き戻す * - * 壊れるとワールド全体が開けなくなるため、 - * 一時ファイルへ書いてから `level.dat_old` へ退避し、最後に置き換える + * `level.dat`がおかしくなるとワールド全体が開けなくなるため、 + * 一時ファイルへ書いて内容をディスクへ確実に書き出してから、既存の`level.dat`を`level.dat_old`へ退避し、最後に置き換える */ saveLevel(): void { this.#ensureOpen(); @@ -585,14 +589,28 @@ export class MinecraftWorld { const temporary = `${path}.tmp`; const backup = `${path}_old`; - writeFile(temporary, this.level.toNamedTag()); + const encoded = writeBytes(this.level.toNamedTag()); - // 既存の level.dat は、置き換える前に level.dat_old へ退避する - if (existsSync(path)) { - copyFileSync(path, backup); - } + try { + // 一時ファイルへ書き、置き換える前に内容をディスクへ確実に書き出す + const descriptor = openSync(temporary, "w"); + + try { + writeFileSync(descriptor, encoded); + fsyncSync(descriptor); + } finally { + closeSync(descriptor); + } - renameSync(temporary, path); + // 既存のlevel.datは、置き換える前にlevel.dat_oldへ退避する + if (existsSync(path)) { + copyFileSync(path, backup); + } + + renameSync(temporary, path); + } catch (error) { + throw new SpringNbtError(ErrorCode.Io, `level.dat を書けない: ${path}`, { cause: error }); + } } /** 開いている次元をすべて閉じる */ @@ -618,23 +636,19 @@ export class MinecraftWorld { } /** - * `session.lock` を確認する(TypeScript 版では何もしない) + * `session.lock`を確認する(TypeScript版では何もしない) * - * Minecraft は起動中このファイルのロックを保持し続けるので、本来は - * 「ロックを取れるか」で判定する - * しかし Node には移植性のある - * ファイルロックの手段が無く、ファイルの存在自体は起動していなくても残るため、 + * Minecraftは起動中このファイルのロックを保持し続けるので、本来は「ロックを取れるか」で判定する + * しかしNodeには移植性のあるファイルロックの手段が無く、ファイルの存在自体は起動していなくても残るため、 * 存在確認だけでは何も判定できない * - * 誤った判定を返すよりは何もしないほうが安全なので、素通しする - * - * 仕様: docs/spec/40-world-layout.md 3章 / docs/adr/0008-session-lock.md + * 誤った判定を返すよりは何もしないほうが安全なので、確認せずに進める */ function checkSessionLock(directory: string): void { void directory; } -/** 名前空間が省略されていたら `minecraft:` を補う */ +/** 名前空間が省略されていたら`minecraft:`を補う */ function normalizeDimensionId(dimensionId: string): string { if (dimensionId.includes(":")) { return dimensionId; diff --git a/typescript/src/world/palettedContainer.ts b/typescript/src/world/palettedContainer.ts index d33ce1a..86218b6 100644 --- a/typescript/src/world/palettedContainer.ts +++ b/typescript/src/world/palettedContainer.ts @@ -2,11 +2,9 @@ * パレットとビットストレージの組 * セクション内のブロック状態やバイオームを格納する * - * パレットの要素は**生の `NbtTag` のまま**持つ - * こうすると、触っていないブロックについては Minecraft が書き出したときの + * パレットの要素は**生の`NbtTag`のまま**持つ + * こうすると、触っていないブロックについてはMinecraftが書き出したときの * プロパティの並び順まで含めてそのまま書き戻せる - * - * 仕様: `docs/spec/31-paletted-container.md` */ import { SpringNbtError } from "../errors.js"; @@ -17,8 +15,8 @@ import { BitStorage } from "./bitStorage.js"; * パレットとビットストレージの組 * セクション内のブロック状態やバイオームを格納する * - * パレットの要素は**生の `NbtTag` のまま**持つ - * こうすると、触っていないブロックの + * パレットの要素は**生の`NbtTag`のまま**持つ + * こうすると、触っていないブロックについてはMinecraftが書き出したときの * プロパティの並び順まで含めてそのまま書き戻せる */ export class PalettedContainer { @@ -35,7 +33,7 @@ export class PalettedContainer { /** * エントリ数 - * ブロックなら 4096、バイオームなら 64 + * ブロックなら4096、バイオームなら64 */ get entryCount(): number { return this.#entryCount; @@ -43,7 +41,7 @@ export class PalettedContainer { /** * ビット幅の下限 - * ブロックなら 4、バイオームなら 1 + * ブロックなら4、バイオームなら1 */ get minBits(): number { return this.#minBits; @@ -59,7 +57,7 @@ export class PalettedContainer { /** * 現在のビット幅 - * パレットが 1 要素なら 0(記憶域を持たない) + * パレットが1要素なら0(記憶域を持たない) */ get bitsPerEntry(): number { if (this.#storage === undefined) { @@ -76,7 +74,7 @@ export class PalettedContainer { return result; } - /** NBT から読み込む */ + /** NBTから読み込む */ static fromNbt( nbt: NbtCompound, entryCount: number, @@ -90,7 +88,7 @@ export class PalettedContainer { throw SpringNbtError.malformed("palette が無いか空"); } - // パレットの要素は生の NbtTag のまま持つ + // パレットの要素は生のNbtTagのまま持つ // 並び順まで元どおりに書き戻すため for (const entry of paletteTag) { result.#palette.push(entry); @@ -99,7 +97,7 @@ export class PalettedContainer { const data = nbt.optLongArray("data"); if (data === undefined) { - // パレットが 1 要素なら data は無くてよい + // パレットが1要素ならdataは無くてよい if (result.#palette.length !== 1) { throw SpringNbtError.malformed( `palette が ${result.#palette.length} 要素なのに data が無い`, @@ -113,7 +111,7 @@ export class PalettedContainer { result.#storage = BitStorage.fromLongs(data, bits, entryCount, lenientBitStorage); // 取り出した添字がパレットの範囲に収まっているか確かめる - // 黙って 0 番目で代替すると、壊れたデータをそうと分からない形で書き戻してしまう + // エラーを出さずに0番目で代替すると、おかしくなったデータをそうと分からない形で書き戻してしまう for (let index = 0; index < entryCount; index++) { const value = result.#storage.get(index); @@ -127,7 +125,7 @@ export class PalettedContainer { return result; } - /** NBT へ変換する */ + /** NBTへ変換する */ toNbt(): NbtCompound { const result = new NbtCompound(); const paletteTag = new NbtList(); @@ -137,8 +135,8 @@ export class PalettedContainer { paletteTag.add(entry); } - // パレットが 1 要素なら data は書かない - // Minecraft と同じ振る舞い + // パレットが1要素ならdataは書かない + // Minecraftと同じ振る舞い if (this.#storage !== undefined && this.#palette.length > 1) { result.set("data", new NbtLongArray(this.#storage.toLongs())); } @@ -151,7 +149,7 @@ export class PalettedContainer { get(index: number): NbtTag { this.#checkIndex(index); - // 記憶域が無いということは、全エントリがパレットの 0 番目 + // 記憶域が無いということは、全エントリがパレットの0番目 if (this.#storage === undefined) { return this.#palette[0]; } @@ -167,7 +165,7 @@ export class PalettedContainer { this.#checkIndex(index); const paletteIndex = this.#indexOfOrAdd(value); - // 記憶域が無く、書き込む値も 0 番目なら何もしなくてよい + // 記憶域が無く、書き込む値も0番目なら何もしなくてよい if (this.#storage === undefined && paletteIndex === 0) { return; } @@ -177,8 +175,8 @@ export class PalettedContainer { } /** - * 全エントリを 1 つの値で埋める - * パレットもその 1 要素だけにする + * 全エントリを1つの値で埋める + * パレットもその1要素だけにする */ fill(value: NbtTag): void { this.#palette.length = 0; @@ -189,7 +187,7 @@ export class PalettedContainer { /** * どのエントリからも参照されていないパレット要素を取り除き、添字を振り直す * - * 大量の `set` を行う用途で遅くならないよう、明示的に呼んだときだけ実行する + * 大量の`set`を行う用途で遅くならないよう、明示的に呼んだときだけ実行する */ compact(): void { if (this.#storage === undefined) { @@ -233,7 +231,7 @@ export class PalettedContainer { this.#palette.push(...compacted); if (compacted.length === 1) { - // 1 要素になったら記憶域を捨てる + // 1要素になったら記憶域を捨てる this.#storage = undefined; } else { this.#storage = rebuilt; @@ -245,7 +243,7 @@ export class PalettedContainer { * 無ければ末尾へ追加する */ #indexOfOrAdd(value: NbtTag): number { - // パレットは高々 4096 要素なので線形探索で足りる + // パレットは高々4096要素なので線形探索で足りる for (let index = 0; index < this.#palette.length; index++) { if (nbtEquals(this.#palette[index], value)) { return index; @@ -261,7 +259,7 @@ export class PalettedContainer { const required = Math.max(this.#minBits, ceilLog2(this.#palette.length)); if (this.#storage === undefined) { - // これまで単一値だったので、全エントリが 0 番目のまま始まる + // これまで単一値だったので、全エントリが0番目のまま始まる this.#storage = BitStorage.create(required, this.#entryCount); return; } @@ -284,13 +282,13 @@ export class PalettedContainer { } /** - * `count` 個の値を表すのに必要な最小ビット数 - * 1 なら 0 + * `count`個の値を表すのに必要な最小ビット数 + * 1なら0 */ export function ceilLog2(count: number): number { let bits = 0; - // 1 を超える分だけシフトして数える + // 1を超える分だけシフトして数える while (1 << bits < count) { bits += 1; } @@ -301,8 +299,8 @@ export function ceilLog2(count: number): number { /** * タグどうしが等しいか * - * NBT 層のクラスは `equals` を持たないので、パレットの重複判定用にここで比べる - * パレットに入るのは Compound(ブロック)か String(バイオーム)だけ + * パレットの重複判定に使う + * パレットに入るのはCompound(ブロック)かString(バイオーム)だけ */ function nbtEquals(left: NbtTag, right: NbtTag): boolean { if (left.type !== right.type) { @@ -331,7 +329,7 @@ function nbtEquals(left: NbtTag, right: NbtTag): boolean { return true; } - // それ以外は値の比較で足りる(パレットに入るのは String まで) + // それ以外は値の比較で足りる(パレットに入るのはStringまで) return String((left as { value?: unknown }).value) === String((right as { value?: unknown }).value); } diff --git a/typescript/tests/anvil.test.ts b/typescript/tests/anvil.test.ts index e791ac1..0ad1ce8 100644 --- a/typescript/tests/anvil.test.ts +++ b/typescript/tests/anvil.test.ts @@ -1,11 +1,9 @@ /** - * Anvil リージョンファイルの読み書き。 + * Anvilリージョンファイルの読み書き * - * 仕様: docs/spec/20-anvil-region.md - * - * 他言語版と同じ検証項目を持つ。 - * 共通テストベクタによる言語間比較は spec/run-conformance.sh が担当し、 - * ここでは API の振る舞いを直接確かめる。 + * 他言語版と同じ検証項目を持つ + * 共通テストベクタによる言語間比較はspec/run-conformance.shが担当し、 + * ここではAPIの振る舞いを直接確かめる */ import assert from "node:assert/strict"; @@ -20,6 +18,7 @@ import { ErrorCode, SpringNbtError } from "../src/errors.js"; import { ChunkCompression, ChunkPos, + RawChunk, RegionFile, RegionFileMode, RegionFolder, @@ -32,7 +31,7 @@ const HERE = fileURLToPath(new URL(".", import.meta.url)); const REPO_ROOT = join(HERE, "..", "..", ".."); const VECTORS = join(REPO_ROOT, "spec", "testdata", "anvil"); -/** 共通テストベクタのディレクトリ。 */ +/** 共通テストベクタのディレクトリ */ function vectorDir(name: string): string { const path = join(VECTORS, name); @@ -43,12 +42,12 @@ function vectorDir(name: string): string { return path; } -/** テストごとの一時ディレクトリを作る。 */ +/** テストごとの一時ディレクトリを作る */ function makeWorkDir(): string { return mkdtempSync(join(tmpdir(), "springnbt-test-")); } -/** ベクタを一時ディレクトリへ複製し、書き込みテストで原本を汚さないようにする。 */ +/** ベクタを一時ディレクトリへ複製し、書き込みテストで原本を汚さないようにする */ function copyVector(name: string, work: string): string { const destination = join(work, name); cpSync(vectorDir(name), destination, { recursive: true }); @@ -65,12 +64,16 @@ function sampleChunk(x: number, z: number): NbtCompound { return chunk; } -/** 圧縮しても縮まないバイト列を作る。サイズの制御が効くようにするため。 */ +/** + * 圧縮しても縮まないバイト列を作る + * サイズを狙いどおりに制御できるようにするため + */ function incompressible(length: number): Int8Array { const result = new Int8Array(length); let state = 0x12345678; - // 線形合同法で疑似乱数を作る。テストの再現性を保つため固定の種を使う + // 線形合同法で疑似乱数を作る + // テストの再現性を保つため、固定の種を使う for (let index = 0; index < length; index++) { state = (Math.imul(state, 1664525) + 1013904223) >>> 0; result[index] = (state >>> 24) << 24 >> 24; @@ -168,7 +171,7 @@ test("圧縮方式が混在していても読める", () => { test("LZ4 圧縮のチャンクを読める", () => { const region = RegionFile.open(join(vectorDir("lz4"), "r.0.0.mca")); - // 1 ブロック / 2 ブロック連結 / 無圧縮ブロック / 重なりのあるマッチ + // 1ブロック / 2ブロック連結 / 無圧縮ブロック / 重なりのあるマッチ for (let x = 0; x < 4; x++) { assert.equal(region.readChunkRaw(x, 0)?.compression, ChunkCompression.Lz4); assert.equal(region.readChunk(x, 0)?.getInt("xPos"), x); @@ -190,6 +193,28 @@ test("LZ4Block のマジックが壊れていたら弾く", () => { region.close(); }); +test("途中で切れたGZipのチャンクはMALFORMED_DATAになる", () => { + const work = makeWorkDir(); + + try { + const directory = copyVector("lz4", work); + const region = RegionFile.open(join(directory, "r.0.0.mca"), RegionFileMode.ReadWrite); + + // GZipのヘッダだけで本体が無い、途中で切れたチャンク + const truncated = Uint8Array.from([0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff]); + region.writeChunkRaw(0, 0, new RawChunk(ChunkCompression.Gzip, truncated)); + + assert.throws( + () => region.readChunk(0, 0), + (error: SpringNbtError) => error.code === ErrorCode.MalformedData, + ); + + region.close(); + } finally { + rmSync(work, { recursive: true, force: true }); + } +}); + test("LZ4 では書き出せない", () => { const work = makeWorkDir(); @@ -197,7 +222,7 @@ test("LZ4 では書き出せない", () => { const directory = copyVector("lz4", work); const region = RegionFile.open(join(directory, "r.0.0.mca"), RegionFileMode.ReadWrite); - // LZ4 は読み込みのみ対応なので、圧縮して書き出すことはできない + // LZ4は読み込みのみ対応なので、圧縮して書き出すことはできない const chunk = region.readChunk(0, 0)!; assert.throws( () => region.writeChunk(0, 0, chunk, ChunkCompression.Lz4), @@ -218,7 +243,8 @@ test("触っていない LZ4 チャンクは圧縮方式が保たれる", () => const path = join(directory, "r.0.0.mca"); const before = readFileSync(path); - // 触らずに閉じるだけ。生バイトを素通しするので LZ4 のまま残る + // 触らずに閉じるだけ + // 生バイトに手を加えないので、LZ4のまま残る const region = RegionFile.open(path, RegionFileMode.ReadWrite); region.close(); @@ -250,7 +276,7 @@ test("壊れたヘッダを弾く", () => { test("担当外のチャンク座標を弾く", () => { const region = RegionFile.open(join(vectorDir("empty"), "r.0.0.mca")); - // r.0.0 が担当するのは 0..31 の範囲だけ + // r.0.0が担当するのは0..31の範囲だけ assertErrorCode(() => region.hasChunk(32, 0), ErrorCode.InvalidArgument); region.close(); }); @@ -266,7 +292,7 @@ test("読み取り専用では書き込めない", () => { // --------------------------------------------------------------------------- test("開いて何も変えずに書き戻すとバイトが変わらない", () => { - // 触っていないチャンクの配置を保つことが、既存ワールドを壊さない前提になる + // 触っていないチャンクの配置を保つことが、既存ワールドをおかしくしないための前提になる const work = makeWorkDir(); try { @@ -338,7 +364,7 @@ test("大きくなったチャンクは移動し、他を壊さない", () => { const directory = copyVector("fragmented", work); const path = join(directory, "r.0.0.mca"); - // 5 セクタぶんになる大きなチャンクを作る + // 5セクタぶんになる大きなチャンクを作る const big = sampleChunk(0, 0); big.set("filler", new NbtByteArray(incompressible(5 * SECTOR_SIZE))); @@ -348,7 +374,7 @@ test("大きくなったチャンクは移動し、他を壊さない", () => { region.close(); const reopened = RegionFile.open(path); - // 動かした結果、他の 2 チャンクが壊れていないこと + // 動かした結果、他の2チャンクがおかしくなっていないこと assert.equal(reopened.chunkPositions().length, 3); assert.equal(reopened.readChunk(5, 3)?.getInt("xPos"), 5); assert.equal(reopened.readChunk(31, 31)?.getInt("xPos"), 31); @@ -442,7 +468,7 @@ test("巨大なチャンクは外部ファイルへ退避され、縮めば戻 try { const path = join(work, "r.0.0.mca"); - // 1MiB を超えるよう、圧縮の効かないデータを詰める + // 1MiBを超えるよう、圧縮しても縮まないデータを詰める const huge = sampleChunk(1, 2); huge.set("filler", new NbtByteArray(incompressible(1200 * 1024))); @@ -509,7 +535,7 @@ test("フォルダは複数リージョンへチャンクを振り分ける", () folder.flush(); folder.close(); - // 3 つの異なるリージョンへ振り分けられる + // 3つの異なるリージョンへ振り分けられる for (const name of ["r.0.0.mca", "r.-1.-1.mca", "r.1.1.mca"]) { assert.ok(existsSync(join(work, name)), `${name} が作られていない`); } @@ -526,11 +552,37 @@ test("フォルダは複数リージョンへチャンクを振り分ける", () } }); +test("読み書きのフォルダは書き出すときにだけ無いディレクトリを作る", () => { + const work = makeWorkDir(); + + try { + const missing = join(work, "missing", "region"); + + // 読むだけなら、無いディレクトリは作らない + const reader = RegionFolder.open(missing, RegionFileMode.ReadWrite); + assert.equal(reader.readChunk(0, 0), undefined); + reader.close(); + assert.equal(existsSync(missing), false); + + // 書き出すときに作る + const writer = RegionFolder.open(missing, RegionFileMode.ReadWrite); + writer.writeChunk(0, 0, sampleChunk(0, 0)); + writer.close(); + + const reopened = RegionFolder.open(missing); + assert.equal(reopened.readChunk(0, 0)?.getInt("xPos"), 0); + reopened.close(); + } finally { + rmSync(work, { recursive: true, force: true }); + } +}); + test("RegionFolder: キャッシュ上限を超えると古いリージョンから閉じる", () => { const work = mkdtempSync(join(tmpdir(), "springnbt-lru-")); try { - // 上限 2 で 4 リージョンへ書く。古いものは閉じられるが内容は失われない + // 上限2で4リージョンへ書く + // 古いものは閉じられるが、内容は失われない const folder = RegionFolder.open(work, RegionFileMode.ReadWrite, 2); for (let region = 0; region < 4; region++) { diff --git a/typescript/tests/blockPos.test.ts b/typescript/tests/blockPos.test.ts index f4816fc..fc0f6a1 100644 --- a/typescript/tests/blockPos.test.ts +++ b/typescript/tests/blockPos.test.ts @@ -1,7 +1,5 @@ /** * ブロック座標と範囲 - * - * 仕様: `docs/spec/30-chunk-format.md` 5章 */ import assert from "node:assert/strict"; @@ -52,7 +50,7 @@ test("範囲内の座標をすべて返す", () => { assert.equal(positions.length, box.volume); assert.equal(positions.length, 2 * 3 * 4); - // 並びは Y、Z、X の順で、X がいちばん内側で動く + // 並びはY、Z、Xの順で、Xがいちばん内側で動く assert.ok(positions[0].equals(new BlockPos(0, 0, 0))); assert.ok(positions[1].equals(new BlockPos(1, 0, 0))); assert.ok(positions[2].equals(new BlockPos(0, 0, 1))); diff --git a/typescript/tests/nbt.test.ts b/typescript/tests/nbt.test.ts index 9705e9d..39bcde9 100644 --- a/typescript/tests/nbt.test.ts +++ b/typescript/tests/nbt.test.ts @@ -1,9 +1,9 @@ /** - * NBT レイヤの単体テスト。 + * NBTレイヤの単体テスト * - * 他言語版と同じ検証項目を持つ。 - * 共通テストベクタによる言語間比較は spec/run-conformance.sh が担当し、 - * ここでは API の振る舞いを直接確かめる。 + * 他言語版と同じ検証項目を持つ + * 共通テストベクタによる言語間比較はspec/run-conformance.shが担当し、 + * ここではAPIの振る舞いを直接確かめる */ import assert from "node:assert/strict"; @@ -40,7 +40,7 @@ import { const UNCOMPRESSED_READ = { compression: Compression.None }; const UNCOMPRESSED_WRITE = { compression: Compression.None }; -/** 仕様書どおりに組んだ最小の NBT。 */ +/** 仕様書どおりに組んだ最小のNBT */ function helloWorldBytes(): Uint8Array { return Uint8Array.from([ // ルート: TAG_Compound、名前 "hello world" @@ -101,7 +101,7 @@ test("MUTF-8: 不正な入力を弾く", () => { Uint8Array.from([0xe3, 0x81]), ]; - // 素の 0x00 / 冗長符号化 / 4バイト形式 / 途中で切れた入力 のすべてを拒否する + // 素の0x00、冗長符号化、4バイト形式、途中で切れた入力のすべてを拒否する for (const data of cases) { assertErrorCode(() => mutf8.decode(data), ErrorCode.MalformedData); } @@ -124,6 +124,16 @@ test("手で組んだ hello_world を読める", () => { assert.equal(named.tag.getString("name"), "Bananrama"); }); +test("NaNのビットパターンを変えずに書き戻す", () => { + // ペイロード付き・負・シグナリングのNaNを、Float / Doubleの両方で持つCompound + const original = Uint8Array.from([ + 0x0a, 0x00, 0x00, 0x05, 0x00, 0x01, 0x61, 0x7f, 0xc0, 0x00, 0x01, 0x05, 0x00, 0x01, 0x62, 0xff, 0xc0, 0x00, 0x00, 0x05, 0x00, 0x01, 0x63, 0x7f, 0x80, 0x00, 0x01, 0x06, 0x00, 0x01, 0x64, 0x7f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x06, 0x00, 0x01, 0x65, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x66, 0x7f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + ]); + const named = readBytes(original, UNCOMPRESSED_READ); + + assert.deepEqual(writeBytes(named, UNCOMPRESSED_WRITE), original); +}); + test("読んで書き直すと同じバイト列になる", () => { const original = helloWorldBytes(); const named = readBytes(original, UNCOMPRESSED_READ); @@ -148,7 +158,7 @@ test("浮動小数点の特殊値はビットパターンが保たれる", () => const encoded = writeBytes(new NamedTag("", root), UNCOMPRESSED_WRITE); const decoded = readBytes(encoded, UNCOMPRESSED_READ).tag; - // -0 と +0 は === では区別できないので Object.is で見る + // -0と+0は === では区別できないので、Object.isで見る assert.ok(Object.is(decoded.getDouble("negative_zero"), -0)); assert.ok(Number.isNaN(decoded.getFloat("nan"))); assert.equal(decoded.getDouble("infinity"), Number.POSITIVE_INFINITY); @@ -188,11 +198,23 @@ test("リストは異なる型の混在を拒否する", () => { assertErrorCode(() => list.add(new NbtString("x")), ErrorCode.UnexpectedTagType); }); +test("リストの範囲外の位置は要素型を確定させずに拒否する", () => { + const list = new NbtList(); + + assert.throws(() => list.set(0, new NbtInt(1)), RangeError); + assert.throws(() => list.insert(1, new NbtInt(1)), RangeError); + assert.throws(() => list.get(0), RangeError); + assert.throws(() => list.removeAt(0), RangeError); + + // 失敗した操作で要素型が確定していない + assert.equal(list.elementType, TagType.End); +}); + test("型付き取得子はキー欠落と型不一致を区別する", () => { const root = new NbtCompound(); root.set("value", new NbtString("text")); - // キーが無い場合は undefined + // キーが無い場合はundefined assert.equal(root.optInt("missing"), undefined); // 型が違う場合はキーの有無に関わらず例外 @@ -203,7 +225,7 @@ test("型付き取得子はキー欠落と型不一致を区別する", () => { test("圧縮方式が自動判定される", () => { const named = readBytes(helloWorldBytes(), UNCOMPRESSED_READ); - // 3 種の方式それぞれで、書き出した結果を方式指定なしで読み戻せること + // 3種の方式それぞれで、書き出した結果を方式指定なしで読み戻せること for (const method of [Compression.Gzip, Compression.Zlib, Compression.None]) { const encoded = writeBytes(named, { compression: method }); assert.equal(detectCompression(encoded), method); @@ -211,6 +233,19 @@ test("圧縮方式が自動判定される", () => { } }); +test("壊れた圧縮データはMALFORMED_DATAになる", () => { + const named = readBytes(helloWorldBytes(), UNCOMPRESSED_READ); + const gzip = writeBytes(named, { compression: Compression.Gzip }); + const truncated = gzip.slice(0, Math.floor(gzip.length / 2)); + const brokenGzip = Uint8Array.from([0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff]); + const brokenZlib = Uint8Array.from([0x78, 0x9c, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]); + + // 途中で切れたGZip、中身の壊れたGZip、中身の壊れたZlibは、どれも仕様に反するデータとして扱う + for (const data of [truncated, brokenGzip, brokenZlib]) { + assertErrorCode(() => readBytes(data), ErrorCode.MalformedData); + } +}); + test("ネットワーク形式はルート名を持たない", () => { const root = new NbtCompound(); root.set("x", new NbtInt(1)); @@ -220,7 +255,7 @@ test("ネットワーク形式はルート名を持たない", () => { compression: Compression.None, }); - // タグID + ペイロード のみで、名前長の 2 バイトが無い + // タグID + ペイロードのみで、名前長の2バイトが無い assert.equal(encoded[0], 0x0a); assert.equal(encoded[1], 0x03); @@ -242,7 +277,7 @@ test("途中で切れた入力を弾く", () => { }); test("巨大な宣言長は確保前に弾く", () => { - // ルート直下に「長さ 0x7FFFFFFF の ByteArray」を宣言するだけの入力 + // ルート直下に「長さ0x7FFFFFFFのByteArray」を宣言するだけの入力 const data = Uint8Array.from([ 0x0a, 0x00, 0x00, 0x07, 0x00, 0x01, 0x61, 0x7f, 0xff, 0xff, 0xff, ]); @@ -278,7 +313,7 @@ test("書き込みで Auto は指定できない", () => { }); test("整数の範囲は構築時に検査される", () => { - // number は幅を持たないため、構築時に検査しないと書き出しで黙って壊れる + // numberは幅を持たないため、構築時に検査しないと書き出しで気づかないうちにおかしくなる assertErrorCode(() => new NbtByte(128), ErrorCode.InvalidArgument); assertErrorCode(() => new NbtByte(-129), ErrorCode.InvalidArgument); assertErrorCode(() => new NbtShort(32768), ErrorCode.InvalidArgument); @@ -287,7 +322,7 @@ test("整数の範囲は構築時に検査される", () => { }); test("Long は bigint で保持され精度が落ちない", () => { - // number では 2^53 を超える整数を正確に表せないため bigint を使う + // numberでは2^53を超える整数を正確に表せないため、bigintを使う const root = new NbtCompound(); root.set("seed", new NbtLong(-4172144997902289642n)); @@ -303,6 +338,10 @@ test("Float は構築時に binary32 へ丸められる", () => { assert.notEqual(value, 0.1); }); +/** + * TAG_Endを除く12種のタグ型をすべて含むCompoundを作る + * TAG_EndはCompoundの終端として書き出されるので、往復させれば13種すべてを読み書きする + */ function buildAllTags(): NbtCompound { const root = new NbtCompound(); root.set("byte", new NbtByte(-128)); @@ -335,7 +374,7 @@ function buildAllTags(): NbtCompound { function buildNestedCompound(depth: number): Uint8Array { const bytes: number[] = [0x0a, 0x00, 0x00]; - // ルート + (depth - 1) 段の入れ子 + // ルート + (depth - 1)段の入れ子 for (let index = 0; index < depth - 1; index++) { bytes.push(0x0a, 0x00, 0x01, 0x63); } @@ -389,7 +428,8 @@ test("拡張された整数リテラル", () => { }); test("16進リテラルの接尾辞の扱いは仕様で固定されている", () => { - // 仕様 11 の 2.1: 16進では b/d/f を数字として読む。幅接尾辞は s/l のみ + // 16進ではb/d/fを数字として読む + // 幅接尾辞はs/lのみ assert.equal((snbt.parse("0xFF") as NbtInt).value, 255); assert.equal((snbt.parse("0xFFb") as NbtInt).value, 4091); assert.equal((snbt.parse("0xFFl") as NbtLong).value, 255n); @@ -422,7 +462,7 @@ test("型付き配列", () => { BigInt64Array.from([1n, 2n]), ); - // 接尾辞なしでも範囲内なら受理する(Minecraft 自身がそう書き出すため) + // 接尾辞なしでも範囲内なら受理する(Minecraft自身がそう書き出すため) assert.deepEqual((snbt.parse("[B; 1, 2]") as NbtByteArray).value, Int8Array.from([1, 2])); assertErrorCode(() => snbt.parse("[B; 200]"), ErrorCode.MalformedData); }); @@ -433,7 +473,7 @@ test("末尾カンマを許す", () => { }); test("異種リストは受理しない", () => { - // バイナリ NBT へ写せないため受理しない (adr/0006) + // バイナリNBTへ写せないため受理しない assertErrorCode(() => snbt.parse('[1, "a"]'), ErrorCode.MalformedData); }); @@ -446,6 +486,26 @@ test("エスケープシーケンス", () => { assertErrorCode(() => snbt.parse('"\\N{SNOWMAN}"'), ErrorCode.UnsupportedFeature); }); +test("サロゲートのエスケープはどこでも同じ規則に従う", () => { + const lone = String.fromCharCode(0xd800); + + // \Uでサロゲートの範囲を書くと、\uと同じく孤立サロゲートになる + assert.equal((snbt.parse('"\\U0000D800"') as NbtString).value, lone); + assert.equal((snbt.parse('"\\uD800"') as NbtString).value, lone); + + // 対になったサロゲートは補助文字1文字と同じ + assert.equal((snbt.parse('"\\uD83D\\uDE00"') as NbtString).value, "\u{1F600}"); + + // コードポイントの範囲外と、キーの孤立サロゲートは仕様に反する + for (const source of ['"\\U00110000"', '{"\\uD800":1}', '{"\\U0000D800":1}']) { + assertErrorCode(() => snbt.parse(source), ErrorCode.MalformedData); + } + + // APIからも孤立サロゲートのキーは設定できない + const compound = new NbtCompound(); + assertErrorCode(() => compound.set(lone, new NbtInt(1)), ErrorCode.InvalidArgument); +}); + test("単一引用符の文字列", () => { assert.equal((snbt.parse("'say \"hi\"'") as NbtString).value, 'say "hi"'); }); @@ -533,7 +593,7 @@ test("Double の正準10進表記", () => { }); test("負のゼロは符号を保つ", () => { - // JavaScript の toExponential は -0 の符号を落とすため、ここが崩れやすい + // JavaScriptのtoExponentialは-0の符号を落とすため、ここで出力を間違えやすい assert.equal(snbt.write(new NbtDouble(-0)), "-0.0d"); assert.equal(snbt.write(new NbtFloat(-0)), "-0.0f"); }); @@ -545,6 +605,12 @@ test("特殊値の表記", () => { assert.equal(snbt.write(new NbtFloat(Number.NaN)), "NaNf"); }); +test("binary32の範囲を超える値は符号付きの無限大になる", () => { + assert.equal((snbt.parse("1e39f") as NbtFloat).value, Number.POSITIVE_INFINITY); + assert.equal((snbt.parse("-1e39f") as NbtFloat).value, Number.NEGATIVE_INFINITY); + assert.equal(new NbtFloat(1e39).value, Number.POSITIVE_INFINITY); +}); + test("書き出した表記を読み戻すとビットが一致する", () => { const doubles = [0, -0, 1, -1, 0.1, 1 / 3, 1e300, 1e-300, 4903]; const view = new DataView(new ArrayBuffer(8)); @@ -580,14 +646,14 @@ test("equals: 同じ型で同じ値なら等しい", () => { }); test("equals: 型が違えば等しくない", () => { - // 値が同じでもタグの型が違えば別物 + // 値が同じでもタグの型が違えば等しくない assert.ok(!new NbtInt(1).equals(new NbtShort(1))); assert.ok(!new NbtInt(1).equals(1)); assert.ok(!new NbtInt(1).equals(undefined)); }); test("equals: 浮動小数点はビットパターンで比べる", () => { - // NaN 同士は等しく、+0.0 と -0.0 は等しくない + // NaN同士は等しく、+0.0と-0.0は等しくない assert.ok(new NbtFloat(NaN).equals(new NbtFloat(NaN))); assert.ok(new NbtDouble(NaN).equals(new NbtDouble(NaN))); assert.ok(!new NbtFloat(0).equals(new NbtFloat(-0))); @@ -609,7 +675,7 @@ test("equals: リストは要素型と並びを見る", () => { reversed.add(new NbtInt(1)); assert.ok(!left.equals(reversed)); - // 空でも要素型が違えば別物 + // 空でも要素型が違えば等しくない assert.ok(!new NbtList(TagType.Int).equals(new NbtList(TagType.Byte))); }); @@ -623,7 +689,7 @@ test("equals: Compound はキーの並び順も見る", () => { same.set("b", new NbtInt(2)); assert.ok(left.equals(same)); - // 中身は同じでも挿入順が違えば別物 + // 中身は同じでも挿入順が違えば等しくない const reordered = new NbtCompound(); reordered.set("b", new NbtInt(2)); reordered.set("a", new NbtInt(1)); @@ -719,7 +785,7 @@ test("型付き設定子は取得子と対になっている", () => { assert.deepEqual(root.getIntArray("ia"), Int32Array.from([1, -1])); assert.deepEqual(root.getLongArray("la"), BigInt64Array.from([1n, -1n])); - // 真偽値は TAG_Byte の 0 / 1 として入る + // 真偽値はTAG_Byteの0 / 1として入る assert.equal(root.get("t").type, TagType.Byte); }); @@ -736,7 +802,7 @@ test("型付き設定子は挿入順を変えない", () => { }); test("どの型も人が読める文字列になる", () => { - // 中身の形式は仕様で決めていないので、[object Object] にならないことだけを見る + // 中身の形式は仕様で決めていないので、[object Object]にならないことだけを見る const list = new NbtList(); list.add(new NbtInt(1)); @@ -759,7 +825,7 @@ test("どの型も人が読める文字列になる", () => { new NamedTag("root", compound), ]; - // どれも既定の [object Object] のままになっていないこと + // どれも既定の[object Object]のままになっていないこと for (const value of values) { assert.notEqual(String(value), "[object Object]", value.constructor.name); } @@ -770,7 +836,7 @@ test("どの型も人が読める文字列になる", () => { assert.equal(String(compound), "{1 要素}"); }); -/** 複数のバイト列をつなぐ。 */ +/** 複数のバイト列をつなぐ */ function concat(...parts: Uint8Array[]): Uint8Array { let total = 0; @@ -791,6 +857,6 @@ function concat(...parts: Uint8Array[]): Uint8Array { return joined; } -/** 型が推論できることを確かめるだけの参照(未使用変数の警告を避ける)。 */ +/** 型が推論できることを確かめるだけの参照(未使用変数の警告を避ける) */ const _tagTypeReference: NbtTag | undefined = undefined; void _tagTypeReference; diff --git a/typescript/tests/world.test.ts b/typescript/tests/world.test.ts index 4eb2429..414aab9 100644 --- a/typescript/tests/world.test.ts +++ b/typescript/tests/world.test.ts @@ -1,15 +1,12 @@ /** - * World / Block レイヤ。 + * World / Blockレイヤ * - * 仕様: docs/spec/30-chunk-format.md / 31-paletted-container.md / 40-world-layout.md - * - * 他言語版と同じ検証項目を持つ。 - * 共通テストベクタによる言語間比較は spec/run-conformance.sh が担当し、 - * ここでは API の振る舞いを直接確かめる。 + * 他言語版と同じ検証項目を持つ + * 共通テストベクタによる言語間比較はspec/run-conformance.shが担当し、ここではAPIの振る舞いを直接確かめる */ import assert from "node:assert/strict"; -import { mkdtempSync, rmSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; @@ -35,30 +32,32 @@ import { NbtString, readFile, writeBytes, + writeFile, } from "../src/nbt/index.js"; +import { RegionFile, RegionFileMode } from "../src/anvil/index.js"; const HERE = fileURLToPath(new URL(".", import.meta.url)); const REPO_ROOT = join(HERE, "..", "..", ".."); const VECTORS = join(REPO_ROOT, "spec", "testdata", "world"); -/** 共通テストベクタ(world/*.nbt)のパス。 */ +/** 共通テストベクタ(world/*.nbt)のパス */ function vectorPath(name: string): string { return join(VECTORS, `${name}.nbt`); } -/** テストベクタをチャンクとして読む。 */ +/** テストベクタをチャンクとして読む */ function loadChunk(name: string, options?: ChunkReadOptions): Chunk { return Chunk.fromNbt(readFile(vectorPath(name)).tag, options); } -/** ブロックのパレット要素を作る。 */ +/** ブロックのパレット要素を作る */ function blockEntry(name: string): NbtCompound { const entry = new NbtCompound(); entry.set("Name", new NbtString(name)); return entry; } -/** SpringNbtError が指定のコードで投げられることを確かめる。 */ +/** SpringNbtErrorが指定のコードで投げられることを確かめる */ function assertErrorCode(code: ErrorCode, action: () => unknown): void { assert.throws(action, (error: unknown) => { assert.ok(error instanceof SpringNbtError); @@ -127,7 +126,7 @@ test("BlockState: 壊れた文字列は INVALID_ARGUMENT", () => { "minecraft:oak_stairs[]extra", ]; - // 壊し方ごとに同じエラーコードになることを確かめる + // 崩し方ごとに同じエラーコードになることを確かめる for (const text of broken) { assertErrorCode(ErrorCode.InvalidArgument, () => BlockState.parse(text)); } @@ -160,7 +159,8 @@ test("BitStorage: 書いた値をそのまま読み出せる", () => { test("BitStorage: long 境界を跨がずに詰める", () => { const storage = BitStorage.create(5, 4096); - // bits=5 なら 1 つの long に 12 個。12 個目は次の long の最下位から始まる + // bits=5なら1つのlongに12個 + // 添字12は次のlongの最下位から始まる storage.set(11, 31); storage.set(12, 1); const longs = storage.toLongs(); @@ -192,7 +192,7 @@ test("BitStorage: ビット幅に対して長さが合わない配列は MALFORM }); test("BitStorage: 寛容モードなら長さからビット幅を逆算する", () => { - // 4096 エントリを 342 long で表せるのは bits=5 のときだけ + // 4096エントリを342 longで表せるのはbits=5のときだけ const storage = BitStorage.fromLongs(new BigInt64Array(342), 4, 4096, true); assert.equal(storage.bitsPerEntry, 5); }); @@ -231,7 +231,7 @@ test("PalettedContainer: 単一値のコンテナは data を持たない", () = test("PalettedContainer: 値を足すとパレットとビット幅が広がる", () => { const container = PalettedContainer.filled(blockEntry("minecraft:air"), 4096, 4); - // パレットを 17 要素まで増やして bits=4 から 5 への拡張を起こす + // 17種のブロックを足してパレットを18要素にし、bits=4から5への拡張を起こす for (let index = 0; index < 17; index++) { container.set(index, blockEntry(`minecraft:block_${index}`)); } @@ -257,7 +257,7 @@ test("PalettedContainer: compact で未使用のパレット要素が消える", container.compact(); - // 残るのは実際に使われている air と dirt の 2 つ + // 残るのは実際に使われているairとdirtの2つ assert.equal(container.palette.length, 2); assert.equal((container.get(0) as NbtCompound).getString("Name"), "minecraft:dirt"); }); @@ -298,7 +298,8 @@ test("Chunk: ビット幅 5 のチャンクを端から端まで読める", () = const chunk = loadChunk("palette_17"); const head = ["minecraft:air", "minecraft:stone"]; - // ベクタの添字は (位置 * 11) % 17。パレット先頭 2 つだけ名前が違う + // ベクタの添字は (位置 * 11) % 17 + // パレットの先頭2つだけ名前が違う for (let position = 0; position < 4096; position++) { const paletteIndex = (position * 11) % 17; const block = chunk.getBlock(position & 15, -64 + (position >> 8), (position >> 4) & 15); @@ -347,7 +348,7 @@ test("Chunk: 変更したチャンクには印が付く", () => { chunk.setBlock(3, -60, 7, "minecraft:stone"); assert.equal(chunk.isModified, true); - // 保存済みとして印を下ろせる + // 保存済みとして印を外せる chunk.isModified = false; assert.equal(chunk.isModified, false); @@ -363,7 +364,7 @@ test("Chunk: バイオームは 4 ブロック単位で効く", () => { const chunk = loadChunk("palette_1"); chunk.setBiome(0, -64, 0, "minecraft:desert"); - // 同じ 4×4×4 の枠内はまとめて変わる + // 同じ4×4×4の枠内はまとめて変わる assert.equal(chunk.getBiome(3, -61, 3), "minecraft:desert"); assert.equal(chunk.getBiome(4, -64, 0), "minecraft:plains"); }); @@ -397,13 +398,13 @@ test("Chunk: ブロックを置き換えると同じ座標の付随データが assert.equal(chunk.raw.getList("block_ticks").size, 2); assert.equal(chunk.raw.getList("fluid_ticks").size, 1); - // (0,-64,0) には chest と block_tick、(1,-64,1) には furnace と fluid_tick がある + // (0,-64,0)にはchestとblock_tick、(1,-64,1)にはfurnaceとfluid_tickがある chunk.setBlock(0, -64, 0, BlockState.parse("minecraft:stone")); chunk.setBlock(1, -64, 1, BlockState.parse("minecraft:stone")); const entities = chunk.raw.getList("block_entities"); - // 触っていない (15,-50,15) の barrel だけが残る + // 触っていない(15,-50,15)のbarrelだけが残る assert.equal(entities.size, 1); assert.equal((entities.get(0) as NbtCompound).getString("id"), "minecraft:barrel"); @@ -427,8 +428,7 @@ test("Chunk: 同じブロックを置き直しても付随データは消えな }); test("Chunk: 別のチャンクの同じ相対座標は消さない", () => { - // 付随データは絶対座標で持つので、チャンク座標を取り違えると - // 無関係な要素を消してしまう + // 付随データは絶対座標で持つので、チャンク座標を間違えると無関係な要素を消してしまう const root = readFile(vectorPath("block_entities")).tag; root.set("xPos", new NbtInt(1)); root.set("zPos", new NbtInt(1)); @@ -436,7 +436,8 @@ test("Chunk: 別のチャンクの同じ相対座標は消さない", () => { const chunk = Chunk.fromNbt(root); chunk.setBlock(0, -64, 0, BlockState.parse("minecraft:stone")); - // このチャンクの (0,-64,0) は絶対座標 (16,-64,16)。どれとも一致しない + // このチャンクの(0,-64,0)は絶対座標(16,-64,16) + // どれとも一致しない assert.equal(chunk.raw.getList("block_entities").size, 3); }); @@ -464,10 +465,10 @@ test("Chunk: チャンク内の相対座標が範囲外なら INVALID_ARGUMENT", }); // --------------------------------------------------------------------------- -// DataVersion の扱い +// DataVersionの扱い // --------------------------------------------------------------------------- -/** DataVersion だけを差し替えたチャンクを作る。 */ +/** DataVersionだけを差し替えたチャンクを作る */ function foreignChunk(): NbtCompound { const root = readFile(vectorPath("palette_1")).tag; root.set("DataVersion", new NbtInt(3953)); @@ -514,11 +515,11 @@ test("DataVersion: 許可すれば古いチャンクも元のバージョンの onVersionMismatch: VersionMismatchAction.Ignore, }); - // DataVersion は読んだ値のまま残す + // DataVersionは読んだ値のまま残す assert.equal(chunk.toNbt({ allowForeignDataVersion: true }).getInt("DataVersion"), 3953); }); -/** 対象より新しい DataVersion を持つチャンクを作る。 */ +/** 対象より新しいDataVersionを持つチャンクを作る */ function newerChunk(): NbtCompound { const root = readFile(vectorPath("palette_1")).tag; root.setInt("DataVersion", 5015); @@ -528,7 +529,7 @@ function newerChunk(): NbtCompound { test("DataVersion: 新しいバージョンのチャンクは警告を出さない", () => { const warnings: string[] = []; - // 形式が同じであれば、新しいバージョンでも黙って読める + // 形式が同じであれば、新しいバージョンでも警告を出さずに読める const chunk = Chunk.fromNbt(newerChunk(), { onVersionMismatch: VersionMismatchAction.Warn, onWarning: (message) => warnings.push(message), @@ -549,7 +550,7 @@ test("DataVersion: 新しいバージョンのチャンクはエラー設定で test("DataVersion: 新しいバージョンのチャンクはそのまま書き戻せる", () => { const chunk = Chunk.fromNbt(newerChunk()); - // 許可を出さなくても書き戻せて、DataVersion も変わらない + // 許可を出さなくても書き戻せて、DataVersionも変わらない assert.equal(chunk.toNbt().getInt("DataVersion"), 5015); }); @@ -577,3 +578,34 @@ test("MinecraftWorld: level.dat が無いディレクトリは IO", () => { rmSync(work, { recursive: true, force: true }); } }); + +test("MinecraftWorld: 次元経由で読んだチャンクの警告も通知先へ届く", () => { + const work = mkdtempSync(join(tmpdir(), "springnbt-world-")); + + try { + const region = join(work, "dimensions", "minecraft", "overworld", "region"); + mkdirSync(region, { recursive: true }); + + // DataVersionが扱える形式より古いチャンクを1つだけ持つワールドを作る + const level = new NbtCompound(); + level.set("Data", new NbtCompound()); + writeFile(join(work, "level.dat"), new NamedTag("", level)); + + const file = RegionFile.open(join(region, "r.0.0.mca"), RegionFileMode.ReadWrite); + file.writeChunk(0, 0, foreignChunk()); + file.close(); + + const warnings: string[] = []; + const world = MinecraftWorld.open(work, { chunkRead: { onWarning: (message) => warnings.push(message) } }); + const overworld = world.dimension("minecraft:overworld"); + assert.ok(overworld !== undefined); + assert.ok(overworld.chunk(0, 0) !== undefined); + world.close(); + + // ワールドを開くときに渡した通知先が、次元経由の読み込みでも呼ばれる + assert.equal(warnings.length, 1); + } finally { + // テストごとに作った一時ディレクトリを片付ける + rmSync(work, { recursive: true, force: true }); + } +});