[RAW] First version for direction + reach using a moving trace.

This does not change the used methods much, thus it will rather allow
much more cheating, however it allows some basic testing for false
positives. The reach implementation has been slightly optimized to run
faster. The current implementation is not final and only uses trace
elements that were added in the current tick, the latest is always
included.

Next steps will probably be:
* Stricter methods for an individual TraceEntry (demand near-exact hit).
* Don't allow random latency shifting. Maintain a window-thing.
* Probably get rid of the classic method for attacking other entities.
This commit is contained in:
asofold 2014-03-24 19:45:21 +01:00
parent f0f4a7ec2c
commit ea682417bc
5 changed files with 344 additions and 37 deletions

View File

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

View File

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

View File

@ -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.<br>
* Calculate shared data before checking.<br>
* Maintain a latency window.<br>
* Check all in one loop, with pre- and invalidation conditions.<br>
* 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<TraceEntry> 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;
}
}

View File

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

View File

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