diff --git a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/fight/Direction.java b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/fight/Direction.java index 6c7ff759..9522c078 100644 --- a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/fight/Direction.java +++ b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/fight/Direction.java @@ -7,6 +7,7 @@ import org.bukkit.util.Vector; import fr.neatmonster.nocheatplus.checks.Check; import fr.neatmonster.nocheatplus.checks.CheckType; +import fr.neatmonster.nocheatplus.checks.moving.LocationTrace.TraceEntry; import fr.neatmonster.nocheatplus.utilities.TrigUtil; /** @@ -22,7 +23,7 @@ public class Direction extends Check { } /** - * Checks a player. + * "Classic" check. * * @param player * the player @@ -35,8 +36,9 @@ public class Direction extends Check { // Safeguard, if entity is complex, this check will fail due to giant and hard to define hitboxes. // if (damaged instanceof EntityComplex || damaged instanceof EntityComplexPart) - if (mcAccess.isComplexPart(damaged)) - return false; + if (mcAccess.isComplexPart(damaged)) { + return false; + } // Find out how wide the entity is. final double width = mcAccess.getWidth(damaged); @@ -75,16 +77,113 @@ public class Direction extends Check { // Deal an attack penalty time. data.attackPenalty.applyPenalty(cc.directionPenalty); } - } else - // Reward the player by lowering their violation level. + } else { + // Reward the player by lowering their violation level. data.directionVL *= 0.8D; - + } + return cancel; } - + + /** + * Data context for iterating over TraceEntry instances. + * @param player + * @param loc + * @param damaged + * @param damagedLoc + * @param data + * @param cc + * @return + */ public DirectionContext getContext(final Player player, final Location loc, final Entity damaged, final Location damagedLoc, final FightData data, final FightConfig cc) { final DirectionContext context = new DirectionContext(); - // TODO: implement... + context.damagedComplex = mcAccess.isComplexPart(damaged); + // Find out how wide the entity is. + context.damagedWidth = mcAccess.getWidth(damaged); + // entity.height is broken and will always be 0, therefore. Calculate height instead based on boundingBox. + context.damagedHeight = mcAccess.getHeight(damaged); + context.direction = loc.getDirection(); + context.lengthDirection = context.direction.length(); return context; } + + /** + * Check if the player fails the direction check, no change of FightData. + * @param player + * @param loc + * @param damaged + * @param dLoc + * @param context + * @param data + * @param cc + * @return + */ + public boolean loopCheck(final Player player, final Location loc, final Entity damaged, final TraceEntry dLoc, final DirectionContext context, final FightData data, final FightConfig cc) { + + // Ignore complex entities for the moment. + if (context.damagedComplex) { + // TODO: Revise :p + return false; + } + boolean cancel = false; + + // TODO: allow any hit on the y axis (might just adapt interface to use foot position + height)! + + // How far "off" is the player with their aim. We calculate from the players eye location and view direction to + // the center of the target entity. If the line of sight is more too far off, "off" will be bigger than 0. + + final double off; + if (cc.directionStrict){ + off = TrigUtil.combinedDirectionCheck(loc, player.getEyeHeight(), context.direction, dLoc.x, dLoc.y + context.damagedHeight / 2D, dLoc.z, context.damagedWidth, context.damagedHeight, TrigUtil.DIRECTION_PRECISION, 80.0); + } + else{ + // Also take into account the angle. + off = TrigUtil.directionCheck(loc, player.getEyeHeight(), context.direction, dLoc.x, dLoc.y + context.damagedHeight / 2D, dLoc.z, context.damagedWidth, context.damagedHeight, TrigUtil.DIRECTION_PRECISION); + } + + if (off > 0.1) { + // Player failed the check. Let's try to guess how far they were from looking directly to the entity... + final Vector blockEyes = new Vector(dLoc.x - loc.getX(), dLoc.y + context.damagedHeight / 2D - loc.getY() - player.getEyeHeight(), dLoc.z - loc.getZ()); + final double distance = blockEyes.crossProduct(context.direction).length() / context.lengthDirection; + context.minViolation = Math.min(context.minViolation, distance); + } + context.minResult = Math.min(context.minResult, off); + + return cancel; + } + + /** + * Apply changes to FightData according to check results (context), trigger violations. + * @param player + * @param loc + * @param damaged + * @param context + * @param forceViolation + * @param data + * @param cc + * @return + */ + public boolean loopFinish(final Player player, final Location loc, final Entity damaged, final DirectionContext context, final boolean forceViolation, final FightData data, final FightConfig cc) { + boolean cancel = false; + final double off = forceViolation && context.minViolation != Double.MAX_VALUE ? context.minViolation : context.minResult; + if (off > 0.1) { + // Add the overall violation level of the check. + data.directionVL += context.minViolation; + + // Execute whatever actions are associated with this check and the violation level and find out if we should + // cancel the event. + cancel = executeActions(player, data.directionVL, context.minViolation, cc.directionActions); + + if (cancel) { + // Deal an attack penalty time. + data.attackPenalty.applyPenalty(cc.directionPenalty); + } + } else { + // Reward the player by lowering their violation level. + data.directionVL *= 0.8D; + } + + return cancel; + } + } diff --git a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/fight/DirectionContext.java b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/fight/DirectionContext.java index cd6b5b31..f8c139ef 100644 --- a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/fight/DirectionContext.java +++ b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/fight/DirectionContext.java @@ -1,10 +1,23 @@ package fr.neatmonster.nocheatplus.checks.fight; +import org.bukkit.util.Vector; + /** * Context data for the direction check, for repeated use within a loop. * @author mc_dev * */ public class DirectionContext { - + + public boolean damagedComplex; + public double damagedWidth; + public double damagedHeight; + public Vector direction = null; + public double lengthDirection; + + /** Minimum value for the distance that was a violation. */ + public double minViolation = Double.MAX_VALUE; + /** Minimum value for off. */ + public double minResult = Double.MAX_VALUE; + } diff --git a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/fight/FightListener.java b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/fight/FightListener.java index 9a601143..5ff5718f 100644 --- a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/fight/FightListener.java +++ b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/fight/FightListener.java @@ -1,5 +1,7 @@ package fr.neatmonster.nocheatplus.checks.fight; +import java.util.Iterator; + import org.bukkit.Location; import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.Entity; @@ -26,6 +28,7 @@ import fr.neatmonster.nocheatplus.checks.combined.Combined; import fr.neatmonster.nocheatplus.checks.combined.Improbable; import fr.neatmonster.nocheatplus.checks.inventory.Items; import fr.neatmonster.nocheatplus.checks.moving.LocationTrace; +import fr.neatmonster.nocheatplus.checks.moving.LocationTrace.TraceEntry; import fr.neatmonster.nocheatplus.checks.moving.MediumLiftOff; import fr.neatmonster.nocheatplus.checks.moving.MovingConfig; import fr.neatmonster.nocheatplus.checks.moving.MovingData; @@ -135,8 +138,9 @@ public class FightListener extends CheckListener implements JoinLeaveListener{ // TODO: dist < width => skip some checks (direction, ..) final LocationTrace damagedTrace; + final Player damagedPlayer; if (damaged instanceof Player){ - final Player damagedPlayer = (Player) damaged; + damagedPlayer = (Player) damaged; if (cc.debug && damagedPlayer.hasPermission(Permissions.ADMINISTRATION_DEBUG)){ damagedPlayer.sendMessage("Attacked by " + player.getName() + ": inv=" + mcAccess.getInvulnerableTicks(damagedPlayer) + " ndt=" + damagedPlayer.getNoDamageTicks()); } @@ -149,11 +153,12 @@ public class FightListener extends CheckListener implements JoinLeaveListener{ // (This is done even if the event has already been cancelled, to keep track, if the player is on a horse.) damagedTrace = MovingData.getData(damagedPlayer).updateTrace(damagedPlayer, damagedLoc, tick); } else { + damagedPlayer = null; // TODO: This is a temporary workaround. // Use a fake trace. // TODO: Provide for entities too? E.g. one per player, or a fully fledged bookkeeping thing (EntityData). final MovingConfig mcc = MovingConfig.getConfig(damagedLoc.getWorld().getName()); - damagedTrace = new LocationTrace(mcc.traceSize, mcc.traceMergeDist); - damagedTrace.addEntry(tick, damagedLoc.getX(), damagedLoc.getY(), damagedLoc.getZ()); + damagedTrace = null; //new LocationTrace(mcc.traceSize, mcc.traceMergeDist); + //damagedTrace.addEntry(tick, damagedLoc.getX(), damagedLoc.getY(), damagedLoc.getZ()); } if (cc.cancelDead){ @@ -213,37 +218,84 @@ public class FightListener extends CheckListener implements JoinLeaveListener{ // TODO: Order of all these checks ... // Checks that use LocationTrace. - - /** - * Iterate trace for trigonometric checks.
- * Calculate shared data before checking.
- * Maintain a latency window.
- * Check all in one loop, with pre- and invalidation conditions.
- * If some checks are disabled, window estimation must still be done fro the remaining ones. - * - */ - - // TODO: Later optimize (...) + + // TODO: Later optimize (...), should reverse check window ? // First loop through reach and direction, to determine a window. final boolean reachEnabled = !cancelled && reach.isEnabled(player); - //final ReachContext reachContext = reachEnabled ? reach.getContext(player, loc, damaged, damagedLoc, data, cc) : null; - - if (reachEnabled && reach.check(player, loc, damaged, damagedLoc, data, cc)) { - cancelled = true; - } - final boolean directionEnabled = !cancelled && direction.isEnabled(player); - //final DirectionContext directionContext = directionEnabled ? direction.getContext(player, loc, damaged, damagedLoc, data, cc) : null; - if (directionEnabled && direction.check(player, loc, damaged, damagedLoc, data, cc)) { - cancelled = true; + if (reachEnabled || directionEnabled) { + if (damagedPlayer != null) { + // TODO: Move to a method (trigonometric checks). + final ReachContext reachContext = reachEnabled ? reach.getContext(player, loc, damaged, damagedLoc, data, cc) : null; + final DirectionContext directionContext = directionEnabled ? direction.getContext(player, loc, damaged, damagedLoc, data, cc) : null; + + final long traceOldest = tick; // - damagedTrace.getMaxSize(); // TODO: Set by window. + // TODO: Iterating direction: could also start from latest, be it on occasion. + Iterator traceIt = damagedTrace.maxAgeIterator(traceOldest); + + boolean violation = true; // No tick with all checks passed. + boolean reachPassed = !reachEnabled; // Passed individually for some tick. + boolean directionPassed = !directionEnabled; // Passed individually for some tick. + // TODO: Maintain a latency estimate + max diff and invalidate completely (i.e. iterate from latest NEXT time)], or just max latency. + while (traceIt.hasNext()) { + final TraceEntry entry = traceIt.next(); + // Simplistic just check both until end or hit. + // TODO: Other default distances/tolerances. + boolean thisPassed = true; + if (reachEnabled) { + if (reach.loopCheck(player, loc, damagedPlayer, entry, reachContext, data, cc)) { + thisPassed = false; + } else { + reachPassed = true; + } + } + if (directionEnabled && (reachPassed || !directionPassed)) { + if (direction.loopCheck(player, damagedLoc, damagedPlayer, entry, directionContext, data, cc)) { + thisPassed = false; + } else { + directionPassed = true; + } + } + if (thisPassed) { + // TODO: Log/set estimated latency. + violation = false; + break; + } + } + // TODO: How to treat mixed state: violation && reachPassed && directionPassed [current: use min violation // thinkable: silent cancel, if actions have cancel (!)] + // TODO: Adapt according to strictness settings? + if (reachEnabled) { + // TODO: Might ignore if already cancelled by mixed/silent cancel. + if (reach.loopFinish(player, loc, damagedPlayer, reachContext, violation, data, cc)) { + cancelled = true; + } + } + if (directionEnabled) { + // TODO: Might ignore if already cancelled. + if (direction.loopFinish(player, loc, damagedPlayer, directionContext, violation, data, cc)) { + cancelled = true; + } + } + // TODO: Log exact state, probably record min/max latency (individually). + } else { + // Still use the classic methods for non-players. maybe[] + if (reachEnabled && reach.check(player, loc, damaged, damagedLoc, data, cc)) { + cancelled = true; + } + + if (directionEnabled && direction.check(player, loc, damaged, damagedLoc, data, cc)) { + cancelled = true; + } + } } // Check angle with allowed window. if (angle.isEnabled(player)) { + // TODO: Revise, use own trace. // The "fast turning" checks are checked in any case because they accumulate data. - // Improbable yaw changing. + // Improbable yaw changing: Moving events might be missing up to a ten degrees change. if (Combined.checkYawRate(player, loc.getYaw(), now, worldName, cc.yawRateCheck)) { // (Check or just feed). // TODO: Work into this somehow attacking the same aim and/or similar aim position (not cancel then). @@ -251,6 +303,9 @@ public class FightListener extends CheckListener implements JoinLeaveListener{ } // Angle check. if (angle.check(player, worldChanged, data, cc)) { + if (!cancelled && cc.debug) { + System.out.println(player.getName() + " fight.angle cancel without yawrate cancel."); + } cancelled = true; } } diff --git a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/fight/Reach.java b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/fight/Reach.java index 67cb6238..de50d3de 100644 --- a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/fight/Reach.java +++ b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/fight/Reach.java @@ -12,9 +12,11 @@ import org.bukkit.util.Vector; import fr.neatmonster.nocheatplus.checks.Check; import fr.neatmonster.nocheatplus.checks.CheckType; import fr.neatmonster.nocheatplus.checks.combined.Improbable; +import fr.neatmonster.nocheatplus.checks.moving.LocationTrace.TraceEntry; import fr.neatmonster.nocheatplus.permissions.Permissions; import fr.neatmonster.nocheatplus.utilities.StringUtil; import fr.neatmonster.nocheatplus.utilities.TickTask; +import fr.neatmonster.nocheatplus.utilities.TrigUtil; /** * The Reach check will find out if a player interacts with something that's too far away. @@ -43,7 +45,7 @@ public class Reach extends Check { } /** - * Checks a player. + * "Classic" check. * * @param player * the player @@ -132,10 +134,136 @@ public class Reach extends Check { return cancel; } - - public ReachContext getContext(final Player player, final Location loc, final Entity damaged, final Location damagedLoc, final FightData data, final FightConfig cc) { + + /** + * Data context for iterating over TraceEntry instances. + * @param player + * @param pLoc + * @param damaged + * @param damagedLoc + * @param data + * @param cc + * @return + */ + public ReachContext getContext(final Player player, final Location pLoc, final Entity damaged, final Location damagedLoc, final FightData data, final FightConfig cc) { final ReachContext context = new ReachContext(); - // TODO: Implement + context.distanceLimit = player.getGameMode() == GameMode.CREATIVE ? CREATIVE_DISTANCE : cc.reachSurvivalDistance + getDistMod(damaged); + context.distanceMin = (context.distanceLimit - cc.reachReduceDistance) / context.distanceLimit; + context.damagedHeight = mcAccess.getHeight(damaged); + //context.eyeHeight = player.getEyeHeight(); + context.pY = pLoc.getY() + player.getEyeHeight(); return context; } + + /** + * Check if the player fails the reach check, no change of FightData. + * @param player + * @param pLoc + * @param damaged + * @param dRef + * @param context + * @param data + * @param cc + * @return + */ + public boolean loopCheck(final Player player, final Location pLoc, final Entity damaged, final TraceEntry dRef, final ReachContext context, final FightData data, final FightConfig cc) { + boolean cancel = false; + + // Refine y position. + final double dY = dRef.y; + double y = dRef.y; + + if (context.pY <= dY) { + // Keep the foot level y. + } + else if (context.pY >= dY + context.damagedHeight) { + y = dY + context.damagedHeight; // Highest ref y. + } + else { + y = context.pY; // Level with damaged. + } + + // Distance is calculated from eye location to center of targeted. If the player is further away from their target + // than allowed, the difference will be assigned to "distance". + // TODO: Run check on squared distances (quite easy to change to stored boundary-sq values). + final double lenpRel = TrigUtil.distance(dRef.x, y, dRef.z, pLoc.getX(), context.pY, pLoc.getZ()); + + double violation = lenpRel - context.distanceLimit; + + if (violation > 0 || lenpRel - context.distanceLimit * data.reachMod > 0){ + // TODO: The silent cancel parts should be sen as "no violation" ? + // Set minimum violation in context + context.minViolation = Math.min(context.minViolation, lenpRel); + cancel = true; + } + context.minResult = Math.min(context.minResult, lenpRel); + + return cancel; + + } + + /** + * Apply changes to FightData according to check results (context), trigger violations. + * @param player + * @param pLoc + * @param damaged + * @param context + * @param forceViolation + * @param data + * @param cc + * @return + */ + public boolean loopFinish(final Player player, final Location pLoc, final Entity damaged, final ReachContext context, final boolean forceViolation, final FightData data, final FightConfig cc) { + final double lenpRel = forceViolation && context.minViolation != Double.MAX_VALUE ? context.minViolation : context.minResult; + double violation = lenpRel - context.distanceLimit; + boolean cancel = false; + if (violation > 0) { + // They failed, increment violation level. This is influenced by lag, so don't do it if there was lag. + if (TickTask.getLag(1000) < 1.5f){ + // TODO: 1.5 is a fantasy value. + data.reachVL += violation; + } + + // Execute whatever actions are associated with this check and the violation level and find out if we should + // cancel the event. + cancel = executeActions(player, data.reachVL, violation, cc.reachActions); + if (Improbable.check(player, (float) violation / 2f, System.currentTimeMillis(), "fight.reach")){ + cancel = true; + } + if (cancel && cc.reachPenalty > 0){ + // Apply an attack penalty time. + data.attackPenalty.applyPenalty(cc.reachPenalty); + } + } + else if (lenpRel - context.distanceLimit * data.reachMod > 0){ + // Silent cancel. + if (cc.reachPenalty > 0) { + data.attackPenalty.applyPenalty(cc.reachPenalty / 2); + } + cancel = true; + Improbable.feed(player, (float) (lenpRel - context.distanceLimit * data.reachMod) / 4f, System.currentTimeMillis()); + } + else{ + // Player passed the check, reward them. + data.reachVL *= 0.8D; + + } + // Adaption amount for dynamic range. + final double DYNAMIC_STEP = cc.reachReduceStep / cc.reachSurvivalDistance; + if (!cc.reachReduce){ + data.reachMod = 1d; + } + else if (lenpRel > context.distanceLimit - cc.reachReduceDistance){ + data.reachMod = Math.max(context.distanceMin, data.reachMod - DYNAMIC_STEP); + } + else{ + data.reachMod = Math.min(1.0, data.reachMod + DYNAMIC_STEP); + } + + if (cc.debug && player.hasPermission(Permissions.ADMINISTRATION_DEBUG)){ + player.sendMessage("NC+: Attack/reach " + damaged.getType()+ " height="+ StringUtil.fdec3.format(context.damagedHeight) + " dist=" + StringUtil.fdec3.format(lenpRel) +" @" + StringUtil.fdec3.format(data.reachMod)); + } + + return cancel; + } } diff --git a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/fight/ReachContext.java b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/fight/ReachContext.java index ab1a1930..59ba9963 100644 --- a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/fight/ReachContext.java +++ b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/fight/ReachContext.java @@ -6,5 +6,17 @@ package fr.neatmonster.nocheatplus.checks.fight; * */ public class ReachContext { + + public double distanceLimit; + public double distanceMin; + public double damagedHeight; + /** Attacking player. */ + public double eyeHeight; + /** Eye location y of the attacking player. */ + public double pY; + /** Minimum value of lenpRel that was a violation. */ + public double minViolation = Double.MAX_VALUE; + /** Minimum value of lenpRel. */ + public double minResult = Double.MAX_VALUE; }