前情提要
在上一篇教程中,我们主要干了下面几件事:介绍ProtocolLib插件、引入ProtocolLib作为依赖、利用ProtocolLib进行基本的数据包监听
如果你顺着上一篇的教程做,现在你应该可以监听玩家打开Inventory的事件了
同时,如果你还记得的话,我们当时监听的数据包的类型是PacketType.Play.Server.OPEN_WINDOW
接下来,我们就来着手分析数据包
数据包里包着什么:去哪儿找 & 怎么找
数据包,顾名思义,就是一坨数据包起来。其本身是没有意义的,只不过我们人为地赋予了其中的数据一些意义,于是数据包也就有了意义
而赋予意义的这个东西,就是“协议”
一个协议中,规定了各个数据包(当然不可能只有一种数据包),以及数据包的各个部分所表示的具体含义
这就像0和1一样:你可以用0表示False,1表示True(例如C语言(stdbool.h中的定义)、Python语言);也可以用0表示True,1表示False(Bash脚本、Linux下程序返回码为0表示程序正常结束)。一切只不过是我们自己事先的规定罢了。只要我们愿意,随时都可以改变
有点扯远了。总而言之,我们肯定需要一个人来告诉我们一个数据包中各部分的数据是什么意思,也就是告诉我们Mine craft的协议,我们才能正确地读取/修改数据包,否则就叫瞎JB乱搞
这个人不是我,也不是Mojang,也不是ProtocolLib作者;而是一个网站:wiki.vg
毫不夸张地说,这个网站就相当于Minecraft协议的维基百科,是ProtocolLib开发人员应该人手一本的“圣经”
打开刚刚的网站链接,由于这个页面默认显示的是最新版本的Minecraft的协议内容(截至撰稿日期,是1.19.3版本),与我们教程的版本(1.19.2)不符,所以我们要先调整一下:
点击红框部分
会跳转到下面的页面,点击我们所需版本号(1.19.2)右边的page
即可跳转到对应1.19.2版本的协议页面
可以看到,现在的页面已经是1.19.2版本的协议了
接下来,我们要在协议的茫茫数据包定义中找到我们需要的,即PacketType.Play.Server.OPEN_WINDOW
Server还是Client?Sending还是Receiving?Clientbound还是Serverbound?
让我们仔细观察一下我们在代码中指定的数据包:
PacketType.Play.Server.OPEN_WINDOW
我们可以清楚地看到,该数据包(OPEN_WINDOW)被分类到了Server的类别下,不难得出该数据包是从服务端发往客户端的
其实你仔细想想也知道:如果客户端能不经过服务端点头而直接打开一个Inventory,那服务端如何才能知道哪个Inventory经过了变动,有什么物品被改变了,又如何实现数据的精准同步呢?
因此OPEN_WINDOW这个数据包只能由服务端发往客户端,表示服务端已经知晓并允许该客户端打开某个Inventory,且我们应当在onPacketSending
而非onPacketReceiving
中对该数据包进行处理
如果你尝试在onPacketReceiving
中对该数据包进行处理,你将会在控制台中得到如下的异常:java.lang.IllegalStateException: Override onPacketSending to get notifcations of sent packets!
这和你调用super.onPacketSending(event)
是一个效果,都是告诉你一定要重写onPacketSending
这个方法,而不是调用父类的实现或者南辕北辙地重写onPacketReceiving
方法去了
这不是什么违反逻辑的东西,大家应该不难理解
可是,你又会发现,在wiki.vg上,数据包是按照Clientbound和Serverbound进行分类的,而不是像ProtocolLib里面的Server和Client
那这两个东西是不是一样的呢?
既然我这么说了,那肯定是有坑的
没错,PacketType.Play.Server
中的数据包应该到Clientbound里面去找,PacketType.Play.Client
中的数据包应该到Serverbound里面去找
你大概可以理解成:由服务端发往客户端的数据包(PacketType.Play.Server
),是和客户端(client)绑定(bound)的;反之,由客户端发送到服务端的数据包(PacketType.Play.Client
),是和服务端(server)绑定(bound)的
如果你理解不了,那就直接死记硬背吧,这个也不是什么大碍
利用Packet ID查找特定的数据包(例子1)
回到数据包
我相信一些好奇宝宝早就已经在wiki.vg的页面Ctrl+F过了,不论是OPEN_WINDOW还是OPEN WINDOW,毛都找不到
原因很简单:ProtocolLib中对数据包的命名和wiki.vg中的有出入
那我们如何才能辨别同一个数据包呢?要是每个数据包都有自己独特的标识就好了!
不错,你说对了
如果你在wiki.vg的页面往下翻翻就会看到,每一个数据包的结构定义表格中都会有一项Packet ID
我拿另一个数据包(上图)举例:在这个数据包(生成经验球)中,wiki.vg上标明它的Packet ID为0x01
哪怕是江湖小白也应该知道,"0x"打头的东西一般都是十六进制数。0x01就相当于十进制里的1
拿到了Packet ID,我们就要去找在ProtocolLib中有关数据包类型定义的部分,寻找关于这个数据包的蛛丝马迹
那么ProtocolLib在哪里定义了数据包的类型呢?
看看前面的PacketType.Play.Server.OPEN_WINDOW
。不出意外的话,应该就是开头的PacketType
了
借助IDEA自带的反编译功能,我们直接就能看到PacketType.class
的源码
在这个1000多行的文件中,定义了ProtocolLib所有的数据包类型
这时候我们再用Ctrl+F,哪怕搜索个1,也能很快定位到两处数据包的定义:
在PacketType.Play.Server中的
SPAWN_ENTITY_EXPERIENCE_ORB = new PacketType(PacketType.Play.PROTOCOL, SENDER, 1, new String[]{"SpawnEntityExperienceOrb", "SPacketSpawnExperienceOrb"})
和
在PacketType.Play.Client中的
TILE_NBT_QUERY = new PacketType(PacketType.Play.PROTOCOL, SENDER, 1, new String[]{"TileNBTQuery"})
由于IDEA会显示形参的名字,我们可以看到这两个数据包的第三个参数currentId
都是1
尽管如此,都到这一步了,哪怕英语啥都不懂的人,都应该知道与图中数据包对应的是第1个数据包而非第2个数据包
而且,图片中也写了该数据包Bound To -> Client
,是属于Clientbound的,因此应该在PacketType.Play.Server里面
再不济,你仔细想想:像生成实体这种东西,难道能让客户端不经服务端的点头随意生成吗?肯定是要服务端让客户端生成,客户端才能生成,否则岂不就乱了套?所以这个数据包肯定是在PacketType.Play.Server里面的
利用Packet ID查找特定的数据包(例子2)
我知道有些人可能要吐槽:你绕了这么多弯,我在IDEA里面打到PacketType.Play.Server.
的时候,就跳出来候选的SPAWN_ENTITY_EXPERIENCE_ORB
了。英文我又不是不懂,哪需要你这么麻烦?
你这么说,我也没办法反驳。因为这个数据包在wiki.vg和ProtocolLib里名字很像,确实很好找
但是万一碰到wiki.vg的命名和ProtocolLib的命名差距很大的时候,不就手足无措了吗?
我举个后面的教程中会用到的一个数据包为例:PacketType.Play.Server.WINDOW_ITEMS
不管你在wiki.vg里怎么搜索,都找不到什么WINDOW_ITEMS
这时,我们在PacketType中找到WINDOW_ITEMS
的定义:
WINDOW_ITEMS = new PacketType(PacketType.Play.PROTOCOL, SENDER, 17, new String[]{"WindowItems", "SPacketWindowItems"});
可以看到,其Packet ID为17,转换成十六进制就是0x11
然后,我们再通过Packet ID到wiki.vg上反查,再结合之前说到的“PacketType.Play.Server
应该到Clientbound里去找”,很快就能定位到如下的数据包:
没错,ProtocolLib中的PacketType.Play.Server.WINDOW_ITEMS
,在wiki.vg中是Set Container Content
Packet ID陷阱
看到这里,你可能会觉得:我艹,有了Packet ID,就算你数据包穿上衣服,老子照样能认出你
但你还真别说,有时候Packet ID也不好使
还记得我在第一章里面提供了两种引入ProtocolLib依赖的方法吗?一种是使用构建版本#606,另一种是用Maven
我猜很多读者为了方便,直接用了Maven的方法,用了5.0.0-SNAPSHOT版本
在例子1的时候可能还好,在例子2的时候就一脸懵逼了。因为他们的PacketType.class
里面对于WINDOW_ITEMS
的定义是这样的:
WINDOW_ITEMS = new PacketType(PacketType.Play.PROTOCOL, SENDER, 16, new String[]{"WindowItems", "SPacketWindowItems"});
他们查到的Packet ID是16,或者说0x10。在wiki.vg中与此对应的数据包是:
但是这个咋看咋不像WINDOW_ITEMS
,简直就是两个完全相反的东西
这并不是wiki.vg搞错了,也不是ProtocolLib搞错了,而完全是版本的问题
我之前提到过,这个latest dev build适用的是Minecraft 1.19.3版本,而因为我们教程基于的是1.19.2版本,所以要求大家用的wiki.vg一直是1.19.2版本的
我们不妨打开1.19.3版本的wiki.vg看看:
我们可以看到,在1.19.3版本的wiki.vg上,清清楚楚地写着0x10对应的是Set Container Content,和我们之前的结论对上了
想想我在本篇教程开头说到的:
一切只不过是我们自己事先的规定罢了。只要我们愿意,随时都可以改变。
没错,Minecraft把协议给改了
实际上,Minecraft改协议并不是什么罕见的事情。不光在像1.18到1.19这样的版本跨度上会改变,甚至从1.19.2到1.19.3的小版本号更新都会有所改变
而我们并不是Minecraft的开发人员,无权决定要不要改,哪里改哪里不改。我们只能随着Minecraft协议的变动而变动
因此,你要记住这个教训:ProtocolLib不是版本越新越好,只有最适合的版本才是最好的。因为不同版本之间的协议很有可能存在差异(尤其是dev版本)
这就是为什么我推荐使用构建版本#606,而非5.0.0-SNAPSHOT的原因了
当然,如果是日常的bug修复,那还是建议及时更新的
数据包里包着什么:怎么看
经过前面大量的铺垫,我们终于要来看看究竟怎么看一个数据包的结构了
可是我还不打算用PacketType.Play.Server.OPEN_WINDOW
作例子。因为我们在下一篇教程里会着重分析它,现在就分析完了,未免有点可惜
于是,我们再次请出PacketType.Play.Server.SPAWN_ENTITY_EXPERIENCE_ORB
来助我们一臂之力
图片中最上方的粗体字,是这个数据包的类型(生成经验球)
紧接着粗体字下方的,是对这个数据包的说明(生成一个或多个经验球)
下面就是表格了
针对这个表格,我也列了一个表格进行详细说明
表格字段 | 说明 |
---|---|
Packet ID | 用于标识一个数据包的类型,我们已经发掘过了 |
State | 该数据包处于哪个阶段 可选值: Handshaking(握手,就是客户端向服务端打个招呼,告诉服务端“我要 Status(状态查询,主要是服务端告诉客户端一些信息,包括在线人数、最大人数、服务端版本、协议版本、MOTD、小图标等等(我们在“多人游戏”界面看到的东西)) Login(登录,包括登录成功、断开连接、通讯加密、数据包压缩等等) Play(游戏内容,是最主要的数据包类型) |
Bound To | 和哪一端绑定 可选值:Server, Client |
Field Name | 数据包所包含的数据的名称 |
Field Type | 数据包所包含的数据的类型 |
Notes | 数据包所包含的数据的说明 |
当然,对于这个数据包,我们最关注的,还是图中红框的部分:Field Name, Field Type, Notes
我们从易到难说起:
X, Y, Z:想必我不用多说什么了,就是生成经验球的位置
Count:结合后面Notes部分的说明,可知该字段表示玩家在收集了经验球后会获得多少经验值
Entity ID:这个需要着重强调一下
Entity ID
乍一看,你的第一直觉可能会告诉你:这不就是实体的UUID吗,有什么特别的?
其实不然,Entity ID和UUID还是有区别的
不过别担心,这个Entity ID在Spigot API中也有所涉及,就让我们从亲切的Spigot API的角度来看看这两者的区别
首先让我们看看Spigot API中,对于getUniqueId
这个方法的描述:
然后我们再看看对于getEntityId
方法的描述:
一眼看上去,不论是中文还是英文,写的都差不多
但是两者有一个除了方法名称和返回值类型外的关键区别:“持久性”
官方文档中对于getUniqueId
方法有一个“持久”的修饰,说明这个UUID是永远不变的,不管你服务器重启多少次,不管你在哪里,只要知道一个实体的UUID,都能顺藤摸瓜找到UUID背后唯一的“它”;
而getEntityId
就不一样了,Entity ID更像是一个“句柄”——在一定的条件下它是唯一的,但是超出了这个条件就不敢保证了,而且它是会变的
对于一个玩家,他的UUID永远不会变,但是他的Entity ID在他每次登录服务器时都会发生变化
我这么强调Entity ID自然是有原因的,因为以后的教程中会涉及到它。这里相当于先给大家提个醒
结语
这篇教程主要围绕着Minecraft协议展开,向大家介绍了wiki.vg这个网站,也教了大家如何利用Packet ID在ProtocolLib和wiki.vg之间自如穿梭,同时强调了使用Packet ID时要注意的地方,以及选对ProtocolLib版本的重要性
最后,我们简单地说了一下如何在wiki.vg上看一个数据包的结构,还有实体的Entity ID与UUID的区别
在下一篇教程中,我们就要正式开始实现插件的国际化了,首当其冲的便是我们的老朋友PacketType.Play.Server.OPEN_WINDOW
布置任务
在这篇教程最后的最后,我再给大家布置一个任务。大家看心情完成
内容很简单,在wiki.vg上找到与ProtocolLib中PacketType.Play.Server.OPEN_WINDOW
对应的数据包的名字,我们在下一篇教程中会用到它
相信这个任务对于现在的你们来说,不是会不会的问题,而是想不想的问题
还是提醒一下:使用构建版本#606的,查对应1.19.2版本的wiki.vg;使用5.0.0-SNAPSHOT的,查对应1.19.3版本的wiki.vg
相关链接
最新版的wiki.vg(截至撰稿日期,是Minecraft 1.19.3版本):https://wiki.vg/Protocol
Minecraft 1.19.2版本的wiki.vg:https://wiki.vg/index.php?title=Protocol&oldid=17873