一、前言
在开发EnderDragon插件时,有用户曾提出过这样的需求:不想ban掉末影水晶物品,能否禁止玩家复活末影龙?
也就是说,不能影响末影水晶的生成,但是当玩家放置末影水晶时,阻止复活仪式的发生。
针对这个问题,我进行了一番探究。
二、探究
2.1 监听什么事件
首先,我们需要确定应该要监听什么事件。
下面这段代码实现了输出指定玩家距离5格内,发生的所有事件。
public static Plugin plugin;
@Override
public void onEnable() {
plugin = this;
saveDefaultConfig();
reloadConfig();
Reflections reflections = new Reflections("org.bukkit");
Set> events = reflections.getSubTypesOf(Event.class);
for (Class clazz : events) {
if (Event.class.isAssignableFrom(clazz) && Event.class != clazz) {
events.add(clazz);
}
}
Set blacklist = new HashSet
blacklist.forEach(it->Bukkit.getLogger().info(it));
for (Class event : events) {
String[] splits = event.getName().split("\\.");
if(blacklist.contains(splits[splits.length-1])) continue;
if (!Modifier.isAbstract(event.getModifiers())) {
try {
plugin.getServer().getPluginManager().registerEvent(event, new MyListener(), EventPriority.MONITOR, new MyEventExecutor(), plugin);
} catch (IllegalPluginAccessException e1) {
Bukkit.getLogger().warning("Can't listen to the event: " + event.getName());
}
}
}
}
static class MyListener implements Listener {
MyListener() {
}
}
static class MyEventExecutor implements EventExecutor {
public void execute(Listener listener, Event event) {
boolean tag = false;
if(event instanceof EntityEvent){
Location loc = ((EntityEvent) event).getEntity().getLocation();
Player p = Bukkit.getPlayerExact("Xanadu13");
if(p != null && p.getWorld().equals(loc.getWorld()) && loc.distance(p.getLocation())
tag = true;
}
}
else if(event instanceof BlockEvent){
Location loc = ((BlockEvent) event).getBlock().getLocation();
Player p = Bukkit.getPlayerExact("Xanadu13");
if(p != null && p.getWorld().equals(loc.getWorld()) && loc.distance(p.getLocation())
tag = true;
}
}
else{
tag = true;
}
if(tag) Bukkit.getLogger().info(event.getEventName());
}
}复制代码
为避免大量无意义事件刷屏,黑名单(blacklist)中屏蔽了一部分事件。以下是屏蔽列表:
blacklist:
- VehicleUpdateEvent
- VehicleBlockCollisionEvent
- BlockPhysicsEvents
- ChunkUnloadEvent
- ChunkLoadEvent
- PlayerMoveEvent
- GenericGameEvent
- EntitiesLoadEvent
- EntitiesUnloadEvent
- PlayerToggleSprintEvent
- PlayerStatisticIncrementEvent
- PlayerItemHeldEvent
- PlayerSwapHandItemsEvent
- WorldSaveEvent
- BlockPhysicsEvent
- ItemSpawnEvent
- UnknownCommandEvent
- EntityExhaustionEvent
- PlayerGameModeChangeEvent
- ChunkPopulateEvent
- PlayerCommandPreprocessEvent
- FoodLevelChangeEvent
- EntityAirChangeEvent复制代码
写成插件并安装后,在末地祭坛边上放置一个末影水晶,在控制台中看到如下输出:

