- 查看: 1K
- 回复: 1
更多是我的个人经验总结和了解,可能有部分错漏,欢迎指正。
什么是 NMS
NMS 全称net.minecraft.server 包,是早在 1.17 以前(其实要更久,非常久)就形成的概念。当时 Minecraft 原版服务端的类都被混淆,给 remap 并 relocate 平铺到了类似 net.minecraft.server.v1_xx_Rx 格式的包中,为了方便,大伙们约定俗成地将其简称为 NMS。从 1.17 开始官方放出了 mojang mapping 混淆表,并且把 relocate 给取消了。上述的那些类,现在不在 net.minecraft.server 里面了,而是在 net.minecraft 里面,有层次地排列。它们本质上都是一样的东西,所以我们依然将它叫作 NMS。不是所有插件都需要自己调用 NMS,亲爱的 CraftBukkit 已经帮你包装过 NMS 了,它将原版混淆过的服务端包装成 Bukkit 接口,其它衍生服务端(Spigot、Paper、Folia 等)再基于它,包装更多的原版接口,甚至修改原版服务端逻辑。所以你平常写的插件,都是通过 Bukkit 接口去访问 NMS 的。有 SpigotMC 社区的md_5在给你兜底,你几乎不需要去考虑不同游戏版本的接口变动问题,他们会尽自己所能保证 Bukkit 的接口不会改变。
正如之前所说,CraftBukkit 已经帮我们包装过 NMS 了,那么插件主动去调用 NMS 的动机就非常明显了,那就是 CraftBukkit、Spigot、Paper、Folia 做得都不够全面,也不可能做得全面。总有一些功能是仅使用包装过的接口无法实现的,比如获取或设置一个数值(最常见的需求)、重新实现一个接口用来骗过服务端(NPC插件),等等。这些没有被服务端核心探索过的东西,就有可能成为一些令人惊叹的插件的实现方法。
准备依赖
在十几年前,引入 NMS 依赖是非常简单的一件事,直到服务端核心圈子里出现了一件大事,就再也不存在比较官方的依赖分发了。直到目前,有以下几种常见的获取 NMS 依赖方法
- 通过 BuildTools 构建,自动发布到本地仓库
- 添加本地 jar 依赖(从服务端核心的
META-INF/versions/spigot-xxx.jar获得,或者用 BuildTools 构建后在本地仓库获得) - 使用 paperweight,在开发者侧自动构建服务端核心并反混淆,在编译插件时自动重新混淆 NMS 部分
- 使用网络依赖(见后文)
- BuildTools 需要开发者另外下载,而且构建时间不短,构建一次就要了老命,因为这个特点,并不适合在 Github Actions 之类的自动构建系统使用(虽然 Actions 也能配置保存和读取缓存,但是麻烦)。
- 添加本地 jar 作为依赖的缺点显而易见,对于现代的包管理器(Gradle、Maven 等)来说,这是最不优雅的依赖引用方法,而且还要传一个几十兆大小的文件到仓库。方便,但非常不优雅,也占项目地方。
- 使用 paperweight 其实就相当于自动运行 BuildTools,它用起来非常方便,也非常优雅。但你要知道,虽然它不是 BuildTools,但也要跟 BuildTools 一样跑下载服务端、拉取仓库、反编译、应用 patch 等等流程的,这跑起来就非常地慢。更别说一个插件很有可能要跨非常多的版本做支持,我通常都是做 1.8 到最新版支持的,这起码十几二十个版本(1.8-1.19 的最新子版本 12 个,还有 1.20 的 R1 到 R4,1.21 的 R1 到 R4 一共 8 个版本,总共 20 个版本)。都要跑一遍 paperweight 我都能写一个新插件了,太慢了,所以不推荐使用。
- 使用网络依赖似乎全都是优点,速度只跟网络挂钩,所有内容都预构建好了只需要下载引用就行了。但是你猜为什么在十几年前引入 NMS 依赖是非常简单的事,就是因为发到了自家的 maven 仓库。所有分发了 NMS 依赖的仓库都是第三方仓库,没有吃 DMCA 的风险也有停止运营风险。
目前我收集到以下几个可用性比较高的 maven 仓库
- RoseWoodDev 接手 PlayerPoints 那个组织的公开仓库,国内速度稍快,只有 1.8+ 的依赖
- CodeMC 一个开源社区的公益仓库,国内速度时快时慢,有最低 1.7.2 的依赖
- lumine 开发 MythicMobs 那个组织的公开仓库,速度还行,有最低 1.6.4 的依赖,但是命名有点怪怪的,依赖也没那么全,只能凑合着用
- 1.8 开始基本都用
org.spigotmc:spigot(注意不是 spigot-api,spigot-api 不带 NMS) - 比较旧的版本会用
org.bukkit:craftbukkit,当然,现在到最新的版本也有分发 craftbukkit 依赖
配置项目
核心思想是,做一个通用的接口(interface),然后针对每一个 Minecraft 服务端版本都做一个子模块,去实现这个接口,在这些实现中调用 NMS 包的方法和字段,与原版内容交互。然后在插件加载时,根据服务端版本去反射寻找实现,找得到就继续正常加载,找不到就打印警告并卸载插件。
好,“根据服务端版本”这么小小的一句话,因为各方势力的割据,在插件开发者这里造成了不小的麻烦。Spigot 的
Bukkit.getServer() 实现始终都是 org.bukkit.craftbukkit.v1_xx_Rx.CraftServer,所以我们只要获取它的包名,就能获取当前服务器的 NMS 版本,即 v1_xx_Rx,比如 v1_19_R3 就是 1.19.4(要注意的是 NMS 版本和游戏版本没有必然关系,它们的对应关系也没文档可以查,只能靠实际经验)。但是在 Paper 高版本的某一个版本起,他们的
Bukkit.getServer() 实现在 org.bukkit.craftbukkit.CraftServer!你要是用这个方法获取,是包获取不到 NMS 版本的。当然,在这个版本起,Paper 也会自动处理要加载插件,把一些 NMS 访问给自动修正,也就是把调用包名的 v1_xx_Rx 给删掉。如果你是直接调用而没有使用反射调用,不需要担心它的修改。所以,使用
Bukkit.getServer() 方法获取不到版本(报错)的时候,要换成 Bukkit.getVersion() 的方法,获取 Bukkit 版本最前面出现的游戏版本即可。这里就不多讲代码了,展开讲要写很长,感兴趣的话可以看我的仓库。包括我自己做的 NMS 项目模板和使用方法也在仓库的主分支。
调用NMS
配置好了依赖才能调用,上面我们说了那么多,都是为了调用作准备。核心思想是通过
Bukkit接口 -> CraftBukkit -> 原版接口 这样的路径来访问原版对象的实例。就以模板项目中的
LivingEntity 为例,它在 CraftBukkit 对应的实现是 CraftLivingEntity,只要强制转换就行了。要是你实在不放心就加个判断,但是没必要,大多数情况下不会有问题,出问题的时候通常是插件冲突,到时再具体情况具体分析。
Java:
CraftLivingEntity craft = (CraftLivingEntity) entity;
好,我们拿到 CraftBukkit 那一层的实例了。CraftBukkit 这一层可能会有比 Bukkit 更多的内部方法,但通常不会有很多。一般来说,只需要访问
handle 字段,或者调用 getHandle() 方法,就能拿到 Minecraft 原版对象。不同版本的获取方法不同,有的版本还可能不会把 handle 字段给设置成公开的,要反射去取。
Java:
EntityLiving nms = craft.getHandle();
接下来就是你的活了,拿到了 Minecraft 原版这一层的实例,你想怎么改就怎么改。
当然改起来还是很憋屈的,毕竟名称都是混淆的,需要去查 MCP (1.7-1.12)、 yarn (1.13+) 之类的混淆表。因为我不常写大项目,命名基本都能猜得出来,没查过几次混淆表(而且查的基本都是高版本的),就不介绍用法了。
改起来是真的憋屈,混淆刚刚说过,除此之外,你只能读取或修改一些值(还有
private、protected 修饰符限制),调用一些没包装到 Bukkit 接口的方法,最多重写某个抽象类或者接口,而不能修改服务端原有的代码。相比,修改服务端源码的自由度要高很多,但兼容性非常低。看你的需求来选择调用 NMS 还是直接改服务端,对于私人服务器,直接改服务端源码可以实现一些纯插件完全无法实现的功能,体现服务器的独特性、创新性。- 内容版权许可
- CC BY-SA 署名-相同方式共享
最后编辑: