501 lines
21 KiB
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().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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|