SimpleImpl

SimpleImpl

SimpleImpl 是围绕 SimpleChannel 类构建的数据包系统的名称,是在客户端和服务端之间发送自定义数据最简单的方式。

消息经编码进入 FriendlyByteBuf,接收端解码并在网络线程处理,游戏逻辑由 enqueueWork 调度到主线程。

原创说明图,依据 Forge 1.21.x 官方文档 绘制。

入门

首先创建 SimpleChannel 对象。建议放在独立类中,例如 ModidPacketHandler,并将通道创建为静态字段:

private static final String PROTOCOL_VERSION = "1";
public static final SimpleChannel INSTANCE = NetworkRegistry.newSimpleChannel(
  ResourceLocation.fromNamespaceAndPath("mymodid", "main"),
  () -> PROTOCOL_VERSION,
  PROTOCOL_VERSION::equals,
  PROTOCOL_VERSION::equals
);

第一个参数是通道名称;第二个是返回当前网络协议版本的 Supplier<String>;第三和第四个分别是 Predicate<String>,用于检查传入连接的协议版本是否与客户端或服务端网络兼容。此处直接与 PROTOCOL_VERSION 比较,意味着客户端和服务端的版本必须一致,否则 FML 拒绝登录。

版本检查器

如果 Mod 不要求另一端拥有特定网络通道,甚至不要求另一端为 Forge,必须正确编写版本兼容检查器(Predicate<String> 参数),处理检查器可能收到的额外“元版本”(定义在 NetworkRegistry 中):

  • ABSENT:另一端缺少此通道。此时另一端仍是 Forge,可能有其他 Mod。
  • ACCEPTVANILLA:另一端是原版或非 Forge 端。

对两者均返回 false 意味着另一端必须存在此通道;直接复制上面的代码就是如此。这些值也用于服务器列表 ping 兼容性检查,决定多人服务器选择界面中的绿色勾号或红叉。

注册数据包

接下来声明要发送和接收的消息类型。使用 INSTANCE#registerMessage,它接收五个参数:

  1. 数据包判别符,即通道内唯一 ID。建议用局部变量保存 ID,每次调用时使用 id++,以保证 ID 唯一。
  2. 数据包类 MSG。
  3. BiConsumer<MSG, FriendlyByteBuf>,负责将消息编码到提供的 FriendlyByteBuf。
  4. Function<FriendlyByteBuf, MSG>,负责从缓冲区解码消息。
  5. BiConsumer<MSG, Supplier<NetworkEvent.Context>>,负责处理消息本身。

最后三个参数可以是 Java 静态方法或实例方法的方法引用。实例方法 MSG#encode(FriendlyByteBuf) 仍满足 BiConsumer<MSG, FriendlyByteBuf>,因为 MSG 成为隐式的第一个参数。

处理数据包

数据包处理器可以访问消息对象和网络上下文。上下文允许访问发送数据包的玩家(在服务端时),并提供将线程安全任务加入队列的方法。

public static void handle(MyMessage msg, Supplier<NetworkEvent.Context> ctx) {
  ctx.get().enqueueWork(() -> {
    // 需要线程安全的工作(大多数工作)
    ServerPlayer sender = ctx.get().getSender(); // 发送此数据包的客户端
    // 处理逻辑
  });
  ctx.get().setPacketHandled(true);
}

服务端发送到客户端的数据包应在另一个类中处理,并通过 DistExecutor#unsafeRunWhenOn 包装:

// 在 Packet 类中
public static void handle(MyClientMessage msg, Supplier<NetworkEvent.Context> ctx) {
  ctx.get().enqueueWork(() ->
    // 确保只在物理客户端执行
    DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> ClientPacketHandlerClass.handlePacket(msg, ctx))
  );
  ctx.get().setPacketHandled(true);
}

// 在 ClientPacketHandlerClass 中
public static void handlePacket(MyClientMessage msg, Supplier<NetworkEvent.Context> ctx) {
  // 处理逻辑
}

注意 #setPacketHandled 用于告知网络系统数据包已成功处理。

警告:从 Minecraft 1.8 开始,数据包默认在网络线程处理,因此处理器不能直接操作大多数游戏对象。Forge 通过提供的 NetworkEvent$Context 提供在主线程执行代码的便捷方式:调用 NetworkEvent$Context#enqueueWork(Runnable),它会在下一次适当时机在主线程运行给定的 Runnable。

警告:在服务端处理数据包时应采取防御措施,客户端可能发送意外数据以利用处理逻辑。

常见问题是任意区块生成漏洞,通常发生在服务器信任客户端发送的方块坐标并据此访问方块和方块实体时。访问未加载区域会使服务器生成该区域或从磁盘加载,随后又写入磁盘。这可能被利用,严重损害服务器性能并耗尽存储空间,而且不留痕迹。一般原则是仅在 Level#hasChunkAt 为 true 时访问方块和方块实体。

发送数据包

发送到服务端

客户端一次只连接一个服务器,因此只有一种发送方式。使用之前定义的 SimpleChannel,调用 INSTANCE.sendToServer(new MyMessage());如果该类型有处理器,消息将发送给它。

发送到客户端

可以直接通过 SimpleChannel 发送:HANDLER.sendTo(new MyClientMessage(), serverPlayer.connection.getConnection(), NetworkDirection.PLAY_TO_CLIENT)。不过这可能不方便,Forge 提供了便捷方法:

// 发送给一位玩家
INSTANCE.send(PacketDistributor.PLAYER.with(serverPlayer), new MyMessage());

// 发送给追踪此世界区块的所有玩家
INSTANCE.send(PacketDistributor.TRACKING_CHUNK.with(levelChunk), new MyMessage());

// 发送给所有已连接玩家
INSTANCE.send(PacketDistributor.ALL.noArg(), new MyMessage());

还有其他 PacketDistributor 类型,详见该类文档。

原文:SimpleImpl。