序列化流水线
基本信息
| 属性 | 值 |
|---|---|
| 分发中心 | com.falsepattern.chunk.internal.DataRegistryImpl(16 个 public 静态分发方法) |
| 挂载点 | 全部靠 core mod 的 Mixin,无 Forge 事件 |
| 字节序 | ByteBuffer.order(ByteOrder.LITTLE_ENDIAN)(两条 chunk 路径都显式设置) |
⚠️ 本 mod 没有"生成 / 人口生成"流水线——见分类页的"本 mod 没有的东西"。实际存在的是区块数据序列化流水线,共 5 条独立路径。每条路径都是 core mod 用 @Inject / @Overwrite / @Redirect 把 DataRegistryImpl 的某个方法塞进原版调用点。
路径一:存盘(服务端)
AnvilChunkLoaderMixin 对 writeChunkToNBT 做 @Inject(at = @At("HEAD"), cancellable = true, require = 1),注释说明为何不 @Overwrite:“Inject-cancel instead of overwrite for compat with Metaworlds-Mixins”。
顺序:
| 步 | 动作 | 落点 |
|---|---|---|
| 1 | 写 V=1、xPos、zPos、LastUpdate、TerrainPopulated、InhabitedTime |
本体 |
| 2 | writeSubChunks → nbt.setTag("Sections", subChunksNBT) |
见下 |
| 3 | writeCustomData → DataRegistryImpl.writeChunkToNBT(chunk, nbt) |
chunkNBTManagers 逐个 |
| 4 | writeEntities → Entities、TileEntities、TileTicks |
本体 |
| 5 | ci.cancel()(原版方法体不执行) |
writeSubChunks 内部对每个非 null 的 ExtendedBlockStorage:建 NBTTagCompound,写 "Y" = subChunk.getYLocation() >> 4 & 255,再调 DataRegistryImpl.writeSubChunkToNBT(chunk, subChunk, subChunkNBT),最后 append。
⚠️ writeSubChunks 不检查 subChunk 是否为空(原版会跳过 isEmpty() 的段,ChunkAPI 的版本一律写入)。
DataRegistryImpl.writeSubChunkToNBT / writeChunkToNBT 都是同一形状的无条件遍历:
for (val manager : <有序集合>.values())
manager.writeXxxToNBT(..., createManagerNBT(manager.xxxPrivilegedAccess(), nbt, manager));
即:manager 按 ordering 升序依次调用;特权 manager 拿到原始 NBT 根,非特权拿到 domain:id 子标签。
writeEntities 内部细节:逐 chunk.entityLists[i] 调 entity.writeToNBTOptional(entityNBT),异常时 FMLLog.log(Level.ERROR, e, "An Entity type %s has thrown an exception trying to write state. It will not persist. Report this to the mod author", ...) 并继续;TileEntity 同理,消息是 "...has throw an exception..."(原文拼写错误 has throw)。TileTicks 用 update.func_151351_a() 取方块、update.scheduledTime - world.getTotalWorldTime() 算剩余延迟。
路径二:读档
readChunkFromNBT 的 @Inject(at = @At("HEAD")),返回类型用 CallbackInfoReturnable<Chunk>:
| 步 | 动作 |
|---|---|
| 1 | 从 NBT 读 xPos/zPos,new Chunk(world, x, z) |
| 2 | 恢复 isTerrainPopulated、inhabitedTime |
| 3 | readSubChunks → DataRegistryImpl.readSubChunkFromNBT(chunk, subChunk, subChunkNBT),然后 subChunk.removeInvalidBlocks() |
| 4 | readCustomData → DataRegistryImpl.readChunkFromNBT(chunk, nbt) |
| 5 | chunk.setStorageArrays(subChunkList) |
| 6 | cir.setReturnValue(chunk) |
注释:“End this method here and split off entity loading to another method” —— 实体的加载被完全剥离(原版在 readChunkFromNBT 末尾加载实体,此处不加载)。
readSubChunks 用 byte segments = 16 固定 16 段,new ExtendedBlockStorage(yLevel << 4, !chunk.worldObj.provider.hasNoSky),按 subChunkList[yLevel] = subChunk 定位。
⚠️ readSubChunks 只遍历 NBT 里实际存在的段,不存在的 yLevel 位置保持 null;而写侧会跳过 null 段——若某 subChunk 被写为空但 NBT 有条目,读侧仍会分配。
读侧特权判定用的是 getManagerNBT(privileged, root, manager):不存在键时返回一个新的空 NBTTagCompound 而不是 null。所以 readChunkFromNBT 收到的 nbt 参数实际永不为 null,源码注释里 “The NBT may be null” 的警告偏保守。
路径三:区块数据包 S→C(S21PacketChunkData)
发送端
| 步 | 位置 | 动作 |
|---|---|---|
| 1 | S21PacketChunkDataMixin.func_149275_c() @Overwrite |
直接 return DataRegistryImpl.maxPacketSize(); —— 原版返回固定 12288,这里换成按注册表累加的动态值 |
| 2 | func_149269_a(Chunk, boolean, int) @Overwrite |
扫描 subChunks,条件为 subChunks[i] != null && (!forceUpdate || !subChunks[i].isEmpty()) && (subChunkMask & 1 << i) != 0,累出 extracted.field_150280_b |
| 3 | 同上 | LockHelper.bufferLockS21PacketChunkData.tryLock()(失败则 Thread.yield() 自旋),按需扩容静态 buffer 数组 |
| 4 | 同上 | DataRegistryImpl.writeToBuffer(chunk, extracted.field_150280_b, forceUpdate, buffer) → 拷进 extracted.field_150282_a |
| 5 | 同上 | finally 释放锁 |
| 6 | writePacketData(PacketBuffer) @Overwrite |
deflateGate.acquireUninterruptibly() → 懒 deflate() → 写 xPosition/zPosition/forceUpdate/subChunkMask & 0xFFFF/data.length/deflatedSize/压缩体 |
writeToBuffer 的格式(按 ordering 升序,manager 之间用长度前缀切段):
putInt(managerCount)
for each manager (ordering 升序):
putInt(id 的 UTF-8 字节长度) + id 字节
putInt(该段实际长度) ← 回填
manager.writeToBuffer(chunk, subChunkMask, forceUpdate, slice)
实现手法是先预留 4 字节长度槽(int start = buf.position() + 4),用 createSlice 把 buffer 切成受限视图喂给 manager,再把 slice.position() 当作实际长度回填,最后跳到 start + length。
接收端(ChunkMixin,客户端专用)
ChunkMixin.fillChunk(byte[] data, int subChunkMask, int subChunkMSBMask, boolean forceUpdate) @SideOnly(Side.CLIENT) + @Overwrite。顺序:
| 步 | 动作 |
|---|---|
| 1 | 遍历 chunkTileEntityMap,每个 TE updateContainingBlockInfo() + 读 metadata 与 block |
| 2 | boolean hasSky = !this.worldObj.provider.hasNoSky,按 subChunkMask 补建 new ExtendedBlockStorage(i << 4, hasSky) |
| 3 | DataRegistryImpl.readFromBuffer((Chunk) (Object) this, subChunkMask, forceUpdate, data) |
| 4 | 对 mask 命中的段 removeInvalidBlocks() |
| 5 | isLightPopulated = true; isTerrainPopulated = true; |
| 6 | this.generateHeightMap() |
| 7 | 校验 TE 是否失效,收集进 invalidList 后统一 te.invalidate() |
⚠️ 步骤 3 传入的是 byte[] data,与 DataRegistryImpl.readFromBuffer(Chunk, int, boolean, byte[]) 的 byte[] 重载匹配——这是另一个重载,ChunkMixin 直接调内部实现而非 API。
⚠️ fillChunk 的 subChunkMSBMask 形参被完全忽略——MSB 信息由 ChunkAPI 自己的 BlockIDManager 头里那份 msbMask 承载(见 内置原版数据管理器)。
多方区块包:S26PacketMapChunkBulkMixin 有 vanilla 与 thermos 两份(见 Mixin 清单),都把解压缓冲按 new byte[S21PacketChunkData.func_149275_c() * chunkCount] 一次性申请,并各用一把 LockHelper 里的独立锁。
路径四:单方块变更 S→C(S23PacketBlockChange)
| 端 | 机制 |
|---|---|
| 服务端构造(世界给定) | S23PacketBlockChangeMixin 对 <init>(IIILnet/minecraft/world/World;)V 做 @Inject(at = @At("RETURN"), require = 1) → DataRegistryImpl.writeBlockToPacket(chunk, x & 0xf, y, z & 0xf, this) |
| 服务端构造(区块给定) | chunkapi$init(x, y, z, chunk) —— 先 chunk.getBlock/getBlockMetadata 填 field_148883_d/field_148884_e,再调同一个 writeBlockToPacket |
| 客户端收包 | NetHandlerPlayClientMixin.doHandleBlockChange 对 handleBlockChange 做 @Inject(at = @At(value = "RETURN"), require = 1) → DataRegistryImpl.readBlockFromPacket(...) |
| 序列化 | S23PacketBlockChangeMixin.readPacketData/writePacketData @Overwrite:先 data.writeLong(BlockPosUtil.packToLong(xCoord, yCoord, zCoord)),再调 DataRegistryImpl.read/writeBlockPacketToBuffer(this, data) |
writeBlockPacketToBuffer 的格式:
writeInt(blockPacketManagers.size())
for each manager (ordering 升序):
writeStringToBuffer(ord.id)
manager.writeBlockPacketToBuffer(packet, buffer)
⚠️ 与 chunk 路径不同,这里没有长度前缀——接收侧靠 blockPacketManagers.get(id) 精确找到对应 manager,找不到就是 NPE(见 数据注册表)。因此单方块包对客户端与服务端的 manager 集合一致性要求是硬性的,不能像 chunk 包那样降级跳过。
BlockPosUtil(internal 包)负责坐标打包:NUM_X_BITS = 26、NUM_Z_BITS = 26、NUM_Y_BITS = 12(64 - 26 - 26),提供 packToLong(int,int,int) 与 getX/getY/getZ。Y 只有 12 位(±2048),对 1.7.10 的高度范围够用。
路径五:多方块变更 S→C(S22PacketMultiBlockChange)
源码注释直言:“S22PacketMultiBlockChange is an absolute, utter pain in the ass to properly integrate with.”
服务端:PlayerInstanceMixin 对 sendChunkUpdate 做 @Redirect(at = @At(value = "NEW", target = "(I[SLnet/minecraft/world/chunk/Chunk;)Lnet/minecraft/network/play/server/S22PacketMultiBlockChange;")),把 new 出来的包劫持成手动构造:
val packet = new S22PacketMultiBlockChange();
((CustomPacketMultiBlockChange) packet).chunkapi$init(count, positions, chunk);
⚠️ 这段用到了 PlayerManager$PlayerInstance 的 AT 公开化(见 Access Transformer)。
禁止原版构造器:S22PacketMultiBlockChangeMixin 对 <init>(I[SLnet/minecraft/world/chunk/Chunk;)V 注入并 throw new IllegalStateException("S22PacketMultiBlockChange constructor is not supported by ChunkAPI. Please report this to FalsePattern!")。注入点是 @At(value = "FIELD", target = "Lnet/minecraft/network/play/server/S22PacketMultiBlockChange;field_148925_b:Lnet/minecraft/world/ChunkCoordIntPair;", unsafe = true)。
结构改造:chunkapi$init 把原版"一个包 + short[] 压缩坐标"改成一个包 + S23PacketBlockChange[] 子包数组:
coord = new ChunkCoordIntPair(chunk.xPosition, chunk.zPos)
subPackets = new S23PacketBlockChange[count]
for i in 0..count:
x = crammedPositions[i] >> 12 & 0xf
z = crammedPositions[i] >> 8 & 0xf
y = crammedPositions[i] & 0xff
subPackets[i] = new S23PacketBlockChange() 然后 chunkapi$init(x, y, z, chunk)
客户端:NetHandlerPlayClientMixin.handleMultiBlockChange @Overwrite,遍历 chunkapi$subPackets(),对每个子包调 clientWorldController.func_147492_c(x + bX, y, z + bZ, subPacket.func_148880_c(), subPacket.func_148881_g()) 落地方块,再调 DataRegistryImpl.readBlockFromPacket(chunk, x, y, z, subPacket)。
⚠️ 此处 bX = cX * 16、bZ = cZ * 16(区块坐标乘 16),但子包的 x/z 已是区块内坐标——而 chunk 是用 getChunkFromChunkCoords(cX, cZ) 取的。写侧 PlayerInstanceMixin 传的 positions 若已是区块内坐标则自洽,若仍是原版的区块内编码也自洽(>> 12 & 0xf / >> 8 & 0xf 正是区块内坐标编码)。
路径六:level.dat(存档元数据)
ChunkAPICoreModContainer 实现 WorldAccessContainer:
| 方法 | 动作 |
|---|---|
getDataForWriting(SaveHandler, WorldInfo) |
新建 NBTTagCompound → DataRegistryImpl.writeLevelDat(tag) → 返回 |
readData(SaveHandler, WorldInfo, Map, NBTTagCompound) |
DataRegistryImpl.readLevelDat(tag) |
writeLevelDat 写入的结构:
"version" = Tags.MOD_VERSION
"managers" = { "<domain:id>" : { "version": ..., "uninstallMessage": ... } , ... }
⚠️ 跳过所有以 "minecraft:" 开头的 manager——即内建 6 个 manager 不进 level.dat,只有第三方 manager 被登记。这正是"内建 manager 与原版完全兼容"的体现。
读侧 readLevelDat 是 读档兼容闸门的主体,详见该条目。
内部锁
internal.mixin.helpers.LockHelper 定义两把 ReentrantLock,全部 static final:
| 锁 | 保护对象 |
|---|---|
bufferLockS21PacketChunkData |
S21PacketChunkData 的静态 buffer 数组 |
bufferLockS26PacketMapChunkBulk |
S26PacketMapChunkBulk 的 inflaterBuffer 实例字段 |
获取方式都是 while (!lock.tryLock()) { Thread.yield(); } —— 自旋而非阻塞,且都配 try/finally 释放。
相关条目
- API 接口 — 流水线各步调用的接口方法签名
- 内置原版数据管理器 — 每条路径上实际写字段的 6 个 manager
- 数据注册表 — 分发方法的集合来源与特权 NBT 命名空间
- Mixin 清单 — 六条路径对应的 10 个 mixin 类
- 核心 mod 与读档闸门 — level.dat 兼容检查与自动备份