7月31日同步更新
This commit is contained in:
146
mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/4-底层内容/1-认识NMS与OBC.md
Normal file
146
mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/4-底层内容/1-认识NMS与OBC.md
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
front:
|
||||
hard: 高级
|
||||
time: 20分钟
|
||||
---
|
||||
|
||||
# 认识 NMS 和 OBC
|
||||
|
||||
nms,即 Java 包 `net.minecraft.server`,存放的是 Minecraft 服务端游戏逻辑代码,这篇教程将会采用 `Spigot` 作为 Bukkit API(就你们日常开发用的API) 体系的例子(CraftBukkit 也是 SpigotMC 维护,且基本所有 Bukkit 分支都是基于 Spigot 开发)
|
||||
|
||||
BukkitAPI 真的涵盖了不少东西,但是 BukkitAPI 并不是十全十美的,有时候我们确实需要使用 NMS。
|
||||
~~Forge其实也有个net.minecraft.server,但他和Spigot的没半点关系~~
|
||||
## 使用 NMS 之前
|
||||
|
||||
md_5:*Wait!* 你真的需要使用 NMS 吗?
|
||||
> NMS不是API。当你遇到什么想做的事情的时候,你不应该第一时间去考虑 NMS 或者 发包
|
||||
令人费解的是,我们几乎每天都看到人们专门使用 NMS 做一些简单的事情,如 ScoreBoard、BossBar 或粒子。但是实际上,自从Mojang添加了这些东西之后,Spigot/Bukkit-API 早就有这些功能了。
|
||||
|
||||
每当你考虑使用 NMS 时,请思考以下问题:
|
||||
1. 我是否需要NMS来做这个?
|
||||
2. 是否有一个API来实现这个功能?
|
||||
3. 我可以为这个贡献/创建/ ***提议*** 一个API吗?
|
||||
|
||||
|
||||
对NMS的滥用造成的后果非常严重。
|
||||
- 1. 插件将失去版本迁移的能力(针对单个版本而言)
|
||||
- 2. 阻碍了 API 的发展并且树立了一个坏榜样。
|
||||
|
||||
如果你确实想清楚确实没有现有的 API 能帮到你,那么来...
|
||||
|
||||
# 怎么使用 NMS
|
||||
NMS 里的内容太多,故本教程**不会**教授NMS有什么东西,但是可以教你怎么玩。
|
||||
|
||||
## 开发环境准备
|
||||
由于DMCA的原因,Bukkit不会直接提供NMS,您需要将构建好的服务端代码引入项目中
|
||||
|
||||
如果没有且你正在使用 `Gradle` , `Maven` 这样的依赖管理器,考虑如下方法(图文):
|
||||
|
||||
这边以Gradle为例
|
||||
1. 在项目根目录创建libs文件夹用于放第三方库
|
||||

|
||||
2. 将构建好的服务端直接拖入libs文件夹中
|
||||

|
||||
3. 配置Gradle
|
||||

|
||||
4. 同步Gradle配置
|
||||

|
||||
5. 你已经成功引入NMS
|
||||
|
||||
## 获取到一个来自 API 背后的对象
|
||||
*当你从控制台直接输出一个 `Player` 对象时,会发生什么?*
|
||||
|
||||
你会得到一个 `CraftPlayer{name=玩家名}`而不是NMS里面的`EntityPlayer`。这是因为在Bukkit-API背后还有一层,他叫`OBC`,也就是`org.bukkit.craftbukkit`。
|
||||
|
||||
OBC 是 Bukkit API 的实现,其本质是NMS的封装,因此我们并不需要太关心它。
|
||||
|
||||
最简单的一个说法就是,实际上我们操作NMS封装API也是OBC所做的事情。
|
||||
|
||||
就上文问题,怎么拿到一个 NMS 里面的 `EntityPlayer`?
|
||||
|
||||
首先我们需要使用反编译工具,当然了,你也可以通过IDEA直接查看。
|
||||
|
||||
来看 `CraftPlayer` 对 `EntityPlayer` 做了什么..
|
||||
|
||||

|
||||
CraftPlayer 将 EntityPlayer 传给了他的父类构造器,我们接着追踪..
|
||||

|
||||
而 CraftPlayer 的父类 CraftHumanEntity 依然将 EntityPlayer 传入父类 CraftLivingEntity,我们继续翻阅到 CraftLivingEntity 中
|
||||

|
||||
经过一条不 是 很 长的继承链后,我们找到了 `CraftEntity`,看来 `EntityPlayer` 最后是被传道这里了!
|
||||
> 注意:OBC里面的类 `implements` 的都是BukkitAPI的实现,不要搞混了!
|
||||
|
||||
然后往上翻,看看 entity 是什么情况。
|
||||

