处理器注册表

基本信息

属性 值
类型 静态服务定位器(封包序列化扩展点)
实现类 com.gtnewhorizons.foundation.HandlerRegistry
数据载体 com.gtnewhorizons.foundation.BlockPacketInfo
对外 API 包 com.gtnewhorizons.foundation.api(Gradle apiPackage = api)
注册入口 Foundation.preInit(FMLPreInitializationEvent)
注册阶段 FML PreInit

行为

HandlerRegistry 持有两个静态列表与一个静态字节计数,全部在 Mixin 清单 列出的 mixin 里被回调。它是 Foundation 真正对外的「扩展点」:其他 mod 实现 api 包里的接口并调用 registerChunkPacketHandler / registerBlockPacketHandler, 其数据就会自动被编进 S21 / S22 / S23 封包,无需自己写 mixin。

静态状态

字段 类型 初值
chunkPacketHandlers List<ChunkPacketHandler> 空 ArrayList
blockPacketHandlers List<BlockPacketHandler> 空 ArrayList
chunkPacketBytes int 0

注册方法

方法 行为
registerChunkPacketHandler(ChunkPacketHandler handler) 先把 handler.maxBytesPerChunk() 累加进 chunkPacketBytes,再 add 进列表
registerBlockPacketHandler(BlockPacketHandler handler) 仅 add,不计入任何字节预算

静态回调(供 mixin 调用)

方法 调用方 mixin 行为
writeS23Packets(S23PacketBlockChange, PacketBuffer) MixinS23PacketBlockChange 强转成 IMixinS23PacketBlockChange → createBlockPacketInfo() → 遍历 blockPacketHandlers 调 writeBlockPacket(info, data) → syncBlockPacketInfo(info) 把结果写回被 shadow 的字段
readS23Packets(S23PacketBlockChange, PacketBuffer) MixinS23PacketBlockChange 同上,改为 readBlockPacket
writeS22Packets(BlockPacketInfo, PacketBuffer) MixinS22PacketMultiBlockChange 遍历 blockPacketHandlers 调 writeBlockPacket
readS22Packets(BlockPacketInfo, PacketBuffer) MixinS22PacketMultiBlockChange 遍历 blockPacketHandlers 调 readBlockPacket
writeChunkPackets(Chunk, boolean, int, byte[]) MixinS21PacketChunkData 见下
readChunkPackets(Chunk, boolean, int, byte[]) client/MixinChunk 见下
getChunkPacketBytes() MixinS21PacketChunkData 返回 chunkPacketBytes

注意 S23 的读写都先建 BlockPacketInfo 再回写(syncBlockPacketInfo), handler 在读路径上拿到的是已经填好 x/y/z、block/metadata 仍为默认的 info, 因此 handler 必须负责把 block 与 metadata 写回去;S22 的读写则直接用 mixin 自己的 blockPackets 数组元素。

chunk 封包的「长度前缀分片」协议

writeChunkPackets 的核心是给每个 handler 分配一个定长切片,并在切片前 4 字节回填 真实写入长度:

  1. buffer.putInt(chunkPacketHandlers.size()) —— 先写 handler 数量。
  2. 对每个 handler:sliceStart = position + 4(给长度前缀留位)→ 临时把 limit 设为 sliceStart + handler.maxBytesPerChunk() → buffer.slice() 得到该 handler 的独立 ByteBuffer → 恢复原 limit/position → 调用 handler.writeChunkPacket(chunk, sendUpdates, flagSubChunks, slice) → 取 slice.position() 作为 sliceLength → buffer.putInt(sliceLength) → position(sliceStart + sliceLength)。
  3. 返回 buffer.position() 作为整个缓冲区的最终大小,交给 S21PacketChunkData 做 inflate/deflate(源码注释即说明这一点)。

源码注释解释了为什么不能直接复用 ByteBuffer:ByteBuffer.slice(int, int) 需要 Java 13,而本项目目标是 JVM 8。

readChunkPackets 是完全对称的逆过程:先读 count,然后对 i 从 0 到 count-1 按注册顺序索引 chunkPacketHandlers.get(i),读该分片的 sliceLength,切出 ByteBuffer 交给 handler.readChunkPacket。

关键约束

注册顺序必须在客户端与服务端完全一致

readChunkPackets 的索引来源是网络上的 count 与顺序,不是某个 handler 的标识符。 这意味着:

  • 客户端与服务端注册 handler 的顺序与数量必须逐项一致,否则数据被错配到别的 handler 上解析(不报错,但区块数据损坏)。
  • 只在单侧注册 handler 会直接导致协议错位。

handler == null 分支实际不可达

readChunkPackets 里的防御检查:

ChunkPacketHandler handler = chunkPacketHandlers.get(i);
if (handler == null) {
    Foundation.LOG.error("Received unregistered chunk packet data");
    continue;
}

chunkPacketHandlers 是 ArrayList,元素只会是 registerChunkPacketHandler 传入的非 null 处理器;而当线上 count 大于本地列表长度时,get(i) 抛的是 IndexOutOfBoundsException 而非返回 null。因此该 error 日志分支在当前实现下 不会触发,实际故障会以未捕获异常形式出现。

BlockPacketInfo 位于根包而非 api 包

api.BlockPacketHandler 的方法签名使用 com.gtnewhorizons.foundation.BlockPacketInfo, 而该类位于根包 com.gtnewhorizons.foundation(BlockPacketInfo.java),不在 com.gtnewhorizons.foundation.api 内。实现该接口的外部 mod 必须 import 根包。

数值

数值 值
ChunkPacketHandler 接口方法数 3(maxBytesPerChunk、writeChunkPacket、readChunkPacket)
BlockPacketHandler 接口方法数 2(writeBlockPacket、readBlockPacket)
线上分片协议固定开销 4 字节(handler 数量)+ 每 handler 4 字节(分片长度)
使用内置 6 个 chunk handler 时的协议开销 4 + 4 × 6 = 28 字节
chunkPacketBytes 累加时机 registerChunkPacketHandler 内,注册即累加
BlockPacketInfo 可变字段 5(x、y、z、Block block、int metadata)
BlockPacketInfo 构造函数 2(3 参版把 block 置 null、metadata 置 0)

相关条目