NoCheatPlus/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/moving/player/NoFall.java

400 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.moving.player;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityDamageEvent.DamageCause;
import fr.neatmonster.nocheatplus.checks.Check;
import fr.neatmonster.nocheatplus.checks.CheckType;
import fr.neatmonster.nocheatplus.checks.moving.MovingConfig;
import fr.neatmonster.nocheatplus.checks.moving.MovingData;
import fr.neatmonster.nocheatplus.checks.moving.magic.Magic;
import fr.neatmonster.nocheatplus.checks.moving.model.LocationData;
import fr.neatmonster.nocheatplus.checks.moving.model.PlayerMoveData;
import fr.neatmonster.nocheatplus.compat.BridgeHealth;
import fr.neatmonster.nocheatplus.players.IPlayerData;
import fr.neatmonster.nocheatplus.utilities.location.PlayerLocation;
/**
* A check to see if people cheat by tricking the server to not deal them fall damage.
*/
public class NoFall extends Check {
/** For temporary use: LocUtil.clone before passing deeply, call setWorld(null) after use. */
private final Location useLoc = new Location(null, 0, 0, 0);
/**
* Instantiates a new no fall check.
*/
public NoFall() {
super(CheckType.MOVING_NOFALL);
}
/**
* Calculate the damage in hearts from the given fall distance.
* @param fallDistance
* @return
*/
public static final double getDamage(final float fallDistance) {
return fallDistance - Magic.FALL_DAMAGE_DIST;
}
/**
* Deal damage if appropriate. To be used for if the player is on ground
* somehow. Contains checking for skipping conditions (getAllowFlight set +
* configured to skip).
*
* @param mcPlayer
* @param data
* @param y
* @param previousSetBackY
* The set back y from lift-off. If not present:
* Double.NEGATIVE_INFINITY.
*/
private void handleOnGround(final Player player, final double y, final double previousSetBackY,
final boolean reallyOnGround, final MovingData data, final MovingConfig cc,
final IPlayerData pData) {
// Damage to be dealt.
final float fallDist = (float) getApplicableFallHeight(player, y, previousSetBackY, data);
final double maxD = getDamage(fallDist);
if (maxD >= Magic.FALL_DAMAGE_MINIMUM) {
// Check skipping conditions.
if (cc.noFallSkipAllowFlight && player.getAllowFlight()) {
data.clearNoFallData();
data.noFallSkipAirCheck = true;
// Not resetting the fall distance here, let Minecraft or the issue tracker deal with that.
}
else {
// TODO: more effects like sounds, maybe use custom event with violation added.
if (pData.isDebugActive(type)) {
debug(player, "NoFall deal damage" + (reallyOnGround ? "" : "violation") + ": " + maxD);
}
// TODO: might not be necessary: if (mcPlayer.invulnerableTicks <= 0) [no damage event for resetting]
// TODO: Detect fake fall distance accumulation here as well.
data.noFallSkipAirCheck = true;
dealFallDamage(player, maxD);
}
}
else {
data.clearNoFallData();
player.setFallDistance(0);
}
}
/**
* Estimate the applicable fall height for the given data.
*
* @param player
* @param y
* @param previousSetBackY
* The set back y from lift-off. If not present:
* Double.NEGATIVE_INFINITY.
* @param data
* @return
*/
private static double getApplicableFallHeight(final Player player, final double y,
final double previousSetBackY, final MovingData data) {
//return getDamage(Math.max((float) (data.noFallMaxY - y), Math.max(data.noFallFallDistance, player.getFallDistance())));
final double yDistance = Math.max(data.noFallMaxY - y, data.noFallFallDistance);
if (yDistance > 0.0 && data.jumpAmplifier > 0.0
&& previousSetBackY != Double.NEGATIVE_INFINITY) {
// Fall height counts below previous set-back-y.
// TODO: Likely updating the amplifier after lift-off doesn't make sense.
// TODO: In case of velocity... skip too / calculate max exempt height?
final double correction = data.noFallMaxY - previousSetBackY;
if (correction > 0.0) {
final float effectiveDistance = (float) Math.max(0.0, yDistance - correction);
return effectiveDistance;
}
}
return yDistance;
}
public static double getApplicableFallHeight(final Player player, final double y, final MovingData data) {
return getApplicableFallHeight(player, y,
data.hasSetBack() ? data.getSetBackY() : Double.NEGATIVE_INFINITY, data);
}
/**
* Test if fall damage would be dealt accounting for the given data.
*
* @param player
* @param y
* @param previousSetBackY
* The set back y from lift-off. If not present:
* Double.NEGATIVE_INFINITY.
* @param data
* @return
*/
public boolean willDealFallDamage(final Player player, final double y,
final double previousSetBackY, final MovingData data) {
return getDamage((float) getApplicableFallHeight(player, y,
previousSetBackY, data)) - Magic.FALL_DAMAGE_DIST >= Magic.FALL_DAMAGE_MINIMUM;
}
/**
*
* @param player
* @param minY
* @param reallyOnGround
* @param data
* @param cc
*/
private void adjustFallDistance(final Player player, final double minY, final boolean reallyOnGround, final MovingData data, final MovingConfig cc) {
final float noFallFallDistance = Math.max(data.noFallFallDistance, (float) (data.noFallMaxY - minY));
if (noFallFallDistance >= Magic.FALL_DAMAGE_DIST) {
final float fallDistance = player.getFallDistance();
if (noFallFallDistance - fallDistance >= 0.5f // TODO: Why not always adjust, if greater?
|| noFallFallDistance >= Magic.FALL_DAMAGE_DIST && fallDistance < Magic.FALL_DAMAGE_DIST // Ensure damage.
) {
player.setFallDistance(noFallFallDistance);
}
}
data.clearNoFallData();
}
private void dealFallDamage(final Player player, final double damage) {
if (mcAccess.getHandle().dealFallDamageFiresAnEvent().decide()) {
// TODO: Better decideOptimistically?
mcAccess.getHandle().dealFallDamage(player, damage);
}
else {
final EntityDamageEvent event = BridgeHealth.getEntityDamageEvent(player, DamageCause.FALL, damage);
Bukkit.getPluginManager().callEvent(event);
if (!event.isCancelled()) {
// TODO: account for no damage ticks etc.
player.setLastDamageCause(event);
mcAccess.getHandle().dealFallDamage(player, BridgeHealth.getDamage(event));
}
}
// Currently resetting is done from within the damage event handler.
// TODO: MUST detect if event fired at all (...) and override, if necessary. Best probe once per class (with YES).
// data.clearNoFallData();
player.setFallDistance(0);
}
/**
* Checks a player. Expects from and to using cc.yOnGround.
*
* @param player
* the player
* @param from
* the from
* @param to
* the to
* @param previousSetBackY
* The set back y from lift-off. If not present:
* Double.NEGATIVE_INFINITY.
*/
public void check(final Player player, final PlayerLocation pFrom, final PlayerLocation pTo,
final double previousSetBackY,
final MovingData data, final MovingConfig cc, final IPlayerData pData) {
final boolean debug = pData.isDebugActive(type);
final PlayerMoveData thisMove = data.playerMoves.getCurrentMove();
final LocationData from = thisMove.from;
final LocationData to = thisMove.to;
final double fromY = from.getY();
final double toY = to.getY();
final double yDiff = toY - fromY;
final double oldNFDist = data.noFallFallDistance;
// Reset-cond is not touched by yOnGround.
// TODO: Distinguish water depth vs. fall distance ?
/*
* TODO: Account for flags instead (F_FALLDIST_ZERO and
* F_FALLDIST_HALF). Resetcond as trigger: if (resetFrom) { ...
*/
// TODO: Also handle from and to independently (rather fire twice than wait for next time).
final boolean fromReset = from.resetCond;
final boolean toReset = to.resetCond;
final boolean fromOnGround, toOnGround;
// Adapt yOnGround if necessary (sf uses another setting).
if (yDiff < 0 && cc.yOnGround < cc.noFallyOnGround) {
// In fact this is somewhat heuristic, but it seems to work well.
// Missing on-ground seems to happen with running down pyramids rather.
// TODO: Should be obsolete.
adjustYonGround(pFrom, pTo , cc.noFallyOnGround);
fromOnGround = pFrom.isOnGround();
toOnGround = pTo.isOnGround();
} else {
fromOnGround = from.onGround;
toOnGround = to.onGround;
}
// TODO: early returns (...)
final double minY = Math.min(fromY, toY);
if (fromReset) {
// Just reset.
data.clearNoFallData();
// Ensure very big/strange moves don't yield violations.
if (toY - fromY <= -Magic.FALL_DAMAGE_DIST) {
data.noFallSkipAirCheck = true;
}
}
else if (fromOnGround || !toOnGround && thisMove.touchedGround) {
// Check if to deal damage (fall back damage check).
touchDown(player, minY, previousSetBackY, data, cc, pData); // Includes the current y-distance on descend!
// Ensure very big/strange moves don't yield violations.
if (toY - fromY <= -Magic.FALL_DAMAGE_DIST) {
data.noFallSkipAirCheck = true;
}
}
else if (toReset) {
// Just reset.
data.clearNoFallData();
}
else if (toOnGround) {
// Check if to deal damage.
if (yDiff < 0) {
// In this case the player has traveled further: add the difference.
data.noFallFallDistance -= yDiff;
}
touchDown(player, minY, previousSetBackY, data, cc, pData);
}
else {
// Ensure fall distance is correct, or "anyway"?
}
// Set reference y for nofall (always).
/*
* TODO: Consider setting this before handleOnGround (at least for
* resetTo). This is after dealing damage, needs to be done differently.
*/
data.noFallMaxY = Math.max(Math.max(fromY, toY), data.noFallMaxY);
// TODO: fall distance might be behind (!)
// TODO: should be the data.noFallMaxY be counted in ?
final float mcFallDistance = player.getFallDistance(); // Note: it has to be fetched here.
// SKIP: data.noFallFallDistance = Math.max(mcFallDistance, data.noFallFallDistance);
// Add y distance.
if (!toReset && !toOnGround && yDiff < 0) {
data.noFallFallDistance -= yDiff;
}
else if (cc.noFallAntiCriticals && (toReset || toOnGround || (fromReset || fromOnGround || thisMove.touchedGround) && yDiff >= 0)) {
final double max = Math.max(data.noFallFallDistance, mcFallDistance);
if (max > 0.0 && max < 0.75) { // (Ensure this does not conflict with deal-damage set to false.)
if (debug) {
debug(player, "NoFall: Reset fall distance (anticriticals): mc=" + mcFallDistance +" / nf=" + data.noFallFallDistance);
}
if (data.noFallFallDistance > 0) {
data.noFallFallDistance = 0;
}
if (mcFallDistance > 0f) {
player.setFallDistance(0f);
}
}
}
if (debug) {
debug(player, "NoFall: mc=" + mcFallDistance +" / nf=" + data.noFallFallDistance + (oldNFDist < data.noFallFallDistance ? " (+" + (data.noFallFallDistance - oldNFDist) + ")" : "") + " | ymax=" + data.noFallMaxY);
}
}
/**
* Called during check.
*
* @param player
* @param minY
* @param previousSetBackY
* The set back y from lift-off. If not present:
* Double.NEGATIVE_INFINITY.
* @param data
* @param cc
*/
private void touchDown(final Player player, final double minY, final double previousSetBackY,
final MovingData data, final MovingConfig cc, IPlayerData pData) {
if (cc.noFallDealDamage) {
handleOnGround(player, minY, previousSetBackY, true, data, cc, pData);
}
else {
adjustFallDistance(player, minY, true, data, cc);
}
}
/**
* Set yOnGround for from and to, if needed, should be obsolete.
* @param from
* @param to
* @param cc
*/
private void adjustYonGround(final PlayerLocation from, final PlayerLocation to, final double yOnGround) {
if (!from.isOnGround()) {
from.setyOnGround(yOnGround);
}
if (!to.isOnGround()) {
to.setyOnGround(yOnGround);
}
}
/**
* Quit or kick: adjust fall distance if necessary.
* @param player
*/
public void onLeave(final Player player, final MovingData data,
final IPlayerData pData) {
final float fallDistance = player.getFallDistance();
// TODO: Might also detect too high mc fall dist.
if (data.noFallFallDistance > fallDistance) {
final double playerY = player.getLocation(useLoc).getY();
useLoc.setWorld(null);
if (player.isFlying() || player.getGameMode() == GameMode.CREATIVE
|| player.getAllowFlight()
&& pData.getGenericInstance(MovingConfig.class).noFallSkipAllowFlight) {
// Forestall potential issues with flying plugins.
player.setFallDistance(0f);
data.noFallFallDistance = 0f;
data.noFallMaxY = playerY;
} else {
// Might use tolerance, might log, might use method (compare: MovingListener.onEntityDamage).
// Might consider triggering violations here as well.
final float yDiff = (float) (data.noFallMaxY - playerY);
final float maxDist = Math.max(yDiff, data.noFallFallDistance);
player.setFallDistance(maxDist);
}
}
}
/**
* This is called if a player fails a check and gets set back, to avoid using that to avoid fall damage the player might be dealt damage here.
* @param player
* @param data
*/
public void checkDamage(final Player player, final double y,
final MovingData data, final IPlayerData pData) {
final MovingConfig cc = pData.getGenericInstance(MovingConfig.class);
// Deal damage.
handleOnGround(player, y, data.hasSetBack() ? data.getSetBackY() : Double.NEGATIVE_INFINITY,
false, data, cc, pData);
}
}