NoCheatPlus/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/moving/vehicle/VehicleEnvelope.java

471 lines
19 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.vehicle;
import java.util.LinkedList;
import java.util.List;
import org.bukkit.entity.Boat;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Minecart;
import org.bukkit.entity.Pig;
import org.bukkit.entity.Player;
import fr.neatmonster.nocheatplus.actions.ParameterName;
import fr.neatmonster.nocheatplus.checks.Check;
import fr.neatmonster.nocheatplus.checks.CheckType;
import fr.neatmonster.nocheatplus.checks.ViolationData;
import fr.neatmonster.nocheatplus.checks.moving.MovingConfig;
import fr.neatmonster.nocheatplus.checks.moving.MovingData;
import fr.neatmonster.nocheatplus.checks.moving.location.setback.SetBackEntry;
import fr.neatmonster.nocheatplus.checks.moving.magic.MagicVehicle;
import fr.neatmonster.nocheatplus.checks.moving.model.PlayerMoveData;
import fr.neatmonster.nocheatplus.checks.moving.model.VehicleMoveData;
import fr.neatmonster.nocheatplus.checks.moving.model.VehicleMoveInfo;
import fr.neatmonster.nocheatplus.checks.workaround.WRPT;
import fr.neatmonster.nocheatplus.players.IPlayerData;
import fr.neatmonster.nocheatplus.utilities.ReflectionUtil;
import fr.neatmonster.nocheatplus.utilities.StringUtil;
/**
* Vehicle moving envelope check, for Minecraft 1.9 and higher.
*
* @author asofold
*
*/
public class VehicleEnvelope extends Check {
/**
* Check specific details for re-use.
*
* @author asofold
*
*/
public class CheckDetails {
public boolean canClimb;
public boolean canRails;
public boolean canJump, canStepUpBlock; // TODO: Model as heights?
public double maxAscend;
public double gravityTargetSpeed;
/** Simplified type, like BOAT, MINECART. */
public EntityType simplifiedType; // Not sure can be kept up.
public boolean checkAscendMuch;
public boolean checkDescendMuch;
/** From could be a new set back location. */
public boolean fromIsSafeMedium;
/** To could be a new set back location. */
public boolean toIsSafeMedium;
/** Interpreted differently depending on check. */
public boolean inAir;
public void reset() {
canClimb = canRails = canJump = canStepUpBlock = false;
maxAscend = 0.0;
checkAscendMuch = checkDescendMuch = true;
fromIsSafeMedium = toIsSafeMedium = inAir = false;
simplifiedType = null;
gravityTargetSpeed = MagicVehicle.boatVerticalFallTarget;
}
}
/** Tags for checks. */
private final List<String> tags = new LinkedList<String>();
/** Extra details to log on debug. */
private final List<String> debugDetails = new LinkedList<String>();
/** Details for re-use. */
private final CheckDetails checkDetails = new CheckDetails();
private final Class<?> bestHorse;
public VehicleEnvelope() {
super(CheckType.MOVING_VEHICLE_ENVELOPE);
Class<?> clazz = ReflectionUtil.getClass("org.bukkit.entity.AbstractHorse");
bestHorse = clazz == null ? ReflectionUtil.getClass("org.bukkit.entity.Horse") : clazz;
}
public SetBackEntry check(final Player player, final Entity vehicle,
final VehicleMoveData thisMove, final boolean isFake,
final MovingData data, final MovingConfig cc,
final IPlayerData pData) {
final boolean debug = pData.isDebugActive(type);
// Delegate to a sub-check.
tags.clear();
tags.add("entity." + vehicle.getType());
if (debug) {
debugDetails.clear();
data.ws.setJustUsedIds(debugDetails); // Add just used workaround ids to this list directly, for now.
}
final boolean violation = checkEntity(player, vehicle, thisMove, isFake,
data, cc, debug);
if (debug && !debugDetails.isEmpty()) {
debugDetails(player);
debugDetails.clear();
}
if (violation) {
// Add up one for now.
data.vehicleEnvelopeVL += 1.0;
final ViolationData vd = new ViolationData(this, player, data.vehicleEnvelopeVL, 1, cc.vehicleEnvelopeActions);
vd.setParameter(ParameterName.TAGS, StringUtil.join(tags, "+"));
if (executeActions(vd).willCancel()) {
return data.vehicleSetBacks.getValidSafeMediumEntry();
}
}
else {
data.vehicleEnvelopeVL *= 0.99; // Random cool down for now.
// Do not set a set back here.
}
return null;
}
private void debugDetails(final Player player) {
if (!tags.isEmpty()) {
debugDetails.add("tags:");
debugDetails.add(StringUtil.join(tags, "+"));
}
final StringBuilder builder = new StringBuilder(500);
builder.append("Details:\n");
for (final String detail : debugDetails) {
builder.append(" , ");
builder.append(detail);
}
debug(player, builder.toString());
debugDetails.clear();
}
private double getHDistCap(final EntityType type, final MovingConfig cc) {
final Double v = cc.vehicleEnvelopeHorizontalSpeedCap.get(type);
if (v == null) {
return cc.vehicleEnvelopeHorizontalSpeedCap.get(null);
}
else {
return v;
}
}
private boolean checkEntity(final Player player, final Entity vehicle,
final VehicleMoveData thisMove, final boolean isFake,
final MovingData data, final MovingConfig cc,
final boolean debug) {
boolean violation = false;
if (debug) {
debugDetails.add("inair: " + data.sfJumpPhase);
}
// Medium dependent checking.
// TODO: Try pigs on layered snow. Consider actual bounding box / lost-ground / ...
// Maximum thinkable horizontal speed.
// TODO: Further distinguish, best set in CheckDetails.
if (maxDistHorizontal(thisMove, getHDistCap(checkDetails.simplifiedType, cc))) { // Override type for now.
return true;
}
// TODO: Could limit descend by 2*maxDescend, ascend by much less.
// TODO: Split code to methods.
// TODO: Get extended liquid specs (allow confine to certain flags, here: water). Contains info if water is only flowing down, surface properties (non liquid blocks?), still water.
if (thisMove.from.inWeb) {
// TODO: Check anything?
if (debug) {
debugDetails.add("");
}
// if (thisMove.yDistance > 0.0) {
// tags.add("ascend_web");
// return true;
// }
// TODO: Enforce not ascending ?
// TODO: max speed.
}
else if (checkDetails.canClimb && thisMove.from.onClimbable) {
// TODO: Order.
checkDetails.checkAscendMuch = checkDetails.checkDescendMuch = false;
if (Math.abs(thisMove.yDistance) > MagicVehicle.climbSpeed) {
violation = true;
tags.add("climbspeed");
}
}
else if (checkDetails.canRails && thisMove.fromOnRails) {
// TODO: Might invert to trigger violation if exceeds distance (always disable a/d_much).
if (Math.abs(thisMove.yDistance) < MagicVehicle.maxRailsVertical) {
checkDetails.checkAscendMuch = checkDetails.checkDescendMuch = false;
}
}
else if (thisMove.from.inWater && thisMove.to.inWater) {
// Default in-medium move.
if (debug) {
debugDetails.add("water-water");
}
// TODO: Should still cover extreme moves here.
// Special case moving up after falling.
// TODO: Move to MagicVehicle.oddInWater
// TODO: Check past moves for falling (not yet available).
// TODO: Check if the target location somehow is the surface.
if (MagicVehicle.oddInWater(thisMove, checkDetails, data)) {
// (Assume players can't control sinking boats for now.)
checkDetails.checkDescendMuch = checkDetails.checkAscendMuch = false;
violation = false;
}
}
else if (thisMove.from.onGround && thisMove.to.onGround) {
// Default on-ground move.
// TODO: Should still cover extreme moves here.
if (checkDetails.canStepUpBlock && thisMove.yDistance > 0.0 && thisMove.yDistance <= 1.0) {
checkDetails.checkAscendMuch = false;
tags.add("step_up");
}
if (thisMove.from.onIce && thisMove.to.onIce) {
// Default on-ice move.
if (debug) {
debugDetails.add("ice-ice");
}
// TODO: Should still cover extreme moves here.
}
else {
// (TODO: actually a default on-ground move.)
if (debug) {
debugDetails.add("ground-ground");
}
}
}
else if (checkDetails.inAir) {
// In-air move.
if (checkInAir(thisMove, data, debug)) {
violation = true;
}
}
else {
// Some transition to probably handle.
if (debug) {
debugDetails.add("?-?");
}
// TODO: Lift off speed etc.
// TODO: Clearly overlaps other cases.
// TODO: Skipped vehicle move events happen here as well (...).
if (!checkDetails.toIsSafeMedium) {
// TODO: At least do something here?
}
}
// Maximum ascend speed.
if (checkDetails.checkAscendMuch && thisMove.yDistance > checkDetails.maxAscend) {
tags.add("ascend_much");
violation = true;
}
// Maximum descend speed.
if (checkDetails.checkDescendMuch && thisMove.yDistance < -MagicVehicle.maxDescend) {
// TODO: At times it looks like one move is skipped, resulting in double distance ~ -5 and at the same time 'vehicle moved too quickly'.
// TODO: Test with log this to console to see the order of things.
tags.add("descend_much");
violation = true;
}
if (!violation) {
// No violation.
// TODO: sfJumpPhase is abused for in-air move counting here.
if (checkDetails.inAir) {
data.sfJumpPhase ++;
}
else {
// Adjust set back.
if (checkDetails.toIsSafeMedium) {
data.vehicleSetBacks.setSafeMediumEntry(thisMove.to);
data.sfJumpPhase = 0;
}
else if (checkDetails.fromIsSafeMedium) {
data.vehicleSetBacks.setSafeMediumEntry(thisMove.from);
data.sfJumpPhase = 0;
}
// Reset the resetNotInAir workarounds.
data.ws.resetConditions(WRPT.G_RESET_NOTINAIR);
}
data.vehicleSetBacks.setLastMoveEntry(thisMove.to);
}
return violation;
}
/**
* Prepare checkDetails according to vehicle-specific interpretation of side
* conditions.
*
* @param vehicle
* @param moveInfo Cheating.
* @param thisMove
*/
protected void prepareCheckDetails(final Entity vehicle, final VehicleMoveInfo moveInfo, final VehicleMoveData thisMove) {
checkDetails.reset();
// TODO: These properties are for boats, might need to distinguish further.
checkDetails.fromIsSafeMedium = thisMove.from.inWater || thisMove.from.onGround || thisMove.from.inWeb;
checkDetails.toIsSafeMedium = thisMove.to.inWater || thisMove.to.onGround || thisMove.to.inWeb;
checkDetails.inAir = !checkDetails.fromIsSafeMedium && !checkDetails.toIsSafeMedium;
// Distinguish by entity class (needs future proofing at all?).
if (vehicle instanceof Boat) {
checkDetails.simplifiedType = EntityType.BOAT;
checkDetails.maxAscend = MagicVehicle.maxAscend;
}
else if (vehicle instanceof Minecart) {
checkDetails.simplifiedType = EntityType.MINECART;
// Bind to rails.
checkDetails.canRails = true;
thisMove.setExtraMinecartProperties(moveInfo); // Cheating.
if (thisMove.fromOnRails) {
checkDetails.fromIsSafeMedium = true;
checkDetails.inAir = false;
}
if (thisMove.toOnRails) {
checkDetails.toIsSafeMedium = true;
checkDetails.inAir = false;
}
checkDetails.gravityTargetSpeed = 0.79;
}
else if (bestHorse != null && bestHorse.isAssignableFrom(vehicle.getClass())) {
// TODO: Climbable? -> seems not.
checkDetails.simplifiedType = EntityType.HORSE; // TODO: 1.11 - Use AbstractHorse?
checkDetails.canJump = checkDetails.canStepUpBlock = true;
}
else if (vehicle instanceof Pig) {
// TODO: Climbable!
checkDetails.simplifiedType = EntityType.PIG;
checkDetails.canJump = false;
checkDetails.canStepUpBlock = true;
checkDetails.canClimb = true;
}
else {
checkDetails.simplifiedType = thisMove.vehicleType;
}
// Generic settings.
// (maxAscend is not checked for stepping up blocks)
if (checkDetails.canJump) {
checkDetails.maxAscend = 1.0; // Coarse envelope. Actual lift off gain should be checked on demand.
}
// Climbable
if (checkDetails.canClimb) {
if (thisMove.from.onClimbable) {
checkDetails.fromIsSafeMedium = true;
checkDetails.inAir = false;
}
if (thisMove.to.onClimbable) {
checkDetails.toIsSafeMedium = true;
checkDetails.inAir = false;
}
}
}
/**
* Generic in-air check.
* @param thisMove
* @param data
* @return
*/
private boolean checkInAir(final VehicleMoveData thisMove, final MovingData data,
final boolean debug) {
// TODO: Distinguish sfJumpPhase and inAirDescendCount (after reaching the highest point).
if (debug) {
debugDetails.add("air-air");
}
if (checkDetails.canJump) {
// TODO: Max. y-distance to set back.
// TODO: Friction.
}
else {
if (thisMove.yDistance > 0.0) {
tags.add("ascend_at_all");
return true;
}
}
boolean violation = false;
// Absolute vertical distance to set back.
// TODO: Add something like this.
// final double setBackYdistance = to.getY() - data.vehicleSetBacks.getValidSafeMediumEntry().getY();
// if (data.sfJumpPhase > 4) {
// double estimate = Math.min(2.0, MagicVehicle.boatGravityMin * ((double) data.sfJumpPhase / 4.0) * ((double) data.sfJumpPhase / 4.0 + 1.0) / 2.0);
// if (setBackYdistance > -estimate) {
// tags.add("slow_fall_vdistsb");
// return true;
// }
// }
// Enforce falling speed (vdist) envelope by in-air phase count.
// Slow falling (vdist), do not bind to descending in general.
final double minDescend = -(thisMove.yDistance < -MagicVehicle.boatLowGravitySpeed ? MagicVehicle.boatGravityMinAtSpeed : MagicVehicle.boatGravityMin) * (checkDetails.canJump ? Math.max(data.sfJumpPhase - MagicVehicle.maxJumpPhaseAscend, 0) : data.sfJumpPhase);
final double maxDescend = getInAirMaxDescend(thisMove, data);
if (data.sfJumpPhase > (checkDetails.canJump ? MagicVehicle.maxJumpPhaseAscend : 1)
&& thisMove.yDistance > Math.max(minDescend, -checkDetails.gravityTargetSpeed)) {
tags.add("slow_fall_vdist");
violation = true;
}
// Fast falling (vdist).
else if (data.sfJumpPhase > 1 && thisMove.yDistance < maxDescend) {
// TODO: Allow one skipped move per jump phase (1, 2, 3).
tags.add("fast_fall_vdist");
violation = true;
}
if (violation) {
// Post violation detection workarounds.
if (MagicVehicle.oddInAir(thisMove, minDescend, maxDescend, checkDetails, data)) {
violation = false;
checkDetails.checkDescendMuch = checkDetails.checkAscendMuch = false; // (Full envelope has been checked.)
}
if (debug) {
debugDetails.add("maxDescend: " + maxDescend);
}
}
return violation;
}
private double getInAirMaxDescend(final PlayerMoveData thisMove, final MovingData data) {
double maxDescend = -MagicVehicle.boatGravityMax * data.sfJumpPhase - 0.5;
final VehicleMoveData firstPastMove = data.vehicleMoves.getFirstPastMove();
if (thisMove.yDistance < maxDescend && firstPastMove.toIsValid) {
if (firstPastMove.yDistance < maxDescend && firstPastMove.yDistance > maxDescend * 2.5) {
// Simply continue with friction.
maxDescend = Math.min(maxDescend, firstPastMove.yDistance - (MagicVehicle.boatGravityMax + MagicVehicle.boatGravityMin) / 2.0);
debugDetails.add("desc_frict");
}
else if (firstPastMove.specialCondition && thisMove.yDistance > -1.5) {
// After special set-back confirm move, observed ca. -1.1.
maxDescend = Math.min(maxDescend, -1.5);
debugDetails.add("desc_special");
}
}
return maxDescend;
}
private boolean maxDistHorizontal(final VehicleMoveData thisMove, final double maxDistanceHorizontal) {
if (thisMove.hDistance > maxDistanceHorizontal) {
tags.add("hdist");
return true;
}
else {
return false;
}
}
}