再结合bukkit文档,可以确定答案在PlayerInteractEvent、EntityPlaceEvent、EntitySpawnEvent三者之间。
2.2 EntitySpawnEvent可行吗
结论:不可行。
如果直接监听EntitySpawnEvent,我们可以写出这样的代码:
@EventHandler
public void OnCrystalSpawn(EntitySpawnEvent e){
if(e.getEntityType() != EntityType.ENDER_CRYSTAL) return;
if(e.getEntity().getWorld().getEnvironment() != World.Environment.THE_END) return;
e.setCancelled(true);
}复制代码
但这样会导致末地不能生成任何末影水晶,这显然不是我们想要的结果。
看到这里,可能有人会想:楼主真笨,判断一下末影水晶生成的位置,离祭坛远一点就不取消事件不就行了?
但这样真的可行吗?
其实这样操作是存在漏洞的。玩家可以在远处放置末影水晶,然后用活塞将其推到祭坛附近(用水消除末影水晶底座的火后才能推动,否则会爆炸),再手持水晶右键一下基岩/黑曜石,你会发现复活仪式居然开始了。
这是因为玩家尝试放置末影水晶时会触发检测,如果满足仪式复活的条件就会直接启动。(具体逻辑会在后文分析)
2.3 EntityPlaceEvent可行吗
结论:在1.13.2及以上版本可行。
代码如下:
@EventHandler
public void OnCrystalPlaced(EntityPlaceEvent e){
if(e.getEntityType() != EntityType.ENDER_CRYSTAL) return;
if(e.getEntity().getWorld().getEnvironment() != World.Environment.THE_END) return;
e.setCancelled(true);
}复制代码
而1.13.2以下版本没有这个事件,我们应该如何在低版本监听末影水晶放置呢?
2.4 底层逻辑
Lnet/minecraft/server/v1_12_R1/ItemEndCrystal;a(Lnet/minecraft/server/v1_12_R1/EntityHuman;Lnet/minecraft/server/v1_12_R1/World;
Lnet/minecraft/server/v1_12_R1/BlockPosition;
Lnet/minecraft/server/v1_12_R1/EnumHand;
Lnet/minecraft/server/v1_12_R1/EnumDirection;
F, F, F)Lnet/minecraft/server/v1_12_R1/EnumInteractionResult;
public EnumInteractionResult a(EntityHuman var1, World var2, BlockPosition var3, EnumHand var4, EnumDirection var5, float var6, float var7, float var8) {
IBlockData var9 = var2.getType(var3);
if (var9.getBlock() != Blocks.OBSIDIAN && var9.getBlock() != Blocks.BEDROCK) {
return EnumInteractionResult.FAIL;
} else {
BlockPosition var10 = var3.up();
ItemStack var11 = var1.b(var4);
if (!var1.a(var10, var5, var11)) {
return EnumInteractionResult.FAIL;
} else {
BlockPosition var12 = var10.up();
boolean var13 = !var2.isEmpty(var10) && !var2.getType(var10).getBlock().a(var2, var10);
var13 |= !var2.isEmpty(var12) && !var2.getType(var12).getBlock().a(var2, var12);
if (var13) {
return EnumInteractionResult.FAIL;
} else {
double var14 = (double)var10.getX();
double var16 = (double)var10.getY();
double var18 = (double)var10.getZ();
List var20 = var2.getEntities((Entity)null, new AxisAlignedBB(var14, var16, var18, var14 + 1.0, var16 + 2.0, var18 + 1.0));
if (!var20.isEmpty()) {
return EnumInteractionResult.FAIL;
} else {
if (!var2.isClientSide) {
EntityEnderCrystal var21 = new EntityEnderCrystal(var2, (double)((float)var3.getX() + 0.5F), (double)(var3.getY() + 1), (double)((float)var3.getZ() + 0.5F));
var21.setShowingBottom(false);
var2.addEntity(var21);
if (var2.worldProvider instanceof WorldProviderTheEnd) {
EnderDragonBattle var22 = ((WorldProviderTheEnd)var2.worldProvider).t();
var22.e();
}
}
var11.subtract(1);
return EnumInteractionResult.SUCCESS;
}
}
}
}
}复制代码
当玩家手持末影水晶尝试交互时,判断交互是否成功就是由这个函数完成。
其中,这段代码
if (var2.worldProvider instanceof WorldProviderTheEnd) {
EnderDragonBattle var22 = ((WorldProviderTheEnd)var2.worldProvider).t();
var22.e();
}复制代码
就是本帖2.2末尾提到的“触发检测”,其内部实现如下:
public void e() {
if (this.k && this.p == null) {
BlockPosition var1 = this.o;
if (var1 == null) {
a.debug("Tried to respawn, but need to find the portal first.");
ShapeDetector.ShapeDetectorCollection var2 = this.h();
if (var2 == null) {
a.debug("Couldn't find a portal, so we made one.");
this.a(true);
} else {
a.debug("Found the exit portal & temporarily using it.");
}
var1 = this.o;
}
ArrayList var7 = Lists.newArrayList();
BlockPosition var3 = var1.up(1);
Iterator var4 = EnumDirectionLimit.HORIZONTAL.iterator();
while(var4.hasNext()) {
EnumDirection var5 = (EnumDirection)var4.next();
List var6 = this.d.a(EntityEnderCrystal.class, new AxisAlignedBB(var3.shift(var5, 2)));
if (var6.isEmpty()) {
return;
}
var7.addAll(var6);
}
a.debug("Found all crystals, respawning dragon.");
this.a((List)var7);
}
}复制代码
这个函数的大致功能就是检测当前环境是否满足复活末影龙的要求,如果满足,就启动复活仪式。至此,低版本的监听末影水晶放置的逻辑已经呼之欲出。
我们只需要监听PlayerInteractEvent,判断此次交互是否能成功放置末影水晶。如果能,就取消事件,由插件生成一个末影水晶;如果不能,就什么也不干。
在此附上一份我的代码实现:
@EventHandler(priority = EventPriority.HIGHEST)
public void OnCrystalPlaced(final PlayerInteractEvent e){
if(!Config.resist_player_respawn) return;
if(e.getAction() != Action.RIGHT_CLICK_BLOCK) return;
if(e.getMaterial() != Material.END_CRYSTAL) return;
if(e.getClickedBlock() == null) return;
Material blockType = e.getClickedBlock().getType();
if(blockType != Material.OBSIDIAN && blockType != Material.BEDROCK) return;
World world = e.getPlayer().getWorld();
if(world.getEnvironment() != World.Environment.THE_END) return;
Block block = e.getClickedBlock();
int d0 = block.getX();
int d1 = block.getY() + 1;
int d2 = block.getZ();
if(world.getBlockAt(d0, d1, d2).getType() != Material.AIR) return;
Location cen = block.getLocation().clone().add(0.5,1,0.5);
Collection list = world.getNearbyEntities(cen,0.5,1,0.5);
if(!list.isEmpty()) return;
if(e.getPlayer().getGameMode() == GameMode.ADVENTURE) return;
e.setCancelled(true);
if(e.getPlayer().getGameMode() != GameMode.CREATIVE){
ItemStack item = e.getItem();
assert item != null;
int amount = item.getAmount();
e.getItem().setAmount(amount-1);
}
EnderCrystal crystal = (EnderCrystal) world.spawnEntity(cen,EntityType.ENDER_CRYSTAL);
crystal.setShowingBottom(false);
}复制代码
三、总结
在1.13.2及以上版本,可以直接监听EntityPlaceEvent事件。在1.13.2以下版本,可以监听PlayerInteractEvent事件,并在插件内完成能否成功放置末影水晶的判断。
具体实现代码在分析过程中已经给出。
在开发EnderDragon插件时,有用户曾提出过这样的需求:不想ban掉末影水晶物品,能否禁止玩家复活末影龙?
也就是说,不能影响末影水晶的生成,但是当玩家放置末影水晶时,阻止复活仪式的发生。
针对这个问题,我进行了一番探究。
二、探究
2.1 监听什么事件
首先,我们需要确定应该要监听什么事件。
下面这段代码实现了输出指定玩家距离5格内,发生的所有事件。
public static Plugin plugin;
@Override
public void onEnable() {
plugin = this;
saveDefaultConfig();
reloadConfig();
Reflections reflections = new Reflections("org.bukkit");
Set> events = reflections.getSubTypesOf(Event.class);
for (Class clazz : events) {
if (Event.class.isAssignableFrom(clazz) && Event.class != clazz) {
events.add(clazz);
}
}
Set blacklist = new HashSet
blacklist.forEach(it->Bukkit.getLogger().info(it));
for (Class event : events) {
String[] splits = event.getName().split("\\.");
if(blacklist.contains(splits[splits.length-1])) continue;
if (!Modifier.isAbstract(event.getModifiers())) {
try {
plugin.getServer().getPluginManager().registerEvent(event, new MyListener(), EventPriority.MONITOR, new MyEventExecutor(), plugin);
} catch (IllegalPluginAccessException e1) {
Bukkit.getLogger().warning("Can't listen to the event: " + event.getName());
}
}
}
}
static class MyListener implements Listener {
MyListener() {
}
}
static class MyEventExecutor implements EventExecutor {
public void execute(Listener listener, Event event) {
boolean tag = false;
if(event instanceof EntityEvent){
Location loc = ((EntityEvent) event).getEntity().getLocation();
Player p = Bukkit.getPlayerExact("Xanadu13");
if(p != null && p.getWorld().equals(loc.getWorld()) && loc.distance(p.getLocation())
tag = true;
}
}
else if(event instanceof BlockEvent){
Location loc = ((BlockEvent) event).getBlock().getLocation();
Player p = Bukkit.getPlayerExact("Xanadu13");
if(p != null && p.getWorld().equals(loc.getWorld()) && loc.distance(p.getLocation())
tag = true;
}
}
else{
tag = true;
}
if(tag) Bukkit.getLogger().info(event.getEventName());
}
}复制代码
为避免大量无意义事件刷屏,黑名单(blacklist)中屏蔽了一部分事件。以下是屏蔽列表:
blacklist:
- VehicleUpdateEvent
- VehicleBlockCollisionEvent
- BlockPhysicsEvents
- ChunkUnloadEvent
- ChunkLoadEvent
- PlayerMoveEvent
- GenericGameEvent
- EntitiesLoadEvent
- EntitiesUnloadEvent
- PlayerToggleSprintEvent
- PlayerStatisticIncrementEvent
- PlayerItemHeldEvent
- PlayerSwapHandItemsEvent
- WorldSaveEvent
- BlockPhysicsEvent
- ItemSpawnEvent
- UnknownCommandEvent
- EntityExhaustionEvent
- PlayerGameModeChangeEvent
- ChunkPopulateEvent
- PlayerCommandPreprocessEvent
- FoodLevelChangeEvent
- EntityAirChangeEvent复制代码
写成插件并安装后,在末地祭坛边上放置一个末影水晶,在控制台中看到如下输出:

