package tc.oc.pgm.listeners; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Logger; import javax.annotation.Nullable; import javax.inject.Inject; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ListMultimap; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.block.BlockState; import org.bukkit.entity.Arrow; import org.bukkit.entity.Player; import org.bukkit.entity.TNTPrimed; import org.bukkit.event.EntityAction; import org.bukkit.event.Event; import org.bukkit.event.EventBus; import org.bukkit.event.EventException; import org.bukkit.event.EventHandlerMeta; import org.bukkit.event.EventPriority; import org.bukkit.event.EventRegistry; import org.bukkit.event.Listener; import org.bukkit.event.block.Action; import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.event.block.BlockBurnEvent; import org.bukkit.event.block.BlockDispenseEvent; import org.bukkit.event.block.BlockFadeEvent; import org.bukkit.event.block.BlockFallEvent; import org.bukkit.event.block.BlockFormEvent; import org.bukkit.event.block.BlockFromToEvent; import org.bukkit.event.block.BlockGrowEvent; import org.bukkit.event.block.BlockIgniteEvent; import org.bukkit.event.block.BlockMultiPlaceEvent; import org.bukkit.event.block.BlockPistonEvent; import org.bukkit.event.block.BlockPistonExtendEvent; import org.bukkit.event.block.BlockPistonRetractEvent; import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.block.BlockSpreadEvent; import org.bukkit.event.entity.EntityChangeBlockEvent; import org.bukkit.event.entity.EntityExplodeEvent; import org.bukkit.event.entity.ExplosionPrimeByEntityEvent; import org.bukkit.event.entity.ExplosionPrimeEvent; import org.bukkit.event.player.PlayerBucketEmptyEvent; import org.bukkit.event.player.PlayerBucketFillEvent; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.material.PistonExtensionMaterial; import tc.oc.commons.bukkit.util.BlockStateUtils; import tc.oc.commons.bukkit.util.BukkitEvents; import tc.oc.commons.bukkit.util.Materials; import tc.oc.commons.core.inject.Proxied; import tc.oc.commons.core.logging.Loggers; import tc.oc.commons.core.plugin.PluginFacet; import tc.oc.commons.core.reflect.Methods; import tc.oc.pgm.PGM; import tc.oc.pgm.blockdrops.BlockDropsMatchModule; import tc.oc.pgm.events.BlockTransformEvent; import tc.oc.pgm.events.ParticipantBlockTransformEvent; import tc.oc.pgm.events.PlayerBlockTransformEvent; import tc.oc.pgm.match.Match; import tc.oc.pgm.match.MatchFinder; import tc.oc.pgm.match.MatchManager; import tc.oc.pgm.match.MatchPlayer; import tc.oc.pgm.match.MatchPlayerState; import tc.oc.pgm.match.ParticipantState; import tc.oc.pgm.tnt.InstantTNTPlaceEvent; import tc.oc.pgm.tracker.BlockResolver; import tc.oc.pgm.tracker.EntityResolver; public class BlockTransformListener implements PluginFacet, Listener { private static final BlockFace[] NEIGHBORS = { BlockFace.WEST, BlockFace.EAST, BlockFace.DOWN, BlockFace.UP, BlockFace.NORTH, BlockFace.SOUTH }; @Retention(RetentionPolicy.RUNTIME) @interface EventWrapper {} private final Logger logger; private final EventBus eventBus; private final EventRegistry eventRegistry; private final MatchFinder matchFinder; private final BlockResolver blockResolver; private final EntityResolver entityResolver; private final ListMultimap<Event, BlockTransformEvent> currentEvents = ArrayListMultimap.create(); @Inject BlockTransformListener(Loggers loggers, EventBus eventBus, EventRegistry eventRegistry, MatchManager matchFinder, @Proxied BlockResolver blockResolver, @Proxied EntityResolver entityResolver) { this.logger = loggers.get(getClass()); this.eventBus = eventBus; this.eventRegistry = eventRegistry; this.matchFinder = matchFinder; this.blockResolver = blockResolver; this.entityResolver = entityResolver; } @Override public void enable() { // Find all the @EventWrapper methods in this class and register them at EVERY priority level. for(final Method method : Methods.annotatedMethods(getClass(), EventWrapper.class)) { final Class<? extends Event> eventClass = method.getParameterTypes()[0].asSubclass(Event.class); for(final EventPriority priority : EventPriority.values()) { Event.register(eventRegistry.bindHandler(new EventHandlerMeta<>(eventClass, priority, false), this, (listener, event) -> { // Ignore events from non-match worlds if(matchFinder.getMatch(event) == null) return; if(!BukkitEvents.isCancelled(event)) { // At the first priority level, call the event handler method. // If it decides to generate a BlockTransformEvent, it will be stored in currentEvents. if(priority == EventPriority.LOWEST) { if(eventClass.isInstance(event)) { try { method.invoke(listener, event); } catch (InvocationTargetException ex) { throw new EventException(ex.getCause(), event); } catch (Throwable t) { throw new EventException(t, event); } } } } // Check for cached events and dispatch them at the current priority level only. // The BTE needs to be dispatched even after it's cancelled, because we DO have // listeners that depend on receiving cancelled events e.g. WoolMatchModule. for(BlockTransformEvent bte : currentEvents.get(event)) { eventBus.callEvent(bte, priority); } // After dispatching the last priority level, clean up the cached events and do post-event stuff. // This needs to happen even if the event is cancelled. if(priority == EventPriority.MONITOR) { finishCauseEvent(event); } })); } } } private void finishCauseEvent(Event causeEvent) { List<BlockTransformEvent> wrapperEvents = currentEvents.removeAll(causeEvent); for(BlockTransformEvent bte : wrapperEvents) { processCancelMessage(bte); } for(BlockTransformEvent bte : wrapperEvents) { processBlockDrops(bte); } // A few of the event handlers need to do some post-processing after the wrapper event returns. if(causeEvent instanceof EntityExplodeEvent) { finishEntityExplode((EntityExplodeEvent) causeEvent, wrapperEvents); } else if(causeEvent instanceof BlockPistonEvent) { finishPistonMove((BlockPistonEvent) causeEvent, wrapperEvents); } } private void callEvent(final BlockTransformEvent event) { logger.fine("Generated event " + event); currentEvents.put(event.getCause(), event); } private @Nullable Player getPlayerActor(Event event) { if(event instanceof EntityAction) { final EntityAction entityAction = (EntityAction) event; if(entityAction.getActor() instanceof Player) { return (Player) entityAction.getActor(); } } return null; } private BlockTransformEvent callEvent(Event cause, BlockState oldState, BlockState newState) { return callEvent(cause, oldState, newState, getPlayerActor(cause)); } private BlockTransformEvent callEvent(Event cause, BlockState oldState, BlockState newState, @Nullable Player player) { MatchPlayer matchPlayer = PGM.getMatchManager().getPlayer(player); return callEvent(cause, oldState, newState, matchPlayer == null ? null : matchPlayer.playerState()); } private BlockTransformEvent callEvent(Event cause, BlockState oldState, BlockState newState, @Nullable MatchPlayerState player) { BlockTransformEvent event; if(player == null) { event = new BlockTransformEvent(cause, oldState, newState); } else if(player instanceof ParticipantState) { event = new ParticipantBlockTransformEvent(cause, oldState, newState, (ParticipantState) player); } else { event = new PlayerBlockTransformEvent(cause, oldState, newState, player); } callEvent(event); return event; } // ------------------------ // ---- Placing blocks ---- // ------------------------ @EventWrapper public void onBlockPlace(final BlockPlaceEvent event) { if(event instanceof BlockMultiPlaceEvent) { for(BlockState oldState : ((BlockMultiPlaceEvent) event).getReplacedBlockStates()) { callEvent(event, oldState, oldState.getBlock().getState(), event.getPlayer()); } } else { callEvent(event, event.getBlockReplacedState(), event.getBlock().getState(), event.getPlayer()); } } @SuppressWarnings("deprecation") @EventWrapper public void onPlayerBucketEmpty(final PlayerBucketEmptyEvent event) { Block block = event.getBlockClicked().getRelative(event.getBlockFace()); Material contents = Materials.materialInBucket(event.getBucket()); if(contents == null) { return; } BlockState newBlock = BlockStateUtils.cloneWithMaterial(block, contents); this.callEvent(event, block.getState(), newBlock, event.getPlayer()); } @EventWrapper public void onBlockForm(final BlockGrowEvent event) { this.callEvent(new BlockTransformEvent(event, event.getBlock().getState(), event.getNewState())); } @EventWrapper public void onBlockForm(final BlockFormEvent event) { callEvent(event, event.getBlock().getState(), event.getNewState()); } @EventWrapper public void onBlockSpread(final BlockSpreadEvent event) { // This fires for: fire, grass, mycelium, mushrooms, and vines // Fire is already handled by BlockIgniteEvent if(event.getNewState().getType() != Material.FIRE) { this.callEvent(new BlockTransformEvent(event, event.getBlock().getState(), event.getNewState())); } } @SuppressWarnings("deprecation") @EventWrapper public void onBlockFromTo(BlockFromToEvent event) { if(event.getToBlock().getType() != event.getBlock().getType()) { BlockState oldState = event.getToBlock().getState(); BlockState newState = event.getToBlock().getState(); newState.setType(event.getBlock().getType()); newState.setRawData(event.getBlock().getData()); // Check for lava ownership this.callEvent(event, oldState, newState, blockResolver.getOwner(event.getBlock())); } } @EventWrapper public void onBlockIgnite(final BlockIgniteEvent event) { // Flint & steel generates a BlockPlaceEvent if(event.getCause() == BlockIgniteEvent.IgniteCause.FLINT_AND_STEEL) return; BlockState oldState = event.getBlock().getState(); BlockState newState = BlockStateUtils.cloneWithMaterial(event.getBlock(), Material.FIRE); ParticipantState igniter = null; if(event.getIgnitingEntity() != null) { // The player themselves using flint & steel, or any of // several types of owned entity starting or spreading a fire. igniter = entityResolver.getOwner(event.getIgnitingEntity()); } else if(event.getIgnitingBlock() != null) { // Fire, lava, or flint & steel in a dispenser igniter = blockResolver.getOwner(event.getIgnitingBlock()); } callEvent(event, oldState, newState, igniter); } // ------------------------- // ---- Breaking blocks ---- // ------------------------- @EventWrapper public void onBlockBreak(final BlockBreakEvent event) { BlockState state = event.getBlock().getState(); this.callEvent(event, state, BlockStateUtils.toAir(state), event.getPlayer()); } @EventWrapper public void onPlayerBucketFill(final PlayerBucketFillEvent event) { BlockState state = event.getBlockClicked().getRelative(event.getBlockFace()).getState(); this.callEvent(event, state, BlockStateUtils.toAir(state), event.getPlayer()); } @EventWrapper public void onPrimeTNT(ExplosionPrimeEvent event) { if(event.getEntity() instanceof TNTPrimed && !(event instanceof InstantTNTPlaceEvent)) { Block block = event.getEntity().getLocation().getBlock(); if(block.getType() == Material.TNT) { ParticipantState player; if(event instanceof ExplosionPrimeByEntityEvent) { player = entityResolver.getOwner(((ExplosionPrimeByEntityEvent) event).getPrimer()); } else { player = null; } callEvent(event, block.getState(), BlockStateUtils.toAir(block), player); } } } @EventWrapper public void onEntityExplode(final EntityExplodeEvent event) { ParticipantState playerState = entityResolver.getOwner(event.getEntity()); for(Block block : event.blockList()) { if(block.getType() != Material.TNT) { // Don't cancel the explosion when individual blocks are cancelled callEvent(event, block.getState(), BlockStateUtils.toAir(block), playerState).setPropagateCancel(false); } } } private void finishEntityExplode(EntityExplodeEvent causeEvent, Collection<BlockTransformEvent> wrapperEvents) { // Remove blocks from the explosion if their wrapper event was cancelled for(BlockTransformEvent wrapper : wrapperEvents) { if(wrapper.isCancelled()) { causeEvent.blockList().remove(wrapper.getOldState().getBlock()); } } } @EventWrapper public void onBlockBurn(final BlockBurnEvent event) { Match match = PGM.getMatchManager().getMatch(event.getBlock().getWorld()); if(match == null) return; BlockState oldState = event.getBlock().getState(); BlockState newState = BlockStateUtils.toAir(oldState); MatchPlayerState igniterState = null; for(BlockFace face : NEIGHBORS) { Block neighbor = oldState.getBlock().getRelative(face); if(neighbor.getType() == Material.FIRE) { igniterState = blockResolver.getOwner(neighbor); if(igniterState != null) break; } } this.callEvent(event, oldState, newState, igniterState); } @EventWrapper public void onBlockFade(final BlockFadeEvent event) { BlockState state = event.getBlock().getState(); this.callEvent(new BlockTransformEvent(event, state, BlockStateUtils.toAir(state))); } // ----------------------- // ---- Moving blocks ---- // ----------------------- private void onPistonMove(BlockPistonEvent event, List<Block> blocks, Map<Block, BlockState> newStates) { // The block list in a piston event includes only the pushed blocks, not the empty spaces they are // pushed into. We need to build our own map of the post-event block states. // Add the pushed blocks at their destination for(Block block : blocks) { Block dest = block.getRelative(event.getDirection()); newStates.put(dest, BlockStateUtils.cloneWithMaterial(dest, block.getState().getData())); } // Add air blocks where a block is leaving, and no other block is replacing it for(Block block : blocks) { if(!newStates.containsKey(block)) { newStates.put(block, BlockStateUtils.toAir(block.getState())); } } // Fire events for all changing blocks. for(BlockState newState : newStates.values()) { this.callEvent(new BlockTransformEvent(event, newState.getBlock().getState(), newState)); } } private void finishPistonMove(BlockPistonEvent causeEvent, Collection<BlockTransformEvent> wrapperEvents) { // If ANY of the pushed block events are cancelled, the piston jams and the entire causing event is cancelled. for(BlockTransformEvent bte : wrapperEvents) { if(bte.isCancelled()) { causeEvent.setCancelled(true); break; } } } @EventWrapper public void onBlockPistonExtend(final BlockPistonExtendEvent event) { Map<Block, BlockState> newStates = new HashMap<>(); // Add the arm of the piston, which will extend into the adjacent block. PistonExtensionMaterial pistonExtension = new PistonExtensionMaterial(Material.PISTON_EXTENSION); pistonExtension.setFacingDirection(event.getDirection()); BlockState pistonExtensionState = event.getBlock().getRelative(event.getDirection()).getState(); pistonExtensionState.setType(pistonExtension.getItemType()); pistonExtensionState.setData(pistonExtension); newStates.put(event.getBlock(), pistonExtensionState); this.onPistonMove(event, event.getBlocks(), newStates); } @EventWrapper public void onBlockPistonRetract(final BlockPistonRetractEvent event) { this.onPistonMove(event, event.getBlocks(), new HashMap<Block, BlockState>()); } // ----------------------------- // ---- Transforming blocks ---- // ----------------------------- @EventWrapper public void onEntityChangeBlock(final EntityChangeBlockEvent event) { // Igniting TNT with an arrow is already handled from the ExplosionPrimeEvent if(event.getEntity() instanceof Arrow && event.getBlock().getType() == Material.TNT && event.getTo() == Material.AIR) return; callEvent(event, event.getBlock().getState(), BlockStateUtils.cloneWithMaterial(event.getBlock(), event.getToData()), entityResolver.getOwner(event.getEntity())); } @EventWrapper public void onBlockTrample(final PlayerInteractEvent event) { if(event.getAction() == Action.PHYSICAL) { Block block = event.getClickedBlock(); if(block != null) { Material oldType = getTrampledType(block.getType()); if(oldType != null) { callEvent(event, BlockStateUtils.cloneWithMaterial(block, oldType), block.getState(), event.getPlayer()); } } } } @EventWrapper public void onDispenserDispense(final BlockDispenseEvent event) { if(Materials.isBucket(event.getItem())) { // Yes, the location the dispenser is facing is stored in "velocity" for some ungodly reason Block targetBlock = event.getVelocity().toLocation(event.getBlock().getWorld()).getBlock(); Material contents = Materials.materialInBucket(event.getItem()); if(Materials.isLiquid(contents) || (contents == Material.AIR && targetBlock.isLiquid())) { callEvent(event, targetBlock.getState(), BlockStateUtils.cloneWithMaterial(targetBlock, contents), blockResolver.getOwner(event.getBlock())); } } } @EventWrapper public void onBlockFall(BlockFallEvent event) { this.callEvent(new BlockTransformEvent(event, event.getBlock().getState(), BlockStateUtils.toAir(event.getBlock().getState()))); } private static Material getTrampledType(Material newType) { switch(newType) { case SOIL: return Material.DIRT; default: return null; } } // -------------------------- // ---- Event Processing ---- // -------------------------- public void processCancelMessage(final BlockTransformEvent event) { if(event instanceof PlayerBlockTransformEvent && event.isCancelled() && event.getCancelMessage() != null && event.isManual()) { ((PlayerBlockTransformEvent) event).getPlayerState().getAudience().sendWarning(event.getCancelMessage(), false); } } public void processBlockDrops(BlockTransformEvent event) { // If the event has been altered with custom block drops/replacement, // call on the BlockDropsMatchModule to handle this. We do this here // because doBlockDrops will cancel the event, and we don't want any // other listeners to think the event is cancelled when it isn't. if(event != null && !event.isCancelled() && event.getDrops() != null) { Match match = PGM.getMatchManager().getMatch(event.getWorld()); if(match != null) { BlockDropsMatchModule bdmm = match.getMatchModule(BlockDropsMatchModule.class); if(bdmm != null) { bdmm.doBlockDrops(event); } } } } }