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..ceacfb9392 100644 --- a/src/api/java/baritone/api/Settings.java +++ b/src/api/java/baritone/api/Settings.java @@ -1549,10 +1549,6 @@ public final class Settings { */ public final Setting elytraChatSpam = new Setting<>(false); - /** - * May reduce memory usage by using a custom allocator for pathfinding - */ - 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..aac4e108a2 100644 --- a/src/main/java/baritone/cache/CachedRegion.java +++ b/src/main/java/baritone/cache/CachedRegion.java @@ -44,7 +44,7 @@ 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 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/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..6c21c89290 100644 --- a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java +++ b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java @@ -19,10 +19,11 @@ 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.process.elytra.pathfinder.Raytracer; 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 +35,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 +45,22 @@ 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(); @@ -85,28 +73,28 @@ 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 = 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.hasChunkFromCaller(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); + boolean isSolid = !pair.second().isAir(); + 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 @@ -198,9 +185,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 NetherPathfinder.isVisible(this.context, 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; } /** @@ -212,9 +197,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 NetherPathfinder.isVisible(this.context, 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) { @@ -229,16 +212,37 @@ 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; + for (int i = 0; i < count; i++) { + if (!clear(src, dst, i)) { + return false; + } + } + return true; case Visibility.NONE: - return NetherPathfinder.isVisibleMulti(this.context, 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 NetherPathfinder.isVisibleMulti(this.context, 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"); @@ -249,7 +253,16 @@ 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); + 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) { @@ -257,7 +270,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 +286,7 @@ public void destroy() { e.printStackTrace(); } - NetherPathfinder.freeContext(this.context); + this.context.close(); } public long getSeed() { @@ -296,7 +309,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 +334,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 +356,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 +367,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..cb44fcc4dd 100644 --- a/src/main/java/baritone/process/elytra/UnpackedSegment.java +++ b/src/main/java/baritone/process/elytra/UnpackedSegment.java @@ -18,9 +18,8 @@ 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; 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 new file mode 100644 index 0000000000..f0961a0721 --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/BaritoneRegion.java @@ -0,0 +1,123 @@ +/* + * 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 baritone.cache.CachedRegion; +import java.io.DataInputStream; +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 { + + /** 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/cache}, + * or null if the directory is not one. + */ + 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 null; + } + } + + 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 NetherPathfinder.Dimension dim = dimensionOf(dir); + if (dim == null) { + 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, NetherPathfinder.Dimension dimension, ChunkSink sink) throws IOException { + final DataInputStream in = new DataInputStream(raw); + final int magic = in.readInt(); + 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 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); + } + + /** + * 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] >> (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/Chunk.java b/src/main/java/baritone/process/elytra/pathfinder/Chunk.java new file mode 100644 index 0000000000..4f5927c45c --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/Chunk.java @@ -0,0 +1,221 @@ +/* + * 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 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 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. + *

+ * 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 { + + public static final int HEIGHT = 384; + 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; + + /** 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[][] 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 byte[] filled = new byte[SECTIONS]; + private final boolean shared; + + public Chunk() { + this.shared = false; + } + + private Chunk(boolean solid) { + this.shared = true; + if (solid) { + for (int i = 0; i < SECTIONS; i++) { + this.sections[i] = new long[SECTION_LONGS]; + Arrays.fill(this.sections[i], -1L); + this.filled[i] = (byte) 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 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 section. */ + 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) { + final int i = y >> 4; + return i >= 0 && i < SECTIONS ? this.sections[i] : null; + } + + /** 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] & 0xFF : 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.sections[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. 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) { + if (this.shared) { + throw new UnsupportedOperationException("the shared air and solid chunks are read only"); + } + long[] s = this.sections[y >> 4]; + if (s == null) { + if (!solid) { + return; + } + 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)); + final int x8 = x8Index(x, y, z); + if (solid) { + 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.sections[section] = null; + return; + } + long[] s = this.sections[section]; + if (s == null) { + s = this.sections[section] = new long[SECTION_LONGS]; + } + this.filled[section] = (byte) 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.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.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[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 < SECTION_LONGS; j++) { + long v = s[j]; + for (int k = 0; k < 8; k++) { + out[(i * SECTION_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/NetherPathfinder.java b/src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java new file mode 100644 index 0000000000..93e806913f --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java @@ -0,0 +1,279 @@ +/* + * 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; +import java.util.Objects; +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 { + + /** 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 enum Dimension { + OVERWORLD, NETHER, END + } + + /** 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; + /** true for a chunk the game gave; false for one generated from the seed or assumed to be air */ + final boolean fromCaller; + + 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(false, 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 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 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, 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)"); + } + this.seed = seed; + this.baritoneCache = baritoneCacheDirCanBeNull; + this.dimension = dimension; + this.maxHeight = maxHeight; + this.generator = ChunkGeneratorHell.fromSeed(seed); + } + + public long getSeed() { + return this.seed; + } + + public Dimension getDimension() { + return this.dimension; + } + + public int getMaxHeight() { + return this.maxHeight; + } + + @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; + } + + private static boolean inBounds(int y) { + return y >= 0 && y < Chunk.HEIGHT; + } + + + // ---- the chunk table ---- + + /** 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(true, 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; + } + + public boolean hasChunkFromCaller(int x, int z) { + final Entry e = this.chunks.get(key(x, z)); + return e != null && e.fromCaller; + } + + /** 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"); + } + // 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); + } + } + + /** 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); + } + + // ---- 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.fromCaller) { + 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(false, 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, CacheMiss fakeChunkMode) { + if (fakeChunkMode == CacheMiss.GENERATE) { + return getOrGenChunk(cx, cz); + } + 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 milliseconds 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.currentTimeMillis(); + final boolean read = BaritoneRegion.load(this.baritoneCache, regionX, regionZ, + (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/NodePos.java b/src/main/java/baritone/process/elytra/pathfinder/NodePos.java new file mode 100644 index 0000000000..5b5a216543 --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/NodePos.java @@ -0,0 +1,67 @@ +/* + * 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; + +/** 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 int x; + private final int y; + private final int z; + + NodePos(Size size, BlockPos approxPosition) { + this.size = size; + final int shift = size.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.x << shift, this.y << shift, this.z << shift); + } + + BlockPos absolutePosCenter() { + final int half = this.size.width() / 2; + return this.absolutePosZero().offset(half, half, half); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof NodePos)) { + return false; + } + final NodePos n = (NodePos) o; + 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.x; + hash = 8734625L * hash + this.y; + hash = 2873465L * hash + this.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..7f3f68c634 --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/PathFinder.java @@ -0,0 +1,412 @@ +/* + * 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 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; +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; + // 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 { + 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.getY() >= 0 && pos.getY() < worldHeight; + } + + private static boolean closeToGoal(NodePos node, BlockPos goal) { + 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.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.distSqr(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, boolean fromCaller); + } + + /** + * 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(Direction 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.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.above(size), corner.east(size).above(size)}; + case SOUTH: + corner = origin; + 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.above(size), corner.south(size).above(size)}; + default: // WEST + corner = origin.east(size); + return new BlockPos[]{corner, corner.south(size), corner.above(size), corner.south(size).above(size)}; + } + } + + 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, fromCaller); + return; + } + final BlockPos pos = neighborNode.absolutePosZero(); + if (chunk.isEmpty(size, pos.getX() & 15, pos.getY(), pos.getZ() & 15)) { + callback.accept(neighborNode, chunk, fromCaller); + 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, 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, 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, fromCaller, 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, boolean fromCaller) { + final PathNode neighborNode = getNodeAtPosition(this.map, neighborPos, this.goalCenter); + final double cost = fromCaller ? 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.distSqr(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 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(); + + final Search s = new Search(goalCenter, startCenter, fakeChunkCost); + final LongSet doneFull = new LongOpenHashSet(); + + final PathNode startNode = getNodeAtPosition(s.map, start, goal.absolutePosZero()); + final BlockPos startZero = start.absolutePosZero(); + ctx.tryLoadRegion((startZero.getX() >> 4), (startZero.getZ() >> 4)); + startNode.cost = 0; + startNode.combinedCost = startNode.estimatedCostToGoal; + s.openSet.insert(startNode); + ctx.getRealChunkFromCacheOrFakeChunkMaybeGen((startZero.getX() >> 4), (startZero.getZ() >> 4), fakeChunkMode); + + s.bestSoFar = startNode; + s.bestHeuristicSoFar = startNode.estimatedCostToGoal; + + 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; // 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.currentTimeMillis() - 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.getX() >> 4); + final int cz = (bpos.getZ() >> 4); + final NetherPathfinder.Entry currentChunk = ctx.getChunkOrAir(cx, cz); + if (!currentChunk.fromCaller) { + 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 (Direction face : ALL_FACES) { + final NodePos neighborNodePos = new NodePos(size, bpos.relative(face, size.width())); + final BlockPos origin = neighborNodePos.absolutePosZero(); + if (face == Direction.UP || face == Direction.DOWN) { + if (!isInBounds(ctx.maxHeight, origin)) continue; + } + 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.fromCaller, 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.getX() >> 4), (end.getZ() >> 4), 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.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.above(w))); + push(queue, visited, new NodePos(size, blockPos.below(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..e5789a040a --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/PathNode.java @@ -0,0 +1,45 @@ +/* + * 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; + +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 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 new file mode 100644 index 0000000000..aea4f3df20 --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/PathSegment.java @@ -0,0 +1,31 @@ +/* + * 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 net.minecraft.core.BlockPos; + +public class PathSegment { + public final boolean finished; + public final List blocks; + + public PathSegment(boolean finished, List blocks) { + this.finished = finished; + 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 new file mode 100644 index 0000000000..86c4e9644d --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/Raytracer.java @@ -0,0 +1,369 @@ +/* + * 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; +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" + * (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}. + */ +public 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[] 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 section[off >>> 3] == 0; + default: return Chunk.byteAt(section, off) == 0; + } + } + + /** + * 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[] 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; + } + if (tx0 >= targetLen || ty0 >= targetLen || tz0 >= targetLen) { + return Double.POSITIVE_INFINITY; + } + if (level == 0) { // leaf + if (((Chunk.byteAt(section, 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 (section == null || emptyNode(section, 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 { + // 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: + 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: + 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: + 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: + 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: + 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: + 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: + 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: + 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; + } + } 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[] section, 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, section, 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 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. + */ + 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; + 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 = 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 null; + } + return new Vec3(fx, fy, fz); + } + 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 = 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[] 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, section, chunk.filled(ny), step); + if (result == FINISHED) { + return null; + } + if (result == HIT) { + 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 new Vec3(fx + dx * step.hitLen, fy + dy * step.hitLen, fz + dz * step.hitLen); + } + 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.getX(), fromBlock.getY(), fromBlock.getZ(), currentBlock.getX(), currentBlock.getY(), currentBlock.getZ(), NetherPathfinder.CacheMiss.GENERATE) != null) { + 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..3a8c121205 --- /dev/null +++ b/src/main/java/baritone/process/elytra/pathfinder/package-info.java @@ -0,0 +1,30 @@ +/* + * 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. 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. + */ +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..18b6a79960 --- /dev/null +++ b/src/test/java/baritone/process/elytra/pathfinder/BaritoneRegionTest.java @@ -0,0 +1,140 @@ +/* + * 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.nio.file.Files; +import java.nio.file.Path; +import java.util.BitSet; +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), 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)) { + 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 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); + } + } + } + } + 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]; + 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)); + 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)); + } + + @Test + public void aSearchLoadsTheRegionItReaches() throws Exception { + final Path dir = Files.createTempDirectory("np-region"); + final Path regions = dir.resolve("the_nether_128").resolve("cache"); + 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.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 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/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 new file mode 100644 index 0000000000..b4eccb941b --- /dev/null +++ b/src/test/java/baritone/process/elytra/pathfinder/Bench.java @@ -0,0 +1,111 @@ +/* + * 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; +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(); + 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++; + 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.blocks.size(); 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/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/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..18556f9061 --- /dev/null +++ b/src/test/java/baritone/process/elytra/pathfinder/ChunkTest.java @@ -0,0 +1,148 @@ +/* + * 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.section(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)); + + // 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); + 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.section(64)); // the section 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 x8 still has its other blocks", chunk.isEmptyX8(15, 79, 15)); + } + + @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)); + + 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)); + assertSame(chunk, 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 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)); + assertSame(chunk, ctx.getChunkOrAir(0, 0).chunk); + } +} 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..5c5a66992e --- /dev/null +++ b/src/test/java/baritone/process/elytra/pathfinder/DegenerateRaysTest.java @@ -0,0 +1,142 @@ +/* + * 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.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; + +/** + * 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 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 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(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(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(visible(ctx, 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 { + 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 + } + } + } + // both ends infinite: the length is NaN rather than infinite + try { + 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 + } + } + + @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 = visible(ctx, SOLID, ox, oy, oz, 8.0, 68.0, 8.0); + final double nudge = 1e-6; + 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++; + } + } + } + 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 (visible(ctx, 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 aPointAmongOtherRaysIsAnsweredLikeAnyOther() { + final NetherPathfinder ctx = world(); + // 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 new file mode 100644 index 0000000000..980ba3aedb --- /dev/null +++ b/src/test/java/baritone/process/elytra/pathfinder/Oracle.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 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 java/oracle/oracle.cpp. */ +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, 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++) { + 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); + } + } + } + } + } + } +} 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..bc84a23a19 --- /dev/null +++ b/src/test/java/baritone/process/elytra/pathfinder/PathFindTest.java @@ -0,0 +1,153 @@ +/* + * 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; +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 NetherPathfinder.Dimension NETHER = NetherPathfinder.Dimension.NETHER; + + @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.blocks.size() >= 2); + int lastX = Integer.MIN_VALUE; + for (BlockPos packed : segment.blocks) { + final int x = packed.getX(); + assertTrue("x goes forward", x > lastX); + lastX = x; + assertEquals(0, packed.getZ(), 16); + assertEquals(60, packed.getY(), 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.blocks.size() + " points", segment.blocks.size() <= 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(segment.blocks.get(segment.blocks.size() - 1).getX() > 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 (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)); + } + } + + @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.hasChunkFromCaller(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); + // 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 + 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(segment.blocks.get(segment.blocks.size() - 1).getX() > 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, 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 new file mode 100644 index 0000000000..c0edc8364e --- /dev/null +++ b/src/test/java/baritone/process/elytra/pathfinder/RaytraceTest.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 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.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); + int wrongHit = 0; + int wrongWhere = 0; + for (Oracle.Ray r : rays) { + 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 != null && (hit.x != r.where[0] || hit.y != r.where[1] || hit.z != 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(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 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, 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 Chunk chunk = ctx.allocateAndInsertChunk(0, 0); + chunk.setBlock(8, 64, 8, true); + 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 + 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(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(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); + ctx.allocateAndInsertChunk(0, 0); + ctx.allocateAndInsertChunk(1, 0); + ctx.allocateAndInsertChunk(2, 0).setBlock(3, 70, 4, true); + 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(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 eachRayAnswersOnItsOwn() { + final NetherPathfinder ctx = new NetherPathfinder(1, null, NetherPathfinder.Dimension.NETHER, 128); + ctx.allocateAndInsertChunk(0, 0).setBlock(8, 64, 8, 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; + } +} 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 0000000000..41021aa5d7 Binary files /dev/null and b/src/test/resources/rays.bin differ