跳到主要内容

🗂️ 数据文件格式

Sparrow Sync 将各类玩家数据分别编码成 NBT,再按类型存入独立的数据块。读取某一类数据时,可以通过索引直接找到它的块。

对接二进制数据时,会遇到两种布局。

数据来源二进制布局内容
导出的 .snapshot 文件、Redis 快照缓存完整二进制快照快照头 + Meta NBT + 独立数据帧
数据库的 data 字段独立数据帧数据帧头 + 索引 NBT + 数据块

数据库 data 保存整个独立数据帧,从数据帧的 format 字节开始。 它与完整快照内部的 dataFrame 使用相同布局,可以按下文的数据帧结构直接读取。

编码约定​

偏移从 0 开始,单位均为字节;布局图中的偏移相对当前快照、数据帧或数据块的起点。多字节整数使用大端序,u8、u16、u32 表示无符号整数,i32 表示有符号整数。

NBT 使用无名根 Tag 编码,即项目中的 NBT.toBytes(tag, false)。Meta 和索引均为未压缩的 compound;类型数据的压缩方式记录在各自的块头中。CRC32 占 4 字节,保留完整的 32 位值。

完整二进制快照​

完整快照依次写入固定的 7 字节头部、M 字节 Meta NBT,以及一个完整的独立数据帧。Meta 段始终存在,数据帧从 7 + M 开始。

01 / BINARY SNAPSHOT完整二进制快照
7 B 固定头部
按字节顺序读取
点击字段查看编码说明示意宽度不代表字节比例
formatu8

格式标记,当前写入 1。

相对偏移 0长度 1 B
总字节数7 + M + dataFrame.length

Meta NBT​

Meta 是一份独立的 compound,字段如下。字段在 compound 内的排列顺序不固定,读取时按键取值。

键NBT 类型内容
idIntArrayTag,4 个整数快照 UUID
playerIntArrayTag,4 个整数玩家 UUID
tsLongTag保存请求接受时刻的毫秒时间戳
causeStringTag保存原因的枚举名,例如 COMMAND
pinnedByteTag是否豁免自动轮转,0b 为否,1b 为是
serverStringTag来源服务器标识
mcDataIntTagMinecraft 数据版本

Meta 的长度和 CRC32 记录在前面的快照头中。Meta 结束后,直接接上数据帧的第一个字节。

独立数据帧​

数据帧保存全部类型数据。前面的索引给出各块位置,后面的块区连续存放各类型的内容。设索引 NBT 长度为 I,块区起点就是数据帧起点加 9 + I。

02 / DATA FRAME独立数据帧
9 B 固定头部
按字节顺序读取
点击字段查看编码说明示意宽度不代表字节比例
formatu8

数据帧的格式标记,当前写入 1。数据库 data 字段从这个字节开始。

相对偏移 0长度 1 B
总字节数9 + I + Σ(13 + P)

索引如何定位数据块​

索引是一个 compound,每个键都是完整的 DataKey,值为 IntTag。下面用 SNBT 展示一份索引的内容。

索引 NBT 的 SNBT 表示
{"demo:first": 0, "demo:second": 33}

这里的 0 和 33 都相对块区起点。第一块从块区起点开始,第二块从其后第 33 字节开始,所以第一块总长 33 字节,其中块头占 13 字节、payload 占 20 字节。

从索引偏移计算实际位置
blockBase = dataFrameStart + 9 + I
blockStart = blockBase + index[dataKey]
payloadStart = blockStart + 13

从完整快照读取时,dataFrameStart = 7 + M;从数据库 data 字节数组读取时,dataFrameStart = 0。

将索引项按偏移排序后,相邻偏移给出前一块的结束位置,最后一块延伸到数据帧末尾。compound 中的键排列顺序不决定块的物理顺序。索引只存偏移,长度和压缩算法在块头中。

DataKey 使用 namespace:value 文本,内置类型的命名空间为 sparrow_sync。帧内没有类型数据时,仍保留空 compound 索引,块区为空。

数据块与 payload​

一个块对应一个 DataKey。每块都有自己的 13 字节头部和 payload,同一个数据帧中的块可以使用不同压缩算法。

03 / DATA BLOCK单个类型的数据块
13 B 固定头部
按字节顺序读取
点击字段查看编码说明示意宽度不代表字节比例
compressorIdu8

当前块的压缩算法编号。内置 0 = NONE、1 = DEFLATE、2 = ZSTD。

相对偏移 0长度 1 B
总字节数13 + P
compressorId算法payload 的内容
0NONE未压缩的 NBT 字节
1DEFLATE带 zlib 包装的 DEFLATE 压缩字节,对应 Java DeflaterOutputStream
2ZSTDZstandard 压缩字节

读取时按当前块的 compressorId 处理 payload,得到 rawLength 字节的 NBT。CRC32 覆盖存储的 payload;对于压缩块,应在解压前对这些字节计算 CRC。

类型值在 NBT 中的位置​

假设插件为 demo:first 提供的 Tag 是 {count: 3},块编码器会加上完整 DataKey。解压后的内容如下,示例以 SNBT 表示。

payload 还原后的 NBT
{
"demo:first": {
count: 3
}
}

根 compound 只包含当前类型这一个键。rawLength 计算整份 compound 的二进制长度,包括外层 demo:first;插件的 PlayerDataType.encode 提供的值、decode 收到的值都是内层 {count: 3}。

因此,提取类型内容的过程是先从索引定位块,读取块头并还原 payload,再从根 compound 中按完整 DataKey 取出类型 Tag。类型 Tag 的内部结构由对应插件定义。