序列化流水线

基本信息

属性 值
分发中心 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 释放。

相关条目