跳到正文
文档
本页内容

Blob格式

.blob文件的字节布局:24字节的文件头、它的检查顺序以及其中的封套

.blob保存的数据与.json相同,只是二进制形式。它是与JSON平等的关卡格式,而不是缓存:关卡可以只保存为.blob

关卡volcano,19341个物体大小
level.json15.7 MB
level.blob5.1 MB,读取耗时203毫秒

.blob不保证的是可读性。它无法用眼睛读,也无法做diff,所以它在任何地方都不是默认格式

每个模型的编解码器都由Roslyn生成器产生

文件头

偏移大小字段值
04magicuint 0x4F424842,字节为42 48 42 4F(BHBO)
42编解码器代ushort,BlobFormat.Generation = 1
62标志ushort,第0位FlagHashed = 存在哈希。其余各位保留,必须为0
88载荷长度long
168哈希ulong,载荷的xxHash64,以编解码器代作为种子

文件头长度为BlobFormat.HeaderLength = 24字节,载荷紧随其后

检查顺序

BlobFormat.ReadHeader按固定顺序检查文件头。文件头通过之前不分配任何内存:

  1. magic
  2. 编解码器代等于1
  3. 没有设置未知标志
  4. 声明的长度等于实际长度
  5. 若设置了FlagHashed,哈希一致

每种失败都是带有各自消息的BlobFormatException。“文件已损坏”和“文件来自更新的构建”要求玩家做的事不同,所以从不合并为一个错误

哈希有意不采用加密哈希。它负责发现损坏,防伪造是OpenPGP层的职责。更多:归档包与保护

编码

  • 小端序,定宽数字,不用varint
  • 字符串是int字节数加上UTF-8内容
  • null用长度-1表示。因此空列表和缺失的列表在往返之后仍能区分
  • 多态值以一个字节的标签开头,0xFF保留给null。标签是模型的GetModelType(),与JSON在[tag, payload]中写入的判别值相同
  • 从文件读出的数量先经BlobReader.ReadCount检查,然后才为它分配内存

封套和两种代

每个标有[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();

这里有两种不同的代:

  • 文件头中的编解码器代描述字节布局。它一变,所有旧的.blob都整个无法读取:没有可回退的东西
  • 每个封套中的模型代描述一个域的结构。旧的会迁移,新的会以NewerGenerationException拒绝。更多:版本与迁移

两者不能合并成一个数字。模型代位于封套内部,而读取方只有在已经知道字节布局时才能找到封套。模型变化从不改变编解码器代

扩展文件头

文件头没有备用字节,也没有记录自身长度的字段。它有两种扩展方式:

  • 标志。还有15位空闲。旧的读取方遇到不认识的标志会拒绝文件,而不是错误地读取它
  • 新的编解码器代。新布局可以改变任何东西,包括文件头长度。新的读取方可以在支持新代的同时继续读取旧代

载荷之后不能追加任何内容。声明的长度必须等于实际长度,所以任何多余的尾部都会被拒绝。新的关卡数据放进载荷内部,通过模型代来实现

须知

内容没有恰好在声明的长度处结束,无论长了还是短了,都视为损坏。有时一个根通过了所有文件头检查,却仍然解析失败。这时按它的长度跳过它,并保留默认值。这次跳过会记录在SerializationReport中,绝不会悄无声息