379 lines
16 KiB
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();
|
|
}
|
|
}
|
|
|
|
}
|