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;
}