521 lines
21 KiB
Java
521 lines
21 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.inventory;
|
|
|
|
import org.bukkit.GameMode;
|
|
import org.bukkit.Location;
|
|
import org.bukkit.Material;
|
|
import org.bukkit.entity.ComplexEntityPart;
|
|
import org.bukkit.entity.Entity;
|
|
import org.bukkit.entity.HumanEntity;
|
|
import org.bukkit.entity.Item;
|
|
import org.bukkit.entity.LivingEntity;
|
|
import org.bukkit.entity.Player;
|
|
import org.bukkit.event.EventHandler;
|
|
import org.bukkit.event.EventPriority;
|
|
import org.bukkit.event.block.Action;
|
|
import org.bukkit.event.entity.EntityPortalEnterEvent;
|
|
import org.bukkit.event.entity.EntityShootBowEvent;
|
|
import org.bukkit.event.entity.FoodLevelChangeEvent;
|
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
|
import org.bukkit.event.inventory.InventoryEvent;
|
|
import org.bukkit.event.inventory.InventoryOpenEvent;
|
|
import org.bukkit.event.player.PlayerChangedWorldEvent;
|
|
import org.bukkit.event.player.PlayerDropItemEvent;
|
|
import org.bukkit.event.player.PlayerInteractEntityEvent;
|
|
import org.bukkit.event.player.PlayerInteractEvent;
|
|
import org.bukkit.event.player.PlayerItemHeldEvent;
|
|
import org.bukkit.event.player.PlayerPortalEvent;
|
|
import org.bukkit.event.player.PlayerTeleportEvent;
|
|
import org.bukkit.inventory.Inventory;
|
|
import org.bukkit.inventory.InventoryView;
|
|
import org.bukkit.inventory.ItemStack;
|
|
import org.bukkit.inventory.PlayerInventory;
|
|
|
|
import fr.neatmonster.nocheatplus.NCPAPIProvider;
|
|
import fr.neatmonster.nocheatplus.checks.CheckListener;
|
|
import fr.neatmonster.nocheatplus.checks.CheckType;
|
|
import fr.neatmonster.nocheatplus.checks.combined.Combined;
|
|
import fr.neatmonster.nocheatplus.checks.combined.Improbable;
|
|
import fr.neatmonster.nocheatplus.checks.moving.util.MovingUtil;
|
|
import fr.neatmonster.nocheatplus.compat.Bridge1_9;
|
|
import fr.neatmonster.nocheatplus.compat.BridgeHealth;
|
|
import fr.neatmonster.nocheatplus.components.entity.IEntityAccessVehicle;
|
|
import fr.neatmonster.nocheatplus.components.registry.event.IGenericInstanceHandle;
|
|
import fr.neatmonster.nocheatplus.components.registry.feature.JoinLeaveListener;
|
|
import fr.neatmonster.nocheatplus.players.DataManager;
|
|
import fr.neatmonster.nocheatplus.players.IPlayerData;
|
|
import fr.neatmonster.nocheatplus.stats.Counters;
|
|
import fr.neatmonster.nocheatplus.utilities.InventoryUtil;
|
|
|
|
/**
|
|
* Central location to listen to events that are relevant for the inventory checks.
|
|
*
|
|
* @see InventoryEvent
|
|
*/
|
|
public class InventoryListener extends CheckListener implements JoinLeaveListener{
|
|
|
|
/** The drop check. */
|
|
private final Drop drop = addCheck(new Drop());
|
|
|
|
/** The fast click check. */
|
|
private final FastClick fastClick = addCheck(new FastClick());
|
|
|
|
/** The instant bow check. */
|
|
private final InstantBow instantBow = addCheck(new InstantBow());
|
|
|
|
/** The instant eat check. */
|
|
private final InstantEat instantEat = addCheck(new InstantEat());
|
|
|
|
protected final Items items = addCheck(new Items());
|
|
|
|
private final Open open = addCheck(new Open());
|
|
|
|
/** 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 idIllegalItem = counters.registerKey("illegalitem");
|
|
private final int idEggOnEntity = counters.registerKey("eggonentity");
|
|
|
|
private final IGenericInstanceHandle<IEntityAccessVehicle> handleVehicles =
|
|
NCPAPIProvider.getNoCheatPlusAPI().getGenericInstanceHandle(IEntityAccessVehicle.class);
|
|
|
|
public InventoryListener() {
|
|
super(CheckType.INVENTORY);
|
|
}
|
|
|
|
/**
|
|
* We listen to EntityShootBow events for the InstantBow check.
|
|
*
|
|
* @param event
|
|
* the event
|
|
*/
|
|
@EventHandler(
|
|
ignoreCancelled = true, priority = EventPriority.LOWEST)
|
|
public void onEntityShootBow(final EntityShootBowEvent event) {
|
|
// Only if a player shot the arrow.
|
|
if (event.getEntity() instanceof Player) {
|
|
final Player player = (Player) event.getEntity();
|
|
final IPlayerData pData = DataManager.getPlayerData(player);
|
|
if (instantBow.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)) {
|
|
// No else if with this, could be cancelled due to other checks feeding, does not have actions.
|
|
event.setCancelled(true);
|
|
}
|
|
final InventoryConfig cc = pData.getGenericInstance(InventoryConfig.class);
|
|
// Still check instantBow, whatever yawrate says.
|
|
if (instantBow.check(player, event.getForce(), now)) {
|
|
// The check requested the event to be cancelled.
|
|
event.setCancelled(true);
|
|
}
|
|
else if (cc.instantBowImprobableWeight > 0.0f) {
|
|
if (cc.instantBowImprobableFeedOnly) {
|
|
Improbable.feed(player, cc.instantBowImprobableWeight, now);
|
|
}
|
|
else if (Improbable.check(player, cc.instantBowImprobableWeight,
|
|
now, "inventory.instantbow", pData)) {
|
|
// Combined fighting speed (Else if: Matter of taste, preventing extreme cascading and actions spam).
|
|
event.setCancelled(true);
|
|
}
|
|
}
|
|
useLoc.setWorld(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* We listen to FoodLevelChange events because Bukkit doesn't provide a PlayerFoodEating Event (or whatever it would
|
|
* be called).
|
|
*
|
|
* @param event
|
|
* the event
|
|
*/
|
|
@EventHandler(
|
|
ignoreCancelled = true, priority = EventPriority.LOWEST)
|
|
public void onFoodLevelChange(final FoodLevelChangeEvent event) {
|
|
// Only if a player ate food.
|
|
if (event.getEntity() instanceof Player) {
|
|
final Player player = (Player) event.getEntity();
|
|
final IPlayerData pData = DataManager.getPlayerData(player);
|
|
if (instantEat.isEnabled(player, pData)
|
|
&& instantEat.check(player, event.getFoodLevel())) {
|
|
event.setCancelled(true);
|
|
}
|
|
else if (player.isDead() && BridgeHealth.getHealth(player) <= 0.0) {
|
|
// Eat after death.
|
|
event.setCancelled(true);
|
|
counters.addPrimaryThread(idCancelDead, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* We listen to InventoryClick events for the FastClick check.
|
|
*
|
|
* @param event
|
|
* the event
|
|
*/
|
|
@EventHandler(
|
|
ignoreCancelled = true, priority = EventPriority.LOWEST)
|
|
public void onInventoryClick(final InventoryClickEvent event) {
|
|
if (!(event.getWhoClicked() instanceof Player)) {
|
|
return;
|
|
}
|
|
final long now = System.currentTimeMillis();
|
|
final HumanEntity entity = event.getWhoClicked();
|
|
if (!(entity instanceof Player)) {
|
|
return;
|
|
}
|
|
final Player player = (Player) entity;
|
|
final IPlayerData pData = DataManager.getPlayerData(player);
|
|
final InventoryData data = pData.getGenericInstance(InventoryData.class);
|
|
final int slot = event.getSlot();
|
|
if (pData.isDebugActive(checkType)) {
|
|
outputDebugInventoryClick(player, slot, event, data);
|
|
}
|
|
if (slot == InventoryView.OUTSIDE || slot < 0) {
|
|
data.lastClickTime = now;
|
|
return;
|
|
}
|
|
|
|
final ItemStack cursor = event.getCursor();
|
|
final ItemStack clicked = event.getCurrentItem();
|
|
boolean cancel = false;
|
|
// Illegal enchantment checks.
|
|
try{
|
|
if (!cancel && Items.checkIllegalEnchantments(player, clicked, pData)) {
|
|
cancel = true;
|
|
counters.addPrimaryThread(idIllegalItem, 1);
|
|
}
|
|
}
|
|
catch(final ArrayIndexOutOfBoundsException e) {} // Hotfix (CB)
|
|
try{
|
|
if (!cancel && Items.checkIllegalEnchantments(player, cursor, pData)) {
|
|
cancel = true;
|
|
counters.addPrimaryThread(idIllegalItem, 1);
|
|
}
|
|
}
|
|
catch(final ArrayIndexOutOfBoundsException e) {} // Hotfix (CB)
|
|
|
|
// Fast inventory manipulation check.
|
|
if (fastClick.isEnabled(player, pData)) {
|
|
final InventoryConfig cc = pData.getGenericInstance(InventoryConfig.class);
|
|
if (player.getGameMode() != GameMode.CREATIVE || !cc.fastClickSpareCreative) {
|
|
if (fastClick.check(player, now,
|
|
event.getView(), slot, cursor, clicked, event.isShiftClick(),
|
|
data, cc, pData)) {
|
|
// The check requested the event to be cancelled.
|
|
cancel = true;
|
|
}
|
|
// Feed the improbable.
|
|
Improbable.feed(player, 0.7f, System.currentTimeMillis());
|
|
}
|
|
}
|
|
data.lastClickTime = now;
|
|
if (cancel) {
|
|
event.setCancelled(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Debug inventory classes. Contains information about classes, to indicate
|
|
* if cross-plugin compatibility issues can be dealt with easily.
|
|
*
|
|
* @param player
|
|
* @param slot
|
|
* @param event
|
|
* @param data
|
|
*/
|
|
private void outputDebugInventoryClick(final Player player, final int slot, final InventoryClickEvent event,
|
|
final InventoryData data) {
|
|
// TODO: Check if this breaks legacy compat (disable there perhaps).
|
|
// TODO: Consider only logging where different from expected (CraftXY, more/other viewer than player).
|
|
|
|
final StringBuilder builder = new StringBuilder(512);
|
|
builder.append("Inventory click: slot: " + slot);
|
|
|
|
// Viewers.
|
|
builder.append(" , Viewers: ");
|
|
for (final HumanEntity entity : event.getViewers()) {
|
|
builder.append(entity.getName());
|
|
builder.append("(");
|
|
builder.append(entity.getClass().getName());
|
|
builder.append(")");
|
|
}
|
|
|
|
// Inventory view.
|
|
builder.append(" , View: ");
|
|
final InventoryView view = event.getView();
|
|
builder.append(view.getClass().getName());
|
|
|
|
// Bottom inventory.
|
|
addInventory(player, view.getBottomInventory(), " , Bottom: ", builder);
|
|
|
|
// Top inventory.
|
|
addInventory(player, view.getBottomInventory(), " , Top: ", builder);
|
|
|
|
// Event class.
|
|
builder.append(" , Event: ");
|
|
builder.append(event.getClass().getName());
|
|
|
|
// Log debug.
|
|
debug(player, builder.toString());
|
|
}
|
|
|
|
private void addInventory(final Player player, final Inventory inventory, final String prefix,
|
|
final StringBuilder builder) {
|
|
builder.append(prefix);
|
|
if (inventory == null) {
|
|
builder.append("(none)");
|
|
}
|
|
else {
|
|
final String name = inventory.getName();
|
|
final String title = inventory.getTitle();
|
|
final boolean same = name == null && title == null || name != null && name.equals(title);
|
|
builder.append((same ? name : (name + "/" + title)));
|
|
builder.append("/");
|
|
builder.append(inventory.getClass().getName());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* We listen to DropItem events for the Drop check.
|
|
*
|
|
* @param event
|
|
* the event
|
|
*/
|
|
@EventHandler(
|
|
ignoreCancelled = true, priority = EventPriority.LOWEST)
|
|
public void onPlayerDropItem(final PlayerDropItemEvent event) {
|
|
|
|
final Player player = event.getPlayer();
|
|
final IPlayerData pData = DataManager.getPlayerData(player);
|
|
|
|
// Illegal enchantments hotfix check.
|
|
final Item item = event.getItemDrop();
|
|
if (item != null) {
|
|
// No cancel here.
|
|
Items.checkIllegalEnchantments(player, item.getItemStack(), pData);
|
|
}
|
|
|
|
// If the player died, all their items are dropped so ignore them.
|
|
if (event.getPlayer().isDead())
|
|
return;
|
|
|
|
if (pData.isCheckActive(CheckType.INVENTORY_DROP, player)) {
|
|
if (drop.check(player)) {
|
|
// TODO: Is the following command still correct? If so, adapt actions.
|
|
/*
|
|
* Cancelling drop events is not save (in certain circumstances
|
|
* items will disappear completely). So don't
|
|
*/
|
|
// do it and kick players instead by default.
|
|
event.setCancelled(true);
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
/**
|
|
* We listen to PlayerInteract events for the InstantEat and InstantBow checks.
|
|
*
|
|
* @param event
|
|
* the event
|
|
*/
|
|
@EventHandler(ignoreCancelled = false, priority = EventPriority.LOWEST)
|
|
public final void onPlayerInteract(final PlayerInteractEvent event) {
|
|
// Only interested in right-clicks while holding an item.
|
|
if (event.getAction() != Action.RIGHT_CLICK_AIR && event.getAction() != Action.RIGHT_CLICK_BLOCK)
|
|
return;
|
|
|
|
final Player player = event.getPlayer();
|
|
final IPlayerData pData = DataManager.getPlayerData(player);
|
|
final InventoryData data = pData.getGenericInstance(InventoryData.class);
|
|
|
|
boolean resetAll = false;
|
|
|
|
if (event.hasItem()) {
|
|
final ItemStack item = event.getItem();
|
|
final Material type = item.getType();
|
|
// TODO: Get Magic values (800) from the config.
|
|
// TODO: Cancelled / deny use item -> reset all?
|
|
if (type == Material.BOW) {
|
|
final long now = System.currentTimeMillis();
|
|
// It was a bow, the player starts to pull the string, remember this time.
|
|
data.instantBowInteract = (data.instantBowInteract > 0 && now - data.instantBowInteract < 800)
|
|
? Math.min(System.currentTimeMillis(), data.instantBowInteract) : System.currentTimeMillis();
|
|
}
|
|
else if (InventoryUtil.isConsumable(type)) {
|
|
final long now = System.currentTimeMillis();
|
|
// It was food, the player starts to eat some food, remember this time and the type of food.
|
|
data.instantEatFood = type;
|
|
data.instantEatInteract = (data.instantEatInteract > 0 && now - data.instantEatInteract < 800)
|
|
? Math.min(System.currentTimeMillis(), data.instantEatInteract) : System.currentTimeMillis();
|
|
data.instantBowInteract = 0; // Who's monitoring this indentation code?
|
|
} else resetAll = true;
|
|
|
|
// Illegal enchantments hotfix check.
|
|
if (Items.checkIllegalEnchantments(player, item, pData)) {
|
|
event.setCancelled(true);
|
|
counters.addPrimaryThread(idIllegalItem, 1);
|
|
}
|
|
}
|
|
else {
|
|
resetAll = true;
|
|
}
|
|
|
|
if (resetAll) {
|
|
// Nothing that we are interested in, reset data.
|
|
if (pData.isDebugActive(CheckType.INVENTORY_INSTANTEAT)
|
|
&& data.instantEatFood != null) {
|
|
debug(player, "PlayerInteractEvent, reset fastconsume (legacy: instanteat).");
|
|
}
|
|
data.instantBowInteract = 0;
|
|
data.instantEatInteract = 0;
|
|
data.instantEatFood = null;
|
|
}
|
|
}
|
|
|
|
@EventHandler(ignoreCancelled = false, priority = EventPriority.LOWEST)
|
|
public final void onPlayerInteractEntity(final PlayerInteractEntityEvent event) {
|
|
final Player player = event.getPlayer();
|
|
if (player.getGameMode() == GameMode.CREATIVE) {
|
|
return;
|
|
}
|
|
if (player.isDead() && BridgeHealth.getHealth(player) <= 0.0) {
|
|
// No zombies.
|
|
event.setCancelled(true);
|
|
counters.addPrimaryThread(idCancelDead, 1);
|
|
return;
|
|
}
|
|
else if (MovingUtil.hasScheduledPlayerSetBack(player)) {
|
|
event.setCancelled(true);
|
|
return;
|
|
}
|
|
// TODO: Activate mob-egg check only for specific server versions.
|
|
final ItemStack stack = Bridge1_9.getUsedItem(player, event);
|
|
Entity entity = event.getRightClicked();
|
|
if (stack != null && stack.getType() == Material.MONSTER_EGG
|
|
&& (entity == null || entity instanceof LivingEntity || entity instanceof ComplexEntityPart)
|
|
&& items.isEnabled(player, DataManager.getPlayerData(player))) {
|
|
event.setCancelled(true);
|
|
counters.addPrimaryThread(idEggOnEntity, 1);
|
|
return;
|
|
}
|
|
}
|
|
|
|
@EventHandler(ignoreCancelled = false, priority = EventPriority.LOWEST)
|
|
public final void onPlayerInventoryOpen(final InventoryOpenEvent event) {
|
|
// Possibly already prevented by block + entity interaction.
|
|
final HumanEntity entity = event.getPlayer();
|
|
if (entity instanceof Player) {
|
|
if (MovingUtil.hasScheduledPlayerSetBack((Player) entity)) {
|
|
event.setCancelled(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
@EventHandler(priority = EventPriority.MONITOR)
|
|
public void onItemHeldChange(final PlayerItemHeldEvent event) {
|
|
final Player player = event.getPlayer();
|
|
final IPlayerData pData = DataManager.getPlayerData(player);
|
|
final InventoryData data = pData.getGenericInstance(InventoryData.class);
|
|
if (pData.isDebugActive(checkType) && data.instantEatFood != null) {
|
|
debug(player, "PlayerItemHeldEvent, reset fastconsume (legacy: instanteat).");
|
|
}
|
|
data.instantBowInteract = 0;
|
|
data.instantEatInteract = 0;
|
|
data.instantEatFood = null;
|
|
|
|
// Illegal enchantments hotfix check.
|
|
final PlayerInventory inv = player.getInventory();
|
|
Items.checkIllegalEnchantments(player, inv.getItem(event.getNewSlot()), pData);
|
|
Items.checkIllegalEnchantments(player, inv.getItem(event.getPreviousSlot()), pData);
|
|
}
|
|
|
|
@EventHandler(priority = EventPriority.MONITOR)
|
|
public void onPlayerChangedWorld(final PlayerChangedWorldEvent event) {
|
|
open.check(event.getPlayer());
|
|
}
|
|
|
|
@EventHandler(priority = EventPriority.MONITOR)
|
|
public void onPlayerPortal(final PlayerPortalEvent event) {
|
|
// Note: ignore cancelother setting.
|
|
open.check(event.getPlayer());
|
|
}
|
|
|
|
@EventHandler(priority = EventPriority.MONITOR)
|
|
public void onEntityPortal(final EntityPortalEnterEvent event) {
|
|
// Check passengers flat for now.
|
|
final Entity entity = event.getEntity();
|
|
if (entity instanceof Player) {
|
|
open.check((Player) entity);
|
|
}
|
|
else {
|
|
for (final Entity passenger : handleVehicles.getHandle().getEntityPassengers(entity)) {
|
|
if (passenger instanceof Player) {
|
|
// Note: ignore cancelother setting.
|
|
open.check((Player) passenger);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@EventHandler(priority = EventPriority.MONITOR)
|
|
public void onPlayerTeleport(final PlayerTeleportEvent event) {
|
|
// Note: ignore cancelother setting.
|
|
open.check(event.getPlayer());
|
|
}
|
|
|
|
@Override
|
|
public void playerJoins(Player player) {
|
|
// Ignore
|
|
}
|
|
|
|
@Override
|
|
public void playerLeaves(Player player) {
|
|
open.check(player);
|
|
}
|
|
|
|
// @EventHandler(priority = EventPriority.MONITOR)
|
|
// public void onVehicleDestroy(final VehicleDestroyEvent event) {
|
|
// final Entity entity = event.getVehicle();
|
|
// if (entity instanceof InventoryHolder) { // Fail on 1.4 ?
|
|
// checkInventoryHolder((InventoryHolder) entity);
|
|
// }
|
|
// }
|
|
//
|
|
// @EventHandler(priority = EventPriority.MONITOR)
|
|
// public void onBlockBreak(final BlockBreakEvent event) {
|
|
// final Block block = event.getBlock();
|
|
// if (block == null) {
|
|
// return;
|
|
// }
|
|
// // TODO: + explosions !? + entity change block + ...
|
|
// }
|
|
//
|
|
// private void checkInventoryHolder(InventoryHolder entity) {
|
|
// // TODO Auto-generated method stub
|
|
//
|
|
// }
|
|
|
|
}
|