NoCheatPlus/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/blockbreak/BlockBreakListener.java

379 lines
16 KiB
Java

/*
* 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 <http://www.gnu.org/licenses/>.
*/
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();
}
}
}