跳转至

原文:MinecraftForge/Documentation · 目标版本:Forge 1.20.1

SimpleImpl(简单实现)

SimpleImpl 是围绕 SimpleChannel 类构建的数据包(Packet)系统的名称。使用这套系统是与客户端和服务器之间发送自定义数据最简单的方式。

入门(Getting Started)

首先需要创建你的 SimpleChannel 对象。我们建议在单独的类中创建它,比如一个名为 ModidPacketHandler 的类。将 SimpleChannel 作为该类的静态字段创建,如下所示:

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

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

版本检查器(The Version Checker)

如果你的模组不要求对端必须存在某个网络通道,甚至不要求对端是 Forge 实例,你就应当妥善定义你的版本兼容性检查器(即 Predicate<String> 参数),以处理版本检查器可能接收到的额外"元版本"(定义在 NetworkRegistry 中)。这些元版本包括:

  • ABSENT —— 该通道在对端缺失。注意,这种情况下对端仍然是 Forge 端点,可能装有其他模组。
  • ACCEPTVANILLA —— 对端是原版(vanilla,即非 Forge)端点。

两者都返回 false 意味着该通道必须存在于对端。如果直接照抄上面的代码,行为就是这样。注意,这些值也用于服务器列表 ping 的兼容性检查,该检查负责在多人游戏服务器选择界面中显示绿色对勾 / 红色叉号。

注册数据包(Registering Packets)

接下来,我们必须声明想要发送和接收的消息类型。这通过 INSTANCE#registerMessage 完成,它接受 5 个参数:

  • 第一个参数是数据包的判别符(discriminator)。这是该通道内唯一的数据包 ID。我们建议使用局部变量保存 ID,然后用 id++ 调用 registerMessage。这样可以保证 ID 100% 唯一。
  • 第二个参数是实际的数据包类 MSG
  • 第三个参数是一个 BiConsumer<MSG, FriendlyByteBuf>,负责将消息编码进传入的 FriendlyByteBuf
  • 第四个参数是一个 Function<FriendlyByteBuf, MSG>,负责从传入的 FriendlyByteBuf 解码消息。
  • 最后一个参数是一个 BiConsumer<MSG, Supplier<NetworkEvent.Context>>,负责处理消息本身。

最后三个参数可以是 Java 中静态方法或实例方法的方法引用。注意,实例方法 MSG#encode(FriendlyByteBuf) 依然满足 BiConsumer<MSG, FriendlyByteBuf>MSG 只是变成了隐式的第一个参数。

处理数据包(Handling Packets)

数据包处理器中有几点需要强调。处理器可以同时访问消息对象和网络上下文(context)。上下文允许访问发送该数据包的玩家(如果在服务器端),并提供一种排队执行线程安全工作(thread-safe work)的方式。

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

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

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

// 在 ClientPacketHandlerClass 中
public static void handlePacket(MyClientMessage msg, Supplier<NetworkEvent.Context> ctx) {
  // 做一些事情
}

注意 #setPacketHandled 的存在,它用于告诉网络系统该数据包已成功完成处理。

Warning

自 Minecraft 1.8 起,数据包默认在网络线程上处理。

这意味着你的处理器_不能_直接与大多数游戏对象交互。Forge 提供了一种便捷方式,通过传入的 NetworkEvent$Context 让你的代码改在主线程执行。只需调用 NetworkEvent$Context#enqueueWork(Runnable),它会在下一次机会到来时在主线程上调用给定的 Runnable

Warning

在服务器端处理数据包时要保持防御心态。客户端可能通过发送意外数据来利用(exploit)数据包处理。

一个常见的问题是容易受到任意区块生成(arbitrary chunk generation)的攻击。这通常发生在服务器信任客户端发送的方块位置来访问方块(Block)和方块实体(BlockEntity)时。当访问世界中未加载区域的方块和方块实体时,服务器会生成该区域或从磁盘加载它,然后很快又写回磁盘。这可以被利用来对服务器的性能和存储空间造成灾难性损害,且不留痕迹。

为避免这个问题,一条通用的经验法则是:仅当 Level#hasChunkAt 为 true 时才访问方块和方块实体。

发送数据包(Sending Packets)

发送到服务器

只有一种方式可以向服务器发送数据包,因为客户端同时只能连接一个服务器。为此,我们必须再次使用之前定义的 SimpleChannel。直接调用 INSTANCE.sendToServer(new MyMessage()) 即可。消息会被发送到其类型的处理器(如果存在的话)。

Note

在 1.20.1 中,sendToServer 等价于 INSTANCE.send(PacketDistributor.SERVER.noArg(), new MyMessage())(向服务器发包)。

发送到客户端

数据包可以直接发送给客户端,方式是在 SimpleChannel 上调用 HANDLER.sendTo(new MyClientMessage(), serverPlayer.connection.getConnection(), NetworkDirection.PLAY_TO_CLIENT)。然而,这相当不便。Forge 提供了一些便捷函数可供使用:

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

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

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

还有更多 PacketDistributor 类型可用;更多细节请参阅 PacketDistributor 类的文档。

待复核清单

  • 已统一修正:1.20.1(Mojang 官方映射)应使用 new ResourceLocation("mymodid", "main"),而非 1.21 的静态工厂写法 ResourceLocation.fromNamespaceAndPath(...)
  • 原文「发送到客户端」一节提到的 HANDLER.sendTo(...)SimpleChannel#sendTo + NetworkDirection.PLAY_TO_CLIENT)是 1.12/1.16 时代的旧 API,1.20.1 的 SimpleChannel 已无 sendTo 方法;译文按校准要点保留原文叙述并标注改用 PacketDistributor,请主会话复核。
  • 1.20.1 中 NetworkRegistry 位于 net.minecraftforge.network 包,newSimpleChannel 四个参数(名称、协议版本 Supplier、两个版本匹配 Predicate)与原文一致,无需改动。
  • 已按校准补充说明:1.20.1 不使用 1.20.2+ 的 RegisterPayloadHandlersEvent / PayloadRegistrar;本页内容(SimpleChannel)即 1.20.1 的正解。
  • 客户端向服务器发包:原文的 sendToServer 在 1.20.1 仍可用(内部即 PacketDistributor.SERVER.noArg()),已在正文以 note 注明。