Skip to content

自定义发包 ​

服务端的数据变更,最终都是通过**数据包(Packet)**发给客户端,客户端收到后更新画面。这是 NMS 最常见的使用场景。

为什么不能直接赋值 ​

有些数据改了不会自动同步给客户端。比如改玩家经验值,直接改 NMS 字段没用——必须经过 DataWatcher 判断后发包,客户端才会更新经验条。

翻 DataWatcher 的混淆代码,能找到类似这样的方法:

java
public void refresh(EntityPlayer to) {
    if (!this.d()) {
        List<b<?>> list = this.packAll();
        if (list != null && to.getBukkitEntity().canSee(this.d.getBukkitEntity())) {
            to.c.a(new PacketPlayOutEntityMetadata(this.d.af(), list));
        }
    }
}

它把 PacketPlayOutEntityMetadata 包发了出去——这就是同步经验值的方式。

数据包从哪查 ​

每个数据包对应协议里的一个条目。查 Minecraft Wiki 协议页,按功能找:

  • EntityMetadata → 更新实体元数据(经验值属于玩家特有元数据)
  • Title → 发送 Title 文字
  • Chat → 发聊天消息

找到后,NMS 里对应类名一般是 PacketPlayOut<Xxx>:

协议条目NMS 类
EntityMetadataPacketPlayOutEntityMetadata
TitlePacketPlayOutTitle
Entity DestroyPacketPlayOutEntityDestroy

怎么构造并发包 ​

以发 Title 为例(1.8~1.11 期间 Bukkit 还没 Title API,只能这么干):

  1. 查对应版本协议,找 Title 数据包的参数
  2. 看 NMS 里 PacketPlayOutTitle 的构造函数,对比 Wiki 的 Action 描述猜参数
  3. 选参数最多的构造函数(自定义度最高)
  4. 构造包,通过 playerConnection.sendPacket 发出
java
public void sendTitle(Player player, String title, String subtitle,
                      int fadeIn, int stay, int fadeOut) {
    // 文本基础组件(MC 的文本组件)
    IChatBaseComponent titleText = IChatBaseComponent.ChatSerializer.a("{\"text\": \"" + title + "\"}");
    IChatBaseComponent subText = IChatBaseComponent.ChatSerializer.a("{\"text\": \"" + subtitle + "\"}");

    // 主标题包
    PacketPlayOutTitle titlePacket = new PacketPlayOutTitle(
        PacketPlayOutTitle.EnumTitleAction.TITLE, titleText, fadeIn, stay, fadeOut);
    // 副标题包
    PacketPlayOutTitle subPacket = new PacketPlayOutTitle(
        PacketPlayOutTitle.EnumTitleAction.SUBTITLE, subText, fadeIn, stay, fadeOut);

    // 从 Player 拿到 EntityPlayer 的 playerConnection,发包
    ((CraftPlayer) player).getHandle().playerConnection.sendPacket(titlePacket);
    ((CraftPlayer) player).getHandle().playerConnection.sendPacket(subPacket);
}

主副标题要发两个包

一个 TITLE action 的包发主标题,一个 SUBTITLE action 的包发副标题,两个都要发。

版本不兼容的痛 ​

翻各版本 NMS 会发现:每个版本的包参数名、类位置、混淆名都不一样。直接 import 写死,换个版本就 ClassNotFoundException 或 NoSuchMethodError。

要做多版本兼容,得:

  • 用反射按 version 分支
  • 或用 Java 接口抽象,各版本各写一个实现

这就是为什么不推荐 NMS

发个 Title 在 1.12.2 是 PacketPlayOutTitle,到高版本可能类名/参数全变。维护多版本兼容是噩梦。

用 ProtocolLib 解放自己 ​

既然 NMS 发包兼容性这么差,开发者通常不自己写反射,而是用 ProtocolLib 第三方库:

java
// ProtocolLib 发包(伪代码示意,API 随版本略有差异)
ProtocolManager manager = ProtocolLibrary.getProtocolManager();
PacketContainer packet = manager.createPacket(PacketType.Play.Server.TITLE);
packet.getTitleActions().write(0, TitleAction.TITLE);
// ... 设置字段
manager.sendServerPacket(player, packet);

ProtocolLib 替你处理了:

  • 版本兼容(自动适配各版 NMS)
  • 反射细节
  • 包类型枚举(PacketType.Play.Server.TITLE)

能用 API 就别发包

高版本 Bukkit 已有 Player.sendTitle(见Title 与 ActionBar),根本不需要发包。只有 API 真没有的功能(如自定义实体行为、特殊视觉)才考虑发包,且优先用 ProtocolLib 而非裸 NMS。

发包调试技巧 ​

  • 查协议:Minecraft Wiki 协议页 是权威,按版本查包结构和字段
  • 反编译服务端:用 IDEA 直接查看 PacketPlayOut* 类,对比构造函数和 Wiki 描述猜参数
  • 抓包工具:装个抓包插件/Mod 实时看客户端收到的包,验证你发的对不对
  • 先在单版本跑通:别一上来就想兼容所有版本,先在目标版本跑通,再考虑兼容

小结 ​

方式适合缺点
Bukkit API(sendTitle 等)API 已支持的功能覆盖有限
ProtocolLib发包需求,需跨版本多一个依赖
裸 NMS 发包ProtocolLib 也没有的特殊包版本地狱,维护噩梦

底层内容的忠告

到这里你已经看完了 NMS/发包。记住:这是最后的手段。先 API,再 ProtocolLib,最后才是裸 NMS。能用上层绝不用下层——这是写出来能在多版本长期运行的插件的关键。


插件开发教程到此全部结束。接下来进入玩法开发——把学到的插件能力组合成完整的网络游戏玩法。