/* * 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.blockplace; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.block.BlockState; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.entity.Projectile; import org.bukkit.event.Event.Result; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.block.Action; import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.block.SignChangeEvent; import org.bukkit.event.entity.ProjectileLaunchEvent; import org.bukkit.event.player.PlayerAnimationEvent; import org.bukkit.event.player.PlayerInteractEvent; 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.combined.Combined; import fr.neatmonster.nocheatplus.checks.combined.CombinedConfig; import fr.neatmonster.nocheatplus.checks.combined.Improbable; import fr.neatmonster.nocheatplus.checks.moving.MovingConfig; 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.Bridge1_9; import fr.neatmonster.nocheatplus.compat.BridgeMisc; 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.InventoryUtil; import fr.neatmonster.nocheatplus.utilities.ReflectionUtil; 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 place checks. * * @see BlockPlaceEvent */ public class BlockPlaceListener extends CheckListener { private static final int p1 = 73856093; private static final int p2 = 19349663; private static final int p3 = 83492791; private static final int getHash(final int x, final int y, final int z) { return p1 * x ^ p2 * y ^ p3 * z; } public static int getCoordHash(final Block block) { return getHash(block.getX(), block.getY(), block.getZ()); } public static int getBlockPlaceHash(final Block block, final Material mat) { int hash = getCoordHash(block); if (mat != null) { hash |= mat.name().hashCode(); } hash |= block.getWorld().getName().hashCode(); return hash; } /** Against. */ private final Against against = addCheck(new Against()); /** AutoSign. */ private final AutoSign autoSign = addCheck(new AutoSign()); /** The direction check. */ private final Direction direction = addCheck(new Direction()); /** The fast place check. */ private final FastPlace fastPlace = addCheck(new FastPlace()); /** The no swing check. */ private final NoSwing noSwing = addCheck(new NoSwing()); /** The reach check. */ private final Reach reach = addCheck(new Reach()); /** The speed check. */ private final Speed speed = addCheck(new Speed()); /** For temporary use: LocUtil.clone before passing deeply, call setWorld(null) after use. */ private final Location useLoc = new Location(null, 0, 0, 0); private final Counters counters = NCPAPIProvider.getNoCheatPlusAPI().getGenericInstance(Counters.class); private final int idBoatsAnywhere = counters.registerKey("boatsanywhere"); private final int idEnderPearl = counters.registerKey("throwenderpearl"); private final Class blockMultiPlaceEvent = ReflectionUtil.getClass("org.bukkit.event.block.BlockMultiPlaceEvent"); private final boolean hasGetReplacedState = ReflectionUtil.getMethodNoArgs(BlockPlaceEvent.class, "getReplacedState", BlockState.class) != null; public BlockPlaceListener() { super(CheckType.BLOCKPLACE); } /** * We listen to BlockPlace events for obvious reasons. * * @param event * the event */ @EventHandler( ignoreCancelled = true, priority = EventPriority.LOWEST) public void onBlockPlace(final BlockPlaceEvent event) { final Block block = event.getBlockPlaced(); final Block blockAgainst = event.getBlockAgainst(); // Skip any null blocks. if (block == null || blockAgainst == null) { return; } // TODO: What if same block? // TODO: Revise material use (not block.get... ?) //final Material mat = block.getType(); final Player player = event.getPlayer(); final Material placedMat; if (hasGetReplacedState) { placedMat = event.getBlockPlaced().getType(); } else if (Bridge1_9.hasGetItemInOffHand()) { final ItemStack stack = event.getItemInHand(); placedMat = BlockProperties.isAir(stack) ? Material.AIR : stack.getType(); } else { placedMat = Bridge1_9.getItemInMainHand(player).getType(); // Safety first. } boolean cancelled = false; final IPlayerData pData = DataManager.getPlayerData(player); // TODO: Use for data + config getting etc. final BlockPlaceData data = pData.getGenericInstance(BlockPlaceData.class); final BlockPlaceConfig cc = pData.getGenericInstance(BlockPlaceConfig.class); final BlockInteractData bdata = pData.getGenericInstance(BlockInteractData.class); final int tick = TickTask.getTick(); // isInteractBlock - the block placed against is the block last interacted with. final boolean isInteractBlock = !bdata.getLastIsCancelled() && bdata.matchesLastBlock(tick, blockAgainst); int skippedRedundantChecks = 0; final boolean debug = pData.isDebugActive(CheckType.BLOCKPLACE); final boolean shouldSkipSome; if (blockMultiPlaceEvent != null && event.getClass() == blockMultiPlaceEvent) { if (placedMat == Material.BEDROCK || Bridge1_9.hasEndCrystalItem() && placedMat == Bridge1_9.END_CRYSTAL_ITEM) { shouldSkipSome = true; } else { if (debug) { debug(player, "Block place " + event.getClass().getName() + " " + placedMat); } shouldSkipSome = false; } } else { shouldSkipSome = false; } if (placedMat == Material.SIGN) { // Might move to MONITOR priority. data.autoSignPlacedTime = System.currentTimeMillis(); // Always hash as sign post for improved compatibility with Lockette etc. data.autoSignPlacedHash = getBlockPlaceHash(block, Material.SIGN); } // Don't run checks, if a set back is scheduled. if (!cancelled && pData.isPlayerSetBackScheduled()) { cancelled = true; } // Fast place check. if (!cancelled && fastPlace.isEnabled(player, pData)) { if (fastPlace.check(player, block, tick, data, cc, pData)) { cancelled = true; } else { // Feed the improbable. Improbable.feed(player, 0.5f, System.currentTimeMillis(), pData); } } // No swing check (player doesn't swing their arm when placing a lily pad). if (!cancelled && !cc.noSwingExceptions.contains(placedMat) && noSwing.isEnabled(player, pData) && noSwing.check(player, data, cc)) { // Consider skipping all insta placables or using simplified version (true or true within time frame). cancelled = true; } final FlyingQueueHandle flyingHandle; final boolean reachCheck = pData.isCheckActive(CheckType.BLOCKPLACE_REACH, player); final boolean directionCheck = pData.isCheckActive(CheckType.BLOCKPLACE_DIRECTION, player); if (reachCheck || directionCheck) { flyingHandle = new FlyingQueueHandle(pData); final Location loc = player.getLocation(useLoc); final double eyeHeight = MovingUtil.getEyeHeight(player); // Reach check (distance). if (!cancelled && !shouldSkipSome) { if (isInteractBlock && bdata.isPassedCheck(CheckType.BLOCKINTERACT_REACH)) { skippedRedundantChecks ++; } else if (reachCheck && reach.check(player, eyeHeight, block, data, cc)) { cancelled = true; } } // Direction check. if (!cancelled && !shouldSkipSome) { if (isInteractBlock && bdata.isPassedCheck(CheckType.BLOCKINTERACT_DIRECTION)) { skippedRedundantChecks ++; } else if (directionCheck && direction.check(player, loc, eyeHeight, block, flyingHandle, data, cc, pData)) { cancelled = true; } } useLoc.setWorld(null); } else { flyingHandle = null; } // Surrounding material. if (!cancelled && against.isEnabled(player, pData) && against.check(player, block, placedMat, blockAgainst, isInteractBlock, data, cc, pData)) { cancelled = true; } // If one of the checks requested to cancel the event, do so. if (cancelled) { event.setCancelled(cancelled); } else { // Debug log (only if not cancelled, to avoid spam). if (debug) { debugBlockPlace(player, placedMat, block, blockAgainst, skippedRedundantChecks, flyingHandle, pData); } } // Cleanup // Reminder(currently unused): useLoc.setWorld(null); } private void debugBlockPlace(final Player player, final Material placedMat, final Block block, final Block blockAgainst, final int skippedRedundantChecks, final FlyingQueueHandle flyingHandle, final IPlayerData pData) { debug(player, "Block place(" + placedMat + "): " + block.getX() + ", " + block.getY() + ", " + block.getZ()); BlockInteractListener.debugBlockVSBlockInteract(player, checkType, blockAgainst, "onBlockPlace(blockAgainst)", Action.RIGHT_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; } } } @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) public void onSignChange(final SignChangeEvent event) { if (event.getClass() != SignChangeEvent.class) { // Built in plugin compatibility. // TODO: Don't understand why two consecutive events editing the same block are a problem. return; } final Player player = event.getPlayer(); final Block block = event.getBlock(); final String[] lines = event.getLines(); if (block == null || lines == null || player == null) { // Somewhat defensive. return; } final IPlayerData pData = DataManager.getPlayerData(player); if (autoSign.isEnabled(player, pData) && autoSign.check(player, block, lines, pData)) { event.setCancelled(true); } } /** * 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. DataManager.getPlayerData(event.getPlayer()).getGenericInstance(BlockPlaceData.class).noSwingArmSwung = true; } /** * We listener to PlayerInteract events to prevent players from spamming the server with monster eggs. * * @param event * the event */ @EventHandler(ignoreCancelled = false, priority = EventPriority.LOWEST) public void onPlayerInteract(final PlayerInteractEvent event) { if (event.isCancelled()) { // TODO: Might run checks if (event.useInteractedBlock()) ... return; } if (event.getAction() != Action.RIGHT_CLICK_BLOCK) { return; } final Player player = event.getPlayer(); final ItemStack stack = Bridge1_9.getUsedItem(player, event); if (stack == null) { return; } final IPlayerData pData = DataManager.getPlayerData(player); final BlockPlaceConfig cc = pData.getGenericInstance(BlockPlaceConfig.class); final Material type = stack.getType(); if (InventoryUtil.isBoat(type)) { if (cc.preventBoatsAnywhere) { // TODO: Alter config (activation, allow on top of ground). // TODO: Version/plugin specific alteration for 'default'. checkBoatsAnywhere(player, event, cc, pData); } } else if (type == Material.MONSTER_EGG) { // Check blockplace.speed. if (speed.isEnabled(player, pData) && speed.check(player, cc, pData)) { // If the check was positive, cancel the event. event.setCancelled(true); } } } private void checkBoatsAnywhere(final Player player, final PlayerInteractEvent event, final BlockPlaceConfig cc, final IPlayerData pData) { // Check boats-anywhere. final Block block = event.getClickedBlock(); final Material mat = block.getType(); // TODO: allow lava ? if (BlockProperties.isWater(mat)) { return; } // TODO: Shouldn't this be the opposite face? final BlockFace blockFace = event.getBlockFace(); final Block relBlock = block.getRelative(blockFace); final Material relMat = relBlock.getType(); // TODO: Placing inside of water, but not "against" ? if (BlockProperties.isWater(relMat)) { return; } // TODO: Add a check type for exemption? if (!pData.hasPermission(Permissions.BLOCKPLACE_BOATSANYWHERE, player)) { final Result previousUseBlock = event.useInteractedBlock(); event.setCancelled(true); event.setUseItemInHand(Result.DENY); event.setUseInteractedBlock(previousUseBlock == Result.DEFAULT ? Result.ALLOW : previousUseBlock); counters.addPrimaryThread(idBoatsAnywhere, 1); } } /** * We listen to ProjectileLaunch events to prevent players from launching projectiles too quickly. * * @param event * the event */ @EventHandler( ignoreCancelled = true, priority = EventPriority.LOWEST) public void onProjectileLaunch(final ProjectileLaunchEvent event) { // The shooter needs to be a player. final Projectile projectile = event.getEntity(); final Player player = BridgeMisc.getShooterPlayer(projectile); if (player == null) { return; } if (MovingUtil.hasScheduledPlayerSetBack(player)) { // TODO: Should log. event.setCancelled(true); return; } // And the projectile must be one the following: EntityType type = event.getEntityType(); switch (type) { case ENDER_PEARL: break; case ENDER_SIGNAL: break; case EGG: break; case SNOWBALL: break; case THROWN_EXP_BOTTLE: break; case SPLASH_POTION: break; default: return; } // Do the actual check... final IPlayerData pData = DataManager.getPlayerData(player); final BlockPlaceConfig cc = pData.getGenericInstance(BlockPlaceConfig.class); boolean cancel = false; if (speed.isEnabled(player, pData)) { final long now = System.currentTimeMillis(); final Location loc = player.getLocation(useLoc); if (Combined.checkYawRate(player, loc.getYaw(), now, loc.getWorld().getName(), pData)) { // Yawrate (checked extra). cancel = true; } if (speed.check(player, cc, pData)) { // If the check was positive, cancel the event. cancel = true; } else if (Improbable.check(player, 0.6f, now, "blockplace.speed", pData)) { // Combined fighting speed. cancel = true; } } // Ender pearl glitch (ab-) use. if (!cancel && type == EntityType.ENDER_PEARL) { if (!pData.getGenericInstance(CombinedConfig.class).enderPearlCheck) { // Do nothing ! // TODO: Might have further flags? } else if (!BlockProperties.isPassable(projectile.getLocation(useLoc))) { // Launch into a block. // TODO: This might be a general check later. cancel = true; } else { if (!BlockProperties.isPassable(player.getEyeLocation(), projectile.getLocation(useLoc))) { // (Spare a useLoc2, for this is seldom rather.) // Something between player // TODO: This might be a general check later. cancel = true; } else { final Material mat = player.getLocation(useLoc).getBlock().getType(); final long flags = BlockProperties.F_CLIMBABLE | BlockProperties.F_LIQUID | BlockProperties.F_IGN_PASSABLE; if (!BlockProperties.isAir(mat) && (BlockProperties.getBlockFlags(mat) & flags) == 0 && !mcAccess.getHandle().hasGravity(mat)) { // Still fails on piston traps etc. if (!BlockProperties.isPassable(player.getLocation(), projectile.getLocation()) && !BlockProperties.isOnGroundOrResetCond(player, player.getLocation(), pData.getGenericInstance(MovingConfig.class).yOnGround)) { cancel = true; } } } } if (cancel) { counters.addPrimaryThread(idEnderPearl, 1); } } // Cancelled ? if (cancel) { event.setCancelled(true); } // Cleanup. useLoc.setWorld(null); } }