--- front: hard: 高级 time: 30分钟 --- # 自定义发包 ## 数据包介绍 翻阅NMS我们可以知道的是,服务端的数据变更一般都是通过 DataWatcher 来判断以后通过 数据包的方式发送到客户端,客户端接收到数据包后对玩家客户端内容从而进行更改 这就是为什么上一个教程中,我们更改玩家的经验时不能直接通过赋值的方式进行更改 而是通过内置的一个“神秘(混淆了)”封装的方法去更改,这是因为只有通过此方法,才能将参数传参给父类 然后在通过父类的方法赋值给 DataWatcher 而翻阅 DataWatcher 类里的方法我们在一大堆混淆的代码发现了 ```java public void refresh(EntityPlayer to) { if (!this.d()) { List> list = this.packAll(); if (list != null && to.getBukkitEntity().canSee(this.d.getBukkitEntity())) { to.c.a(new PacketPlayOutEntityMetadata(this.d.af(), list)); } } } ``` 此方法大概率就是发送数据包的方法了. 此时你会发现,他将 `PacketPlayOutEntityMetadata` 包发了出去 那这个包是干嘛的呢? 我们查阅[我的世界维基百科数据包](https://minecraft.wiki/w/Java_Edition_protocol/Packets?oldid=2772385) 可以发现其中的EntityMetadata 正好是更新现有生物实体的元数据,而经验值属于玩家实体特有的元数据,所以符合我们上述的说法 ![](../images/0_20.png) ## 如何自定义一个数据包并发出去呢 说了那么多如何自定义一个数据包发出去呢 在这里做一个示例便是 在1.8更新,我的世界更新了Title等一系列文字显示的内容,但是Bukkit并没有及时的更新相关的API 直到1.12.2才更新快速发送Title的方法,所以在1.8 - 1.11 期间,开发者想要发送Title则需要自己通过封装NMS去手动发包 我们查阅对应版本的数据包,在这里我拿1.12.2的wiki去演示 [我的世界维基百科数据包](https://minecraft.wiki/w/Java_Edition_protocol/Packets?oldid=2772385) 找到有关于Title的,可以发现此数据包 ![](../images/0_21.png) 我们可以看到基本介绍,他是发送给客户端的,状态是Play,那么在NMS里应该就是 `PacketPlayOutTitle` 再查看PacketPlayOutTitle类,虽然里面全部混淆了 但是对比成员变量与Wiki中Action描述可以不难猜出PacketPlayOutTitle包构造函数里的参数 按照顺序每个参数类型都是按照顺序填写的 ![](../images/0_22.png) 我们发现有四个构造函数,那我们选择能够自定义程度最高的,也就是参数填入最多的那一个构造函数 第一个参数传入 Title 类型 (TITLE、SUBTITLE) > 这就代表着我们要同时发2个包。一个是TITLE的数据包,另一个是SUBTITLE的数据包。 第二个参数传入 `IChatBaseComponent` ,这个是我的世界的文本基础组件。 第三个参数传入 淡入时间 第四个参数传入 停留时间 第五个参数传入 淡出时间 当我们都填写好参数后,可以写出下述代码 ```java public void sendTitle(Player player, String title, String subtitle, int fadeIn, int stayIn, int fadeOut) { IChatBaseComponent titlesend = IChatBaseComponent.ChatSerializer.a("{\"text\": \"" + title + "\"}"); IChatBaseComponent subtitlesend = IChatBaseComponent.ChatSerializer.a("{\"text\": \"" + subtitle + "\"}"); PacketPlayOutTitle packet = new PacketPlayOutTitle(PacketPlayOutTitle.EnumTitleAction.TITLE, titlesend, fadeIn, stayIn, fadeOut); PacketPlayOutTitle packet2 = new PacketPlayOutTitle(PacketPlayOutTitle.EnumTitleAction.SUBTITLE, subtitlesend, fadeIn, stayIn, fadeOut); (((CraftPlayer)player).getHandle()).playerConnection.sendPacket((Packet)packet); (((CraftPlayer)player).getHandle()).playerConnection.sendPacket((Packet)packet2); } ``` 此时我们即可快速调用自己写的 sendTitle 快速发送一份Title文字 但是翻阅各版本NMS可以发现,每一个版本的Packet的参数混淆名、类的位置等都不一样 所以这就导致我们通过NMS去发送title,就可能导致版本不互通 所以这就需要我们对各版本都有处理,主要就是利用到Java的接口对不同版本都有一定的兼容,这里就不再过多介绍 > 由于NMS兼容性确实很差,一般来说开发者在操作Packet的时候都是通过第三方库 ProtocolLib 处理