Skip to content

Latest commit

 

History

History
210 lines (158 loc) · 9 KB

File metadata and controls

210 lines (158 loc) · 9 KB

11. SNBT (Stringified NBT)

NBT のテキスト表現。コマンドやデバッグ出力で使われる形式で、 本ライブラリはパースと出力の両方に対応する。

1.21.5 で導入された拡張構文を含む。記述は Java版 26.2 で確認した。

前提: 10 NBT バイナリ形式


1. 文法

value       = compound | list | array | string | number | boolean | function ;

compound    = "{" [ pair { "," pair } [ "," ] ] "}" ;
pair        = key ":" value ;
key         = bare_string | quoted_string ;

list        = "[" [ value { "," value } [ "," ] ] "]" ;
array       = "[" ("B" | "I" | "L") ";" [ value { "," value } [ "," ] ] "]" ;

number      = [ sign ] ( int_literal | float_literal ) [ suffix ] ;
int_literal = dec_digits | "0x" hex_digits | "0b" bin_digits ;
float_literal = dec_digits "." [ dec_digits ] [ exponent ]
              | "." dec_digits [ exponent ]
              | dec_digits exponent ;
exponent    = ("e" | "E") [ sign ] dec_digits ;
suffix      = [ "u" | "s" ] ( "b" | "B" | "s" | "S" | "l" | "L" | "f" | "F" | "d" | "D" ) ;

boolean     = "true" | "false" ;
function    = ("bool" | "uuid") "(" value ")" ;

string      = bare_string | quoted_string ;
bare_string = ( "a".."z" | "A".."Z" | "0".."9" | "_" | "-" | "." | "+" )+ ;
quoted_string = '"' { char | escape } '"' | "'" { char | escape } "'" ;

数値リテラルの区切り文字 _ は桁の間に置ける(123_456 = 123456)。


2. 型の決定

記法 NBT 型
1b 1B Byte
1s 1S Short
1 Int
1l 1L Long
1.0f 1.0F Float
1.0 1.0d 1.0D Double
true false Byte1b / 0b
[B; 1b, 2b] ByteArray
[I; 1, 2] IntArray
[L; 1L, 2L] LongArray
[1, 2] List<Int>
{} Compound
その他 String

接尾辞のない整数は Int、小数点や指数を含む数は DoubleInt の範囲を超える接尾辞なし整数は MALFORMED_DATA(暗黙に Long へ格上げしない)。

符号接尾辞 u / s は幅接尾辞の前に置く(10ub = 符号なしとして解釈した後に Byte へ格納)。 u 付きの値が対象幅の符号なし最大値を超えたら MALFORMED_DATA

2.1 16進リテラルの接尾辞

0x に続く b B d D f F16進数字として解釈する。 したがって 16進リテラルに使える幅接尾辞は s S l L だけとする。

0xFF    -> Int 255        (FF は 16進数字)
0xFFb   -> Int 4091       (b も 16進数字。Byte 接尾辞ではない)
0xFFl   -> Long 255
0xFFs   -> Short 255

これは Minecraft の実装が明示していない曖昧点で、 どちらに解釈しても文法上は成り立ってしまう。 黙って一方を選ぶと全言語で挙動がずれるため、ここで明示的に固定する。 Byte / Float / Double を16進で書きたい場合は10進表記を使う。

2進リテラル (0b) の数字は 01 だけなので、この曖昧さは起きない。 全ての幅接尾辞を使える。ただし 0b 単体は「10進の 0 に Byte 接尾辞」と解釈する (0b は真偽値の false として広く使われているため)。

2.2 特殊な浮動小数点値

Infinity / -Infinity / NaN を受理する。幅接尾辞を付けられる。 接尾辞が無ければ Double

bool(x)x が 0 以外なら 1b、0 なら 0b を返す。 uuid("...") は UUID を IntArray 4 要素(上位から順)へ変換する。


3. 文字列とエスケープ

引用符は "' のどちらも使える。文字列内では使っていない方の引用符をそのまま書ける。

エスケープ 意味
\\ \
\" \' 引用符
\b \s \t \n \f \r 制御文字(\s は空白)
\xNN 8bit 値
\uNNNN UTF-16 コード単位
\UNNNNNNNN コードポイント
\N{名前} Unicode 文字名(例: \N{SNOWMAN}

出力側は " で囲み、次のものだけをエスケープする。

対象 出力
" \ \" \\
\b \t \n \f \r 同じ短縮形
その他の制御文字 (U+0000U+001F, U+007F) \uXXXX(小文字16進4桁)
孤立サロゲート (U+D800U+DFFF で対になっていないもの) \uXXXX

補助文字(正しいサロゲートペア)はエスケープせず、そのまま出す。 UTF-16 を基本とする言語(C# / Java)では 2 つのコード単位に分かれて見えるため、 素朴に「サロゲートならエスケープ」と実装すると コードポイント単位の言語(Python / Rust)と出力が食い違う。 判定は必ず「対になっているか」で行う。

\N{...} は入力としてのみ受理し、出力では使わない(実装間で名前表が揃わないため)。


4. リストの異種要素

1.21.5 以降、[1, "a", {}] のような異なる型の混在リストが SNBT で書ける。 これは List<Compound> へ暗黙変換され、各要素が {"": <値>} のような包み方ではなく、Minecraft 側の規則に従って正規化される。

本ライブラリはバイナリ NBT の制約(リストは単一型)を優先し、 異種リストをパースした場合は MALFORMED_DATA を返す。 コマンド文字列の解釈まで担うライブラリではないため、 バイナリへ写せない表現は受理しない(→ adr/0006)。


5. 出力

Snbt.parse(text)          -> NbtTag
Snbt.write(tag)           -> String    -- 1行。空白なし
Snbt.write_pretty(tag)    -> String    -- インデント 4、要素ごとに改行

出力規則:

  • Compound のキーは、bare_string として書ける場合は引用符なし。それ以外は " で囲む
  • キーの順序は挿入順(ソートしない)
  • Float / Double は下記の正準10進表記に接尾辞を付ける(1.0f / 1.0d
  • Double は接尾辞 d必ず付ける(省略すると読み戻し時に型が同じでも意図が読み取りにくいため)
  • 特殊値は Infinityd / -Infinityd / NaNdFloat なら接尾辞 f
  • 型付き配列の要素は接尾辞付きで書く([B; 1b, 2b] / [L; 1L])。IntArray のみ接尾辞なし

型付き配列のパースでは、要素が Byte / Short / Int / Long のいずれでも受理し、 対象の幅に収まるか検査したうえで格納する。Minecraft 自身が [I; 1, 2] のように接尾辞なしで書き出すため、厳格にすると実データを読めない。 範囲外の値は MALFORMED_DATA

パースして出力し直した結果が入力と一致することは保証しない(空白や引用の揺れがあるため)。 保証するのは「SNBT → NBT → SNBT → NBT」で NBT が一致することである。

5.1 浮動小数点の正準10進表記

各言語の標準の数値書式(C# の "R"、Java の Float.toString、Python の repr、 Rust の {})は互いに一致しない。指数表記へ切り替わる閾値も、指数部の桁数も、 E の大文字小文字も処理系ごとに違う。 そのままでは SNBT 出力の言語間一致が成立しないため、次の手順を仕様として固定する。

canonical_decimal(値 v, f32 か f64 か):
  1. 有効数字 p を 1 から順に増やす(f32 は 9 まで、f64 は 17 まで)
       s = v を「有効数字 p 桁の指数表記」で書式化したもの
       s を同じ幅の浮動小数点として読み戻し、ビットパターンが v と一致したら採用
  2. 採用した s から、仮数の数字列 digits(末尾のゼロを除去)と
     10進指数 exp10 を取り出す(v = 0.digits × 10^(exp10+1) ではなく
     v = digits[0].digits[1..] × 10^exp10 と読む)
  3. -4 <= exp10 <= 16 なら固定小数点表記、そうでなければ指数表記にする

     固定小数点表記:
        exp10 >= 0 -> 整数部 = digits の先頭 (exp10+1) 桁(足りなければ 0 で右詰め)
                      小数部 = 残りの桁。無ければ "0"
        exp10 <  0 -> "0." + ("0" × (-exp10 - 1)) + digits

     指数表記:
        digits[0] + "." + (digits[1..] があればそれ、無ければ "0") + "E" + exp10
  • 小数部は必ず 1 桁以上書く(1 ではなく 1.0)。接尾辞なしでも小数と分かるようにするため
  • 指数記号は大文字 E、指数はゼロ詰めしない(1.0E201.0E-30
  • 負値は先頭に - を付ける。-0.0-0.0 と書く
  • 特殊値は Infinity / -Infinity / NaN

例:

f32 の出力 f64 の出力
1 1.0f 1.0d
0.75 0.75f 0.75d
0.49823147 0.49823147f
2000 2000.0f 2000.0d
0.015 0.015d
1e20 1.0E20f 1.0E20d