Skip to content

Replace the native nether-pathfinder with a Java port - #5117

Open
0Mattias wants to merge 15 commits into
cabaletta:1.21.4from
0Mattias:java-nether-pathfinder
Open

0Mattias wants to merge 15 commits into
cabaletta:1.21.4from
0Mattias:java-nether-pathfinder

Conversation

@0Mattias

@0Mattias 0Mattias commented Sep 12, 2026

Copy link
Copy Markdown

Fifteen commits: the three below, then the changes from the reviews, one commit per comment.

  1. baritone.process.elytra.pathfinder: the whole library in plain Java, one class per C++ file, with its tests and oracle files (what the native library answered for 625 generated chunks and 4000 rays; the port reproduces every one, hit positions to the bit). 34 tests, junit 4 like the rest.
  2. The seam. NetherPathfinderContext gets a Chunk where it had a pointer, BlockStateOctreeInterface caches one, and the dependency, the babbaj maven repo, the fabric include, the forge/neoforge shadowCommon, the proguard keep rule and the natives for each platform go. Every system is supported now, so NullElytraProcess and the check in ElytraProcess.create go too. elytraCustomAllocator stays as a deprecated no-op so settings files still load.
  3. A fix to the region reader. Baritone writes a cached chunk's two bits per block with BitSet.toByteArray(), lowest bit first; the native reader took each byte's bits from the top down (baritone.cpp, get2Bits), so every aligned run of four blocks along x came back mirrored in any chunk a search took from the region cache. The port had copied it; now it reads what baritone writes, with a test built the way CachedChunk builds a chunk.

