/* * 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 . */ 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 tags = new LinkedList(); /** Extra details to log on debug. */ private final List debugDetails = new LinkedList(); /** 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; } } }