First rough version of a LocationTrace implementation.

Just the raw implementation + initial test cases. To keep memory use
constant, a ring-buffer with some maximal size will be used. The
iterators are meant for faster implementation, rather than fastest
iteration. Later some trigonometric functions could be added to
LocationTrace, depending on if that may gain a lot of performance.

Next we will add the logics for adding entries and resetting the trace
to NCP (moving, teleporting, joining), on Logout the trace must not stay
in MovingData but should be garbage collected. That should be a
milestone dev build, though it does nothing for the user, it might help
finding crash bugs :p.

Soon to follow will be changing some fight checks to be able to use the
moving trace, then alter them to actually use it. Fight and interact
checks could also do moving consistency checking (tp exploit).

Who reads this?
This commit is contained in:
asofold 2014-03-20 23:29:21 +01:00
parent 2478ae787b
commit f1342fd021
2 changed files with 385 additions and 0 deletions

View File

@ -0,0 +1,206 @@
package fr.neatmonster.nocheatplus.checks.moving;
import fr.neatmonster.nocheatplus.utilities.TrigUtil;
/**
* This class is meant to record locations for players moving, in order to allow to be more
* lenient for the case of latency.
* @author mc_dev
*
*/
public class LocationTrace {
public static final class TraceEntry {
/** We keep it open, if ticks or ms are used. */
public long time;
/** Coordinates. */
public double x, y, z;
public void set(long time, double x, double y, double z) {
this.x = x;
this.y = y;
this.z = z;
this.time = time;
}
}
/**
* Iterate from oldest to latest. Not a fully featured Iterator.
* @author mc_dev
*
*/
public static final class TraceIterator {
private final TraceEntry[] entries;
/** Index as in LocationTrace */
private final int index;
private final int size;
private int currentIndex;
private final boolean ascend;
protected TraceIterator(TraceEntry[] entries, int index, int size, int currentIndex, boolean ascend) {
if (currentIndex >= entries.length || currentIndex < 0 ||
currentIndex <= index - size || currentIndex > index && currentIndex <= index - size + entries.length) {
// This should also prevent iterators for size == 0, for the moment (!).
throw new IllegalArgumentException("startIndex out of bounds.");
}
this.entries = entries;
this.index = index;
this.size = size;
this.currentIndex = currentIndex;
this.ascend = ascend;
}
public final TraceEntry next() {
if (!hasNext()) {
throw new IndexOutOfBoundsException("No more entries to iterate.");
}
final TraceEntry entry = entries[currentIndex];
if (ascend) {
currentIndex ++;
if (currentIndex >= entries.length) {
currentIndex = 0;
}
int ref = index - size + 1;
if (ref < 0) {
ref += entries.length;
}
if (currentIndex == ref) {
// Invalidate the iterator.
currentIndex = -1;
}
} else {
currentIndex --;
if (currentIndex < 0) {
currentIndex = entries.length - 1;
}
if (currentIndex == index) {
// Invalidate the iterator.
currentIndex = - 1;
}
}
return entry;
}
public final boolean hasNext() {
// Just check if currentIndex is within range.
return currentIndex >= 0 && currentIndex <= index && currentIndex > index - size || currentIndex > index && currentIndex >= index - size + entries.length;
}
}
/** A Ring. */
private final TraceEntry[] entries;
/** Last element index. */
private int index = -1;
/** Number of valid entries. */
private int size = 0;
private final double mergeDistSq;
// (No world name stored: Should be reset on world changes.)
public LocationTrace(int bufferSize, double mergeDist) {
// TODO: Specify entry-merging conditions.
if (bufferSize < 1) {
throw new IllegalArgumentException("Expect bufferSize > 0, got instead: " + bufferSize);
}
entries = new TraceEntry[bufferSize];
for (int i = 0; i < bufferSize; i++) {
entries[i] = new TraceEntry();
}
this.mergeDistSq = mergeDist * mergeDist;
}
public final void addEntry(final long time, final double x, final double y, final double z) {
// TODO: Consider setting the squared distance to last entry
if (size > 0) {
final TraceEntry oldEntry = entries[index];
// TODO: Consider duration of staying there ?
if (x == oldEntry.x && y == oldEntry.y && z == oldEntry.z) {
oldEntry.time = time;
return;
}
else if (TrigUtil.distanceSquared(x, y, z, oldEntry.x, oldEntry.y, oldEntry.z) <= mergeDistSq) {
// TODO: Could use Manhattan, after all.
/**
* TODO: <br>
* The last entry has to be up to date, but it can lead to a "stray entry" far off the second one on time.<br>
* Introducing a mergeTime could also help against keeping too many outdated entries.<br>
* On merging conditions, checking dist/time vs. the second latest element could be feasible, supposedly with double distance. <br>
*/
oldEntry.set(time, x, y, z);
return;
}
}
// Advance index.
index++;
if (index == entries.length) {
index = 0;
}
if (size < entries.length) {
size ++;
}
final TraceEntry newEntry = entries[index];
newEntry.set(time, x, y, z);
}
/** Reset content pointers - call with world changes. */
public void reset() {
index = 0;
size = 0;
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
/**
* Iterate from latest to oldest.
* @return
*/
public TraceIterator latestIterator() {
return new TraceIterator(entries, index, size, index, false);
}
/**
* Iterate from oldest to latest.
* @return
*/
public TraceIterator oldestIterator() {
final int currentIndex = index - size + 1;
return new TraceIterator(entries, index, size, currentIndex < 0 ? currentIndex + entries.length : currentIndex, true);
}
/**
* Iterate from entry with max. age to latest, always includes latest.
* @param tick Absolute tick value for oldest accepted tick.
* @param ms Absolute ms value for oldest accepted ms;
* @return
*/
public TraceIterator maxAgeIterator(long time) {
int currentIndex = index;
int tempIndex = currentIndex;
int steps = 1;
while (steps < size) {
tempIndex --;
if (tempIndex < 0) {
tempIndex += size;
}
final TraceEntry entry = entries[tempIndex];
if (entry.time >= time) {
// Continue.
currentIndex = tempIndex;
} else {
break;
}
steps ++;
}
return new TraceIterator(entries, index, size, currentIndex, true);
}
}

View File

@ -0,0 +1,179 @@
package fr.neatmonster.nocheatplus.test;
import static org.junit.Assert.fail;
import java.util.Random;
import org.junit.Test;
import fr.neatmonster.nocheatplus.checks.moving.LocationTrace;
import fr.neatmonster.nocheatplus.checks.moving.LocationTrace.TraceEntry;
import fr.neatmonster.nocheatplus.checks.moving.LocationTrace.TraceIterator;
public class TestLocationTrace {
protected static final Random random = new Random(System.nanoTime() + 133345691);
public static double rand(double radius) {
return rand(0.0, radius);
}
public static double rand(double center, double radius) {
return center + 2.0 * radius * (random.nextDouble() - 0.5);
}
@Test
public void testSize() {
int size = 80;
double mergeDist = -0.1;
LocationTrace trace = new LocationTrace(size, mergeDist);
// Empty
if (!trace.isEmpty()) {
fail("Trace must be empty on start.");
}
if (trace.size() != 0) {
fail("Size must be 0 at start.");
}
// Adding up to size elements.
for (int i = 0; i < size ; i++) {
trace.addEntry(i, i, i, i);
if (trace.size() != i + 1) {
fail("Wrong size, expect " + (i + 1) + ", got instead: " + trace.size());
}
}
// Adding a lot of elements.
for (int i = 0; i < 1000; i ++) {
trace.addEntry(i + size, i, i, i);
if (trace.size() != size) {
fail("Wrong size, expect " + size + ", got instead: " + trace.size());
}
}
}
@Test
public void testMergeZeroDist() {
int size = 80;
double mergeDist = 0.0;
LocationTrace trace = new LocationTrace(size, mergeDist);
for (int i = 0; i < 1000; i ++) {
trace.addEntry(i + size, 0 , 0, 0);
if (trace.size() != 1) {
fail("Wrong size, expect 1, got instead: " + trace.size());
}
}
}
@Test
public void testMergeUpdateAlwaysDist() {
// This assumes the last location is always updated.
int size = 80;
double mergeDist = Math.sqrt(0.6 * 0.6 * 3);
LocationTrace trace = new LocationTrace(size, mergeDist);
double x = 0;
double y = 0;
double z = 0;
trace.addEntry(0 , x, y, z);
for (int i = 0; i < 1000; i ++) {
x = rand(x, 0.5);
y = rand(y, 0.5);
z = rand(z, 0.5);
trace.addEntry(i + 1, x, y, z);
if (trace.size() != 1) {
fail("Wrong size, expect 1, got instead: " + trace.size());
}
}
}
// TODO: Test iterators.
@Test
public void testEmptyIterator() {
// Expected to fail.
int size = 80;
double mergeDist = -0.1;
LocationTrace trace = new LocationTrace(size, mergeDist);
try {
trace.oldestIterator();
fail("Expect an exception on trying to get an empty iterator (oldest).");
} catch (IllegalArgumentException ex) {
}
try {
trace.latestIterator();
fail("Expect an exception on trying to get an empty iterator (latest).");
} catch (IllegalArgumentException ex) {
}
try {
trace.maxAgeIterator(0);
fail("Expect an exception on trying to get an empty iterator (maxAge).");
} catch (IllegalArgumentException ex) {
}
}
@Test
public void testIteratorSizeAndOrder() {
// Expected to fail.
int size = 80;
double mergeDist = -0.1;
LocationTrace trace = new LocationTrace(size, mergeDist);
// Adding up to size elements.
for (int i = 0; i < size; i++) {
trace.addEntry(i, i, i, i);
}
// Test size with one time filled up.
testIteratorSizeAndOrder(trace);
// Add size / 2 elements, to test cross-boundary iteration.
for (int i = 0; i < size / 2; i++) {
trace.addEntry(i + size, i, i, i);
}
// Test size again.
testIteratorSizeAndOrder(trace);
}
private void testIteratorSizeAndOrder(LocationTrace trace) {
int size = trace.size();
TraceIterator[] iterators = new TraceIterator[] {
trace.oldestIterator(),
trace.latestIterator(),
trace.maxAgeIterator(0) // Asserts entries to start at time >= 0.
};
String[] iteratorNames = new String[]{
"oldest",
"latest",
"maxAge"
};
int[] increments = new int[] {
1,
-1,
1
};
for (int i = 0; i < iterators.length; i++) {
int n = 0;
TraceIterator it = iterators[i];
TraceEntry last = null;
TraceEntry current = null;
while (it.hasNext()) {
current = it.next();
n ++;
if (n > size) {
fail("Iterator (" + iteratorNames[i] + ") iterates too long.");
}
if (last != null) {
if (current.time - last.time != increments[i]) {
fail("Bad time increment (" + iteratorNames[i] + "). Expected " + increments[i] + ", got instead: " + (current.time - last.time));
}
}
last = current;
}
if (n != size) {
fail("Iterator (" + iteratorNames[i] + ") should have " + size + " elements, found instead: " + n);
}
}
}
// TODO: Iterator order for various iterators (!)
}