/* * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package fr.neatmonster.nocheatplus.checks.blockbreak; import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.entity.Player; import org.bukkit.event.Cancellable; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.block.Action; import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.event.block.BlockDamageEvent; import org.bukkit.event.player.PlayerAnimationEvent; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.player.PlayerItemHeldEvent; import org.bukkit.inventory.ItemStack; import fr.neatmonster.nocheatplus.NCPAPIProvider; import fr.neatmonster.nocheatplus.checks.CheckListener; import fr.neatmonster.nocheatplus.checks.CheckType; import fr.neatmonster.nocheatplus.checks.blockinteract.BlockInteractData; import fr.neatmonster.nocheatplus.checks.blockinteract.BlockInteractListener; import fr.neatmonster.nocheatplus.checks.inventory.Items; import fr.neatmonster.nocheatplus.checks.moving.util.MovingUtil; import fr.neatmonster.nocheatplus.checks.net.FlyingQueueHandle; import fr.neatmonster.nocheatplus.checks.net.model.DataPacketFlying; import fr.neatmonster.nocheatplus.compat.AlmostBoolean; import fr.neatmonster.nocheatplus.compat.Bridge1_9; import fr.neatmonster.nocheatplus.hooks.NCPExemptionManager; import fr.neatmonster.nocheatplus.permissions.Permissions; import fr.neatmonster.nocheatplus.players.DataManager; import fr.neatmonster.nocheatplus.players.IPlayerData; import fr.neatmonster.nocheatplus.stats.Counters; import fr.neatmonster.nocheatplus.utilities.TickTask; import fr.neatmonster.nocheatplus.utilities.map.BlockProperties; /** * Central location to listen to events that are relevant for the block break checks. * * @see BlockBreakEvent */ public class BlockBreakListener extends CheckListener { /** The direction check. */ private final Direction direction = addCheck(new Direction()); /** The fast break check (per block breaking speed). */ private final FastBreak fastBreak = addCheck(new FastBreak()); /** The frequency check (number of blocks broken) */ private final Frequency frequency = addCheck(new Frequency()); /** The no swing check. */ private final NoSwing noSwing = addCheck(new NoSwing()); /** The reach check. */ private final Reach reach = addCheck(new Reach()); /** The wrong block check. */ private final WrongBlock wrongBlock = addCheck(new WrongBlock()); private AlmostBoolean isInstaBreak = AlmostBoolean.NO; private final Counters counters = NCPAPIProvider.getNoCheatPlusAPI().getGenericInstance(Counters.class); private final int idCancelDIllegalItem = counters.registerKey("illegalitem"); /** For temporary use: LocUtil.clone before passing deeply, call setWorld(null) after use. */ private final Location useLoc = new Location(null, 0, 0, 0); public BlockBreakListener(){ super(CheckType.BLOCKBREAK); } /** * We listen to BlockBreak events for obvious reasons. * * @param event * the event */ @EventHandler(ignoreCancelled = false, priority = EventPriority.LOWEST) public void onBlockBreak(final BlockBreakEvent event) { final long now = System.currentTimeMillis(); final Player player = event.getPlayer(); final IPlayerData pData = DataManager.getPlayerData(player); // Illegal enchantments hotfix check. // TODO: Legacy / encapsulate fully there. if (Items.checkIllegalEnchantmentsAllHands(player, pData)) { event.setCancelled(true); counters.addPrimaryThread(idCancelDIllegalItem, 1); } else if (MovingUtil.hasScheduledPlayerSetBack(player)) { event.setCancelled(true); } // Cancelled events only leads to resetting insta break. if (event.isCancelled()) { isInstaBreak = AlmostBoolean.NO; return; } // TODO: maybe invalidate instaBreak on some occasions. final Block block = event.getBlock(); boolean cancelled = false; // Do the actual checks, if still needed. It's a good idea to make computationally cheap checks first, because // it may save us from doing the computationally expensive checks. final BlockBreakConfig cc = pData.getGenericInstance(BlockBreakConfig.class); final BlockBreakData data = pData.getGenericInstance(BlockBreakData.class); final BlockInteractData bdata = pData.getGenericInstance(BlockInteractData.class); /* * Re-check if this is a block interacted with before. With instantly * broken blocks, this may be off by one orthogonally. */ final int tick = TickTask.getTick(); final boolean isInteractBlock = !bdata.getLastIsCancelled() && bdata.matchesLastBlock(tick, block); int skippedRedundantChecks = 0; final GameMode gameMode = player.getGameMode(); // Has the player broken a block that was not damaged before? final boolean wrongBlockEnabled = wrongBlock.isEnabled(player, pData); if (wrongBlockEnabled && wrongBlock.check(player, block, cc, data, pData, isInstaBreak)) { cancelled = true; } // Has the player broken more blocks per second than allowed? if (!cancelled && frequency.isEnabled(player, pData) && frequency.check(player, tick, cc, data, pData)) { cancelled = true; } // Has the player broken blocks faster than possible? if (!cancelled && gameMode != GameMode.CREATIVE && fastBreak.isEnabled(player, pData) && fastBreak.check(player, block, isInstaBreak, cc, data, pData)) { cancelled = true; } // Did the arm of the player move before breaking this block? if (!cancelled && noSwing.isEnabled(player, pData) && noSwing.check(player, data, pData)) { cancelled = true; } final FlyingQueueHandle flyingHandle; final boolean reachEnabled = reach.isEnabled(player, pData); final boolean directionEnabled = direction.isEnabled(player, pData); if (reachEnabled || directionEnabled) { flyingHandle = new FlyingQueueHandle(pData); final Location loc = player.getLocation(useLoc); final double eyeHeight = MovingUtil.getEyeHeight(player); // Is the block really in reach distance? if (!cancelled) { if (isInteractBlock && bdata.isPassedCheck(CheckType.BLOCKINTERACT_REACH)) { skippedRedundantChecks ++; } else if (reachEnabled && reach.check(player, eyeHeight, block, data, cc)) { cancelled = true; } } // Did the player look at the block at all? // TODO: Skip if checks were run on this block (all sorts of hashes/conditions). if (!cancelled) { if (isInteractBlock && (bdata.isPassedCheck(CheckType.BLOCKINTERACT_DIRECTION) || bdata.isPassedCheck(CheckType.BLOCKINTERACT_VISIBLE))) { skippedRedundantChecks ++; } else if (directionEnabled && direction.check(player, loc, eyeHeight, block, flyingHandle, data, cc, pData)) { cancelled = true; } } useLoc.setWorld(null); } else { flyingHandle = null; } // Destroying liquid blocks. if (!cancelled && BlockProperties.isLiquid(block.getType()) && !pData.hasPermission(Permissions.BLOCKBREAK_BREAK_LIQUID, player) && !NCPExemptionManager.isExempted(player, CheckType.BLOCKBREAK_BREAK)){ cancelled = true; } // On cancel... if (cancelled) { event.setCancelled(cancelled); // Reset damage position: // TODO: Review this (!), check if set at all !? data.clickedX = block.getX(); data.clickedY = block.getY(); data.clickedZ = block.getZ(); } else { // Invalidate last damage position: // data.clickedX = Integer.MAX_VALUE; // Debug log (only if not cancelled, to avoid spam). if (pData.isDebugActive(CheckType.BLOCKBREAK)) { debugBlockBreakResult(player, block, skippedRedundantChecks, flyingHandle, pData); } } if (isInstaBreak.decideOptimistically()) { data.wasInstaBreak = now; } else { data.wasInstaBreak = 0; } // Adjust data. data.fastBreakBreakTime = now; // data.fastBreakfirstDamage = now; isInstaBreak = AlmostBoolean.NO; } private void debugBlockBreakResult(final Player player, final Block block, final int skippedRedundantChecks, final FlyingQueueHandle flyingHandle, final IPlayerData pData) { debug(player, "Block break(" + block.getType() + "): " + block.getX() + ", " + block.getY() + ", " + block.getZ()); BlockInteractListener.debugBlockVSBlockInteract(player, checkType, block, "onBlockBreak", Action.LEFT_CLICK_BLOCK, pData); if (skippedRedundantChecks > 0) { debug(player, "Skipped redundant checks: " + skippedRedundantChecks); } if (flyingHandle != null && flyingHandle.isFlyingQueueFetched()) { final int flyingIndex = flyingHandle.getFirstIndexWithContentIfFetched(); final DataPacketFlying packet = flyingHandle.getIfFetched(flyingIndex); if (packet != null) { debug(player, "Flying packet queue used at index " + flyingIndex + ": pitch=" + packet.getPitch() + ",yaw=" + packet.getYaw()); return; } } } /** * We listen to PlayerAnimation events because it is (currently) equivalent to "player swings arm" and we want to * check if they did that between block breaks. * * @param event * the event */ @EventHandler( priority = EventPriority.MONITOR) public void onPlayerAnimation(final PlayerAnimationEvent event) { // Just set a flag to true when the arm was swung. // debug(player, "Animation"); DataManager.getPlayerData(event.getPlayer()).getGenericInstance(BlockBreakData.class).noSwingArmSwung = true; } /** * We listen to BlockInteract events to be (at least in many cases) able to distinguish between block break events * that were triggered by players actually digging and events that were artificially created by plugins. * * @param event * the event */ @EventHandler( ignoreCancelled = false, priority = EventPriority.LOWEST) public void onPlayerInteract(final PlayerInteractEvent event) { // debug(player, "Interact("+event.isCancelled()+"): " + event.getClickedBlock()); // The following is to set the "first damage time" for a block. // Return if it is not left clicking a block. // (Allows right click to be ignored.) isInstaBreak = AlmostBoolean.NO; if (event.getAction() != Action.LEFT_CLICK_BLOCK) { return; } checkBlockDamage(event.getPlayer(), event.getClickedBlock(), event); } @EventHandler(ignoreCancelled = false, priority = EventPriority.LOWEST) public void onBlockDamageLowest(final BlockDamageEvent event) { /* * TODO: Add a check type BLOCKDAMAGE_CONFIRM (no permission): * Cancel if the block doesn't match (MC 1.11.2, other ...)? */ if (MovingUtil.hasScheduledPlayerSetBack(event.getPlayer())) { event.setCancelled(true); } else if (event.getInstaBreak()) { // Indicate that this might have been set by CB/MC. // TODO: Set in BlockInteractListener !! isInstaBreak = AlmostBoolean.MAYBE; } } @EventHandler(ignoreCancelled = false, priority = EventPriority.MONITOR) public void onBlockDamage(final BlockDamageEvent event) { if (!event.isCancelled() && event.getInstaBreak()) { // Keep MAYBE. if (isInstaBreak != AlmostBoolean.MAYBE) { isInstaBreak = AlmostBoolean.YES; } } else { isInstaBreak = AlmostBoolean.NO; } checkBlockDamage(event.getPlayer(), event.getBlock(), event); } private void checkBlockDamage(final Player player, final Block block, final Cancellable event){ final long now = System.currentTimeMillis(); final IPlayerData pData = DataManager.getPlayerData(player); final BlockBreakData data = pData.getGenericInstance(BlockBreakData.class); // if (event.isCancelled()){ // // Reset the time, to avoid certain kinds of cheating. => WHICH ? // data.fastBreakfirstDamage = now; // data.clickedX = Integer.MAX_VALUE; // Should be enough to reset that one. // return; // } // Do not care about null blocks. if (block == null) { return; } final int tick = TickTask.getTick(); // Skip if already set to the same block without breaking within one tick difference. final ItemStack stack = Bridge1_9.getItemInMainHand(player); final Material tool = stack == null ? null: stack.getType(); if (data.toolChanged(tool)) { // Update. } else if (tick < data.clickedTick || now < data.fastBreakfirstDamage || now < data.fastBreakBreakTime) { // Time/tick ran backwards: Update. // Tick running backwards should not happen in the main thread unless for reload. A plugin could reset it (not intended). } else if (data.fastBreakBreakTime < data.fastBreakfirstDamage && data.clickedX == block.getX() && data.clickedZ == block.getZ() && data.clickedY == block.getY()){ // Preserve first damage time. if (tick - data.clickedTick <= 1 ) { return; } } // (Always set, the interact event only fires once: the first time.) // Only record first damage: data.setClickedBlock(block, tick, now, tool); // Compare with BlockInteract data (debug first). if (pData.isDebugActive(CheckType.BLOCKBREAK)) { BlockInteractListener.debugBlockVSBlockInteract(player, this.checkType, block, "checkBlockDamage", Action.LEFT_CLICK_BLOCK, pData); } } @EventHandler(ignoreCancelled = false, priority = EventPriority.MONITOR) public void onItemHeld(final PlayerItemHeldEvent event) { // Reset clicked block. // TODO: Not for 1.5.2 and before? final Player player = event.getPlayer(); final BlockBreakData data = DataManager.getPlayerData(player).getGenericInstance(BlockBreakData.class); if (data.toolChanged(player.getInventory().getItem(event.getNewSlot()))) { data.resetClickedBlock(); } } }