Since the first push, from babbaj's and ZacSharp's reviews: the FFI leftovers are gone (arguments packed into longs, the port's own BlockPos and Face, the raytrace wrappers, the duplicate region magic), Raytracer returns the hit position instead of a boolean, the block update asks isAir(), the x8 summary is a byte[] per section and stays exact on every clear (the exact flag is gone, no whole-chunk writer clears a block), Chunk says what keeps its readers and writers apart instead of describing a race, a chunk's origin is a final boolean fromCaller, insertChunkData and setChunkState are deleted, NodePos holds three ints, the set of generated chunks is a fastutil LongOpenHashSet, and the neighbour order is kept as the native library had it so the A* search expands the same way.

Left alone on purpose: the read-write lock and the two executors. The port doesn't need them (chunks are objects, the table is a ConcurrentHashMap, lookups, inserts, culls, searches and rays can all run at once), but dropping them is its own change.

Three inputs the native library couldn't take are handled now, because the elytra solver does send them: a zero-length ray (native: raytrace whiffed, exit(696969)), a NaN or infinite coordinate (native hangs on an infinity in z; the port throws IllegalArgumentException), and a ray ending exactly on a voxel corner, which every ray at a path node is (native could step into a node the ray never enters and exit; the port terminates, tested).

Same machine and inputs as the tables on #31:

native java
200k rays through real terrain, 30-150 blocks 0.75 us/ray 0.56-0.67 us/ray
pathFind over 800 blocks, 20 runs 0.39 ms avg 0.43-0.45 ms avg
main.cpp's 100k block generating search 3.0 s 2.9-3.1 s

Tried for real in my fabric fork (0Mattias/cheesecake#19, same seam): unit tests plus four in-world elytra flights, overworld above the build limit, auto-jump, nether below and above the roof, all pass with the port. Here: ./gradlew build on JDK 21, all four loaders, tests included.

0Mattias and others added 3 commits September 12, 2026 04:04
The whole of babbaj's nether-pathfinder in plain Java, under
baritone.process.elytra.pathfinder, as it stands on the java-port branch
of 0Mattias/nether-pathfinder at 6d807d0, written for
babbaj/nether-pathfinder#31. Each class is the C++ file of the same name:
the chunk octree, the raytracer, the A* search over octree cubes, the
Nether terrain generator and the region-file reader. The page allocator,
the thread pool, the JNI layer and the Unsafe accessors have no
counterpart: a chunk is an object that lives as long as it is referenced,
the table is a ConcurrentHashMap, and lookups, inserts, the search and
rays can run at once from any thread.

The tests hold the port to the native library's answers. An FNV-1a hash of
each of 625 generated chunks and the hit and hit position of 4000 rays
over generated terrain, recorded by the oracle in that repository, are
reproduced to the bit. The search, the table, region files, cancel, the
segment time-out and the rays the native library could not take -- a
point, a coordinate that is not finite, an end on a voxel corner -- have
tests of their own. Nothing uses the package yet; the next commit does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
NetherPathfinderContext asks baritone.process.elytra.pathfinder for paths
and lines of sight, with a Chunk object where the native library handed out
a pointer: the chunk packer fills a Chunk, a block update sets a bit on one,
the solver's block lookups cache one, and there is nothing to free. The
Unsafe fill of an all-solid section is Chunk.fillSection, and destroy()
closes the table. The dev.babbaj:nether-pathfinder dependency, its maven
repository, the nested jar on Fabric, the shadowed jar on Forge and
NeoForge, the ProGuard keep rule and the natives it carried for each
platform go, and with them the system check: every system is supported, so
NullElytraProcess goes too, and elytraCustomAllocator is kept only so that
a settings file naming it still loads.

The read-write lock stays, on the same sides as before, so that the threads
run in the order they always have. The port needs none of it -- a chunk is
an object that stays valid for whoever holds it, and the table takes
lookups, inserts and culls from any thread at once -- and dropping it is a
follow-up.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Baritone's CachedRegion writes a chunk's two bits per block with
BitSet.toByteArray(): bit n sits in byte n / 8 at position n % 8, the
lowest bit first. The native reader took a byte's bits from the top down
(baritone.cpp, get2Bits: `>> (6 - (i % 8))`) and the port copied it, so
block x of every aligned run of four along x came back as block x ^ 3:
each such run mirrored, in every chunk a search took from the region cache
rather than from the game. Hard to see in flight, since the cache only
fills in what the game has not loaded and terrain in runs of four is close
to its own mirror image, but wrong. The test packed its file the same
wrong way and passed; it now builds the chunk with a BitSet as CachedChunk
does, sets one block solid and one water, and checks that the mirror
positions are air. The region directory is Baritone's `cache`, not
`regions`, in the docs and the test, and the oracle's doc names its real
path. The same fix, on the port's own branch, is 0Mattias/nether-pathfinder
4784d69.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@0Mattias

Copy link
Copy Markdown
Author

wait realized something, fixing now

Comment thread src/api/java/baritone/api/Settings.java Outdated
* settings file naming it still loads.
*/
@Deprecated
public final Setting<Boolean> elytraCustomAllocator = new Setting<>(true);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this one can be safely removed

/** Reads the chunks of one of Baritone's cached region files ({@code r.X.Z.bcr}). */
final class BaritoneRegion {

private static final int MAGIC = 456022911;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make baritone.cache.CachedRegion.CACHED_REGION_MAGIC public and reference that instead of having this duplicate field

public static final int DIMENSION_NETHER = 1;
public static final int DIMENSION_END = 2;

static final int STATE_FROM_JAVA = 0;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"from java" is now a silly name now that both sides are written in java. maybe "from caller" would be better? im not really sure what to call this.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest "real", in line with the existing "fake chunk" terminology, but "from caller" works as well.

public final class NetherPathfinder implements AutoCloseable {

// How the raytracer will treat chunks that aren't actually observed.
public static final int CACHE_MISS_GENERATE = 0;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these can just be an enum now

public static final int CACHE_MISS_AIR = 1;
public static final int CACHE_MISS_SOLID = 2;

public static final int DIMENSION_OVERWORLD = 0;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

* hit a solid block, and if hitPosOutCanBeNull is given, where. Points and the segments between
* them must have 0 <= y < 384.
*/
public void raytrace(int fakeChunkMode, int inputs, double[] start, double[] end, boolean[] hitsOut, double[] hitPosOutCanBeNull) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function can be removed now that we can just call Raytracer.raytrace directly.

the array arguments were also an ffi optimization so wherever arrays were constructed to call this can be simplified

* first that is blocked; -1 if there is none. So -1 means "none clear" in the first mode and
* "all clear" in the second.
*/
public int isVisibleMulti(int fakeChunkMode, int inputs, double[] start, double[] end, boolean anyIfTrueElseAll) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the same reasons as above this function can be removed/move to the file it was called

}

/** Whether there is line of sight between the two points. */
public boolean isVisible(int fakeChunkMode, double x1, double y1, double z1, double x2, double y2, double z2) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another redundant function

* of no length is a point, which is a hit if it is inside a block; a coordinate that is not a
* finite number is refused.
*/
static boolean raytrace(NetherPathfinder ctx, double fx, double fy, double fz, double tx, double ty, double tz, int fakeChunkMode, double[] hitOut, int hitIndex) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of a boolean this should return the hit position if there's a hit, otherwise null. the last 2 arguments can be removed

s[off >>> 3] |= mask;
} else {
s[off >>> 3] &= ~mask;
if (allZero(s, x8 * (X8_BYTES / 8), X8_BYTES / 8)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why??

Every inline comment from the first review, in order: the native allocator
setting is gone; BaritoneRegion checks CachedRegion.CACHED_REGION_MAGIC
instead of a copy; "from java" is "from caller"; the cache-miss modes and
the dimensions are enums; PathSegment carries a List<BlockPos> instead of
packed longs; the search and region-read timers use currentTimeMillis;
the port uses Minecraft's BlockPos and its own class is deleted; slabs are
sections; the Raytracer's switch computes the child origin and offset once;
the batched raytrace, isVisible and isVisibleMulti wrappers are removed and
callers use Raytracer.raytrace directly, which returns the hit position or
null; and the x8 summary bit is no longer maintained on clears, since a
stale set bit only costs a reader a scan.

Also carried: pathFind clears the cancel flag on the way out rather than in,
so a cancel that lands while a search is still queued behind the lock is
honoured by that search instead of wiped; CancelTest covers it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@0Mattias

Copy link
Copy Markdown
Author

Thanks for the review. All of it (including your new comments) is now applied and pushed here. Quick overview of how the port fits together first, since a few of your points touch the seams, then each comment in order, then what I checked.

How the port is put together

The Java port lives in baritone.process.elytra.pathfinder and follows the native library file for file. Chunk is the same octree layout as Chunk.h: 24 sections of 512 bytes, one bit per block, plus a byte per section that says which x8 cubes may hold a block. ChunkGeneratorHell and the two noise generators produce Nether terrain from the seed. PathFinder is the A* over cubes of 2 to 16 blocks, with the binary heap. Raytracer is the Revelles octree traversal from Refiner.cpp. BaritoneRegion reads Baritone's .bcr region cache. NetherPathfinder is the context: the chunk table, the search, and the cancel flag.

The integration is thin on purpose. NetherPathfinderContext keeps the methods Baritone already called (pathFindAsync, raytrace, insertChunk, cullFarChunks, etc.) but holds a NetherPathfinder object instead of a native pointer. BlockStateOctreeInterface reads chunks through it. There is no JNI and no Unsafe, nothing to load, so ElytraProcess no longer needs isSupported or a null process.

Correctness is checked against recorded output from the native library: chunk hashes for a generated world, 4,000 rays with their hit positions, and searches over the same terrain. Those tests did not change in this round and still match bit for bit.

Your comments, in order

  1. Settings.java, elytraCustomAllocator. Removed, including the deprecation stub. It only ever selected the native allocator.

  2. BaritoneRegion, duplicate magic number. CachedRegion.CACHED_REGION_MAGIC is public now and BaritoneRegion uses it. The local copy is gone.

  3. "from java" naming. Took your suggestion: STATE_FROM_CALLER, hasChunkFromCaller, and a fromCaller parameter on setChunkState. The distinction is between chunks the game handed in and chunks the port made up (generated, or assumed air). "From caller" reads fine for that.

  4. CACHE_MISS_* as an enum. Now NetherPathfinder.CacheMiss { GENERATE, AIR, SOLID }. The range check on the int is gone with it.

  5. DIMENSION_* as an enum. Now NetherPathfinder.Dimension { OVERWORLD, NETHER, END }. BaritoneRegion.dimensionOf returns the enum, or null for a directory that is not a cache, instead of -1.

  6. Packing into longs. Gone. PathSegment holds a List<BlockPos>, pathFind returns it directly, and UnpackedSegment.from maps it to BetterBlockPos. The packing constants, packBlockPos, and the round-trip test for them are deleted.

  7. and 10. currentTimeMillis. Done in both places: the search timeouts in PathFinder.findPathSegment and the region-read time in tryLoadRegion, which is subtracted from the timeouts and is in milliseconds too now. For the record, nanoTime was there because it is monotonic and currentTimeMillis can jump with the wall clock. At 30-second timeouts that does not matter.

  8. Minecraft's BlockPos. The port's own BlockPos class is deleted and net.minecraft.core.BlockPos is used everywhere. Two small things moved: Face got an offset(pos, n) method for what the old class did with a face, and the floor is Mth.floor.

  9. Slabs are sections. Renamed everywhere in Chunk and Raytracer: SECTIONS, sections, section(y), SECTION_LONGS, the parameters, and the comments.

  10. The unreadable switch in Raytracer. The three arguments every case computed the same way (the child's origin on x, y and z) plus the child's byte offset are now computed once before the switch, from the octant index i = currNode ^ a, with a comment explaining the reflection and what each bit of i means. Each case now only differs in which t-parameters it passes and which node comes next, which is the part that actually varies.

12., 13. and 14. NetherPathfinder.raytrace, isVisibleMulti, isVisible. All three removed. Callers call Raytracer.raytrace directly. The array versions of raytrace(count, src, dst, ...) on NetherPathfinderContext stay, because they are Baritone's own API and ElytraBehavior sends its hitbox rays through them in batches. They now loop and ask the raytracer one ray at a time, so the array handling stops at that seam instead of running through the port.

  1. Raytracer.raytrace returning a boolean with out-parameters. It returns the hit position as a Vec3, or null when the ray reaches its end. The hitOut array and index parameters are gone. The oracle test compares against the returned position.

  2. Chunk, "why??" That line cleared the x8 summary bit when the last block in that cube was cleared, and to know that it had to scan the cube's eight longs on every clear. Why it was there: the filled byte per section says which x8 cubes hold a block, and I had kept it exact on clears so isEmptyX8 would be exact too. But nothing needs it exact. Every reader only uses the bit to skip a cube it knows is empty, so a set bit that outlives its blocks costs a reader one scan and can never give a wrong answer. So I dropped the clear-side maintenance. The bit is set when a block is set and reset only by fillSection. The javadoc now says the bit means "may hold a block", and the test checks that a clear leaves it set while the exact query sees the block gone.

Also in this push

Two things that are not from the review. pathFind used to clear the cancel flag on the way in. That meant a cancel() arriving while a search was still queued behind the lock, which is exactly the one destroy() sends, got wiped, and that search then ran to its full timeout while holding the lock. It is cleared on the way out now, so a cancel is honoured by the search it was aimed at and does not leak into the next one. CancelTest covers it. And since visibility is answered per ray now, the batch methods answer exactly what the rays say, one at a time, instead of a batch-level shortcut.

Verified

./gradlew :test: 77 tests, all passing, including the chunk hash and 4,000 ray comparisons against the native library. ./gradlew :fabric:build is green, and the jars carry the port with no native library and no leftover BlockPos class. The same code is flying in my Fabric fork, where four in-world elytra runs (Overworld above the build limit, auto-jump, Nether below the roof and above it) pass end to end.

Let me know what you think, I can change any of this as needed.

* Magic value to detect invalid cache files, or incompatible cache files saved in an old version of Baritone
*/
private static final int CACHED_REGION_MAGIC = 456022911;
// Public: the elytra pathfinder's region reader checks the same header.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove the comment

// outlives its blocks costs a reader a scan of the cube and never a wrong answer;
// fillSection resets it.
s[off >>> 3] &= ~mask;
if (allZero(s, x8 * (X8_BYTES / 8), X8_BYTES / 8)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still needed because queueBlockUpdate may set random blocks to air but for performance this check should be done when updating individual blocks from queueBlockUpdate. In all other places blocks are set the entire chunk is being set at once and in those cases this check is not needed. Because this function is a bit complicated instead of creating a separate function just add an argument to this one that conditionally runs this code that is only set to try by queueBlockUpdate. then for convenience add another setBlock function with the same signature as we currently have but passes false for that new argument.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there actually any place which calls this with solid=false and exact=false outside tests? From a quick search it looks like all callers either call setBlock(x, y, z, solid, true) or know that they are writing to an empty chunk and only call setBlock(x, y, z, true), suppressing calls with solid=false.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im pretty sure no

public final boolean finished;
public final long[] packed;

/** The blocks of the path, in order. */

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove the comment

babbaj's second review, in order. The x8 summary bit has to be cleared
with its cube's last block after all: queueBlockUpdate sets single blocks
to air in chunks the search is using, and a stale bit would leave a cube
reading as possibly solid for as long as the chunk lives. But the scan
that keeping the bit exact costs is wasted on the callers that fill a
whole chunk at once, which only ever set blocks. So setBlock takes an
`exact` flag that runs the scan on a clear, the four-argument setBlock
passes false, and queueBlockUpdate alone passes true; ChunkTest checks
both kinds of clear. The comment above CachedRegion.CACHED_REGION_MAGIC
and the javadoc on PathSegment.blocks go.

Also dropped, since they were unused: EOFException in BaritoneRegion,
OutputStream in BaritoneRegionTest, List in NetherPathfinder and
assertNull in RaytraceTest.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@0Mattias

Copy link
Copy Markdown
Author

Thanks again for the review. Pushed 8827e15. setBlock takes an exact flag now: a clear that empties its x8 also clears the cube's bit in filled, and only queueBlockUpdate passes true. The four-arg setBlock passes false, so the whole-chunk fills skip the scan. ChunkTest covers both kinds of clear. The two comments are gone, and I dropped four unused imports while I was in there.

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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while we're here we should also replace this with .isAir()


import net.minecraft.core.BlockPos;

enum Face {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be deleted and replaced with minecraft's Direction

0Mattias and others added 2 commits September 13, 2026 00:43
babbaj's third review. The block update test compared the state against
Blocks.AIR's default state, so a block that became cave air counted as
solid. isAir covers cave air and void air too, and writeChunkData already
treats cave air as air when it packs a whole chunk, so a single block
update and a chunk fill now agree on what air is. AIR_BLOCK_STATE had no
other use and goes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
babbaj's fourth review. Face only named the six directions and moved a
BlockPos along one, which Direction and BlockPos.relative already do, so
the enum goes and PathFinder takes a Direction. The neighbour array keeps
the native library's order, UP, DOWN, NORTH, SOUTH, EAST, WEST, rather
than Direction.values(), so the search expands neighbours as it always
has.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@0Mattias

Copy link
Copy Markdown
Author

Both in, pushed eb62fc0. isAir() in the block update and AIR_BLOCK_STATE is gone, and Face is deleted, PathFinder takes a Direction now and moves with BlockPos.relative. Just a heads up, I kept the neighbour array in the old order (UP, DOWN, NORTH, SOUTH, EAST, WEST) instead of Direction.values() so the A* search expands the same way it did before. Tests green, and the four in-world flights in my fork pass too.

@ZacSharp ZacSharp left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not at all familiar with the C++ codebase so I cannot say much about the port. I've tried to not leave comments which likely apply to the C++ code as well.

I've mostly skipped over the tests.

One thing to perhaps care about later is that this spams allocations. Some years ago benchmarks showed the main pathfinder was faster after removing every single BlockPos allocation. The JVM might since have gotten better at handling this kind of short-lived-allocation spam, but it might be worth a try once things have settled.

Comment thread src/main/java/baritone/process/elytra/pathfinder/Chunk.java Outdated
Comment thread src/main/java/baritone/process/elytra/pathfinder/Chunk.java Outdated
Comment thread src/main/java/baritone/process/elytra/pathfinder/Chunk.java Outdated
Comment thread src/main/java/baritone/process/elytra/pathfinder/Chunk.java
Comment thread src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java Outdated
Comment thread src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java Outdated
Comment thread src/main/java/baritone/process/elytra/pathfinder/NetherPathfinder.java Outdated
Comment thread src/main/java/baritone/process/elytra/pathfinder/NodePos.java Outdated
Comment thread src/main/java/baritone/process/elytra/pathfinder/PathFinder.java Outdated
Comment thread src/main/java/baritone/process/elytra/pathfinder/package-info.java Outdated
0Mattias and others added 8 commits September 16, 2026 18:59
ZacSharp's review, on Chunk.setBlock and isEmptyX8. The exact flag
existed so that the callers that fill a whole chunk would not pay the
scan that keeping the summary exact costs on a clear, but none of them
ever clears a block: writeChunkData, insertChunkData, BaritoneRegion and
the generator only set, and the block update, the one caller that
clears, already asked for exact. So the flag decided nothing, and with
it gone the summary is exact by construction, which answers the question
of whether isEmptyX8 and isEmptyX16 may say no for an empty cube: they
may not, and a summary that disagrees with the blocks is a bug in
whatever wrote them, as babbaj put it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ZacSharp asked whether the JIT or the CPU may reorder the two plain
writes in setBlock, and babbaj asked why a race was expected there at
all. The comment claimed an order the language does not promise:
without a fence, a reader on another thread may see the block before
the summary bit. It was written for a reader that does not exist. Chunk
was ported to be safe on its own, like the native code, but every
reader in Baritone takes NetherPathfinderContext's read lock, the
solver thread, the game thread's tick and a search that does not
generate alike, while a block update, a chunk pack, a cull and a
generating search hold its write lock, so no reader can watch a block
being set. The class comment now says so instead of describing a race,
and the comment on the write goes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ZacSharp's review. A section has eight x8 cubes, so its summary needs
eight bits, and the class comment already called it a byte; the int
array was an oversight. filled(y) still returns an int, the byte masked
to its eight bits, so the raytracer's tests of it are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ZacSharp found it unused and babbaj said to remove it. It was the
native library's entry point for a chunk as an array of booleans; the
game's chunks come in through allocateAndInsertChunk and are packed
straight into the Chunk, and only a test called this. dimensionHeight
served only its length check and goes with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ZacSharp asked whether the chunk state should be an enum or a boolean,
and babbaj noticed that setChunkState, its only writer, is never
called, so the field should be final and not volatile. It is now a
final boolean, fromCaller, in place of the two int constants, and
setChunkState goes; the test helper that marked generated chunks as the
game's now generates them itself and copies the blocks into chunks it
inserts as the game's.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ZacSharp's review. The BlockPos was private and only ever read back
through its three getters, so the node holds the coordinates itself and
allocates one object fewer per node the search creates.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ZacSharp's review, babbaj agreed. The set of chunks whose neighbours
the search has generated was a HashSet of boxed Longs; fastutil's long
set holds the keys unboxed. The keys are what NetherPathfinder.key
gives, as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ZacSharp's review, babbaj agreed the comment was outdated: the package
took Minecraft's BlockPos, Direction, Vec3 and Mth in the first review
round and the comment still said it used nothing of Minecraft's.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@0Mattias

Copy link
Copy Markdown
Author

Thanks both. All eight are in, pushed 1a3c211..63a5813, one commit per comment:

  • filled is a byte[], one byte per section, and its comment just says what the bits mean.
  • The exact flag is gone. No whole-chunk writer ever clears a block, as Zac found, so the flag never skipped a scan for anyone, and without it the summary is exact on every clear: isEmptyX8 and isEmptyX16 can no longer say "not empty" for an empty cube. If they ever disagree with the blocks, that is a bug in whatever wrote them, as babbaj says.
  • The "x8 is marked before its block" comment is gone too. To answer babbaj: there is no race. The comment was written as if Chunk had to be safe on its own, like the native code, but every reader in Baritone holds NetherPathfinderContext's read lock and every writer its write lock, so nothing can watch a block being set. The class comment says that now instead.
  • Entry.state is a final boolean fromCaller, setChunkState and insertChunkData are deleted, and the test helper generates its chunks itself. I kept "from caller" since it matches hasChunkFromCaller.
  • NodePos holds three ints instead of a BlockPos, doneFull is a fastutil LongOpenHashSet, and the package comment names the Minecraft classes it uses.

On the allocations: the NodePos change drops one object per node the search creates. The rest I would rather look at once the review has settled, with a benchmark, as you say. Tests green, and the four in-world flights in my fork pass too.

@babbaj

babbaj commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

There's still a lot of BlockPos objects being created, especially where they are made just to construct NodePos objects.

Comment on lines +53 to +56
// 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this the lock that prevents writing to a chunk while search is reading it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Elytra Pathfinding Navigating Below Y=32 in Nether Unexpected exit during nether pathng Add a option to allow unsupported cpus to use efly

3 participants