From de58952afcade1592e180d7bb2f656de707afe9b Mon Sep 17 00:00:00 2001 From: Mattias Rask <231212847+0Mattias@users.noreply.github.com> Date: Sat, 12 Sep 2026 04:04:30 -0400 Subject: [PATCH 01/15] Add the Java port of nether-pathfinder The whole of babbaj's nether-pathfinder in plain Java, under baritone.process.elytra.pathfinder, as it stands on the java-port branch of 0Mattias/nether-pathfinder at 6d807d0, written for babbaj/nether-pathfinder#31. Each class is the C++ file of the same name: the chunk octree, the raytracer, the A* search over octree cubes, the Nether terrain generator and the region-file reader. The page allocator, the thread pool, the JNI layer and the Unsafe accessors have no counterpart: a chunk is an object that lives as long as it is referenced, the table is a ConcurrentHashMap, and lookups, inserts, the search and rays can run at once from any thread. The tests hold the port to the native library's answers. An FNV-1a hash of each of 625 generated chunks and the hit and hit position of 4000 rays over generated terrain, recorded by the oracle in that repository, are reproduced to the bit. The search, the table, region files, cancel, the segment time-out and the rays the native library could not take -- a point, a coordinate that is not finite, an end on a voxel corner -- have tests of their own. Nothing uses the package yet; the next commit does. Co-Authored-By: Claude Fable 5.1 --- .../elytra/pathfinder/BaritoneRegion.java | 120 ++++ .../elytra/pathfinder/BinaryHeapOpenSet.java | 104 +++ .../process/elytra/pathfinder/BlockPos.java | 126 ++++ .../process/elytra/pathfinder/Chunk.java | 216 ++++++ .../elytra/pathfinder/ChunkGeneratorHell.java | 169 +++++ .../process/elytra/pathfinder/Face.java | 22 + .../elytra/pathfinder/NetherPathfinder.java | 385 +++++++++++ .../process/elytra/pathfinder/NodePos.java | 58 ++ .../pathfinder/NoiseGeneratorImproved.java | 124 ++++ .../pathfinder/NoiseGeneratorOctaves.java | 58 ++ .../process/elytra/pathfinder/PathFinder.java | 406 ++++++++++++ .../process/elytra/pathfinder/PathNode.java | 43 ++ .../elytra/pathfinder/PathSegment.java | 29 + .../process/elytra/pathfinder/Raytracer.java | 379 +++++++++++ .../process/elytra/pathfinder/Size.java | 41 ++ .../elytra/pathfinder/package-info.java | 29 + .../elytra/pathfinder/BaritoneRegionTest.java | 118 ++++ .../process/elytra/pathfinder/Bench.java | 108 +++ .../elytra/pathfinder/ChunkGeneratorTest.java | 49 ++ .../process/elytra/pathfinder/ChunkTest.java | 170 +++++ .../elytra/pathfinder/DegenerateRaysTest.java | 144 ++++ .../process/elytra/pathfinder/Oracle.java | 100 +++ .../elytra/pathfinder/PathFindTest.java | 171 +++++ .../elytra/pathfinder/RaytraceTest.java | 124 ++++ src/test/resources/chunk-hashes.txt | 625 ++++++++++++++++++ src/test/resources/rays.bin | Bin 0 -> 292004 bytes 26 files changed, 3918 insertions(+) create mode 100644 src/main/java/baritone/process/elytra/pathfinder/BaritoneRegion.java create mode 100644 src/main/java/baritone/process/elytra/pathfinder/BinaryHeapOpenSet.java create mode 100644 src/main/java/baritone/process/elytra/pathfinder/BlockPos.java create mode 100644 src/main/java/baritone/process/elytra/pathfinder/Chunk.java create mode 100644 src/main/java/baritone/process/elytra/pathfinder/ChunkGeneratorHell.java create mode 100644 src/main/java/baritone/process/elytra/pathfinder/Face.java create mode 100644 src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java create mode 100644 src/main/java/baritone/process/elytra/pathfinder/NodePos.java create mode 100644 src/main/java/baritone/process/elytra/pathfinder/NoiseGeneratorImproved.java create mode 100644 src/main/java/baritone/process/elytra/pathfinder/NoiseGeneratorOctaves.java create mode 100644 src/main/java/baritone/process/elytra/pathfinder/PathFinder.java create mode 100644 src/main/java/baritone/process/elytra/pathfinder/PathNode.java create mode 100644 src/main/java/baritone/process/elytra/pathfinder/PathSegment.java create mode 100644 src/main/java/baritone/process/elytra/pathfinder/Raytracer.java create mode 100644 src/main/java/baritone/process/elytra/pathfinder/Size.java create mode 100644 src/main/java/baritone/process/elytra/pathfinder/package-info.java create mode 100644 src/test/java/baritone/process/elytra/pathfinder/BaritoneRegionTest.java create mode 100644 src/test/java/baritone/process/elytra/pathfinder/Bench.java create mode 100644 src/test/java/baritone/process/elytra/pathfinder/ChunkGeneratorTest.java create mode 100644 src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java create mode 100644 src/test/java/baritone/process/elytra/pathfinder/DegenerateRaysTest.java create mode 100644 src/test/java/baritone/process/elytra/pathfinder/Oracle.java create mode 100644 src/test/java/baritone/process/elytra/pathfinder/PathFindTest.java create mode 100644 src/test/java/baritone/process/elytra/pathfinder/RaytraceTest.java create mode 100644 src/test/resources/chunk-hashes.txt create mode 100644 src/test/resources/rays.bin diff --git a/src/main/java/baritone/process/elytra/pathfinder/BaritoneRegion.java b/src/main/java/baritone/process/elytra/pathfinder/BaritoneRegion.java new file mode 100644 index 0000000000..5f88ce8696 --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/BaritoneRegion.java @@ -0,0 +1,120 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.zip.GZIPInputStream; + +/** Reads the chunks of one of Baritone's cached region files ({@code r.X.Z.bcr}). */ +final class BaritoneRegion { + + private static final int MAGIC = 456022911; + + /** What the reader hands each chunk it finds. */ + interface ChunkSink { + void accept(int chunkX, int chunkZ, Chunk chunk); + } + + private BaritoneRegion() {} + + /** + * The dimension of a Baritone cache directory such as {@code .../the_nether_128/regions}, + * or -1 if the directory is not one. + */ + static int dimensionOf(String dir) { + final Path parent = Paths.get(dir).getParent(); + final String name = parent == null || parent.getFileName() == null ? "" : parent.getFileName().toString(); + switch (name) { + case "the_nether_128": return NetherPathfinder.DIMENSION_NETHER; + case "overworld_384": return NetherPathfinder.DIMENSION_OVERWORLD; + case "the_end_256": return NetherPathfinder.DIMENSION_END; + default: return -1; + } + } + + static Path regionFile(String dir, int regionX, int regionZ) { + return Paths.get(dir, "r." + regionX + "." + regionZ + ".bcr"); + } + + /** Reads the region file for (regionX, regionZ) under dir, if there is one. Returns whether a file was read. */ + static boolean load(String dir, int regionX, int regionZ, ChunkSink sink) { + final int dim = dimensionOf(dir); + if (dim == -1) { + return false; + } + final Path file = regionFile(dir, regionX, regionZ); + if (!Files.isRegularFile(file)) { + return false; + } + try (InputStream in = new GZIPInputStream(Files.newInputStream(file), 1 << 16)) { + parse(in, regionX, regionZ, dim, sink); + } catch (IOException | RuntimeException e) { + System.err.println("[nether-pathfinder] exception thrown parsing region " + regionX + "," + regionZ + ": " + e); + } + return true; + } + + /** Parses an already decompressed region stream. */ + static void parse(InputStream raw, int regionX, int regionZ, int dimension, ChunkSink sink) throws IOException { + final DataInputStream in = new DataInputStream(raw); + final int magic = in.readInt(); + if (magic != MAGIC) { + System.err.println("[nether-pathfinder] bad magic for baritone region " + regionX + "," + regionZ); + return; + } + final int height = dimension == NetherPathfinder.DIMENSION_OVERWORLD ? 384 : 256; + final byte[] data = new byte[(2 * 16 * 16 * height) / 8]; + for (int x = 0; x < 32; x++) { + for (int z = 0; z < 32; z++) { + final int present = in.readByte(); + if (present == 1) { + in.readFully(data); + sink.accept(x + 32 * regionX, z + 32 * regionZ, parseChunk(height, data)); + } + } + } + } + + private static int positionIndex(int x, int y, int z) { + return (x << 1) | (z << 5) | (y << 9); + } + + private static int get2Bits(int i, byte[] data) { + return (data[i / 8] >> (6 - (i % 8))) & 0b11; + } + + private static Chunk parseChunk(int height, byte[] data) { + final Chunk chunk = new Chunk(); + for (int y = 0; y < height; y++) { + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + if (get2Bits(positionIndex(x, y, z), data) != 0) { + chunk.setBlock(x, y, z, true); + } + } + } + } + return chunk; + } +} diff --git a/src/main/java/baritone/process/elytra/pathfinder/BinaryHeapOpenSet.java b/src/main/java/baritone/process/elytra/pathfinder/BinaryHeapOpenSet.java new file mode 100644 index 0000000000..de560b58e9 --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/BinaryHeapOpenSet.java @@ -0,0 +1,104 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +import java.util.Arrays; + +/** Baritone's binary heap, as the native library also has it. */ +final class BinaryHeapOpenSet { + + private static final int INITIAL_CAPACITY = 1024; + + private PathNode[] array = new PathNode[INITIAL_CAPACITY]; + private int size = 0; + + int getSize() { + return this.size; + } + + boolean isEmpty() { + return this.size == 0; + } + + void insert(PathNode value) { + if (this.size >= this.array.length - 1) { + this.array = Arrays.copyOf(this.array, this.array.length << 1); + } + this.size++; + value.heapPosition = this.size; + this.array[this.size] = value; + update(value); + } + + void update(PathNode val) { + int index = val.heapPosition; + int parentIndex = index >>> 1; + final double cost = val.combinedCost; + PathNode parentNode = this.array[parentIndex]; + while (index > 1 && parentNode.combinedCost > cost) { + this.array[index] = parentNode; + this.array[parentIndex] = val; + val.heapPosition = parentIndex; + parentNode.heapPosition = index; + index = parentIndex; + parentIndex = index >>> 1; + parentNode = this.array[parentIndex]; + } + } + + PathNode removeLowest() { + if (this.size == 0) { + throw new IllegalStateException("empty"); + } + final PathNode result = this.array[1]; + final PathNode val = this.array[this.size]; + this.array[1] = val; + val.heapPosition = 1; + this.array[this.size] = null; + this.size--; + result.heapPosition = -1; + if (this.size < 2) { + return result; + } + int index = 1; + int smallerChild = 2; + final double cost = val.combinedCost; + do { + PathNode smallerChildNode = this.array[smallerChild]; + double smallerChildCost = smallerChildNode.combinedCost; + if (smallerChild < this.size) { + final PathNode rightChildNode = this.array[smallerChild + 1]; + final double rightChildCost = rightChildNode.combinedCost; + if (smallerChildCost > rightChildCost) { + smallerChild++; + smallerChildCost = rightChildCost; + smallerChildNode = rightChildNode; + } + } + if (cost <= smallerChildCost) { + break; + } + this.array[index] = smallerChildNode; + this.array[smallerChild] = val; + val.heapPosition = smallerChild; + smallerChildNode.heapPosition = index; + index = smallerChild; + } while ((smallerChild <<= 1) <= this.size); + return result; + } +} diff --git a/src/main/java/baritone/process/elytra/pathfinder/BlockPos.java b/src/main/java/baritone/process/elytra/pathfinder/BlockPos.java new file mode 100644 index 0000000000..402dba3d2f --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/BlockPos.java @@ -0,0 +1,126 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +/** An integer position. Immutable. */ +final class BlockPos { + + final int x; + final int y; + final int z; + + BlockPos(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } + + int chunkX() { + return this.x >> 4; + } + + int chunkZ() { + return this.z >> 4; + } + + double distanceToSq(BlockPos pos) { + final double dx = pos.x - this.x; + final double dy = pos.y - this.y; + final double dz = pos.z - this.z; + return (dx * dx) + (dy * dy) + (dz * dz); + } + + double distanceTo(BlockPos pos) { + return Math.sqrt(this.distanceToSq(pos)); + } + + BlockPos offset(Face face, int n) { + switch (face) { + case UP: return up(n); + case DOWN: return down(n); + case NORTH: return north(n); + case SOUTH: return south(n); + case EAST: return east(n); + default: return west(n); + } + } + + BlockPos up(int n) { + return new BlockPos(this.x, this.y + n, this.z); + } + + BlockPos down(int n) { + return new BlockPos(this.x, this.y - n, this.z); + } + + BlockPos east(int n) { + return new BlockPos(this.x + n, this.y, this.z); + } + + BlockPos west(int n) { + return new BlockPos(this.x - n, this.y, this.z); + } + + BlockPos north(int n) { + return new BlockPos(this.x, this.y, this.z - n); + } + + BlockPos south(int n) { + return new BlockPos(this.x, this.y, this.z + n); + } + + BlockPos plus(int n) { + return new BlockPos(this.x + n, this.y + n, this.z + n); + } + + BlockPos shiftRight(int n) { + return new BlockPos(this.x >> n, this.y >> n, this.z >> n); + } + + BlockPos shiftLeft(int n) { + return new BlockPos(this.x << n, this.y << n, this.z << n); + } + + static int floor(double v) { + final int i = (int) v; + return v < i ? i - 1 : i; + } + + static BlockPos of(double x, double y, double z) { + return new BlockPos(floor(x), floor(y), floor(z)); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof BlockPos)) { + return false; + } + final BlockPos p = (BlockPos) o; + return p.x == this.x && p.y == this.y && p.z == this.z; + } + + @Override + public int hashCode() { + return (this.x * 8734625) ^ (this.y * 2873465) ^ (this.z * 3457689); + } + + @Override + public String toString() { + return "{" + this.x + ", " + this.y + ", " + this.z + "}"; + } +} diff --git a/src/main/java/baritone/process/elytra/pathfinder/Chunk.java b/src/main/java/baritone/process/elytra/pathfinder/Chunk.java new file mode 100644 index 0000000000..8cc39316a3 --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/Chunk.java @@ -0,0 +1,216 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +import java.util.Arrays; + +/** + * A 16 by 384 by 16 column of blocks, one bit each, laid out as an octree so that a cube of 2, 4, + * 8 or 16 blocks can be tested for emptiness with a few long reads. The layout is the native + * library's: 24 slabs of 16x16x16 blocks (512 bytes each); a slab is 8 x8 cubes of 64 bytes; an + * x8 is 8 x4 cubes of 8 bytes; an x4 is 8 x2 cubes of one byte; the 8 bits of that byte are the + * blocks. A slab in which no block has been set is not allocated. Beside the slabs, one byte per + * slab says which of its x8 cubes hold a block, so that the question a search asks first and a ray + * asks of every x16 and x8 it enters, whether a cube that big is empty, is a bit test rather than a + * scan of 64 or 8 longs. + *

+ * Reads and writes are plain (not synchronized), as they were in the native library: a reader + * that races a writer may see a partly written slab, which is the same as before, and nothing + * worse can happen. + */ +public final class Chunk { + + public static final int HEIGHT = 384; + public static final int SLABS = HEIGHT / 16; + static final int SLAB_LONGS = 64; // 512 bytes + static final int X8_BYTES = 64; + static final int X4_BYTES = 8; + + /** A chunk with no blocks. Shared; writes to it throw. */ + public static final Chunk AIR = new Chunk(false); + /** A chunk with every block solid. Shared; writes to it throw. */ + public static final Chunk SOLID = new Chunk(true); + + private final long[][] slabs = new long[SLABS][]; + /** Bit i of entry s is set while x8 cube i of slab s holds a block. A slab that is null has 0. */ + private final int[] filled = new int[SLABS]; + private final boolean shared; + + public Chunk() { + this.shared = false; + } + + private Chunk(boolean solid) { + this.shared = true; + if (solid) { + for (int i = 0; i < SLABS; i++) { + this.slabs[i] = new long[SLAB_LONGS]; + Arrays.fill(this.slabs[i], -1L); + this.filled[i] = 0xFF; + } + } + } + + // index math, the same as the native Chunk.h + static int x8Index(int x, int y, int z) { + return ((x & 8) >> 1) | ((y & 8) >> 2) | ((z & 8) >> 3); + } + + static int x4Index(int x, int y, int z) { + return ((x & 4)) | ((y & 4) >> 1) | ((z & 4) >> 2); + } + + static int x2Index(int x, int y, int z) { + return ((x & 2) << 1) | ((y & 2)) | ((z & 2) >> 1); + } + + static int bitIndex(int x, int y, int z) { + return ((x & 1) << 2) | ((y & 1) << 1) | ((z & 1)); + } + + /** Byte offset, within its slab, of the x2 cube that holds (x, y, z). */ + static int x2Offset(int x, int y, int z) { + return x8Index(x, y, z) * X8_BYTES + x4Index(x, y, z) * X4_BYTES + x2Index(x, y, z); + } + + /** The byte at {@code off} in a slab. */ + static int byteAt(long[] slab, int off) { + return (int) (slab[off >>> 3] >>> ((off & 7) << 3)) & 0xFF; + } + + static boolean allZero(long[] slab, int from, int longs) { + long acc = 0; + for (int i = 0; i < longs; i++) { + acc |= slab[from + i]; + } + return acc == 0; + } + + /** The slab holding y, or null if nothing has been set in it (or y is outside the chunk). */ + long[] slab(int y) { + final int i = y >> 4; + return i >= 0 && i < SLABS ? this.slabs[i] : null; + } + + /** Which x8 cubes of the slab holding y hold a block, one bit each in x8Index order; 0 outside the chunk. */ + int filled(int y) { + final int i = y >> 4; + return i >= 0 && i < SLABS ? this.filled[i] : 0; + } + + /** Coordinates are chunk relative: x and z in 0..15, y in 0..383. */ + public boolean isSolid(int x, int y, int z) { + final long[] s = this.slabs[y >> 4]; + if (s == null) { + return false; + } + final int off = x2Offset(x, y, z); + return ((s[off >>> 3] >>> (((off & 7) << 3) + bitIndex(x, y, z))) & 1L) != 0; + } + + /** Coordinates are chunk relative: x and z in 0..15, y in 0..383. */ + public void setBlock(int x, int y, int z, boolean solid) { + if (this.shared) { + throw new UnsupportedOperationException("the shared air and solid chunks are read only"); + } + long[] s = this.slabs[y >> 4]; + if (s == null) { + if (!solid) { + return; + } + s = this.slabs[y >> 4] = new long[SLAB_LONGS]; + } + final int off = x2Offset(x, y, z); + final long mask = 1L << (((off & 7) << 3) + bitIndex(x, y, z)); + final int x8 = x8Index(x, y, z); + if (solid) { + // the x8 is marked before its block is set, so a reader racing this write never skips a block it can see + this.filled[y >> 4] |= 1 << x8; + s[off >>> 3] |= mask; + } else { + s[off >>> 3] &= ~mask; + if (allZero(s, x8 * (X8_BYTES / 8), X8_BYTES / 8)) { + this.filled[y >> 4] &= ~(1 << x8); + } + } + } + + /** Sets every block of one 16x16x16 section (0..23) at once. */ + public void fillSection(int section, boolean solid) { + if (this.shared) { + throw new UnsupportedOperationException("the shared air and solid chunks are read only"); + } + if (!solid) { + this.filled[section] = 0; + this.slabs[section] = null; + return; + } + long[] s = this.slabs[section]; + if (s == null) { + s = this.slabs[section] = new long[SLAB_LONGS]; + } + this.filled[section] = 0xFF; + Arrays.fill(s, -1L); + } + + public boolean isEmpty(Size size, int x, int y, int z) { + switch (size) { + case X1: return !isSolid(x, y, z); + case X2: return isEmptyX2(x, y, z); + case X4: return isEmptyX4(x, y, z); + case X8: return isEmptyX8(x, y, z); + default: return isEmptyX16(y); + } + } + + public boolean isEmptyX16(int y) { + return this.filled[y >> 4] == 0; + } + + public boolean isEmptyX8(int x, int y, int z) { + return (this.filled[y >> 4] & (1 << x8Index(x, y, z))) == 0; + } + + public boolean isEmptyX4(int x, int y, int z) { + final long[] s = this.slabs[y >> 4]; + return s == null || s[x8Index(x, y, z) * (X8_BYTES / 8) + x4Index(x, y, z)] == 0; + } + + public boolean isEmptyX2(int x, int y, int z) { + final long[] s = this.slabs[y >> 4]; + return s == null || byteAt(s, x2Offset(x, y, z)) == 0; + } + + /** The chunk's 12288 bytes in the native layout (little endian words), for hashing in tests. */ + byte[] toBytes() { + final byte[] out = new byte[SLABS * SLAB_LONGS * 8]; + for (int i = 0; i < SLABS; i++) { + final long[] s = this.slabs[i]; + if (s == null) { + continue; + } + for (int j = 0; j < SLAB_LONGS; j++) { + long v = s[j]; + for (int k = 0; k < 8; k++) { + out[(i * SLAB_LONGS + j) * 8 + k] = (byte) (v >>> (k * 8)); + } + } + } + return out; + } +} diff --git a/src/main/java/baritone/process/elytra/pathfinder/ChunkGeneratorHell.java b/src/main/java/baritone/process/elytra/pathfinder/ChunkGeneratorHell.java new file mode 100644 index 0000000000..65140296ee --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/ChunkGeneratorHell.java @@ -0,0 +1,169 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinTask; + +/** The nether's terrain shape for a seed, as Minecraft generated it and the native library ported it. */ +final class ChunkGeneratorHell { + + private final NoiseGeneratorOctaves lperlinNoise1; + private final NoiseGeneratorOctaves lperlinNoise2; + private final NoiseGeneratorOctaves perlinNoise1; + + private ChunkGeneratorHell(NoiseGeneratorOctaves lperlinNoise1, NoiseGeneratorOctaves lperlinNoise2, NoiseGeneratorOctaves perlinNoise1) { + this.lperlinNoise1 = lperlinNoise1; + this.lperlinNoise2 = lperlinNoise2; + this.perlinNoise1 = perlinNoise1; + } + + static ChunkGeneratorHell fromSeed(long seed) { + final Random rand = new Random(seed); + // constructed in this order: they share the one random + final NoiseGeneratorOctaves a = new NoiseGeneratorOctaves(rand, 16); + final NoiseGeneratorOctaves b = new NoiseGeneratorOctaves(rand, 16); + final NoiseGeneratorOctaves c = new NoiseGeneratorOctaves(rand, 8); + return new ChunkGeneratorHell(a, b, c); + } + + /** Fills {@code chunk} with the terrain of chunk (x, z). Only sets blocks, never clears them. */ + void generateChunk(int x, int z, Chunk chunk) { + prepareHeights(x, z, chunk); + } + + Chunk generateChunk(int x, int z) { + final Chunk chunk = new Chunk(); + prepareHeights(x, z, chunk); + return chunk; + } + + private static final int X_SIZE = 5; + private static final int Y_SIZE = 17; + private static final int Z_SIZE = 5; + + private double[] getHeights(int xOffset, int yOffset, int zOffset) { + final double[] buffer = new double[X_SIZE * Y_SIZE * Z_SIZE]; + // The three noise fields take most of a chunk's time and do not depend on each other, so + // two go to the common pool while this thread does the third, as the native library did + // with its own worker threads. A pool thread that is itself generating a chunk helps out + // while it waits, so nesting is fine. + final ForkJoinTask arTask = ForkJoinPool.commonPool().submit((Callable) () -> + this.lperlinNoise1.generateNoiseOctaves(xOffset, yOffset, zOffset, X_SIZE, Y_SIZE, Z_SIZE, 684.412, 2053.236, 684.412)); + final ForkJoinTask brTask = ForkJoinPool.commonPool().submit((Callable) () -> + this.lperlinNoise2.generateNoiseOctaves(xOffset, yOffset, zOffset, X_SIZE, Y_SIZE, Z_SIZE, 684.412, 2053.236, 684.412)); + final double[] pnr = this.perlinNoise1.generateNoiseOctaves(xOffset, yOffset, zOffset, X_SIZE, Y_SIZE, Z_SIZE, 8.555150000000001, 34.2206, 8.555150000000001); + final double[] ar = arTask.join(); + final double[] br = brTask.join(); + + int i = 0; + final double[] adouble = new double[Y_SIZE]; + for (int j = 0; j < Y_SIZE; ++j) { + adouble[j] = Math.cos((double) j * Math.PI * 6.0 / (double) Y_SIZE) * 2.0; + double d2 = (double) j; + if (j > Y_SIZE / 2) { + d2 = (double) (Y_SIZE - 1 - j); + } + if (d2 < 4.0) { + d2 = 4.0 - d2; + adouble[j] -= d2 * d2 * d2 * 10.0; + } + } + + for (int l = 0; l < X_SIZE; ++l) { + for (int i1 = 0; i1 < Z_SIZE; ++i1) { + for (int k = 0; k < Y_SIZE; ++k) { + final double d4 = adouble[k]; + final double d5 = ar[i] / 512.0; + final double d6 = br[i] / 512.0; + final double d7 = (pnr[i] / 10.0 + 1.0) / 2.0; + double d8; + if (d7 < 0.0) { + d8 = d5; + } else if (d7 > 1.0) { + d8 = d6; + } else { + d8 = d5 + (d6 - d5) * d7; + } + d8 = d8 - d4; + if (k > Y_SIZE - 4) { + final double d9 = (double) ((float) (k - (Y_SIZE - 4)) / 3.0F); + d8 = d8 * (1.0 - d9) + -10.0 * d9; + } + buffer[i] = d8; + ++i; + } + } + } + return buffer; + } + + private void prepareHeights(int x, int z, Chunk primer) { + final double[] buffer = this.getHeights(x * 4, 0, z * 4); + final int j = 64 / 2 + 1; // 64 = sea level + + for (int j1 = 0; j1 < 4; ++j1) { + for (int k1 = 0; k1 < 4; ++k1) { + for (int l1 = 0; l1 < 16; ++l1) { + double d1 = buffer[((j1 + 0) * 5 + k1 + 0) * 17 + l1 + 0]; + double d2 = buffer[((j1 + 0) * 5 + k1 + 1) * 17 + l1 + 0]; + double d3 = buffer[((j1 + 1) * 5 + k1 + 0) * 17 + l1 + 0]; + double d4 = buffer[((j1 + 1) * 5 + k1 + 1) * 17 + l1 + 0]; + final double d5 = (buffer[((j1 + 0) * 5 + k1 + 0) * 17 + l1 + 1] - d1) * 0.125; + final double d6 = (buffer[((j1 + 0) * 5 + k1 + 1) * 17 + l1 + 1] - d2) * 0.125; + final double d7 = (buffer[((j1 + 1) * 5 + k1 + 0) * 17 + l1 + 1] - d3) * 0.125; + final double d8 = (buffer[((j1 + 1) * 5 + k1 + 1) * 17 + l1 + 1] - d4) * 0.125; + + for (int i2 = 0; i2 < 8; ++i2) { + double d10 = d1; + double d11 = d2; + final double d12 = (d3 - d1) * 0.25; + final double d13 = (d4 - d2) * 0.25; + + for (int j2 = 0; j2 < 4; ++j2) { + double d15 = d10; + final double d16 = (d11 - d10) * 0.25; + + for (int k2 = 0; k2 < 4; ++k2) { + boolean solid = false; + if (l1 * 8 + i2 < j) { + solid = true; // lava + } + if (d15 > 0.0) { + solid = true; // netherrack + } + if (solid) { + primer.setBlock(j2 + j1 * 4, i2 + l1 * 8, k2 + k1 * 4, true); + } + d15 += d16; + } + d10 += d12; + d11 += d13; + } + d1 += d5; + d2 += d6; + d3 += d7; + d4 += d8; + } + } + } + } + } +} diff --git a/src/main/java/baritone/process/elytra/pathfinder/Face.java b/src/main/java/baritone/process/elytra/pathfinder/Face.java new file mode 100644 index 0000000000..d318777a57 --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/Face.java @@ -0,0 +1,22 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +enum Face { + UP, DOWN, NORTH, SOUTH, EAST, WEST +} diff --git a/src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java b/src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java new file mode 100644 index 0000000000..e3f4b7649b --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java @@ -0,0 +1,385 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A pathfinder and raytracer over a cache of one-bit-per-block chunks, for flying the nether by + * elytra. One instance is what the native library called a context: a world's seed and dimension, + * the chunks it has been given or has generated, and a search that can be cancelled. + *

+ * The method names are the ones the native API had, with objects where it had pointers: a chunk + * is a {@link Chunk} that stays valid for as long as it is referenced, and there is nothing to free. + * Lookups, inserts, the search and rays may all run at the same time from different threads. + */ +public final class NetherPathfinder implements AutoCloseable { + + // How the raytracer will treat chunks that aren't actually observed. + public static final int CACHE_MISS_GENERATE = 0; + public static final int CACHE_MISS_AIR = 1; + public static final int CACHE_MISS_SOLID = 2; + + public static final int DIMENSION_OVERWORLD = 0; + public static final int DIMENSION_NETHER = 1; + public static final int DIMENSION_END = 2; + + static final int STATE_FROM_JAVA = 0; + static final int STATE_FAKE = 1; // could be generated or just air + + private static final int NUM_X_BITS = 26; // 1 + MathHelper.log2(MathHelper.smallestEncompassingPowerOfTwo(30000000)); + private static final int NUM_Z_BITS = NUM_X_BITS; + private static final int NUM_Y_BITS = 64 - NUM_X_BITS - NUM_Z_BITS; + private static final int Y_SHIFT = NUM_Z_BITS; + private static final int X_SHIFT = Y_SHIFT + NUM_Y_BITS; + private static final long X_MASK = (1L << NUM_X_BITS) - 1L; + private static final long Y_MASK = (1L << NUM_Y_BITS) - 1L; + private static final long Z_MASK = (1L << NUM_Z_BITS) - 1L; + + /** A chunk in the table, where it is, and whether it came from the game or was made up. */ + static final class Entry { + final Chunk chunk; + final int x; + final int z; + volatile int state; + + Entry(int state, Chunk chunk, int x, int z) { + this.state = state; + this.chunk = chunk; + this.x = x; + this.z = z; + } + } + + private static final Entry AIR_ENTRY = new Entry(STATE_FAKE, Chunk.AIR, 0, 0); + + private final ConcurrentHashMap chunks = new ConcurrentHashMap<>(); + private final Set checkedRegions = ConcurrentHashMap.newKeySet(); + private final AtomicBoolean cancelFlag = new AtomicBoolean(); + private final ChunkGeneratorHell generator; + private final String baritoneCache; + private final long seed; + private final int dimension; + final int maxHeight; + + /** + * @param seed the world seed, for generating terrain where no chunk is known + * @param baritoneCacheDirCanBeNull Baritone's region directory for the dimension, whose chunks a search loads as it reaches them; or null + * @param dimension one of the DIMENSION_ constants + * @param maxHeight the height the search and rays stay under, 1 to 384 + */ + public NetherPathfinder(long seed, String baritoneCacheDirCanBeNull, int dimension, int maxHeight) { + if (dimension < 0 || dimension > 2) { + throw new IllegalArgumentException("Invalid dimension"); + } + if (maxHeight <= 0 || maxHeight > Chunk.HEIGHT) { + throw new IllegalArgumentException("Invalid max height (must be between 0 and 384)"); + } + this.seed = seed; + this.baritoneCache = baritoneCacheDirCanBeNull; + this.dimension = dimension; + this.maxHeight = maxHeight; + this.generator = ChunkGeneratorHell.fromSeed(seed); + } + + /** The native API's name for the constructor. */ + public static NetherPathfinder newContext(long seed, String baritoneCacheDirCanBeNull, int dimension, int maxHeight) { + return new NetherPathfinder(seed, baritoneCacheDirCanBeNull, dimension, maxHeight); + } + + /** There is no native library any more, so every system is supported. Kept for callers of the old API. */ + public static boolean isThisSystemSupported() { + return true; + } + + public long getSeed() { + return this.seed; + } + + public int getDimension() { + return this.dimension; + } + + public int getMaxHeight() { + return this.maxHeight; + } + + /** Cancels a running search and forgets every chunk. The native API's name for it. */ + public void freeContext() { + close(); + } + + @Override + public void close() { + cancel(); + this.chunks.clear(); + this.checkedRegions.clear(); + } + + /** + * The table's key for chunk (x, z). The pair packed into a long would do, but the table hashes a + * Long to its upper half xor its lower half, x ^ z, which over the chunks around a player takes + * a few dozen values and turns the table's bins into trees that every lookup then walks; so the + * packed pair is multiplied by an odd constant, a bijection that spreads every bit of both + * halves over the upper one. The key is not decoded anywhere: an entry knows its own position. + */ + static long key(int x, int z) { + return (((long) x << 32) | (z & 0xFFFFFFFFL)) * 0x9E3779B97F4A7C15L; + } + + static int dimensionHeight(int dimension) { + return dimension == DIMENSION_OVERWORLD ? 384 : 256; + } + + static long packBlockPos(BlockPos pos) { + return ((long) pos.x & X_MASK) << X_SHIFT | ((long) pos.y & Y_MASK) << Y_SHIFT | ((long) pos.z & Z_MASK); + } + + private static boolean inBounds(int y) { + return y >= 0 && y < Chunk.HEIGHT; + } + + private static void checkFakeChunkMode(int mode) { + if (mode < 0 || mode > 2) { + throw new IllegalArgumentException("fakeChunkMode must be 0 (Generate), 1 (Air), or 2 (Solid)"); + } + } + + // ---- the chunk table ---- + + /** + * Inserts a chunk from the game, replacing any chunk at that position. {@code data} has one + * boolean per block, indexed {@code y << 8 | z << 4 | x}, 16 * 16 * 256 of them (384 in the overworld). + */ + public void insertChunkData(int chunkX, int chunkZ, boolean[] data) { + final int blocksInChunk = 16 * 16 * dimensionHeight(this.dimension); + if (data.length != blocksInChunk) { + throw new IllegalArgumentException(this.dimension == DIMENSION_OVERWORLD ? "input is not 16 * 16 * 384 elements" : "input is not 16 * 16 * 256 elements"); + } + final Chunk chunk = new Chunk(); + for (int i = 0; i < blocksInChunk; i++) { + if (data[i]) { + chunk.setBlock(i & 0xF, i >> 8, (i >> 4) & 0xF, true); + } + } + this.chunks.put(key(chunkX, chunkZ), new Entry(STATE_FROM_JAVA, chunk, chunkX, chunkZ)); + } + + /** Inserts a new, empty chunk from the game at (x, z), replacing any chunk there, and returns it to be filled. */ + public Chunk allocateAndInsertChunk(int x, int z) { + final Chunk chunk = new Chunk(); + this.chunks.put(key(x, z), new Entry(STATE_FROM_JAVA, chunk, x, z)); + return chunk; + } + + /** The chunk at (x, z), or the shared all-solid or all-air chunk if there is none. Do not write to the shared ones. */ + public Chunk getChunkOrDefault(int x, int z, boolean solid) { + final Entry e = this.chunks.get(key(x, z)); + return e != null ? e.chunk : (solid ? Chunk.SOLID : Chunk.AIR); + } + + /** The chunk at (x, z), or null. */ + public Chunk getChunk(int x, int z) { + final Entry e = this.chunks.get(key(x, z)); + return e != null ? e.chunk : null; + } + + /** Marks the chunk at (x, z) as from the game or as made up. Returns true if the chunk existed and the change was made. */ + public boolean setChunkState(int x, int z, boolean fromJava) { + final Entry e = this.chunks.get(key(x, z)); + if (e == null) { + return false; + } + e.state = fromJava ? STATE_FROM_JAVA : STATE_FAKE; + return true; + } + + public boolean hasChunkFromJava(int x, int z) { + final Entry e = this.chunks.get(key(x, z)); + return e != null && e.state == STATE_FROM_JAVA; + } + + /** Whether the table holds chunk (x, z), whichever way it got there. */ + boolean hasChunk(int x, int z) { + return this.chunks.containsKey(key(x, z)); + } + + /** Forgets every chunk more than maxDistanceBlocks (in whole chunks) from (chunkX, chunkZ). */ + public void cullFarChunks(int chunkX, int chunkZ, int maxDistanceBlocks) { + final long distChunks = maxDistanceBlocks / 16; + final long distSq = distChunks * distChunks; + this.chunks.values().removeIf(entry -> { + final long dx = entry.x - chunkX; + final long dz = entry.z - chunkZ; + return dx * dx + dz * dz > distSq; + }); + } + + /** How many chunks the table holds. */ + public int chunkCount() { + return this.chunks.size(); + } + + // ---- searching ---- + + /** + * A path from (x1, y1, z1) towards (x2, y2, z2), or null if none was found in time or the + * search was cancelled. The segment is finished if it reached the goal. + * + * @param atLeastX4 do not squeeze through cubes smaller than 4 blocks + * @param refine drop points that are visible from an earlier one + * @param failTimeoutInMillis give up after this long with nothing; 0 for 30 seconds + * @param defaultAirElseGenerate treat unknown chunks as air, instead of generating them from the seed + * @param fakeChunkCost the cost of a step through a chunk that was not given by the game, against 1 for one that was + */ + public PathSegment pathFind(int x1, int y1, int z1, int x2, int y2, int z2, boolean atLeastX4, boolean refine, int failTimeoutInMillis, boolean defaultAirElseGenerate, double fakeChunkCost) { + if (!inBounds(y1) || !inBounds(y2)) { + throw new IllegalArgumentException("Invalid y1 or y2"); + } + this.cancelFlag.set(false); + final Size size = atLeastX4 ? Size.X4 : Size.X2; + final NodePos start = PathFinder.findAir(this, size, new BlockPos(x1, y1, z1), defaultAirElseGenerate); + final NodePos goal = PathFinder.findAir(this, size, new BlockPos(x2, y2, z2), defaultAirElseGenerate); + final PathFinder.Path path = PathFinder.findPathSegment(this, start, goal, atLeastX4, failTimeoutInMillis, defaultAirElseGenerate, fakeChunkCost); + if (path == null) { + return null; + } + final List blocks = refine ? Raytracer.refine(this, path.blocks) : path.blocks; + final long[] packed = new long[blocks.size()]; + for (int i = 0; i < packed.length; i++) { + packed[i] = packBlockPos(blocks.get(i)); + } + return new PathSegment(path.type == PathFinder.Path.Type.FINISHED, packed); + } + + /** Asks a running search to stop. Returns whether it had already been asked. */ + public boolean cancel() { + return this.cancelFlag.getAndSet(true); + } + + boolean isCancelled() { + return this.cancelFlag.get(); + } + + boolean clearCancelled() { + return this.cancelFlag.getAndSet(false); + } + + // ---- rays ---- + + /** + * Traces {@code inputs} segments, start[i*3..] to end[i*3..]. hitsOut[i] says whether segment i + * hit a solid block, and if hitPosOutCanBeNull is given, where. Points and the segments between + * them must have 0 <= y < 384. + */ + public void raytrace(int fakeChunkMode, int inputs, double[] start, double[] end, boolean[] hitsOut, double[] hitPosOutCanBeNull) { + checkFakeChunkMode(fakeChunkMode); + if (start.length < (inputs * 3) || end.length < (inputs * 3) || hitsOut.length < inputs || (hitPosOutCanBeNull != null && hitPosOutCanBeNull.length < (inputs * 3))) { + throw new IllegalArgumentException("Bad array lengths idiot"); + } + for (int i = 0; i < inputs; i++) { + hitsOut[i] = Raytracer.raytrace(this, start[i * 3], start[i * 3 + 1], start[i * 3 + 2], end[i * 3], end[i * 3 + 1], end[i * 3 + 2], fakeChunkMode, hitPosOutCanBeNull, i * 3); + } + } + + /** + * With anyIfTrueElseAll, the index of the first segment that is clear, else the index of the + * first that is blocked; -1 if there is none. So -1 means "none clear" in the first mode and + * "all clear" in the second. + */ + public int isVisibleMulti(int fakeChunkMode, int inputs, double[] start, double[] end, boolean anyIfTrueElseAll) { + checkFakeChunkMode(fakeChunkMode); + if (start.length < (inputs * 3) || end.length < (inputs * 3)) { + throw new IllegalArgumentException("Bad array lengths idiot"); + } + for (int i = 0; i < inputs; i++) { + final boolean hit = Raytracer.raytrace(this, start[i * 3], start[i * 3 + 1], start[i * 3 + 2], end[i * 3], end[i * 3 + 1], end[i * 3 + 2], fakeChunkMode, null, 0); + if (!hit) { + if (anyIfTrueElseAll) { + return i; + } + } else if (!anyIfTrueElseAll) { + return i; + } + } + return -1; + } + + /** Whether there is line of sight between the two points. */ + public boolean isVisible(int fakeChunkMode, double x1, double y1, double z1, double x2, double y2, double z2) { + checkFakeChunkMode(fakeChunkMode); + return !Raytracer.raytrace(this, x1, y1, z1, x2, y2, z2, fakeChunkMode, null, 0); + } + + // ---- for the search and the rays ---- + + Entry getChunkOrAir(int cx, int cz) { + final Entry e = this.chunks.get(key(cx, cz)); + return e != null ? e : AIR_ENTRY; + } + + Chunk getRealChunkOrDefault(int cx, int cz, boolean solid) { + final Entry e = this.chunks.get(key(cx, cz)); + if (e == null || e.state != STATE_FROM_JAVA) { + return solid ? Chunk.SOLID : Chunk.AIR; + } + return e.chunk; + } + + /** The chunk at (cx, cz), generated from the seed and inserted if there is none. */ + Chunk getOrGenChunk(int cx, int cz) { + final long key = key(cx, cz); + final Entry e = this.chunks.get(key); + if (e != null) { + return e.chunk; + } + final Chunk chunk = this.generator.generateChunk(cx, cz); + final Entry previous = this.chunks.putIfAbsent(key, new Entry(STATE_FAKE, chunk, cx, cz)); + // someone else generated this chunk while we were generating it + return previous != null ? previous.chunk : chunk; + } + + Chunk getRealChunkFromCacheOrFakeChunkMaybeGen(int cx, int cz, int fakeChunkMode) { + if (fakeChunkMode == CACHE_MISS_GENERATE) { + return getOrGenChunk(cx, cz); + } + return getRealChunkOrDefault(cx, cz, fakeChunkMode == CACHE_MISS_SOLID); + } + + /** + * Loads the Baritone region holding chunk (cx, cz) the first time it is asked about. A chunk + * that is already in the table wins over the file's. Returns the nanoseconds spent on the file. + */ + long tryLoadRegion(int cx, int cz) { + if (this.baritoneCache == null) { + return 0; + } + final int regionX = cx >> 5; + final int regionZ = cz >> 5; + if (!this.checkedRegions.add(key(regionX, regionZ))) { + return 0; + } + final long t1 = System.nanoTime(); + final boolean read = BaritoneRegion.load(this.baritoneCache, regionX, regionZ, + (x, z, chunk) -> this.chunks.putIfAbsent(key(x, z), new Entry(STATE_FROM_JAVA, chunk, x, z))); + return read ? System.nanoTime() - t1 : 0; + } +} diff --git a/src/main/java/baritone/process/elytra/pathfinder/NodePos.java b/src/main/java/baritone/process/elytra/pathfinder/NodePos.java new file mode 100644 index 0000000000..dc235e4ade --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/NodePos.java @@ -0,0 +1,58 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +/** A cube of the octree: its size and its position in units of that size. */ +final class NodePos { + + final Size size; + /** absolute position divided by the width */ + private final BlockPos pos; + + NodePos(Size size, BlockPos approxPosition) { + this.size = size; + this.pos = approxPosition.shiftRight(size.shift()); + } + + BlockPos absolutePosZero() { + return this.pos.shiftLeft(this.size.shift()); + } + + BlockPos absolutePosCenter() { + return this.absolutePosZero().plus(this.size.width() / 2); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof NodePos)) { + return false; + } + final NodePos n = (NodePos) o; + return n.size == this.size && n.pos.equals(this.pos); + } + + @Override + public int hashCode() { + long hash = 3241; + hash = 6406146L * hash + this.size.ordinal(); + hash = 3457689L * hash + this.pos.x; + hash = 8734625L * hash + this.pos.y; + hash = 2873465L * hash + this.pos.z; + return (int) (hash ^ (hash >>> 32)); + } +} diff --git a/src/main/java/baritone/process/elytra/pathfinder/NoiseGeneratorImproved.java b/src/main/java/baritone/process/elytra/pathfinder/NoiseGeneratorImproved.java new file mode 100644 index 0000000000..20b0587058 --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/NoiseGeneratorImproved.java @@ -0,0 +1,124 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +import java.util.Random; + +/** Minecraft's improved Perlin noise, as the native library ported it. */ +final class NoiseGeneratorImproved { + + private static final double[] GRAD_X = {1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, -1.0, 0.0}; + private static final double[] GRAD_Y = {1.0, 1.0, -1.0, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0}; + private static final double[] GRAD_Z = {0.0, 0.0, 0.0, 0.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 0.0, 1.0, 0.0, -1.0}; + + private final short[] permutations = new short[512]; + private final double xCoord; + private final double yCoord; + private final double zCoord; + + NoiseGeneratorImproved(Random random) { + this.xCoord = random.nextDouble() * 256.0; + this.yCoord = random.nextDouble() * 256.0; + this.zCoord = random.nextDouble() * 256.0; + for (int i = 0; i < 256; i++) { + this.permutations[i] = (short) i; + } + for (int l = 0; l < 256; ++l) { + final int j = random.nextInt(256 - l) + l; + final short k = this.permutations[l]; + this.permutations[l] = this.permutations[j]; + this.permutations[j] = k; + this.permutations[l + 256] = this.permutations[l]; + } + } + + private static double lerp(double a, double b, double c) { + return b + a * (c - b); + } + + private static double grad(int hash, double x, double y, double z) { + final int i = hash & 15; + return GRAD_X[i] * x + GRAD_Y[i] * y + GRAD_Z[i] * z; + } + + void populateNoiseArray(double[] noiseArray, double xOffset, double yOffset, double zOffset, int xSize, int ySize, int zSize, double xScale, double yScale, double zScale, double noiseScale) { + if (ySize == 1) { + throw new IllegalArgumentException("ySize == 1 uses a code path the native library did not keep"); + } + final short[] p = this.permutations; + int i = 0; + final double d0 = 1.0 / noiseScale; + int k = -1; + double d1 = 0.0; + double d2 = 0.0; + double d3 = 0.0; + double d4 = 0.0; + + for (int l2 = 0; l2 < xSize; ++l2) { + double d5 = xOffset + (double) l2 * xScale + this.xCoord; + int i3 = (int) d5; + if (d5 < (double) i3) { + --i3; + } + final int j3 = i3 & 255; + d5 = d5 - (double) i3; + final double d6 = d5 * d5 * d5 * (d5 * (d5 * 6.0 - 15.0) + 10.0); + + for (int k3 = 0; k3 < zSize; ++k3) { + double d7 = zOffset + (double) k3 * zScale + this.zCoord; + int l3 = (int) d7; + if (d7 < (double) l3) { + --l3; + } + final int i4 = l3 & 255; + d7 = d7 - (double) l3; + final double d8 = d7 * d7 * d7 * (d7 * (d7 * 6.0 - 15.0) + 10.0); + + for (int j4 = 0; j4 < ySize; ++j4) { + double d9 = yOffset + (double) j4 * yScale + this.yCoord; + int k4 = (int) d9; + if (d9 < (double) k4) { + --k4; + } + final int l4 = k4 & 255; + d9 = d9 - (double) k4; + final double d10 = d9 * d9 * d9 * (d9 * (d9 * 6.0 - 15.0) + 10.0); + + if (j4 == 0 || l4 != k) { + k = l4; + final int l = p[j3] + l4; + final int i1 = p[l] + i4; + final int j1 = p[l + 1] + i4; + final int k1 = p[j3 + 1] + l4; + final int l1 = p[k1] + i4; + final int i2 = p[k1 + 1] + i4; + d1 = lerp(d6, grad(p[i1], d5, d9, d7), grad(p[l1], d5 - 1.0, d9, d7)); + d2 = lerp(d6, grad(p[j1], d5, d9 - 1.0, d7), grad(p[i2], d5 - 1.0, d9 - 1.0, d7)); + d3 = lerp(d6, grad(p[i1 + 1], d5, d9, d7 - 1.0), grad(p[l1 + 1], d5 - 1.0, d9, d7 - 1.0)); + d4 = lerp(d6, grad(p[j1 + 1], d5, d9 - 1.0, d7 - 1.0), grad(p[i2 + 1], d5 - 1.0, d9 - 1.0, d7 - 1.0)); + } + + final double d11 = lerp(d10, d1, d2); + final double d12 = lerp(d10, d3, d4); + final double d13 = lerp(d8, d11, d12); + noiseArray[i++] += d13 * d0; + } + } + } + } +} diff --git a/src/main/java/baritone/process/elytra/pathfinder/NoiseGeneratorOctaves.java b/src/main/java/baritone/process/elytra/pathfinder/NoiseGeneratorOctaves.java new file mode 100644 index 0000000000..414705b115 --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/NoiseGeneratorOctaves.java @@ -0,0 +1,58 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +import java.util.Random; + +final class NoiseGeneratorOctaves { + + private final NoiseGeneratorImproved[] generators; + + NoiseGeneratorOctaves(Random random, int octaves) { + this.generators = new NoiseGeneratorImproved[octaves]; + for (int i = 0; i < octaves; i++) { + this.generators[i] = new NoiseGeneratorImproved(random); + } + } + + private static long lfloor(double value) { + final long i = (long) value; + return value < (double) i ? i - 1L : i; + } + + double[] generateNoiseOctaves(int xOffset, int yOffset, int zOffset, int xSize, int ySize, int zSize, double xScale, double yScale, double zScale) { + final double[] noiseArray = new double[xSize * ySize * zSize]; + double d3 = 1.0; + for (int j = 0; j < this.generators.length; ++j) { + double d0 = (double) xOffset * d3 * xScale; + final double d1 = (double) yOffset * d3 * yScale; + double d2 = (double) zOffset * d3 * zScale; + long k = lfloor(d0); + long l = lfloor(d2); + d0 = d0 - (double) k; + d2 = d2 - (double) l; + k = k % 16777216L; + l = l % 16777216L; + d0 = d0 + (double) k; + d2 = d2 + (double) l; + this.generators[j].populateNoiseArray(noiseArray, d0, d1, d2, xSize, ySize, zSize, xScale * d3, yScale * d3, zScale * d3, d3); + d3 /= 2.0; + } + return noiseArray; + } +} diff --git a/src/main/java/baritone/process/elytra/pathfinder/PathFinder.java b/src/main/java/baritone/process/elytra/pathfinder/PathFinder.java new file mode 100644 index 0000000000..fb911e90c5 --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/PathFinder.java @@ -0,0 +1,406 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ForkJoinTask; + +/** + * A* over cubes of the chunk octrees, as the native PathFinder.cpp had it. A node is a cube of 2 + * to 16 blocks that is all air; a step to a neighbour grows the neighbour to the largest empty + * cube it sits in, or shrinks it into the four smaller cubes on the shared face until they are + * empty or too small. + */ +final class PathFinder { + + static final double MIN_DIST_PATH = 5; // might want to increase this + private static final double MIN_IMPROVEMENT = 0.01; + private static final Face[] ALL_FACES = {Face.UP, Face.DOWN, Face.NORTH, Face.SOUTH, Face.EAST, Face.WEST}; + + static final class Path { + enum Type { + SEGMENT, + FINISHED + } + + final Type type; + final BlockPos start; + final BlockPos goal; // where the path wants to go, not necessarily where it ends + final List blocks; + + Path(Type type, BlockPos start, BlockPos goal, List blocks) { + this.type = type; + this.start = start; + this.goal = goal; + this.blocks = blocks; + } + + BlockPos getEndPos() { + // this should basically never be empty + return !this.blocks.isEmpty() ? this.blocks.get(this.blocks.size() - 1) : this.start; + } + } + + private PathFinder() {} + + private static PathNode getNodeAtPosition(Map map, NodePos pos, BlockPos goal) { + PathNode node = map.get(pos); + if (node == null) { + node = new PathNode(pos, goal); + map.put(pos, node); + } + return node; + } + + private static Path createPath(PathNode end, BlockPos startPos, BlockPos goal, Path.Type type) { + final List blocks = new ArrayList<>(); + for (PathNode current = end; current != null; current = current.previous) { + blocks.add(current.pos.absolutePosCenter()); + } + Collections.reverse(blocks); + return new Path(type, startPos, goal, blocks); + } + + static boolean isInBounds(int worldHeight, BlockPos pos) { + return pos.y >= 0 && pos.y < worldHeight; + } + + private static boolean closeToGoal(NodePos node, BlockPos goal) { + return node.absolutePosCenter().distanceToSq(goal) <= 16 * 16; + } + + private static boolean inGoal(NodePos node, BlockPos goal) { + if (!closeToGoal(node, goal)) return false; + final BlockPos c1 = node.absolutePosZero(); + final BlockPos c2 = c1.plus(node.size.width()); + return goal.x >= c1.x && goal.x <= c2.x && + goal.y >= c1.y && goal.y <= c2.y && + goal.z >= c1.z && goal.z <= c2.z; + } + + private static Path bestPathSoFar(PathNode end, BlockPos startPos, BlockPos goal) { + final double distSq = startPos.distanceToSq(end.pos.absolutePosCenter()); + if (distSq > MIN_DIST_PATH * MIN_DIST_PATH) { + return createPath(end, startPos, goal, Path.Type.SEGMENT); + } + // the path took too long and got nowhere + return null; + } + + // ---- neighbour expansion ---- + + private interface Callback { + void accept(NodePos neighbor, Chunk chunk, int state); + } + + /** + * Called inside a big neighbour cube; returns the 4 sub cubes that are adjacent to the + * original cube. The face is relative to the original cube, the size is the sub cubes'. + */ + private static BlockPos[] neighborCubes(Face face, Size sz, BlockPos origin) { + final int size = sz.width(); + final BlockPos corner; + switch (face) { + case UP: + corner = origin; + return new BlockPos[]{corner, corner.east(size), corner.south(size), corner.east(size).south(size)}; + case DOWN: + corner = origin.up(size); + return new BlockPos[]{corner, corner.east(size), corner.south(size), corner.east(size).south(size)}; + case NORTH: + corner = origin.south(size); + return new BlockPos[]{corner, corner.east(size), corner.up(size), corner.east(size).up(size)}; + case SOUTH: + corner = origin; + return new BlockPos[]{corner, corner.east(size), corner.up(size), corner.east(size).up(size)}; + case EAST: + corner = origin; + return new BlockPos[]{corner, corner.south(size), corner.up(size), corner.south(size).up(size)}; + default: // WEST + corner = origin.east(size); + return new BlockPos[]{corner, corner.south(size), corner.up(size), corner.south(size).up(size)}; + } + } + + private static void forEachNeighborInCube(Chunk chunk, int state, NodePos neighborNode, Face face, Size size, boolean sizeChange, Size minSize, Callback callback) { + if (sizeChange) { + callback.accept(neighborNode, chunk, state); + return; + } + final BlockPos pos = neighborNode.absolutePosZero(); + if (chunk.isEmpty(size, pos.x & 15, pos.y, pos.z & 15)) { + callback.accept(neighborNode, chunk, state); + return; + } + if (size != Size.X1) { + final Size nextSize = size.smaller(); + // Don't shrink cubes to X1 because they suck and make the path try to squeeze through small areas + if (nextSize.ordinal() < minSize.ordinal()) return; + for (BlockPos subCube : neighborCubes(face, nextSize, pos)) { + forEachNeighborInCube(chunk, state, new NodePos(nextSize, subCube), face, nextSize, false, minSize, callback); + } + } + } + + /** Grows the neighbour to the largest empty cube it is in, then iterates what is on the face. */ + private static void growThenIterate(Chunk chunk, int state, NodePos pos, Face face, Size minSize, Callback callback) { + final Size originalSize = pos.size; + final BlockPos bpos = pos.absolutePosZero(); + final int lx = bpos.x & 15; + final int lz = bpos.z & 15; + for (Size s = originalSize; ; s = s.larger()) { + if (s == Size.X16 || !chunk.isEmpty(s.larger(), lx, bpos.y, lz)) { + forEachNeighborInCube(chunk, state, new NodePos(s, bpos), face, s, originalSize != s, minSize, callback); + return; + } + } + } + + // ---- the search ---- + + private static final class Search implements Callback { + final Map map = new HashMap<>(); + final BinaryHeapOpenSet openSet = new BinaryHeapOpenSet(); + final BlockPos goalCenter; + final BlockPos startCenter; + final double fakeChunkCost; + PathNode currentNode; + PathNode bestSoFar; + double bestHeuristicSoFar; + boolean failing = true; + + Search(BlockPos goalCenter, BlockPos startCenter, double fakeChunkCost) { + this.goalCenter = goalCenter; + this.startCenter = startCenter; + this.fakeChunkCost = fakeChunkCost; + } + + @Override + public void accept(NodePos neighborPos, Chunk chunk, int state) { + final PathNode neighborNode = getNodeAtPosition(this.map, neighborPos, this.goalCenter); + final double cost = state == NetherPathfinder.STATE_FROM_JAVA ? 1 : this.fakeChunkCost; + final double tentativeCost = this.currentNode.cost + cost; + if (neighborNode.cost - tentativeCost > MIN_IMPROVEMENT) { + neighborNode.previous = this.currentNode; + neighborNode.cost = tentativeCost; + neighborNode.combinedCost = tentativeCost + neighborNode.estimatedCostToGoal; + + if (neighborNode.isOpen()) { + this.openSet.update(neighborNode); + } else { + this.openSet.insert(neighborNode); // dont double count, dont insert into open set if it's already there + } + + final double heuristic = neighborNode.combinedCost; + if (this.bestHeuristicSoFar - heuristic > MIN_IMPROVEMENT) { + this.bestHeuristicSoFar = heuristic; + this.bestSoFar = neighborNode; + if (this.failing && this.startCenter.distanceToSq(neighborPos.absolutePosCenter()) > MIN_DIST_PATH * MIN_DIST_PATH) { + this.failing = false; + } + } + } + } + } + + /** + * One segment of a path from start towards goal. Returns a FINISHED path if it reached the + * goal, a SEGMENT if it ran out of time but got somewhere, and null if it got nowhere or was + * cancelled. + */ + static Path findPathSegment(NetherPathfinder ctx, NodePos start, NodePos goal, boolean x4Min, int timeoutMs, boolean airIfFake, double fakeChunkCost) { + final int fakeChunkMode = airIfFake ? NetherPathfinder.CACHE_MISS_AIR : NetherPathfinder.CACHE_MISS_GENERATE; + final Size minSize = x4Min ? Size.X4 : Size.X2; + final BlockPos goalCenter = goal.absolutePosCenter(); + final BlockPos startCenter = start.absolutePosCenter(); + + final Search s = new Search(goalCenter, startCenter, fakeChunkCost); + final Set doneFull = new HashSet<>(); + + final PathNode startNode = getNodeAtPosition(s.map, start, goal.absolutePosZero()); + final BlockPos startZero = start.absolutePosZero(); + ctx.tryLoadRegion(startZero.chunkX(), startZero.chunkZ()); + startNode.cost = 0; + startNode.combinedCost = startNode.estimatedCostToGoal; + s.openSet.insert(startNode); + ctx.getRealChunkFromCacheOrFakeChunkMaybeGen(startZero.chunkX(), startZero.chunkZ(), fakeChunkMode); + + s.bestSoFar = startNode; + s.bestHeuristicSoFar = startNode.estimatedCostToGoal; + + final long startTime = System.nanoTime(); + final long primaryTimeoutTime = startTime + 500L * 1_000_000L; + final long timeout = (timeoutMs != 0 ? timeoutMs : 30_000L) * 1_000_000L; + final long failureTimeout = startTime + timeout; + long timeDoingIO = 0; + + int numNodes = 0; + int fakeChunkVisits = 0; // if this gets too high we return + while (!s.openSet.isEmpty()) { + if ((numNodes & 63) == 0) { // only look at the clock once every 64 nodes + final long now = System.nanoTime() - timeDoingIO; + if (now >= failureTimeout || (!s.failing && now >= primaryTimeoutTime)) { + break; + } else if (ctx.isCancelled()) { + return null; + } + } + numNodes++; + + final PathNode currentNode = s.openSet.removeLowest(); + s.currentNode = currentNode; + if (inGoal(currentNode.pos, goalCenter)) { + return createPath(currentNode, startCenter, goalCenter, Path.Type.FINISHED); + } + final NodePos pos = currentNode.pos; + final Size size = pos.size; + final BlockPos bpos = pos.absolutePosZero(); + final int cx = bpos.chunkX(); + final int cz = bpos.chunkZ(); + final NetherPathfinder.Entry currentChunk = ctx.getChunkOrAir(cx, cz); + if (currentChunk.state != NetherPathfinder.STATE_FROM_JAVA) { + fakeChunkVisits++; + } else { + fakeChunkVisits = 0; + } + if (fakeChunkVisits >= 100 && airIfFake) { + return bestPathSoFar(s.bestSoFar, startCenter, goalCenter); + } + if (!airIfFake && doneFull.add(NetherPathfinder.key(cx, cz))) { + generateMissingNeighbours(ctx, cx, cz); + } + + for (Face face : ALL_FACES) { + final NodePos neighborNodePos = new NodePos(size, bpos.offset(face, size.width())); + final BlockPos origin = neighborNodePos.absolutePosZero(); + if (face == Face.UP || face == Face.DOWN) { + if (!isInBounds(ctx.maxHeight, origin)) continue; + } + final int neighborCx = origin.chunkX(); + final int neighborCz = origin.chunkZ(); + timeDoingIO += ctx.tryLoadRegion(neighborCx, neighborCz); + final NetherPathfinder.Entry entry = neighborCx == cx && neighborCz == cz ? currentChunk : ctx.getChunkOrAir(neighborCx, neighborCz); + growThenIterate(entry.chunk, entry.state, neighborNodePos, face, minSize, s); + } + } + return bestPathSoFar(s.bestSoFar, startCenter, goalCenter); + } + + private static final int[] NEIGHBOUR_OFFSETS = {0, -1, 0, 1, 1, 0, -1, 0}; // north, south, east, west + + /** + * Generates those of the four chunks around (cx, cz) that the table does not have yet, all at + * once: the first on this thread and the others on the common pool, as the native library did + * on its worker threads. A chunk the table has costs one probe. The native library handed all + * four to its threads whether they existed or not, and its threads were idle workers of its own; + * a task on the common pool costs more than the probe, and a search over terrain that is all + * there, which is what a flight over loaded chunks runs, would pay it for every chunk it enters. + */ + private static void generateMissingNeighbours(NetherPathfinder ctx, int cx, int cz) { + List> missing = null; + for (int i = 0; i < NEIGHBOUR_OFFSETS.length; i += 2) { + final int nx = cx + NEIGHBOUR_OFFSETS[i]; + final int nz = cz + NEIGHBOUR_OFFSETS[i + 1]; + if (ctx.hasChunk(nx, nz)) { + continue; + } + if (missing == null) { + missing = new ArrayList<>(4); + } + missing.add(ForkJoinTask.adapt((Runnable) () -> ctx.getOrGenChunk(nx, nz))); + } + if (missing != null) { + ForkJoinTask.invokeAll(missing); // runs the first here and forks the rest to the pool + } + } + + /** Segment after segment until the goal, generating terrain. What the native main.cpp ran. */ + static Path findPathFull(NetherPathfinder ctx, NodePos start, NodePos goal, double fakeChunkCost) { + if (!isInBounds(ctx.maxHeight, start.absolutePosCenter())) { + throw new IllegalArgumentException("start is out of bounds"); + } + final List segments = new ArrayList<>(); + while (true) { + final NodePos lastPathEnd = !segments.isEmpty() ? new NodePos(Size.X2, segments.get(segments.size() - 1).getEndPos()) : start; + final Path path = findPathSegment(ctx, lastPathEnd, goal, true, 0, false, fakeChunkCost); + if (path == null) { + if (ctx.clearCancelled()) { + return null; + } + break; + } + final BlockPos end = path.getEndPos(); + ctx.cullFarChunks(end.chunkX(), end.chunkZ(), 200); + segments.add(path); + if (path.type == Path.Type.FINISHED) break; + } + if (segments.isEmpty()) { + return null; + } + final List blocks = new ArrayList<>(); + for (Path segment : segments) { + blocks.addAll(segment.blocks); + } + final Path first = segments.get(0); + return new Path(segments.get(segments.size() - 1).type, first.start, first.goal, blocks); + } + + /** The nearest cube of the given size around start that is all air, searching outwards. */ + static NodePos findAir(NetherPathfinder ctx, Size size, BlockPos start1x, boolean airIfFake) { + if (!isInBounds(ctx.maxHeight, start1x)) { + throw new IllegalArgumentException("position is out of bounds: " + start1x); + } + final NodePos start = new NodePos(size, start1x); + final ArrayDeque queue = new ArrayDeque<>(); + final Set visited = new HashSet<>(); + queue.add(start); + visited.add(start); + final int w = size.width(); + while (!queue.isEmpty()) { + final NodePos node = queue.poll(); + final BlockPos blockPos = node.absolutePosZero(); + if (isInBounds(ctx.maxHeight, blockPos)) { + final Chunk chunk = airIfFake + ? ctx.getChunkOrAir(blockPos.chunkX(), blockPos.chunkZ()).chunk + : ctx.getOrGenChunk(blockPos.chunkX(), blockPos.chunkZ()); + if (chunk.isEmpty(size, blockPos.x & 15, blockPos.y, blockPos.z & 15)) { + return node; + } + push(queue, visited, new NodePos(size, blockPos.west(w))); + push(queue, visited, new NodePos(size, blockPos.east(w))); + push(queue, visited, new NodePos(size, blockPos.north(w))); + push(queue, visited, new NodePos(size, blockPos.south(w))); + push(queue, visited, new NodePos(size, blockPos.up(w))); + push(queue, visited, new NodePos(size, blockPos.down(w))); + } + } + // shouldn't be possible to exit the while loop + throw new IllegalStateException("no air anywhere around " + start1x); + } + + private static void push(ArrayDeque queue, Set visited, NodePos pos) { + if (visited.add(pos)) queue.add(pos); + } +} diff --git a/src/main/java/baritone/process/elytra/pathfinder/PathNode.java b/src/main/java/baritone/process/elytra/pathfinder/PathNode.java new file mode 100644 index 0000000000..d50047fc43 --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/PathNode.java @@ -0,0 +1,43 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +final class PathNode { + + static final double COST_INF = 1000000.0; + + final NodePos pos; + final double estimatedCostToGoal; + double cost = COST_INF; + double combinedCost = 0; + PathNode previous = null; + int heapPosition = -1; + + PathNode(NodePos pos, BlockPos goal) { + this.pos = pos; + this.estimatedCostToGoal = heuristic(pos, goal); + } + + boolean isOpen() { + return this.heapPosition != -1; + } + + private static double heuristic(NodePos pos, BlockPos goal) { + return pos.absolutePosCenter().distanceTo(goal) - (pos.size.width() * 4); + } +} diff --git a/src/main/java/baritone/process/elytra/pathfinder/PathSegment.java b/src/main/java/baritone/process/elytra/pathfinder/PathSegment.java new file mode 100644 index 0000000000..bf2e28ce1f --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/PathSegment.java @@ -0,0 +1,29 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +public class PathSegment { + public final boolean finished; + public final long[] packed; + + + public PathSegment(boolean finished, long[] packed) { + this.finished = finished; + this.packed = packed; + } +} \ No newline at end of file diff --git a/src/main/java/baritone/process/elytra/pathfinder/Raytracer.java b/src/main/java/baritone/process/elytra/pathfinder/Raytracer.java new file mode 100644 index 0000000000..e70e4a5110 --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/Raytracer.java @@ -0,0 +1,379 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +import java.util.ArrayList; +import java.util.List; + +/** + * A ray through the chunk octrees: "An efficient parametric algorithm for octree traversal" + * (Revelles, Urena, Lastra), as the native Refiner.cpp had it. The traversal only walks positive + * directions, so the ray is reflected around each x16 node's centre and the child indices are + * flipped back with {@code a}. + */ +final class Raytracer { + + static final int MISS = 0; + static final int FINISHED = 1; + static final int HIT = 2; + + static final int PLANE_YZ = 0; + static final int PLANE_XZ = 1; + static final int PLANE_XY = 2; + + /** What one x16 step reported, beyond its return code. */ + private static final class Step { + int exitPlane; + double hitLen; + } + + private Raytracer() {} + + private static double reflect(double x, double target) { + return target - (x - target); + } + + private static double max(double x, double y) { + if (Double.isNaN(y)) { + return x; + } + return x > y ? x : y; + } + + private static double min(double x, double y) { + if (Double.isNaN(y)) { + return x; + } + return x < y ? x : y; + } + + private static int exitPlane(double tx1, double ty1, double tz1) { + if (tx1 < ty1) { + if (tx1 < tz1) return PLANE_YZ; + } else { + if (ty1 < tz1) return PLANE_XZ; + } + return PLANE_XY; + } + + private static int firstNode(double tx0, double ty0, double tz0, double txm, double tym, double tzm) { + int answer = 0; + if (tx0 > ty0) { + if (tx0 > tz0) { // plane YZ + if (tym < tx0) answer |= 2; + if (tzm < tx0) answer |= 1; + return answer; + } + } else { + if (ty0 > tz0) { // plane XZ + if (txm < ty0) answer |= 4; + if (tzm < ty0) answer |= 1; + return answer; + } + } + // plane XY + if (txm < tz0) answer |= 4; + if (tym < tz0) answer |= 2; + return answer; + } + + private static int newNode(double txm, int x, double tym, int y, double tzm, int z) { + int out = z; // XY plane + if (txm < tym) { + out = txm < tzm ? x : out; // YZ plane + } else { + out = tym < tzm ? y : out; // XZ plane + } + return out; + } + + private static double m(double origin, int nodeMin, int nodeMax, double t0, double t1) { + if (t0 == Double.POSITIVE_INFINITY || t1 == Double.POSITIVE_INFINITY) { + // 3.3 + final int center = (nodeMin + nodeMax) / 2; + return origin < center ? Double.POSITIVE_INFINITY : Double.NEGATIVE_INFINITY; + } + return 0.5 * (t0 + t1); + } + + /** Bytes of a child of a node at each level: x16 (4) has x8 children of 64 bytes, and so on. x2's children are bits. */ + private static final int[] CHILD_BYTES = {0, 0, 1, 8, 64}; + + private static boolean emptyNode(long[] slab, int filled, int off, int level) { + switch (level) { + case 4: return filled == 0; + case 3: return (filled & (1 << (off >>> 6))) == 0; // the x8 at byte offset off is x8 number off / 64 + case 2: return slab[off >>> 3] == 0; + default: return Chunk.byteAt(slab, off) == 0; + } + } + + /** + * Walks the ray through one node. level 4 is an x16 (the whole slab), 0 a block; a node is + * the slab plus a byte offset, and at level 0 {@code bit} says which bit of the byte. {@code + * filled} is the slab's summary of which of its x8 cubes hold a block, see {@link Chunk#filled}. + * Returns NaN for a miss, positive infinity when the ray ended inside without hitting, and + * otherwise the length along the ray at which it hit. + */ + private static double procSubtree(int a, double ox, double oy, double oz, double targetLen, + double tx0, double ty0, double tz0, double tx1, double ty1, double tz1, + int level, int nx, int ny, int nz, long[] slab, int filled, int off, int bit) { + // if this node is behind us + if (tx1 < 0.0 || ty1 < 0.0 || tz1 < 0.0) { + return Double.NaN; + } + if (tx0 >= targetLen || ty0 >= targetLen || tz0 >= targetLen) { + return Double.POSITIVE_INFINITY; + } + if (level == 0) { // leaf + if (((Chunk.byteAt(slab, off) >> bit) & 1) == 0) { + return Double.NaN; + } + if (tx0 > ty0 && tx0 > tz0) { + return tx0; // plane YZ + } else if (ty0 > tz0) { + return ty0; // plane XZ + } else { + return tz0; // plane XY + } + } + if (slab == null || emptyNode(slab, filled, off, level)) { + // we know that all the leafs are empty + return Double.NaN; + } + final int w = 1 << level; + final double txm = m(ox, nx, nx + w, tx0, tx1); + final double tym = m(oy, ny, ny + w, ty0, ty1); + final double tzm = m(oz, nz, nz + w, tz0, tz1); + + final int half = w >> 1; + final int childLevel = level - 1; + final int childBytes = CHILD_BYTES[level]; + int currNode = firstNode(tx0, ty0, tz0, txm, tym, tzm); + do { + final int i; + final double r; + switch (currNode) { + case 0: + i = a; + r = procSubtree(a, ox, oy, oz, targetLen, tx0, ty0, tz0, txm, tym, tzm, childLevel, nx + ((i & 4) != 0 ? half : 0), ny + ((i & 2) != 0 ? half : 0), nz + ((i & 1) != 0 ? half : 0), slab, filled, level == 1 ? off : off + i * childBytes, i); + if (!Double.isNaN(r)) return r; + currNode = newNode(txm, 4, tym, 2, tzm, 1); + break; + case 1: + i = 1 ^ a; + r = procSubtree(a, ox, oy, oz, targetLen, tx0, ty0, tzm, txm, tym, tz1, childLevel, nx + ((i & 4) != 0 ? half : 0), ny + ((i & 2) != 0 ? half : 0), nz + ((i & 1) != 0 ? half : 0), slab, filled, level == 1 ? off : off + i * childBytes, i); + if (!Double.isNaN(r)) return r; + currNode = newNode(txm, 5, tym, 3, tz1, 8); + break; + case 2: + i = 2 ^ a; + r = procSubtree(a, ox, oy, oz, targetLen, tx0, tym, tz0, txm, ty1, tzm, childLevel, nx + ((i & 4) != 0 ? half : 0), ny + ((i & 2) != 0 ? half : 0), nz + ((i & 1) != 0 ? half : 0), slab, filled, level == 1 ? off : off + i * childBytes, i); + if (!Double.isNaN(r)) return r; + currNode = newNode(txm, 6, ty1, 8, tzm, 3); + break; + case 3: + i = 3 ^ a; + r = procSubtree(a, ox, oy, oz, targetLen, tx0, tym, tzm, txm, ty1, tz1, childLevel, nx + ((i & 4) != 0 ? half : 0), ny + ((i & 2) != 0 ? half : 0), nz + ((i & 1) != 0 ? half : 0), slab, filled, level == 1 ? off : off + i * childBytes, i); + if (!Double.isNaN(r)) return r; + currNode = newNode(txm, 7, ty1, 8, tz1, 8); + break; + case 4: + i = 4 ^ a; + r = procSubtree(a, ox, oy, oz, targetLen, txm, ty0, tz0, tx1, tym, tzm, childLevel, nx + ((i & 4) != 0 ? half : 0), ny + ((i & 2) != 0 ? half : 0), nz + ((i & 1) != 0 ? half : 0), slab, filled, level == 1 ? off : off + i * childBytes, i); + if (!Double.isNaN(r)) return r; + currNode = newNode(tx1, 8, tym, 6, tzm, 5); + break; + case 5: + i = 5 ^ a; + r = procSubtree(a, ox, oy, oz, targetLen, txm, ty0, tzm, tx1, tym, tz1, childLevel, nx + ((i & 4) != 0 ? half : 0), ny + ((i & 2) != 0 ? half : 0), nz + ((i & 1) != 0 ? half : 0), slab, filled, level == 1 ? off : off + i * childBytes, i); + if (!Double.isNaN(r)) return r; + currNode = newNode(tx1, 8, tym, 7, tz1, 8); + break; + case 6: + i = 6 ^ a; + r = procSubtree(a, ox, oy, oz, targetLen, txm, tym, tz0, tx1, ty1, tzm, childLevel, nx + ((i & 4) != 0 ? half : 0), ny + ((i & 2) != 0 ? half : 0), nz + ((i & 1) != 0 ? half : 0), slab, filled, level == 1 ? off : off + i * childBytes, i); + if (!Double.isNaN(r)) return r; + currNode = newNode(tx1, 8, ty1, 8, tzm, 7); + break; + default: + i = 7 ^ a; + r = procSubtree(a, ox, oy, oz, targetLen, txm, tym, tzm, tx1, ty1, tz1, childLevel, nx + ((i & 4) != 0 ? half : 0), ny + ((i & 2) != 0 ? half : 0), nz + ((i & 1) != 0 ? half : 0), slab, filled, level == 1 ? off : off + i * childBytes, i); + if (!Double.isNaN(r)) return r; + return Double.NaN; + } + } while (currNode < 8); + return Double.NaN; + } + + /** One x16 node. The ray is already reflected. */ + private static int raytrace16(int a, double rox, double roy, double roz, double rdx, double rdy, double rdz, double targetLen, + int nx, int ny, int nz, long[] slab, int filled, Step step) { + // IEEE stability fix + final double divx = 1 / rdx; + final double divy = 1 / rdy; + final double divz = 1 / rdz; + + final double tx0 = (nx - rox) * divx; + final double tx1 = (nx + 16 - rox) * divx; + final double ty0 = (ny - roy) * divy; + final double ty1 = (ny + 16 - roy) * divy; + final double tz0 = (nz - roz) * divz; + final double tz1 = (nz + 16 - roz) * divz; + + // condition 10 + final double tmin = max(max(tx0, ty0), tz0); + final double tmax = min(min(tx1, ty1), tz1); + if (tmin <= tmax) { + final double r = procSubtree(a, rox, roy, roz, targetLen, tx0, ty0, tz0, tx1, ty1, tz1, 4, nx, ny, nz, slab, filled, 0, 0); + if (r == Double.POSITIVE_INFINITY) { + return FINISHED; + } + if (!Double.isNaN(r)) { + step.hitLen = r; + return HIT; + } + step.exitPlane = exitPlane(tx1, ty1, tz1); + return MISS; + } + // The ray only touches this node along an edge or at a corner, and rounding made the + // interval empty. Either the ray ends there, or it continues into the node on the other + // side of that edge; either way there is nothing to test in this node. + if (tmin >= targetLen) { + return FINISHED; + } + step.exitPlane = exitPlane(tx1, ty1, tz1); + return MISS; + } + + /** + * Traces the segment from (fx, fy, fz) to (tx, ty, tz). Returns true if it hit a solid block; + * the hit position is then written to {@code hitOut} at {@code hitIndex}, if the array is not null. + * The two points must be inside 0 <= y < 384, as must be the segment between them. A segment + * of no length is a point, which is a hit if it is inside a block; a coordinate that is not a + * finite number is refused. + */ + static boolean raytrace(NetherPathfinder ctx, double fx, double fy, double fz, double tx, double ty, double tz, int fakeChunkMode, double[] hitOut, int hitIndex) { + final double vx = tx - fx; + final double vy = ty - fy; + final double vz = tz - fz; + final double targetLen = Math.sqrt(vx * vx + vy * vy + vz * vz); + if (!(targetLen < Double.POSITIVE_INFINITY)) { + // NaN, or an infinite coordinate: dividing by it would make the direction NaN and the + // walk below would step from node to node for ever. The native library did just that. + throw new IllegalArgumentException("ray with a coordinate that is not a finite number: (" + + fx + ", " + fy + ", " + fz + ") to (" + tx + ", " + ty + ", " + tz + ")"); + } + if (targetLen == 0.0) { + // A point. The same division would make the direction NaN here too, and the native + // library exited the process. A ray that starts inside a block reports a hit at its + // origin, so a point does the same, and one in the air hits nothing. + final int bx = BlockPos.floor(fx); + final int by = BlockPos.floor(fy); + final int bz = BlockPos.floor(fz); + final Chunk chunk = ctx.getRealChunkFromCacheOrFakeChunkMaybeGen(bx >> 4, bz >> 4, fakeChunkMode); + if (by < 0 || by >= Chunk.HEIGHT || !chunk.isSolid(bx & 15, by, bz & 15)) { + return false; + } + if (hitOut != null) { + hitOut[hitIndex] = fx; + hitOut[hitIndex + 1] = fy; + hitOut[hitIndex + 2] = fz; + } + return true; + } + final double dx = vx / targetLen; + final double dy = vy / targetLen; + final double dz = vz / targetLen; + int a = 0; + if (dx < 0.0) a |= 4; + if (dy < 0.0) a |= 2; + if (dz < 0.0) a |= 1; + + int nx = BlockPos.floor(fx) & ~15; + int ny = BlockPos.floor(fy) & ~15; + int nz = BlockPos.floor(fz) & ~15; + final Step step = new Step(); + while (true) { + final Chunk chunk = ctx.getRealChunkFromCacheOrFakeChunkMaybeGen(nx >> 4, nz >> 4, fakeChunkMode); + final long[] slab = chunk.slab(ny); + // reflect around the node's centre so the traversal only has to walk positive directions + double rox = fx, roy = fy, roz = fz, rdx = dx, rdy = dy, rdz = dz; + if ((a & 4) != 0) { rox = reflect(fx, nx + 8); rdx = -dx; } + if ((a & 2) != 0) { roy = reflect(fy, ny + 8); rdy = -dy; } + if ((a & 1) != 0) { roz = reflect(fz, nz + 8); rdz = -dz; } + final int result = raytrace16(a, rox, roy, roz, rdx, rdy, rdz, targetLen, nx, ny, nz, slab, chunk.filled(ny), step); + if (result == FINISHED) { + return false; + } + if (result == HIT) { + if (hitOut != null) { + if (step.hitLen < 0.0) { + // the origin was inside an occupied block, so the hit is the origin + hitOut[hitIndex] = fx; + hitOut[hitIndex + 1] = fy; + hitOut[hitIndex + 2] = fz; + } else { + hitOut[hitIndex] = fx + dx * step.hitLen; + hitOut[hitIndex + 1] = fy + dy * step.hitLen; + hitOut[hitIndex + 2] = fz + dz * step.hitLen; + } + } + return true; + } + switch (step.exitPlane) { + case PLANE_XY: + nz += (a & 1) != 0 ? -16 : 16; + break; + case PLANE_XZ: + ny += (a & 2) != 0 ? -16 : 16; + break; + default: + nx += (a & 4) != 0 ? -16 : 16; + break; + } + } + } + + private static int lastVisibleNode(NetherPathfinder ctx, List path, int currentNode) { + if (currentNode == path.size() - 1) { + return currentNode; + } + final BlockPos fromBlock = path.get(currentNode); + int lastVisible = currentNode + 1; // can assume that the next node is always visible from the previous + for (int i = lastVisible + 1; i < path.size(); i++) { + final BlockPos currentBlock = path.get(i); + if (fromBlock.equals(currentBlock)) continue; // the pathfinder can produce 2 consecutive equal points and that breaks the raytracer + if (raytrace(ctx, fromBlock.x, fromBlock.y, fromBlock.z, currentBlock.x, currentBlock.y, currentBlock.z, NetherPathfinder.CACHE_MISS_GENERATE, null, 0)) { + return lastVisible; + } + lastVisible = i; + } + return lastVisible; + } + + /** Drops the points of a path that are visible from an earlier one. */ + static List refine(NetherPathfinder ctx, List path) { + final List out = new ArrayList<>(); + for (int i = 0; i < path.size(); i = lastVisibleNode(ctx, path, i)) { + out.add(path.get(i)); + if (i == path.size() - 1) break; + } + return out; + } +} diff --git a/src/main/java/baritone/process/elytra/pathfinder/Size.java b/src/main/java/baritone/process/elytra/pathfinder/Size.java new file mode 100644 index 0000000000..bfb13800e0 --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/Size.java @@ -0,0 +1,41 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +/** The edge length of a cube in the octree, as a power of two. */ +enum Size { + X1, X2, X4, X8, X16; + + private static final Size[] VALUES = values(); + + int shift() { + return ordinal(); + } + + int width() { + return 1 << ordinal(); + } + + Size smaller() { + return VALUES[ordinal() - 1]; + } + + Size larger() { + return VALUES[ordinal() + 1]; + } +} diff --git a/src/main/java/baritone/process/elytra/pathfinder/package-info.java b/src/main/java/baritone/process/elytra/pathfinder/package-info.java new file mode 100644 index 0000000000..e9e898ff7b --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/package-info.java @@ -0,0 +1,29 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +/** + * A pathfinder and raytracer for flying the Nether by elytra: babbaj's nether-pathfinder + * (https://github.com/babbaj/nether-pathfinder), which Baritone loaded as a native library + * through JNI, in plain Java. Each class is the C++ file of the same name, ported one for one, + * and the tests hold the terrain generator and the raytracer to the native library's own + * recorded answers over a few hundred generated chunks and a few thousand rays. Nothing in here + * refers to Minecraft; the seam is {@link baritone.process.elytra.NetherPathfinderContext}, + * which packs the game's chunks into {@link baritone.process.elytra.pathfinder.Chunk}s and asks + * {@link baritone.process.elytra.pathfinder.NetherPathfinder} for paths and lines of sight. + * The port was written for babbaj/nether-pathfinder#31. + */ +package baritone.process.elytra.pathfinder; diff --git a/src/test/java/baritone/process/elytra/pathfinder/BaritoneRegionTest.java b/src/test/java/baritone/process/elytra/pathfinder/BaritoneRegionTest.java new file mode 100644 index 0000000000..7feb7ff795 --- /dev/null +++ b/src/test/java/baritone/process/elytra/pathfinder/BaritoneRegionTest.java @@ -0,0 +1,118 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.zip.GZIPOutputStream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class BaritoneRegionTest { + + /** A nether region with one chunk at (3, 5) that has the block (1, 2, 3) set. */ + private static byte[] region() throws Exception { + final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (GZIPOutputStream gz = new GZIPOutputStream(bytes); DataOutputStream out = new DataOutputStream(gz)) { + out.writeInt(456022911); + for (int x = 0; x < 32; x++) { + for (int z = 0; z < 32; z++) { + if (x == 3 && z == 5) { + out.writeByte(1); + final byte[] data = new byte[(2 * 16 * 16 * 256) / 8]; + final int bit = (1 << 1) | (3 << 5) | (2 << 9); // positionIndex(1, 2, 3) + data[bit / 8] |= 1 << (6 - (bit % 8)); + out.write(data); + } else { + out.writeByte(0); + } + } + } + } + return bytes.toByteArray(); + } + + @Test + public void parsesAChunk() throws Exception { + final Chunk[] found = new Chunk[1]; + final int[] at = new int[2]; + try (java.io.InputStream in = new java.util.zip.GZIPInputStream(new java.io.ByteArrayInputStream(region()))) { + BaritoneRegion.parse(in, -1, 2, NetherPathfinder.DIMENSION_NETHER, (x, z, chunk) -> { + found[0] = chunk; + at[0] = x; + at[1] = z; + }); + } + assertNotNull(found[0]); + assertEquals(3 - 32, at[0]); + assertEquals(5 + 64, at[1]); + assertTrue(found[0].isSolid(1, 2, 3)); + assertFalse(found[0].isSolid(3, 2, 1)); + assertTrue(found[0].isEmptyX16(64)); + } + + @Test + public void aSearchLoadsTheRegionItReaches() throws Exception { + final Path dir = Files.createTempDirectory("np-region"); + final Path regions = dir.resolve("the_nether_128").resolve("regions"); + Files.createDirectories(regions); + Files.write(regions.resolve("r.0.0.bcr"), region()); + try { + final NetherPathfinder ctx = new NetherPathfinder(1, regions.toString(), NetherPathfinder.DIMENSION_NETHER, 128); + assertNull(ctx.getChunk(3, 5)); + assertTrue(ctx.tryLoadRegion(20, 20) > 0); + assertNotNull(ctx.getChunk(3, 5)); + assertTrue(ctx.hasChunkFromJava(3, 5)); + assertTrue(ctx.getChunk(3, 5).isSolid(1, 2, 3)); + // a region is only read once, and one that does not exist costs nothing + assertEquals(0, ctx.tryLoadRegion(21, 21)); + assertEquals(0, ctx.tryLoadRegion(100, 100)); + // a chunk the game gave before the file is read wins + final NetherPathfinder again = new NetherPathfinder(1, regions.toString(), NetherPathfinder.DIMENSION_NETHER, 128); + final Chunk mine = again.allocateAndInsertChunk(3, 5); + again.tryLoadRegion(0, 0); + assertEquals(mine, again.getChunk(3, 5)); + // and a search reaching the region loads it by itself + final NetherPathfinder searched = new NetherPathfinder(1, regions.toString(), NetherPathfinder.DIMENSION_NETHER, 128); + searched.pathFind(0, 60, 0, 200, 60, 200, true, false, 1000, true, 8.0); + assertNotNull(searched.getChunk(3, 5)); + } finally { + Files.deleteIfExists(regions.resolve("r.0.0.bcr")); + Files.deleteIfExists(regions); + Files.deleteIfExists(regions.getParent()); + Files.deleteIfExists(dir); + } + } + + @Test + public void directoryNamesDecideTheDimension() { + assertEquals(NetherPathfinder.DIMENSION_NETHER, BaritoneRegion.dimensionOf("/a/b/the_nether_128/regions")); + assertEquals(NetherPathfinder.DIMENSION_OVERWORLD, BaritoneRegion.dimensionOf("/a/b/overworld_384/regions")); + assertEquals(NetherPathfinder.DIMENSION_END, BaritoneRegion.dimensionOf("/a/b/the_end_256/regions")); + assertEquals(-1, BaritoneRegion.dimensionOf("/a/b/somewhere/regions")); + } +} diff --git a/src/test/java/baritone/process/elytra/pathfinder/Bench.java b/src/test/java/baritone/process/elytra/pathfinder/Bench.java new file mode 100644 index 0000000000..67a6638850 --- /dev/null +++ b/src/test/java/baritone/process/elytra/pathfinder/Bench.java @@ -0,0 +1,108 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +import java.util.Arrays; + +/** + * The numbers the port is held to, over the same kind of input as the native benchmarks: rays + * through real terrain, path searches over it, and the 100k-block generating search of main.cpp. + * Run it by hand: java -cp ... baritone.process.elytra.pathfinder.Bench + */ +public final class Bench { + + private static long splitmix(long[] s) { + long z = (s[0] += 0x9E3779B97F4A7C15L); + z = (z ^ (z >>> 30)) * 0xBF58476D1CE4E5B9L; + z = (z ^ (z >>> 27)) * 0x94D049BB133111EBL; + return z ^ (z >>> 31); + } + + private static double between(long[] s, double lo, double hi) { + return lo + (splitmix(s) >>> 11) * 0x1.0p-53 * (hi - lo); + } + + public static void main(String[] args) { + final int side = args.length > 0 ? Integer.parseInt(args[0]) : 64; + final int rays = args.length > 1 ? Integer.parseInt(args[1]) : 200000; + + long t0 = System.nanoTime(); + final NetherPathfinder ctx = Oracle.generatedWorld(side); + System.out.printf("%d chunks generated in %d ms%n", side * side, (System.nanoTime() - t0) / 1_000_000); + + final long[] rng = {42}; + final double lim = side * 16.0; + final double[] from = new double[rays * 3], to = new double[rays * 3]; + for (int i = 0; i < rays;) { + final double fx = between(rng, 8.0, lim - 8.0), fy = between(rng, 32.0, 120.0), fz = between(rng, 8.0, lim - 8.0); + if (ctx.getChunkOrDefault((int) fx >> 4, (int) fz >> 4, true).isSolid((int) fx & 15, (int) fy, (int) fz & 15)) continue; + final double a = between(rng, 0, 6.283185307179586), l = between(rng, 30.0, 150.0); + from[i * 3] = fx; from[i * 3 + 1] = fy; from[i * 3 + 2] = fz; + to[i * 3] = Math.max(1.0, Math.min(lim - 1.0, fx + Math.cos(a) * l)); + to[i * 3 + 1] = Math.max(1.0, Math.min(126.0, fy + between(rng, -30.0, 30.0))); + to[i * 3 + 2] = Math.max(1.0, Math.min(lim - 1.0, fz + Math.sin(a) * l)); + i++; + } + final boolean[] hits = new boolean[rays]; + for (int rep = 0; rep < 5; rep++) { + t0 = System.nanoTime(); + ctx.raytrace(NetherPathfinder.CACHE_MISS_SOLID, rays, from, to, hits, null); + final double ms = (System.nanoTime() - t0) / 1e6; + int hit = 0; + for (boolean h : hits) if (h) hit++; + System.out.printf("java: %d rays (30-150 blocks, air origins), SOLID mode: %.0f ms, %.3f us/ray, %.2f%% hit%n", rays, ms, ms * 1000 / rays, 100.0 * hit / rays); + } + + // The same searches twice: treating a chunk the table lacks as air, and generating it. Every + // chunk is in the table by the timed pass, so the second never generates and must cost no + // more than the first; it is the search a flight that predicts terrain runs over the chunks + // it has loaded. The untimed pass generates what the searches reach past the world's edge. + final long searchSeed = rng[0]; + for (int pass = 0; pass < 2; pass++) { + for (boolean airIfFake : new boolean[]{true, false}) { + rng[0] = searchSeed; + for (int dist : new int[]{300, 800}) { + long total = 0, blocks = 0; + int finished = 0; + final int runs = 20; + final long[] us = new long[runs]; // one search that runs into the 500 ms timeout dominates the average + for (int r = 0; r < runs; r++) { + final int sx = (int) between(rng, 16.0, lim - 16.0 - dist), sz = (int) between(rng, 16.0, lim - 16.0); + t0 = System.nanoTime(); + final PathSegment p = ctx.pathFind(sx, 60, sz, sx + dist, 60, sz, true, false, 10000, airIfFake, 8.0); + us[r] = (System.nanoTime() - t0) / 1000; + total += us[r]; + if (p != null) { blocks += p.packed.length; if (p.finished) finished++; } + } + if (pass == 0) continue; + Arrays.sort(us); + System.out.printf("java pathFind %s over %d blocks: avg %d us, median %d us, avg %d path nodes, %d/%d reached the goal%n", + airIfFake ? "airIfFake" : "generating, every chunk present,", dist, total / runs, us[runs / 2], blocks / runs, finished, runs); + } + } + } + + final NetherPathfinder fresh = new NetherPathfinder(Oracle.SEED, null, NetherPathfinder.DIMENSION_NETHER, 128); + t0 = System.nanoTime(); + final NodePos start = PathFinder.findAir(fresh, Size.X4, new BlockPos(0, 50, 0), false); + final NodePos goal = PathFinder.findAir(fresh, Size.X4, new BlockPos(100000, 50, 0), false); + final PathFinder.Path path = PathFinder.findPathFull(fresh, start, goal, 1); + final double s = (System.nanoTime() - t0) / 1e9; + System.out.printf("java: 100k-block generating search (main.cpp): %.2f s, path of %d blocks to %s, %d chunks kept%n", s, path.blocks.size(), path.getEndPos(), fresh.chunkCount()); + } +} diff --git a/src/test/java/baritone/process/elytra/pathfinder/ChunkGeneratorTest.java b/src/test/java/baritone/process/elytra/pathfinder/ChunkGeneratorTest.java new file mode 100644 index 0000000000..e987378fe0 --- /dev/null +++ b/src/test/java/baritone/process/elytra/pathfinder/ChunkGeneratorTest.java @@ -0,0 +1,49 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class ChunkGeneratorTest { + + @Test + public void everyChunkMatchesTheNativeGeneratorBitForBit() throws Exception { + final List expected = Oracle.chunkHashes(); + assertEquals(25 * 25, expected.size()); + final ChunkGeneratorHell generator = ChunkGeneratorHell.fromSeed(Oracle.SEED); + int wrong = 0; + for (String[] line : expected) { + final int x = Integer.parseInt(line[0]); + final int z = Integer.parseInt(line[1]); + final long hash = Long.parseUnsignedLong(line[2], 16); + if (Oracle.fnv1a(generator.generateChunk(x, z).toBytes()) != hash) wrong++; + } + assertEquals("chunks that differ from the native generator", 0, wrong); + } + + @Test + public void generationIsDeterministic() { + final ChunkGeneratorHell a = ChunkGeneratorHell.fromSeed(Oracle.SEED); + final ChunkGeneratorHell b = ChunkGeneratorHell.fromSeed(Oracle.SEED); + assertEquals(Oracle.fnv1a(a.generateChunk(100, -7).toBytes()), Oracle.fnv1a(b.generateChunk(100, -7).toBytes())); + } +} diff --git a/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java b/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java new file mode 100644 index 0000000000..6f4e011cdf --- /dev/null +++ b/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java @@ -0,0 +1,170 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class ChunkTest { + + @Test + public void setAndGetEveryBlockOfASection() { + final Chunk chunk = new Chunk(); + for (int x = 0; x < 16; x++) for (int y = 48; y < 64; y++) for (int z = 0; z < 16; z++) { + final boolean solid = ((x * 7 + y * 3 + z * 11) & 3) == 0; + chunk.setBlock(x, y, z, solid); + } + for (int x = 0; x < 16; x++) for (int y = 48; y < 64; y++) for (int z = 0; z < 16; z++) { + assertEquals(((x * 7 + y * 3 + z * 11) & 3) == 0, chunk.isSolid(x, y, z)); + } + // clearing works too + chunk.setBlock(3, 50, 4, false); + assertFalse(chunk.isSolid(3, 50, 4)); + // untouched sections stay air and unallocated + assertNull(chunk.slab(0)); + assertFalse(chunk.isSolid(0, 0, 0)); + } + + @Test + public void emptinessAtEveryLevel() { + final Chunk chunk = new Chunk(); + assertTrue(chunk.isEmptyX16(64)); + chunk.setBlock(9, 70, 3, true); + assertFalse(chunk.isEmptyX16(64)); + assertFalse(chunk.isEmptyX8(9, 70, 3)); + assertTrue(chunk.isEmptyX8(1, 70, 3)); + assertFalse(chunk.isEmptyX4(9, 70, 3)); + assertTrue(chunk.isEmptyX4(13, 70, 3)); + assertFalse(chunk.isEmptyX2(9, 70, 3)); + assertTrue(chunk.isEmptyX2(11, 70, 3)); + assertFalse(chunk.isEmpty(Size.X1, 9, 70, 3)); + assertTrue(chunk.isEmpty(Size.X1, 8, 70, 3)); + assertTrue(chunk.isEmpty(Size.X16, 0, 80, 0)); + + // clearing a block empties its x8 and x16 again only once it was the last one there + chunk.setBlock(10, 70, 3, true); + chunk.setBlock(9, 70, 3, false); + assertFalse(chunk.isEmptyX8(9, 70, 3)); + assertFalse(chunk.isEmptyX16(64)); + chunk.setBlock(10, 70, 3, false); + assertTrue(chunk.isEmptyX8(9, 70, 3)); + assertTrue(chunk.isEmptyX16(64)); + assertNotNull(chunk.slab(64)); // the slab stays allocated + } + + @Test + public void sharedChunksAreWhatTheySayAndReadOnly() { + assertTrue(Chunk.SOLID.isSolid(0, 0, 0)); + assertTrue(Chunk.SOLID.isSolid(15, 383, 15)); + assertFalse(Chunk.SOLID.isEmptyX16(200)); + assertFalse(Chunk.AIR.isSolid(15, 383, 15)); + assertTrue(Chunk.AIR.isEmptyX16(200)); + try { + Chunk.AIR.setBlock(0, 0, 0, true); + throw new AssertionError("expected UnsupportedOperationException"); + } catch (UnsupportedOperationException expected) { + // good + } + } + + @Test + public void fillSection() { + final Chunk chunk = new Chunk(); + chunk.fillSection(4, true); + assertTrue(chunk.isSolid(0, 64, 0)); + assertTrue(chunk.isSolid(15, 79, 15)); + assertFalse(chunk.isSolid(0, 80, 0)); + assertFalse(chunk.isEmptyX8(15, 79, 15)); + assertTrue(chunk.isEmptyX8(0, 80, 0)); + chunk.fillSection(4, false); + assertTrue(chunk.isEmptyX16(64)); + assertTrue(chunk.isEmptyX8(15, 79, 15)); + chunk.fillSection(4, true); + chunk.setBlock(15, 79, 15, false); + assertFalse("the section still has the other blocks", chunk.isEmptyX8(15, 79, 15)); + } + + @Test + public void insertChunkDataUsesTheBlockStateContainerIndex() { + final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.DIMENSION_NETHER, 128); + final boolean[] data = new boolean[16 * 16 * 256]; + data[(77 << 8) | (5 << 4) | 12] = true; + ctx.insertChunkData(-3, 9, data); + final Chunk chunk = ctx.getChunk(-3, 9); + assertNotNull(chunk); + assertTrue(chunk.isSolid(12, 77, 5)); + assertFalse(chunk.isSolid(5, 77, 12)); + assertTrue(ctx.hasChunkFromJava(-3, 9)); + try { + ctx.insertChunkData(0, 0, new boolean[10]); + throw new AssertionError("expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + // good + } + } + + @Test + public void tableOperations() { + final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.DIMENSION_NETHER, 128); + assertSame(Chunk.AIR, ctx.getChunkOrDefault(1, 1, false)); + assertSame(Chunk.SOLID, ctx.getChunkOrDefault(1, 1, true)); + assertNull(ctx.getChunk(1, 1)); + assertFalse(ctx.setChunkState(1, 1, true)); + + final Chunk chunk = ctx.allocateAndInsertChunk(1, 1); + assertSame(chunk, ctx.getChunk(1, 1)); + assertSame(chunk, ctx.getChunkOrDefault(1, 1, true)); + assertTrue(ctx.hasChunkFromJava(1, 1)); + assertTrue(ctx.setChunkState(1, 1, false)); + assertFalse(ctx.hasChunkFromJava(1, 1)); + // a fake chunk is a default to the search's real-chunk lookup + assertSame(Chunk.SOLID, ctx.getRealChunkOrDefault(1, 1, true)); + assertSame(chunk, ctx.getChunkOrAir(1, 1).chunk); + + // replacing keeps the new one + final Chunk again = ctx.allocateAndInsertChunk(1, 1); + assertSame(again, ctx.getChunk(1, 1)); + + ctx.allocateAndInsertChunk(40, 0); + ctx.allocateAndInsertChunk(-40, 0); + assertEquals(3, ctx.chunkCount()); + ctx.cullFarChunks(0, 0, 200); // 12 chunks + assertEquals(1, ctx.chunkCount()); + assertNotNull(ctx.getChunk(1, 1)); + ctx.close(); + assertEquals(0, ctx.chunkCount()); + } + + @Test + public void generatedChunksAreFakeUntilMarked() { + final NetherPathfinder ctx = new NetherPathfinder(Oracle.SEED, null, NetherPathfinder.DIMENSION_NETHER, 128); + final Chunk chunk = ctx.getOrGenChunk(0, 0); + assertSame(chunk, ctx.getOrGenChunk(0, 0)); + assertFalse(ctx.hasChunkFromJava(0, 0)); + assertSame(Chunk.AIR, ctx.getRealChunkFromCacheOrFakeChunkMaybeGen(0, 0, NetherPathfinder.CACHE_MISS_AIR)); + assertSame(chunk, ctx.getRealChunkFromCacheOrFakeChunkMaybeGen(0, 0, NetherPathfinder.CACHE_MISS_GENERATE)); + ctx.setChunkState(0, 0, true); + assertSame(chunk, ctx.getRealChunkFromCacheOrFakeChunkMaybeGen(0, 0, NetherPathfinder.CACHE_MISS_AIR)); + } +} diff --git a/src/test/java/baritone/process/elytra/pathfinder/DegenerateRaysTest.java b/src/test/java/baritone/process/elytra/pathfinder/DegenerateRaysTest.java new file mode 100644 index 0000000000..efff4c674a --- /dev/null +++ b/src/test/java/baritone/process/elytra/pathfinder/DegenerateRaysTest.java @@ -0,0 +1,144 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * The rays the native library could not take: a segment of no length and a coordinate that is not + * a finite number made it exit the process or walk for ever, and a segment ending exactly on a + * voxel boundary sometimes did. Each test has a timeout because the failure mode is a hang. + */ +public class DegenerateRaysTest { + + private static final int SOLID = NetherPathfinder.CACHE_MISS_SOLID; + + /** Chunk (0, 0) from the game: section 3 (y 48 to 63) all solid, and one block at (5, 70, 5). */ + private static NetherPathfinder world() { + final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.DIMENSION_NETHER, 128); + final Chunk chunk = ctx.allocateAndInsertChunk(0, 0); + chunk.fillSection(3, true); + chunk.setBlock(5, 70, 5, true); + return ctx; + } + + @Test(timeout = 2000) + public void aPointInTheAirIsVisibleFromItself() { + final NetherPathfinder ctx = world(); + assertTrue(ctx.isVisible(SOLID, 3.5, 71.5, 3.5, 3.5, 71.5, 3.5)); + final boolean[] hits = new boolean[1]; + ctx.raytrace(SOLID, 1, new double[]{3.5, 71.5, 3.5}, new double[]{3.5, 71.5, 3.5}, hits, null); + assertFalse(hits[0]); + } + + @Test(timeout = 2000) + public void aPointInsideABlockIsAHitAtItself() { + final NetherPathfinder ctx = world(); + assertFalse(ctx.isVisible(SOLID, 5.5, 70.5, 5.5, 5.5, 70.5, 5.5)); + final boolean[] hits = new boolean[1]; + final double[] hitPos = new double[3]; + ctx.raytrace(SOLID, 1, new double[]{5.25, 70.5, 5.75}, new double[]{5.25, 70.5, 5.75}, hits, hitPos); + assertTrue(hits[0]); + assertEquals(5.25, hitPos[0], 0); + assertEquals(70.5, hitPos[1], 0); + assertEquals(5.75, hitPos[2], 0); + // a point outside the world is in the air, as a ray there is + assertTrue(ctx.isVisible(SOLID, 5.5, 400.0, 5.5, 5.5, 400.0, 5.5)); + } + + @Test(timeout = 2000) + public void aCoordinateThatIsNotFiniteIsRefused() { + final NetherPathfinder ctx = world(); + final double[] bad = {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}; + for (double v : bad) { + for (int axis = 0; axis < 6; axis++) { + final double[] p = {3.5, 71.5, 3.5, 8.5, 71.5, 8.5}; + p[axis] = v; + try { + ctx.isVisible(SOLID, p[0], p[1], p[2], p[3], p[4], p[5]); + throw new AssertionError("accepted " + v + " at " + axis); + } catch (IllegalArgumentException expected) { + // good + } + } + } + // both ends infinite: the length is NaN rather than infinite + try { + ctx.isVisible(SOLID, Double.POSITIVE_INFINITY, 71.5, 3.5, Double.POSITIVE_INFINITY, 71.5, 8.5); + throw new AssertionError("accepted two infinities"); + } catch (IllegalArgumentException expected) { + // good + } + } + + @Test(timeout = 5000) + public void aRayEndingOnAVoxelCornerInTheAirAnswersLikeOneEndingJustInside() { + // Every node of a flight path is a corner inside its air cube, like (8, 68, 8) here, four + // blocks above the solid section and clear of the block at (5, 70, 5). + final NetherPathfinder ctx = world(); + int checked = 0; + for (double ox = 0.3; ox < 16; ox += 1.7) { + for (double oz = 0.7; oz < 16; oz += 1.9) { + for (double oy = 40.2; oy < 100; oy += 4.9) { + final boolean exact = ctx.isVisible(SOLID, ox, oy, oz, 8.0, 68.0, 8.0); + final double nudge = 1e-6; + final boolean inside = ctx.isVisible(SOLID, ox, oy, oz, + 8.0 + (ox < 8.0 ? -nudge : nudge), 68.0 + (oy < 68.0 ? -nudge : nudge), 8.0 + (oz < 8.0 ? -nudge : nudge)); + assertEquals("from " + ox + ", " + oy + ", " + oz, inside, exact); + checked++; + } + } + } + assertTrue(checked > 500); + } + + @Test(timeout = 5000) + public void aRayEndingOnACornerOfASolidBlockAnswers() { + // (8, 64, 8) is on the solid section's top face. Whether a ray that ends there grazes the + // section is a tie that rounding settles either way; what matters is that it answers. + // The native library sometimes stepped into a node the ray never entered and exited. + final NetherPathfinder ctx = world(); + int blocked = 0, clear = 0; + for (double ox = 0.3; ox < 16; ox += 1.7) { + for (double oz = 0.7; oz < 16; oz += 1.9) { + for (double oy = 64.2; oy < 100; oy += 4.9) { + if (ctx.isVisible(SOLID, ox, oy, oz, 8.0, 64.0, 8.0)) clear++; else blocked++; + } + } + } + assertTrue("some answers of each kind: " + clear + " clear, " + blocked + " blocked", clear > 0 && clear + blocked > 300); + } + + @Test(timeout = 2000) + public void aBatchWithAPointInItStillAnswersTheRest() { + final NetherPathfinder ctx = world(); + final double[] from = {3.5, 71.5, 3.5, 2.5, 70.5, 5.5, 2.5, 75.5, 2.5}; + final double[] to = {3.5, 71.5, 3.5, 8.5, 70.5, 5.5, 8.5, 75.5, 8.5}; // a point, a ray through the block, a clear ray + final boolean[] hits = new boolean[3]; + ctx.raytrace(SOLID, 3, from, to, hits, null); + assertFalse(hits[0]); + assertTrue(hits[1]); + assertFalse(hits[2]); + assertEquals(1, ctx.isVisibleMulti(SOLID, 3, from, to, false)); // the first blocked one + assertEquals(0, ctx.isVisibleMulti(SOLID, 3, from, to, true)); // the first clear one: the point + } +} diff --git a/src/test/java/baritone/process/elytra/pathfinder/Oracle.java b/src/test/java/baritone/process/elytra/pathfinder/Oracle.java new file mode 100644 index 0000000000..a2b98cc7d9 --- /dev/null +++ b/src/test/java/baritone/process/elytra/pathfinder/Oracle.java @@ -0,0 +1,100 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** What the native library answered, written by oracle.cpp in the nether-pathfinder repository. */ +final class Oracle { + + static final long SEED = 146008555100680L; + + static final class Ray { + final double[] from = new double[3]; + final double[] to = new double[3]; + boolean hit; + final double[] where = new double[3]; + } + + private Oracle() {} + + static long fnv1a(byte[] data) { + long h = 0xcbf29ce484222325L; + for (byte b : data) { + h ^= (b & 0xFF); + h *= 0x100000001b3L; + } + return h; + } + + /** "x z hash" per line. */ + static List chunkHashes() throws IOException { + final List out = new ArrayList<>(); + try (InputStream in = Oracle.class.getResourceAsStream("/chunk-hashes.txt"); + BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.US_ASCII))) { + String line; + while ((line = reader.readLine()) != null) { + if (!line.isEmpty()) out.add(line.split(" ")); + } + } + return out; + } + + static List rays() throws IOException { + final byte[] bytes; + try (InputStream in = Oracle.class.getResourceAsStream("/rays.bin")) { + final java.io.ByteArrayOutputStream buf = new java.io.ByteArrayOutputStream(); + final byte[] chunk = new byte[1 << 16]; + int n; + while ((n = in.read(chunk)) != -1) buf.write(chunk, 0, n); + bytes = buf.toByteArray(); + } + final ByteBuffer b = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + final int count = b.getInt(); + final List out = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + final Ray r = new Ray(); + for (int k = 0; k < 3; k++) r.from[k] = b.getDouble(); + for (int k = 0; k < 3; k++) r.to[k] = b.getDouble(); + r.hit = b.get() != 0; + for (int k = 0; k < 3; k++) r.where[k] = b.getDouble(); + out.add(r); + } + return out; + } + + /** A context over the generated chunks x, z in 0..side-1, marked as if the game had given them. */ + static NetherPathfinder generatedWorld(int side) { + final NetherPathfinder ctx = new NetherPathfinder(SEED, null, NetherPathfinder.DIMENSION_NETHER, 128); + for (int x = 0; x < side; x++) { + for (int z = 0; z < side; z++) { + ctx.getOrGenChunk(x, z); + ctx.setChunkState(x, z, true); + } + } + return ctx; + } +} diff --git a/src/test/java/baritone/process/elytra/pathfinder/PathFindTest.java b/src/test/java/baritone/process/elytra/pathfinder/PathFindTest.java new file mode 100644 index 0000000000..81c4e83289 --- /dev/null +++ b/src/test/java/baritone/process/elytra/pathfinder/PathFindTest.java @@ -0,0 +1,171 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class PathFindTest { + + private static final int NETHER = NetherPathfinder.DIMENSION_NETHER; + + static int unpackX(long packed) { + return (int) (packed >> 38); + } + + static int unpackY(long packed) { + return (int) ((packed << 26) >> 52); + } + + static int unpackZ(long packed) { + return (int) ((packed << 38) >> 38); + } + + @Test + public void packedPositionsRoundTrip() { + for (int[] p : new int[][]{{0, 0, 0}, {1, 2, 3}, {-1, 5, -30000000}, {29999999, 383, 12345}, {-29999999, 0, 29999999}}) { + final long packed = NetherPathfinder.packBlockPos(new BlockPos(p[0], p[1], p[2])); + assertEquals(p[0], unpackX(packed)); + assertEquals(p[1], unpackY(packed)); + assertEquals(p[2], unpackZ(packed)); + } + } + + @Test + public void straightThroughAirWhenNothingIsKnown() { + final NetherPathfinder ctx = new NetherPathfinder(1, null, NETHER, 128); + final PathSegment segment = ctx.pathFind(0, 60, 0, 500, 60, 0, true, false, 10000, true, 8.0); + assertNotNull(segment); + assertTrue(segment.finished); + assertTrue(segment.packed.length >= 2); + int lastX = Integer.MIN_VALUE; + for (long packed : segment.packed) { + final int x = unpackX(packed); + assertTrue("x goes forward", x > lastX); + lastX = x; + assertEquals(0, unpackZ(packed), 16); + assertEquals(60, unpackY(packed), 16); + } + assertEquals(500, lastX, 16); + } + + @Test + public void refiningAStraightPathLeavesItsEnds() { + // refining raytraces in generate mode, so the chunks have to be known and empty for the rays to pass + final NetherPathfinder ctx = new NetherPathfinder(1, null, NETHER, 128); + for (int x = -1; x <= 32; x++) for (int z = -1; z <= 1; z++) ctx.allocateAndInsertChunk(x, z); + final PathSegment segment = ctx.pathFind(0, 60, 0, 500, 60, 0, true, true, 10000, true, 8.0); + assertNotNull(segment); + assertTrue(segment.finished); + assertTrue("refined to " + segment.packed.length + " points", segment.packed.length <= 3); + } + + @Test + public void givesUpAfterAHundredUnknownChunks() { + final NetherPathfinder ctx = new NetherPathfinder(1, null, NETHER, 128); + final PathSegment segment = ctx.pathFind(0, 60, 0, 20000, 60, 0, true, false, 10000, true, 8.0); + assertNotNull(segment); + assertFalse(segment.finished); + assertTrue(unpackX(segment.packed[segment.packed.length - 1]) > 100); + } + + @Test + public void reachesTheGoalOverRealTerrain() { + final NetherPathfinder ctx = Oracle.generatedWorld(24); + final PathSegment segment = ctx.pathFind(20, 60, 20, 340, 60, 200, true, false, 10000, true, 8.0); + assertNotNull(segment); + assertTrue(segment.finished); + // every point of the path is in air + for (long packed : segment.packed) { + final int x = unpackX(packed), y = unpackY(packed), z = unpackZ(packed); + assertFalse("path goes through a block at " + x + "," + y + "," + z, ctx.getChunkOrDefault(x >> 4, z >> 4, true).isSolid(x & 15, y, z & 15)); + } + } + + @Test + public void generatesTerrainWhenAskedTo() { + final NetherPathfinder ctx = new NetherPathfinder(Oracle.SEED, null, NETHER, 128); + final PathSegment segment = ctx.pathFind(20, 60, 20, 300, 60, 20, true, false, 10000, false, 1.0); + assertNotNull(segment); + assertTrue(segment.finished); + assertTrue("generated chunks along the way", ctx.chunkCount() > 20); + assertFalse(ctx.hasChunkFromJava(1, 1)); + } + + @Test + public void cancelStopsARunningSearch() throws Exception { + final NetherPathfinder ctx = new NetherPathfinder(Oracle.SEED, null, NETHER, 128); + final AtomicReference result = new AtomicReference<>(); + final long[] elapsed = new long[1]; + final Thread searcher = new Thread(() -> { + final long t0 = System.nanoTime(); + result.set(ctx.pathFind(0, 50, 0, 100000, 50, 0, true, false, 0, false, 1.0)); + elapsed[0] = (System.nanoTime() - t0) / 1_000_000; + }); + searcher.start(); + Thread.sleep(150); + assertFalse("not cancelled before", ctx.cancel()); + searcher.join(10000); + assertFalse(searcher.isAlive()); + assertNull(result.get()); + assertTrue("returned " + elapsed[0] + " ms after the start", elapsed[0] < 1000); + assertTrue(ctx.cancel()); + } + + @Test + public void aSearchThatCannotFinishReturnsWhatItHasAfterHalfASecond() { + final NetherPathfinder ctx = new NetherPathfinder(Oracle.SEED, null, NETHER, 128); + final long t0 = System.nanoTime(); + final PathSegment segment = ctx.pathFind(0, 50, 0, 100000, 50, 0, true, false, 0, false, 1.0); + final long ms = (System.nanoTime() - t0) / 1_000_000; + assertNotNull(segment); + assertFalse(segment.finished); + assertTrue("took " + ms + " ms", ms >= 500 && ms < 5000); + assertTrue(unpackX(segment.packed[segment.packed.length - 1]) > 50); + } + + @Test + public void rejectsBadArguments() { + final NetherPathfinder ctx = new NetherPathfinder(1, null, NETHER, 128); + try { + ctx.pathFind(0, -1, 0, 10, 60, 0, true, false, 0, true, 1.0); + throw new AssertionError("expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + // good + } + try { + new NetherPathfinder(1, null, NETHER, 500); + throw new AssertionError("expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + // good + } + try { + new NetherPathfinder(1, null, 7, 128); + throw new AssertionError("expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + // good + } + } +} diff --git a/src/test/java/baritone/process/elytra/pathfinder/RaytraceTest.java b/src/test/java/baritone/process/elytra/pathfinder/RaytraceTest.java new file mode 100644 index 0000000000..381329a2b6 --- /dev/null +++ b/src/test/java/baritone/process/elytra/pathfinder/RaytraceTest.java @@ -0,0 +1,124 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class RaytraceTest { + + @Test + public void everyOracleRayAgreesWithTheNativeRaytracer() throws Exception { + final List rays = Oracle.rays(); + assertEquals(4000, rays.size()); + final NetherPathfinder ctx = Oracle.generatedWorld(16); + final double[] where = new double[3]; + int wrongHit = 0; + int wrongWhere = 0; + for (Oracle.Ray r : rays) { + final boolean hit = Raytracer.raytrace(ctx, r.from[0], r.from[1], r.from[2], r.to[0], r.to[1], r.to[2], NetherPathfinder.CACHE_MISS_SOLID, where, 0); + if (hit != r.hit) { + wrongHit++; + } else if (hit && (where[0] != r.where[0] || where[1] != r.where[1] || where[2] != r.where[2])) { + wrongWhere++; + } + } + assertEquals("rays that hit where the native raytracer missed, or the other way round", 0, wrongHit); + assertEquals("hits at a different position than the native raytracer's", 0, wrongWhere); + } + + @Test + public void nothingToHitInAirMode() { + final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.DIMENSION_NETHER, 128); + assertTrue(ctx.isVisible(NetherPathfinder.CACHE_MISS_AIR, 0.5, 64.5, 0.5, 300.5, 20.5, -200.5)); + } + + @Test + public void unknownChunksAreWallsInSolidMode() { + final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.DIMENSION_NETHER, 128); + final double[] hitPos = new double[3]; + final boolean[] hits = new boolean[1]; + ctx.raytrace(NetherPathfinder.CACHE_MISS_SOLID, 1, new double[]{0.5, 64.5, 0.5}, new double[]{30.5, 64.5, 0.5}, hits, hitPos); + assertTrue(hits[0]); + // the origin is inside the solid, so the hit is the origin + assertEquals(0.5, hitPos[0], 0); + assertEquals(64.5, hitPos[1], 0); + assertEquals(0.5, hitPos[2], 0); + } + + @Test + public void hitsTheFaceOfABlock() { + final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.DIMENSION_NETHER, 128); + final Chunk chunk = ctx.allocateAndInsertChunk(0, 0); + chunk.setBlock(8, 64, 8, true); + final double[] hitPos = new double[3]; + final boolean[] hits = new boolean[1]; + ctx.raytrace(NetherPathfinder.CACHE_MISS_AIR, 1, new double[]{2.5, 64.5, 8.5}, new double[]{14.5, 64.5, 8.5}, hits, hitPos); + assertTrue(hits[0]); + assertEquals(8.0, hitPos[0], 1e-9); + assertEquals(64.5, hitPos[1], 1e-9); + assertEquals(8.5, hitPos[2], 1e-9); + // and from the other side + ctx.raytrace(NetherPathfinder.CACHE_MISS_AIR, 1, new double[]{14.5, 64.5, 8.5}, new double[]{2.5, 64.5, 8.5}, hits, hitPos); + assertTrue(hits[0]); + assertEquals(9.0, hitPos[0], 1e-9); + // a ray that stops short of it + assertTrue(ctx.isVisible(NetherPathfinder.CACHE_MISS_AIR, 2.5, 64.5, 8.5, 7.5, 64.5, 8.5)); + // a ray past it in another row + assertTrue(ctx.isVisible(NetherPathfinder.CACHE_MISS_AIR, 2.5, 64.5, 9.5, 14.5, 64.5, 9.5)); + } + + @Test + public void crossesChunkAndSlabBoundaries() { + final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.DIMENSION_NETHER, 128); + ctx.allocateAndInsertChunk(0, 0); + ctx.allocateAndInsertChunk(1, 0); + ctx.allocateAndInsertChunk(2, 0).setBlock(3, 70, 4, true); + assertTrue(ctx.isVisible(NetherPathfinder.CACHE_MISS_AIR, 1.5, 40.5, 4.5, 34.5, 70.5, 4.5)); + assertFalse(ctx.isVisible(NetherPathfinder.CACHE_MISS_AIR, 1.5, 40.5, 4.5, 36.5, 70.5, 4.5)); + assertFalse(ctx.isVisible(NetherPathfinder.CACHE_MISS_AIR, 36.5, 70.5, 4.5, 1.5, 40.5, 4.5)); + } + + @Test + public void theRayThatEndedTheNativeProcessTerminates() { + // nether-pathfinder issue 23: an end exactly on a chunk corner + final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.DIMENSION_NETHER, 128); + assertTrue(ctx.isVisible(NetherPathfinder.CACHE_MISS_AIR, 386.7112215521066, 137.40911926818373, 7.0554159455922285, 416.0, 142.0, 0.0)); + assertFalse(ctx.isVisible(NetherPathfinder.CACHE_MISS_SOLID, 386.7112215521066, 137.40911926818373, 7.0554159455922285, 416.0, 142.0, 0.0)); + } + + @Test + public void isVisibleMultiReportsTheFirstSegmentThatDecides() { + final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.DIMENSION_NETHER, 128); + ctx.allocateAndInsertChunk(0, 0).setBlock(8, 64, 8, true); + final double[] start = {2.5, 64.5, 8.5, 2.5, 64.5, 9.5, 2.5, 64.5, 8.5}; + final double[] end = {14.5, 64.5, 8.5, 14.5, 64.5, 9.5, 14.5, 64.5, 8.5}; + // first clear one is index 1; first blocked one is index 0 + assertEquals(1, ctx.isVisibleMulti(NetherPathfinder.CACHE_MISS_AIR, 3, start, end, true)); + assertEquals(0, ctx.isVisibleMulti(NetherPathfinder.CACHE_MISS_AIR, 3, start, end, false)); + // all clear: -1 in "all" mode; none blocked + final double[] clearEnd = {7.5, 64.5, 8.5, 14.5, 64.5, 9.5, 7.5, 64.5, 8.5}; + assertEquals(-1, ctx.isVisibleMulti(NetherPathfinder.CACHE_MISS_AIR, 3, start, clearEnd, false)); + assertEquals(0, ctx.isVisibleMulti(NetherPathfinder.CACHE_MISS_AIR, 3, start, clearEnd, true)); + } +} diff --git a/src/test/resources/chunk-hashes.txt b/src/test/resources/chunk-hashes.txt new file mode 100644 index 0000000000..cc64f51504 --- /dev/null +++ b/src/test/resources/chunk-hashes.txt @@ -0,0 +1,625 @@ +-12 -12 dd5250b3dcd5e09b +-12 -11 6bcc535bea7d612 +-12 -10 776ca1693db2001 +-12 -9 feca2b12e3b4d045 +-12 -8 226418f9407fb626 +-12 -7 919541cc757400b7 +-12 -6 ee6b74f5f3efd17 +-12 -5 8f55bbde011daf26 +-12 -4 4083390a83ca06df +-12 -3 1d9664eb3e2f5d78 +-12 -2 5eec04661b1832e9 +-12 -1 11dce5922e5c15e +-12 0 15b3dfde9579f447 +-12 1 e95eb813c98ac974 +-12 2 b6dac4b8c0078216 +-12 3 b18584426bca748b +-12 4 4e9ab39f479e0a2 +-12 5 882e6face432bd49 +-12 6 fee37693cf74c0e5 +-12 7 b016af874ea80e65 +-12 8 1dbff1342da3d4f +-12 9 1634d3cdcacd738b +-12 10 ed668e60f01e7cc1 +-12 11 db1295d820a2c210 +-12 12 75a546fc683c21dc +-11 -12 d2f06cf602719ea4 +-11 -11 d1ea6f39e138dac0 +-11 -10 c6ebdfc6a39b2f1d +-11 -9 374d8af68ee19ab9 +-11 -8 1b53baf4089b98a +-11 -7 746e82d53a15ea08 +-11 -6 ec73d885164c3e75 +-11 -5 5adcc31c8c3f343 +-11 -4 40cf8dafa1220020 +-11 -3 812bfb8f88246e21 +-11 -2 9c10be95737e8e86 +-11 -1 faf3fc13df06e96f +-11 0 4ec4799091902463 +-11 1 6df98c16536d3ad1 +-11 2 4f98991cafff02ab +-11 3 6bb345202cf9cd73 +-11 4 a7d8d6c303537df7 +-11 5 b44388361ce2e96d +-11 6 aefae7d0684feaa6 +-11 7 92f3393b06f57d5f +-11 8 43740368f4480a4f +-11 9 37a33aaea575c4cf +-11 10 87829cf0aa23f41a +-11 11 3b145a2fcb5d0d1 +-11 12 de5d32a8177c9b75 +-10 -12 413af0d08ad59058 +-10 -11 b5a4cb197c175f19 +-10 -10 523514f7412a9dd6 +-10 -9 6aa020d40740c907 +-10 -8 d3a5e70310fb2896 +-10 -7 940c01864803d152 +-10 -6 f07880b1086bb530 +-10 -5 a2d879a5c26026e2 +-10 -4 a61a8a2ec97c39d6 +-10 -3 dfe135bd8d82fd5b +-10 -2 16470448538439e9 +-10 -1 589d97b0188bc85 +-10 0 70a348c1bfc8eb0a +-10 1 33fd45c47e1b63da +-10 2 11d05606e03d8a58 +-10 3 4a873d8b7a67ea2a +-10 4 38e8f614d16d99a7 +-10 5 da5c15fb729c4e59 +-10 6 e98d49df28fcbcca +-10 7 91d874bd96907fc3 +-10 8 f074e4872aea889f +-10 9 2be9fd5ec2847c5a +-10 10 62428d9b68269610 +-10 11 ecc47178d0496618 +-10 12 2ee54b4698db6f6b +-9 -12 54e87c46cfcf7c6 +-9 -11 19995e7c022a28a6 +-9 -10 bc940184d8069134 +-9 -9 6d0627e0d24eee40 +-9 -8 6eb2d50814c5a193 +-9 -7 5f0cce58e44ab141 +-9 -6 216e9370df877c04 +-9 -5 1d8667a999ea79e9 +-9 -4 faf7df65421380d +-9 -3 7e8c799343d3956d +-9 -2 fecad6839f7cb4ee +-9 -1 aeae3b1c6aaec88d +-9 0 efbe2b05c091efbb +-9 1 c8239fb40e96ec02 +-9 2 477584a73e4d6780 +-9 3 3cf36fe2001dc8a8 +-9 4 93b12073045fa3f1 +-9 5 31994cbf095a19fd +-9 6 276b890a04856a6e +-9 7 8d102937bf049be3 +-9 8 718e3f65e112fe06 +-9 9 6716d897959a1815 +-9 10 53c07570e595ffe5 +-9 11 91605c6fdd5afe96 +-9 12 8059b525b7aa805d +-8 -12 e7c56a9af95fec10 +-8 -11 56400babceaf7183 +-8 -10 c30b1606665e0131 +-8 -9 4caf922308be9f89 +-8 -8 c01e9a02c7ac5a27 +-8 -7 bfbe7895d801be05 +-8 -6 e49b24000ad52adc +-8 -5 b8aa8652de1ca26c +-8 -4 4c43784840f74c1 +-8 -3 3fd6c40a14455692 +-8 -2 b6eeafe1f9693f58 +-8 -1 3062df6c13aa1e00 +-8 0 8bf8d606de27ede9 +-8 1 cd2efe5fc72b8c7c +-8 2 4b77c69dc63007f1 +-8 3 f51025fe29f71645 +-8 4 9fc5ec6b670be528 +-8 5 53eaa98f4e08564f +-8 6 ff8bc35a43cf3f31 +-8 7 71be7e2aeaf090e0 +-8 8 c8e0bd4aca0a777 +-8 9 37c6d7934a602970 +-8 10 f21681612cc02e74 +-8 11 b8b9f9efc1e7a2b8 +-8 12 885475171b22c574 +-7 -12 61c3878b72aecad8 +-7 -11 6953a1dca8b64ac6 +-7 -10 dbcfe14d75b8e90f +-7 -9 5e4558d12b6fddc9 +-7 -8 22685cd3cced3f26 +-7 -7 abb1e8434e04e81b +-7 -6 346d746b3ffe5a7c +-7 -5 e6063548794cdadf +-7 -4 c521ebc188be6b16 +-7 -3 ca61be2646bc8477 +-7 -2 d8387ca7404589f5 +-7 -1 cc677b74387d0f39 +-7 0 c8e4e4bb2199bd95 +-7 1 720395104d051047 +-7 2 27dba1ddb694073b +-7 3 b92735662f36e812 +-7 4 6dc99764e85890de +-7 5 463bebef7320ccca +-7 6 ee5cca175a5402e +-7 7 d33010a1fdd71ec4 +-7 8 9ebb059d8ff5979f +-7 9 150768e96f7d9c81 +-7 10 992d781af6e4d075 +-7 11 eec6ce95836754fc +-7 12 86e148a4905ba6d7 +-6 -12 6348b3dd548a69d5 +-6 -11 5f07b3abb6894f5 +-6 -10 be48fbd56dbffcd4 +-6 -9 87348f83b31719c3 +-6 -8 202f5be96ab598cc +-6 -7 2711a2a85a0bdcb1 +-6 -6 3711c97e7b0957a8 +-6 -5 74d082a81907464c +-6 -4 b8c59b88b71c7d52 +-6 -3 a9fceb31c00d031 +-6 -2 ad72f19eadb93678 +-6 -1 7e7a7a53ba4a298f +-6 0 af17a086a5fde3fe +-6 1 5b6b2e387f1785cb +-6 2 d7d3dbc1999bd387 +-6 3 a1036f11c37ea44d +-6 4 abcbf61a65bd5d85 +-6 5 8fbfd77364378a78 +-6 6 47d251e6a1d4da6d +-6 7 fcefe4b22e7be6eb +-6 8 1df34533d7638866 +-6 9 3c189dc5367fc1ea +-6 10 fa97ce06f1c7b311 +-6 11 f9e18a7b5ff168bd +-6 12 d5f336ad33256a50 +-5 -12 9fcaf9470ab8fa32 +-5 -11 ddad0c39e353226a +-5 -10 552ccde918723717 +-5 -9 370784ea1d7b2df8 +-5 -8 b377f68c3ea52fd9 +-5 -7 1b832aa0eae964a9 +-5 -6 5b10072a07aea2 +-5 -5 9aa6333d7485df63 +-5 -4 74d85c669ff41139 +-5 -3 bafa8f41cae9595c +-5 -2 87484a3d88b27755 +-5 -1 bcbef3c37d3f3efc +-5 0 d3ce61889ebaeabb +-5 1 75bd81092132a067 +-5 2 f883a136b6f97f19 +-5 3 a0a87814b424be2c +-5 4 1bcf46084c9a7325 +-5 5 1bcf46084c9a7325 +-5 6 dd050edb012b674b +-5 7 47a439ab0b0503b1 +-5 8 56e3703cd9802b7a +-5 9 4b64fcc36ce32018 +-5 10 34352f2bcd2230b3 +-5 11 536813d9b20f59c +-5 12 8ca9739ed883a19b +-4 -12 147dfb2b2f7d3de7 +-4 -11 389068d1d2296260 +-4 -10 922b4c5e1142a5ea +-4 -9 c616048f349e75f2 +-4 -8 a27da617a0864533 +-4 -7 9c3dffdc976a9530 +-4 -6 d392d67aab52d66f +-4 -5 8f06b5e0a5436537 +-4 -4 ae73b004b914b9e9 +-4 -3 1241b1e35de4ecf4 +-4 -2 b3733f68451d1b8b +-4 -1 1a353dae8d08a836 +-4 0 94ce1c15fa6b5e40 +-4 1 14acab73a5510d1 +-4 2 75711be4815a12e3 +-4 3 7516dd83d9e9514f +-4 4 373f1723c1b4913b +-4 5 d5de6ed7c4d2d15f +-4 6 f7914140e81dcc81 +-4 7 a94e3d2c7388ef12 +-4 8 34aa49f9e29e5b54 +-4 9 16e6f48b9545bffe +-4 10 f9abf2b0144d76a4 +-4 11 79bc3695707bb5d5 +-4 12 d77b5f5e48f51b8f +-3 -12 7adfcae74c0d7ce3 +-3 -11 83bf9452acad46 +-3 -10 e9583c9467584850 +-3 -9 c2b67fe51758b274 +-3 -8 3061885a5a07f7b2 +-3 -7 d3cb74e5468d5ba7 +-3 -6 48b4596485e3d888 +-3 -5 a4936aa66b3c5730 +-3 -4 a3223c3550d1e9a2 +-3 -3 66c647974991695d +-3 -2 1007b949d1f0bc53 +-3 -1 ad42a561576c2a7e +-3 0 ef575b910e1d31ec +-3 1 5cb9f9353d72c9cf +-3 2 a30ccad760183ca4 +-3 3 b57326352afec63d +-3 4 fe424d15aada9852 +-3 5 943c86d577f0e08e +-3 6 679bfdcd7a652718 +-3 7 7841baa6126a7e5c +-3 8 db1ceea2f5aeda01 +-3 9 c22e9af0459f30a7 +-3 10 f19c9bdc26a3ae06 +-3 11 274c87fb61919727 +-3 12 1b43726a05392ee6 +-2 -12 7d9a093e3d5821a3 +-2 -11 7adb0ffd7d4b2019 +-2 -10 50a2263b93266ff0 +-2 -9 2b837550ae4204a4 +-2 -8 65c9d2660e5eaadb +-2 -7 f5290d2f0d1fea6a +-2 -6 f09184a18fc886d1 +-2 -5 6661e6e168ac5353 +-2 -4 11634775ffd9552e +-2 -3 6b221348863c11f0 +-2 -2 d1d9075dedf5ac5b +-2 -1 85bc300792ab95cf +-2 0 6dcacd8c2869fd36 +-2 1 c6607b34d2044588 +-2 2 f0c769db91d1704f +-2 3 d958d6b465042f9e +-2 4 80e462ecfdccb1ed +-2 5 49e2e4413c368492 +-2 6 c8e8f46c201eec32 +-2 7 3c10191104fff610 +-2 8 148a653fb96ce3b7 +-2 9 c95f8c605ff4cf4d +-2 10 e1d11e6973ea76e5 +-2 11 10894048688c984b +-2 12 3e37bb89aa9fd1e9 +-1 -12 74e8f2bd6f90e911 +-1 -11 afb900edae72d2df +-1 -10 7de01afc08d3173d +-1 -9 da2c6c7805e5b6ab +-1 -8 ff4e4e10a4015181 +-1 -7 437a3cd58fda90d1 +-1 -6 58d3385a2bbfd380 +-1 -5 9188345103cc40e4 +-1 -4 e945e2911413d271 +-1 -3 d18462aafac682d4 +-1 -2 56b8038f50fc32ca +-1 -1 e892d918392c5fe1 +-1 0 12324398bf6a773f +-1 1 3970fbea4d903421 +-1 2 c751e666f8a78252 +-1 3 619ab9a312c43aa3 +-1 4 b3f281e70fd81857 +-1 5 6dfaacbcf4c82010 +-1 6 9b5150be6d473546 +-1 7 2e4062d41f1eed58 +-1 8 e6efbccd0972eefe +-1 9 a765770878b2b9ea +-1 10 4d417d7b4139ea26 +-1 11 66501afc8ba159b4 +-1 12 da9d14bceb07c07a +0 -12 d8db12c60aa97598 +0 -11 22fc2ff66fb3087e +0 -10 8c297cc140766610 +0 -9 b81077449c701479 +0 -8 f48c33b7f7954944 +0 -7 42f8b250746750d5 +0 -6 2c0408861a0edcfa +0 -5 a25172f588957221 +0 -4 7767347c61a10505 +0 -3 66e516eca42a4d5e +0 -2 6f21eee50207da98 +0 -1 565be172316c6662 +0 0 178e5bd77aa582bc +0 1 77a98cd3506aa4b +0 2 77523163e51fac12 +0 3 c323e3c20067eae7 +0 4 30ce740aa1a03095 +0 5 638886f19a7174a7 +0 6 39f2a1dc75adc819 +0 7 8f08fc41bfa1537e +0 8 58d8178a0d17e7d4 +0 9 71ae38b8b28b2b7b +0 10 24359706960ced16 +0 11 594bca696d3f3455 +0 12 577d3229d3e24a15 +1 -12 4c5a1f6c228b365 +1 -11 c82bf05fa4c548d9 +1 -10 7d3a5770417efb85 +1 -9 3b4135eca1014ed4 +1 -8 890f73440a66528c +1 -7 3cef0cf7ed989817 +1 -6 827222e0c408b126 +1 -5 d070cad21ba7dac5 +1 -4 11adf655d1673301 +1 -3 f989eda38d7a640c +1 -2 fc037a4130dc30b6 +1 -1 9308289d9178bdd0 +1 0 ffad7bd68db319ee +1 1 3abf7ffc65274347 +1 2 70927b6e0e00a818 +1 3 658d79faaa82c172 +1 4 d3b238b7897ee770 +1 5 3c92d4d9c1433c9b +1 6 d9965b208692c59f +1 7 528ec7ec93fbda1d +1 8 747ba5923dcfb4bf +1 9 34cc51754cd1ff24 +1 10 1bcf46084c9a7325 +1 11 1bcf46084c9a7325 +1 12 3a18d8de88e705d3 +2 -12 619beb94cb56b2b +2 -11 51c7cbe471f88fda +2 -10 dec7bcf5b1423caf +2 -9 aec73969ac9eafd2 +2 -8 2f2750483bd60d76 +2 -7 10450f140f79a059 +2 -6 bcbe6a13d86c3061 +2 -5 7eec1c2df367f4c8 +2 -4 bf3220094b1ac7c2 +2 -3 29cd1974e2b1c077 +2 -2 6ed21c497f00952d +2 -1 3a1ec80202abdf9d +2 0 7d92459bc61459af +2 1 3fd9513d1148f608 +2 2 409fb9ee1e1e543e +2 3 da5fcb44a02bb607 +2 4 d3189ccfbfd7bb89 +2 5 e131397c196212b5 +2 6 75858aa80ed55944 +2 7 7e3cd9e133b723ce +2 8 9434c971e7bffd66 +2 9 1bcf46084c9a7325 +2 10 1bcf46084c9a7325 +2 11 1bcf46084c9a7325 +2 12 f734b4c0c0c3f390 +3 -12 795dd99686adc300 +3 -11 f3a2312c1572e799 +3 -10 57e3b440fbec101c +3 -9 627591f043406e9c +3 -8 b0cee67a494c67ef +3 -7 c05e41e7d65c091a +3 -6 ed1792dc7b2d4891 +3 -5 80fc4b5ef63d13aa +3 -4 dc9f413b81f2ef19 +3 -3 1d461070c2ee5dd7 +3 -2 5279e8de55eeb3fd +3 -1 7ffc9bb097ecd39d +3 0 3bae618dc13f01ff +3 1 1969c8eca3b122ba +3 2 b32b442de95f74a +3 3 78f5768098264f7 +3 4 1290feff4a39540f +3 5 9069f8542eaa40aa +3 6 985a34bb74a0c81 +3 7 5b7193f3b873f450 +3 8 cbb99460a5b87c71 +3 9 c8333f1b0660e685 +3 10 eb6070eb15557639 +3 11 6fffab59dfcb766f +3 12 a89a609047f34719 +4 -12 7b545f60b25b0e96 +4 -11 6c647adf8ef7b161 +4 -10 d15736dd603cbe57 +4 -9 dca96ce2d4ff650d +4 -8 e3d0fc28c44a7022 +4 -7 e8b2e49424db4630 +4 -6 33b134a2c34447b4 +4 -5 1e86dadc2f805807 +4 -4 f5b79e3f9d269d29 +4 -3 783deef06f8d932d +4 -2 144a77b049394cd5 +4 -1 5b1f3220caddc7eb +4 0 9272781d6a20e181 +4 1 59026017ce16a53a +4 2 e4aa5b27ee463cdc +4 3 e3b0f535412913ec +4 4 83285891170d4e33 +4 5 81f99dfeae4437ae +4 6 ae7ffc0ad9270c8a +4 7 7fbeb87945b05a72 +4 8 818a30df18721599 +4 9 1bcf46084c9a7325 +4 10 1bcf46084c9a7325 +4 11 8d972e4b7a2ffecd +4 12 1497856649a873b5 +5 -12 6741cf2450d6fc8d +5 -11 d32d25445a3a77f4 +5 -10 ccb09de80a188786 +5 -9 cd1f267f7429ea15 +5 -8 60044c6d58baad3e +5 -7 4d947a4575564c5c +5 -6 938ce468d1a75e67 +5 -5 aa36b023cee494a0 +5 -4 12b87a044af011c5 +5 -3 da4e23e7855b7c35 +5 -2 e415b30566d279ff +5 -1 8cbf4e51b1e601ee +5 0 6de415dc721372a5 +5 1 d931c207c5d2ae0b +5 2 3c25f7cea189999f +5 3 d76f0f1206f79f8e +5 4 414ea9af47cb418e +5 5 5144735f7e8dfa1e +5 6 5ccc10f61b3dde47 +5 7 8557bafa32747020 +5 8 1bcf46084c9a7325 +5 9 1bcf46084c9a7325 +5 10 1bcf46084c9a7325 +5 11 1bcf46084c9a7325 +5 12 8b7a2ec46877cb13 +6 -12 93053070d59bb4cc +6 -11 9c09bf327c51ff60 +6 -10 46c64486c06f4884 +6 -9 d9f8b66df31633ae +6 -8 db76c5a7fadaa164 +6 -7 dbc681821768f6e6 +6 -6 7963f1e62ea8feb3 +6 -5 57d687f77d1058e +6 -4 32507186494e018c +6 -3 7e2a9c25c652a0ce +6 -2 452346301b5fb5b7 +6 -1 1d183a58afc2efc +6 0 eee32ce491930711 +6 1 8b5653860c5c6ada +6 2 6d23b77f1d0f0f1d +6 3 186c09755bf3fe71 +6 4 2753a948b6abdff1 +6 5 700eb4304671ffc2 +6 6 97c14dd559710121 +6 7 138f00fff485bd4 +6 8 e3eba807b8b7a33e +6 9 587994226cf3bf48 +6 10 1bcf46084c9a7325 +6 11 1bcf46084c9a7325 +6 12 1bcf46084c9a7325 +7 -12 f46fb58c9e2e6444 +7 -11 14fa03c79a4ef4dd +7 -10 5e87b17ba017afe7 +7 -9 1779720cb64a4692 +7 -8 8079eb947f0f48b5 +7 -7 cd3bad8cdc1f9d7f +7 -6 6dd293d49c6df0fc +7 -5 86250ea252380c68 +7 -4 d2788d5190e6d74 +7 -3 93ad3d86c4e1b2a1 +7 -2 a009dc698362d82a +7 -1 39d09367f351c8e +7 0 b6a35a9891ec9362 +7 1 2a53b60fd8e2587d +7 2 a9c3c5d043fa084d +7 3 5e3ce497c20fd735 +7 4 ad8df9cacdcb9ddf +7 5 96d774703ed39a8c +7 6 8769b98643c08616 +7 7 5bdc65f4b41bf5ca +7 8 67473461706f3fde +7 9 4cf3a48b6d322412 +7 10 1bcf46084c9a7325 +7 11 2ab4298a35fa019b +7 12 b71a787206bedbf1 +8 -12 1dacf614bfd6b59e +8 -11 990c138324dc6aeb +8 -10 3c44c3608599d314 +8 -9 41f5d0ed0b8b4080 +8 -8 4343f7fffe144c34 +8 -7 a2ea936e838f82ba +8 -6 e21040d6492ef795 +8 -5 2eb31cdc2d9500db +8 -4 3fb5d7b93ef4da05 +8 -3 e0142e5fbff967bf +8 -2 533a2a7de57285a8 +8 -1 aac08512950240be +8 0 283a8d24ff1cd9a7 +8 1 f03c4e0b3c6e2f9c +8 2 ada47beaae72cff5 +8 3 cef2f11cb9daa947 +8 4 cd89460e57b4eabb +8 5 d0878e4c5e8d704b +8 6 c8c7e7e3fb89568b +8 7 120c074c597ca831 +8 8 9c21776ebe178fd +8 9 f331def89a491cee +8 10 687ea1f5f0f50d65 +8 11 3ae108322e14729e +8 12 31d160294e169e56 +9 -12 ecf09e632c7ac549 +9 -11 cf1694bca71a2b76 +9 -10 72d4a60a6a7e0288 +9 -9 6b0c94e226a9c371 +9 -8 ffb5b72b4532b72f +9 -7 ada8b0fe398f066c +9 -6 54360c7a99ef19b8 +9 -5 56f09ac0d39a91c8 +9 -4 15e56c906a11a39 +9 -3 75173a478077cd63 +9 -2 d0c26572ba5e13be +9 -1 9c451fd67fe30d2c +9 0 8288ce7981946858 +9 1 d2ce82724d10a820 +9 2 c0f9349e127739 +9 3 d1630b98cba8829f +9 4 2b8a297f39912f71 +9 5 3a1a72e00c493ac0 +9 6 ef9209e5788c323c +9 7 fc53ca87cebd4a7f +9 8 a1a1c47ee873d3f1 +9 9 fb2a01670f9703ca +9 10 288ab79262ca8e5 +9 11 e34ddfef12e10adc +9 12 d3a1ef78e06b8674 +10 -12 f6672ccf55233f32 +10 -11 50795e691cabf6e5 +10 -10 3477e321d104b29c +10 -9 4e778e7b54448f94 +10 -8 77564689a3f91730 +10 -7 591a83c3dbb9c81b +10 -6 63be6df7b7cf2c46 +10 -5 9bb346ded66d6a31 +10 -4 8209ed3afd6bf14a +10 -3 2489f517339dbf2a +10 -2 f8a6e3973b7cf562 +10 -1 eecdb840680b008e +10 0 4d1d9ac38e3ef600 +10 1 19bdc7f392da8b02 +10 2 74b1e3a1df84cbcb +10 3 6b102a6f8fb66e39 +10 4 fd8cb6221f9c065a +10 5 a22b986de161e50e +10 6 acf38e88e6ac1a5e +10 7 80b71dd920b70583 +10 8 b07a6f8370e8ddbd +10 9 5699eac129db51b +10 10 b838e3665fa6622b +10 11 b5e7ad7d1d1b4e4d +10 12 89b1afd52b452b21 +11 -12 e91877803d8051a6 +11 -11 7e8193b0e1334b23 +11 -10 9adfc1dc42b6deba +11 -9 d33ec420202c2d4 +11 -8 3ac85c6bf4c2d18a +11 -7 8c633d52f7937a92 +11 -6 b4999dd2239a593b +11 -5 93fa37d4e0bbc34f +11 -4 fbbe366f16c013c8 +11 -3 49e713baa7f4a411 +11 -2 ceeeebac565d6d4b +11 -1 5e7b786668af6519 +11 0 2de04fe8e53ef46c +11 1 39b80e2aa1c35756 +11 2 159afabb03328f8e +11 3 13326f2d2a77765e +11 4 c5090f03a3fa12f5 +11 5 74031c5c4da914e7 +11 6 a47a3563c2c6ffac +11 7 d7a13eff4fe1e30 +11 8 2d228ed90025b34 +11 9 1967d59143352879 +11 10 bcd11f001dd93155 +11 11 827ab35bf2aa0e38 +11 12 5598a0caf0cf1dac +12 -12 a6983f2cc7295a7a +12 -11 43134d1ce172042f +12 -10 a3cd7f1585e16779 +12 -9 8b14ac0bc4d2b190 +12 -8 dd3eab609370fd46 +12 -7 4f941f01bd1c05 +12 -6 1ddffe5cba3437c8 +12 -5 663de4a151c4ab29 +12 -4 3be076f1eb7c48de +12 -3 fede4340b98fecdd +12 -2 f206c4e10ad325ec +12 -1 1d25971847238b93 +12 0 f7bf1a5aebdcccdd +12 1 ea24e69d871a3662 +12 2 fd680a709bfc5670 +12 3 82993f4f642793b4 +12 4 df70881fdede4356 +12 5 6d7edc0eb5057508 +12 6 54103a848ee8e012 +12 7 67f042ead23205ab +12 8 19129954000032c4 +12 9 336ba51bf715cbb5 +12 10 8f7a136fcdd74c6a +12 11 53e7cde0f11f33a8 +12 12 a939ed8fe06e3b6d diff --git a/src/test/resources/rays.bin b/src/test/resources/rays.bin new file mode 100644 index 0000000000000000000000000000000000000000..41021aa5d7d2659009bb6cbe1d05f65f8cde40ad GIT binary patch literal 292004 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 Date: Sat, 12 Sep 2026 04:06:29 -0400 Subject: [PATCH 02/15] Fly with the Java pathfinder instead of the native library NetherPathfinderContext asks baritone.process.elytra.pathfinder for paths and lines of sight, with a Chunk object where the native library handed out a pointer: the chunk packer fills a Chunk, a block update sets a bit on one, the solver's block lookups cache one, and there is nothing to free. The Unsafe fill of an all-solid section is Chunk.fillSection, and destroy() closes the table. The dev.babbaj:nether-pathfinder dependency, its maven repository, the nested jar on Fabric, the shadowed jar on Forge and NeoForge, the ProGuard keep rule and the natives it carried for each platform go, and with them the system check: every system is supported, so NullElytraProcess goes too, and elytraCustomAllocator is kept only so that a settings file naming it still loads. The read-write lock stays, on the same sides as before, so that the threads run in the order they always have. The port needs none of it -- a chunk is an object that stays valid for whoever holds it, and the table takes lookups, inserts and culls from any thread at once -- and dropping it is a follow-up. Co-Authored-By: Claude Fable 5.1 --- build.gradle | 6 -- fabric/build.gradle | 1 - forge/build.gradle | 1 - gradle.properties | 2 - neoforge/build.gradle | 1 - scripts/proguard.pro | 5 - src/api/java/baritone/api/Settings.java | 4 +- .../java/baritone/process/ElytraProcess.java | 4 +- .../elytra/BlockStateOctreeInterface.java | 19 ++-- .../elytra/NetherPathfinderContext.java | 80 ++++++-------- .../process/elytra/NullElytraProcess.java | 101 ------------------ .../process/elytra/UnpackedSegment.java | 2 +- 12 files changed, 46 insertions(+), 180 deletions(-) delete mode 100644 src/main/java/baritone/process/elytra/NullElytraProcess.java diff --git a/build.gradle b/build.gradle index 1321228664..601c096ee1 100755 --- a/build.gradle +++ b/build.gradle @@ -72,18 +72,12 @@ allprojects { } } mavenCentral() - maven { - name = 'babbaj-repo' - url = 'https://babbaj.github.io/maven/' - } } dependencies { compileOnly "org.spongepowered:mixin:${project.mixin_version}" compileOnly "org.ow2.asm:asm:${project.asm_version}" - implementation "dev.babbaj:nether-pathfinder:${project.nether_pathfinder_version}" - implementation 'com.google.code.findbugs:jsr305:3.0.2' } diff --git a/fabric/build.gradle b/fabric/build.gradle index ad4e096a9a..a40182b651 100644 --- a/fabric/build.gradle +++ b/fabric/build.gradle @@ -47,7 +47,6 @@ dependencies { common sourceSet.output shadowCommon sourceSet.output } - include "dev.babbaj:nether-pathfinder:${project.nether_pathfinder_version}" } processResources { diff --git a/forge/build.gradle b/forge/build.gradle index 462c24b487..ddc97e1418 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -54,7 +54,6 @@ dependencies { common sourceSet.output shadowCommon sourceSet.output } - shadowCommon "dev.babbaj:nether-pathfinder:${project.nether_pathfinder_version}" } processResources { diff --git a/gradle.properties b/gradle.properties index b3df6acba2..b01130e0a3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,8 +16,6 @@ neoforge_version=9-beta fabric_version=0.16.9 -nether_pathfinder_version=1.6 - // These dependencies are used for common and tweaker // while mod loaders usually ship their own version mixin_version=0.8.5 diff --git a/neoforge/build.gradle b/neoforge/build.gradle index 6661c71fbd..ccafc7e63a 100644 --- a/neoforge/build.gradle +++ b/neoforge/build.gradle @@ -62,7 +62,6 @@ dependencies { common sourceSet.output shadowCommon sourceSet.output } - shadowCommon "dev.babbaj:nether-pathfinder:${project.nether_pathfinder_version}" } processResources { diff --git a/scripts/proguard.pro b/scripts/proguard.pro index 0b764e4d9d..34b76170f8 100644 --- a/scripts/proguard.pro +++ b/scripts/proguard.pro @@ -54,11 +54,6 @@ -dontwarn baritone.utils.schematic.schematica.** -dontwarn baritone.utils.schematic.litematica.** -# nether-pathfinder uses JNI to acess its own classes -# and some of our builds include it before running proguard -# conservatively keep all of it, even though only PathSegment. is needed --keep,allowoptimization class dev.babbaj.pathfinder.** { *; } - # Keep - Applications. Keep all application classes, along with their 'main' # methods. -keepclasseswithmembers public class * { diff --git a/src/api/java/baritone/api/Settings.java b/src/api/java/baritone/api/Settings.java index 6f84e33b45..4c3f99d864 100644 --- a/src/api/java/baritone/api/Settings.java +++ b/src/api/java/baritone/api/Settings.java @@ -1550,8 +1550,10 @@ public final class Settings { public final Setting elytraChatSpam = new Setting<>(false); /** - * May reduce memory usage by using a custom allocator for pathfinding + * Does nothing. The pathfinder is Java now and has no allocator of its own; kept so that a + * settings file naming it still loads. */ + @Deprecated public final Setting elytraCustomAllocator = new Setting<>(true); /** diff --git a/src/main/java/baritone/process/ElytraProcess.java b/src/main/java/baritone/process/ElytraProcess.java index 8ec50d7939..2c9df89eb5 100644 --- a/src/main/java/baritone/process/ElytraProcess.java +++ b/src/main/java/baritone/process/ElytraProcess.java @@ -110,9 +110,7 @@ private ElytraProcess(Baritone baritone) { } public static IElytraProcess create(final Baritone baritone) { - return NetherPathfinderContext.isSupported() - ? new ElytraProcess(baritone) - : new NullElytraProcess(baritone); + return new ElytraProcess(baritone); } @Override diff --git a/src/main/java/baritone/process/elytra/BlockStateOctreeInterface.java b/src/main/java/baritone/process/elytra/BlockStateOctreeInterface.java index 52fdfdd66d..41677697bd 100644 --- a/src/main/java/baritone/process/elytra/BlockStateOctreeInterface.java +++ b/src/main/java/baritone/process/elytra/BlockStateOctreeInterface.java @@ -17,9 +17,8 @@ package baritone.process.elytra; -import dev.babbaj.pathfinder.NetherPathfinder; -import dev.babbaj.pathfinder.Octree; -import net.minecraft.world.level.dimension.DimensionType; +import baritone.process.elytra.pathfinder.Chunk; +import baritone.process.elytra.pathfinder.NetherPathfinder; /** * @author Brady @@ -27,9 +26,11 @@ public final class BlockStateOctreeInterface { private final NetherPathfinderContext context; - private final long contextPtr; + private final NetherPathfinder pathfinder; private final int minY; - transient long chunkPtr; + // The chunk the last lookup fell in, so that a run of lookups inside one chunk costs one table + // lookup. Cleared under the write lock by whatever replaces or culls chunks. + Chunk chunk; // Guarantee that the first lookup will fetch the context by setting MAX_VALUE private int prevChunkX = Integer.MAX_VALUE; @@ -37,7 +38,7 @@ public final class BlockStateOctreeInterface { public BlockStateOctreeInterface(final NetherPathfinderContext context) { this.context = context; - this.contextPtr = context.context; + this.pathfinder = context.context; this.minY = context.minY; } @@ -48,11 +49,11 @@ public boolean get0(final int x, final int y, final int z) { } final int chunkX = x >> 4; final int chunkZ = z >> 4; - if (this.chunkPtr == 0 | ((chunkX ^ this.prevChunkX) | (chunkZ ^ this.prevChunkZ)) != 0) { + if (this.chunk == null | ((chunkX ^ this.prevChunkX) | (chunkZ ^ this.prevChunkZ)) != 0) { this.prevChunkX = chunkX; this.prevChunkZ = chunkZ; - this.chunkPtr = NetherPathfinder.getChunkOrDefault(this.contextPtr, chunkX, chunkZ, true); + this.chunk = this.pathfinder.getChunkOrDefault(chunkX, chunkZ, true); } - return Octree.getBlock(this.chunkPtr, x & 0xF, adjustedY, z & 0xF); + return this.chunk.isSolid(x & 0xF, adjustedY, z & 0xF); } } diff --git a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java index 69e0a181b7..878dc2f292 100644 --- a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java +++ b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java @@ -19,10 +19,10 @@ import baritone.Baritone; import baritone.api.event.events.BlockChangeEvent; +import baritone.process.elytra.pathfinder.Chunk; +import baritone.process.elytra.pathfinder.NetherPathfinder; +import baritone.process.elytra.pathfinder.PathSegment; import baritone.utils.accessor.IPalettedContainer; -import dev.babbaj.pathfinder.NetherPathfinder; -import dev.babbaj.pathfinder.Octree; -import dev.babbaj.pathfinder.PathSegment; import net.minecraft.core.BlockPos; import net.minecraft.resources.ResourceKey; import net.minecraft.util.BitStorage; @@ -34,10 +34,8 @@ import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.chunk.PalettedContainer; import net.minecraft.world.phys.Vec3; -import sun.misc.Unsafe; import java.lang.ref.SoftReference; -import java.lang.reflect.Field; import java.nio.file.Path; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; @@ -46,33 +44,23 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; -import static net.minecraft.world.level.chunk.LevelChunkSection.SECTION_SIZE; - /** * @author Brady */ public final class NetherPathfinderContext implements IElytraPathFinder { - private static final Unsafe UNSAFE; - static { - try { - Field f = Unsafe.class.getDeclaredField("theUnsafe"); - f.setAccessible(true); - UNSAFE = (Unsafe) f.get(null); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - } private static final BlockState AIR_BLOCK_STATE = Blocks.AIR.defaultBlockState(); - // This lock must be held while there are active pointers to chunks in java, - // but we just hold it for the entire tick so we don't have to think much about it. + // The native library needed this lock held while there were pointers to its chunks in Java. + // The port needs none of that -- a chunk is an object that stays valid for whoever holds it, + // and the table takes lookups, inserts and culls from any thread at once -- and the lock is + // kept so that the threads still run in the order they always have. public final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); public final ReentrantReadWriteLock.ReadLock readLock = rwl.readLock(); public final ReentrantReadWriteLock.WriteLock writeLock = rwl.writeLock(); private final int maxHeight; // Visible for access in BlockStateOctreeInterface - final long context; + final NetherPathfinder context; private final long seed; // write locked operations private final ExecutorService writeExecutor = Executors.newSingleThreadExecutor(); @@ -92,21 +80,21 @@ public NetherPathfinderContext(long seed, Path cache, Level world) { int height = Math.min(world.dimensionType().height(), 384); if (!Baritone.settings().elytraAllowAboveRoof.value && dim == NetherPathfinder.DIMENSION_NETHER) height = Math.min(height, 128); this.maxHeight = height; - this.context = NetherPathfinder.newContext(seed, cache != null ? cache.toString() : null, dim, height, Baritone.settings().elytraCustomAllocator.value); + this.context = new NetherPathfinder(seed, cache != null ? cache.toString() : null, dim, height); this.seed = seed; this.boi = new BlockStateOctreeInterface(this); } public boolean hasChunk(ChunkPos pos) { - return NetherPathfinder.hasChunkFromJava(this.context, pos.x, pos.z); + return this.context.hasChunkFromJava(pos.x, pos.z); } public void queueCacheCulling(int chunkX, int chunkZ, int maxDistanceBlocks) { this.writeExecutor.execute(() -> { writeLock.lock(); try { - this.boi.chunkPtr = 0L; - NetherPathfinder.cullFarChunks(this.context, chunkX, chunkZ, maxDistanceBlocks); + this.boi.chunk = null; + this.context.cullFarChunks(chunkX, chunkZ, maxDistanceBlocks); } finally { writeLock.unlock(); } @@ -122,10 +110,10 @@ public void queueForPacking(final LevelChunk chunkIn) { if (chunk != null) { writeLock.lock(); try { - // we might free this chunk - this.boi.chunkPtr = 0L; - long ptr = NetherPathfinder.allocateAndInsertChunk(this.context, chunk.getPos().x, chunk.getPos().z); - writeChunkData(chunk, ptr); + // we might replace this chunk + this.boi.chunk = null; + final Chunk packed = this.context.allocateAndInsertChunk(chunk.getPos().x, chunk.getPos().z); + writeChunkData(chunk, packed); } finally { writeLock.unlock(); } @@ -139,13 +127,13 @@ public void queueBlockUpdate(BlockChangeEvent event) { // not inserting or deleting from the cache hashmap but it would still be bad for this function to race with itself writeLock.lock(); try { - long ptr = NetherPathfinder.getChunk(this.context, chunkPos.x, chunkPos.z); - if (ptr == 0) return; // this shouldn't ever happen + final Chunk chunk = this.context.getChunk(chunkPos.x, chunkPos.z); + if (chunk == null) return; // this shouldn't ever happen event.getBlocks().forEach(pair -> { BlockPos pos = pair.first().below(minY); if (pos.getY() < 0 || pos.getY() >= 384) return; boolean isSolid = pair.second() != AIR_BLOCK_STATE; - Octree.setBlock(ptr, pos.getX() & 15, pos.getY(), pos.getZ() & 15, isSolid); + chunk.setBlock(pos.getX() & 15, pos.getY(), pos.getZ() & 15, isSolid); }); } finally { writeLock.unlock(); @@ -162,8 +150,7 @@ public CompletableFuture pathFindAsync(final BlockPos src, fina return CompletableFuture.supplyAsync(() -> { l.lock(); try { - final PathSegment segment = NetherPathfinder.pathFind( - this.context, + final PathSegment segment = this.context.pathFind( adjustedSrc.getX(), adjustedSrc.getY(), adjustedSrc.getZ(), adjustedDst.getX(), adjustedDst.getY(), adjustedDst.getZ(), !Baritone.settings().elytraAllowTightSpaces.value, // atleastX4 @@ -200,7 +187,7 @@ public boolean raytrace(final double startX, final double startY, final double s final double endX, final double endY, final double endZ) { final double adjustedStartY = startY - this.minY; final double adjustedEndY = endY - this.minY; - return NetherPathfinder.isVisible(this.context, NetherPathfinder.CACHE_MISS_SOLID, startX, adjustedStartY, startZ, endX, adjustedEndY, endZ); + return this.context.isVisible(NetherPathfinder.CACHE_MISS_SOLID, startX, adjustedStartY, startZ, endX, adjustedEndY, endZ); } /** @@ -214,7 +201,7 @@ public boolean raytrace(final double startX, final double startY, final double s public boolean raytrace(final Vec3 start, final Vec3 end) { final Vec3 adjustedStart = start.subtract(0, this.minY, 0); final Vec3 adjustedEnd = end.subtract(0, this.minY, 0); - return NetherPathfinder.isVisible(this.context, NetherPathfinder.CACHE_MISS_SOLID, adjustedStart.x, adjustedStart.y, adjustedStart.z, adjustedEnd.x, adjustedEnd.y, adjustedEnd.z); + return this.context.isVisible(NetherPathfinder.CACHE_MISS_SOLID, adjustedStart.x, adjustedStart.y, adjustedStart.z, adjustedEnd.x, adjustedEnd.y, adjustedEnd.z); } public boolean raytrace(final int count, final double[] src, final double[] dst, final int visibility) { @@ -229,11 +216,11 @@ public boolean raytrace(final int count, final double[] src, final double[] dst, switch (visibility) { case Visibility.ALL: - return NetherPathfinder.isVisibleMulti(this.context, NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, false) == -1; + return this.context.isVisibleMulti(NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, false) == -1; case Visibility.NONE: - return NetherPathfinder.isVisibleMulti(this.context, NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, true) == -1; + return this.context.isVisibleMulti(NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, true) == -1; case Visibility.ANY: - return NetherPathfinder.isVisibleMulti(this.context, NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, true) != -1; + return this.context.isVisibleMulti(NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, true) != -1; default: throw new IllegalArgumentException("lol"); } @@ -249,7 +236,7 @@ public void raytrace(final int count, final double[] src, final double[] dst, fi dst[i] -= this.minY; } - NetherPathfinder.raytrace(this.context, NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, hitsOut, hitPosOut); + this.context.raytrace(NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, hitsOut, hitPosOut); } public boolean passable(int x, int y, int z) { @@ -257,7 +244,7 @@ public boolean passable(int x, int y, int z) { } public void cancel() { - NetherPathfinder.cancel(this.context); + this.context.cancel(); } public void destroy() { @@ -273,7 +260,7 @@ public void destroy() { e.printStackTrace(); } - NetherPathfinder.freeContext(this.context); + this.context.close(); } public long getSeed() { @@ -296,7 +283,7 @@ public int getMaxHeight() { return this.maxHeight; } - private static void writeChunkData(LevelChunk chunk, long chunkPtr) { + private static void writeChunkData(LevelChunk chunk, Chunk packed) { try { LevelChunkSection[] chunkInternalStorageArray = chunk.getSections(); final int maxSections = Math.min(chunkInternalStorageArray.length, 24); // pathfinder support stops at 384/16 sections @@ -321,8 +308,7 @@ private static void writeChunkData(LevelChunk chunk, long chunkPtr) { else if (bs == Blocks.BROWN_MUSHROOM.defaultBlockState()) brownMushroomId = i; } if (airId == -1 & caveAirId == -1) { - final long bytesInSection = SECTION_SIZE / 8; - UNSAFE.setMemory(chunkPtr + (y0 * bytesInSection), bytesInSection, (byte) 0xFF); + packed.fillSection(y0, true); continue; } // pasted from FasterWorldScanner @@ -344,7 +330,7 @@ private static void writeChunkData(LevelChunk chunk, long chunkPtr) { // Avoid unnecessary writes that may trigger a page allocation if (!(value == airId | value == caveAirId) & value != redMushroomId & value != brownMushroomId) { - Octree.setBlock(chunkPtr, x, y, z, true); + packed.setBlock(x, y, z, true); } } } @@ -355,10 +341,6 @@ private static void writeChunkData(LevelChunk chunk, long chunkPtr) { } } - public static boolean isSupported() { - return NetherPathfinder.isThisSystemSupported(); - } - public static final class Visibility { public static final int ALL = 0; public static final int NONE = 1; diff --git a/src/main/java/baritone/process/elytra/NullElytraProcess.java b/src/main/java/baritone/process/elytra/NullElytraProcess.java deleted file mode 100644 index a0f0ff1efb..0000000000 --- a/src/main/java/baritone/process/elytra/NullElytraProcess.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * This file is part of Baritone. - * - * Baritone is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Baritone is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Baritone. If not, see . - */ - -package baritone.process.elytra; - -import baritone.Baritone; -import baritone.api.pathing.goals.Goal; -import baritone.api.process.IElytraProcess; -import baritone.api.process.PathingCommand; -import baritone.api.utils.BetterBlockPos; -import baritone.utils.BaritoneProcessHelper; -import net.minecraft.core.BlockPos; - -import java.util.Collections; -import java.util.List; - -/** - * @author Brady - */ -public final class NullElytraProcess extends BaritoneProcessHelper implements IElytraProcess { - - public NullElytraProcess(Baritone baritone) { - super(baritone); - } - - @Override - public void repackChunks() { - throw new UnsupportedOperationException("Called repackChunks() on NullElytraBehavior"); - } - - @Override - public BlockPos currentDestination() { - return null; - } - - @Override - public List getPath() { - return Collections.emptyList(); - } - - @Override - public void pathTo(BlockPos destination) { - throw new UnsupportedOperationException("Called pathTo() on NullElytraBehavior"); - } - - @Override - public void pathTo(Goal destination) { - throw new UnsupportedOperationException("Called pathTo() on NullElytraBehavior"); - } - - @Override - public void resetState() { - - } - - @Override - public boolean isActive() { - return false; - } - - @Override - public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { - throw new UnsupportedOperationException("Called onTick on NullElytraProcess"); - } - - @Override - public void onLostControl() { - - } - - @Override - public String displayName0() { - return "NullElytraProcess"; - } - - @Override - public boolean isLoaded() { - return false; - } - - @Override - public boolean isSafeToCancel() { - return true; - } - - -} diff --git a/src/main/java/baritone/process/elytra/UnpackedSegment.java b/src/main/java/baritone/process/elytra/UnpackedSegment.java index e50ab32357..8a9eae8ea2 100644 --- a/src/main/java/baritone/process/elytra/UnpackedSegment.java +++ b/src/main/java/baritone/process/elytra/UnpackedSegment.java @@ -18,7 +18,7 @@ package baritone.process.elytra; import baritone.api.utils.BetterBlockPos; -import dev.babbaj.pathfinder.PathSegment; +import baritone.process.elytra.pathfinder.PathSegment; import java.util.Arrays; import java.util.HashMap; From 21f2dbe0a5b08a38cbd969bd7c917e7193efe9db Mon Sep 17 00:00:00 2001 From: Mattias Rask <231212847+0Mattias@users.noreply.github.com> Date: Sat, 12 Sep 2026 04:06:29 -0400 Subject: [PATCH 03/15] Read a cached region's blocks in the order Baritone writes them Baritone's CachedRegion writes a chunk's two bits per block with BitSet.toByteArray(): bit n sits in byte n / 8 at position n % 8, the lowest bit first. The native reader took a byte's bits from the top down (baritone.cpp, get2Bits: `>> (6 - (i % 8))`) and the port copied it, so block x of every aligned run of four along x came back as block x ^ 3: each such run mirrored, in every chunk a search took from the region cache rather than from the game. Hard to see in flight, since the cache only fills in what the game has not loaded and terrain in runs of four is close to its own mirror image, but wrong. The test packed its file the same wrong way and passed; it now builds the chunk with a BitSet as CachedChunk does, sets one block solid and one water, and checks that the mirror positions are air. The region directory is Baritone's `cache`, not `regions`, in the docs and the test, and the oracle's doc names its real path. The same fix, on the port's own branch, is 0Mattias/nether-pathfinder 4784d69. Co-Authored-By: Claude Fable 5.1 --- .../elytra/pathfinder/BaritoneRegion.java | 9 +++- .../elytra/pathfinder/BaritoneRegionTest.java | 41 +++++++++++++++---- .../process/elytra/pathfinder/Oracle.java | 2 +- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/main/java/baritone/process/elytra/pathfinder/BaritoneRegion.java b/src/main/java/baritone/process/elytra/pathfinder/BaritoneRegion.java index 5f88ce8696..8a74fa4eef 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/BaritoneRegion.java +++ b/src/main/java/baritone/process/elytra/pathfinder/BaritoneRegion.java @@ -39,7 +39,7 @@ interface ChunkSink { private BaritoneRegion() {} /** - * The dimension of a Baritone cache directory such as {@code .../the_nether_128/regions}, + * The dimension of a Baritone cache directory such as {@code .../the_nether_128/cache}, * or -1 if the directory is not one. */ static int dimensionOf(String dir) { @@ -100,8 +100,13 @@ private static int positionIndex(int x, int y, int z) { return (x << 1) | (z << 5) | (y << 9); } + /** + * The two bits at bit index i, as {@code BitSet.toByteArray()} lays them out: bit n is in byte + * n / 8 at position n % 8, the lowest bit first. The native reader took a byte's bits from the + * top down, which mirrored every aligned run of four blocks along x. + */ private static int get2Bits(int i, byte[] data) { - return (data[i / 8] >> (6 - (i % 8))) & 0b11; + return (data[i / 8] >> (i % 8)) & 0b11; } private static Chunk parseChunk(int height, byte[] data) { diff --git a/src/test/java/baritone/process/elytra/pathfinder/BaritoneRegionTest.java b/src/test/java/baritone/process/elytra/pathfinder/BaritoneRegionTest.java index 7feb7ff795..60ad801f73 100644 --- a/src/test/java/baritone/process/elytra/pathfinder/BaritoneRegionTest.java +++ b/src/test/java/baritone/process/elytra/pathfinder/BaritoneRegionTest.java @@ -24,6 +24,7 @@ import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.util.BitSet; import java.util.zip.GZIPOutputStream; import static org.junit.Assert.assertEquals; @@ -34,7 +35,15 @@ public class BaritoneRegionTest { - /** A nether region with one chunk at (3, 5) that has the block (1, 2, 3) set. */ + /** + * A nether region with one chunk at (3, 5), written the way Baritone's CachedRegion writes + * one: the magic, then for each of the 32 by 32 chunks a byte saying whether it is present + * and, if it is, the chunk's two bits per block as {@code BitSet.toByteArray()} lays them + * out -- bit n in byte n / 8 at position n % 8, the lowest bit first. Block (x, y, z) owns + * bits {@code (x << 1) | (z << 5) | (y << 9)} and the one after, CachedChunk.getPositionIndex. + * The chunk has block (1, 2, 3) solid (both bits) and block (4, 2, 3) water (one bit), which + * the pathfinder also flies around. + */ private static byte[] region() throws Exception { final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); try (GZIPOutputStream gz = new GZIPOutputStream(bytes); DataOutputStream out = new DataOutputStream(gz)) { @@ -43,9 +52,14 @@ private static byte[] region() throws Exception { for (int z = 0; z < 32; z++) { if (x == 3 && z == 5) { out.writeByte(1); - final byte[] data = new byte[(2 * 16 * 16 * 256) / 8]; - final int bit = (1 << 1) | (3 << 5) | (2 << 9); // positionIndex(1, 2, 3) - data[bit / 8] |= 1 << (6 - (bit % 8)); + final int size = 2 * 16 * 16 * 256; + final BitSet bits = new BitSet(size); + bits.set(positionIndex(1, 2, 3)); + bits.set(positionIndex(1, 2, 3) + 1); + bits.set(positionIndex(4, 2, 3)); + final byte[] data = new byte[size / 8]; + final byte[] written = bits.toByteArray(); + System.arraycopy(written, 0, data, 0, written.length); // CachedRegion pads with zeros out.write(data); } else { out.writeByte(0); @@ -56,6 +70,10 @@ private static byte[] region() throws Exception { return bytes.toByteArray(); } + private static int positionIndex(int x, int y, int z) { + return (x << 1) | (z << 5) | (y << 9); + } + @Test public void parsesAChunk() throws Exception { final Chunk[] found = new Chunk[1]; @@ -71,6 +89,11 @@ public void parsesAChunk() throws Exception { assertEquals(3 - 32, at[0]); assertEquals(5 + 64, at[1]); assertTrue(found[0].isSolid(1, 2, 3)); + assertTrue(found[0].isSolid(4, 2, 3)); + // the other three blocks of each aligned run of four along x are air: a reader that takes + // a byte's bits from the top down mirrors every such run, and would put these at 2 and 7 + assertFalse(found[0].isSolid(2, 2, 3)); + assertFalse(found[0].isSolid(7, 2, 3)); assertFalse(found[0].isSolid(3, 2, 1)); assertTrue(found[0].isEmptyX16(64)); } @@ -78,7 +101,7 @@ public void parsesAChunk() throws Exception { @Test public void aSearchLoadsTheRegionItReaches() throws Exception { final Path dir = Files.createTempDirectory("np-region"); - final Path regions = dir.resolve("the_nether_128").resolve("regions"); + final Path regions = dir.resolve("the_nether_128").resolve("cache"); Files.createDirectories(regions); Files.write(regions.resolve("r.0.0.bcr"), region()); try { @@ -110,9 +133,9 @@ public void aSearchLoadsTheRegionItReaches() throws Exception { @Test public void directoryNamesDecideTheDimension() { - assertEquals(NetherPathfinder.DIMENSION_NETHER, BaritoneRegion.dimensionOf("/a/b/the_nether_128/regions")); - assertEquals(NetherPathfinder.DIMENSION_OVERWORLD, BaritoneRegion.dimensionOf("/a/b/overworld_384/regions")); - assertEquals(NetherPathfinder.DIMENSION_END, BaritoneRegion.dimensionOf("/a/b/the_end_256/regions")); - assertEquals(-1, BaritoneRegion.dimensionOf("/a/b/somewhere/regions")); + assertEquals(NetherPathfinder.DIMENSION_NETHER, BaritoneRegion.dimensionOf("/a/b/the_nether_128/cache")); + assertEquals(NetherPathfinder.DIMENSION_OVERWORLD, BaritoneRegion.dimensionOf("/a/b/overworld_384/cache")); + assertEquals(NetherPathfinder.DIMENSION_END, BaritoneRegion.dimensionOf("/a/b/the_end_256/cache")); + assertEquals(-1, BaritoneRegion.dimensionOf("/a/b/somewhere/cache")); } } diff --git a/src/test/java/baritone/process/elytra/pathfinder/Oracle.java b/src/test/java/baritone/process/elytra/pathfinder/Oracle.java index a2b98cc7d9..6c9e868e39 100644 --- a/src/test/java/baritone/process/elytra/pathfinder/Oracle.java +++ b/src/test/java/baritone/process/elytra/pathfinder/Oracle.java @@ -27,7 +27,7 @@ import java.util.ArrayList; import java.util.List; -/** What the native library answered, written by oracle.cpp in the nether-pathfinder repository. */ +/** What the native library answered, written by java/oracle/oracle.cpp. */ final class Oracle { static final long SEED = 146008555100680L; From a864194924eab5ef82518bebd55ac65738fe37f9 Mon Sep 17 00:00:00 2001 From: Mattias Rask <231212847+0Mattias@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:07:27 -0400 Subject: [PATCH 04/15] Apply the review to the Java pathfinder Every inline comment from the first review, in order: the native allocator setting is gone; BaritoneRegion checks CachedRegion.CACHED_REGION_MAGIC instead of a copy; "from java" is "from caller"; the cache-miss modes and the dimensions are enums; PathSegment carries a List instead of packed longs; the search and region-read timers use currentTimeMillis; the port uses Minecraft's BlockPos and its own class is deleted; slabs are sections; the Raytracer's switch computes the child origin and offset once; the batched raytrace, isVisible and isVisibleMulti wrappers are removed and callers use Raytracer.raytrace directly, which returns the hit position or null; and the x8 summary bit is no longer maintained on clears, since a stale set bit only costs a reader a scan. Also carried: pathFind clears the cancel flag on the way out rather than in, so a cancel that lands while a search is still queued behind the lock is honoured by that search instead of wiped; CancelTest covers it. Co-Authored-By: Claude Fable 5.1 --- src/api/java/baritone/api/Settings.java | 6 - .../java/baritone/cache/CachedRegion.java | 3 +- .../elytra/NetherPathfinderContext.java | 59 ++++-- .../process/elytra/UnpackedSegment.java | 3 +- .../elytra/pathfinder/BaritoneRegion.java | 25 ++- .../process/elytra/pathfinder/BlockPos.java | 126 ------------- .../process/elytra/pathfinder/Chunk.java | 81 ++++---- .../process/elytra/pathfinder/Face.java | 16 +- .../elytra/pathfinder/NetherPathfinder.java | 177 ++++++------------ .../process/elytra/pathfinder/NodePos.java | 17 +- .../process/elytra/pathfinder/PathFinder.java | 77 ++++---- .../process/elytra/pathfinder/PathNode.java | 4 +- .../elytra/pathfinder/PathSegment.java | 11 +- .../process/elytra/pathfinder/Raytracer.java | 112 +++++------ .../elytra/pathfinder/BaritoneRegionTest.java | 18 +- .../process/elytra/pathfinder/Bench.java | 9 +- .../process/elytra/pathfinder/CancelTest.java | 90 +++++++++ .../process/elytra/pathfinder/ChunkTest.java | 32 ++-- .../elytra/pathfinder/DegenerateRaysTest.java | 58 +++--- .../process/elytra/pathfinder/Oracle.java | 2 +- .../elytra/pathfinder/PathFindTest.java | 60 +++--- .../elytra/pathfinder/RaytraceTest.java | 87 +++++---- 22 files changed, 489 insertions(+), 584 deletions(-) delete mode 100644 src/main/java/baritone/process/elytra/pathfinder/BlockPos.java create mode 100644 src/test/java/baritone/process/elytra/pathfinder/CancelTest.java diff --git a/src/api/java/baritone/api/Settings.java b/src/api/java/baritone/api/Settings.java index 4c3f99d864..ceacfb9392 100644 --- a/src/api/java/baritone/api/Settings.java +++ b/src/api/java/baritone/api/Settings.java @@ -1549,12 +1549,6 @@ public final class Settings { */ public final Setting elytraChatSpam = new Setting<>(false); - /** - * Does nothing. The pathfinder is Java now and has no allocator of its own; kept so that a - * settings file naming it still loads. - */ - @Deprecated - public final Setting elytraCustomAllocator = new Setting<>(true); /** * Allow the pathfinder to attempt flight in tighter spaces, useful in caves but can be dangerous. diff --git a/src/main/java/baritone/cache/CachedRegion.java b/src/main/java/baritone/cache/CachedRegion.java index 8aa992d794..d9efa1db27 100644 --- a/src/main/java/baritone/cache/CachedRegion.java +++ b/src/main/java/baritone/cache/CachedRegion.java @@ -44,7 +44,8 @@ public final class CachedRegion implements ICachedRegion { /** * Magic value to detect invalid cache files, or incompatible cache files saved in an old version of Baritone */ - private static final int CACHED_REGION_MAGIC = 456022911; + // Public: the elytra pathfinder's region reader checks the same header. + public static final int CACHED_REGION_MAGIC = 456022911; /** * All of the chunks in this region: A 32x32 array of them. diff --git a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java index 878dc2f292..c69553a81c 100644 --- a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java +++ b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java @@ -22,6 +22,7 @@ import baritone.process.elytra.pathfinder.Chunk; import baritone.process.elytra.pathfinder.NetherPathfinder; import baritone.process.elytra.pathfinder.PathSegment; +import baritone.process.elytra.pathfinder.Raytracer; import baritone.utils.accessor.IPalettedContainer; import net.minecraft.core.BlockPos; import net.minecraft.resources.ResourceKey; @@ -73,12 +74,12 @@ public final class NetherPathfinderContext implements IElytraPathFinder { public NetherPathfinderContext(long seed, Path cache, Level world) { this.dimension = world.dimension(); this.minY = world.dimensionType().minY(); - final int dim; - if (this.dimension == Level.NETHER) dim = NetherPathfinder.DIMENSION_NETHER; - else if (this.dimension == Level.END) dim = NetherPathfinder.DIMENSION_END; - else dim = NetherPathfinder.DIMENSION_OVERWORLD; + final NetherPathfinder.Dimension dim; + if (this.dimension == Level.NETHER) dim = NetherPathfinder.Dimension.NETHER; + else if (this.dimension == Level.END) dim = NetherPathfinder.Dimension.END; + else dim = NetherPathfinder.Dimension.OVERWORLD; int height = Math.min(world.dimensionType().height(), 384); - if (!Baritone.settings().elytraAllowAboveRoof.value && dim == NetherPathfinder.DIMENSION_NETHER) height = Math.min(height, 128); + if (!Baritone.settings().elytraAllowAboveRoof.value && dim == NetherPathfinder.Dimension.NETHER) height = Math.min(height, 128); this.maxHeight = height; this.context = new NetherPathfinder(seed, cache != null ? cache.toString() : null, dim, height); this.seed = seed; @@ -86,7 +87,7 @@ public NetherPathfinderContext(long seed, Path cache, Level world) { } public boolean hasChunk(ChunkPos pos) { - return this.context.hasChunkFromJava(pos.x, pos.z); + return this.context.hasChunkFromCaller(pos.x, pos.z); } public void queueCacheCulling(int chunkX, int chunkZ, int maxDistanceBlocks) { @@ -185,9 +186,7 @@ public CompletableFuture pathFindAsync(final BlockPos src, fina */ public boolean raytrace(final double startX, final double startY, final double startZ, final double endX, final double endY, final double endZ) { - final double adjustedStartY = startY - this.minY; - final double adjustedEndY = endY - this.minY; - return this.context.isVisible(NetherPathfinder.CACHE_MISS_SOLID, startX, adjustedStartY, startZ, endX, adjustedEndY, endZ); + return Raytracer.raytrace(this.context, startX, startY - this.minY, startZ, endX, endY - this.minY, endZ, NetherPathfinder.CacheMiss.SOLID) == null; } /** @@ -199,9 +198,7 @@ public boolean raytrace(final double startX, final double startY, final double s * @return {@code true} if there is visibility between the points */ public boolean raytrace(final Vec3 start, final Vec3 end) { - final Vec3 adjustedStart = start.subtract(0, this.minY, 0); - final Vec3 adjustedEnd = end.subtract(0, this.minY, 0); - return this.context.isVisible(NetherPathfinder.CACHE_MISS_SOLID, adjustedStart.x, adjustedStart.y, adjustedStart.z, adjustedEnd.x, adjustedEnd.y, adjustedEnd.z); + return raytrace(start.x, start.y, start.z, end.x, end.y, end.z); } public boolean raytrace(final int count, final double[] src, final double[] dst, final int visibility) { @@ -216,16 +213,37 @@ public boolean raytrace(final int count, final double[] src, final double[] dst, switch (visibility) { case Visibility.ALL: - return this.context.isVisibleMulti(NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, false) == -1; + for (int i = 0; i < count; i++) { + if (!clear(src, dst, i)) { + return false; + } + } + return true; case Visibility.NONE: - return this.context.isVisibleMulti(NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, true) == -1; + for (int i = 0; i < count; i++) { + if (clear(src, dst, i)) { + return false; + } + } + return true; case Visibility.ANY: - return this.context.isVisibleMulti(NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, true) != -1; + for (int i = 0; i < count; i++) { + if (clear(src, dst, i)) { + return true; + } + } + return false; default: throw new IllegalArgumentException("lol"); } } + /** Whether ray i of a batch already in the pathfinder's y range reaches its end. */ + private boolean clear(double[] src, double[] dst, int i) { + final int o = i * 3; + return Raytracer.raytrace(this.context, src[o], src[o + 1], src[o + 2], dst[o], dst[o + 1], dst[o + 2], NetherPathfinder.CacheMiss.SOLID) == null; + } + public void raytrace(final int count, final double[] src, final double[] dst, final boolean[] hitsOut, final double[] hitPosOut) { if (src.length != count * 3 || dst.length != count * 3) { throw new IllegalArgumentException("Bad array lengths"); @@ -236,7 +254,16 @@ public void raytrace(final int count, final double[] src, final double[] dst, fi dst[i] -= this.minY; } - this.context.raytrace(NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, hitsOut, hitPosOut); + for (int i = 0; i < count; i++) { + final int o = i * 3; + final Vec3 hit = Raytracer.raytrace(this.context, src[o], src[o + 1], src[o + 2], dst[o], dst[o + 1], dst[o + 2], NetherPathfinder.CacheMiss.SOLID); + hitsOut[i] = hit != null; + if (hit != null && hitPosOut != null) { + hitPosOut[o] = hit.x; + hitPosOut[o + 1] = hit.y; + hitPosOut[o + 2] = hit.z; + } + } } public boolean passable(int x, int y, int z) { diff --git a/src/main/java/baritone/process/elytra/UnpackedSegment.java b/src/main/java/baritone/process/elytra/UnpackedSegment.java index 8a9eae8ea2..cb44fcc4dd 100644 --- a/src/main/java/baritone/process/elytra/UnpackedSegment.java +++ b/src/main/java/baritone/process/elytra/UnpackedSegment.java @@ -20,7 +20,6 @@ import baritone.api.utils.BetterBlockPos; import baritone.process.elytra.pathfinder.PathSegment; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -76,7 +75,7 @@ public boolean isFinished() { public static UnpackedSegment from(final PathSegment segment) { return new UnpackedSegment( - Arrays.stream(segment.packed).mapToObj(BetterBlockPos::deserializeFromLong), + segment.blocks.stream().map(BetterBlockPos::new), segment.finished ); } diff --git a/src/main/java/baritone/process/elytra/pathfinder/BaritoneRegion.java b/src/main/java/baritone/process/elytra/pathfinder/BaritoneRegion.java index 8a74fa4eef..5779d12948 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/BaritoneRegion.java +++ b/src/main/java/baritone/process/elytra/pathfinder/BaritoneRegion.java @@ -17,6 +17,7 @@ package baritone.process.elytra.pathfinder; +import baritone.cache.CachedRegion; import java.io.DataInputStream; import java.io.EOFException; import java.io.IOException; @@ -29,8 +30,6 @@ /** Reads the chunks of one of Baritone's cached region files ({@code r.X.Z.bcr}). */ final class BaritoneRegion { - private static final int MAGIC = 456022911; - /** What the reader hands each chunk it finds. */ interface ChunkSink { void accept(int chunkX, int chunkZ, Chunk chunk); @@ -40,16 +39,16 @@ private BaritoneRegion() {} /** * The dimension of a Baritone cache directory such as {@code .../the_nether_128/cache}, - * or -1 if the directory is not one. + * or null if the directory is not one. */ - static int dimensionOf(String dir) { + static NetherPathfinder.Dimension dimensionOf(String dir) { final Path parent = Paths.get(dir).getParent(); final String name = parent == null || parent.getFileName() == null ? "" : parent.getFileName().toString(); switch (name) { - case "the_nether_128": return NetherPathfinder.DIMENSION_NETHER; - case "overworld_384": return NetherPathfinder.DIMENSION_OVERWORLD; - case "the_end_256": return NetherPathfinder.DIMENSION_END; - default: return -1; + case "the_nether_128": return NetherPathfinder.Dimension.NETHER; + case "overworld_384": return NetherPathfinder.Dimension.OVERWORLD; + case "the_end_256": return NetherPathfinder.Dimension.END; + default: return null; } } @@ -59,8 +58,8 @@ static Path regionFile(String dir, int regionX, int regionZ) { /** Reads the region file for (regionX, regionZ) under dir, if there is one. Returns whether a file was read. */ static boolean load(String dir, int regionX, int regionZ, ChunkSink sink) { - final int dim = dimensionOf(dir); - if (dim == -1) { + final NetherPathfinder.Dimension dim = dimensionOf(dir); + if (dim == null) { return false; } final Path file = regionFile(dir, regionX, regionZ); @@ -76,14 +75,14 @@ static boolean load(String dir, int regionX, int regionZ, ChunkSink sink) { } /** Parses an already decompressed region stream. */ - static void parse(InputStream raw, int regionX, int regionZ, int dimension, ChunkSink sink) throws IOException { + static void parse(InputStream raw, int regionX, int regionZ, NetherPathfinder.Dimension dimension, ChunkSink sink) throws IOException { final DataInputStream in = new DataInputStream(raw); final int magic = in.readInt(); - if (magic != MAGIC) { + if (magic != CachedRegion.CACHED_REGION_MAGIC) { System.err.println("[nether-pathfinder] bad magic for baritone region " + regionX + "," + regionZ); return; } - final int height = dimension == NetherPathfinder.DIMENSION_OVERWORLD ? 384 : 256; + final int height = dimension == NetherPathfinder.Dimension.OVERWORLD ? 384 : 256; final byte[] data = new byte[(2 * 16 * 16 * height) / 8]; for (int x = 0; x < 32; x++) { for (int z = 0; z < 32; z++) { diff --git a/src/main/java/baritone/process/elytra/pathfinder/BlockPos.java b/src/main/java/baritone/process/elytra/pathfinder/BlockPos.java deleted file mode 100644 index 402dba3d2f..0000000000 --- a/src/main/java/baritone/process/elytra/pathfinder/BlockPos.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * This file is part of Baritone. - * - * Baritone is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Baritone is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Baritone. If not, see . - */ - -package baritone.process.elytra.pathfinder; - -/** An integer position. Immutable. */ -final class BlockPos { - - final int x; - final int y; - final int z; - - BlockPos(int x, int y, int z) { - this.x = x; - this.y = y; - this.z = z; - } - - int chunkX() { - return this.x >> 4; - } - - int chunkZ() { - return this.z >> 4; - } - - double distanceToSq(BlockPos pos) { - final double dx = pos.x - this.x; - final double dy = pos.y - this.y; - final double dz = pos.z - this.z; - return (dx * dx) + (dy * dy) + (dz * dz); - } - - double distanceTo(BlockPos pos) { - return Math.sqrt(this.distanceToSq(pos)); - } - - BlockPos offset(Face face, int n) { - switch (face) { - case UP: return up(n); - case DOWN: return down(n); - case NORTH: return north(n); - case SOUTH: return south(n); - case EAST: return east(n); - default: return west(n); - } - } - - BlockPos up(int n) { - return new BlockPos(this.x, this.y + n, this.z); - } - - BlockPos down(int n) { - return new BlockPos(this.x, this.y - n, this.z); - } - - BlockPos east(int n) { - return new BlockPos(this.x + n, this.y, this.z); - } - - BlockPos west(int n) { - return new BlockPos(this.x - n, this.y, this.z); - } - - BlockPos north(int n) { - return new BlockPos(this.x, this.y, this.z - n); - } - - BlockPos south(int n) { - return new BlockPos(this.x, this.y, this.z + n); - } - - BlockPos plus(int n) { - return new BlockPos(this.x + n, this.y + n, this.z + n); - } - - BlockPos shiftRight(int n) { - return new BlockPos(this.x >> n, this.y >> n, this.z >> n); - } - - BlockPos shiftLeft(int n) { - return new BlockPos(this.x << n, this.y << n, this.z << n); - } - - static int floor(double v) { - final int i = (int) v; - return v < i ? i - 1 : i; - } - - static BlockPos of(double x, double y, double z) { - return new BlockPos(floor(x), floor(y), floor(z)); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof BlockPos)) { - return false; - } - final BlockPos p = (BlockPos) o; - return p.x == this.x && p.y == this.y && p.z == this.z; - } - - @Override - public int hashCode() { - return (this.x * 8734625) ^ (this.y * 2873465) ^ (this.z * 3457689); - } - - @Override - public String toString() { - return "{" + this.x + ", " + this.y + ", " + this.z + "}"; - } -} diff --git a/src/main/java/baritone/process/elytra/pathfinder/Chunk.java b/src/main/java/baritone/process/elytra/pathfinder/Chunk.java index 8cc39316a3..0e7678683e 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/Chunk.java +++ b/src/main/java/baritone/process/elytra/pathfinder/Chunk.java @@ -22,22 +22,22 @@ /** * A 16 by 384 by 16 column of blocks, one bit each, laid out as an octree so that a cube of 2, 4, * 8 or 16 blocks can be tested for emptiness with a few long reads. The layout is the native - * library's: 24 slabs of 16x16x16 blocks (512 bytes each); a slab is 8 x8 cubes of 64 bytes; an + * library's: 24 sections of 16x16x16 blocks (512 bytes each); a section is 8 x8 cubes of 64 bytes; an * x8 is 8 x4 cubes of 8 bytes; an x4 is 8 x2 cubes of one byte; the 8 bits of that byte are the - * blocks. A slab in which no block has been set is not allocated. Beside the slabs, one byte per - * slab says which of its x8 cubes hold a block, so that the question a search asks first and a ray + * blocks. A section in which no block has been set is not allocated. Beside the sections, one byte per + * section says which of its x8 cubes may hold a block, so that the question a search asks first and a ray * asks of every x16 and x8 it enters, whether a cube that big is empty, is a bit test rather than a * scan of 64 or 8 longs. *

* Reads and writes are plain (not synchronized), as they were in the native library: a reader - * that races a writer may see a partly written slab, which is the same as before, and nothing + * that races a writer may see a partly written section, which is the same as before, and nothing * worse can happen. */ public final class Chunk { public static final int HEIGHT = 384; - public static final int SLABS = HEIGHT / 16; - static final int SLAB_LONGS = 64; // 512 bytes + public static final int SECTIONS = HEIGHT / 16; + static final int SECTION_LONGS = 64; // 512 bytes static final int X8_BYTES = 64; static final int X4_BYTES = 8; @@ -46,9 +46,9 @@ public final class Chunk { /** A chunk with every block solid. Shared; writes to it throw. */ public static final Chunk SOLID = new Chunk(true); - private final long[][] slabs = new long[SLABS][]; - /** Bit i of entry s is set while x8 cube i of slab s holds a block. A slab that is null has 0. */ - private final int[] filled = new int[SLABS]; + private final long[][] sections = new long[SECTIONS][]; + /** Bit i of entry s is set once a block is set in x8 cube i of section s, and stays set until fillSection resets it: set means the cube may hold a block, clear that it does not. A section that is null has 0. */ + private final int[] filled = new int[SECTIONS]; private final boolean shared; public Chunk() { @@ -58,9 +58,9 @@ public Chunk() { private Chunk(boolean solid) { this.shared = true; if (solid) { - for (int i = 0; i < SLABS; i++) { - this.slabs[i] = new long[SLAB_LONGS]; - Arrays.fill(this.slabs[i], -1L); + for (int i = 0; i < SECTIONS; i++) { + this.sections[i] = new long[SECTION_LONGS]; + Arrays.fill(this.sections[i], -1L); this.filled[i] = 0xFF; } } @@ -83,39 +83,32 @@ static int bitIndex(int x, int y, int z) { return ((x & 1) << 2) | ((y & 1) << 1) | ((z & 1)); } - /** Byte offset, within its slab, of the x2 cube that holds (x, y, z). */ + /** Byte offset, within its section, of the x2 cube that holds (x, y, z). */ static int x2Offset(int x, int y, int z) { return x8Index(x, y, z) * X8_BYTES + x4Index(x, y, z) * X4_BYTES + x2Index(x, y, z); } - /** The byte at {@code off} in a slab. */ - static int byteAt(long[] slab, int off) { - return (int) (slab[off >>> 3] >>> ((off & 7) << 3)) & 0xFF; + /** The byte at {@code off} in a section. */ + static int byteAt(long[] section, int off) { + return (int) (section[off >>> 3] >>> ((off & 7) << 3)) & 0xFF; } - static boolean allZero(long[] slab, int from, int longs) { - long acc = 0; - for (int i = 0; i < longs; i++) { - acc |= slab[from + i]; - } - return acc == 0; - } - /** The slab holding y, or null if nothing has been set in it (or y is outside the chunk). */ - long[] slab(int y) { + /** The section holding y, or null if nothing has been set in it (or y is outside the chunk). */ + long[] section(int y) { final int i = y >> 4; - return i >= 0 && i < SLABS ? this.slabs[i] : null; + return i >= 0 && i < SECTIONS ? this.sections[i] : null; } - /** Which x8 cubes of the slab holding y hold a block, one bit each in x8Index order; 0 outside the chunk. */ + /** Which x8 cubes of the section holding y may hold a block, one bit each in x8Index order; 0 outside the chunk. */ int filled(int y) { final int i = y >> 4; - return i >= 0 && i < SLABS ? this.filled[i] : 0; + return i >= 0 && i < SECTIONS ? this.filled[i] : 0; } /** Coordinates are chunk relative: x and z in 0..15, y in 0..383. */ public boolean isSolid(int x, int y, int z) { - final long[] s = this.slabs[y >> 4]; + final long[] s = this.sections[y >> 4]; if (s == null) { return false; } @@ -128,12 +121,12 @@ public void setBlock(int x, int y, int z, boolean solid) { if (this.shared) { throw new UnsupportedOperationException("the shared air and solid chunks are read only"); } - long[] s = this.slabs[y >> 4]; + long[] s = this.sections[y >> 4]; if (s == null) { if (!solid) { return; } - s = this.slabs[y >> 4] = new long[SLAB_LONGS]; + s = this.sections[y >> 4] = new long[SECTION_LONGS]; } final int off = x2Offset(x, y, z); final long mask = 1L << (((off & 7) << 3) + bitIndex(x, y, z)); @@ -143,10 +136,10 @@ public void setBlock(int x, int y, int z, boolean solid) { this.filled[y >> 4] |= 1 << x8; s[off >>> 3] |= mask; } else { + // The x8's bit in filled stays. It means the cube may hold a block, so one that + // outlives its blocks costs a reader a scan of the cube and never a wrong answer; + // fillSection resets it. s[off >>> 3] &= ~mask; - if (allZero(s, x8 * (X8_BYTES / 8), X8_BYTES / 8)) { - this.filled[y >> 4] &= ~(1 << x8); - } } } @@ -157,12 +150,12 @@ public void fillSection(int section, boolean solid) { } if (!solid) { this.filled[section] = 0; - this.slabs[section] = null; + this.sections[section] = null; return; } - long[] s = this.slabs[section]; + long[] s = this.sections[section]; if (s == null) { - s = this.slabs[section] = new long[SLAB_LONGS]; + s = this.sections[section] = new long[SECTION_LONGS]; } this.filled[section] = 0xFF; Arrays.fill(s, -1L); @@ -187,27 +180,27 @@ public boolean isEmptyX8(int x, int y, int z) { } public boolean isEmptyX4(int x, int y, int z) { - final long[] s = this.slabs[y >> 4]; + final long[] s = this.sections[y >> 4]; return s == null || s[x8Index(x, y, z) * (X8_BYTES / 8) + x4Index(x, y, z)] == 0; } public boolean isEmptyX2(int x, int y, int z) { - final long[] s = this.slabs[y >> 4]; + final long[] s = this.sections[y >> 4]; return s == null || byteAt(s, x2Offset(x, y, z)) == 0; } /** The chunk's 12288 bytes in the native layout (little endian words), for hashing in tests. */ byte[] toBytes() { - final byte[] out = new byte[SLABS * SLAB_LONGS * 8]; - for (int i = 0; i < SLABS; i++) { - final long[] s = this.slabs[i]; + final byte[] out = new byte[SECTIONS * SECTION_LONGS * 8]; + for (int i = 0; i < SECTIONS; i++) { + final long[] s = this.sections[i]; if (s == null) { continue; } - for (int j = 0; j < SLAB_LONGS; j++) { + for (int j = 0; j < SECTION_LONGS; j++) { long v = s[j]; for (int k = 0; k < 8; k++) { - out[(i * SLAB_LONGS + j) * 8 + k] = (byte) (v >>> (k * 8)); + out[(i * SECTION_LONGS + j) * 8 + k] = (byte) (v >>> (k * 8)); } } } diff --git a/src/main/java/baritone/process/elytra/pathfinder/Face.java b/src/main/java/baritone/process/elytra/pathfinder/Face.java index d318777a57..ba9fc80cc0 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/Face.java +++ b/src/main/java/baritone/process/elytra/pathfinder/Face.java @@ -17,6 +17,20 @@ package baritone.process.elytra.pathfinder; +import net.minecraft.core.BlockPos; + enum Face { - UP, DOWN, NORTH, SOUTH, EAST, WEST + UP, DOWN, NORTH, SOUTH, EAST, WEST; + + /** {@code pos} moved n blocks this way. */ + BlockPos offset(BlockPos pos, int n) { + switch (this) { + case UP: return pos.above(n); + case DOWN: return pos.below(n); + case NORTH: return pos.north(n); + case SOUTH: return pos.south(n); + case EAST: return pos.east(n); + default: return pos.west(n); + } + } } diff --git a/src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java b/src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java index e3f4b7649b..a8d470eb2c 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java +++ b/src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java @@ -17,7 +17,9 @@ package baritone.process.elytra.pathfinder; +import net.minecraft.core.BlockPos; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; @@ -33,27 +35,23 @@ */ public final class NetherPathfinder implements AutoCloseable { - // How the raytracer will treat chunks that aren't actually observed. - public static final int CACHE_MISS_GENERATE = 0; - public static final int CACHE_MISS_AIR = 1; - public static final int CACHE_MISS_SOLID = 2; + /** What a ray or a search makes of a chunk the table does not hold. */ + public enum CacheMiss { + /** Generate it from the seed, and keep it. */ + GENERATE, + /** Treat it as all air. */ + AIR, + /** Treat it as all solid. */ + SOLID + } - public static final int DIMENSION_OVERWORLD = 0; - public static final int DIMENSION_NETHER = 1; - public static final int DIMENSION_END = 2; + public enum Dimension { + OVERWORLD, NETHER, END + } - static final int STATE_FROM_JAVA = 0; + static final int STATE_FROM_CALLER = 0; static final int STATE_FAKE = 1; // could be generated or just air - private static final int NUM_X_BITS = 26; // 1 + MathHelper.log2(MathHelper.smallestEncompassingPowerOfTwo(30000000)); - private static final int NUM_Z_BITS = NUM_X_BITS; - private static final int NUM_Y_BITS = 64 - NUM_X_BITS - NUM_Z_BITS; - private static final int Y_SHIFT = NUM_Z_BITS; - private static final int X_SHIFT = Y_SHIFT + NUM_Y_BITS; - private static final long X_MASK = (1L << NUM_X_BITS) - 1L; - private static final long Y_MASK = (1L << NUM_Y_BITS) - 1L; - private static final long Z_MASK = (1L << NUM_Z_BITS) - 1L; - /** A chunk in the table, where it is, and whether it came from the game or was made up. */ static final class Entry { final Chunk chunk; @@ -77,19 +75,17 @@ static final class Entry { private final ChunkGeneratorHell generator; private final String baritoneCache; private final long seed; - private final int dimension; + private final Dimension dimension; final int maxHeight; /** * @param seed the world seed, for generating terrain where no chunk is known * @param baritoneCacheDirCanBeNull Baritone's region directory for the dimension, whose chunks a search loads as it reaches them; or null - * @param dimension one of the DIMENSION_ constants + * @param dimension the dimension the chunks are from, which decides their height * @param maxHeight the height the search and rays stay under, 1 to 384 */ - public NetherPathfinder(long seed, String baritoneCacheDirCanBeNull, int dimension, int maxHeight) { - if (dimension < 0 || dimension > 2) { - throw new IllegalArgumentException("Invalid dimension"); - } + public NetherPathfinder(long seed, String baritoneCacheDirCanBeNull, Dimension dimension, int maxHeight) { + Objects.requireNonNull(dimension, "dimension"); if (maxHeight <= 0 || maxHeight > Chunk.HEIGHT) { throw new IllegalArgumentException("Invalid max height (must be between 0 and 384)"); } @@ -100,21 +96,11 @@ public NetherPathfinder(long seed, String baritoneCacheDirCanBeNull, int dimensi this.generator = ChunkGeneratorHell.fromSeed(seed); } - /** The native API's name for the constructor. */ - public static NetherPathfinder newContext(long seed, String baritoneCacheDirCanBeNull, int dimension, int maxHeight) { - return new NetherPathfinder(seed, baritoneCacheDirCanBeNull, dimension, maxHeight); - } - - /** There is no native library any more, so every system is supported. Kept for callers of the old API. */ - public static boolean isThisSystemSupported() { - return true; - } - public long getSeed() { return this.seed; } - public int getDimension() { + public Dimension getDimension() { return this.dimension; } @@ -122,11 +108,6 @@ public int getMaxHeight() { return this.maxHeight; } - /** Cancels a running search and forgets every chunk. The native API's name for it. */ - public void freeContext() { - close(); - } - @Override public void close() { cancel(); @@ -145,23 +126,14 @@ static long key(int x, int z) { return (((long) x << 32) | (z & 0xFFFFFFFFL)) * 0x9E3779B97F4A7C15L; } - static int dimensionHeight(int dimension) { - return dimension == DIMENSION_OVERWORLD ? 384 : 256; - } - - static long packBlockPos(BlockPos pos) { - return ((long) pos.x & X_MASK) << X_SHIFT | ((long) pos.y & Y_MASK) << Y_SHIFT | ((long) pos.z & Z_MASK); + static int dimensionHeight(Dimension dimension) { + return dimension == Dimension.OVERWORLD ? 384 : 256; } private static boolean inBounds(int y) { return y >= 0 && y < Chunk.HEIGHT; } - private static void checkFakeChunkMode(int mode) { - if (mode < 0 || mode > 2) { - throw new IllegalArgumentException("fakeChunkMode must be 0 (Generate), 1 (Air), or 2 (Solid)"); - } - } // ---- the chunk table ---- @@ -172,7 +144,7 @@ private static void checkFakeChunkMode(int mode) { public void insertChunkData(int chunkX, int chunkZ, boolean[] data) { final int blocksInChunk = 16 * 16 * dimensionHeight(this.dimension); if (data.length != blocksInChunk) { - throw new IllegalArgumentException(this.dimension == DIMENSION_OVERWORLD ? "input is not 16 * 16 * 384 elements" : "input is not 16 * 16 * 256 elements"); + throw new IllegalArgumentException(this.dimension == Dimension.OVERWORLD ? "input is not 16 * 16 * 384 elements" : "input is not 16 * 16 * 256 elements"); } final Chunk chunk = new Chunk(); for (int i = 0; i < blocksInChunk; i++) { @@ -180,13 +152,13 @@ public void insertChunkData(int chunkX, int chunkZ, boolean[] data) { chunk.setBlock(i & 0xF, i >> 8, (i >> 4) & 0xF, true); } } - this.chunks.put(key(chunkX, chunkZ), new Entry(STATE_FROM_JAVA, chunk, chunkX, chunkZ)); + this.chunks.put(key(chunkX, chunkZ), new Entry(STATE_FROM_CALLER, chunk, chunkX, chunkZ)); } /** Inserts a new, empty chunk from the game at (x, z), replacing any chunk there, and returns it to be filled. */ public Chunk allocateAndInsertChunk(int x, int z) { final Chunk chunk = new Chunk(); - this.chunks.put(key(x, z), new Entry(STATE_FROM_JAVA, chunk, x, z)); + this.chunks.put(key(x, z), new Entry(STATE_FROM_CALLER, chunk, x, z)); return chunk; } @@ -203,18 +175,18 @@ public Chunk getChunk(int x, int z) { } /** Marks the chunk at (x, z) as from the game or as made up. Returns true if the chunk existed and the change was made. */ - public boolean setChunkState(int x, int z, boolean fromJava) { + public boolean setChunkState(int x, int z, boolean fromCaller) { final Entry e = this.chunks.get(key(x, z)); if (e == null) { return false; } - e.state = fromJava ? STATE_FROM_JAVA : STATE_FAKE; + e.state = fromCaller ? STATE_FROM_CALLER : STATE_FAKE; return true; } - public boolean hasChunkFromJava(int x, int z) { + public boolean hasChunkFromCaller(int x, int z) { final Entry e = this.chunks.get(key(x, z)); - return e != null && e.state == STATE_FROM_JAVA; + return e != null && e.state == STATE_FROM_CALLER; } /** Whether the table holds chunk (x, z), whichever way it got there. */ @@ -254,20 +226,23 @@ public PathSegment pathFind(int x1, int y1, int z1, int x2, int y2, int z2, bool if (!inBounds(y1) || !inBounds(y2)) { throw new IllegalArgumentException("Invalid y1 or y2"); } - this.cancelFlag.set(false); - final Size size = atLeastX4 ? Size.X4 : Size.X2; - final NodePos start = PathFinder.findAir(this, size, new BlockPos(x1, y1, z1), defaultAirElseGenerate); - final NodePos goal = PathFinder.findAir(this, size, new BlockPos(x2, y2, z2), defaultAirElseGenerate); - final PathFinder.Path path = PathFinder.findPathSegment(this, start, goal, atLeastX4, failTimeoutInMillis, defaultAirElseGenerate, fakeChunkCost); - if (path == null) { - return null; - } - final List blocks = refine ? Raytracer.refine(this, path.blocks) : path.blocks; - final long[] packed = new long[blocks.size()]; - for (int i = 0; i < packed.length; i++) { - packed[i] = packBlockPos(blocks.get(i)); + // The flag is cleared when the search ends, not when one starts. Clearing it here would + // drop a cancel that arrived while this call was still queued behind the lock -- which is + // exactly the cancel that destroy() sends -- and that search would then run to its full + // timeout holding the lock. Clearing on the way out instead means a cancellation is + // honoured by the search it was aimed at and never leaks into the search after it. + try { + final Size size = atLeastX4 ? Size.X4 : Size.X2; + final NodePos start = PathFinder.findAir(this, size, new BlockPos(x1, y1, z1), defaultAirElseGenerate); + final NodePos goal = PathFinder.findAir(this, size, new BlockPos(x2, y2, z2), defaultAirElseGenerate); + final PathFinder.Path path = PathFinder.findPathSegment(this, start, goal, atLeastX4, failTimeoutInMillis, defaultAirElseGenerate, fakeChunkCost); + if (path == null) { + return null; + } + return new PathSegment(path.type == PathFinder.Path.Type.FINISHED, refine ? Raytracer.refine(this, path.blocks) : path.blocks); + } finally { + this.cancelFlag.set(false); } - return new PathSegment(path.type == PathFinder.Path.Type.FINISHED, packed); } /** Asks a running search to stop. Returns whether it had already been asked. */ @@ -283,52 +258,6 @@ boolean clearCancelled() { return this.cancelFlag.getAndSet(false); } - // ---- rays ---- - - /** - * Traces {@code inputs} segments, start[i*3..] to end[i*3..]. hitsOut[i] says whether segment i - * hit a solid block, and if hitPosOutCanBeNull is given, where. Points and the segments between - * them must have 0 <= y < 384. - */ - public void raytrace(int fakeChunkMode, int inputs, double[] start, double[] end, boolean[] hitsOut, double[] hitPosOutCanBeNull) { - checkFakeChunkMode(fakeChunkMode); - if (start.length < (inputs * 3) || end.length < (inputs * 3) || hitsOut.length < inputs || (hitPosOutCanBeNull != null && hitPosOutCanBeNull.length < (inputs * 3))) { - throw new IllegalArgumentException("Bad array lengths idiot"); - } - for (int i = 0; i < inputs; i++) { - hitsOut[i] = Raytracer.raytrace(this, start[i * 3], start[i * 3 + 1], start[i * 3 + 2], end[i * 3], end[i * 3 + 1], end[i * 3 + 2], fakeChunkMode, hitPosOutCanBeNull, i * 3); - } - } - - /** - * With anyIfTrueElseAll, the index of the first segment that is clear, else the index of the - * first that is blocked; -1 if there is none. So -1 means "none clear" in the first mode and - * "all clear" in the second. - */ - public int isVisibleMulti(int fakeChunkMode, int inputs, double[] start, double[] end, boolean anyIfTrueElseAll) { - checkFakeChunkMode(fakeChunkMode); - if (start.length < (inputs * 3) || end.length < (inputs * 3)) { - throw new IllegalArgumentException("Bad array lengths idiot"); - } - for (int i = 0; i < inputs; i++) { - final boolean hit = Raytracer.raytrace(this, start[i * 3], start[i * 3 + 1], start[i * 3 + 2], end[i * 3], end[i * 3 + 1], end[i * 3 + 2], fakeChunkMode, null, 0); - if (!hit) { - if (anyIfTrueElseAll) { - return i; - } - } else if (!anyIfTrueElseAll) { - return i; - } - } - return -1; - } - - /** Whether there is line of sight between the two points. */ - public boolean isVisible(int fakeChunkMode, double x1, double y1, double z1, double x2, double y2, double z2) { - checkFakeChunkMode(fakeChunkMode); - return !Raytracer.raytrace(this, x1, y1, z1, x2, y2, z2, fakeChunkMode, null, 0); - } - // ---- for the search and the rays ---- Entry getChunkOrAir(int cx, int cz) { @@ -338,7 +267,7 @@ Entry getChunkOrAir(int cx, int cz) { Chunk getRealChunkOrDefault(int cx, int cz, boolean solid) { final Entry e = this.chunks.get(key(cx, cz)); - if (e == null || e.state != STATE_FROM_JAVA) { + if (e == null || e.state != STATE_FROM_CALLER) { return solid ? Chunk.SOLID : Chunk.AIR; } return e.chunk; @@ -357,16 +286,16 @@ Chunk getOrGenChunk(int cx, int cz) { return previous != null ? previous.chunk : chunk; } - Chunk getRealChunkFromCacheOrFakeChunkMaybeGen(int cx, int cz, int fakeChunkMode) { - if (fakeChunkMode == CACHE_MISS_GENERATE) { + Chunk getRealChunkFromCacheOrFakeChunkMaybeGen(int cx, int cz, CacheMiss fakeChunkMode) { + if (fakeChunkMode == CacheMiss.GENERATE) { return getOrGenChunk(cx, cz); } - return getRealChunkOrDefault(cx, cz, fakeChunkMode == CACHE_MISS_SOLID); + return getRealChunkOrDefault(cx, cz, fakeChunkMode == CacheMiss.SOLID); } /** * Loads the Baritone region holding chunk (cx, cz) the first time it is asked about. A chunk - * that is already in the table wins over the file's. Returns the nanoseconds spent on the file. + * that is already in the table wins over the file's. Returns the milliseconds spent on the file. */ long tryLoadRegion(int cx, int cz) { if (this.baritoneCache == null) { @@ -377,9 +306,9 @@ long tryLoadRegion(int cx, int cz) { if (!this.checkedRegions.add(key(regionX, regionZ))) { return 0; } - final long t1 = System.nanoTime(); + final long t1 = System.currentTimeMillis(); final boolean read = BaritoneRegion.load(this.baritoneCache, regionX, regionZ, - (x, z, chunk) -> this.chunks.putIfAbsent(key(x, z), new Entry(STATE_FROM_JAVA, chunk, x, z))); - return read ? System.nanoTime() - t1 : 0; + (x, z, chunk) -> this.chunks.putIfAbsent(key(x, z), new Entry(STATE_FROM_CALLER, chunk, x, z))); + return read ? System.currentTimeMillis() - t1 : 0; } } diff --git a/src/main/java/baritone/process/elytra/pathfinder/NodePos.java b/src/main/java/baritone/process/elytra/pathfinder/NodePos.java index dc235e4ade..081b3f9d1d 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/NodePos.java +++ b/src/main/java/baritone/process/elytra/pathfinder/NodePos.java @@ -17,6 +17,8 @@ package baritone.process.elytra.pathfinder; +import net.minecraft.core.BlockPos; + /** A cube of the octree: its size and its position in units of that size. */ final class NodePos { @@ -26,15 +28,18 @@ final class NodePos { NodePos(Size size, BlockPos approxPosition) { this.size = size; - this.pos = approxPosition.shiftRight(size.shift()); + final int shift = size.shift(); + this.pos = new BlockPos(approxPosition.getX() >> shift, approxPosition.getY() >> shift, approxPosition.getZ() >> shift); } BlockPos absolutePosZero() { - return this.pos.shiftLeft(this.size.shift()); + final int shift = this.size.shift(); + return new BlockPos(this.pos.getX() << shift, this.pos.getY() << shift, this.pos.getZ() << shift); } BlockPos absolutePosCenter() { - return this.absolutePosZero().plus(this.size.width() / 2); + final int half = this.size.width() / 2; + return this.absolutePosZero().offset(half, half, half); } @Override @@ -50,9 +55,9 @@ public boolean equals(Object o) { public int hashCode() { long hash = 3241; hash = 6406146L * hash + this.size.ordinal(); - hash = 3457689L * hash + this.pos.x; - hash = 8734625L * hash + this.pos.y; - hash = 2873465L * hash + this.pos.z; + hash = 3457689L * hash + this.pos.getX(); + hash = 8734625L * hash + this.pos.getY(); + hash = 2873465L * hash + this.pos.getZ(); return (int) (hash ^ (hash >>> 32)); } } diff --git a/src/main/java/baritone/process/elytra/pathfinder/PathFinder.java b/src/main/java/baritone/process/elytra/pathfinder/PathFinder.java index fb911e90c5..4bb4899626 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/PathFinder.java +++ b/src/main/java/baritone/process/elytra/pathfinder/PathFinder.java @@ -17,6 +17,7 @@ package baritone.process.elytra.pathfinder; +import net.minecraft.core.BlockPos; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; @@ -84,24 +85,24 @@ private static Path createPath(PathNode end, BlockPos startPos, BlockPos goal, P } static boolean isInBounds(int worldHeight, BlockPos pos) { - return pos.y >= 0 && pos.y < worldHeight; + return pos.getY() >= 0 && pos.getY() < worldHeight; } private static boolean closeToGoal(NodePos node, BlockPos goal) { - return node.absolutePosCenter().distanceToSq(goal) <= 16 * 16; + return node.absolutePosCenter().distSqr(goal) <= 16 * 16; } private static boolean inGoal(NodePos node, BlockPos goal) { if (!closeToGoal(node, goal)) return false; final BlockPos c1 = node.absolutePosZero(); - final BlockPos c2 = c1.plus(node.size.width()); - return goal.x >= c1.x && goal.x <= c2.x && - goal.y >= c1.y && goal.y <= c2.y && - goal.z >= c1.z && goal.z <= c2.z; + final BlockPos c2 = c1.offset(node.size.width(), node.size.width(), node.size.width()); + return goal.getX() >= c1.getX() && goal.getX() <= c2.getX() && + goal.getY() >= c1.getY() && goal.getY() <= c2.getY() && + goal.getZ() >= c1.getZ() && goal.getZ() <= c2.getZ(); } private static Path bestPathSoFar(PathNode end, BlockPos startPos, BlockPos goal) { - final double distSq = startPos.distanceToSq(end.pos.absolutePosCenter()); + final double distSq = startPos.distSqr(end.pos.absolutePosCenter()); if (distSq > MIN_DIST_PATH * MIN_DIST_PATH) { return createPath(end, startPos, goal, Path.Type.SEGMENT); } @@ -127,20 +128,20 @@ private static BlockPos[] neighborCubes(Face face, Size sz, BlockPos origin) { corner = origin; return new BlockPos[]{corner, corner.east(size), corner.south(size), corner.east(size).south(size)}; case DOWN: - corner = origin.up(size); + corner = origin.above(size); return new BlockPos[]{corner, corner.east(size), corner.south(size), corner.east(size).south(size)}; case NORTH: corner = origin.south(size); - return new BlockPos[]{corner, corner.east(size), corner.up(size), corner.east(size).up(size)}; + return new BlockPos[]{corner, corner.east(size), corner.above(size), corner.east(size).above(size)}; case SOUTH: corner = origin; - return new BlockPos[]{corner, corner.east(size), corner.up(size), corner.east(size).up(size)}; + return new BlockPos[]{corner, corner.east(size), corner.above(size), corner.east(size).above(size)}; case EAST: corner = origin; - return new BlockPos[]{corner, corner.south(size), corner.up(size), corner.south(size).up(size)}; + return new BlockPos[]{corner, corner.south(size), corner.above(size), corner.south(size).above(size)}; default: // WEST corner = origin.east(size); - return new BlockPos[]{corner, corner.south(size), corner.up(size), corner.south(size).up(size)}; + return new BlockPos[]{corner, corner.south(size), corner.above(size), corner.south(size).above(size)}; } } @@ -150,7 +151,7 @@ private static void forEachNeighborInCube(Chunk chunk, int state, NodePos neighb return; } final BlockPos pos = neighborNode.absolutePosZero(); - if (chunk.isEmpty(size, pos.x & 15, pos.y, pos.z & 15)) { + if (chunk.isEmpty(size, pos.getX() & 15, pos.getY(), pos.getZ() & 15)) { callback.accept(neighborNode, chunk, state); return; } @@ -168,10 +169,10 @@ private static void forEachNeighborInCube(Chunk chunk, int state, NodePos neighb private static void growThenIterate(Chunk chunk, int state, NodePos pos, Face face, Size minSize, Callback callback) { final Size originalSize = pos.size; final BlockPos bpos = pos.absolutePosZero(); - final int lx = bpos.x & 15; - final int lz = bpos.z & 15; + final int lx = bpos.getX() & 15; + final int lz = bpos.getZ() & 15; for (Size s = originalSize; ; s = s.larger()) { - if (s == Size.X16 || !chunk.isEmpty(s.larger(), lx, bpos.y, lz)) { + if (s == Size.X16 || !chunk.isEmpty(s.larger(), lx, bpos.getY(), lz)) { forEachNeighborInCube(chunk, state, new NodePos(s, bpos), face, s, originalSize != s, minSize, callback); return; } @@ -200,7 +201,7 @@ private static final class Search implements Callback { @Override public void accept(NodePos neighborPos, Chunk chunk, int state) { final PathNode neighborNode = getNodeAtPosition(this.map, neighborPos, this.goalCenter); - final double cost = state == NetherPathfinder.STATE_FROM_JAVA ? 1 : this.fakeChunkCost; + final double cost = state == NetherPathfinder.STATE_FROM_CALLER ? 1 : this.fakeChunkCost; final double tentativeCost = this.currentNode.cost + cost; if (neighborNode.cost - tentativeCost > MIN_IMPROVEMENT) { neighborNode.previous = this.currentNode; @@ -217,7 +218,7 @@ public void accept(NodePos neighborPos, Chunk chunk, int state) { if (this.bestHeuristicSoFar - heuristic > MIN_IMPROVEMENT) { this.bestHeuristicSoFar = heuristic; this.bestSoFar = neighborNode; - if (this.failing && this.startCenter.distanceToSq(neighborPos.absolutePosCenter()) > MIN_DIST_PATH * MIN_DIST_PATH) { + if (this.failing && this.startCenter.distSqr(neighborPos.absolutePosCenter()) > MIN_DIST_PATH * MIN_DIST_PATH) { this.failing = false; } } @@ -231,7 +232,7 @@ public void accept(NodePos neighborPos, Chunk chunk, int state) { * cancelled. */ static Path findPathSegment(NetherPathfinder ctx, NodePos start, NodePos goal, boolean x4Min, int timeoutMs, boolean airIfFake, double fakeChunkCost) { - final int fakeChunkMode = airIfFake ? NetherPathfinder.CACHE_MISS_AIR : NetherPathfinder.CACHE_MISS_GENERATE; + final NetherPathfinder.CacheMiss fakeChunkMode = airIfFake ? NetherPathfinder.CacheMiss.AIR : NetherPathfinder.CacheMiss.GENERATE; final Size minSize = x4Min ? Size.X4 : Size.X2; final BlockPos goalCenter = goal.absolutePosCenter(); final BlockPos startCenter = start.absolutePosCenter(); @@ -241,26 +242,26 @@ static Path findPathSegment(NetherPathfinder ctx, NodePos start, NodePos goal, b final PathNode startNode = getNodeAtPosition(s.map, start, goal.absolutePosZero()); final BlockPos startZero = start.absolutePosZero(); - ctx.tryLoadRegion(startZero.chunkX(), startZero.chunkZ()); + ctx.tryLoadRegion((startZero.getX() >> 4), (startZero.getZ() >> 4)); startNode.cost = 0; startNode.combinedCost = startNode.estimatedCostToGoal; s.openSet.insert(startNode); - ctx.getRealChunkFromCacheOrFakeChunkMaybeGen(startZero.chunkX(), startZero.chunkZ(), fakeChunkMode); + ctx.getRealChunkFromCacheOrFakeChunkMaybeGen((startZero.getX() >> 4), (startZero.getZ() >> 4), fakeChunkMode); s.bestSoFar = startNode; s.bestHeuristicSoFar = startNode.estimatedCostToGoal; - final long startTime = System.nanoTime(); - final long primaryTimeoutTime = startTime + 500L * 1_000_000L; - final long timeout = (timeoutMs != 0 ? timeoutMs : 30_000L) * 1_000_000L; + final long startTime = System.currentTimeMillis(); + final long primaryTimeoutTime = startTime + 500L; + final long timeout = timeoutMs != 0 ? timeoutMs : 30_000L; final long failureTimeout = startTime + timeout; - long timeDoingIO = 0; + long timeDoingIO = 0; // milliseconds spent reading region files, which the timeouts do not count int numNodes = 0; int fakeChunkVisits = 0; // if this gets too high we return while (!s.openSet.isEmpty()) { if ((numNodes & 63) == 0) { // only look at the clock once every 64 nodes - final long now = System.nanoTime() - timeDoingIO; + final long now = System.currentTimeMillis() - timeDoingIO; if (now >= failureTimeout || (!s.failing && now >= primaryTimeoutTime)) { break; } else if (ctx.isCancelled()) { @@ -277,10 +278,10 @@ static Path findPathSegment(NetherPathfinder ctx, NodePos start, NodePos goal, b final NodePos pos = currentNode.pos; final Size size = pos.size; final BlockPos bpos = pos.absolutePosZero(); - final int cx = bpos.chunkX(); - final int cz = bpos.chunkZ(); + final int cx = (bpos.getX() >> 4); + final int cz = (bpos.getZ() >> 4); final NetherPathfinder.Entry currentChunk = ctx.getChunkOrAir(cx, cz); - if (currentChunk.state != NetherPathfinder.STATE_FROM_JAVA) { + if (currentChunk.state != NetherPathfinder.STATE_FROM_CALLER) { fakeChunkVisits++; } else { fakeChunkVisits = 0; @@ -293,13 +294,13 @@ static Path findPathSegment(NetherPathfinder ctx, NodePos start, NodePos goal, b } for (Face face : ALL_FACES) { - final NodePos neighborNodePos = new NodePos(size, bpos.offset(face, size.width())); + final NodePos neighborNodePos = new NodePos(size, face.offset(bpos, size.width())); final BlockPos origin = neighborNodePos.absolutePosZero(); if (face == Face.UP || face == Face.DOWN) { if (!isInBounds(ctx.maxHeight, origin)) continue; } - final int neighborCx = origin.chunkX(); - final int neighborCz = origin.chunkZ(); + final int neighborCx = (origin.getX() >> 4); + final int neighborCz = (origin.getZ() >> 4); timeDoingIO += ctx.tryLoadRegion(neighborCx, neighborCz); final NetherPathfinder.Entry entry = neighborCx == cx && neighborCz == cz ? currentChunk : ctx.getChunkOrAir(neighborCx, neighborCz); growThenIterate(entry.chunk, entry.state, neighborNodePos, face, minSize, s); @@ -352,7 +353,7 @@ static Path findPathFull(NetherPathfinder ctx, NodePos start, NodePos goal, doub break; } final BlockPos end = path.getEndPos(); - ctx.cullFarChunks(end.chunkX(), end.chunkZ(), 200); + ctx.cullFarChunks((end.getX() >> 4), (end.getZ() >> 4), 200); segments.add(path); if (path.type == Path.Type.FINISHED) break; } @@ -383,17 +384,17 @@ static NodePos findAir(NetherPathfinder ctx, Size size, BlockPos start1x, boolea final BlockPos blockPos = node.absolutePosZero(); if (isInBounds(ctx.maxHeight, blockPos)) { final Chunk chunk = airIfFake - ? ctx.getChunkOrAir(blockPos.chunkX(), blockPos.chunkZ()).chunk - : ctx.getOrGenChunk(blockPos.chunkX(), blockPos.chunkZ()); - if (chunk.isEmpty(size, blockPos.x & 15, blockPos.y, blockPos.z & 15)) { + ? ctx.getChunkOrAir((blockPos.getX() >> 4), (blockPos.getZ() >> 4)).chunk + : ctx.getOrGenChunk((blockPos.getX() >> 4), (blockPos.getZ() >> 4)); + if (chunk.isEmpty(size, blockPos.getX() & 15, blockPos.getY(), blockPos.getZ() & 15)) { return node; } push(queue, visited, new NodePos(size, blockPos.west(w))); push(queue, visited, new NodePos(size, blockPos.east(w))); push(queue, visited, new NodePos(size, blockPos.north(w))); push(queue, visited, new NodePos(size, blockPos.south(w))); - push(queue, visited, new NodePos(size, blockPos.up(w))); - push(queue, visited, new NodePos(size, blockPos.down(w))); + push(queue, visited, new NodePos(size, blockPos.above(w))); + push(queue, visited, new NodePos(size, blockPos.below(w))); } } // shouldn't be possible to exit the while loop diff --git a/src/main/java/baritone/process/elytra/pathfinder/PathNode.java b/src/main/java/baritone/process/elytra/pathfinder/PathNode.java index d50047fc43..e5789a040a 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/PathNode.java +++ b/src/main/java/baritone/process/elytra/pathfinder/PathNode.java @@ -17,6 +17,8 @@ package baritone.process.elytra.pathfinder; +import net.minecraft.core.BlockPos; + final class PathNode { static final double COST_INF = 1000000.0; @@ -38,6 +40,6 @@ boolean isOpen() { } private static double heuristic(NodePos pos, BlockPos goal) { - return pos.absolutePosCenter().distanceTo(goal) - (pos.size.width() * 4); + return Math.sqrt(pos.absolutePosCenter().distSqr(goal)) - (pos.size.width() * 4); } } diff --git a/src/main/java/baritone/process/elytra/pathfinder/PathSegment.java b/src/main/java/baritone/process/elytra/pathfinder/PathSegment.java index bf2e28ce1f..5cc0cb8357 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/PathSegment.java +++ b/src/main/java/baritone/process/elytra/pathfinder/PathSegment.java @@ -17,13 +17,16 @@ package baritone.process.elytra.pathfinder; +import java.util.List; +import net.minecraft.core.BlockPos; + public class PathSegment { public final boolean finished; - public final long[] packed; - + /** The blocks of the path, in order. */ + public final List blocks; - public PathSegment(boolean finished, long[] packed) { + public PathSegment(boolean finished, List blocks) { this.finished = finished; - this.packed = packed; + this.blocks = blocks; } } \ No newline at end of file diff --git a/src/main/java/baritone/process/elytra/pathfinder/Raytracer.java b/src/main/java/baritone/process/elytra/pathfinder/Raytracer.java index e70e4a5110..86c4e9644d 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/Raytracer.java +++ b/src/main/java/baritone/process/elytra/pathfinder/Raytracer.java @@ -17,8 +17,11 @@ package baritone.process.elytra.pathfinder; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; import java.util.ArrayList; import java.util.List; +import net.minecraft.world.phys.Vec3; /** * A ray through the chunk octrees: "An efficient parametric algorithm for octree traversal" @@ -26,7 +29,7 @@ * directions, so the ray is reflected around each x16 node's centre and the child indices are * flipped back with {@code a}. */ -final class Raytracer { +public final class Raytracer { static final int MISS = 0; static final int FINISHED = 1; @@ -114,25 +117,25 @@ private static double m(double origin, int nodeMin, int nodeMax, double t0, doub /** Bytes of a child of a node at each level: x16 (4) has x8 children of 64 bytes, and so on. x2's children are bits. */ private static final int[] CHILD_BYTES = {0, 0, 1, 8, 64}; - private static boolean emptyNode(long[] slab, int filled, int off, int level) { + private static boolean emptyNode(long[] section, int filled, int off, int level) { switch (level) { case 4: return filled == 0; case 3: return (filled & (1 << (off >>> 6))) == 0; // the x8 at byte offset off is x8 number off / 64 - case 2: return slab[off >>> 3] == 0; - default: return Chunk.byteAt(slab, off) == 0; + case 2: return section[off >>> 3] == 0; + default: return Chunk.byteAt(section, off) == 0; } } /** - * Walks the ray through one node. level 4 is an x16 (the whole slab), 0 a block; a node is - * the slab plus a byte offset, and at level 0 {@code bit} says which bit of the byte. {@code - * filled} is the slab's summary of which of its x8 cubes hold a block, see {@link Chunk#filled}. + * Walks the ray through one node. level 4 is an x16 (the whole section), 0 a block; a node is + * the section plus a byte offset, and at level 0 {@code bit} says which bit of the byte. {@code + * filled} is the section's summary of which of its x8 cubes hold a block, see {@link Chunk#filled}. * Returns NaN for a miss, positive infinity when the ray ended inside without hitting, and * otherwise the length along the ray at which it hit. */ private static double procSubtree(int a, double ox, double oy, double oz, double targetLen, double tx0, double ty0, double tz0, double tx1, double ty1, double tz1, - int level, int nx, int ny, int nz, long[] slab, int filled, int off, int bit) { + int level, int nx, int ny, int nz, long[] section, int filled, int off, int bit) { // if this node is behind us if (tx1 < 0.0 || ty1 < 0.0 || tz1 < 0.0) { return Double.NaN; @@ -141,7 +144,7 @@ private static double procSubtree(int a, double ox, double oy, double oz, double return Double.POSITIVE_INFINITY; } if (level == 0) { // leaf - if (((Chunk.byteAt(slab, off) >> bit) & 1) == 0) { + if (((Chunk.byteAt(section, off) >> bit) & 1) == 0) { return Double.NaN; } if (tx0 > ty0 && tx0 > tz0) { @@ -152,7 +155,7 @@ private static double procSubtree(int a, double ox, double oy, double oz, double return tz0; // plane XY } } - if (slab == null || emptyNode(slab, filled, off, level)) { + if (section == null || emptyNode(section, filled, off, level)) { // we know that all the leafs are empty return Double.NaN; } @@ -166,54 +169,54 @@ private static double procSubtree(int a, double ox, double oy, double oz, double final int childBytes = CHILD_BYTES[level]; int currNode = firstNode(tx0, ty0, tz0, txm, tym, tzm); do { - final int i; + // Child currNode of this node in the reflected frame; xor with a gives the real octant, + // i. Its origin is the node's, moved by half the width along each axis whose bit is set + // in i (4 is x, 2 is y, 1 is z), and its bytes start i children in from the node's -- + // except below an x2, whose children are the bits of one byte, which i indexes instead. + final int i = currNode ^ a; + final int cx = nx + ((i & 4) != 0 ? half : 0); + final int cy = ny + ((i & 2) != 0 ? half : 0); + final int cz = nz + ((i & 1) != 0 ? half : 0); + final int childOff = level == 1 ? off : off + i * childBytes; final double r; switch (currNode) { case 0: - i = a; - r = procSubtree(a, ox, oy, oz, targetLen, tx0, ty0, tz0, txm, tym, tzm, childLevel, nx + ((i & 4) != 0 ? half : 0), ny + ((i & 2) != 0 ? half : 0), nz + ((i & 1) != 0 ? half : 0), slab, filled, level == 1 ? off : off + i * childBytes, i); + r = procSubtree(a, ox, oy, oz, targetLen, tx0, ty0, tz0, txm, tym, tzm, childLevel, cx, cy, cz, section, filled, childOff, i); if (!Double.isNaN(r)) return r; currNode = newNode(txm, 4, tym, 2, tzm, 1); break; case 1: - i = 1 ^ a; - r = procSubtree(a, ox, oy, oz, targetLen, tx0, ty0, tzm, txm, tym, tz1, childLevel, nx + ((i & 4) != 0 ? half : 0), ny + ((i & 2) != 0 ? half : 0), nz + ((i & 1) != 0 ? half : 0), slab, filled, level == 1 ? off : off + i * childBytes, i); + r = procSubtree(a, ox, oy, oz, targetLen, tx0, ty0, tzm, txm, tym, tz1, childLevel, cx, cy, cz, section, filled, childOff, i); if (!Double.isNaN(r)) return r; currNode = newNode(txm, 5, tym, 3, tz1, 8); break; case 2: - i = 2 ^ a; - r = procSubtree(a, ox, oy, oz, targetLen, tx0, tym, tz0, txm, ty1, tzm, childLevel, nx + ((i & 4) != 0 ? half : 0), ny + ((i & 2) != 0 ? half : 0), nz + ((i & 1) != 0 ? half : 0), slab, filled, level == 1 ? off : off + i * childBytes, i); + r = procSubtree(a, ox, oy, oz, targetLen, tx0, tym, tz0, txm, ty1, tzm, childLevel, cx, cy, cz, section, filled, childOff, i); if (!Double.isNaN(r)) return r; currNode = newNode(txm, 6, ty1, 8, tzm, 3); break; case 3: - i = 3 ^ a; - r = procSubtree(a, ox, oy, oz, targetLen, tx0, tym, tzm, txm, ty1, tz1, childLevel, nx + ((i & 4) != 0 ? half : 0), ny + ((i & 2) != 0 ? half : 0), nz + ((i & 1) != 0 ? half : 0), slab, filled, level == 1 ? off : off + i * childBytes, i); + r = procSubtree(a, ox, oy, oz, targetLen, tx0, tym, tzm, txm, ty1, tz1, childLevel, cx, cy, cz, section, filled, childOff, i); if (!Double.isNaN(r)) return r; currNode = newNode(txm, 7, ty1, 8, tz1, 8); break; case 4: - i = 4 ^ a; - r = procSubtree(a, ox, oy, oz, targetLen, txm, ty0, tz0, tx1, tym, tzm, childLevel, nx + ((i & 4) != 0 ? half : 0), ny + ((i & 2) != 0 ? half : 0), nz + ((i & 1) != 0 ? half : 0), slab, filled, level == 1 ? off : off + i * childBytes, i); + r = procSubtree(a, ox, oy, oz, targetLen, txm, ty0, tz0, tx1, tym, tzm, childLevel, cx, cy, cz, section, filled, childOff, i); if (!Double.isNaN(r)) return r; currNode = newNode(tx1, 8, tym, 6, tzm, 5); break; case 5: - i = 5 ^ a; - r = procSubtree(a, ox, oy, oz, targetLen, txm, ty0, tzm, tx1, tym, tz1, childLevel, nx + ((i & 4) != 0 ? half : 0), ny + ((i & 2) != 0 ? half : 0), nz + ((i & 1) != 0 ? half : 0), slab, filled, level == 1 ? off : off + i * childBytes, i); + r = procSubtree(a, ox, oy, oz, targetLen, txm, ty0, tzm, tx1, tym, tz1, childLevel, cx, cy, cz, section, filled, childOff, i); if (!Double.isNaN(r)) return r; currNode = newNode(tx1, 8, tym, 7, tz1, 8); break; case 6: - i = 6 ^ a; - r = procSubtree(a, ox, oy, oz, targetLen, txm, tym, tz0, tx1, ty1, tzm, childLevel, nx + ((i & 4) != 0 ? half : 0), ny + ((i & 2) != 0 ? half : 0), nz + ((i & 1) != 0 ? half : 0), slab, filled, level == 1 ? off : off + i * childBytes, i); + r = procSubtree(a, ox, oy, oz, targetLen, txm, tym, tz0, tx1, ty1, tzm, childLevel, cx, cy, cz, section, filled, childOff, i); if (!Double.isNaN(r)) return r; currNode = newNode(tx1, 8, ty1, 8, tzm, 7); break; default: - i = 7 ^ a; - r = procSubtree(a, ox, oy, oz, targetLen, txm, tym, tzm, tx1, ty1, tz1, childLevel, nx + ((i & 4) != 0 ? half : 0), ny + ((i & 2) != 0 ? half : 0), nz + ((i & 1) != 0 ? half : 0), slab, filled, level == 1 ? off : off + i * childBytes, i); + r = procSubtree(a, ox, oy, oz, targetLen, txm, tym, tzm, tx1, ty1, tz1, childLevel, cx, cy, cz, section, filled, childOff, i); if (!Double.isNaN(r)) return r; return Double.NaN; } @@ -223,7 +226,7 @@ private static double procSubtree(int a, double ox, double oy, double oz, double /** One x16 node. The ray is already reflected. */ private static int raytrace16(int a, double rox, double roy, double roz, double rdx, double rdy, double rdz, double targetLen, - int nx, int ny, int nz, long[] slab, int filled, Step step) { + int nx, int ny, int nz, long[] section, int filled, Step step) { // IEEE stability fix final double divx = 1 / rdx; final double divy = 1 / rdy; @@ -240,7 +243,7 @@ private static int raytrace16(int a, double rox, double roy, double roz, double final double tmin = max(max(tx0, ty0), tz0); final double tmax = min(min(tx1, ty1), tz1); if (tmin <= tmax) { - final double r = procSubtree(a, rox, roy, roz, targetLen, tx0, ty0, tz0, tx1, ty1, tz1, 4, nx, ny, nz, slab, filled, 0, 0); + final double r = procSubtree(a, rox, roy, roz, targetLen, tx0, ty0, tz0, tx1, ty1, tz1, 4, nx, ny, nz, section, filled, 0, 0); if (r == Double.POSITIVE_INFINITY) { return FINISHED; } @@ -262,13 +265,13 @@ private static int raytrace16(int a, double rox, double roy, double roz, double } /** - * Traces the segment from (fx, fy, fz) to (tx, ty, tz). Returns true if it hit a solid block; - * the hit position is then written to {@code hitOut} at {@code hitIndex}, if the array is not null. + * Traces the segment from (fx, fy, fz) to (tx, ty, tz). Returns where it hit a solid block, or + * null if it reached its end. * The two points must be inside 0 <= y < 384, as must be the segment between them. A segment * of no length is a point, which is a hit if it is inside a block; a coordinate that is not a * finite number is refused. */ - static boolean raytrace(NetherPathfinder ctx, double fx, double fy, double fz, double tx, double ty, double tz, int fakeChunkMode, double[] hitOut, int hitIndex) { + public static Vec3 raytrace(NetherPathfinder ctx, double fx, double fy, double fz, double tx, double ty, double tz, NetherPathfinder.CacheMiss fakeChunkMode) { final double vx = tx - fx; final double vy = ty - fy; final double vz = tz - fz; @@ -283,19 +286,14 @@ static boolean raytrace(NetherPathfinder ctx, double fx, double fy, double fz, d // A point. The same division would make the direction NaN here too, and the native // library exited the process. A ray that starts inside a block reports a hit at its // origin, so a point does the same, and one in the air hits nothing. - final int bx = BlockPos.floor(fx); - final int by = BlockPos.floor(fy); - final int bz = BlockPos.floor(fz); + final int bx = Mth.floor(fx); + final int by = Mth.floor(fy); + final int bz = Mth.floor(fz); final Chunk chunk = ctx.getRealChunkFromCacheOrFakeChunkMaybeGen(bx >> 4, bz >> 4, fakeChunkMode); if (by < 0 || by >= Chunk.HEIGHT || !chunk.isSolid(bx & 15, by, bz & 15)) { - return false; + return null; } - if (hitOut != null) { - hitOut[hitIndex] = fx; - hitOut[hitIndex + 1] = fy; - hitOut[hitIndex + 2] = fz; - } - return true; + return new Vec3(fx, fy, fz); } final double dx = vx / targetLen; final double dy = vy / targetLen; @@ -305,36 +303,28 @@ static boolean raytrace(NetherPathfinder ctx, double fx, double fy, double fz, d if (dy < 0.0) a |= 2; if (dz < 0.0) a |= 1; - int nx = BlockPos.floor(fx) & ~15; - int ny = BlockPos.floor(fy) & ~15; - int nz = BlockPos.floor(fz) & ~15; + int nx = Mth.floor(fx) & ~15; + int ny = Mth.floor(fy) & ~15; + int nz = Mth.floor(fz) & ~15; final Step step = new Step(); while (true) { final Chunk chunk = ctx.getRealChunkFromCacheOrFakeChunkMaybeGen(nx >> 4, nz >> 4, fakeChunkMode); - final long[] slab = chunk.slab(ny); + final long[] section = chunk.section(ny); // reflect around the node's centre so the traversal only has to walk positive directions double rox = fx, roy = fy, roz = fz, rdx = dx, rdy = dy, rdz = dz; if ((a & 4) != 0) { rox = reflect(fx, nx + 8); rdx = -dx; } if ((a & 2) != 0) { roy = reflect(fy, ny + 8); rdy = -dy; } if ((a & 1) != 0) { roz = reflect(fz, nz + 8); rdz = -dz; } - final int result = raytrace16(a, rox, roy, roz, rdx, rdy, rdz, targetLen, nx, ny, nz, slab, chunk.filled(ny), step); + final int result = raytrace16(a, rox, roy, roz, rdx, rdy, rdz, targetLen, nx, ny, nz, section, chunk.filled(ny), step); if (result == FINISHED) { - return false; + return null; } if (result == HIT) { - if (hitOut != null) { - if (step.hitLen < 0.0) { - // the origin was inside an occupied block, so the hit is the origin - hitOut[hitIndex] = fx; - hitOut[hitIndex + 1] = fy; - hitOut[hitIndex + 2] = fz; - } else { - hitOut[hitIndex] = fx + dx * step.hitLen; - hitOut[hitIndex + 1] = fy + dy * step.hitLen; - hitOut[hitIndex + 2] = fz + dz * step.hitLen; - } + if (step.hitLen < 0.0) { + // the origin was inside an occupied block, so the hit is the origin + return new Vec3(fx, fy, fz); } - return true; + return new Vec3(fx + dx * step.hitLen, fy + dy * step.hitLen, fz + dz * step.hitLen); } switch (step.exitPlane) { case PLANE_XY: @@ -359,7 +349,7 @@ private static int lastVisibleNode(NetherPathfinder ctx, List path, in for (int i = lastVisible + 1; i < path.size(); i++) { final BlockPos currentBlock = path.get(i); if (fromBlock.equals(currentBlock)) continue; // the pathfinder can produce 2 consecutive equal points and that breaks the raytracer - if (raytrace(ctx, fromBlock.x, fromBlock.y, fromBlock.z, currentBlock.x, currentBlock.y, currentBlock.z, NetherPathfinder.CACHE_MISS_GENERATE, null, 0)) { + if (raytrace(ctx, fromBlock.getX(), fromBlock.getY(), fromBlock.getZ(), currentBlock.getX(), currentBlock.getY(), currentBlock.getZ(), NetherPathfinder.CacheMiss.GENERATE) != null) { return lastVisible; } lastVisible = i; diff --git a/src/test/java/baritone/process/elytra/pathfinder/BaritoneRegionTest.java b/src/test/java/baritone/process/elytra/pathfinder/BaritoneRegionTest.java index 60ad801f73..1e4a0f801d 100644 --- a/src/test/java/baritone/process/elytra/pathfinder/BaritoneRegionTest.java +++ b/src/test/java/baritone/process/elytra/pathfinder/BaritoneRegionTest.java @@ -79,7 +79,7 @@ public void parsesAChunk() throws Exception { final Chunk[] found = new Chunk[1]; final int[] at = new int[2]; try (java.io.InputStream in = new java.util.zip.GZIPInputStream(new java.io.ByteArrayInputStream(region()))) { - BaritoneRegion.parse(in, -1, 2, NetherPathfinder.DIMENSION_NETHER, (x, z, chunk) -> { + BaritoneRegion.parse(in, -1, 2, NetherPathfinder.Dimension.NETHER, (x, z, chunk) -> { found[0] = chunk; at[0] = x; at[1] = z; @@ -105,22 +105,22 @@ public void aSearchLoadsTheRegionItReaches() throws Exception { Files.createDirectories(regions); Files.write(regions.resolve("r.0.0.bcr"), region()); try { - final NetherPathfinder ctx = new NetherPathfinder(1, regions.toString(), NetherPathfinder.DIMENSION_NETHER, 128); + final NetherPathfinder ctx = new NetherPathfinder(1, regions.toString(), NetherPathfinder.Dimension.NETHER, 128); assertNull(ctx.getChunk(3, 5)); assertTrue(ctx.tryLoadRegion(20, 20) > 0); assertNotNull(ctx.getChunk(3, 5)); - assertTrue(ctx.hasChunkFromJava(3, 5)); + assertTrue(ctx.hasChunkFromCaller(3, 5)); assertTrue(ctx.getChunk(3, 5).isSolid(1, 2, 3)); // a region is only read once, and one that does not exist costs nothing assertEquals(0, ctx.tryLoadRegion(21, 21)); assertEquals(0, ctx.tryLoadRegion(100, 100)); // a chunk the game gave before the file is read wins - final NetherPathfinder again = new NetherPathfinder(1, regions.toString(), NetherPathfinder.DIMENSION_NETHER, 128); + final NetherPathfinder again = new NetherPathfinder(1, regions.toString(), NetherPathfinder.Dimension.NETHER, 128); final Chunk mine = again.allocateAndInsertChunk(3, 5); again.tryLoadRegion(0, 0); assertEquals(mine, again.getChunk(3, 5)); // and a search reaching the region loads it by itself - final NetherPathfinder searched = new NetherPathfinder(1, regions.toString(), NetherPathfinder.DIMENSION_NETHER, 128); + final NetherPathfinder searched = new NetherPathfinder(1, regions.toString(), NetherPathfinder.Dimension.NETHER, 128); searched.pathFind(0, 60, 0, 200, 60, 200, true, false, 1000, true, 8.0); assertNotNull(searched.getChunk(3, 5)); } finally { @@ -133,9 +133,9 @@ public void aSearchLoadsTheRegionItReaches() throws Exception { @Test public void directoryNamesDecideTheDimension() { - assertEquals(NetherPathfinder.DIMENSION_NETHER, BaritoneRegion.dimensionOf("/a/b/the_nether_128/cache")); - assertEquals(NetherPathfinder.DIMENSION_OVERWORLD, BaritoneRegion.dimensionOf("/a/b/overworld_384/cache")); - assertEquals(NetherPathfinder.DIMENSION_END, BaritoneRegion.dimensionOf("/a/b/the_end_256/cache")); - assertEquals(-1, BaritoneRegion.dimensionOf("/a/b/somewhere/cache")); + assertEquals(NetherPathfinder.Dimension.NETHER, BaritoneRegion.dimensionOf("/a/b/the_nether_128/cache")); + assertEquals(NetherPathfinder.Dimension.OVERWORLD, BaritoneRegion.dimensionOf("/a/b/overworld_384/cache")); + assertEquals(NetherPathfinder.Dimension.END, BaritoneRegion.dimensionOf("/a/b/the_end_256/cache")); + assertNull(BaritoneRegion.dimensionOf("/a/b/somewhere/cache")); } } diff --git a/src/test/java/baritone/process/elytra/pathfinder/Bench.java b/src/test/java/baritone/process/elytra/pathfinder/Bench.java index 67a6638850..b4eccb941b 100644 --- a/src/test/java/baritone/process/elytra/pathfinder/Bench.java +++ b/src/test/java/baritone/process/elytra/pathfinder/Bench.java @@ -17,6 +17,7 @@ package baritone.process.elytra.pathfinder; +import net.minecraft.core.BlockPos; import java.util.Arrays; /** @@ -61,7 +62,9 @@ public static void main(String[] args) { final boolean[] hits = new boolean[rays]; for (int rep = 0; rep < 5; rep++) { t0 = System.nanoTime(); - ctx.raytrace(NetherPathfinder.CACHE_MISS_SOLID, rays, from, to, hits, null); + for (int i = 0; i < rays; i++) { + hits[i] = Raytracer.raytrace(ctx, from[i * 3], from[i * 3 + 1], from[i * 3 + 2], to[i * 3], to[i * 3 + 1], to[i * 3 + 2], NetherPathfinder.CacheMiss.SOLID) != null; + } final double ms = (System.nanoTime() - t0) / 1e6; int hit = 0; for (boolean h : hits) if (h) hit++; @@ -87,7 +90,7 @@ public static void main(String[] args) { final PathSegment p = ctx.pathFind(sx, 60, sz, sx + dist, 60, sz, true, false, 10000, airIfFake, 8.0); us[r] = (System.nanoTime() - t0) / 1000; total += us[r]; - if (p != null) { blocks += p.packed.length; if (p.finished) finished++; } + if (p != null) { blocks += p.blocks.size(); if (p.finished) finished++; } } if (pass == 0) continue; Arrays.sort(us); @@ -97,7 +100,7 @@ public static void main(String[] args) { } } - final NetherPathfinder fresh = new NetherPathfinder(Oracle.SEED, null, NetherPathfinder.DIMENSION_NETHER, 128); + final NetherPathfinder fresh = new NetherPathfinder(Oracle.SEED, null, NetherPathfinder.Dimension.NETHER, 128); t0 = System.nanoTime(); final NodePos start = PathFinder.findAir(fresh, Size.X4, new BlockPos(0, 50, 0), false); final NodePos goal = PathFinder.findAir(fresh, Size.X4, new BlockPos(100000, 50, 0), false); diff --git a/src/test/java/baritone/process/elytra/pathfinder/CancelTest.java b/src/test/java/baritone/process/elytra/pathfinder/CancelTest.java new file mode 100644 index 0000000000..317b3c3171 --- /dev/null +++ b/src/test/java/baritone/process/elytra/pathfinder/CancelTest.java @@ -0,0 +1,90 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra.pathfinder; + +import org.junit.Test; + + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * When a cancel takes effect. A search is cancelled by a flag the searching thread reads, so the + * question each of these asks is which search a given cancel belongs to. + */ +public class CancelTest { + + private static final NetherPathfinder.Dimension NETHER = NetherPathfinder.Dimension.NETHER; + + private static NetherPathfinder context() { + return new NetherPathfinder(Oracle.SEED, null, NETHER, 128); + } + + /** + * The search PathFindTest uses to fill half a second: real terrain, far enough away that it + * cannot arrive. Left alone it returns an unfinished segment once the primary timeout is up, + * so "returned null quickly" means cancelled and nothing else. + */ + private static PathSegment longSearch(NetherPathfinder ctx) { + return ctx.pathFind(0, 50, 0, 100_000, 50, 0, true, false, 0, false, 1.0); + } + + /** A few hundred blocks through known-empty chunks, which finishes in well under a second. */ + private static PathSegment shortSearch(NetherPathfinder ctx) { + return ctx.pathFind(0, 60, 0, 500, 60, 0, true, false, 10_000, true, 8.0); + } + + @Test(timeout = 60_000) + public void aCancelThatArrivesBeforeTheSearchStartsStillStopsIt() { + // This is the cancel destroy() sends. It can land while a search is still queued behind the + // pathfinder's lock: clearing the flag as a search started used to drop it, and that search + // then ran on with the context already torn down, holding the lock the game thread wants. + final NetherPathfinder ctx = context(); + assertFalse("nothing has asked it to stop yet", ctx.cancel()); + + final long t0 = System.nanoTime(); + final PathSegment segment = longSearch(ctx); + final long ms = (System.nanoTime() - t0) / 1_000_000L; + + assertNull("a cancelled search has no path", segment); + assertTrue("gave up at once rather than running the search out, took " + ms + " ms", ms < 400); + } + + @Test(timeout = 60_000) + public void aCancelAppliesToOneSearchAndNotTheNext() { + // The search that was cancelled clears the flag on its way out, so the next one starts + // clean. Without that, one cancel would kill every later search on the same context. + final NetherPathfinder ctx = context(); + ctx.cancel(); + assertNull("the first search after the cancel is the one it stops", longSearch(ctx)); + + final PathSegment segment = shortSearch(ctx); + assertNotNull("the search after a cancelled one runs normally", segment); + assertTrue(segment.finished); + } + + @Test(timeout = 60_000) + public void aSearchThatFinishesLeavesTheFlagClear() { + final NetherPathfinder ctx = context(); + assertNotNull(shortSearch(ctx)); + // getAndSet reports what it replaced, so a false here says the flag was down + assertFalse("a completed search did not leave the flag set", ctx.cancel()); + } +} diff --git a/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java b/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java index 6f4e011cdf..3259552bac 100644 --- a/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java +++ b/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java @@ -42,7 +42,7 @@ public void setAndGetEveryBlockOfASection() { chunk.setBlock(3, 50, 4, false); assertFalse(chunk.isSolid(3, 50, 4)); // untouched sections stay air and unallocated - assertNull(chunk.slab(0)); + assertNull(chunk.section(0)); assertFalse(chunk.isSolid(0, 0, 0)); } @@ -68,9 +68,13 @@ public void emptinessAtEveryLevel() { assertFalse(chunk.isEmptyX8(9, 70, 3)); assertFalse(chunk.isEmptyX16(64)); chunk.setBlock(10, 70, 3, false); - assertTrue(chunk.isEmptyX8(9, 70, 3)); - assertTrue(chunk.isEmptyX16(64)); - assertNotNull(chunk.slab(64)); // the slab stays allocated + // The blocks are gone, which the exact question sees. The x8 and x16 summaries are + // allowed to lag behind a clear, and only ever towards "may hold a block". + assertFalse(chunk.isSolid(9, 70, 3)); + assertFalse(chunk.isSolid(10, 70, 3)); + assertFalse(chunk.isEmptyX8(9, 70, 3)); + assertFalse(chunk.isEmptyX16(64)); + assertNotNull(chunk.section(64)); // the section stays allocated } @Test @@ -107,7 +111,7 @@ public void fillSection() { @Test public void insertChunkDataUsesTheBlockStateContainerIndex() { - final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.DIMENSION_NETHER, 128); + final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.Dimension.NETHER, 128); final boolean[] data = new boolean[16 * 16 * 256]; data[(77 << 8) | (5 << 4) | 12] = true; ctx.insertChunkData(-3, 9, data); @@ -115,7 +119,7 @@ public void insertChunkDataUsesTheBlockStateContainerIndex() { assertNotNull(chunk); assertTrue(chunk.isSolid(12, 77, 5)); assertFalse(chunk.isSolid(5, 77, 12)); - assertTrue(ctx.hasChunkFromJava(-3, 9)); + assertTrue(ctx.hasChunkFromCaller(-3, 9)); try { ctx.insertChunkData(0, 0, new boolean[10]); throw new AssertionError("expected IllegalArgumentException"); @@ -126,7 +130,7 @@ public void insertChunkDataUsesTheBlockStateContainerIndex() { @Test public void tableOperations() { - final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.DIMENSION_NETHER, 128); + final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.Dimension.NETHER, 128); assertSame(Chunk.AIR, ctx.getChunkOrDefault(1, 1, false)); assertSame(Chunk.SOLID, ctx.getChunkOrDefault(1, 1, true)); assertNull(ctx.getChunk(1, 1)); @@ -135,9 +139,9 @@ public void tableOperations() { final Chunk chunk = ctx.allocateAndInsertChunk(1, 1); assertSame(chunk, ctx.getChunk(1, 1)); assertSame(chunk, ctx.getChunkOrDefault(1, 1, true)); - assertTrue(ctx.hasChunkFromJava(1, 1)); + assertTrue(ctx.hasChunkFromCaller(1, 1)); assertTrue(ctx.setChunkState(1, 1, false)); - assertFalse(ctx.hasChunkFromJava(1, 1)); + assertFalse(ctx.hasChunkFromCaller(1, 1)); // a fake chunk is a default to the search's real-chunk lookup assertSame(Chunk.SOLID, ctx.getRealChunkOrDefault(1, 1, true)); assertSame(chunk, ctx.getChunkOrAir(1, 1).chunk); @@ -158,13 +162,13 @@ public void tableOperations() { @Test public void generatedChunksAreFakeUntilMarked() { - final NetherPathfinder ctx = new NetherPathfinder(Oracle.SEED, null, NetherPathfinder.DIMENSION_NETHER, 128); + final NetherPathfinder ctx = new NetherPathfinder(Oracle.SEED, null, NetherPathfinder.Dimension.NETHER, 128); final Chunk chunk = ctx.getOrGenChunk(0, 0); assertSame(chunk, ctx.getOrGenChunk(0, 0)); - assertFalse(ctx.hasChunkFromJava(0, 0)); - assertSame(Chunk.AIR, ctx.getRealChunkFromCacheOrFakeChunkMaybeGen(0, 0, NetherPathfinder.CACHE_MISS_AIR)); - assertSame(chunk, ctx.getRealChunkFromCacheOrFakeChunkMaybeGen(0, 0, NetherPathfinder.CACHE_MISS_GENERATE)); + assertFalse(ctx.hasChunkFromCaller(0, 0)); + assertSame(Chunk.AIR, ctx.getRealChunkFromCacheOrFakeChunkMaybeGen(0, 0, NetherPathfinder.CacheMiss.AIR)); + assertSame(chunk, ctx.getRealChunkFromCacheOrFakeChunkMaybeGen(0, 0, NetherPathfinder.CacheMiss.GENERATE)); ctx.setChunkState(0, 0, true); - assertSame(chunk, ctx.getRealChunkFromCacheOrFakeChunkMaybeGen(0, 0, NetherPathfinder.CACHE_MISS_AIR)); + assertSame(chunk, ctx.getRealChunkFromCacheOrFakeChunkMaybeGen(0, 0, NetherPathfinder.CacheMiss.AIR)); } } diff --git a/src/test/java/baritone/process/elytra/pathfinder/DegenerateRaysTest.java b/src/test/java/baritone/process/elytra/pathfinder/DegenerateRaysTest.java index efff4c674a..5c5a66992e 100644 --- a/src/test/java/baritone/process/elytra/pathfinder/DegenerateRaysTest.java +++ b/src/test/java/baritone/process/elytra/pathfinder/DegenerateRaysTest.java @@ -17,10 +17,13 @@ package baritone.process.elytra.pathfinder; +import net.minecraft.world.phys.Vec3; import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; /** @@ -30,11 +33,11 @@ */ public class DegenerateRaysTest { - private static final int SOLID = NetherPathfinder.CACHE_MISS_SOLID; + private static final NetherPathfinder.CacheMiss SOLID = NetherPathfinder.CacheMiss.SOLID; /** Chunk (0, 0) from the game: section 3 (y 48 to 63) all solid, and one block at (5, 70, 5). */ private static NetherPathfinder world() { - final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.DIMENSION_NETHER, 128); + final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.Dimension.NETHER, 128); final Chunk chunk = ctx.allocateAndInsertChunk(0, 0); chunk.fillSection(3, true); chunk.setBlock(5, 70, 5, true); @@ -44,25 +47,21 @@ private static NetherPathfinder world() { @Test(timeout = 2000) public void aPointInTheAirIsVisibleFromItself() { final NetherPathfinder ctx = world(); - assertTrue(ctx.isVisible(SOLID, 3.5, 71.5, 3.5, 3.5, 71.5, 3.5)); - final boolean[] hits = new boolean[1]; - ctx.raytrace(SOLID, 1, new double[]{3.5, 71.5, 3.5}, new double[]{3.5, 71.5, 3.5}, hits, null); - assertFalse(hits[0]); + assertTrue(visible(ctx, SOLID, 3.5, 71.5, 3.5, 3.5, 71.5, 3.5)); + assertNull(Raytracer.raytrace(ctx, 3.5, 71.5, 3.5, 3.5, 71.5, 3.5, SOLID)); } @Test(timeout = 2000) public void aPointInsideABlockIsAHitAtItself() { final NetherPathfinder ctx = world(); - assertFalse(ctx.isVisible(SOLID, 5.5, 70.5, 5.5, 5.5, 70.5, 5.5)); - final boolean[] hits = new boolean[1]; - final double[] hitPos = new double[3]; - ctx.raytrace(SOLID, 1, new double[]{5.25, 70.5, 5.75}, new double[]{5.25, 70.5, 5.75}, hits, hitPos); - assertTrue(hits[0]); - assertEquals(5.25, hitPos[0], 0); - assertEquals(70.5, hitPos[1], 0); - assertEquals(5.75, hitPos[2], 0); + assertFalse(visible(ctx, SOLID, 5.5, 70.5, 5.5, 5.5, 70.5, 5.5)); + final Vec3 hit = Raytracer.raytrace(ctx, 5.25, 70.5, 5.75, 5.25, 70.5, 5.75, SOLID); + assertNotNull(hit); + assertEquals(5.25, hit.x, 0); + assertEquals(70.5, hit.y, 0); + assertEquals(5.75, hit.z, 0); // a point outside the world is in the air, as a ray there is - assertTrue(ctx.isVisible(SOLID, 5.5, 400.0, 5.5, 5.5, 400.0, 5.5)); + assertTrue(visible(ctx, SOLID, 5.5, 400.0, 5.5, 5.5, 400.0, 5.5)); } @Test(timeout = 2000) @@ -74,7 +73,7 @@ public void aCoordinateThatIsNotFiniteIsRefused() { final double[] p = {3.5, 71.5, 3.5, 8.5, 71.5, 8.5}; p[axis] = v; try { - ctx.isVisible(SOLID, p[0], p[1], p[2], p[3], p[4], p[5]); + visible(ctx, SOLID, p[0], p[1], p[2], p[3], p[4], p[5]); throw new AssertionError("accepted " + v + " at " + axis); } catch (IllegalArgumentException expected) { // good @@ -83,7 +82,7 @@ public void aCoordinateThatIsNotFiniteIsRefused() { } // both ends infinite: the length is NaN rather than infinite try { - ctx.isVisible(SOLID, Double.POSITIVE_INFINITY, 71.5, 3.5, Double.POSITIVE_INFINITY, 71.5, 8.5); + visible(ctx, SOLID, Double.POSITIVE_INFINITY, 71.5, 3.5, Double.POSITIVE_INFINITY, 71.5, 8.5); throw new AssertionError("accepted two infinities"); } catch (IllegalArgumentException expected) { // good @@ -99,9 +98,9 @@ public void aRayEndingOnAVoxelCornerInTheAirAnswersLikeOneEndingJustInside() { for (double ox = 0.3; ox < 16; ox += 1.7) { for (double oz = 0.7; oz < 16; oz += 1.9) { for (double oy = 40.2; oy < 100; oy += 4.9) { - final boolean exact = ctx.isVisible(SOLID, ox, oy, oz, 8.0, 68.0, 8.0); + final boolean exact = visible(ctx, SOLID, ox, oy, oz, 8.0, 68.0, 8.0); final double nudge = 1e-6; - final boolean inside = ctx.isVisible(SOLID, ox, oy, oz, + final boolean inside = visible(ctx, SOLID, ox, oy, oz, 8.0 + (ox < 8.0 ? -nudge : nudge), 68.0 + (oy < 68.0 ? -nudge : nudge), 8.0 + (oz < 8.0 ? -nudge : nudge)); assertEquals("from " + ox + ", " + oy + ", " + oz, inside, exact); checked++; @@ -121,7 +120,7 @@ public void aRayEndingOnACornerOfASolidBlockAnswers() { for (double ox = 0.3; ox < 16; ox += 1.7) { for (double oz = 0.7; oz < 16; oz += 1.9) { for (double oy = 64.2; oy < 100; oy += 4.9) { - if (ctx.isVisible(SOLID, ox, oy, oz, 8.0, 64.0, 8.0)) clear++; else blocked++; + if (visible(ctx, SOLID, ox, oy, oz, 8.0, 64.0, 8.0)) clear++; else blocked++; } } } @@ -129,16 +128,15 @@ public void aRayEndingOnACornerOfASolidBlockAnswers() { } @Test(timeout = 2000) - public void aBatchWithAPointInItStillAnswersTheRest() { + public void aPointAmongOtherRaysIsAnsweredLikeAnyOther() { final NetherPathfinder ctx = world(); - final double[] from = {3.5, 71.5, 3.5, 2.5, 70.5, 5.5, 2.5, 75.5, 2.5}; - final double[] to = {3.5, 71.5, 3.5, 8.5, 70.5, 5.5, 8.5, 75.5, 8.5}; // a point, a ray through the block, a clear ray - final boolean[] hits = new boolean[3]; - ctx.raytrace(SOLID, 3, from, to, hits, null); - assertFalse(hits[0]); - assertTrue(hits[1]); - assertFalse(hits[2]); - assertEquals(1, ctx.isVisibleMulti(SOLID, 3, from, to, false)); // the first blocked one - assertEquals(0, ctx.isVisibleMulti(SOLID, 3, from, to, true)); // the first clear one: the point + // a point, a ray through the block, a clear ray + assertNull(Raytracer.raytrace(ctx, 3.5, 71.5, 3.5, 3.5, 71.5, 3.5, SOLID)); + assertNotNull(Raytracer.raytrace(ctx, 2.5, 70.5, 5.5, 8.5, 70.5, 5.5, SOLID)); + assertNull(Raytracer.raytrace(ctx, 2.5, 75.5, 2.5, 8.5, 75.5, 8.5, SOLID)); + } + /** Whether the ray reaches its end: the pathfinder's old isVisible, one ray at a time now. */ + private static boolean visible(NetherPathfinder ctx, NetherPathfinder.CacheMiss mode, double x1, double y1, double z1, double x2, double y2, double z2) { + return Raytracer.raytrace(ctx, x1, y1, z1, x2, y2, z2, mode) == null; } } diff --git a/src/test/java/baritone/process/elytra/pathfinder/Oracle.java b/src/test/java/baritone/process/elytra/pathfinder/Oracle.java index 6c9e868e39..1822c34547 100644 --- a/src/test/java/baritone/process/elytra/pathfinder/Oracle.java +++ b/src/test/java/baritone/process/elytra/pathfinder/Oracle.java @@ -88,7 +88,7 @@ static List rays() throws IOException { /** A context over the generated chunks x, z in 0..side-1, marked as if the game had given them. */ static NetherPathfinder generatedWorld(int side) { - final NetherPathfinder ctx = new NetherPathfinder(SEED, null, NetherPathfinder.DIMENSION_NETHER, 128); + final NetherPathfinder ctx = new NetherPathfinder(SEED, null, NetherPathfinder.Dimension.NETHER, 128); for (int x = 0; x < side; x++) { for (int z = 0; z < side; z++) { ctx.getOrGenChunk(x, z); diff --git a/src/test/java/baritone/process/elytra/pathfinder/PathFindTest.java b/src/test/java/baritone/process/elytra/pathfinder/PathFindTest.java index 81c4e83289..bc84a23a19 100644 --- a/src/test/java/baritone/process/elytra/pathfinder/PathFindTest.java +++ b/src/test/java/baritone/process/elytra/pathfinder/PathFindTest.java @@ -17,6 +17,7 @@ package baritone.process.elytra.pathfinder; +import net.minecraft.core.BlockPos; import org.junit.Test; import java.util.concurrent.atomic.AtomicReference; @@ -29,29 +30,7 @@ public class PathFindTest { - private static final int NETHER = NetherPathfinder.DIMENSION_NETHER; - - static int unpackX(long packed) { - return (int) (packed >> 38); - } - - static int unpackY(long packed) { - return (int) ((packed << 26) >> 52); - } - - static int unpackZ(long packed) { - return (int) ((packed << 38) >> 38); - } - - @Test - public void packedPositionsRoundTrip() { - for (int[] p : new int[][]{{0, 0, 0}, {1, 2, 3}, {-1, 5, -30000000}, {29999999, 383, 12345}, {-29999999, 0, 29999999}}) { - final long packed = NetherPathfinder.packBlockPos(new BlockPos(p[0], p[1], p[2])); - assertEquals(p[0], unpackX(packed)); - assertEquals(p[1], unpackY(packed)); - assertEquals(p[2], unpackZ(packed)); - } - } + private static final NetherPathfinder.Dimension NETHER = NetherPathfinder.Dimension.NETHER; @Test public void straightThroughAirWhenNothingIsKnown() { @@ -59,14 +38,14 @@ public void straightThroughAirWhenNothingIsKnown() { final PathSegment segment = ctx.pathFind(0, 60, 0, 500, 60, 0, true, false, 10000, true, 8.0); assertNotNull(segment); assertTrue(segment.finished); - assertTrue(segment.packed.length >= 2); + assertTrue(segment.blocks.size() >= 2); int lastX = Integer.MIN_VALUE; - for (long packed : segment.packed) { - final int x = unpackX(packed); + for (BlockPos packed : segment.blocks) { + final int x = packed.getX(); assertTrue("x goes forward", x > lastX); lastX = x; - assertEquals(0, unpackZ(packed), 16); - assertEquals(60, unpackY(packed), 16); + assertEquals(0, packed.getZ(), 16); + assertEquals(60, packed.getY(), 16); } assertEquals(500, lastX, 16); } @@ -79,7 +58,7 @@ public void refiningAStraightPathLeavesItsEnds() { final PathSegment segment = ctx.pathFind(0, 60, 0, 500, 60, 0, true, true, 10000, true, 8.0); assertNotNull(segment); assertTrue(segment.finished); - assertTrue("refined to " + segment.packed.length + " points", segment.packed.length <= 3); + assertTrue("refined to " + segment.blocks.size() + " points", segment.blocks.size() <= 3); } @Test @@ -88,7 +67,7 @@ public void givesUpAfterAHundredUnknownChunks() { final PathSegment segment = ctx.pathFind(0, 60, 0, 20000, 60, 0, true, false, 10000, true, 8.0); assertNotNull(segment); assertFalse(segment.finished); - assertTrue(unpackX(segment.packed[segment.packed.length - 1]) > 100); + assertTrue(segment.blocks.get(segment.blocks.size() - 1).getX() > 100); } @Test @@ -98,8 +77,8 @@ public void reachesTheGoalOverRealTerrain() { assertNotNull(segment); assertTrue(segment.finished); // every point of the path is in air - for (long packed : segment.packed) { - final int x = unpackX(packed), y = unpackY(packed), z = unpackZ(packed); + for (BlockPos packed : segment.blocks) { + final int x = packed.getX(), y = packed.getY(), z = packed.getZ(); assertFalse("path goes through a block at " + x + "," + y + "," + z, ctx.getChunkOrDefault(x >> 4, z >> 4, true).isSolid(x & 15, y, z & 15)); } } @@ -111,7 +90,7 @@ public void generatesTerrainWhenAskedTo() { assertNotNull(segment); assertTrue(segment.finished); assertTrue("generated chunks along the way", ctx.chunkCount() > 20); - assertFalse(ctx.hasChunkFromJava(1, 1)); + assertFalse(ctx.hasChunkFromCaller(1, 1)); } @Test @@ -131,7 +110,10 @@ public void cancelStopsARunningSearch() throws Exception { assertFalse(searcher.isAlive()); assertNull(result.get()); assertTrue("returned " + elapsed[0] + " ms after the start", elapsed[0] < 1000); - assertTrue(ctx.cancel()); + // The search that was stopped clears the flag as it returns, so this cancel is the first + // one again. It used to be cleared at the start of the next search instead, which dropped a + // cancel that arrived while that search was still queued -- see CancelTest. + assertFalse("the cancelled search cleared the flag on its way out", ctx.cancel()); } @Test @@ -143,7 +125,7 @@ public void aSearchThatCannotFinishReturnsWhatItHasAfterHalfASecond() { assertNotNull(segment); assertFalse(segment.finished); assertTrue("took " + ms + " ms", ms >= 500 && ms < 5000); - assertTrue(unpackX(segment.packed[segment.packed.length - 1]) > 50); + assertTrue(segment.blocks.get(segment.blocks.size() - 1).getX() > 50); } @Test @@ -162,10 +144,10 @@ public void rejectsBadArguments() { // good } try { - new NetherPathfinder(1, null, 7, 128); - throw new AssertionError("expected IllegalArgumentException"); - } catch (IllegalArgumentException expected) { - // good + new NetherPathfinder(1, null, null, 128); + throw new AssertionError("expected NullPointerException"); + } catch (NullPointerException expected) { + // good: the dimension is an enum, so the only bad one is a missing one } } } diff --git a/src/test/java/baritone/process/elytra/pathfinder/RaytraceTest.java b/src/test/java/baritone/process/elytra/pathfinder/RaytraceTest.java index 381329a2b6..5e495259c8 100644 --- a/src/test/java/baritone/process/elytra/pathfinder/RaytraceTest.java +++ b/src/test/java/baritone/process/elytra/pathfinder/RaytraceTest.java @@ -17,12 +17,15 @@ package baritone.process.elytra.pathfinder; +import net.minecraft.world.phys.Vec3; import org.junit.Test; import java.util.List; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; public class RaytraceTest { @@ -32,14 +35,13 @@ public void everyOracleRayAgreesWithTheNativeRaytracer() throws Exception { final List rays = Oracle.rays(); assertEquals(4000, rays.size()); final NetherPathfinder ctx = Oracle.generatedWorld(16); - final double[] where = new double[3]; int wrongHit = 0; int wrongWhere = 0; for (Oracle.Ray r : rays) { - final boolean hit = Raytracer.raytrace(ctx, r.from[0], r.from[1], r.from[2], r.to[0], r.to[1], r.to[2], NetherPathfinder.CACHE_MISS_SOLID, where, 0); - if (hit != r.hit) { + final Vec3 hit = Raytracer.raytrace(ctx, r.from[0], r.from[1], r.from[2], r.to[0], r.to[1], r.to[2], NetherPathfinder.CacheMiss.SOLID); + if ((hit != null) != r.hit) { wrongHit++; - } else if (hit && (where[0] != r.where[0] || where[1] != r.where[1] || where[2] != r.where[2])) { + } else if (hit != null && (hit.x != r.where[0] || hit.y != r.where[1] || hit.z != r.where[2])) { wrongWhere++; } } @@ -49,76 +51,71 @@ public void everyOracleRayAgreesWithTheNativeRaytracer() throws Exception { @Test public void nothingToHitInAirMode() { - final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.DIMENSION_NETHER, 128); - assertTrue(ctx.isVisible(NetherPathfinder.CACHE_MISS_AIR, 0.5, 64.5, 0.5, 300.5, 20.5, -200.5)); + final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.Dimension.NETHER, 128); + assertTrue(visible(ctx, NetherPathfinder.CacheMiss.AIR, 0.5, 64.5, 0.5, 300.5, 20.5, -200.5)); } @Test public void unknownChunksAreWallsInSolidMode() { - final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.DIMENSION_NETHER, 128); - final double[] hitPos = new double[3]; - final boolean[] hits = new boolean[1]; - ctx.raytrace(NetherPathfinder.CACHE_MISS_SOLID, 1, new double[]{0.5, 64.5, 0.5}, new double[]{30.5, 64.5, 0.5}, hits, hitPos); - assertTrue(hits[0]); + final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.Dimension.NETHER, 128); + final Vec3 hit = Raytracer.raytrace(ctx, 0.5, 64.5, 0.5, 30.5, 64.5, 0.5, NetherPathfinder.CacheMiss.SOLID); + assertNotNull(hit); // the origin is inside the solid, so the hit is the origin - assertEquals(0.5, hitPos[0], 0); - assertEquals(64.5, hitPos[1], 0); - assertEquals(0.5, hitPos[2], 0); + assertEquals(0.5, hit.x, 0); + assertEquals(64.5, hit.y, 0); + assertEquals(0.5, hit.z, 0); } @Test public void hitsTheFaceOfABlock() { - final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.DIMENSION_NETHER, 128); + final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.Dimension.NETHER, 128); final Chunk chunk = ctx.allocateAndInsertChunk(0, 0); chunk.setBlock(8, 64, 8, true); - final double[] hitPos = new double[3]; - final boolean[] hits = new boolean[1]; - ctx.raytrace(NetherPathfinder.CACHE_MISS_AIR, 1, new double[]{2.5, 64.5, 8.5}, new double[]{14.5, 64.5, 8.5}, hits, hitPos); - assertTrue(hits[0]); - assertEquals(8.0, hitPos[0], 1e-9); - assertEquals(64.5, hitPos[1], 1e-9); - assertEquals(8.5, hitPos[2], 1e-9); + Vec3 hit = Raytracer.raytrace(ctx, 2.5, 64.5, 8.5, 14.5, 64.5, 8.5, NetherPathfinder.CacheMiss.AIR); + assertNotNull(hit); + assertEquals(8.0, hit.x, 1e-9); + assertEquals(64.5, hit.y, 1e-9); + assertEquals(8.5, hit.z, 1e-9); // and from the other side - ctx.raytrace(NetherPathfinder.CACHE_MISS_AIR, 1, new double[]{14.5, 64.5, 8.5}, new double[]{2.5, 64.5, 8.5}, hits, hitPos); - assertTrue(hits[0]); - assertEquals(9.0, hitPos[0], 1e-9); + hit = Raytracer.raytrace(ctx, 14.5, 64.5, 8.5, 2.5, 64.5, 8.5, NetherPathfinder.CacheMiss.AIR); + assertNotNull(hit); + assertEquals(9.0, hit.x, 1e-9); // a ray that stops short of it - assertTrue(ctx.isVisible(NetherPathfinder.CACHE_MISS_AIR, 2.5, 64.5, 8.5, 7.5, 64.5, 8.5)); + assertTrue(visible(ctx, NetherPathfinder.CacheMiss.AIR, 2.5, 64.5, 8.5, 7.5, 64.5, 8.5)); // a ray past it in another row - assertTrue(ctx.isVisible(NetherPathfinder.CACHE_MISS_AIR, 2.5, 64.5, 9.5, 14.5, 64.5, 9.5)); + assertTrue(visible(ctx, NetherPathfinder.CacheMiss.AIR, 2.5, 64.5, 9.5, 14.5, 64.5, 9.5)); } @Test public void crossesChunkAndSlabBoundaries() { - final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.DIMENSION_NETHER, 128); + final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.Dimension.NETHER, 128); ctx.allocateAndInsertChunk(0, 0); ctx.allocateAndInsertChunk(1, 0); ctx.allocateAndInsertChunk(2, 0).setBlock(3, 70, 4, true); - assertTrue(ctx.isVisible(NetherPathfinder.CACHE_MISS_AIR, 1.5, 40.5, 4.5, 34.5, 70.5, 4.5)); - assertFalse(ctx.isVisible(NetherPathfinder.CACHE_MISS_AIR, 1.5, 40.5, 4.5, 36.5, 70.5, 4.5)); - assertFalse(ctx.isVisible(NetherPathfinder.CACHE_MISS_AIR, 36.5, 70.5, 4.5, 1.5, 40.5, 4.5)); + assertTrue(visible(ctx, NetherPathfinder.CacheMiss.AIR, 1.5, 40.5, 4.5, 34.5, 70.5, 4.5)); + assertFalse(visible(ctx, NetherPathfinder.CacheMiss.AIR, 1.5, 40.5, 4.5, 36.5, 70.5, 4.5)); + assertFalse(visible(ctx, NetherPathfinder.CacheMiss.AIR, 36.5, 70.5, 4.5, 1.5, 40.5, 4.5)); } @Test public void theRayThatEndedTheNativeProcessTerminates() { // nether-pathfinder issue 23: an end exactly on a chunk corner - final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.DIMENSION_NETHER, 128); - assertTrue(ctx.isVisible(NetherPathfinder.CACHE_MISS_AIR, 386.7112215521066, 137.40911926818373, 7.0554159455922285, 416.0, 142.0, 0.0)); - assertFalse(ctx.isVisible(NetherPathfinder.CACHE_MISS_SOLID, 386.7112215521066, 137.40911926818373, 7.0554159455922285, 416.0, 142.0, 0.0)); + final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.Dimension.NETHER, 128); + assertTrue(visible(ctx, NetherPathfinder.CacheMiss.AIR, 386.7112215521066, 137.40911926818373, 7.0554159455922285, 416.0, 142.0, 0.0)); + assertFalse(visible(ctx, NetherPathfinder.CacheMiss.SOLID, 386.7112215521066, 137.40911926818373, 7.0554159455922285, 416.0, 142.0, 0.0)); } @Test - public void isVisibleMultiReportsTheFirstSegmentThatDecides() { - final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.DIMENSION_NETHER, 128); + public void eachRayAnswersOnItsOwn() { + final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.Dimension.NETHER, 128); ctx.allocateAndInsertChunk(0, 0).setBlock(8, 64, 8, true); - final double[] start = {2.5, 64.5, 8.5, 2.5, 64.5, 9.5, 2.5, 64.5, 8.5}; - final double[] end = {14.5, 64.5, 8.5, 14.5, 64.5, 9.5, 14.5, 64.5, 8.5}; - // first clear one is index 1; first blocked one is index 0 - assertEquals(1, ctx.isVisibleMulti(NetherPathfinder.CACHE_MISS_AIR, 3, start, end, true)); - assertEquals(0, ctx.isVisibleMulti(NetherPathfinder.CACHE_MISS_AIR, 3, start, end, false)); - // all clear: -1 in "all" mode; none blocked - final double[] clearEnd = {7.5, 64.5, 8.5, 14.5, 64.5, 9.5, 7.5, 64.5, 8.5}; - assertEquals(-1, ctx.isVisibleMulti(NetherPathfinder.CACHE_MISS_AIR, 3, start, clearEnd, false)); - assertEquals(0, ctx.isVisibleMulti(NetherPathfinder.CACHE_MISS_AIR, 3, start, clearEnd, true)); + // through the block, past it in another row, and stopping short of it + assertFalse(visible(ctx, NetherPathfinder.CacheMiss.AIR, 2.5, 64.5, 8.5, 14.5, 64.5, 8.5)); + assertTrue(visible(ctx, NetherPathfinder.CacheMiss.AIR, 2.5, 64.5, 9.5, 14.5, 64.5, 9.5)); + assertTrue(visible(ctx, NetherPathfinder.CacheMiss.AIR, 2.5, 64.5, 8.5, 7.5, 64.5, 8.5)); + } + /** Whether the ray reaches its end: the pathfinder's old isVisible, one ray at a time now. */ + private static boolean visible(NetherPathfinder ctx, NetherPathfinder.CacheMiss mode, double x1, double y1, double z1, double x2, double y2, double z2) { + return Raytracer.raytrace(ctx, x1, y1, z1, x2, y2, z2, mode) == null; } } From 8827e1578215234d89ac6458ca1c2c231a08dc20 Mon Sep 17 00:00:00 2001 From: Mattias Rask <231212847+0Mattias@users.noreply.github.com> Date: Sat, 12 Sep 2026 18:29:42 -0400 Subject: [PATCH 05/15] Keep the x8 summary bit exact for single block updates babbaj's second review, in order. The x8 summary bit has to be cleared with its cube's last block after all: queueBlockUpdate sets single blocks to air in chunks the search is using, and a stale bit would leave a cube reading as possibly solid for as long as the chunk lives. But the scan that keeping the bit exact costs is wasted on the callers that fill a whole chunk at once, which only ever set blocks. So setBlock takes an `exact` flag that runs the scan on a clear, the four-argument setBlock passes false, and queueBlockUpdate alone passes true; ChunkTest checks both kinds of clear. The comment above CachedRegion.CACHED_REGION_MAGIC and the javadoc on PathSegment.blocks go. Also dropped, since they were unused: EOFException in BaritoneRegion, OutputStream in BaritoneRegionTest, List in NetherPathfinder and assertNull in RaytraceTest. Co-Authored-By: Claude Fable 5.1 --- .../java/baritone/cache/CachedRegion.java | 1 - .../elytra/NetherPathfinderContext.java | 3 +- .../elytra/pathfinder/BaritoneRegion.java | 1 - .../process/elytra/pathfinder/Chunk.java | 32 ++++++++++++++++--- .../elytra/pathfinder/NetherPathfinder.java | 1 - .../elytra/pathfinder/PathSegment.java | 1 - .../elytra/pathfinder/BaritoneRegionTest.java | 1 - .../process/elytra/pathfinder/ChunkTest.java | 22 +++++++------ .../elytra/pathfinder/RaytraceTest.java | 1 - 9 files changed, 42 insertions(+), 21 deletions(-) diff --git a/src/main/java/baritone/cache/CachedRegion.java b/src/main/java/baritone/cache/CachedRegion.java index d9efa1db27..aac4e108a2 100644 --- a/src/main/java/baritone/cache/CachedRegion.java +++ b/src/main/java/baritone/cache/CachedRegion.java @@ -44,7 +44,6 @@ public final class CachedRegion implements ICachedRegion { /** * Magic value to detect invalid cache files, or incompatible cache files saved in an old version of Baritone */ - // Public: the elytra pathfinder's region reader checks the same header. public static final int CACHED_REGION_MAGIC = 456022911; /** diff --git a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java index c69553a81c..432a53f09a 100644 --- a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java +++ b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java @@ -134,7 +134,8 @@ public void queueBlockUpdate(BlockChangeEvent event) { BlockPos pos = pair.first().below(minY); if (pos.getY() < 0 || pos.getY() >= 384) return; boolean isSolid = pair.second() != AIR_BLOCK_STATE; - chunk.setBlock(pos.getX() & 15, pos.getY(), pos.getZ() & 15, isSolid); + // one block at a time in a chunk that is in use, so keep the x8 summary exact + chunk.setBlock(pos.getX() & 15, pos.getY(), pos.getZ() & 15, isSolid, true); }); } finally { writeLock.unlock(); diff --git a/src/main/java/baritone/process/elytra/pathfinder/BaritoneRegion.java b/src/main/java/baritone/process/elytra/pathfinder/BaritoneRegion.java index 5779d12948..f0961a0721 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/BaritoneRegion.java +++ b/src/main/java/baritone/process/elytra/pathfinder/BaritoneRegion.java @@ -19,7 +19,6 @@ import baritone.cache.CachedRegion; import java.io.DataInputStream; -import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; diff --git a/src/main/java/baritone/process/elytra/pathfinder/Chunk.java b/src/main/java/baritone/process/elytra/pathfinder/Chunk.java index 0e7678683e..e99626d217 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/Chunk.java +++ b/src/main/java/baritone/process/elytra/pathfinder/Chunk.java @@ -47,7 +47,7 @@ public final class Chunk { public static final Chunk SOLID = new Chunk(true); private final long[][] sections = new long[SECTIONS][]; - /** Bit i of entry s is set once a block is set in x8 cube i of section s, and stays set until fillSection resets it: set means the cube may hold a block, clear that it does not. A section that is null has 0. */ + /** Bit i of entry s is set once a block is set in x8 cube i of section s. fillSection resets it, an exact setBlock clears it with the cube's last block, and a plain clear leaves it: set means the cube may hold a block, clear that it does not. A section that is null has 0. */ private final int[] filled = new int[SECTIONS]; private final boolean shared; @@ -93,6 +93,13 @@ static int byteAt(long[] section, int off) { return (int) (section[off >>> 3] >>> ((off & 7) << 3)) & 0xFF; } + static boolean allZero(long[] section, int from, int longs) { + long acc = 0; + for (int i = 0; i < longs; i++) { + acc |= section[from + i]; + } + return acc == 0; + } /** The section holding y, or null if nothing has been set in it (or y is outside the chunk). */ long[] section(int y) { @@ -116,8 +123,23 @@ public boolean isSolid(int x, int y, int z) { return ((s[off >>> 3] >>> (((off & 7) << 3) + bitIndex(x, y, z))) & 1L) != 0; } - /** Coordinates are chunk relative: x and z in 0..15, y in 0..383. */ + /** + * Coordinates are chunk relative: x and z in 0..15, y in 0..383. A clear leaves the x8's bit in + * filled as it is, which suits the callers that fill a whole chunk at once: they only ever set + * blocks, so the scan that keeping the bit exact costs would be wasted on them. Clearing single + * blocks in a chunk that is in use is {@link #setBlock(int, int, int, boolean, boolean)}. + */ public void setBlock(int x, int y, int z, boolean solid) { + setBlock(x, y, z, solid, false); + } + + /** + * As {@link #setBlock(int, int, int, boolean)}; with {@code exact}, a clear that empties its x8 + * cube also clears the cube's bit in filled, at the cost of a scan of the cube's eight longs. A + * block update in a chunk the search is using passes true, so that a broken block does not leave + * its cube reading as possibly solid for as long as the chunk lives. + */ + public void setBlock(int x, int y, int z, boolean solid, boolean exact) { if (this.shared) { throw new UnsupportedOperationException("the shared air and solid chunks are read only"); } @@ -136,10 +158,10 @@ public void setBlock(int x, int y, int z, boolean solid) { this.filled[y >> 4] |= 1 << x8; s[off >>> 3] |= mask; } else { - // The x8's bit in filled stays. It means the cube may hold a block, so one that - // outlives its blocks costs a reader a scan of the cube and never a wrong answer; - // fillSection resets it. s[off >>> 3] &= ~mask; + if (exact && allZero(s, x8 * (X8_BYTES / 8), X8_BYTES / 8)) { + this.filled[y >> 4] &= ~(1 << x8); + } } } diff --git a/src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java b/src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java index a8d470eb2c..392d26789c 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java +++ b/src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java @@ -18,7 +18,6 @@ package baritone.process.elytra.pathfinder; import net.minecraft.core.BlockPos; -import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; diff --git a/src/main/java/baritone/process/elytra/pathfinder/PathSegment.java b/src/main/java/baritone/process/elytra/pathfinder/PathSegment.java index 5cc0cb8357..aea4f3df20 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/PathSegment.java +++ b/src/main/java/baritone/process/elytra/pathfinder/PathSegment.java @@ -22,7 +22,6 @@ public class PathSegment { public final boolean finished; - /** The blocks of the path, in order. */ public final List blocks; public PathSegment(boolean finished, List blocks) { diff --git a/src/test/java/baritone/process/elytra/pathfinder/BaritoneRegionTest.java b/src/test/java/baritone/process/elytra/pathfinder/BaritoneRegionTest.java index 1e4a0f801d..18b6a79960 100644 --- a/src/test/java/baritone/process/elytra/pathfinder/BaritoneRegionTest.java +++ b/src/test/java/baritone/process/elytra/pathfinder/BaritoneRegionTest.java @@ -21,7 +21,6 @@ import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; -import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.BitSet; diff --git a/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java b/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java index 3259552bac..d16112f482 100644 --- a/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java +++ b/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java @@ -62,19 +62,23 @@ public void emptinessAtEveryLevel() { assertTrue(chunk.isEmpty(Size.X1, 8, 70, 3)); assertTrue(chunk.isEmpty(Size.X16, 0, 80, 0)); - // clearing a block empties its x8 and x16 again only once it was the last one there + // an exact clear empties its x8 and x16 again only once it was the last block there chunk.setBlock(10, 70, 3, true); - chunk.setBlock(9, 70, 3, false); + chunk.setBlock(9, 70, 3, false, true); assertFalse(chunk.isEmptyX8(9, 70, 3)); assertFalse(chunk.isEmptyX16(64)); - chunk.setBlock(10, 70, 3, false); - // The blocks are gone, which the exact question sees. The x8 and x16 summaries are - // allowed to lag behind a clear, and only ever towards "may hold a block". + chunk.setBlock(10, 70, 3, false, true); + assertTrue(chunk.isEmptyX8(9, 70, 3)); + assertTrue(chunk.isEmptyX16(64)); + assertNotNull(chunk.section(64)); // the section stays allocated + // a plain clear, which is what the bulk fills use, leaves the x8 and x16 summaries as they + // are: the block is gone, which the exact question sees, and the summaries err only towards + // "may hold a block" + chunk.setBlock(9, 70, 3, true); + chunk.setBlock(9, 70, 3, false); assertFalse(chunk.isSolid(9, 70, 3)); - assertFalse(chunk.isSolid(10, 70, 3)); assertFalse(chunk.isEmptyX8(9, 70, 3)); assertFalse(chunk.isEmptyX16(64)); - assertNotNull(chunk.section(64)); // the section stays allocated } @Test @@ -105,8 +109,8 @@ public void fillSection() { assertTrue(chunk.isEmptyX16(64)); assertTrue(chunk.isEmptyX8(15, 79, 15)); chunk.fillSection(4, true); - chunk.setBlock(15, 79, 15, false); - assertFalse("the section still has the other blocks", chunk.isEmptyX8(15, 79, 15)); + chunk.setBlock(15, 79, 15, false, true); + assertFalse("the x8 still has its other blocks", chunk.isEmptyX8(15, 79, 15)); } @Test diff --git a/src/test/java/baritone/process/elytra/pathfinder/RaytraceTest.java b/src/test/java/baritone/process/elytra/pathfinder/RaytraceTest.java index 5e495259c8..c0edc8364e 100644 --- a/src/test/java/baritone/process/elytra/pathfinder/RaytraceTest.java +++ b/src/test/java/baritone/process/elytra/pathfinder/RaytraceTest.java @@ -25,7 +25,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; public class RaytraceTest { From 65d533a3ea0d06b284a1c5a2c425a5536965d955 Mon Sep 17 00:00:00 2001 From: Mattias Rask <231212847+0Mattias@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:43:22 -0400 Subject: [PATCH 06/15] Ask a block update's state whether it is air babbaj's third review. The block update test compared the state against Blocks.AIR's default state, so a block that became cave air counted as solid. isAir covers cave air and void air too, and writeChunkData already treats cave air as air when it packs a whole chunk, so a single block update and a chunk fill now agree on what air is. AIR_BLOCK_STATE had no other use and goes. Co-Authored-By: Claude Fable 5.1 --- .../java/baritone/process/elytra/NetherPathfinderContext.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java index 432a53f09a..d99d844eea 100644 --- a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java +++ b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java @@ -50,7 +50,6 @@ */ public final class NetherPathfinderContext implements IElytraPathFinder { - private static final BlockState AIR_BLOCK_STATE = Blocks.AIR.defaultBlockState(); // The native library needed this lock held while there were pointers to its chunks in Java. // The port needs none of that -- a chunk is an object that stays valid for whoever holds it, // and the table takes lookups, inserts and culls from any thread at once -- and the lock is @@ -133,7 +132,7 @@ public void queueBlockUpdate(BlockChangeEvent event) { event.getBlocks().forEach(pair -> { BlockPos pos = pair.first().below(minY); if (pos.getY() < 0 || pos.getY() >= 384) return; - boolean isSolid = pair.second() != AIR_BLOCK_STATE; + boolean isSolid = !pair.second().isAir(); // one block at a time in a chunk that is in use, so keep the x8 summary exact chunk.setBlock(pos.getX() & 15, pos.getY(), pos.getZ() & 15, isSolid, true); }); From eb62fc04939976da8533d5087ed6842fea063aa4 Mon Sep 17 00:00:00 2001 From: Mattias Rask <231212847+0Mattias@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:49:11 -0400 Subject: [PATCH 07/15] Use Minecraft's Direction instead of the port's Face babbaj's fourth review. Face only named the six directions and moved a BlockPos along one, which Direction and BlockPos.relative already do, so the enum goes and PathFinder takes a Direction. The neighbour array keeps the native library's order, UP, DOWN, NORTH, SOUTH, EAST, WEST, rather than Direction.values(), so the search expands neighbours as it always has. Co-Authored-By: Claude Fable 5.1 --- .../process/elytra/pathfinder/Face.java | 36 ------------------- .../process/elytra/pathfinder/PathFinder.java | 17 +++++---- 2 files changed, 10 insertions(+), 43 deletions(-) delete mode 100644 src/main/java/baritone/process/elytra/pathfinder/Face.java diff --git a/src/main/java/baritone/process/elytra/pathfinder/Face.java b/src/main/java/baritone/process/elytra/pathfinder/Face.java deleted file mode 100644 index ba9fc80cc0..0000000000 --- a/src/main/java/baritone/process/elytra/pathfinder/Face.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * This file is part of Baritone. - * - * Baritone is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Baritone is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Baritone. If not, see . - */ - -package baritone.process.elytra.pathfinder; - -import net.minecraft.core.BlockPos; - -enum Face { - UP, DOWN, NORTH, SOUTH, EAST, WEST; - - /** {@code pos} moved n blocks this way. */ - BlockPos offset(BlockPos pos, int n) { - switch (this) { - case UP: return pos.above(n); - case DOWN: return pos.below(n); - case NORTH: return pos.north(n); - case SOUTH: return pos.south(n); - case EAST: return pos.east(n); - default: return pos.west(n); - } - } -} diff --git a/src/main/java/baritone/process/elytra/pathfinder/PathFinder.java b/src/main/java/baritone/process/elytra/pathfinder/PathFinder.java index 4bb4899626..a253a65258 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/PathFinder.java +++ b/src/main/java/baritone/process/elytra/pathfinder/PathFinder.java @@ -18,6 +18,7 @@ package baritone.process.elytra.pathfinder; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; @@ -38,7 +39,9 @@ final class PathFinder { static final double MIN_DIST_PATH = 5; // might want to increase this private static final double MIN_IMPROVEMENT = 0.01; - private static final Face[] ALL_FACES = {Face.UP, Face.DOWN, Face.NORTH, Face.SOUTH, Face.EAST, Face.WEST}; + // The native PathFinder.cpp's order rather than Direction.values(), so the search expands + // neighbours as it always has. + private static final Direction[] ALL_FACES = {Direction.UP, Direction.DOWN, Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST}; static final class Path { enum Type { @@ -120,7 +123,7 @@ private interface Callback { * Called inside a big neighbour cube; returns the 4 sub cubes that are adjacent to the * original cube. The face is relative to the original cube, the size is the sub cubes'. */ - private static BlockPos[] neighborCubes(Face face, Size sz, BlockPos origin) { + private static BlockPos[] neighborCubes(Direction face, Size sz, BlockPos origin) { final int size = sz.width(); final BlockPos corner; switch (face) { @@ -145,7 +148,7 @@ private static BlockPos[] neighborCubes(Face face, Size sz, BlockPos origin) { } } - private static void forEachNeighborInCube(Chunk chunk, int state, NodePos neighborNode, Face face, Size size, boolean sizeChange, Size minSize, Callback callback) { + private static void forEachNeighborInCube(Chunk chunk, int state, NodePos neighborNode, Direction face, Size size, boolean sizeChange, Size minSize, Callback callback) { if (sizeChange) { callback.accept(neighborNode, chunk, state); return; @@ -166,7 +169,7 @@ private static void forEachNeighborInCube(Chunk chunk, int state, NodePos neighb } /** Grows the neighbour to the largest empty cube it is in, then iterates what is on the face. */ - private static void growThenIterate(Chunk chunk, int state, NodePos pos, Face face, Size minSize, Callback callback) { + private static void growThenIterate(Chunk chunk, int state, NodePos pos, Direction face, Size minSize, Callback callback) { final Size originalSize = pos.size; final BlockPos bpos = pos.absolutePosZero(); final int lx = bpos.getX() & 15; @@ -293,10 +296,10 @@ static Path findPathSegment(NetherPathfinder ctx, NodePos start, NodePos goal, b generateMissingNeighbours(ctx, cx, cz); } - for (Face face : ALL_FACES) { - final NodePos neighborNodePos = new NodePos(size, face.offset(bpos, size.width())); + for (Direction face : ALL_FACES) { + final NodePos neighborNodePos = new NodePos(size, bpos.relative(face, size.width())); final BlockPos origin = neighborNodePos.absolutePosZero(); - if (face == Face.UP || face == Face.DOWN) { + if (face == Direction.UP || face == Direction.DOWN) { if (!isInBounds(ctx.maxHeight, origin)) continue; } final int neighborCx = (origin.getX() >> 4); From 1a3c211322bca5e77d9d431758d95267c8704518 Mon Sep 17 00:00:00 2001 From: Mattias Rask <231212847+0Mattias@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:59:56 -0400 Subject: [PATCH 08/15] Keep the x8 summary exact on every clear ZacSharp's review, on Chunk.setBlock and isEmptyX8. The exact flag existed so that the callers that fill a whole chunk would not pay the scan that keeping the summary exact costs on a clear, but none of them ever clears a block: writeChunkData, insertChunkData, BaritoneRegion and the generator only set, and the block update, the one caller that clears, already asked for exact. So the flag decided nothing, and with it gone the summary is exact by construction, which answers the question of whether isEmptyX8 and isEmptyX16 may say no for an empty cube: they may not, and a summary that disagrees with the blocks is a bug in whatever wrote them, as babbaj put it. Co-Authored-By: Claude Fable 5.1 --- .../elytra/NetherPathfinderContext.java | 3 +-- .../process/elytra/pathfinder/Chunk.java | 25 ++++++------------- .../process/elytra/pathfinder/ChunkTest.java | 16 +++--------- 3 files changed, 12 insertions(+), 32 deletions(-) diff --git a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java index d99d844eea..6c21c89290 100644 --- a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java +++ b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java @@ -133,8 +133,7 @@ public void queueBlockUpdate(BlockChangeEvent event) { BlockPos pos = pair.first().below(minY); if (pos.getY() < 0 || pos.getY() >= 384) return; boolean isSolid = !pair.second().isAir(); - // one block at a time in a chunk that is in use, so keep the x8 summary exact - chunk.setBlock(pos.getX() & 15, pos.getY(), pos.getZ() & 15, isSolid, true); + chunk.setBlock(pos.getX() & 15, pos.getY(), pos.getZ() & 15, isSolid); }); } finally { writeLock.unlock(); diff --git a/src/main/java/baritone/process/elytra/pathfinder/Chunk.java b/src/main/java/baritone/process/elytra/pathfinder/Chunk.java index e99626d217..3aff540274 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/Chunk.java +++ b/src/main/java/baritone/process/elytra/pathfinder/Chunk.java @@ -25,7 +25,7 @@ * library's: 24 sections of 16x16x16 blocks (512 bytes each); a section is 8 x8 cubes of 64 bytes; an * x8 is 8 x4 cubes of 8 bytes; an x4 is 8 x2 cubes of one byte; the 8 bits of that byte are the * blocks. A section in which no block has been set is not allocated. Beside the sections, one byte per - * section says which of its x8 cubes may hold a block, so that the question a search asks first and a ray + * section says which of its x8 cubes hold a block, so that the question a search asks first and a ray * asks of every x16 and x8 it enters, whether a cube that big is empty, is a bit test rather than a * scan of 64 or 8 longs. *

@@ -47,7 +47,7 @@ public final class Chunk { public static final Chunk SOLID = new Chunk(true); private final long[][] sections = new long[SECTIONS][]; - /** Bit i of entry s is set once a block is set in x8 cube i of section s. fillSection resets it, an exact setBlock clears it with the cube's last block, and a plain clear leaves it: set means the cube may hold a block, clear that it does not. A section that is null has 0. */ + /** Bit i of entry s says whether x8 cube i of section s holds a block; a section that is null has 0. */ private final int[] filled = new int[SECTIONS]; private final boolean shared; @@ -107,7 +107,7 @@ long[] section(int y) { return i >= 0 && i < SECTIONS ? this.sections[i] : null; } - /** Which x8 cubes of the section holding y may hold a block, one bit each in x8Index order; 0 outside the chunk. */ + /** Which x8 cubes of the section holding y hold a block, one bit each in x8Index order; 0 outside the chunk. */ int filled(int y) { final int i = y >> 4; return i >= 0 && i < SECTIONS ? this.filled[i] : 0; @@ -124,22 +124,11 @@ public boolean isSolid(int x, int y, int z) { } /** - * Coordinates are chunk relative: x and z in 0..15, y in 0..383. A clear leaves the x8's bit in - * filled as it is, which suits the callers that fill a whole chunk at once: they only ever set - * blocks, so the scan that keeping the bit exact costs would be wasted on them. Clearing single - * blocks in a chunk that is in use is {@link #setBlock(int, int, int, boolean, boolean)}. + * Coordinates are chunk relative: x and z in 0..15, y in 0..383. A clear that empties its x8 + * cube also clears the cube's bit in filled, which costs a scan of the cube's eight longs; the + * callers that fill a whole chunk only ever set blocks, so they never pay it. */ public void setBlock(int x, int y, int z, boolean solid) { - setBlock(x, y, z, solid, false); - } - - /** - * As {@link #setBlock(int, int, int, boolean)}; with {@code exact}, a clear that empties its x8 - * cube also clears the cube's bit in filled, at the cost of a scan of the cube's eight longs. A - * block update in a chunk the search is using passes true, so that a broken block does not leave - * its cube reading as possibly solid for as long as the chunk lives. - */ - public void setBlock(int x, int y, int z, boolean solid, boolean exact) { if (this.shared) { throw new UnsupportedOperationException("the shared air and solid chunks are read only"); } @@ -159,7 +148,7 @@ public void setBlock(int x, int y, int z, boolean solid, boolean exact) { s[off >>> 3] |= mask; } else { s[off >>> 3] &= ~mask; - if (exact && allZero(s, x8 * (X8_BYTES / 8), X8_BYTES / 8)) { + if (allZero(s, x8 * (X8_BYTES / 8), X8_BYTES / 8)) { this.filled[y >> 4] &= ~(1 << x8); } } diff --git a/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java b/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java index d16112f482..4665c11b6e 100644 --- a/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java +++ b/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java @@ -62,23 +62,15 @@ public void emptinessAtEveryLevel() { assertTrue(chunk.isEmpty(Size.X1, 8, 70, 3)); assertTrue(chunk.isEmpty(Size.X16, 0, 80, 0)); - // an exact clear empties its x8 and x16 again only once it was the last block there + // a clear empties its x8 and x16 again only once it was the last block there chunk.setBlock(10, 70, 3, true); - chunk.setBlock(9, 70, 3, false, true); + chunk.setBlock(9, 70, 3, false); assertFalse(chunk.isEmptyX8(9, 70, 3)); assertFalse(chunk.isEmptyX16(64)); - chunk.setBlock(10, 70, 3, false, true); + chunk.setBlock(10, 70, 3, false); assertTrue(chunk.isEmptyX8(9, 70, 3)); assertTrue(chunk.isEmptyX16(64)); assertNotNull(chunk.section(64)); // the section stays allocated - // a plain clear, which is what the bulk fills use, leaves the x8 and x16 summaries as they - // are: the block is gone, which the exact question sees, and the summaries err only towards - // "may hold a block" - chunk.setBlock(9, 70, 3, true); - chunk.setBlock(9, 70, 3, false); - assertFalse(chunk.isSolid(9, 70, 3)); - assertFalse(chunk.isEmptyX8(9, 70, 3)); - assertFalse(chunk.isEmptyX16(64)); } @Test @@ -109,7 +101,7 @@ public void fillSection() { assertTrue(chunk.isEmptyX16(64)); assertTrue(chunk.isEmptyX8(15, 79, 15)); chunk.fillSection(4, true); - chunk.setBlock(15, 79, 15, false, true); + chunk.setBlock(15, 79, 15, false); assertFalse("the x8 still has its other blocks", chunk.isEmptyX8(15, 79, 15)); } From 9e6432c3d5d7a552b2929dccdd310cf05f5c6cbf Mon Sep 17 00:00:00 2001 From: Mattias Rask <231212847+0Mattias@users.noreply.github.com> Date: Wed, 16 Sep 2026 19:00:00 -0400 Subject: [PATCH 09/15] Say what keeps a chunk's readers and writers apart ZacSharp asked whether the JIT or the CPU may reorder the two plain writes in setBlock, and babbaj asked why a race was expected there at all. The comment claimed an order the language does not promise: without a fence, a reader on another thread may see the block before the summary bit. It was written for a reader that does not exist. Chunk was ported to be safe on its own, like the native code, but every reader in Baritone takes NetherPathfinderContext's read lock, the solver thread, the game thread's tick and a search that does not generate alike, while a block update, a chunk pack, a cull and a generating search hold its write lock, so no reader can watch a block being set. The class comment now says so instead of describing a race, and the comment on the write goes. Co-Authored-By: Claude Fable 5.1 --- .../java/baritone/process/elytra/pathfinder/Chunk.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/baritone/process/elytra/pathfinder/Chunk.java b/src/main/java/baritone/process/elytra/pathfinder/Chunk.java index 3aff540274..98284350b6 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/Chunk.java +++ b/src/main/java/baritone/process/elytra/pathfinder/Chunk.java @@ -29,9 +29,11 @@ * asks of every x16 and x8 it enters, whether a cube that big is empty, is a bit test rather than a * scan of 64 or 8 longs. *

- * Reads and writes are plain (not synchronized), as they were in the native library: a reader - * that races a writer may see a partly written section, which is the same as before, and nothing - * worse can happen. + * Nothing here is synchronized, as nothing was in the native library, and nothing needs to be: + * the callers keep writers and readers apart. NetherPathfinderContext holds its write lock to pack + * a chunk, apply a block update, cull the table or run a search that generates terrain, and its + * read lock for every other search and for every ray, and a chunk generated or read from a region + * file is complete before the table publishes it. */ public final class Chunk { @@ -143,7 +145,6 @@ public void setBlock(int x, int y, int z, boolean solid) { final long mask = 1L << (((off & 7) << 3) + bitIndex(x, y, z)); final int x8 = x8Index(x, y, z); if (solid) { - // the x8 is marked before its block is set, so a reader racing this write never skips a block it can see this.filled[y >> 4] |= 1 << x8; s[off >>> 3] |= mask; } else { From 17e1d5e23c92090f39ee3bf59d9801658d3517b7 Mon Sep 17 00:00:00 2001 From: Mattias Rask <231212847+0Mattias@users.noreply.github.com> Date: Wed, 16 Sep 2026 19:00:00 -0400 Subject: [PATCH 10/15] Store the x8 summary as a byte per section ZacSharp's review. A section has eight x8 cubes, so its summary needs eight bits, and the class comment already called it a byte; the int array was an oversight. filled(y) still returns an int, the byte masked to its eight bits, so the raytracer's tests of it are unchanged. Co-Authored-By: Claude Fable 5.1 --- .../java/baritone/process/elytra/pathfinder/Chunk.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/baritone/process/elytra/pathfinder/Chunk.java b/src/main/java/baritone/process/elytra/pathfinder/Chunk.java index 98284350b6..4f5927c45c 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/Chunk.java +++ b/src/main/java/baritone/process/elytra/pathfinder/Chunk.java @@ -50,7 +50,7 @@ public final class Chunk { private final long[][] sections = new long[SECTIONS][]; /** Bit i of entry s says whether x8 cube i of section s holds a block; a section that is null has 0. */ - private final int[] filled = new int[SECTIONS]; + private final byte[] filled = new byte[SECTIONS]; private final boolean shared; public Chunk() { @@ -63,7 +63,7 @@ private Chunk(boolean solid) { for (int i = 0; i < SECTIONS; i++) { this.sections[i] = new long[SECTION_LONGS]; Arrays.fill(this.sections[i], -1L); - this.filled[i] = 0xFF; + this.filled[i] = (byte) 0xFF; } } } @@ -112,7 +112,7 @@ long[] section(int y) { /** Which x8 cubes of the section holding y hold a block, one bit each in x8Index order; 0 outside the chunk. */ int filled(int y) { final int i = y >> 4; - return i >= 0 && i < SECTIONS ? this.filled[i] : 0; + return i >= 0 && i < SECTIONS ? this.filled[i] & 0xFF : 0; } /** Coordinates are chunk relative: x and z in 0..15, y in 0..383. */ @@ -169,7 +169,7 @@ public void fillSection(int section, boolean solid) { if (s == null) { s = this.sections[section] = new long[SECTION_LONGS]; } - this.filled[section] = 0xFF; + this.filled[section] = (byte) 0xFF; Arrays.fill(s, -1L); } From cb2a12b2c3c365c5618546570e32be3bd01b8ea5 Mon Sep 17 00:00:00 2001 From: Mattias Rask <231212847+0Mattias@users.noreply.github.com> Date: Wed, 16 Sep 2026 19:02:27 -0400 Subject: [PATCH 11/15] Remove insertChunkData ZacSharp found it unused and babbaj said to remove it. It was the native library's entry point for a chunk as an array of booleans; the game's chunks come in through allocateAndInsertChunk and are packed straight into the Chunk, and only a test called this. dimensionHeight served only its length check and goes with it. Co-Authored-By: Claude Fable 5.1 --- .../elytra/pathfinder/NetherPathfinder.java | 22 ------------------- .../process/elytra/pathfinder/ChunkTest.java | 19 ---------------- 2 files changed, 41 deletions(-) diff --git a/src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java b/src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java index 392d26789c..5babd90310 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java +++ b/src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java @@ -125,10 +125,6 @@ static long key(int x, int z) { return (((long) x << 32) | (z & 0xFFFFFFFFL)) * 0x9E3779B97F4A7C15L; } - static int dimensionHeight(Dimension dimension) { - return dimension == Dimension.OVERWORLD ? 384 : 256; - } - private static boolean inBounds(int y) { return y >= 0 && y < Chunk.HEIGHT; } @@ -136,24 +132,6 @@ private static boolean inBounds(int y) { // ---- the chunk table ---- - /** - * Inserts a chunk from the game, replacing any chunk at that position. {@code data} has one - * boolean per block, indexed {@code y << 8 | z << 4 | x}, 16 * 16 * 256 of them (384 in the overworld). - */ - public void insertChunkData(int chunkX, int chunkZ, boolean[] data) { - final int blocksInChunk = 16 * 16 * dimensionHeight(this.dimension); - if (data.length != blocksInChunk) { - throw new IllegalArgumentException(this.dimension == Dimension.OVERWORLD ? "input is not 16 * 16 * 384 elements" : "input is not 16 * 16 * 256 elements"); - } - final Chunk chunk = new Chunk(); - for (int i = 0; i < blocksInChunk; i++) { - if (data[i]) { - chunk.setBlock(i & 0xF, i >> 8, (i >> 4) & 0xF, true); - } - } - this.chunks.put(key(chunkX, chunkZ), new Entry(STATE_FROM_CALLER, chunk, chunkX, chunkZ)); - } - /** Inserts a new, empty chunk from the game at (x, z), replacing any chunk there, and returns it to be filled. */ public Chunk allocateAndInsertChunk(int x, int z) { final Chunk chunk = new Chunk(); diff --git a/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java b/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java index 4665c11b6e..d99b0881f2 100644 --- a/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java +++ b/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java @@ -105,25 +105,6 @@ public void fillSection() { assertFalse("the x8 still has its other blocks", chunk.isEmptyX8(15, 79, 15)); } - @Test - public void insertChunkDataUsesTheBlockStateContainerIndex() { - final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.Dimension.NETHER, 128); - final boolean[] data = new boolean[16 * 16 * 256]; - data[(77 << 8) | (5 << 4) | 12] = true; - ctx.insertChunkData(-3, 9, data); - final Chunk chunk = ctx.getChunk(-3, 9); - assertNotNull(chunk); - assertTrue(chunk.isSolid(12, 77, 5)); - assertFalse(chunk.isSolid(5, 77, 12)); - assertTrue(ctx.hasChunkFromCaller(-3, 9)); - try { - ctx.insertChunkData(0, 0, new boolean[10]); - throw new AssertionError("expected IllegalArgumentException"); - } catch (IllegalArgumentException expected) { - // good - } - } - @Test public void tableOperations() { final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.Dimension.NETHER, 128); From 41ae7b9ea64b3090000d06aef0fed7caa34804f9 Mon Sep 17 00:00:00 2001 From: Mattias Rask <231212847+0Mattias@users.noreply.github.com> Date: Wed, 16 Sep 2026 19:02:27 -0400 Subject: [PATCH 12/15] Make a chunk's origin a final boolean ZacSharp asked whether the chunk state should be an enum or a boolean, and babbaj noticed that setChunkState, its only writer, is never called, so the field should be final and not volatile. It is now a final boolean, fromCaller, in place of the two int constants, and setChunkState goes; the test helper that marked generated chunks as the game's now generates them itself and copies the blocks into chunks it inserts as the game's. Co-Authored-By: Claude Fable 5.1 --- .../elytra/pathfinder/NetherPathfinder.java | 32 ++++++------------- .../process/elytra/pathfinder/PathFinder.java | 22 ++++++------- .../process/elytra/pathfinder/ChunkTest.java | 13 +++----- .../process/elytra/pathfinder/Oracle.java | 24 ++++++++++++-- 4 files changed, 47 insertions(+), 44 deletions(-) diff --git a/src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java b/src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java index 5babd90310..93e806913f 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java +++ b/src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java @@ -48,25 +48,23 @@ public enum Dimension { OVERWORLD, NETHER, END } - static final int STATE_FROM_CALLER = 0; - static final int STATE_FAKE = 1; // could be generated or just air - /** A chunk in the table, where it is, and whether it came from the game or was made up. */ static final class Entry { final Chunk chunk; final int x; final int z; - volatile int state; + /** true for a chunk the game gave; false for one generated from the seed or assumed to be air */ + final boolean fromCaller; - Entry(int state, Chunk chunk, int x, int z) { - this.state = state; + Entry(boolean fromCaller, Chunk chunk, int x, int z) { + this.fromCaller = fromCaller; this.chunk = chunk; this.x = x; this.z = z; } } - private static final Entry AIR_ENTRY = new Entry(STATE_FAKE, Chunk.AIR, 0, 0); + private static final Entry AIR_ENTRY = new Entry(false, Chunk.AIR, 0, 0); private final ConcurrentHashMap chunks = new ConcurrentHashMap<>(); private final Set checkedRegions = ConcurrentHashMap.newKeySet(); @@ -135,7 +133,7 @@ private static boolean inBounds(int y) { /** Inserts a new, empty chunk from the game at (x, z), replacing any chunk there, and returns it to be filled. */ public Chunk allocateAndInsertChunk(int x, int z) { final Chunk chunk = new Chunk(); - this.chunks.put(key(x, z), new Entry(STATE_FROM_CALLER, chunk, x, z)); + this.chunks.put(key(x, z), new Entry(true, chunk, x, z)); return chunk; } @@ -151,19 +149,9 @@ public Chunk getChunk(int x, int z) { return e != null ? e.chunk : null; } - /** Marks the chunk at (x, z) as from the game or as made up. Returns true if the chunk existed and the change was made. */ - public boolean setChunkState(int x, int z, boolean fromCaller) { - final Entry e = this.chunks.get(key(x, z)); - if (e == null) { - return false; - } - e.state = fromCaller ? STATE_FROM_CALLER : STATE_FAKE; - return true; - } - public boolean hasChunkFromCaller(int x, int z) { final Entry e = this.chunks.get(key(x, z)); - return e != null && e.state == STATE_FROM_CALLER; + return e != null && e.fromCaller; } /** Whether the table holds chunk (x, z), whichever way it got there. */ @@ -244,7 +232,7 @@ Entry getChunkOrAir(int cx, int cz) { Chunk getRealChunkOrDefault(int cx, int cz, boolean solid) { final Entry e = this.chunks.get(key(cx, cz)); - if (e == null || e.state != STATE_FROM_CALLER) { + if (e == null || !e.fromCaller) { return solid ? Chunk.SOLID : Chunk.AIR; } return e.chunk; @@ -258,7 +246,7 @@ Chunk getOrGenChunk(int cx, int cz) { return e.chunk; } final Chunk chunk = this.generator.generateChunk(cx, cz); - final Entry previous = this.chunks.putIfAbsent(key, new Entry(STATE_FAKE, chunk, cx, cz)); + final Entry previous = this.chunks.putIfAbsent(key, new Entry(false, chunk, cx, cz)); // someone else generated this chunk while we were generating it return previous != null ? previous.chunk : chunk; } @@ -285,7 +273,7 @@ long tryLoadRegion(int cx, int cz) { } final long t1 = System.currentTimeMillis(); final boolean read = BaritoneRegion.load(this.baritoneCache, regionX, regionZ, - (x, z, chunk) -> this.chunks.putIfAbsent(key(x, z), new Entry(STATE_FROM_CALLER, chunk, x, z))); + (x, z, chunk) -> this.chunks.putIfAbsent(key(x, z), new Entry(true, chunk, x, z))); return read ? System.currentTimeMillis() - t1 : 0; } } diff --git a/src/main/java/baritone/process/elytra/pathfinder/PathFinder.java b/src/main/java/baritone/process/elytra/pathfinder/PathFinder.java index a253a65258..1ee10ccd64 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/PathFinder.java +++ b/src/main/java/baritone/process/elytra/pathfinder/PathFinder.java @@ -116,7 +116,7 @@ private static Path bestPathSoFar(PathNode end, BlockPos startPos, BlockPos goal // ---- neighbour expansion ---- private interface Callback { - void accept(NodePos neighbor, Chunk chunk, int state); + void accept(NodePos neighbor, Chunk chunk, boolean fromCaller); } /** @@ -148,14 +148,14 @@ private static BlockPos[] neighborCubes(Direction face, Size sz, BlockPos origin } } - private static void forEachNeighborInCube(Chunk chunk, int state, NodePos neighborNode, Direction face, Size size, boolean sizeChange, Size minSize, Callback callback) { + private static void forEachNeighborInCube(Chunk chunk, boolean fromCaller, NodePos neighborNode, Direction face, Size size, boolean sizeChange, Size minSize, Callback callback) { if (sizeChange) { - callback.accept(neighborNode, chunk, state); + callback.accept(neighborNode, chunk, fromCaller); return; } final BlockPos pos = neighborNode.absolutePosZero(); if (chunk.isEmpty(size, pos.getX() & 15, pos.getY(), pos.getZ() & 15)) { - callback.accept(neighborNode, chunk, state); + callback.accept(neighborNode, chunk, fromCaller); return; } if (size != Size.X1) { @@ -163,20 +163,20 @@ private static void forEachNeighborInCube(Chunk chunk, int state, NodePos neighb // Don't shrink cubes to X1 because they suck and make the path try to squeeze through small areas if (nextSize.ordinal() < minSize.ordinal()) return; for (BlockPos subCube : neighborCubes(face, nextSize, pos)) { - forEachNeighborInCube(chunk, state, new NodePos(nextSize, subCube), face, nextSize, false, minSize, callback); + forEachNeighborInCube(chunk, fromCaller, new NodePos(nextSize, subCube), face, nextSize, false, minSize, callback); } } } /** Grows the neighbour to the largest empty cube it is in, then iterates what is on the face. */ - private static void growThenIterate(Chunk chunk, int state, NodePos pos, Direction face, Size minSize, Callback callback) { + private static void growThenIterate(Chunk chunk, boolean fromCaller, NodePos pos, Direction face, Size minSize, Callback callback) { final Size originalSize = pos.size; final BlockPos bpos = pos.absolutePosZero(); final int lx = bpos.getX() & 15; final int lz = bpos.getZ() & 15; for (Size s = originalSize; ; s = s.larger()) { if (s == Size.X16 || !chunk.isEmpty(s.larger(), lx, bpos.getY(), lz)) { - forEachNeighborInCube(chunk, state, new NodePos(s, bpos), face, s, originalSize != s, minSize, callback); + forEachNeighborInCube(chunk, fromCaller, new NodePos(s, bpos), face, s, originalSize != s, minSize, callback); return; } } @@ -202,9 +202,9 @@ private static final class Search implements Callback { } @Override - public void accept(NodePos neighborPos, Chunk chunk, int state) { + public void accept(NodePos neighborPos, Chunk chunk, boolean fromCaller) { final PathNode neighborNode = getNodeAtPosition(this.map, neighborPos, this.goalCenter); - final double cost = state == NetherPathfinder.STATE_FROM_CALLER ? 1 : this.fakeChunkCost; + final double cost = fromCaller ? 1 : this.fakeChunkCost; final double tentativeCost = this.currentNode.cost + cost; if (neighborNode.cost - tentativeCost > MIN_IMPROVEMENT) { neighborNode.previous = this.currentNode; @@ -284,7 +284,7 @@ static Path findPathSegment(NetherPathfinder ctx, NodePos start, NodePos goal, b final int cx = (bpos.getX() >> 4); final int cz = (bpos.getZ() >> 4); final NetherPathfinder.Entry currentChunk = ctx.getChunkOrAir(cx, cz); - if (currentChunk.state != NetherPathfinder.STATE_FROM_CALLER) { + if (!currentChunk.fromCaller) { fakeChunkVisits++; } else { fakeChunkVisits = 0; @@ -306,7 +306,7 @@ static Path findPathSegment(NetherPathfinder ctx, NodePos start, NodePos goal, b final int neighborCz = (origin.getZ() >> 4); timeDoingIO += ctx.tryLoadRegion(neighborCx, neighborCz); final NetherPathfinder.Entry entry = neighborCx == cx && neighborCz == cz ? currentChunk : ctx.getChunkOrAir(neighborCx, neighborCz); - growThenIterate(entry.chunk, entry.state, neighborNodePos, face, minSize, s); + growThenIterate(entry.chunk, entry.fromCaller, neighborNodePos, face, minSize, s); } } return bestPathSoFar(s.bestSoFar, startCenter, goalCenter); diff --git a/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java b/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java index d99b0881f2..18556f9061 100644 --- a/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java +++ b/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java @@ -111,16 +111,12 @@ public void tableOperations() { assertSame(Chunk.AIR, ctx.getChunkOrDefault(1, 1, false)); assertSame(Chunk.SOLID, ctx.getChunkOrDefault(1, 1, true)); assertNull(ctx.getChunk(1, 1)); - assertFalse(ctx.setChunkState(1, 1, true)); final Chunk chunk = ctx.allocateAndInsertChunk(1, 1); assertSame(chunk, ctx.getChunk(1, 1)); assertSame(chunk, ctx.getChunkOrDefault(1, 1, true)); assertTrue(ctx.hasChunkFromCaller(1, 1)); - assertTrue(ctx.setChunkState(1, 1, false)); - assertFalse(ctx.hasChunkFromCaller(1, 1)); - // a fake chunk is a default to the search's real-chunk lookup - assertSame(Chunk.SOLID, ctx.getRealChunkOrDefault(1, 1, true)); + assertSame(chunk, ctx.getRealChunkOrDefault(1, 1, true)); assertSame(chunk, ctx.getChunkOrAir(1, 1).chunk); // replacing keeps the new one @@ -138,14 +134,15 @@ public void tableOperations() { } @Test - public void generatedChunksAreFakeUntilMarked() { + public void generatedChunksAreFake() { final NetherPathfinder ctx = new NetherPathfinder(Oracle.SEED, null, NetherPathfinder.Dimension.NETHER, 128); final Chunk chunk = ctx.getOrGenChunk(0, 0); assertSame(chunk, ctx.getOrGenChunk(0, 0)); assertFalse(ctx.hasChunkFromCaller(0, 0)); + // a fake chunk is a default to the search's real-chunk lookup, and itself to the rest + assertSame(Chunk.SOLID, ctx.getRealChunkOrDefault(0, 0, true)); assertSame(Chunk.AIR, ctx.getRealChunkFromCacheOrFakeChunkMaybeGen(0, 0, NetherPathfinder.CacheMiss.AIR)); assertSame(chunk, ctx.getRealChunkFromCacheOrFakeChunkMaybeGen(0, 0, NetherPathfinder.CacheMiss.GENERATE)); - ctx.setChunkState(0, 0, true); - assertSame(chunk, ctx.getRealChunkFromCacheOrFakeChunkMaybeGen(0, 0, NetherPathfinder.CacheMiss.AIR)); + assertSame(chunk, ctx.getChunkOrAir(0, 0).chunk); } } diff --git a/src/test/java/baritone/process/elytra/pathfinder/Oracle.java b/src/test/java/baritone/process/elytra/pathfinder/Oracle.java index 1822c34547..980ba3aedb 100644 --- a/src/test/java/baritone/process/elytra/pathfinder/Oracle.java +++ b/src/test/java/baritone/process/elytra/pathfinder/Oracle.java @@ -86,15 +86,33 @@ static List rays() throws IOException { return out; } - /** A context over the generated chunks x, z in 0..side-1, marked as if the game had given them. */ + /** A context over the generated chunks x, z in 0..side-1, inserted as if the game had given them. */ static NetherPathfinder generatedWorld(int side) { final NetherPathfinder ctx = new NetherPathfinder(SEED, null, NetherPathfinder.Dimension.NETHER, 128); + final ChunkGeneratorHell generator = ChunkGeneratorHell.fromSeed(SEED); for (int x = 0; x < side; x++) { for (int z = 0; z < side; z++) { - ctx.getOrGenChunk(x, z); - ctx.setChunkState(x, z, true); + copy(generator.generateChunk(x, z), ctx.allocateAndInsertChunk(x, z)); } } return ctx; } + + /** Sets in {@code to} every block that is set in {@code from}. */ + private static void copy(Chunk from, Chunk to) { + for (int section = 0; section < Chunk.SECTIONS; section++) { + if (from.section(section << 4) == null) { + continue; + } + for (int y = section << 4; y < (section + 1) << 4; y++) { + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + if (from.isSolid(x, y, z)) { + to.setBlock(x, y, z, true); + } + } + } + } + } + } } From b018f34fded0bbf23d90a293552aa3660baecc7f Mon Sep 17 00:00:00 2001 From: Mattias Rask <231212847+0Mattias@users.noreply.github.com> Date: Wed, 16 Sep 2026 19:02:30 -0400 Subject: [PATCH 13/15] Store a NodePos as three ints ZacSharp's review. The BlockPos was private and only ever read back through its three getters, so the node holds the coordinates itself and allocates one object fewer per node the search creates. Co-Authored-By: Claude Fable 5.1 --- .../process/elytra/pathfinder/NodePos.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/java/baritone/process/elytra/pathfinder/NodePos.java b/src/main/java/baritone/process/elytra/pathfinder/NodePos.java index 081b3f9d1d..5b5a216543 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/NodePos.java +++ b/src/main/java/baritone/process/elytra/pathfinder/NodePos.java @@ -24,17 +24,21 @@ final class NodePos { final Size size; /** absolute position divided by the width */ - private final BlockPos pos; + private final int x; + private final int y; + private final int z; NodePos(Size size, BlockPos approxPosition) { this.size = size; final int shift = size.shift(); - this.pos = new BlockPos(approxPosition.getX() >> shift, approxPosition.getY() >> shift, approxPosition.getZ() >> shift); + this.x = approxPosition.getX() >> shift; + this.y = approxPosition.getY() >> shift; + this.z = approxPosition.getZ() >> shift; } BlockPos absolutePosZero() { final int shift = this.size.shift(); - return new BlockPos(this.pos.getX() << shift, this.pos.getY() << shift, this.pos.getZ() << shift); + return new BlockPos(this.x << shift, this.y << shift, this.z << shift); } BlockPos absolutePosCenter() { @@ -48,16 +52,16 @@ public boolean equals(Object o) { return false; } final NodePos n = (NodePos) o; - return n.size == this.size && n.pos.equals(this.pos); + return n.size == this.size && n.x == this.x && n.y == this.y && n.z == this.z; } @Override public int hashCode() { long hash = 3241; hash = 6406146L * hash + this.size.ordinal(); - hash = 3457689L * hash + this.pos.getX(); - hash = 8734625L * hash + this.pos.getY(); - hash = 2873465L * hash + this.pos.getZ(); + hash = 3457689L * hash + this.x; + hash = 8734625L * hash + this.y; + hash = 2873465L * hash + this.z; return (int) (hash ^ (hash >>> 32)); } } From 921e1badf49ea3e3f921205c486f62158245966a Mon Sep 17 00:00:00 2001 From: Mattias Rask <231212847+0Mattias@users.noreply.github.com> Date: Wed, 16 Sep 2026 19:02:30 -0400 Subject: [PATCH 14/15] Keep the chunks generated around in a LongOpenHashSet ZacSharp's review, babbaj agreed. The set of chunks whose neighbours the search has generated was a HashSet of boxed Longs; fastutil's long set holds the keys unboxed. The keys are what NetherPathfinder.key gives, as before. Co-Authored-By: Claude Fable 5.1 --- .../java/baritone/process/elytra/pathfinder/PathFinder.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/baritone/process/elytra/pathfinder/PathFinder.java b/src/main/java/baritone/process/elytra/pathfinder/PathFinder.java index 1ee10ccd64..7f3f68c634 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/PathFinder.java +++ b/src/main/java/baritone/process/elytra/pathfinder/PathFinder.java @@ -17,6 +17,8 @@ package baritone.process.elytra.pathfinder; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import java.util.ArrayDeque; @@ -241,7 +243,7 @@ static Path findPathSegment(NetherPathfinder ctx, NodePos start, NodePos goal, b final BlockPos startCenter = start.absolutePosCenter(); final Search s = new Search(goalCenter, startCenter, fakeChunkCost); - final Set doneFull = new HashSet<>(); + final LongSet doneFull = new LongOpenHashSet(); final PathNode startNode = getNodeAtPosition(s.map, start, goal.absolutePosZero()); final BlockPos startZero = start.absolutePosZero(); From 63a5813b2b2c396d92fb156b7309dd253b73a4a0 Mon Sep 17 00:00:00 2001 From: Mattias Rask <231212847+0Mattias@users.noreply.github.com> Date: Wed, 16 Sep 2026 19:02:30 -0400 Subject: [PATCH 15/15] Say which Minecraft classes the package uses ZacSharp's review, babbaj agreed the comment was outdated: the package took Minecraft's BlockPos, Direction, Vec3 and Mth in the first review round and the comment still said it used nothing of Minecraft's. Co-Authored-By: Claude Fable 5.1 --- .../baritone/process/elytra/pathfinder/package-info.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/baritone/process/elytra/pathfinder/package-info.java b/src/main/java/baritone/process/elytra/pathfinder/package-info.java index e9e898ff7b..3a8c121205 100644 --- a/src/main/java/baritone/process/elytra/pathfinder/package-info.java +++ b/src/main/java/baritone/process/elytra/pathfinder/package-info.java @@ -20,9 +20,10 @@ * (https://github.com/babbaj/nether-pathfinder), which Baritone loaded as a native library * through JNI, in plain Java. Each class is the C++ file of the same name, ported one for one, * and the tests hold the terrain generator and the raytracer to the native library's own - * recorded answers over a few hundred generated chunks and a few thousand rays. Nothing in here - * refers to Minecraft; the seam is {@link baritone.process.elytra.NetherPathfinderContext}, - * which packs the game's chunks into {@link baritone.process.elytra.pathfinder.Chunk}s and asks + * recorded answers over a few hundred generated chunks and a few thousand rays. The only + * Minecraft in here is BlockPos, Direction, Vec3 and Mth; the seam is + * {@link baritone.process.elytra.NetherPathfinderContext}, which packs the game's chunks into + * {@link baritone.process.elytra.pathfinder.Chunk}s and asks * {@link baritone.process.elytra.pathfinder.NetherPathfinder} for paths and lines of sight. * The port was written for babbaj/nether-pathfinder#31. */