开发者
共 10 个一级导航项网络
Croparia IF 没有把网络层写成一套很重的框架,而是在 Architectury 的 NetworkManager 之上包了一层更轻的统一抽象:
NetworkHandlerNetworkHandlerType
这层抽象的目标很直接:
- 让一个包同时携带“类型信息、编解码、方向、处理逻辑”
- 简化跨平台注册流程
- 让发送侧代码尽量保持统一
如果你只是想理解模组里的网络流向,可以先记住一句话:
NetworkHandler表示“一个可发送、可处理的包”NetworkHandlerType表示“这个包的注册信息”
心智模型
Croparia IF 的网络层可以理解成三层:
NetworkHandlerNetworkHandlerTypeNetworkHandlers
它们的分工分别是:
NetworkHandler- 具体包对象
- 同时实现
CustomPacketPayload - 负责提供自己的类型,并实现
handle(...)
NetworkHandlerType- 描述这个包属于
C2S还是S2C - 持有
CustomPacketPayload.Type - 持有
StreamCodec - 在需要时附带
PacketTransformer
- 描述这个包属于
NetworkHandlers- 统一注册入口
- 把所有包类型接进 Architectury 的收发系统
这套结构的关键不是“抽象得多复杂”,而是让每个包都长成同一种样子:
- 一个 record 或对象表示载荷
- 一个
TYPE常量表示注册信息 - 一个
handle(...)表示接收后的处理逻辑
NetworkHandler
NetworkHandler 是 Croparia IF 对单个网络包的最小抽象。
它主要提供三件事:
type()- 默认从
handlerType()里取出真正的 payload type
- 默认从
send()- 按包方向自动选择发往服务端或广播给客户端
handle(NetworkManager.PacketContext context)- 收到包后的业务逻辑入口
这里最值得注意的是:发送逻辑已经被方向约束住了。
- 如果
handlerType().side()是C2Ssend()会尝试发给服务端
- 如果
handlerType().side()是S2Csend()会走广播,或通过send(ServerPlayer)发给指定玩家
这意味着调用方通常不需要再手动区分“这是不是客户端包”,包类型自己就已经声明了方向。
NetworkHandlerType
NetworkHandlerType 是网络包的注册描述对象。
它封装了:
- 包的
Identifier - 收发方向
NetworkManager.Side StreamCodec- 可选的
PacketTransformer
源码里最常见的创建方式有:
NetworkHandlerType.ofC2S(...)NetworkHandlerType.ofS2C(...)
开发时可以把它理解成“这个包的注册卡片”:
NetworkHandler是实际发送的内容NetworkHandlerType是告诉系统“怎么识别、怎么编解码、走哪一边”
ofS2C(...) 额外支持 PacketTransformer,这点在大包同步时很重要,后面的配方同步会用到。
注册流程
统一注册入口在 NetworkHandlers。
它的思路是:
- 对每个
NetworkHandlerType调用一次register(...) - 根据方向选择正确的 Architectury 注册方法
- 对
S2C包额外兼顾“客户端接收”和“服务端声明 payload type”这两种场景
对开发者来说,这一层最重要的价值是:
- 业务包不需要分别写 Fabric / Forge / NeoForge 三套注册逻辑
- 大多数时候只需要关心
TYPE常量和handle(...)
典型 C2S:CropTransmuter
当前源码里最典型的 C2S 用法,是 CropTransmuter 菜单界面。
这里有两个包:
CropTransmuterSelectPacketCropTransmuterRedstoneModePacket
它们都由客户端界面 CropTransmuterScreen 中的按钮或点击操作发出,再由服务端更新对应的方块实体。
选择产物
CropTransmuterSelectPacket 负责把“玩家在界面里选中了第几个候选产物”发给服务端。
它携带的数据只有:
BlockPos posint selectedIndex
服务端处理时会依次校验:
- 发送方是不是
ServerPlayer - 当前打开的容器是不是
CropTransmuterMenu - 菜单绑定的位置是否与包内位置一致
- 该位置上的方块实体是否真的是
CropTransmuterBlockEntity - 当前输入材料是否存在
selectedIndex是否落在合法范围内
最后才调用 transmuter.setSelectedIndex(...)。
这条链很适合作为参考,因为它体现了 Croparia IF 处理 C2S 包时的基本态度:
- 客户端只发最小必要状态
- 服务端永远重新校验上下文
- 不信任客户端传来的界面状态
切换红石模式
CropTransmuterRedstoneModePacket 更简单,只携带目标方块位置。
服务端收到后会:
- 检查当前菜单和位置是否匹配
- 找到对应
CropTransmuterBlockEntity - 调用
toggleRedstoneMode() - 再通过
menu.broadcastChanges()让菜单数据同步回客户端
这说明这条包的职责非常单一:
- 它只表示“用户请求切换模式”
- 真正的状态变化仍由服务端完成
典型 S2C:配方同步
另一条更值得关注的链,是 SyncedRecipeCache 使用的三段式 S2C 配方同步:
S2CSyncRecipeStartS2CSyncRecipeChunkS2CSyncRecipeEnd
它们共同服务于一个目标:
- 把被标记为“需要客户端同步”的配方类型,以快照形式发送到客户端
为什么是三段式
同步不是一次性直接塞一个大包,而是拆成三个阶段:
Start- 告诉客户端“这次同步的
syncId是什么,要同步哪些配方类型”
- 告诉客户端“这次同步的
Chunk- 按类型、按分片逐段发送实际配方数据
End- 告诉客户端“这次同步结束,可以提交成新的快照了”
这样做的好处是:
- 客户端可以显式区分一轮完整同步
- 大量配方不会挤进单个超大包
- 服务端可以按类型与大小分块发送
SplitPacketTransformer
S2CSyncRecipeChunk 的 TYPE 注册时带了 SplitPacketTransformer。
这说明配方同步不仅在逻辑层面做了分片,在底层网络传输层也显式声明了“大包需要拆分”的处理方式。对开发者来说,这是一条很实用的经验:
- 如果一个
S2C包天然会很大,不要只靠“自己少发一点” - 可以像这里一样,在
NetworkHandlerType上附带 transformer
客户端如何落地
客户端端的落地点在 SyncedRecipeCache:
beginClientSync(...)- 创建本轮同步状态
acceptChunk(...)- 暂存每一块配方数据
endClientSync(...)- 把所有分片合并为新的 live snapshot
- 然后触发
CompatRecipeRefresh.onRecipesUpdated(...)
因此这条网络链路的重点不是“客户端立刻处理每个包”,而是:
- 先在客户端拼出一份完整快照
- 再统一对外刷新配方可见状态
handle(...) 里的两个习惯
目前这几类包的 handle(...) 都有两个很稳定的写法。
context.queue(...)
处理逻辑通常包在 context.queue(...) 里执行。
这意味着 Croparia IF 默认把真正的状态修改放回正确线程上下文,而不是在网络回调线程里直接动世界或客户端缓存。
如果你要新增包,通常也应该延续这个习惯。
先校验上下文,再改状态
无论是 CropTransmuter 还是配方同步,处理逻辑都不是“收到就改”。
常见的校验包括:
- 当前玩家是否存在
- 当前打开的菜单是否匹配
- 方块位置是否一致
- 方块实体类型是否一致
- 索引、材料、分片编号是否仍然合法
这也是这套网络层最值得复用的地方之一:
- 包结构尽量小
- 真正的可信判断尽量留在接收端
什么时候该沿用这套写法
下面这些场景很适合直接照着现有网络层扩展:
- 你要做一个小型 GUI 交互,需要把按钮点击或选择结果发回服务端
- 你要同步一组只读快照到客户端,并希望兼容大批量数据
- 你已经在用
StreamCodec,希望包定义与注册尽量保持统一 - 你希望在 Architectury 之上保留一层更贴近模组业务的抽象
如果你的需求只是很局部、一次性的客户端按钮回调,而不会复用,也不一定非得抽象出更多层;但只要它会进入 Croparia IF 的公共 API,沿用 NetworkHandler / NetworkHandlerType 会更稳。
使用建议
- 新包优先写成“一个载荷对象 + 一个
TYPE常量 + 一个handle(...)”的结构,这样最符合现有代码风格。 C2S包只发送最小必要信息,不要把客户端已经能伪造的大量状态直接当真。- 涉及菜单或方块实体时,始终在服务端重新校验当前位置、容器和实体类型。
- 如果
S2C数据量可能很大,优先考虑像配方同步那样分阶段、分片发送。 - 当一个系统最终还是围绕客户端快照工作时,优先在接收端先暂存、再统一提交,而不是每收一包就立即刷新外部状态。