Bukkit插件开发笔记

这篇文章用来记录个人在Bukkit插件开发中踩到的一些坑和其他琐碎知识…

关于gui

Bukkit Doc中介绍了一些危险操作:

由于InventoryClickEvent是通过修改物品栏的实现类来触发的,所以并非所有与物品栏相关的方法都是安全的。

下面这些属于HumanEntityInventoryView的方法不应该被处理InventoryClickEvent事件的事件处理器调用

HumanEntity.closeInventory()
HumanEntity.openInventory(Inventory)
HumanEntity.openWorkbench(Location, boolean)
HumanEntity.openEnchanting(Location, boolean)
InventoryView.close()
如果一定要调用这些方法,请使用 BukkitScheduler.runTask(Plugin, Runnable)来执行 ,这个方法将在下一个tick执行你的任务。

因为打开新界面、工作台、附魔台都需要先关闭旧的界面。所以上面那段说明说白了就是告诉你:不能在InventoryClickEvent里直接关闭玩家的界面
那么如果我们真的在InventoryClickEvent上执行这些操作又会发生什么呢?
我在某个老板定制的插件中有如下功能要实现:
每隔60秒为某个世界的所有玩家弹出一个GUI,要求玩家选中正确的物品,超时或者选错则执行某段指令。
GUI展示
这项功能较为简单,我们列出如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@EventHandler(ignoreCancelled = true)
public void onInventoryClick(InventoryClickEvent event) {
Inventory inventory = event.getClickedInventory();
if (inventory == null) {
return;
}
// 如果点击的GUI不是我们插件的界面则不操作
if (inventory.getHolder() != this) {
return;
}
// 阻止这次点击事件在Minecraft中生效
event.setCancelled(true);
HumanEntity player = event.getWhoClicked();
//如果玩家点击的是正确的物品
if (event.getCurrentItem().isSimilar(correctItem)) {
// 将玩家从checkPlayers中移除,这样我们的超时检测器就不会找他麻烦
checkPlayers.remove(player.getUniqueId());
// 关闭玩家的GUI
player.closeInventory();
} else {
// 如果玩家选错则执行某些指令
for (String s : commands) {
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), s.replace("%player%", s));
}
}
}

看看这些代码,我们应该已经在第12行中使用event.setCancelled(true);阻止了事件,玩家点击GUI时应该无法取出任何物品,实际情况也确实如此。
只是在第19行调用的player.closeInventory();触发了另外一个问题:玩家现在可以通过shift + 左键点击正确的物品来取出它。
当我的老板向我汇报这个bug的时候,我打开了测试服务器去看看到底是什么情况。
当我使用shift + 左键点击错误的物品时,除了那段指令(广播一条测试消息)被执行以外,什么都没有发生。
而当我使用shift + 左键点击正确的物品时,bug出现了:GUI正常关闭,但物品”你应该点击这个”却进入了我的背包。

因此,我们应该在InventoryClickEvent的事件监听器中关闭某个GUI时,使用BukkitScheduler.runTask(Plugin, Runnable)来执行。
在上面那段代码中,我们把第19行换成Bukkit.getScheduler().runTask(plugin, player::closeInventory);即可解决这个bug。

清除生物AI

在Bukkit 1.8 之前的 API 中是不能直接清除生物AI的,因此如果我们需要清理实体AI的话必须要手动调用NMS代码才能实现。
这里有两种方法:
1.让实体永远盯着某处
这样它就会除了盯着这里其他啥都不会做了
(虽然AI仍然存在,但是盯着某处永远是最高优先级

1
2
3
4
public static void setAI(LivingEntity entity, boolean hasAi) {
EntityLiving handle = ((CraftLivingEntity) entity).getHandle();
handle.getDataWatcher().watch(15, (byte) (hasAi ? 0 : 1));
}

2.使用NBT清除实体AI
通过给实体添加一个NBT标记而让它的AI不起作用

1
2
3
4
5
6
7
8
9
10
public static void noAI(Entity bukkitEntity) {
net.minecraft.server.v1_8_R1.Entity nmsEntity = ((CraftEntity) bukkitEntity).getHandle();
NBTTagCompound tag = nmsEntity.getNBTTag();
if (tag == null) {
tag = new NBTTagCompound();
}
nmsEntity.c(tag);
tag.setInt("NoAI", 1);
nmsEntity.f(tag);
}

对于1.8以上的版本,Bukkit API 已经在 LivingEntity 中提供了一个清除AI的方法:

1
2
wither = (Wither) location.getWorld().spawnEntity(location, EntityType.WITHER);
wither.setAI(false);

反编译查看spigot1.12.2的源码(CraftLivingEntity类)发现,Minecraft在这一个版本中已经提供了setAI的方法:

1
2
3
4
5
6
@Override
public void setAI(final boolean ai) {
if (this.getHandle() instanceof EntityInsentient) {
((EntityInsentient)this.getHandle()).setNoAI(!ai);
}
}

而再深入调查这个setNoAI方法,发现它用的也是我们的第一种方法:

1
2
3
4
public void setNoAI(final boolean flag) {
final byte b0 = this.datawatcher.get(EntityInsentient.a);
this.datawatcher.set(EntityInsentient.a, flag ? ((byte)(b0 | 0x1)) : ((byte)(b0 & 0xFFFFFFFE)));
}

关于自定义AI的一些事

详情请看这篇文章

人生不易,仓鼠断气