ProjectAres/PGM/src/main/java/tc/oc/pgm/listeners/BlockTransformListener.java

501 lines
21 KiB
Java

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().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);
}
}
}
}
}