diff --git a/NCPCompat/src/main/java/fr/neatmonster/nocheatplus/utilities/RayTracing.java b/NCPCompat/src/main/java/fr/neatmonster/nocheatplus/utilities/RayTracing.java new file mode 100644 index 00000000..0976d597 --- /dev/null +++ b/NCPCompat/src/main/java/fr/neatmonster/nocheatplus/utilities/RayTracing.java @@ -0,0 +1,160 @@ +package fr.neatmonster.nocheatplus.utilities; + +import org.bukkit.Location; + +/** + * Ray tracing for block coordinates with entry point offsets. + * @author mc_dev + * + */ +public abstract class RayTracing { + +// /** End point coordinates (from, to) */ +// protected double x0, y0, z0, x1, y1, z1; + +// /** Total distance between end points. */ +// protected double d; + + /** Distance per axis. */ + protected double dX, dY, dZ; + + /** Current block. */ + protected int blockX, blockY, blockZ; + + /** Offset within current block. */ + protected double oX, oY, oZ; + + /** Current "time" in [0..1]. */ + protected double t = Double.MIN_VALUE; + + /** Tolerance for time, for checking the abort condition: 1.0 - t <= tol . */ + protected double tol = 0.0; + + public RayTracing(double x0, double y0, double z0, double x1, double y1, double z1){ + set(x0, y0, z0, x1, y1, z1); + } + + public void set(double x0, double y0, double z0, double x1, double y1, double z1){ +// // TODO: Consider not using end-points at all. +// this.x0 = x0; +// this.y0 = y0; +// this.z0 = z0; +// this.x1 = x1; +// this.y1 = y1; +// this.z1 = z1; +// // Set the "runtime" info. +// d = CheckUtils.distance(x0, y0, z0, x1, y1, z1); + dX = x1 - x0; + dY = y1 - y0; + dZ = z1 - z0; + blockX = Location.locToBlock(x0); + blockY = Location.locToBlock(y0); + blockZ = Location.locToBlock(z0); + oX = (double) (x0 - blockX); + oY = (double) (y0 - blockY); + oZ = (double) (z0 - blockZ); + t = 0.0; + } + + private static final double tDiff(final double dTotal, final double offset){ + if (dTotal > 0.0){ + return (1 - offset) / dTotal; + } + else if (dTotal < 0.0){ + return offset / -dTotal; + } + else{ + return Double.MAX_VALUE; + } + } + + /** + * Loop through blocks. + */ + public void loop(){ + // TODO: Might intercept 0 dist ? + + // Time to block edge. + double tX, tY, tZ, tMin; + while (1.0 - t > tol){ + // Determine smallest time to block edge. + tX = tDiff(dX, oX); + tY = tDiff(dY, oY); + tZ = tDiff(dZ, oZ); + tMin = Math.min(tX, Math.min(tY, tZ)); + if (tMin == Double.MAX_VALUE || t + tMin > 1.0) tMin = 1.0 - t; + // Call step with appropriate arguments. + if (!step(blockX, blockY, blockZ, oX, oY, oZ, tMin)) break; // || tMin == 0) break; + if (t + tMin >= 1.0 - tol) break; + // Advance (add to t etc.). + // x + oX += tMin * dX; + if (tX == tMin){ + if (dX < 0){ + oX = 1; + blockX --; + } + else{ + oX = 0; + blockX ++; + } + } + else if (oX >= 1){ + oX -= 1; + blockX ++; + } + else if (oX < 0){ + oX += 1; + blockX --; + } + // y + oY += tMin * dY; + if (tY == tMin){ + if (dY < 0){ + oY = 1; + blockY --; + } + else{ + oY = 0; + blockY ++; + } + } + else if (oY >= 1){ + oY -= 1; + blockY ++; + } + else if (oY < 0){ + oY += 1; + blockY --; + } + // z + oZ += tMin * dZ; + if (tZ == tMin){ + if (dZ < 0){ + oZ = 1; + blockZ --; + } + else{ + oZ = 0; + blockZ ++; + } + } + else if (oZ >= 1){ + oZ -= 1; + blockZ ++; + } + else if (oZ < 0){ + oZ += 1; + blockZ --; + } + t += tMin; + } + } + + /** + * One step in the loop. + * @return If to continue loop. + */ + protected abstract boolean step(int blockX, int blockY, int blockZ, double oX, double oY, double oZ, double dT); + +} diff --git a/NCPCompat/src/test/java/fr/neatmonster/nocheatplus/test/TestRayTracing.java b/NCPCompat/src/test/java/fr/neatmonster/nocheatplus/test/TestRayTracing.java new file mode 100644 index 00000000..21f83e8c --- /dev/null +++ b/NCPCompat/src/test/java/fr/neatmonster/nocheatplus/test/TestRayTracing.java @@ -0,0 +1,177 @@ +package fr.neatmonster.nocheatplus.test; + +import static org.junit.Assert.fail; + +import java.util.Random; + +import org.bukkit.Location; +import org.junit.Test; + +import fr.neatmonster.nocheatplus.utilities.RayTracing; +import fr.neatmonster.nocheatplus.utilities.StringUtil; + +public class TestRayTracing { + + protected static final Random random = new Random(System.nanoTime() + 13391); + + public static class CountRayTracing extends RayTracing{ + public CountRayTracing(double x0, double y0, double z0, double x1, double y1, double z1) { + super(x0, y0, z0, x1, y1, z1); + } + + protected int done = 0; + @Override + protected boolean step(int blockX, int blockY, int blockZ, double oX, + double oY, double oZ, double dT) { + done ++; + return true; + } + + public int loopCount() { + super.loop(); + return done; + } + } + + public static double[] randomCoords(double max){ + double[] res = new double[6]; + for (int i = 0; i < 6 ; i++){ + res[i] = (random.nextDouble() * 2.0 - 1.0 ) * max; + } + return res; + } + + public static void doFail(String message, double[] coords) { + System.out.println("---- Failure trace ----"); + System.out.println(message); + if (coords != null){ + System.out.println("{" + coords[0] + ", " + coords[1]+ ", " + coords[2] + " , " + coords[3] + ", " + coords[4]+ ", " + coords[5] + "}"); + dumpRawRayTracing(coords); + } + fail(message); + } + + /** + * Mostly block-coordinate consistency checking. + * @param coords + * @return + */ + public static RayTracing checkConsistency(final double[] coords){ + RayTracing rt = new RayTracing(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]) { + + protected int lbx, lby, lbz; + + protected int step = 0; + + /* (non-Javadoc) + * @see fr.neatmonster.nocheatplus.utilities.RayTracing#set(double, double, double, double, double, double) + */ + @Override + public void set(double x0, double y0, double z0, double x1, double y1, double z1) { + super.set(x0, y0, z0, x1, y1, z1); + lbx = blockX - 1; + lby = blockY - 1; + lbz = blockZ - 1; + } + + private boolean ignEdge(double offset, double dTotal){ + return offset == 1 && dTotal > 0 || offset == 0 && dTotal < 0; + } + + @Override + protected boolean step(int blockX, int blockY, int blockZ, double oX, double oY, double oZ, double dT) { + // TODO: This does not check last step for some occasions where it should. + step ++; + if (dT == 0 && 1.0 - (t + dT) > tol){ + if (!ignEdge(oX, dX) && !ignEdge(oY, dY) && !ignEdge(oZ, dZ)){ + doFail("Premature dT = 0 at t = " + StringUtil.fdec3.format(t), coords); + } + } + // TODO: check with last block coordinates + if (lbx == blockX && lby == blockY && lbz == blockZ){ + if (1.0 - (t + dT) > tol){ + doFail("Expect block coordinates to change with each step (step=" + step + ", t=" + StringUtil.fdec3.format(t) +").", coords); + } + } + // TODO: check offsets + // Set to current. + lbx = blockX; + lby = blockY; + lbz = blockZ; + + return true; + } + + @Override + public void loop() { + super.loop(); + checkBlockTarget(coords[3], blockX, oX, dX, "x"); + checkBlockTarget(coords[4], blockY, oY, dY, "y"); + checkBlockTarget(coords[5], blockZ, oZ, dZ, "z"); + } + + private void checkBlockTarget(double target, int current, double offset, double dTotal, String name){ + int b = Location.locToBlock(target); + if (current != b){ + // TODO: Might do with or without these ? +// if (current == b + 1 && dTotal > 0 && offset == 0) return; +// if (current == b - 1 && dTotal < 0 && offset == 1) return; + // Failure. + doFail("Bad target " + name + "-coordinate: " + current + " instead of " + b, coords); + } + } + + }; + rt.loop(); + return rt; + } + + public static RayTracing checkNumberOfSteps(double[] coords, int steps) { + CountRayTracing crt = new CountRayTracing(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]); + int done = crt.loopCount(); + if (done != steps) doFail("Wrong number of steps: " + done + " instead of " + steps, coords); + return crt; + } + + public static void dump(int blockX, int blockY, int blockZ, double oX, double oY, double oZ, double t, double dT) { + System.out.println(StringUtil.fdec3.format(t) + " (+" + StringUtil.fdec3.format(dT) + "): " + blockX + ", "+blockY + ", " + blockZ + " / " + StringUtil.fdec3.format(oX) + ", " + StringUtil.fdec3.format(oY)+ ", " + StringUtil.fdec3.format(oZ)); + } + + public static RayTracing dumpRawRayTracing(double[] coords) { + RayTracing rt = new RayTracing(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]) { + @Override + protected boolean step(int blockX, int blockY, int blockZ, double oX, double oY, double oZ, double dT) { + dump(blockX, blockY, blockZ, oX, oY, oZ, t, dT); + return true; + } + }; + rt.loop(); + return rt; + } + + @Test + public void testNumberOfSteps(){ + // Hand picked stuff. + checkNumberOfSteps(new double[]{0.5, 0.5, 0.5, 1.5, -0.5, 1.5}, 2); + } + + @Test + public void testConsistency(){ + // Past failures / making a difference. + for (double[] coords : new double[][]{ + // Sort by x0. + new double[]{-9.873, -4.773, -3.387, -0.161, -1.879, -7.079}, + new double[]{-3.0066423238842366, 0.8056808285866079, 5.359238045631369 , 2.0000000356757375, -2.3002237817433757, -5.889349195033338}, + new double[]{2.5619753859456917, -5.010424935746547, -7.39326637860553 , -4.678643570182639, -2.0000000105642313, -4.634727842675916}, + new double[]{7.388348424961977, -8.000000029346532, -2.5365675909347507 , 2.17126848312847, 3.236994108042559, -8.423292642985071}, + new double[]{7.525633617461991, 2.654408573114717, 3.5119744782127893 , 9.99999995904821, 9.599753890871172, 6.721727939686946}, + }){ + checkConsistency(coords); + } + // Random tests. + for (int i = 0; i < 100000; i++){ + checkConsistency(randomCoords(10.0)); + } + } + +}