处理器注册表
基本信息
| 属性 | 值 |
|---|---|
| 类型 | 静态服务定位器(封包序列化扩展点) |
| 实现类 | 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 字节回填
真实写入长度:
buffer.putInt(chunkPacketHandlers.size())—— 先写 handler 数量。- 对每个 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)。 - 返回
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) |