From f1342fd021192ae303774668c66922abce397f82 Mon Sep 17 00:00:00 2001 From: asofold Date: Thu, 20 Mar 2014 23:29:21 +0100 Subject: [PATCH] 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? --- .../checks/moving/LocationTrace.java | 206 ++++++++++++++++++ .../nocheatplus/test/TestLocationTrace.java | 179 +++++++++++++++ 2 files changed, 385 insertions(+) create mode 100644 NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/moving/LocationTrace.java create mode 100644 NCPCore/src/test/java/fr/neatmonster/nocheatplus/test/TestLocationTrace.java diff --git a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/moving/LocationTrace.java b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/moving/LocationTrace.java new file mode 100644 index 00000000..0c0a4ac8 --- /dev/null +++ b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/moving/LocationTrace.java @@ -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:
+ * The last entry has to be up to date, but it can lead to a "stray entry" far off the second one on time.
+ * Introducing a mergeTime could also help against keeping too many outdated entries.
+ * On merging conditions, checking dist/time vs. the second latest element could be feasible, supposedly with double distance.
+ */ + 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); + } + +} diff --git a/NCPCore/src/test/java/fr/neatmonster/nocheatplus/test/TestLocationTrace.java b/NCPCore/src/test/java/fr/neatmonster/nocheatplus/test/TestLocationTrace.java new file mode 100644 index 00000000..8c06e4f1 --- /dev/null +++ b/NCPCore/src/test/java/fr/neatmonster/nocheatplus/test/TestLocationTrace.java @@ -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 (!) + +}