【Minecraft】UbuntuのCLI上でNBT→JSONするツールを作った


# 前提

- Ubuntu 18.04.3 LTS
- bash
- jqを使用
    - # `apt install jq`

# あらまし

Minecraftのサーバーを動かしていて、Ubuntuのコマンドライン上でMinecraftのセーブデータを解析したい需要が生じたので、Minecraftのセーブデータのフォーマットと、それをJSON文字列化する方法を調べた。その結果、期待したようなものは得られなかったため、以下のNBTとJSONを相互に変換するプログラムをPerlで作った。

- MirrgieRiana / nbt_to_json_perl https://github.com/MirrgieRiana/nbt_to_json_perl

# 背景

Minecraftのサーバーを動かしていて、Ubuntuのコマンドライン上でMinecraftのセーブデータを解析したい需要が生じた。

## NBT

Minecraftのゲームデータは、NBTフォーマットで記述されている。これは値に型がついたバイナリ形式のJSON的なものである。セーブデータはほとんどがこのフォーマットで書かれている。

詳しい説明はWikiに任せる。

- NBTフォーマット - Minecraft Wiki https://minecraft-ja.gamepedia.com/NBT%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88

# 既存のプログラム

## NBTExplorer

NBTExplorerはぱっと見LinuxのCLI上でJSON化できるツールではなさそうだった。

- NBTExplorer https://www.minecraftforum.net/forums/mapping-and-modding-java-edition/minecraft-tools/1262665-nbtexplorer-nbt-editor-for-windows-and-mac

Windows用のGUIのNBT編集ソフトとしてはとても素晴らしかったが、今回はGUIのないUbuntuを使うので使用できない。

NBTUtilみたいな名前のツールが同封されており、これはWindowsのコマンドプロンプト（Bashからも可能）からならコマンドでNBTを独特のフォーマットに文字列化できる。JSONにもできるかもしれないが、Windows環境が求められたので無視した。

## nbt2json（midnightfreddie）

- GitHub - midnightfreddie/nbt2json https://github.com/midnightfreddie/nbt2json

Goで書かれたツールがあった。
こちらは入れようとしたけどGoの使い方が分からず、Go入門するくらいなら劣化版を自分で作った方が早そうだったので無視した。

## prismarine-nbt

-  PrismarineJS /
prismarine-nbt  https://github.com/PrismarineJS/prismarine-nbt

node用のモジュールらしい。かなりまともみたいなので本記事で作ったものよりよさそう。

## JSON-to-NBT-Converter（KSashaDF）

-  KSashaDF /
JSON-to-NBT-Converter https://github.com/KSashaDF/JSON-to-NBT-Converter

2020年2月27日現在imperfectらしく良い出来ではなさそうだった。

# 成果物

以下のプログラムを作った。

- MirrgieRiana / nbt_to_json_perl https://github.com/MirrgieRiana/nbt_to_json_perl

元々はここに使用例などを記載する予定だったが、結局READMEに色々書いたのでここでは軽く触れるだけに留める。

## NBTバイナリのJSON化

`nbt2json`を使うとNBTバイナリをJSONにできる。ただし、Minecraftのセーブデータは大体gzip圧縮されていることに気を付けなければならない。`nbt2json`は非圧縮のNBTバイナリデータを受け付ける。

- $ `cat sample/1.nbt | ./nbt2json`

```json:
{"key":"","type":10,"value":[{"key":"list","type":9,"value":{"type":1,"values":[1,2]}},{"key":"double","type":6,"value":"0x3fd0000000000000=0.25"},{"key":"int","type":3,"value":-1}]}
```

## 便利化と整形

`-cp`を付けると便利で整形された出力になる。
`-c`が便利化、`p`が整形である。`-c`を付けるとCompoundタグはJSONオブジェクトになるが、タグの順序は保持されないことに気を付けなければならない。

- $ `cat sample/1.nbt | ./nbt2json -cp`

```json:
{
   "C": {
      "Alist": {
         "type": "byte",
         "values": [
            1,
            2
         ]
      },
      "Ddouble": "0x3fd0000000000000=0.25",
      "Iint": -1
   }
}
```

## 逆変換

`json2nbt -c`は`nbt2json -cp`の逆関数で、JSONを呼んでNBTを吐く。
`-c`はCompoundタグの内部のタグの順序が崩れること以外は大体元のNBTを返す。
`nbt2json`にはいろいろなオプションがあるが、いくつかのオプションは`json2nbt`にそのまま食わせても読んでくれる。

浮動小数では、NaNや非正規数など複数のバイナリが一つの文字列表現になることがある。
これを`json2nbt`で逆変換すると恒等にならないため、浮動小数は16進文字列と10進表現ペアとして出力される。
`nbt2json -H`や`nbt2json -B`を使うとすべての数値が16進文字列や2進文字列に強制的になる。
`json2nbt`には`-H`や`-B`のオプションはないが、付けなくても受け付けてくれる。

- $ `cat sample/1.nbt | ./nbt2json -cp | ./json2nbt -c | ./nbt2json -cp`

```json:
{
   "C": {
      "Alist": {
         "type": "byte",
         "values": [
            1,
            2
         ]
      },
      "Ddouble": "0x3fd0000000000000=0.25",
      "Iint": -1
   }
}
```

## `-e`オプションの使用例

`level.dat`を読んでワールドのSeed値を得たい場合、jqとかを使うよりも次のように直接perlの変数を書き換えるのがよい。

- $ `cat level.dat | inflate-gzip | nbt2json -cp -e '$_ = $_->{C}->{CData}->{LRandomSeed}'`

```json:
7546217594578327695
```

jsonをすべて吐いてからjqを使う場合、データの変換のオーバーヘッドがかかるのはともかくとして、jsonは64ビット整数の扱いに難がある。

- $ `cat level.dat | inflate-gzip | nbt2json -cp | jq '.C.CData.LRandomSeed'`

```json:
7546217594578328000
```

そのような場合は`-H`オプションで16進文字列にして出力するとよい。

- $ `cat level.dat | inflate-gzip | nbt2json -cpH | jq '.C.CData.LRandomSeed'`

```json:
"0x6c443070ec80dfb0"
```