自定义发包
服务端的数据变更,最终都是通过**数据包(Packet)**发给客户端,客户端收到后更新画面。这是 NMS 最常见的使用场景。
为什么不能直接赋值
有些数据改了不会自动同步给客户端。比如改玩家经验值,直接改 NMS 字段没用——必须经过 DataWatcher 判断后发包,客户端才会更新经验条。
翻 DataWatcher 的混淆代码,能找到类似这样的方法:
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 类 |
|---|---|
| EntityMetadata | PacketPlayOutEntityMetadata |
| Title | PacketPlayOutTitle |
| Entity Destroy | PacketPlayOutEntityDestroy |
怎么构造并发包
以发 Title 为例(1.8~1.11 期间 Bukkit 还没 Title API,只能这么干):
- 查对应版本协议,找 Title 数据包的参数
- 看 NMS 里
PacketPlayOutTitle的构造函数,对比 Wiki 的 Action 描述猜参数 - 选参数最多的构造函数(自定义度最高)
- 构造包,通过
playerConnection.sendPacket发出
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 第三方库:
// 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。能用上层绝不用下层——这是写出来能在多版本长期运行的插件的关键。
插件开发教程到此全部结束。接下来进入玩法开发——把学到的插件能力组合成完整的网络游戏玩法。
