package tc.oc.pgm.eventrules; import java.util.Optional; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.BlockState; import org.bukkit.entity.Entity; import org.bukkit.entity.Hanging; import org.bukkit.entity.ItemFrame; import org.bukkit.entity.LeashHitch; import org.bukkit.entity.Painting; import org.bukkit.entity.Player; import org.bukkit.event.Cancellable; import org.bukkit.event.Event; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.block.Action; import org.bukkit.event.block.BlockDamageEvent; import org.bukkit.event.block.BlockPhysicsEvent; import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityExplodeEvent; import org.bukkit.event.hanging.HangingBreakByEntityEvent; import org.bukkit.event.hanging.HangingPlaceEvent; import org.bukkit.event.player.PlayerBucketEmptyEvent; import org.bukkit.event.player.PlayerInteractEntityEvent; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.util.BlockVector; import org.bukkit.util.Vector; import tc.oc.commons.bukkit.event.CoarsePlayerMoveEvent; import tc.oc.commons.bukkit.event.GeneralizingEvent; import tc.oc.commons.bukkit.util.BlockStateUtils; import tc.oc.commons.bukkit.util.BlockUtils; import tc.oc.pgm.events.BlockTransformEvent; import tc.oc.pgm.events.ListenerScope; import tc.oc.pgm.events.ParticipantBlockTransformEvent; import tc.oc.pgm.filters.Filter.QueryResponse; import tc.oc.pgm.filters.query.BlockEventQuery; import tc.oc.pgm.filters.query.IBlockQuery; import tc.oc.pgm.filters.query.IEventQuery; import tc.oc.pgm.filters.query.IPlayerQuery; import tc.oc.pgm.filters.query.IQuery; import tc.oc.pgm.filters.query.PlayerBlockEventQuery; import tc.oc.pgm.flag.event.FlagPickupEvent; import tc.oc.pgm.kits.KitPlayerFacet; import tc.oc.pgm.map.ProtoVersions; import tc.oc.pgm.match.Match; import tc.oc.pgm.match.MatchModule; import tc.oc.pgm.match.MatchPlayer; import tc.oc.pgm.match.MatchScope; import tc.oc.pgm.match.ParticipantState; import tc.oc.pgm.utils.MatchPlayers; import static tc.oc.pgm.map.ProtoVersions.REGION_PRIORITY_VERSION; @ListenerScope(MatchScope.LOADED) public class EventRuleMatchModule extends MatchModule implements Listener { protected final EventRuleContext ruleContext; protected final boolean useRegionPriority; public EventRuleMatchModule(Match match, EventRuleContext ruleContext) { super(match); this.ruleContext = ruleContext; this.useRegionPriority = this.getMatch().getMapInfo().proto.isNoOlderThan(REGION_PRIORITY_VERSION); } protected void checkEnterLeave(Event event, MatchPlayer player, Optional<BlockVector> from, BlockVector to) { if(player == null || !player.canInteract()) return; if(this.useRegionPriority) { // We need to handle both scopes in the same loop, because the priority order can interleave them for(EventRule rule : this.ruleContext.getAll()) { if((rule.scope() == EventRuleScope.PLAYER_ENTER && rule.region().enters(from, to)) || (rule.scope() == EventRuleScope.PLAYER_LEAVE && rule.region().exits(from, to))) { if(processQuery(event, rule, player)) { break; // Stop after the first non-abstaining filter } } } } else { // To preserve legacy behavior exactly, these need to be in seperate loops for(EventRule rule : this.ruleContext.get(EventRuleScope.PLAYER_ENTER)) { if(rule.region().enters(from, to)) { processQuery(event, rule, player); } } for(EventRule rule : this.ruleContext.get(EventRuleScope.PLAYER_LEAVE)) { if(rule.region().exits(from, to)) { processQuery(event, rule, player); } } } } @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) public void checkEnterLeave(final CoarsePlayerMoveEvent event) { this.checkEnterLeave(event, match.getPlayer(event.getPlayer()), Optional.of(event.getBlockFrom().toBlockVector()), event.getBlockTo().toBlockVector()); } @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) public void checkFlagPickup(final FlagPickupEvent event) { this.checkEnterLeave(event, event.getCarrier(), Optional.empty(), event.getCarrier().getBukkit().getLocation().toBlockVector()); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void applyEffects(final CoarsePlayerMoveEvent event) { MatchPlayer player = this.match.getPlayer(event.getPlayer()); if(player == null) return; final BlockVector from = event.getBlockFrom().toBlockVector(); final BlockVector to = event.getBlockTo().toBlockVector(); for(EventRule rule : this.ruleContext.get(EventRuleScope.EFFECT)) { if(rule.velocity() == null && rule.kit() == null) continue; boolean enters = rule.region().enters(from, to); boolean exits = rule.region().exits(from, to); if(!enters && !exits) continue; if(!player.canInteract() || rule.filter() == null || rule.filter().query(player) != QueryResponse.DENY) { // Note: works on observers if(enters && rule.velocity() != null) { event.getPlayer().setVelocity(rule.velocity()); } if(rule.kit() != null && player.canInteract()) { if(enters) { player.facet(KitPlayerFacet.class).applyKit(rule.kit(), false); } if(exits && rule.lendKit()) { rule.kit().remove(player); } } } } } @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) public void checkBlockTransform(final BlockTransformEvent event) { final BlockVector pos = BlockUtils.center(event.getNewState()).toBlockVector(); final Optional<ParticipantState> actor = getActor(event); BlockState againstBlock = null; if(event.getCause() instanceof BlockPlaceEvent) { againstBlock = ((BlockPlaceEvent) event.getCause()).getBlockAgainst().getState(); } else if(event.getCause() instanceof PlayerBucketEmptyEvent) { againstBlock = ((PlayerBucketEmptyEvent) event.getCause()).getBlockClicked().getState(); } final IEventQuery breakQuery = PlayerBlockEventQuery.of(event.getOldState(), event, actor); final IEventQuery placeQuery = PlayerBlockEventQuery.of(event.getNewState(), event, actor); final IEventQuery againstQuery = againstBlock == null ? null : PlayerBlockEventQuery.of(againstBlock, event, actor); if(this.useRegionPriority) { // Note that the event may be in multiple scopes, which is why they must all be handled in the same pass ruleLoop: for(EventRule rule : this.ruleContext.getAll()) { switch(rule.scope()) { case BLOCK_BREAK: if(event.isBreak() && rule.region().contains(event.getOldState())) { if(processQuery(rule, breakQuery)) { break ruleLoop; } } break; case BLOCK_PLACE: if(event.isPlace() && rule.region().contains(event.getNewState())) { if(processQuery(rule, placeQuery)) { break ruleLoop; } } break; case BLOCK_PLACE_AGAINST: if(againstQuery != null) { if(rule.region().contains(((IBlockQuery) againstQuery).getBlock())) { if(processQuery(rule, againstQuery)) { break ruleLoop; } } } break; } } } else { // Legacy behavior if(event.isPlace()) { for(EventRule rule : this.ruleContext.get(EventRuleScope.BLOCK_PLACE)) { if(rule.region().contains(pos)) { processQuery(rule, placeQuery); } } } else { for(EventRule rule : this.ruleContext.get(EventRuleScope.BLOCK_BREAK)) { if(rule.region().contains(pos)) { processQuery(rule, breakQuery); } } } } } @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) public void checkBlockPhysics(final BlockPhysicsEvent event) { BlockEventQuery query = new BlockEventQuery(event, event.getBlock().getState()); for(EventRule rule : this.ruleContext.get(EventRuleScope.BLOCK_PHYSICS)) { if(rule.region().contains(event.getBlock()) && processQuery(rule, query)) break; } } @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) public void checkBlockDamage(final BlockDamageEvent event) { MatchPlayer player = this.match.getParticipant(event.getPlayer()); if(player == null) return; PlayerBlockEventQuery query = new PlayerBlockEventQuery(player, event, event.getBlock().getState()); for(EventRule rule : this.ruleContext.get(EventRuleScope.BLOCK_BREAK)) { if(rule.earlyWarning() && rule.region().contains(event.getBlock())) { if(processQuery(rule, query)) { if(event.isCancelled() && rule.message() != null) { player.sendWarning(rule.message(), true); } if(this.useRegionPriority) { break; } } } } } @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) public void checkUse(final PlayerInteractEvent event) { if(event.getAction() == Action.RIGHT_CLICK_BLOCK) { MatchPlayer player = this.match.getParticipant(event.getPlayer()); if(player == null) return; Block block = event.getClickedBlock(); if(block == null) return; this.handleUse(event, block.getState(), player); } } @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) public void checkHangingPlace(final HangingPlaceEvent event) { this.handleHangingPlace(event, getHangingBlockState(event.getEntity()), event.getPlayer()); } @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) public void checkHangingBreak(final HangingBreakByEntityEvent event) { this.handleHangingBreak(event, event.getEntity(), event.getRemover()); } @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) public void checkItemFrameItemRemove(EntityDamageByEntityEvent event) { // This event is fired when popping an item out of an item frame, without breaking the frame itself if(event.getEntity() instanceof ItemFrame && ((ItemFrame) event.getEntity()).getItem() != null) { this.handleHangingBreak(event, (Hanging) event.getEntity(), event.getDamager()); } } @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) public void checkItemFrameRotate(PlayerInteractEntityEvent event) { if(event.getRightClicked() instanceof ItemFrame) { ItemFrame itemFrame = (ItemFrame) event.getRightClicked(); if(itemFrame.getItem() != null) { // If frame contains an item, right-click will rotate it, which is handled as a "use" event this.handleUse(event, getHangingBlockState(itemFrame), this.match.getParticipant(event.getPlayer())); } else if(event.getPlayer().getItemInHand() != null) { // If the frame is empty and it's right clicked with an item, this will place the item in the frame, // which is handled as a "place" event, with the placed item as the block material BlockState blockState = BlockStateUtils.cloneWithMaterial(itemFrame.getLocation().getBlock(), event.getPlayer().getItemInHand().getData()); this.handleHangingPlace(event, blockState, event.getPlayer()); } } } private void handleUse(Event event, BlockState blockState, MatchPlayer player) { if(!player.canInteract()) return; PlayerBlockEventQuery query = new PlayerBlockEventQuery(player, event, blockState); for(EventRule rule : this.ruleContext.get(EventRuleScope.USE)) { if(rule.region().contains(blockState)) { if(processQuery(rule, query)) { if(query.getEvent() instanceof PlayerInteractEvent && ((PlayerInteractEvent) query.getEvent()).isCancelled()) { PlayerInteractEvent pie = (PlayerInteractEvent) query.getEvent(); pie.setCancelled(false); pie.setUseItemInHand(Event.Result.ALLOW); pie.setUseInteractedBlock(Event.Result.DENY); if(rule.message() != null) { player.sendWarning(rule.message(), false); } } if(this.useRegionPriority) { break; } } } } } private void handleHangingPlace(Event event, BlockState blockState, Entity placer) { IEventQuery query = makeBlockQuery(event, placer, blockState); for(EventRule rule : this.ruleContext.get(EventRuleScope.BLOCK_PLACE)) { if(rule.region().contains(blockState)) { if(processQuery(rule, query)) { sendCancelMessage(rule, query); if(this.useRegionPriority) break; } } } } private void handleHangingBreak(Event event, Hanging hanging, Entity breaker) { BlockState blockState = getHangingBlockState(hanging); if(blockState == null) return; IEventQuery query = makeBlockQuery(event, breaker, blockState); for(EventRule rule : this.ruleContext.get(EventRuleScope.BLOCK_BREAK)) { if(rule.region().contains(blockState)) { if(processQuery(rule, query)) { sendCancelMessage(rule, query); if(this.useRegionPriority) break; } } } } private void sendCancelMessage(EventRule rule, IEventQuery query) { if(rule.message() != null && query.getEvent() instanceof Cancellable && ((Cancellable) query.getEvent()).isCancelled() && query instanceof IPlayerQuery) { MatchPlayer player = getMatch().getPlayer(((IPlayerQuery) query).getPlayerId()); if(player != null) player.sendWarning(rule.message(), false); } } private IEventQuery makeBlockQuery(Event event, Entity entity, BlockState block) { if(entity instanceof Player) { MatchPlayer player = this.match.getPlayer((Player) entity); if(MatchPlayers.canInteract(player)) { return new PlayerBlockEventQuery(player, event, block); } } return new BlockEventQuery(event, block); } private Optional<ParticipantState> getActor(BlockTransformEvent event) { // Legacy maps assume that all TNT damage is done by "world" if(getMatch().getMapInfo().proto.isOlderThan(ProtoVersions.FILTER_OWNED_TNT) && event.getCause() instanceof EntityExplodeEvent) { return Optional.empty(); } return Optional.ofNullable(ParticipantBlockTransformEvent.getPlayerState(event)); } private static BlockState getHangingBlockState(Hanging hanging) { Block block = hanging.getLocation().getBlock(); Material type = getHangingType(hanging); return type == null ? null : BlockStateUtils.cloneWithMaterial(block, type); } private static Material getHangingType(Hanging hanging) { if(hanging instanceof Painting) { return Material.PAINTING; } else if(hanging instanceof ItemFrame) { return Material.ITEM_FRAME; } else if(hanging instanceof LeashHitch) { return Material.LEASH; } else { return null; } } protected static boolean processQuery(EventRule rule, IEventQuery query) { return processQuery(query.getEvent(), rule, query); } /** * Query the rule's filter with the given objects. * If the query is denied, cancel the event and set the deny message. * If the query is allowed, un-cancel the event. * If the query abstains, do nothing. * @return false if the query abstained, otherwise true */ protected static boolean processQuery(Event event, EventRule rule, IQuery query) { if(rule.filter() == null) { return false; } switch(rule.filter().query(query)) { case ALLOW: if(event instanceof Cancellable) { ((Cancellable) event).setCancelled(false); } return true; case DENY: if(event instanceof GeneralizingEvent) { ((GeneralizingEvent) event).setCancelled(true, rule.message()); } else if(event instanceof Cancellable) { ((Cancellable) event).setCancelled(true); } return true; default: return false; } } }