|
||||
他的修饰符是 `protected`,这意味着只有继承树内或者同一个包里面才能访问到它,而这在NMS/OBC中是常有的事情。
|
||||
|
||||
## 反射!
|
||||
我们很容易就可以写出这样的代码来获取到这个 entity 对象。
|
||||
|
||||
而这就需要用到Java的特性 —— 反射
|
||||
```java
|
||||
public static final String serverVersion = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3];
|
||||
try{
|
||||
Player player = ....; // Who cares ?
|
||||
Class<?> clazz = Class.forName("org.bukkit.craftbukkit."+serverVersion+".entity.CraftEntity");
|
||||
Field f = clazz.getDeclaredField("entity");
|
||||
f.setAccessible(true);
|
||||
Object result = f.get(player);
|
||||
}catch(Throwable t){
|
||||
t.printStackTrace();
|
||||
}
|
||||
```
|
||||
现在 `result` 里面存的就是我们需要的 EntityPlayer,然后我们可以转换它...做点事情。
|
||||
为什么要这么麻烦?其实下面的代码一样可以做到这个效果:
|
||||
```java
|
||||
try{
|
||||
Player player = ....; // Who cares ?
|
||||
Field f = CraftEntity.class.getDeclaredField("entity");
|
||||
f.setAccessible(true);
|
||||
Object result = f.get(player);
|
||||
}catch(Throwable t){
|
||||
t.printStackTrace();
|
||||
}
|
||||
```
|
||||
|
||||
假设BukkitAPI没有封装修改经验值的方法,我们要修改玩家实体的经验值
|
||||
|
||||
就可以通过刚刚获取到的result的变量判断是否为 NMS 的 EntityPlayer 对象
|
||||
```java
|
||||
try{
|
||||
Player player = event.getPlayer();
|
||||
getLogger().info("test");
|
||||
Class<?> clazz = Class.forName("org.bukkit.craftbukkit."+serverVersion+".entity.CraftEntity");
|
||||
Field f = clazz.getDeclaredField("entity");
|
||||
f.setAccessible(true);
|
||||
Object result = f.get(player);
|
||||
if (result instanceof EntityPlayer){
|
||||
((EntityPlayer) result).d(1000);
|
||||
}
|
||||
}catch(Throwable t){
|
||||
t.printStackTrace();
|
||||
}
|
||||
```
|
||||
|
||||
> 那为什么result的方法是 `d`?
|
||||
> 这是因为 Minecraft源码本身就是混淆的,OBC则是Bukkit反编译探索出来这个大概是什么方法然后进行封装的
|
||||
> 所以我们在和OBC做同样的事情时,也要大概去猜测这些方法名是做什么的
|
||||
> 大家可以尝试翻阅一下 EntityPlayer 源码,其中成员变量 newExp 被 方法 d() 所修改
|
||||
> 所以大概猜测这个就是修改经验值的方法,调用后证实确实是这个方法
|
||||
|
||||
|
||||
## 版本兼容性
|
||||
|
||||
实际上,如果你直接采用了 `CraftEntity.class` 的方法都会对这个 class 建立符号引用。
|
||||
每个版本的 Spigot,无论是 OBC 或者 NMS,他们的包名都会变化——也就是说你的插件会爆炸
|
||||
所以,如果你不想为了一个版本再把逻辑写一次,最好还是用反射的写法。对于公开的方法,也可以使用更高效的 `MethodHandle`。
|
||||
|
||||
如果你有注意到的话, md_5 说过 `NMS 并不是`一个API(其实也包括OBC)。
|
||||
什么意思呢?也就是`使用NMS没有任何安全性保障`,**你反射的字段/方法或许下一个版本就会被删改。**
|
||||
实例惨案: Minecraft 1.17 Spigot大改,使用 Mojang 官方混淆表,以往 NMS 插件**全都**报废。(除了一些自带兼容的服务端)
|
||||
|
||||
# 本章小结
|
||||
- NMS是`net.minecraft.server`,一个包名,放MC逻辑
|
||||
- Spigot的NMS没有安全保障,md_5 都不推荐用
|
||||
- 使用NMS之前要先找是否已经有了对应的 API
|
||||
- 在 Bukkit-API 和 NMS 之间还有一个实现,它叫 OBC。
|
||||
106
mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/4-底层内容/2-自定义发包.md
Normal file
106
mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/4-底层内容/2-自定义发包.md
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
front:
|
||||
hard: 高级
|
||||
time: 30分钟
|
||||
---
|
||||
|
||||
# 自定义发包
|
||||
|
||||
## 数据包介绍
|
||||
|
||||
翻阅NMS我们可以知道的是,服务端的数据变更一般都是通过 DataWatcher 来判断以后通过
|
||||
|
||||
数据包的方式发送到客户端,客户端接收到数据包后对玩家客户端内容从而进行更改
|
||||
|
||||
这就是为什么上一个教程中,我们更改玩家的经验时不能直接通过赋值的方式进行更改
|
||||
|
||||
而是通过内置的一个“神秘(混淆了)”封装的方法去更改,这是因为只有通过此方法,才能将参数传参给父类
|
||||
|
||||
然后在通过父类的方法赋值给 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` 包发了出去
|
||||
|
||||
那这个包是干嘛的呢?
|
||||
|
||||
我们查阅[我的世界维基百科数据包](https://minecraft.wiki/w/Java_Edition_protocol/Packets?oldid=2772385)
|
||||
|
||||
可以发现其中的EntityMetadata 正好是更新现有生物实体的元数据,而经验值属于玩家实体特有的元数据,所以符合我们上述的说法
|
||||
|
||||

|
||||
|
||||
## 如何自定义一个数据包并发出去呢
|
||||
|
||||
说了那么多如何自定义一个数据包发出去呢
|
||||
|
||||
在这里做一个示例便是
|
||||
|
||||
在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的,可以发现此数据包
|
||||
|
||||

|
||||
|
||||
我们可以看到基本介绍,他是发送给客户端的,状态是Play,那么在NMS里应该就是 `PacketPlayOutTitle`
|
||||
|
||||
再查看PacketPlayOutTitle类,虽然里面全部混淆了
|
||||
|
||||
但是对比成员变量与Wiki中Action描述可以不难猜出PacketPlayOutTitle包构造函数里的参数
|
||||
|
||||
按照顺序每个参数类型都是按照顺序填写的
|
||||
|
||||

|
||||
|
||||
我们发现有四个构造函数,那我们选择能够自定义程度最高的,也就是参数填入最多的那一个构造函数
|
||||
|
||||
第一个参数传入 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 处理
|
||||
Reference in New Issue
Block a user