406 lines
18 KiB
Java
406 lines
18 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.blockinteract;
|
|
|
|
import org.bukkit.Location;
|
|
import org.bukkit.Material;
|
|
import org.bukkit.block.Block;
|
|
import org.bukkit.block.BlockFace;
|
|
import org.bukkit.entity.Player;
|
|
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.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.combined.CombinedConfig;
|
|
import fr.neatmonster.nocheatplus.checks.moving.MovingData;
|
|
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.BridgeHealth;
|
|
import fr.neatmonster.nocheatplus.compat.BridgeMisc;
|
|
import fr.neatmonster.nocheatplus.players.DataManager;
|
|
import fr.neatmonster.nocheatplus.players.IPlayerData;
|
|
import fr.neatmonster.nocheatplus.stats.Counters;
|
|
import fr.neatmonster.nocheatplus.utilities.CheckUtils;
|
|
import fr.neatmonster.nocheatplus.utilities.InventoryUtil;
|
|
import fr.neatmonster.nocheatplus.utilities.TickTask;
|
|
import fr.neatmonster.nocheatplus.utilities.location.LocUtil;
|
|
import fr.neatmonster.nocheatplus.utilities.map.BlockProperties;
|
|
|
|
/**
|
|
* Central location to listen to events that are relevant for the block interact checks.
|
|
*
|
|
* @see BlockInteractEvent
|
|
*/
|
|
public class BlockInteractListener extends CheckListener {
|
|
|
|
public static void debugBlockVSBlockInteract(final Player player, final CheckType checkType,
|
|
final Block block, final String prefix, final Action expectedAction,
|
|
final IPlayerData pData) {
|
|
final BlockInteractData bdata = pData.getGenericInstance(BlockInteractData.class);
|
|
final int manhattan = bdata.manhattanLastBlock(block);
|
|
String msg;
|
|
if (manhattan == Integer.MAX_VALUE) {
|
|
msg = "no last block set!";
|
|
}
|
|
else {
|
|
msg = manhattan == 0 ? "same as last block."
|
|
: ("last block differs, Manhattan: " + manhattan);
|
|
if (bdata.getLastIsCancelled()) {
|
|
msg += " / cancelled";
|
|
}
|
|
if (bdata.getLastAction() != expectedAction) {
|
|
msg += " / action=" + bdata.getLastAction();
|
|
}
|
|
}
|
|
CheckUtils.debug(player, checkType, prefix + " BlockInteract: " + msg);
|
|
}
|
|
|
|
// INSTANCE ----
|
|
|
|
/** The looking-direction check. */
|
|
private final Direction direction = addCheck(new Direction());
|
|
|
|
/** The reach-distance check. */
|
|
private final Reach reach = addCheck(new Reach());
|
|
|
|
/** Interact with visible blocks. */
|
|
private final Visible visible = addCheck(new Visible());
|
|
|
|
/** Speed of interaction. */
|
|
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 idCancelDead = counters.registerKey("cancel.dead");
|
|
private final int idCancelOffline = counters.registerKey("cancel.offline");
|
|
private final int idInteractLookCurrent = counters.registerKey("block.interact.look.current");
|
|
private final int idInteractLookFlyingFirst = counters.registerKey("block.interact.look.flying.first");
|
|
private final int idInteractLookFlyingOther = counters.registerKey("block.interact.look.flying.other");
|
|
|
|
public BlockInteractListener() {
|
|
super(CheckType.BLOCKINTERACT);
|
|
}
|
|
|
|
/**
|
|
* We listen to PlayerInteractEvent events for obvious reasons.
|
|
*
|
|
* @param event
|
|
* the event
|
|
*/
|
|
@EventHandler(ignoreCancelled = false, priority = EventPriority.LOWEST)
|
|
public void onPlayerInteract(final PlayerInteractEvent event) {
|
|
final Player player = event.getPlayer();
|
|
final IPlayerData pData = DataManager.getPlayerData(player);
|
|
final BlockInteractData data = pData.getGenericInstance(BlockInteractData.class);
|
|
data.resetLastBlock();
|
|
// Early cancel for interact events with dead players and other.
|
|
final int cancelId;
|
|
if (player.isDead() && BridgeHealth.getHealth(player) <= 0.0) { // TODO: Should be dead !?.
|
|
// Auto-soup after death.
|
|
/*
|
|
* TODO: Allow physical interact after death? Risks could be command
|
|
* blocks used etc.
|
|
*/
|
|
cancelId = idCancelDead;
|
|
}
|
|
else if (!player.isOnline()) {
|
|
cancelId = idCancelOffline;
|
|
}
|
|
else if (MovingUtil.hasScheduledPlayerSetBack(player)) {
|
|
// Might log.
|
|
cancelId = -1; // No counters yet, but do prevent.
|
|
}
|
|
else {
|
|
cancelId = Integer.MIN_VALUE;
|
|
}
|
|
if (cancelId != Integer.MIN_VALUE) {
|
|
event.setUseInteractedBlock(Result.DENY);
|
|
event.setUseItemInHand(Result.DENY);
|
|
event.setCancelled(true);
|
|
data.setPlayerInteractEventResolution(event);
|
|
if (cancelId >= 0) {
|
|
counters.addPrimaryThread(cancelId, 1);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// TODO: Re-arrange for interact spamming. (With ProtocolLib something else is in place as well.)
|
|
final Action action = event.getAction();
|
|
final Block block = event.getClickedBlock();
|
|
final int previousLastTick = data.getLastTick();
|
|
// TODO: Last block setting: better on monitor !?.
|
|
boolean blockChecks = true;
|
|
if (block == null) {
|
|
data.resetLastBlock();
|
|
blockChecks = false;
|
|
}
|
|
else {
|
|
data.setLastBlock(block, action);
|
|
}
|
|
final BlockFace face = event.getBlockFace();
|
|
final ItemStack stack;
|
|
switch(action) {
|
|
case RIGHT_CLICK_AIR:
|
|
// TODO: What else to adapt?
|
|
case LEFT_CLICK_AIR:
|
|
// TODO: What else to adapt?
|
|
case LEFT_CLICK_BLOCK:
|
|
stack = null;
|
|
break;
|
|
case RIGHT_CLICK_BLOCK:
|
|
stack = Bridge1_9.getUsedItem(player, event);
|
|
if (stack != null && stack.getType() == Material.ENDER_PEARL) {
|
|
checkEnderPearlRightClickBlock(player, block, face, event, previousLastTick, data, pData);
|
|
}
|
|
break;
|
|
default:
|
|
data.setPlayerInteractEventResolution(event);
|
|
return;
|
|
}
|
|
|
|
boolean cancelled = false;
|
|
if (event.isCancelled() && event.useInteractedBlock() != Result.ALLOW) {
|
|
if (event.useItemInHand() == Result.ALLOW) {
|
|
blockChecks = false;
|
|
// TODO: Some potential for plugin features...
|
|
}
|
|
else {
|
|
// Can't do more than prevent all (could: set to prevent on highest, if desired).
|
|
data.setPlayerInteractEventResolution(event);
|
|
return;
|
|
}
|
|
}
|
|
|
|
final BlockInteractConfig cc = pData.getGenericInstance(BlockInteractConfig.class);
|
|
boolean preventUseItem = false;
|
|
|
|
final Location loc = player.getLocation(useLoc);
|
|
final FlyingQueueHandle flyingHandle = new FlyingQueueHandle(pData);
|
|
|
|
// TODO: Always run all checks, also for !isBlock ?
|
|
|
|
// Interaction speed.
|
|
if (!cancelled && speed.isEnabled(player, pData)
|
|
&& speed.check(player, data, cc)) {
|
|
cancelled = true;
|
|
preventUseItem = true;
|
|
}
|
|
|
|
if (blockChecks) {
|
|
final double eyeHeight = MovingUtil.getEyeHeight(player);
|
|
// First the reach check.
|
|
if (!cancelled && reach.isEnabled(player, pData)
|
|
&& reach.check(player, loc, eyeHeight, block, data, cc)) {
|
|
cancelled = true;
|
|
}
|
|
|
|
// Second the direction check
|
|
if (!cancelled && direction.isEnabled(player, pData)
|
|
&& direction.check(player, loc, eyeHeight, block, flyingHandle,
|
|
data, cc, pData)) {
|
|
cancelled = true;
|
|
}
|
|
|
|
// Ray tracing for freecam use etc.
|
|
if (!cancelled && visible.isEnabled(player, pData)
|
|
&& visible.check(player, loc, eyeHeight, block, face, action, flyingHandle,
|
|
data, cc, pData)) {
|
|
cancelled = true;
|
|
}
|
|
}
|
|
|
|
// If one of the checks requested to cancel the event, do so.
|
|
if (cancelled) {
|
|
onCancelInteract(player, block, face, event, previousLastTick, preventUseItem,
|
|
data, cc, pData);
|
|
}
|
|
else {
|
|
if (flyingHandle.isFlyingQueueFetched()) {
|
|
// TODO: Update flying queue removing failed entries? At least store index for subsequent checks.
|
|
final int flyingIndex = flyingHandle.getFirstIndexWithContentIfFetched();
|
|
final Integer cId;
|
|
if (flyingIndex == 0) {
|
|
cId = idInteractLookFlyingFirst;
|
|
}
|
|
else {
|
|
cId = idInteractLookFlyingOther;
|
|
}
|
|
counters.add(cId, 1);
|
|
if (pData.isDebugActive(CheckType.BLOCKINTERACT)) {
|
|
// Log which entry was used.
|
|
logUsedFlyingPacket(player, flyingHandle, flyingIndex);
|
|
}
|
|
}
|
|
else {
|
|
counters.addPrimaryThread(idInteractLookCurrent, 1);
|
|
}
|
|
}
|
|
// Set resolution here already:
|
|
data.setPlayerInteractEventResolution(event);
|
|
useLoc.setWorld(null);
|
|
}
|
|
|
|
private void logUsedFlyingPacket(final Player player, final FlyingQueueHandle flyingHandle,
|
|
final int flyingIndex) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
private void onCancelInteract(final Player player, final Block block, final BlockFace face,
|
|
final PlayerInteractEvent event, final int previousLastTick, final boolean preventUseItem,
|
|
final BlockInteractData data, final BlockInteractConfig cc, final IPlayerData pData) {
|
|
final boolean debug = pData.isDebugActive(CheckType.BLOCKINTERACT);
|
|
if (event.isCancelled()) {
|
|
// Just prevent using the block.
|
|
event.setUseInteractedBlock(Result.DENY);
|
|
if (debug) {
|
|
genericDebug(player, block, face, event, "already cancelled: deny use block", previousLastTick, data, cc);
|
|
}
|
|
}
|
|
else {
|
|
final Result previousUseItem = event.useItemInHand();
|
|
event.setCancelled(true);
|
|
event.setUseInteractedBlock(Result.DENY);
|
|
if (
|
|
previousUseItem == Result.DENY || preventUseItem
|
|
// Allow consumption still.
|
|
|| !InventoryUtil.isConsumable(Bridge1_9.getUsedItem(player, event))
|
|
) {
|
|
event.setUseItemInHand(Result.DENY);
|
|
if (debug) {
|
|
genericDebug(player, block, face, event, "deny item use", previousLastTick, data, cc);
|
|
}
|
|
}
|
|
else {
|
|
// Consumable and not prevented otherwise.
|
|
// TODO: Ender pearl?
|
|
event.setUseItemInHand(Result.ALLOW);
|
|
if (debug) {
|
|
genericDebug(player, block, face, event, "allow edible item use", previousLastTick, data, cc);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@EventHandler(ignoreCancelled = false, priority = EventPriority.MONITOR)
|
|
public void onPlayerInteractMonitor(final PlayerInteractEvent event) {
|
|
// Set event resolution.
|
|
final Player player = event.getPlayer();
|
|
final IPlayerData pData = DataManager.getPlayerData(player);
|
|
final BlockInteractData data = pData.getGenericInstance(BlockInteractData.class);
|
|
data.setPlayerInteractEventResolution(event);
|
|
/*
|
|
* TODO: BlockDamageEvent fires before BlockInteract/MONITOR level,
|
|
* BlockBreak after (!). Thus resolution is set on LOWEST already,
|
|
* probably should be HIGHEST to account for other plugins.
|
|
*/
|
|
// Elytra boost.
|
|
/*
|
|
* TODO: Cross check with the next incoming move: has an item been used,
|
|
* is gliding, reset if necessary.
|
|
*/
|
|
//final Block block = event.getClickedBlock();
|
|
// if (data.debug) {
|
|
// debug(player, "BlockInteractResolution: cancelled=" + event.isCancelled()
|
|
// + " action=" + event.getAction() + " block=" + block + " item=" + Bridge1_9.getUsedItem(player, event));
|
|
// }
|
|
if (
|
|
(
|
|
event.getAction() == Action.RIGHT_CLICK_AIR
|
|
// Water doesn't happen, block typically is null.
|
|
// || event.getAction() == Action.RIGHT_CLICK_BLOCK
|
|
// && block != null && BlockProperties.isLiquid(block.getType())
|
|
// TODO: web ?
|
|
)
|
|
&& event.isCancelled() && event.useItemInHand() != Result.DENY) {
|
|
final ItemStack stack = Bridge1_9.getUsedItem(player, event);
|
|
if (stack != null && BridgeMisc.maybeElytraBoost(player, stack.getType())) {
|
|
final int power = BridgeMisc.getFireworksPower(stack);
|
|
final MovingData mData = pData.getGenericInstance(MovingData.class);
|
|
final int ticks = Math.max((1 + power) * 20, 30);
|
|
mData.fireworksBoostDuration = ticks;
|
|
// Expiration tick: not general latency, rather a minimum margin for sudden congestion.
|
|
mData.fireworksBoostTickExpire = TickTask.getTick() + ticks;
|
|
// TODO: Invalidation mechanics: by tick/time well ?
|
|
// TODO: Implement using it in CreativeFly.
|
|
if (pData.isDebugActive(CheckType.MOVING)) {
|
|
debug(player, "Elytra boost (power " + power + "): " + stack);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void checkEnderPearlRightClickBlock(final Player player, final Block block,
|
|
final BlockFace face, final PlayerInteractEvent event,
|
|
final int previousLastTick, final BlockInteractData data,
|
|
final IPlayerData pData) {
|
|
if (block == null || !BlockProperties.isPassable(block.getType())) {
|
|
final CombinedConfig ccc = pData.getGenericInstance(CombinedConfig.class);
|
|
if (ccc.enderPearlCheck && ccc.enderPearlPreventClickBlock) {
|
|
event.setUseItemInHand(Result.DENY);
|
|
if (pData.isDebugActive(CheckType.BLOCKINTERACT)) {
|
|
final BlockInteractConfig cc = pData.getGenericInstance(BlockInteractConfig.class);
|
|
genericDebug(player, block, face, event, "click block: deny use ender pearl", previousLastTick, data, cc);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void genericDebug(final Player player, final Block block, final BlockFace face,
|
|
final PlayerInteractEvent event, final String tag, final int previousLastTick,
|
|
final BlockInteractData data, final BlockInteractConfig cc) {
|
|
final StringBuilder builder = new StringBuilder(512);
|
|
// Rate limit.
|
|
if (data.getLastTick() == previousLastTick && data.subsequentCancel > 0) {
|
|
data.rateLimitSkip ++;
|
|
return;
|
|
}
|
|
// Debug log.
|
|
builder.append("Interact cancel: " + event.isCancelled());
|
|
builder.append(" (");
|
|
builder.append(tag);
|
|
if (block == null) {
|
|
builder.append(") block: null");
|
|
}
|
|
else {
|
|
builder.append(") block: ");
|
|
builder.append(block.getWorld().getName() + "/" + LocUtil.simpleFormat(block));
|
|
builder.append(" type: " + block.getType());
|
|
builder.append(" data: " + BlockProperties.getData(block));
|
|
builder.append(" face: " + face);
|
|
}
|
|
|
|
if (data.rateLimitSkip > 0) {
|
|
builder.append(" skipped(rate-limit: " + data.rateLimitSkip);
|
|
data.rateLimitSkip = 0;
|
|
}
|
|
debug(player, builder.toString());
|
|
}
|
|
|
|
}
|