桃源村服主
一、前言

在开发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事件,并在插件内完成能否成功放置末影水晶的判断。
具体实现代码在分析过程中已经给出。

第一页 上一页 下一页 最后一页