本帖最后由 贺兰兰 于 2021-6-16 17:45 编辑
Bukkit NMS 开发实践 —— 创建你自己的自定义实体(适用于 1.16.3 - 1.16.5 版本)
什么是 NMS?
NMS 是 net.minecraft.server 包的简写,是 CraftBukkit 服务端及其下游服务端的底层实现,其代码包含 Mojang 发布的 Vanilla 服务端代码和 SpigotMC 添加的、用于与 BukkitAPI 进行交互的代码。在开发者无法借助 BukkitAPI 完成所需要的功能时,开发者我常常使用 NMS 进行开发。NMS 开发是底层行为,同时跨版本兼容性较差,除非必须使用,否则还请尽量使用 BukkitAPI。NMS 仅存在于编译后的服务端内部,不属于 BukkitAPI 内容。各版本的 NMS 包名一般均为 net.minecraft.server.v版_本_R号,如 net.minecraft.server.v1_16_R3。NMS 包内为扁平结构,没有二级包。NMS 包内类名为 Spigot 定义的反混淆名;方法、字段名一部分为 Spigot 定义的反混淆名,一部分为原混淆名;方法参数名一般为原混淆名。本教程旨在教授 Bukkit 开发者以 NMS 使用方法,拓展 Bukkit 开发者的开发视野。
自 1.17 后,SpigotMC 开始提供 Mojang 混淆表版本的 Spigot 服务端,这意味着大大简化了开发难度 —— 不需要再对照混淆表一个一个看 NMS 方法名,而可以像 Forge 或是 Fabric 开发者一样使用各自的反混淆代码直接进行开发 —— 只需要使用 Spigot 提供的 SpecialSource 工具将 Mojang Mapping 转换回 obf 版本即可发布。
如何使用 NMS?
要想使用 NMS,您必须手动导入编译好的 CraftBukkit/Spigot 服务端核心,这样才能获取其中内置的 NMS。对于 Paper 及其下游服务端来说,不应该直接导入服务端核心本体,而应该导入运行一次服务端后生成的 patched_x.x.x.jar 文件。
教程:创建自定义实体
很显然,BukkitAPI 没有向我们提供自定义实体的功能,甚至,实体的类型是确定的,不能更改的。因此,要想自定义实体,必须使用 NMS。当然,我们并不能创建 Forge 或是 Fabric 意义上完全自定义模型的实体。但是,我们能够通过继承原版存在的实体,创建一个新的实体类型,为这个新的实体类型指定一些交互。本例中,我们将会通过创建一个会在夜间燃烧、不做任何交互、拥有 Boss 血条的巨人僵尸来演示这一过程。
继承已有实体
让我们创建 EntityCustomGiantZombie 类,继承 net.minecraft.server.v1_16_R3.EntityGiantZombie 类:
复制代码
接下来,初始化该实体,实现超类构造器:
复制代码
注意,此处的 World 不是我们熟识的 org.bukkit.World 接口,而是 net.minecraft.server.v1_16_R3.World 抽象类,因此不能一概而论。
当然,我们可以通过以下代码实现 Bukkit World 和 NMS World 的互转:
复制代码
其实,调用 net.minecraft.server.v1_16_R3.World#getHandle() 返回的并非 org.bukkit.World 接口,而是 org.bukkit.craftbukkit.v1_16_R3.CraftWorld 类,其为 org.bukkit.World 在 CraftBukkit 服务端中的内部实现,因此可以直接转换到 World 接口。事实上,nmsWorld#getWorld() 方法返回的也是 CraftWorld 类。
要想生成该实体,则应该调用 WorldServer#addEntity(Entity, SpawnReason) 方法初始化实体,然后使用 Entity#setPositionRotation(double, double, double, float, float) 传送实体到出生位置。为了简便流程,我们可以创建一个可传入 Bukkit Location,并可以自动设置实体出生位置的构造函数:
复制代码
然后,在适当的位置初始化该实体,比如,某一个 Bukkit EventListener 中:
复制代码
这样,你就能看到一个由你自定义的巨人僵尸实体了!
添加 Boss 血条
接下来,我们尝试向这个自定义实体添加 Boss 血条。
添加 Boss 血条大概需要有三步操作:
首先,我们需要定义一个 Boss 血条。在 EntityCustomGiantZombie 类中添加以下字段:
复制代码
并在底层构造器中初始化这个 Boss 血条:
复制代码
这初始化了一个血条名为金色的 "Boss 血条示例",血条颜色为蓝色的,1/12 比例风格的,在玩家显示 Boss 血条时时天空变暗的 Boss 血条。
然后,我们需要覆盖 void b() 和 void c() 两个方法,这两个方法在 MCP 中描述如下:
复制代码
这正是我们需要的,可以动态显示和隐藏 Boss 血条的方法。覆盖这些方法,并添加一些内容:
复制代码
最后,覆盖 void tick() 方法,该方法一看名字就知道是干什么的了:
复制代码
其中 bossBar.setProgress(float) 接受一个单精度浮点数,为血条剩余的血量百分比。
需要注意的是,一定要调用 super.tick(),否则该怪物完全被冻结,不会产生任何交互。
让怪物在夜间燃烧
要想让怪物在夜间燃烧,则需要在每 tick 检测怪物是否处于夜间环境,如果是,则使怪物燃烧。因此,在 tick() 方法键入以下代码:
复制代码
其中 setOnFire(int, boolean) 的第一个参数为燃烧的 tick 数,由于是 1 tick 检测一次,因此我们在这里填写 1;第二个参数为是否触发 BukkitAPI 的 EntityCombustEvent 事件,为了避免事件被多次调用,这里我们填写 false。
这样,怪物就会在夜间燃烧了。
自定义怪物行为
要想自定义怪物行为,我们需要为怪物添加 PathfinderGoal,因为我们不希望保留怪物原本的行为,因此我们需要刷新怪物的 goalSelector(行为选择器) 和 targetSelector(攻击目标选择器)。他们均为 PathfinderGoalSelector 类型的对象。经过研究该对象我们可以发现,PathfinderGoal 对象被包装为 PathfinderGoalWrapped 对象后,存储于private final Set<PathfinderGoalWrapped> d 字段中,由于该字段是 private final 的,因此我们需要通过反射修改该字段。在怪物的底层构造器中键入以下代码:
复制代码
这样,我们便得到了一个没有任何行为的怪物。如果我们还需要为怪物添加行为,只需要为 goalSelector 或是 targetSelector 添加继承了 PathfinderGoal 类的对象即可。NMS 中本身就包含了大量的 PathfinderGoal,大家可自行探索。
来自群组: Server CT
Bukkit NMS 开发实践 —— 创建你自己的自定义实体(适用于 1.16.3 - 1.16.5 版本)
什么是 NMS?
NMS 是 net.minecraft.server 包的简写,是 CraftBukkit 服务端及其下游服务端的底层实现,其代码包含 Mojang 发布的 Vanilla 服务端代码和 SpigotMC 添加的、用于与 BukkitAPI 进行交互的代码。在开发者无法借助 BukkitAPI 完成所需要的功能时,开发者我常常使用 NMS 进行开发。NMS 开发是底层行为,同时跨版本兼容性较差,除非必须使用,否则还请尽量使用 BukkitAPI。NMS 仅存在于编译后的服务端内部,不属于 BukkitAPI 内容。各版本的 NMS 包名一般均为 net.minecraft.server.v版_本_R号,如 net.minecraft.server.v1_16_R3。NMS 包内为扁平结构,没有二级包。NMS 包内类名为 Spigot 定义的反混淆名;方法、字段名一部分为 Spigot 定义的反混淆名,一部分为原混淆名;方法参数名一般为原混淆名。本教程旨在教授 Bukkit 开发者以 NMS 使用方法,拓展 Bukkit 开发者的开发视野。
自 1.17 后,SpigotMC 开始提供 Mojang 混淆表版本的 Spigot 服务端,这意味着大大简化了开发难度 —— 不需要再对照混淆表一个一个看 NMS 方法名,而可以像 Forge 或是 Fabric 开发者一样使用各自的反混淆代码直接进行开发 —— 只需要使用 Spigot 提供的 SpecialSource 工具将 Mojang Mapping 转换回 obf 版本即可发布。
如何使用 NMS?
要想使用 NMS,您必须手动导入编译好的 CraftBukkit/Spigot 服务端核心,这样才能获取其中内置的 NMS。对于 Paper 及其下游服务端来说,不应该直接导入服务端核心本体,而应该导入运行一次服务端后生成的 patched_x.x.x.jar 文件。
教程:创建自定义实体
很显然,BukkitAPI 没有向我们提供自定义实体的功能,甚至,实体的类型是确定的,不能更改的。因此,要想自定义实体,必须使用 NMS。当然,我们并不能创建 Forge 或是 Fabric 意义上完全自定义模型的实体。但是,我们能够通过继承原版存在的实体,创建一个新的实体类型,为这个新的实体类型指定一些交互。本例中,我们将会通过创建一个会在夜间燃烧、不做任何交互、拥有 Boss 血条的巨人僵尸来演示这一过程。
继承已有实体
让我们创建 EntityCustomGiantZombie 类,继承 net.minecraft.server.v1_16_R3.EntityGiantZombie 类:
- public class EntityCustomGiantZombie extends EntityGiantZombie {}
接下来,初始化该实体,实现超类构造器:
- public EntityCustomGiantZombie(EntityTypes<? extends EntityGiantZombie> var0, World var1) {
- super(var0, var1);
- }
注意,此处的 World 不是我们熟识的 org.bukkit.World 接口,而是 net.minecraft.server.v1_16_R3.World 抽象类,因此不能一概而论。
当然,我们可以通过以下代码实现 Bukkit World 和 NMS World 的互转:
- //Bukkit World to NMS World
- org.bukkit.World bukkitWorld = nmsWorld.getWorld();
- // NMS World to Bukkit World
- net.minecraft.server.v1_16_R3.World nmsWorld = ((CraftWorld) bukkitWorld).getHandle();
其实,调用 net.minecraft.server.v1_16_R3.World#getHandle() 返回的并非 org.bukkit.World 接口,而是 org.bukkit.craftbukkit.v1_16_R3.CraftWorld 类,其为 org.bukkit.World 在 CraftBukkit 服务端中的内部实现,因此可以直接转换到 World 接口。事实上,nmsWorld#getWorld() 方法返回的也是 CraftWorld 类。
要想生成该实体,则应该调用 WorldServer#addEntity(Entity, SpawnReason) 方法初始化实体,然后使用 Entity#setPositionRotation(double, double, double, float, float) 传送实体到出生位置。为了简便流程,我们可以创建一个可传入 Bukkit Location,并可以自动设置实体出生位置的构造函数:
- public EntityCustomGiantZombie(Location loc) {
- this(EntityTypes.GIANT, ((CraftWorld) loc.getWorld()).getHandle());
- setPositionRotation(loc.getX(), loc.getY(), loc.getZ(), loc.getYaw(), loc.getPitch());
- }
然后,在适当的位置初始化该实体,比如,某一个 Bukkit EventListener 中:
- ((CraftWorld) e.getPlayer().getWorld()).getHandle().addEntity(new EntityCustomGiantZombie(e.getPlayer().getLocation()), CreatureSpawnEvent.SpawnReason.CUSTOM);
这样,你就能看到一个由你自定义的巨人僵尸实体了!
添加 Boss 血条
接下来,我们尝试向这个自定义实体添加 Boss 血条。
添加 Boss 血条大概需要有三步操作:
- 当玩家进入追踪视野时显示 Boss 血条
- 当玩家离开追踪视野时隐藏 Boss 血条
- 当怪物受到攻击时令 Boss 血条相应减少血量
首先,我们需要定义一个 Boss 血条。在 EntityCustomGiantZombie 类中添加以下字段:
- private final BossBattleServer bossBar;
并在底层构造器中初始化这个 Boss 血条:
- bossBar = new BossBattleServer(new ChatComponentText("Boss 血条示例").a(EnumChatFormat.GOLD), BossBattle.BarColor.BLUE, BossBattle.BarStyle.NOTCHED_12);
- bossBar.setDarkenSky(true);
- }
这初始化了一个血条名为金色的 "Boss 血条示例",血条颜色为蓝色的,1/12 比例风格的,在玩家显示 Boss 血条时时天空变暗的 Boss 血条。
然后,我们需要覆盖 void b() 和 void c() 两个方法,这两个方法在 MCP 中描述如下:
- /**
- * Add the given player to the list of players tracking this entity. For instance, a player may track a boss in order
- * to view its associated boss bar.
- */
- public void addTrackingPlayer(ServerPlayerEntity player) {
- }
- /**
- * Removes the given player from the list of players tracking this entity. See {[url=home.php?mod=space&uid=41191]@link[/url] Entity#addTrackingPlayer} for
- * more information on tracking.
- */
- public void removeTrackingPlayer(ServerPlayerEntity player) {
- }
这正是我们需要的,可以动态显示和隐藏 Boss 血条的方法。覆盖这些方法,并添加一些内容:
- @Override
- public void b(EntityPlayer entityplayer) {
- super.b(entityplayer);
- this.bossBar.addPlayer(entityplayer);
- }
- @Override
- public void c(EntityPlayer entityPlayer) {
- super.c(entityPlayer);
- this.bossBar.removePlayer(entityPlayer);
- }
最后,覆盖 void tick() 方法,该方法一看名字就知道是干什么的了:
- @Override
- public void tick() {
- super.tick();
- this.bossBar.setProgress(getHealth() / getMaxHealth());
- }
其中 bossBar.setProgress(float) 接受一个单精度浮点数,为血条剩余的血量百分比。
需要注意的是,一定要调用 super.tick(),否则该怪物完全被冻结,不会产生任何交互。
让怪物在夜间燃烧
要想让怪物在夜间燃烧,则需要在每 tick 检测怪物是否处于夜间环境,如果是,则使怪物燃烧。因此,在 tick() 方法键入以下代码:
- if (world.isNight()) setOnFire(1, false);
其中 setOnFire(int, boolean) 的第一个参数为燃烧的 tick 数,由于是 1 tick 检测一次,因此我们在这里填写 1;第二个参数为是否触发 BukkitAPI 的 EntityCombustEvent 事件,为了避免事件被多次调用,这里我们填写 false。
这样,怪物就会在夜间燃烧了。
自定义怪物行为
要想自定义怪物行为,我们需要为怪物添加 PathfinderGoal,因为我们不希望保留怪物原本的行为,因此我们需要刷新怪物的 goalSelector(行为选择器) 和 targetSelector(攻击目标选择器)。他们均为 PathfinderGoalSelector 类型的对象。经过研究该对象我们可以发现,PathfinderGoal 对象被包装为 PathfinderGoalWrapped 对象后,存储于private final Set<PathfinderGoalWrapped> d 字段中,由于该字段是 private final 的,因此我们需要通过反射修改该字段。在怪物的底层构造器中键入以下代码:
- try {
- Field dField = PathfinderGoalSelector.class.getDeclaredField("d");
- dField.setAccessible(true);
- dField.set(goalSelector, Sets.newLinkedHashSet());
- dField.set(targetSelector, Sets.newLinkedHashSet());
- } catch (NoSuchFieldException | IllegalAccessException noSuchFieldException) {
- noSuchFieldException.printStackTrace();
- }
这样,我们便得到了一个没有任何行为的怪物。如果我们还需要为怪物添加行为,只需要为 goalSelector 或是 targetSelector 添加继承了 PathfinderGoal 类的对象即可。NMS 中本身就包含了大量的 PathfinderGoal,大家可自行探索。
来自群组: Server CT
是时候出个1.17的教程了,1.17的NMS变化挺大的
以及,你他喵不是发烧了吗,怎么还有空来发教程?
以及,你他喵不是发烧了吗,怎么还有空来发教程?
?发烧好了?
真好,考完就开始活跃了,贴贴
真好,考完就开始活跃了,贴贴
鬼畜畜 发表于 2021-6-16 17:56
是时候出个1.17的教程了,1.17的NMS变化挺大的
以及,你他喵不是发烧了吗,怎么还有空来发教程?
...
昨天就开始写了,今天发烧本来不写了的
执念让我把这玩意写完发上来
666666666666
神乎其技!6的飞起!
为贺兰大大打Call!!!
那,有办法实现处理mod新增的生物吗?
比如原版spigot处理暮色森林的娜迦,并且在mod客户端内弄出来
比如原版spigot处理暮色森林的娜迦,并且在mod客户端内弄出来
谢谢大佬!!!
请问,为什么没有注册,就是调用IRegistry.a()。
kqn3 发表于 2021-6-22 18:50
请问,为什么没有注册,就是调用IRegistry.a()。
自 1.14 后,不再需要注册实体类型。
看起来好强,但是根本没看懂(bushi
666666666666
1.17没有那个类了
yyeerai 发表于 2021-7-9 20:08
1.17没有那个类了
1.17 没有 NMS 了
1.17没有那个类了
如何才能保存实体?

spigot说的那个remap的调用方法,1.16也能用是吧
我刚学java不知道学到后面能不能学到这种程度
666666666666666666666666
有点6,没系统学习过java看不懂
#在这里快速回复#12312312321
讲的很好verygood
感谢楼主的帮助
此时一位瓜友路过此处cwc
感谢分享,非常好用
还行,挺好用的
大佬能讲讲1.17+的specialsource怎么用吗qwq
这讲的真不错啊。
NMS无敌无敌,我爱NMS