Blobフォーマット
.blobファイルは.jsonと同じデータをバイナリで持ちます:24バイトのヘッダーの後にデータが続きます。キャッシュではなく、対等なレベルフォーマットです
レベルは.blobだけで保存することもできます
レベルvolcano、19,341オブジェクト | サイズ |
|---|---|
level.json | 15.7 MB |
level.blob | 5.1 MB、読み込み203 ms |
.blobは可読性を約束しません。目で読むことも差分で比較することもできないので、どこでもデフォルトにはなっていません
各モデルのコーデックはRoslynのジェネレーターが生成します
ヘッダー
| オフセット | サイズ | フィールド | 値 |
|---|---|---|---|
| 0 | 4 | magic | uint 0x4F424842、バイト列42 48 42 4F(BHBO) |
| 4 | 2 | コーデックの世代 | ushort、BlobFormat.Generation = 1 |
| 6 | 2 | フラグ | ushort、ビット0のFlagHashed=ハッシュあり。残りのビットは予約済みで、0でなければならない |
| 8 | 8 | データ長 | long |
| 16 | 8 | ハッシュ | ulong、コーデックの世代をシードにしたデータのxxHash64 |
ヘッダー長BlobFormat.HeaderLengthは24バイトで、その後にデータが続きます
チェックの順序
BlobFormat.ReadHeaderはヘッダーを厳密な順序でチェックします。ヘッダーが通るまで、メモリは何も確保しません:
- magic
- コーデックの世代が1である
- 未知のフラグがない
- 宣言された長さが実際の長さと等しい
FlagHashedが立っていれば、ハッシュが一致する
失敗はどれも、それぞれ固有のメッセージを持つBlobFormatExceptionになります。「ファイルが破損している」と「ファイルが新しいビルドのもの」とでは、プレイヤーに求めることが違うので、1つのエラーにまとめません
ハッシュはあえて暗号学的なものにしていません。ハッシュは破損を検出し、改ざんからの保護はOpenPGPの層が担います。詳細:アーカイブと保護
エンコード
- リトルエンディアン、固定幅の数値、varintなし
- 文字列は、バイト数の
intに続くUTF-8 nullは長さ-1。そのため、空のリストと存在しないリストは往復変換の後も区別されます- ポリモーフィックな値は1バイトのタグで始まり、
0xFFはnull用に予約されています。タグはモデルのGetModelType()で、JSONが[tag, data]に書くものと同じ印です - ファイルから読んだ個数は、そのためのメモリを確保する前に
BlobReader.ReadCountでチェックされます
エンベロープと2つの世代
[ModelGeneration]を持つルートはそれぞれ、自分のエンベロープを書きます:文字列のドメイン、intのモデルの世代、内容の長さ、そして内容そのもの。ツールはファイルの世代を次のように読みます:
var bytes = File.ReadAllBytes(path);
var reader = new BlobReader(bytes, BlobFormat.HeaderLength, bytes.Length - BlobFormat.HeaderLength);
var domain = reader.ReadString();
var generation = reader.ReadInt();
ここには2つの異なる世代があります:
- ヘッダー内のコーデックの世代は、バイトのレイアウトを表します。これが変わると、古い
.blobはすべて丸ごと読めなくなります。戻る先がないからです - 各エンベロープ内のモデルの世代は、1つのドメインの形を表します。古いものはマイグレーションされ、新しいものは
NewerGenerationExceptionで拒否されます。詳細:バージョン管理とマイグレーション
この2つを1つの数にまとめることはできません。モデルの世代はエンベロープの中にあり、エンベロープを見つけられるのは、バイトのレイアウトをすでに知っている者だけです。モデルの変更でコーデックの世代が動くことはありません
ヘッダーの拡張
ヘッダーには予備のバイトも、自分の長さを持つフィールドもありません。拡張の方法は2つです:
- フラグ。空きは15ビットです。古い読み込み側は、知らないフラグのあるファイルを間違って読むのではなく拒否します
- 新しいコーデックの世代。新しいレイアウトは、ヘッダー長を含めて何でも変えられます。新しい読み込み側は、古い世代と新しい世代を並べて読めます
データの後ろに何かを追記することはできません。宣言された長さは実際の長さと一致しなければならないので、余分な末尾は拒否されます。新しいレベルデータは、モデルの世代を通じてデータの中に入れます
内容が宣言された長さちょうどで終わらない場合は、長すぎても短すぎても破損とみなされます。ルートがヘッダーのチェックをすべて通っても、パースできないことがあります。その場合は長さの分だけスキップされ、デフォルト値のまま残ります。スキップは黙って行われず、必ずSerializationReportに記録されます