再结合bukkit文档,可以确定答案在PlayerInteractEvent、EntityPlaceEvent、EntitySpawnEvent三者之间。
2.2 EntitySpawnEvent可行吗
结论:不可行。
如果直接监听EntitySpawnEvent,我们可以写出这样的代码:
@EventHandler
public void OnCrystalSpawn(EntitySpawnEvent e){
if(e.getEntityType() != EntityType.ENDER_CRYSTAL) return;
if(e.getEntity().getWorld().getEnvironment() != World.Environment.THE_END) return;
e.setCancelled(true);
}复制代码
但这样会导致末地不能生成任何末影水晶,这显然不是我们想要的结果。
看到这里,可能有人会想:楼主真笨,判断一下末影水晶生成的位置,离祭坛远一点就不取消事件不就行了?
但这样真的可行吗?
其实这样操作是存在漏洞的。玩家可以在远处放置末影水晶,然后用活塞将其推到祭坛附近(用水消除末影水晶底座的火后才能推动,否则会爆炸),再手持水晶右键一下基岩/黑曜石,你会发现复活仪式居然开始了。
这是因为玩家尝试放置末影水晶时会触发检测,如果满足仪式复活的条件就会直接启动。(具体逻辑会在后文分析)
2.3 EntityPlaceEvent可行吗
结论:在1.13.2及以上版本可行。
代码如下:
@EventHandler
public void OnCrystalPlaced(EntityPlaceEvent e){
if(e.getEntityType() != EntityType.ENDER_CRYSTAL) return;
if(e.getEntity().getWorld().getEnvironment() != World.Environment.THE_END) return;
e.setCancelled(true);
}复制代码
而1.13.2以下版本没有这个事件,我们应该如何在低版本监听末影水晶放置呢?
2.4 底层逻辑
Lnet/minecraft/server/v1_12_R1/ItemEndCrystal;a(Lnet/minecraft/server/v1_12_R1/EntityHuman;Lnet/minecraft/server/v1_12_R1/World;
Lnet/minecraft/server/v1_12_R1/BlockPosition;
Lnet/minecraft/server/v1_12_R1/EnumHand;
Lnet/minecraft/server/v1_12_R1/EnumDirection;
F, F, F)Lnet/minecraft/server/v1_12_R1/EnumInteractionResult;
public EnumInteractionResult a(EntityHuman var1, World var2, BlockPosition var3, EnumHand var4, EnumDirection var5, float var6, float var7, float var8) {
IBlockData var9 = var2.getType(var3);
if (var9.getBlock() != Blocks.OBSIDIAN && var9.getBlock() != Blocks.BEDROCK) {
return EnumInteractionResult.FAIL;
} else {
BlockPosition var10 = var3.up();
ItemStack var11 = var1.b(var4);
if (!var1.a(var10, var5, var11)) {
return EnumInteractionResult.FAIL;
} else {
BlockPosition var12 = var10.up();
boolean var13 = !var2.isEmpty(var10) && !var2.getType(var10).getBlock().a(var2, var10);
var13 |= !var2.isEmpty(var12) && !var2.getType(var12).getBlock().a(var2, var12);
if (var13) {
return EnumInteractionResult.FAIL;
} else {
double var14 = (double)var10.getX();
double var16 = (double)var10.getY();
double var18 = (double)var10.getZ();
List var20 = var2.getEntities((Entity)null, new AxisAlignedBB(var14, var16, var18, var14 + 1.0, var16 + 2.0, var18 + 1.0));
if (!var20.isEmpty()) {
return EnumInteractionResult.FAIL;
} else {
if (!var2.isClientSide) {
EntityEnderCrystal var21 = new EntityEnderCrystal(var2, (double)((float)var3.getX() + 0.5F), (double)(var3.getY() + 1), (double)((float)var3.getZ() + 0.5F));
var21.setShowingBottom(false);
var2.addEntity(var21);
if (var2.worldProvider instanceof WorldProviderTheEnd) {
EnderDragonBattle var22 = ((WorldProviderTheEnd)var2.worldProvider).t();
var22.e();
}
}
var11.subtract(1);
return EnumInteractionResult.SUCCESS;
}
}
}
}
}复制代码
当玩家手持末影水晶尝试交互时,判断交互是否成功就是由这个函数完成。
其中,这段代码
if (var2.worldProvider instanceof WorldProviderTheEnd) {
EnderDragonBattle var22 = ((WorldProviderTheEnd)var2.worldProvider).t();
var22.e();
}复制代码
就是本帖2.2末尾提到的“触发检测”,其内部实现如下:
public void e() {
if (this.k && this.p == null) {
BlockPosition var1 = this.o;
if (var1 == null) {
a.debug("Tried to respawn, but need to find the portal first.");
ShapeDetector.ShapeDetectorCollection var2 = this.h();
if (var2 == null) {
a.debug("Couldn't find a portal, so we made one.");
this.a(true);
} else {
a.debug("Found the exit portal & temporarily using it.");
}
var1 = this.o;
}
ArrayList var7 = Lists.newArrayList();
BlockPosition var3 = var1.up(1);
Iterator var4 = EnumDirectionLimit.HORIZONTAL.iterator();
while(var4.hasNext()) {
EnumDirection var5 = (EnumDirection)var4.next();
List var6 = this.d.a(EntityEnderCrystal.class, new AxisAlignedBB(var3.shift(var5, 2)));
if (var6.isEmpty()) {
return;
}
var7.addAll(var6);
}
a.debug("Found all crystals, respawning dragon.");
this.a((List)var7);
}
}复制代码
这个函数的大致功能就是检测当前环境是否满足复活末影龙的要求,如果满足,就启动复活仪式。至此,低版本的监听末影水晶放置的逻辑已经呼之欲出。
我们只需要监听PlayerInteractEvent,判断此次交互是否能成功放置末影水晶。如果能,就取消事件,由插件生成一个末影水晶;如果不能,就什么也不干。
在此附上一份我的代码实现:
@EventHandler(priority = EventPriority.HIGHEST)
public void OnCrystalPlaced(final PlayerInteractEvent e){
if(!Config.resist_player_respawn) return;
if(e.getAction() != Action.RIGHT_CLICK_BLOCK) return;
if(e.getMaterial() != Material.END_CRYSTAL) return;
if(e.getClickedBlock() == null) return;
Material blockType = e.getClickedBlock().getType();
if(blockType != Material.OBSIDIAN && blockType != Material.BEDROCK) return;
World world = e.getPlayer().getWorld();
if(world.getEnvironment() != World.Environment.THE_END) return;
Block block = e.getClickedBlock();
int d0 = block.getX();
int d1 = block.getY() + 1;
int d2 = block.getZ();
if(world.getBlockAt(d0, d1, d2).getType() != Material.AIR) return;
Location cen = block.getLocation().clone().add(0.5,1,0.5);
Collection list = world.getNearbyEntities(cen,0.5,1,0.5);
if(!list.isEmpty()) return;
if(e.getPlayer().getGameMode() == GameMode.ADVENTURE) return;
e.setCancelled(true);
if(e.getPlayer().getGameMode() != GameMode.CREATIVE){
ItemStack item = e.getItem();
assert item != null;
int amount = item.getAmount();
e.getItem().setAmount(amount-1);
}
EnderCrystal crystal = (EnderCrystal) world.spawnEntity(cen,EntityType.ENDER_CRYSTAL);
crystal.setShowingBottom(false);
}复制代码
三、总结
在1.13.2及以上版本,可以直接监听EntityPlaceEvent事件。在1.13.2以下版本,可以监听PlayerInteractEvent事件,并在插件内完成能否成功放置末影水晶的判断。
具体实现代码在分析过程中已经给出。