From 9cb3a68b5027644d25d05b9759e2566c4e9d98b3 Mon Sep 17 00:00:00 2001 From: Dek Date: Tue, 25 Aug 2026 13:30:29 -0400 Subject: [PATCH 01/54] fix chunk packing writing solid sections as air The 26.2 port replaced the removed vanilla constant LevelChunkSection.SECTION_SIZE (4096, blocks per section) with a local SECTION_SIZE = 16, so the all-solid fast path memset 2 bytes at offset y0*2 instead of 512 bytes at y0*512 (SIZEOF_X16). Every section without air in its palette was handed to nether-pathfinder as open air, which made elytra path through solid terrain and fail constantly. --- .../java/baritone/process/elytra/NetherPathfinderContext.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java index 3ed1e19155..6cb8564c21 100644 --- a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java +++ b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java @@ -63,7 +63,8 @@ public final class NetherPathfinderContext implements IElytraPathFinder { } } private static final BlockState AIR_BLOCK_STATE = Blocks.AIR.defaultBlockState(); - private static final int SECTION_SIZE = 16; + // blocks per 16x16x16 section (replaces LevelChunkSection.SECTION_SIZE, removed from vanilla in 26.x) + private static final int SECTION_SIZE = 16 * 16 * 16; // 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. public final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); From abcb47d8eb45606ed3b52ca4a4833ef59646a999 Mon Sep 17 00:00:00 2001 From: Dek Date: Tue, 25 Aug 2026 13:30:29 -0400 Subject: [PATCH 02/54] set daemon on baritone threadpool As of mc 26.2 the game awaits non-daemon threads on stop and crash reports if any are still alive. --- src/main/java/baritone/Baritone.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/baritone/Baritone.java b/src/main/java/baritone/Baritone.java index 53f2bd3e0a..6e1405606f 100755 --- a/src/main/java/baritone/Baritone.java +++ b/src/main/java/baritone/Baritone.java @@ -36,6 +36,7 @@ import baritone.utils.InputOverrideHandler; import baritone.utils.PathingControlManager; import baritone.utils.player.BaritonePlayerContext; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import net.minecraft.client.Minecraft; import java.io.IOException; @@ -58,7 +59,13 @@ public class Baritone implements IBaritone { private static final ThreadPoolExecutor threadPool; static { - threadPool = new ThreadPoolExecutor(4, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<>()); + threadPool = new ThreadPoolExecutor(4, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<>(), + new ThreadFactoryBuilder() + .setNameFormat("baritone-pool-%d") + // as of mc 26.2, mc will await non-daemon threads exit on game stop - crash reporting if any non-daemon thread is still alive + .setDaemon(true) + .build() + ); } private final Minecraft mc; From 07373e7f3e0f0970a67ee47043b7ef53e7042901 Mon Sep 17 00:00:00 2001 From: Dek Date: Tue, 25 Aug 2026 13:31:55 -0400 Subject: [PATCH 03/54] add release workflow --- .github/workflows/build-release.yml | 43 +++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/build-release.yml diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 0000000000..a8970858ce --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,43 @@ +name: Build and Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Get Java version + id: java + run: echo "version=$(grep '^java_version=' gradle.properties | cut -d= -f2)" >> $GITHUB_OUTPUT + + - name: Set up JDK ${{ steps.java.outputs.version }} + uses: actions/setup-java@v4 + with: + java-version: ${{ steps.java.outputs.version }} + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with Gradle + run: ./gradlew build -x test + + - name: Release + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v2 + with: + files: dist/* From 9713291828cd4938f32d0229ebaef24cdd4d038a Mon Sep 17 00:00:00 2001 From: Dek Date: Tue, 25 Aug 2026 13:48:42 -0400 Subject: [PATCH 04/54] pin proguard toolchain to the CI JDK --- .github/workflows/build-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index a8970858ce..cf1fc5f6cf 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -34,7 +34,7 @@ jobs: run: chmod +x gradlew - name: Build with Gradle - run: ./gradlew build -x test + run: ./gradlew build -x test -Porg.gradle.java.installations.auto-detect=false "-Porg.gradle.java.installations.paths=$JAVA_HOME" - name: Release if: startsWith(github.ref, 'refs/tags/v') From 7f253b4c44ac274cbcd0e74fb4a1d92e2162448d Mon Sep 17 00:00:00 2001 From: Dek Date: Tue, 25 Aug 2026 13:54:38 -0400 Subject: [PATCH 05/54] use zulu jdk in CI (image temurin 25 package lacks jmods) --- .github/workflows/build-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index cf1fc5f6cf..44fdbd86f7 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -25,7 +25,7 @@ jobs: uses: actions/setup-java@v4 with: java-version: ${{ steps.java.outputs.version }} - distribution: 'temurin' + distribution: 'zulu' - name: Setup Gradle uses: gradle/actions/setup-gradle@v3 From d8e5a09f6530a3bb45188b7a8aff187bbc8a0d36 Mon Sep 17 00:00:00 2001 From: Dek Date: Sun, 30 Aug 2026 15:24:00 -0400 Subject: [PATCH 06/54] fix fall cost charging a phantom tick of gravity velocity(0) is 0, so the first iteration of fallTicks always consumed a tick without covering any distance. Every fall was priced one tick (and the test's own reimplementation one tick of distance) too expensive, which made falling look worse than it is relative to walking down. --- .../java/baritone/api/pathing/movement/ActionCosts.java | 4 +++- .../java/baritone/pathing/movement/ActionCostsTest.java | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/api/java/baritone/api/pathing/movement/ActionCosts.java b/src/api/java/baritone/api/pathing/movement/ActionCosts.java index 691d73b4d5..447ef7c1c3 100644 --- a/src/api/java/baritone/api/pathing/movement/ActionCosts.java +++ b/src/api/java/baritone/api/pathing/movement/ActionCosts.java @@ -87,7 +87,9 @@ static double distanceToTicks(double distance) { double tmpDistance = distance; int tickCount = 0; while (true) { - double fallDistance = velocity(tickCount); + // you have already been falling for one tick by the time you have moved at all; velocity(0) is 0, + // so indexing from tickCount would charge a phantom tick against every fall + double fallDistance = velocity(tickCount + 1); if (tmpDistance <= fallDistance) { return tickCount + tmpDistance / fallDistance; } diff --git a/src/test/java/baritone/pathing/movement/ActionCostsTest.java b/src/test/java/baritone/pathing/movement/ActionCostsTest.java index a6d7e3e720..4cae886678 100644 --- a/src/test/java/baritone/pathing/movement/ActionCostsTest.java +++ b/src/test/java/baritone/pathing/movement/ActionCostsTest.java @@ -31,8 +31,8 @@ public void testFallNBlocksCost() { double blocks = ticksToBlocks(FALL_N_BLOCKS_COST[i]); assertEquals(blocks, i, 0.00000000001); // If you add another 0 the test fails at i=989 LOL } - assertEquals(FALL_1_25_BLOCKS_COST, 6.2344, 0.00001); - assertEquals(FALL_0_25_BLOCKS_COST, 3.0710, 0.00001); + assertEquals(FALL_1_25_BLOCKS_COST, 5.2344, 0.00001); + assertEquals(FALL_0_25_BLOCKS_COST, 2.0710, 0.00001); assertEquals(JUMP_ONE_BLOCK_COST, 3.1634, 0.00001); } @@ -40,10 +40,10 @@ public double ticksToBlocks(double ticks) { double fallDistance = 0; int integralComponent = (int) Math.floor(ticks); for (int tick = 0; tick < integralComponent; tick++) { - fallDistance += velocity(tick); + fallDistance += velocity(tick + 1); } double partialTickComponent = ticks - Math.floor(ticks); - double finalPartialTickVelocity = velocity(integralComponent); + double finalPartialTickVelocity = velocity(integralComponent + 1); double finalPartialTickDistance = finalPartialTickVelocity * partialTickComponent; fallDistance += finalPartialTickDistance; return fallDistance; From 8adc0bb8de817f3a35e036bc642ec1d6506f8a31 Mon Sep 17 00:00:00 2001 From: Dek Date: Sun, 30 Aug 2026 15:24:00 -0400 Subject: [PATCH 07/54] fix descend cost when landing on top of a block The fall ends at newY + 1 when there is a block to land on, so the drop is one block shorter than the one that was probed. Both the plain landing and the water bucket variant were charged for the extra block. --- .../pathing/movement/movements/MovementDescend.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/baritone/pathing/movement/movements/MovementDescend.java b/src/main/java/baritone/pathing/movement/movements/MovementDescend.java index 0052efed2c..afc4863c6a 100644 --- a/src/main/java/baritone/pathing/movement/movements/MovementDescend.java +++ b/src/main/java/baritone/pathing/movement/movements/MovementDescend.java @@ -157,6 +157,8 @@ public static boolean dynamicFallCost(CalculationContext context, int x, int y, BlockState ontoBlock = context.get(destX, newY, destZ); int unprotectedFallHeight = fallHeight - (y - effectiveStartHeight); // equal to fallHeight - y + effectiveFallHeight, which is equal to -newY + effectiveFallHeight, which is equal to effectiveFallHeight - newY double tentativeCost = WALK_OFF_BLOCK_COST + FALL_N_BLOCKS_COST[unprotectedFallHeight] + frontBreak + costSoFar; + // landing on top of a block ends the fall at newY + 1, so we only actually fall one block less than we probed + double tentativeCostOntoBlock = WALK_OFF_BLOCK_COST + FALL_N_BLOCKS_COST[unprotectedFallHeight - 1] + frontBreak + costSoFar; if (reachedMinimum && MovementHelper.isWater(ontoBlock)) { if (!MovementHelper.canWalkThrough(context, destX, newY, destZ, ontoBlock)) { return false; @@ -208,14 +210,14 @@ public static boolean dynamicFallCost(CalculationContext context, int x, int y, res.x = destX; res.y = newY + 1; res.z = destZ; - res.cost = tentativeCost; + res.cost = tentativeCostOntoBlock; return false; } if (reachedMinimum && context.hasWaterBucket && unprotectedFallHeight <= context.maxFallHeightBucket + 1) { res.x = destX; res.y = newY + 1;// this is the block we're falling onto, so dest is +1 res.z = destZ; - res.cost = tentativeCost + context.placeBucketCost(); + res.cost = tentativeCostOntoBlock + context.placeBucketCost(); return true; } else { return false; From a5327d501ed870c0203b72c381e5833578ac1274 Mon Sep 17 00:00:00 2001 From: Dek Date: Sun, 30 Aug 2026 15:24:00 -0400 Subject: [PATCH 08/54] fix GoalRunAway building its bounding box from the wrong corner The max corner was folded against the running min instead of against itself, so with more than one position the box collapsed towards the minimum corner and isInGoal reported positions outside it as inside. --- src/api/java/baritone/api/pathing/goals/GoalRunAway.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/java/baritone/api/pathing/goals/GoalRunAway.java b/src/api/java/baritone/api/pathing/goals/GoalRunAway.java index 1a1b6e50c4..4dd225060b 100644 --- a/src/api/java/baritone/api/pathing/goals/GoalRunAway.java +++ b/src/api/java/baritone/api/pathing/goals/GoalRunAway.java @@ -96,9 +96,9 @@ public double heuristic() {// TODO less hacky solution minX = Math.min(minX, p.getX() - distance); minY = Math.min(minY, p.getY() - distance); minZ = Math.min(minZ, p.getZ() - distance); - maxX = Math.max(minX, p.getX() + distance); - maxY = Math.max(minY, p.getY() + distance); - maxZ = Math.max(minZ, p.getZ() + distance); + maxX = Math.max(maxX, p.getX() + distance); + maxY = Math.max(maxY, p.getY() + distance); + maxZ = Math.max(maxZ, p.getZ() + distance); } DoubleOpenHashSet maybeAlwaysInside = new DoubleOpenHashSet(); // see pull request #1978 double minOutside = Double.POSITIVE_INFINITY; From e6b66d3746b2046ade9d2e48d5f10cf467f9b254 Mon Sep 17 00:00:00 2001 From: Dek Date: Sun, 30 Aug 2026 15:24:00 -0400 Subject: [PATCH 09/54] fix diagonal magma check reading the wrong block The second corner was tested for lava but its magma check looked at the first corner again, so a diagonal could cut over a magma block with allowWalkOnMagmaBlocks off. --- .../baritone/pathing/movement/movements/MovementDiagonal.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/baritone/pathing/movement/movements/MovementDiagonal.java b/src/main/java/baritone/pathing/movement/movements/MovementDiagonal.java index 8be1047582..edc1d579bf 100644 --- a/src/main/java/baritone/pathing/movement/movements/MovementDiagonal.java +++ b/src/main/java/baritone/pathing/movement/movements/MovementDiagonal.java @@ -166,7 +166,7 @@ public static void cost(CalculationContext context, int x, int y, int z, int des return; } BlockState cuttingOver2 = context.get(destX, y - 1, z); - if ((!context.allowWalkOnMagmaBlocks && cuttingOver1.is(Blocks.MAGMA_BLOCK)) || MovementHelper.isLava(cuttingOver2)) { + if ((!context.allowWalkOnMagmaBlocks && cuttingOver2.is(Blocks.MAGMA_BLOCK)) || MovementHelper.isLava(cuttingOver2)) { return; } boolean water = false; From ae63b2928551b1a532547b85723f042464014e09 Mon Sep 17 00:00:00 2001 From: Dek Date: Sun, 30 Aug 2026 15:24:00 -0400 Subject: [PATCH 10/54] fix pathfinder height limit ignoring world min y height() is the number of blocks in the dimension, not the top y level. In the overworld (minY -64, height 384) the ceiling check let the search climb to y=384, 64 blocks above the build limit, and every node up there was wasted work. minY + height is the actual top. --- src/main/java/baritone/pathing/calc/AStarPathFinder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/baritone/pathing/calc/AStarPathFinder.java b/src/main/java/baritone/pathing/calc/AStarPathFinder.java index 1c58cbf20e..f2ae74ad13 100644 --- a/src/main/java/baritone/pathing/calc/AStarPathFinder.java +++ b/src/main/java/baritone/pathing/calc/AStarPathFinder.java @@ -112,7 +112,7 @@ protected Optional calculate0(long primaryTimeout, long failureTimeout) { if (!moves.dynamicXZ && !worldBorder.entirelyContains(newX, newZ)) { continue; } - if (currentNode.y + moves.yOffset > height || currentNode.y + moves.yOffset < minY) { + if (currentNode.y + moves.yOffset > minY + height || currentNode.y + moves.yOffset < minY) { continue; } res.reset(); From 169c5de883b3e9ff10fabd8448b0ebf3625cc94e Mon Sep 17 00:00:00 2001 From: Dek Date: Sun, 30 Aug 2026 15:24:37 -0400 Subject: [PATCH 11/54] fix waterlogged blocks being treated as passable A source water block reached the "it is just water, walk through it" return without ever asking the block itself. Waterlogged fences, walls, panes, bars, stairs and leaves all keep their own collision, so the pathfinder walked into them. --- src/main/java/baritone/pathing/movement/MovementHelper.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/baritone/pathing/movement/MovementHelper.java b/src/main/java/baritone/pathing/movement/MovementHelper.java index 51a23577ba..e63a7f3770 100644 --- a/src/main/java/baritone/pathing/movement/MovementHelper.java +++ b/src/main/java/baritone/pathing/movement/MovementHelper.java @@ -224,7 +224,9 @@ static boolean canWalkThroughPosition(BlockStateInterface bsi, int x, int y, int if (!up.getFluidState().isEmpty() || up.getBlock() instanceof LilyPadBlock) { return false; } - return fluidState.getType() instanceof WaterFluid; + // a waterlogged block (fence, wall, pane, bars, stairs, leaves...) keeps its own collision, + // so it's only passable if the block itself is + return fluidState.getType() instanceof WaterFluid && state.isPathfindable(PathComputationType.LAND); } return state.isPathfindable(PathComputationType.LAND); From 8219f85ac2c2c9cfc545bac36c884546f2ee7152 Mon Sep 17 00:00:00 2001 From: Dek Date: Sun, 30 Aug 2026 15:29:10 -0400 Subject: [PATCH 12/54] don't jump through blocks that can't be walked through fullyPassable listed its exceptions one block at a time, so anything that blocks walking but is not in that list - powder snow, honey, berry bushes, dripstone, whatever the user put in blocksToAvoid - was still considered jumpable. The hitbox is the same width in the air as on the ground. --- src/main/java/baritone/pathing/movement/MovementHelper.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/baritone/pathing/movement/MovementHelper.java b/src/main/java/baritone/pathing/movement/MovementHelper.java index e63a7f3770..d3408f51bb 100644 --- a/src/main/java/baritone/pathing/movement/MovementHelper.java +++ b/src/main/java/baritone/pathing/movement/MovementHelper.java @@ -237,6 +237,11 @@ static Ternary fullyPassableBlockState(BlockState state) { if (block instanceof AirBlock) { // early return for most common case return YES; } + // the player hitbox is the same width in the air as on the ground, so anything we can't walk + // through we can't jump through either (powder snow, honey, berry bushes, dripstone, blocksToAvoid...) + if (canWalkThroughBlockState(state) == NO) { + return NO; + } // exceptions - blocks that are isPassable true, but we can't actually jump through if (block instanceof BaseFireBlock || block == Blocks.TRIPWIRE From 84eb670a6a96fdb53c95d3074f2c05fe44df8590 Mon Sep 17 00:00:00 2001 From: Dek Date: Sun, 30 Aug 2026 15:25:09 -0400 Subject: [PATCH 13/54] give GoalGetToBlock a heuristic heuristic() is what estimatedTicksToGoal treats as "we are there", and the default of 0 is wrong for a goal whose own heuristic never reaches 0: standing on top of the block still scores a full jump. The reported ETA never counted down to zero and kept ticking after arrival. --- .../java/baritone/api/pathing/goals/GoalGetToBlock.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/api/java/baritone/api/pathing/goals/GoalGetToBlock.java b/src/api/java/baritone/api/pathing/goals/GoalGetToBlock.java index 1c04f7c6d1..a0116be6f3 100644 --- a/src/api/java/baritone/api/pathing/goals/GoalGetToBlock.java +++ b/src/api/java/baritone/api/pathing/goals/GoalGetToBlock.java @@ -61,6 +61,13 @@ public double heuristic(int x, int y, int z) { return GoalBlock.calculate(xDiff, yDiff < 0 ? yDiff + 1 : yDiff, zDiff); } + @Override + public double heuristic() { + // the largest heuristic value that is still guaranteed to be inside the goal: the six positions + // adjacent to the block, of which the one directly above is the most expensive + return Math.max(GoalXZ.calculate(1, 0), Math.max(GoalYLevel.calculate(0, 1), GoalYLevel.calculate(0, -1))); + } + @Override public boolean equals(Object o) { if (this == o) { From 26b60fcbd3c0cdb69e5fdc49a50cd66aa2885064 Mon Sep 17 00:00:00 2001 From: Dek Date: Sun, 30 Aug 2026 15:25:09 -0400 Subject: [PATCH 14/54] keep backtrackCostFavoringCoefficient positive A coefficient of 0 or less multiplies out to a non positive action cost, which A* rejects outright, so the whole calculation fails instead of the setting just not favoring anything. --- src/main/java/baritone/utils/pathing/Favoring.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/baritone/utils/pathing/Favoring.java b/src/main/java/baritone/utils/pathing/Favoring.java index bbc23c7ff5..f4e62cff1d 100644 --- a/src/main/java/baritone/utils/pathing/Favoring.java +++ b/src/main/java/baritone/utils/pathing/Favoring.java @@ -39,7 +39,7 @@ public Favoring(IPlayerContext ctx, IPath previous, CalculationContext context) public Favoring(IPath previous, CalculationContext context) { // create one just from previous path, no mob avoidances favorings = new Long2DoubleOpenHashMap(); favorings.defaultReturnValue(1.0D); - double coeff = context.backtrackCostFavoringCoefficient; + double coeff = Math.max(context.backtrackCostFavoringCoefficient, 0.01); // a coefficient of 0 or below would make actionCost non-positive, which A* rejects if (coeff != 1D && previous != null) { previous.positions().forEach(pos -> favorings.put(BetterBlockPos.longHash(pos), coeff)); } From 8d95c8c6348b4aca08866a9e586bc7102aee5d42 Mon Sep 17 00:00:00 2001 From: Dek Date: Sun, 30 Aug 2026 15:29:10 -0400 Subject: [PATCH 15/54] snapshot player state for the pathing thread ToolSet read the live inventory and cached break speeds in a HashMap, and MovementHelper read assumeWalkOnWater straight off the settings, all from the calculation thread. Reading the inventory off thread is a data race against the game thread, and a setting that changes mid calculation makes the cost function inconsistent with the nodes already expanded. The hotbar and the selected slot are now copied on construction, the break speed cache is concurrent, and assumeWalkOnWater is read once per BlockStateInterface. --- .../pathing/movement/MovementHelper.java | 6 ++--- .../baritone/utils/BlockStateInterface.java | 3 +++ src/main/java/baritone/utils/ToolSet.java | 22 ++++++++++++++----- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/main/java/baritone/pathing/movement/MovementHelper.java b/src/main/java/baritone/pathing/movement/MovementHelper.java index d3408f51bb..1c1a1865ed 100644 --- a/src/main/java/baritone/pathing/movement/MovementHelper.java +++ b/src/main/java/baritone/pathing/movement/MovementHelper.java @@ -216,7 +216,7 @@ static boolean canWalkThroughPosition(BlockStateInterface bsi, int x, int y, int return false; } // Everything after this point has to be a special case as it relies on the water not being flowing, which means a special case is needed. - if (Baritone.settings().assumeWalkOnWater.value) { + if (bsi.assumeWalkOnWater) { return false; } @@ -465,11 +465,11 @@ static boolean canWalkOnPosition(BlockStateInterface bsi, int x, int y, int z, B } if (MovementHelper.isFlowing(x, y, z, state, bsi) || upState.getFluidState().getType() == Fluids.FLOWING_WATER) { // the only scenario in which we can walk on flowing water is if it's under still water with jesus off - return isWater(upState) && !Baritone.settings().assumeWalkOnWater.value; + return isWater(upState) && !bsi.assumeWalkOnWater; } // if assumeWalkOnWater is on, we can only walk on water if there isn't water above it // if assumeWalkOnWater is off, we can only walk on water if there is water above it - return isWater(upState) ^ Baritone.settings().assumeWalkOnWater.value; + return isWater(upState) ^ bsi.assumeWalkOnWater; } if (MovementHelper.isLava(state) && !MovementHelper.isFlowing(x, y, z, state, bsi) && Baritone.settings().assumeWalkOnLava.value) { // if we get here it means that assumeWalkOnLava must be true, so put it last diff --git a/src/main/java/baritone/utils/BlockStateInterface.java b/src/main/java/baritone/utils/BlockStateInterface.java index 7a8420d8e2..afd4e5d756 100644 --- a/src/main/java/baritone/utils/BlockStateInterface.java +++ b/src/main/java/baritone/utils/BlockStateInterface.java @@ -53,6 +53,8 @@ public class BlockStateInterface { private final boolean useTheRealWorld; + public final boolean assumeWalkOnWater; + private static final BlockState AIR = Blocks.AIR.defaultBlockState(); public BlockStateInterface(IPlayerContext ctx) { @@ -69,6 +71,7 @@ public BlockStateInterface(IPlayerContext ctx, boolean copyLoadedChunks) { this.provider = (ClientChunkCache) world.getChunkSource(); } this.useTheRealWorld = !Baritone.settings().pathThroughCachedOnly.value; + this.assumeWalkOnWater = Baritone.settings().assumeWalkOnWater.value; if (!ctx.minecraft().isSameThread()) { throw new IllegalStateException("BlockStateInterface must be constructed on the main thread"); } diff --git a/src/main/java/baritone/utils/ToolSet.java b/src/main/java/baritone/utils/ToolSet.java index eca79d90bf..5743aed3a9 100644 --- a/src/main/java/baritone/utils/ToolSet.java +++ b/src/main/java/baritone/utils/ToolSet.java @@ -35,9 +35,9 @@ import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; /** @@ -60,6 +60,14 @@ public class ToolSet { private final LocalPlayer player; + /** + * Snapshot of the hotbar taken on construction. The pathing thread must see one fixed cost function for + * the whole calculation, and it must not touch the live inventory off the main thread. + */ + private final ItemStack[] hotbar = new ItemStack[9]; + + private final int selectedSlot; + /** * Used for evaluating the material cost of a tool. * see {@link #getMaterialCost(ItemStack)} @@ -75,8 +83,12 @@ public class ToolSet { ); public ToolSet(LocalPlayer player) { - breakStrengthCache = new HashMap<>(); + breakStrengthCache = new ConcurrentHashMap<>(); this.player = player; + for (int i = 0; i < 9; i++) { + hotbar[i] = player.getInventory().getItem(i); + } + this.selectedSlot = player.getInventory().getSelectedSlot(); if (Baritone.settings().considerPotionEffects.value) { double amplifier = potionAmplifier(); @@ -143,7 +155,7 @@ public int getBestSlot(Block b, boolean preferSilkTouch, boolean pathingCalculat possible, this lets us make pathing depend on the actual tool to be used (if auto tool is disabled) */ if (!Baritone.settings().autoTool.value && pathingCalculation) { - return player.getInventory().getSelectedSlot(); + return selectedSlot; } int best = 0; @@ -152,7 +164,7 @@ possible, this lets us make pathing depend on the actual tool to be used (if aut boolean bestSilkTouch = false; BlockState blockState = b.defaultBlockState(); for (int i = 0; i < 9; i++) { - ItemStack itemStack = player.getInventory().getItem(i); + ItemStack itemStack = hotbar[i]; if (!Baritone.settings().useSwordToMine.value && itemStack.is(ItemTags.SWORDS)) { continue; } @@ -188,7 +200,7 @@ possible, this lets us make pathing depend on the actual tool to be used (if aut * @return A double containing the destruction ticks with the best tool */ private double getBestDestructionTime(Block b) { - ItemStack stack = player.getInventory().getItem(getBestSlot(b, false, true)); + ItemStack stack = hotbar[getBestSlot(b, false, true)]; return calculateSpeedVsBlock(stack, b.defaultBlockState()) * avoidanceMultiplier(b); } From 1d410c5e98234c63e39a104ec9344bbdfebc3ab4 Mon Sep 17 00:00:00 2001 From: Dek Date: Sun, 30 Aug 2026 15:25:23 -0400 Subject: [PATCH 16/54] make cancelRequested volatile It is set by the game thread and read by the calculation thread, so without it there is nothing that says the search ever has to notice the cancellation. --- src/main/java/baritone/pathing/calc/AbstractNodeCostSearch.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/baritone/pathing/calc/AbstractNodeCostSearch.java b/src/main/java/baritone/pathing/calc/AbstractNodeCostSearch.java index 0bfb6ac3f2..a0b97800ec 100644 --- a/src/main/java/baritone/pathing/calc/AbstractNodeCostSearch.java +++ b/src/main/java/baritone/pathing/calc/AbstractNodeCostSearch.java @@ -58,7 +58,7 @@ public abstract class AbstractNodeCostSearch implements IPathFinder, Helper { private volatile boolean isFinished; - protected boolean cancelRequested; + protected volatile boolean cancelRequested; /** * This is really complicated and hard to explain. I wrote a comment in the old version of MineBot but it was so From 764ef96f3850dc6b68dc67b5632ce25d9a3a6d54 Mon Sep 17 00:00:00 2001 From: Dek Date: Sun, 30 Aug 2026 15:25:23 -0400 Subject: [PATCH 17/54] stop rebuilding throwaway state every tick bestPathSoFar allocates a BetterBlockPos per node and was called on every tick that a calculation was in progress, before the cheap checks that almost always decide the outcome. PathExecutor built an entire CalculationContext per tick to read two fields off it. --- src/main/java/baritone/behavior/PathingBehavior.java | 8 +++++--- src/main/java/baritone/pathing/path/PathExecutor.java | 3 +-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/baritone/behavior/PathingBehavior.java b/src/main/java/baritone/behavior/PathingBehavior.java index 6ed20d50f2..70406117c7 100644 --- a/src/main/java/baritone/behavior/PathingBehavior.java +++ b/src/main/java/baritone/behavior/PathingBehavior.java @@ -139,13 +139,15 @@ private void tickPath() { // we are calculating // are we calculating the right thing though? 🤔 BetterBlockPos calcFrom = inProgress.getStart(); - Optional currentBest = inProgress.bestPathSoFar(); if ((current == null || !current.getPath().getDest().equals(calcFrom)) // if current ends in inProgress's start, then we're ok && !calcFrom.equals(ctx.playerFeet()) && !calcFrom.equals(expectedSegmentStart) // if current starts in our playerFeet or pathStart, then we're ok - && (!currentBest.isPresent() || (!currentBest.get().positions().contains(ctx.playerFeet()) && !currentBest.get().positions().contains(expectedSegmentStart))) // if ) { + // only now is it worth reconstructing the best path so far, which allocates one BetterBlockPos per node + Optional currentBest = inProgress.bestPathSoFar(); // when it was *just* started, currentBest will be empty so we need to also check calcFrom since that's always present - inProgress.cancel(); // cancellation doesn't dispatch any events + if (!currentBest.isPresent() || (!currentBest.get().positions().contains(ctx.playerFeet()) && !currentBest.get().positions().contains(expectedSegmentStart))) { + inProgress.cancel(); // cancellation doesn't dispatch any events + } } } } diff --git a/src/main/java/baritone/pathing/path/PathExecutor.java b/src/main/java/baritone/pathing/path/PathExecutor.java index 04afddfcd7..d76ec17b0d 100644 --- a/src/main/java/baritone/pathing/path/PathExecutor.java +++ b/src/main/java/baritone/pathing/path/PathExecutor.java @@ -27,7 +27,6 @@ import baritone.api.utils.input.Input; import baritone.behavior.PathingBehavior; import baritone.pathing.calc.AbstractNodeCostSearch; -import baritone.pathing.movement.CalculationContext; import baritone.pathing.movement.Movement; import baritone.pathing.movement.MovementHelper; import baritone.pathing.movement.movements.*; @@ -347,7 +346,7 @@ private boolean shouldSprintNextTick() { behavior.baritone.getInputOverrideHandler().setInputForceState(Input.SPRINT, false); // first and foremost, if allowSprint is off, or if we don't have enough hunger, don't try and sprint - if (!new CalculationContext(behavior.baritone, false).canSprint) { + if (!Baritone.settings().allowSprint.value || ctx.player().getFoodData().getFoodLevel() <= 6) { // same as CalculationContext#canSprint, without building a whole context every tick return false; } IMovement current = path.movements().get(pathPosition); From b9af2b5ce1aed4c8877aa81b74e481bde3b303ab Mon Sep 17 00:00:00 2001 From: Dek Date: Sun, 30 Aug 2026 15:25:39 -0400 Subject: [PATCH 18/54] recalculate the elytra segment when a path node is inside a block Finding the nearest node buried in terrain used to just return and hope a later tick had fixed it, which nothing does, so the solver kept aiming at a node inside a wall until it flew into one. Recalculate instead, rejoining the existing path at the last node that is still in open air and still closer to the destination than we are. --- .../process/elytra/ElytraBehavior.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/java/baritone/process/elytra/ElytraBehavior.java b/src/main/java/baritone/process/elytra/ElytraBehavior.java index 7cc23498e1..4985ba22b5 100644 --- a/src/main/java/baritone/process/elytra/ElytraBehavior.java +++ b/src/main/java/baritone/process/elytra/ElytraBehavior.java @@ -336,8 +336,22 @@ private void pathfindAroundObstacles() { } final BetterBlockPos rangeStart = path.get(rangeStartIncl); if (!ElytraBehavior.this.passable(rangeStart.x, rangeStart.y, rangeStart.z, false)) { - // we're in a wall - return; // previous iterations of this function SHOULD have fixed this by now :rage_cat: + // the node closest to us is now inside a block, so the terrain changed since this path was computed + final BetterBlockPos feet = ctx.playerFeet(); + if (!ElytraBehavior.this.passable(feet.x, feet.y, feet.z, false)) { + return; // we're inside a block ourselves, recalculating from here can't succeed + } + int rejoin = rangeEndExcl - 1; + while (rejoin > rangeStartIncl && !ElytraBehavior.this.passable(path.get(rejoin).x, path.get(rejoin).y, path.get(rejoin).z, false)) { + rejoin--; + } + final var wallDest = destinationFixed(); + final OptionalInt rejoinAt = rejoin > rangeStartIncl && path.get(rejoin).distanceSq(wallDest) < feet.distanceSq(wallDest) + ? OptionalInt.of(rejoin) + : OptionalInt.empty(); + this.pathRecalcSegment(rejoinAt) + .thenRun(() -> logVerbose("Recalculated segment, path node was inside a block")); + return; } if (ElytraBehavior.this.process.state != ElytraProcess.State.LANDING && this.ticksNearUnchanged > 100) { From 9113ee8155f4bc72ae715f66d2fa73a7fa9151a7 Mon Sep 17 00:00:00 2001 From: Dek Date: Sun, 30 Aug 2026 15:26:41 -0400 Subject: [PATCH 19/54] match vanilla elytra physics in the flight simulation The simulated step had drifted from LivingEntity.updateFallFlyingMovement: - gravity was the hardcoded 0.08 rather than the player's GRAVITY attribute, and Slow Falling was not accounted for at all - the cosine term came from Mth's lookup table and was multiplied by a min(1, lookDirection.length() / 0.4) factor that vanilla does not have - the "looking down" branch divided by pitchBase2 without vanilla's guard that it is greater than zero, so looking straight up or down produced infinities - drag used float literals, which round differently from the doubles vanilla actually multiplies by Every tick of error compounds over the simulated horizon, and the horizon is what the solver picks a pitch from, so this is the difference between clearing a ridge and flying into it. --- .../process/elytra/ElytraBehavior.java | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/src/main/java/baritone/process/elytra/ElytraBehavior.java b/src/main/java/baritone/process/elytra/ElytraBehavior.java index 4985ba22b5..5ec408e440 100644 --- a/src/main/java/baritone/process/elytra/ElytraBehavior.java +++ b/src/main/java/baritone/process/elytra/ElytraBehavior.java @@ -40,7 +40,9 @@ import net.minecraft.network.protocol.game.ClientboundPlayerPositionPacket; import net.minecraft.util.Mth; import net.minecraft.world.InteractionHand; +import net.minecraft.world.effect.MobEffects; import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.projectile.FireworkRocketEntity; import net.minecraft.world.inventory.ContainerInput; import net.minecraft.world.item.ItemStack; @@ -806,6 +808,8 @@ private final class SolverContext { public final Vec3 motion; public final AABB boundingBox; public final boolean ignoreLava; + public final double gravity; + public final boolean slowFalling; public final FireworkBoost boost; public final IAimProcessor aimProcessor; @@ -823,6 +827,8 @@ public SolverContext(boolean async) { this.motion = ctx.playerMotion(); this.boundingBox = ctx.player().getBoundingBox(); this.ignoreLava = ctx.player().isInLava(); + this.gravity = ctx.player().getAttributeValue(Attributes.GRAVITY); + this.slowFalling = ctx.player().hasEffect(MobEffects.SLOW_FALLING); final Integer fireworkTicksExisted; if (async && ElytraBehavior.this.deployedFireworkLastTick) { @@ -857,6 +863,8 @@ public boolean equals(Object o) { && Objects.equals(this.motion, other.motion) && Objects.equals(this.boundingBox, other.boundingBox) && this.ignoreLava == other.ignoreLava + && this.gravity == other.gravity + && this.slowFalling == other.slowFalling && Objects.equals(this.boost, other.boost); } } @@ -1215,7 +1223,7 @@ private List simulate(final SolverContext context, final Vec3 goalDelta, f ); final Vec3 lookDirection = RotationUtils.calcLookDirectionFromRotation(rotation); - motion = step(motion, lookDirection, rotation.getPitch()); + motion = step(motion, lookDirection, rotation.getPitch(), context.gravity, context.slowFalling); delta = delta.subtract(motion); // Collision box while the player is in motion, with additional padding for safety @@ -1256,7 +1264,16 @@ private List simulate(final SolverContext context, final Vec3 goalDelta, f return displacement; } - private static Vec3 step(final Vec3 motion, final Vec3 lookDirection, final float pitch) { + /** + * A faithful reimplementation of {@code LivingEntity.updateFallFlyingMovement}. Every constant, guard and + * rounding step is kept identical to vanilla so that a simulated trajectory matches what the server will + * actually compute; any divergence here compounds over the simulated horizon and puts the bot into walls. + * + * @param gravity The player's {@code Attributes.GRAVITY} value, which is not always the 0.08 default + * @param slowFalling Whether the player has Slow Falling, which caps gravity while not ascending + */ + private static Vec3 step(final Vec3 motion, final Vec3 lookDirection, final float pitch, + final double gravity, final boolean slowFalling) { double motionX = motion.x; double motionY = motion.y; double motionZ = motion.z; @@ -1264,20 +1281,19 @@ private static Vec3 step(final Vec3 motion, final Vec3 lookDirection, final floa float pitchRadians = pitch * RotationUtils.DEG_TO_RAD_F; double pitchBase2 = Math.sqrt(lookDirection.x * lookDirection.x + lookDirection.z * lookDirection.z); double flatMotion = Math.sqrt(motionX * motionX + motionZ * motionZ); - double thisIsAlwaysOne = lookDirection.length(); - float pitchBase3 = Mth.cos(pitchRadians); - //System.out.println("always the same lol " + -pitchBase + " " + pitchBase3); - //System.out.println("always the same lol " + Math.abs(pitchBase3) + " " + pitchBase2); - //System.out.println("always 1 lol " + thisIsAlwaysOne); - pitchBase3 = (float) ((double) pitchBase3 * (double) pitchBase3 * Math.min(1, thisIsAlwaysOne / 0.4)); - motionY += -0.08 + (double) pitchBase3 * 0.06; + // vanilla uses the exact Math.cos here and keeps the square in double precision, unlike the Mth table + double pitchBase3 = Mth.square(Math.cos((double) pitchRadians)); + // LivingEntity.getEffectiveGravity + double effectiveGravity = slowFalling && motionY <= 0 ? Math.min(gravity, 0.01) : gravity; + + motionY += effectiveGravity * (-1.0 + pitchBase3 * 0.75); if (motionY < 0 && pitchBase2 > 0) { - double speedModifier = motionY * -0.1 * (double) pitchBase3; + double speedModifier = motionY * -0.1 * pitchBase3; motionY += speedModifier; motionX += lookDirection.x * speedModifier / pitchBase2; motionZ += lookDirection.z * speedModifier / pitchBase2; } - if (pitchRadians < 0) { // if you are looking down (below level) + if (pitchRadians < 0 && pitchBase2 > 0) { // if you are looking down (below level) double anotherSpeedModifier = flatMotion * (double) (-Mth.sin(pitchRadians)) * 0.04; motionY += anotherSpeedModifier * 3.2; motionX -= lookDirection.x * anotherSpeedModifier / pitchBase2; @@ -1287,10 +1303,9 @@ private static Vec3 step(final Vec3 motion, final Vec3 lookDirection, final floa motionX += (lookDirection.x / pitchBase2 * flatMotion - motionX) * 0.1; motionZ += (lookDirection.z / pitchBase2 * flatMotion - motionZ) * 0.1; } - motionX *= 0.99f; - motionY *= 0.98f; - motionZ *= 0.99f; - //System.out.println(motionX + " " + motionY + " " + motionZ); + motionX *= 0.9900000095367432; + motionY *= 0.9800000190734863; + motionZ *= 0.9900000095367432; return new Vec3(motionX, motionY, motionZ); } From 70525032710c2402448ea6738d14ea9393ab7ae5 Mon Sep 17 00:00:00 2001 From: Dek Date: Sun, 30 Aug 2026 15:26:59 -0400 Subject: [PATCH 20/54] fly to survive when no pitch reaches the path When solveAngles runs out of relaxations it returned a solution that only had a yaw, leaving the pitch at whatever it happened to be, which in practice means holding the last pitch straight into the terrain. That is the case that matters most: it is what happens when the world changed under a path, when a wall appeared, or when the bot got knocked off course. Fall back to simulating the pitch range and flying the trajectory that stays clear the longest, preferring the one that ends higher on a tie, so the segment recalculation has time to land. If nothing clears more than a dozen ticks and a firework provably buys more than two ticks, force one. --- .../process/elytra/ElytraBehavior.java | 84 ++++++++++++++++++- 1 file changed, 80 insertions(+), 4 deletions(-) diff --git a/src/main/java/baritone/process/elytra/ElytraBehavior.java b/src/main/java/baritone/process/elytra/ElytraBehavior.java index 5ec408e440..9dc7133ae2 100644 --- a/src/main/java/baritone/process/elytra/ElytraBehavior.java +++ b/src/main/java/baritone/process/elytra/ElytraBehavior.java @@ -760,7 +760,71 @@ private Solution solveAngles(final SolverContext context) { } } } - return solution; + return this.solveSurvival(context, solution); + } + + /** + * Last resort when no pitch reaches the path (e.g. the terrain changed and the goal is now walled off): + * fly the pitch that stays collision-free the longest, biased towards climbing, so that the path + * recalculation has time to catch up. If an impact is imminent and a firework would provably extend + * survival, one is forced. + */ + private Solution solveSurvival(final SolverContext context, final Solution unsolved) { + if (landingMode) { + return unsolved; + } + final float yaw = unsolved != null ? unsolved.rotation.getYaw() : ctx.playerRotations().getYaw(); + final int ticks = Math.max(5, Baritone.settings().elytraSimulationTicks.value); + final int ticksBoosted = context.boost.isBoosted() ? Math.max(1, context.boost.getGuaranteedBoostTicks()) : 0; + + final PitchResult best = this.solveSurvivalPitch(context, yaw, ticks, ticksBoosted, 0); + if (best == null) { // cancelled by the game thread + return unsolved; + } + + final int survivedTicks = best.steps.size() - 1; + if (survivedTicks < ticks && survivedTicks <= 12 && !context.boost.isBoosted() && context.hasFireworks) { + final PitchResult boosted = this.solveSurvivalPitch(context, yaw, ticks, 10, 2); + if (boosted != null && boosted.steps.size() > best.steps.size() + 2) { + final Vec3 last = boosted.steps.get(boosted.steps.size() - 1); + this.simulationLine = boosted.steps; + return new Solution(context, new Rotation(yaw, boosted.pitch), context.start.add(last), true, true); + } + } + + this.simulationLine = best.steps; + return new Solution(context, new Rotation(yaw, best.pitch), null, false, false); + } + + private PitchResult solveSurvivalPitch(final SolverContext context, final float yaw, final int ticks, + final int ticksBoosted, final int ticksBoostDelay) { + final float yawRadians = yaw * RotationUtils.DEG_TO_RAD_F; + // a goal far along the direction we are about to face, so that the simulated yaw stays fixed + final Vec3 direction = new Vec3(-Mth.sin(yawRadians) * 1e4, 0, Mth.cos(yawRadians) * 1e4); + final float currentPitch = ctx.playerRotations().getPitch(); + + PitchResult best = null; + // scan the climbing side (negative pitch) first; new walls are usually escapable from above + for (float pitch = currentPitch; pitch >= -90; pitch -= 3) { + if (Thread.interrupted()) return null; + best = this.betterSurvival(context, direction, pitch, ticks, ticksBoosted, ticksBoostDelay, best); + if (best != null && best.steps.size() - 1 >= ticks) return best; + } + for (float pitch = currentPitch + 3; pitch <= 90; pitch += 3) { + if (Thread.interrupted()) return null; + best = this.betterSurvival(context, direction, pitch, ticks, ticksBoosted, ticksBoostDelay, best); + if (best != null && best.steps.size() - 1 >= ticks) return best; + } + return best; + } + + private PitchResult betterSurvival(final SolverContext context, final Vec3 direction, final float pitch, + final int ticks, final int ticksBoosted, final int ticksBoostDelay, + final PitchResult best) { + final List steps = this.simulate(context, direction, pitch, ticks, ticksBoosted, ticksBoostDelay, true); + // longer survival wins; on equal survival prefer the trajectory that ends higher up + final double score = (steps.size() - 1) * 1000.0 + steps.get(steps.size() - 1).y; + return best == null || score > best.dot ? new PitchResult(pitch, score, steps) : best; } private void tickUseFireworks(final Vec3 start, final Vec3 goingTo, final boolean isBoosted, final boolean forceUseFirework) { @@ -810,6 +874,7 @@ private final class SolverContext { public final boolean ignoreLava; public final double gravity; public final boolean slowFalling; + public final boolean hasFireworks; public final FireworkBoost boost; public final IAimProcessor aimProcessor; @@ -829,6 +894,14 @@ public SolverContext(boolean async) { this.ignoreLava = ctx.player().isInLava(); this.gravity = ctx.player().getAttributeValue(Attributes.GRAVITY); this.slowFalling = ctx.player().hasEffect(MobEffects.SLOW_FALLING); + boolean fireworks = false; + for (ItemStack stack : ctx.player().getInventory().getNonEquipmentItems()) { + if (isFireworks(stack)) { + fireworks = true; + break; + } + } + this.hasFireworks = fireworks; final Integer fireworkTicksExisted; if (async && ElytraBehavior.this.deployedFireworkLastTick) { @@ -865,6 +938,7 @@ public boolean equals(Object o) { && this.ignoreLava == other.ignoreLava && this.gravity == other.gravity && this.slowFalling == other.slowFalling + && this.hasFireworks == other.hasFireworks && Objects.equals(this.boost, other.boost); } } @@ -1163,7 +1237,8 @@ private PitchResult solvePitch(final SolverContext context, final Vec3 goal, fin pitch, ticks, ticksBoosted, - ticksBoostDelay + ticksBoostDelay, + false ); if (displacement == null) { continue; @@ -1203,7 +1278,7 @@ private PitchResult solvePitch(final SolverContext context, final Vec3 goal, fin } private List simulate(final SolverContext context, final Vec3 goalDelta, final float pitch, final int ticks, - final int ticksBoosted, final int ticksBoostDelay) { + final int ticksBoosted, final int ticksBoostDelay, final boolean stopOnImpact) { final ITickableAimProcessor aimProcessor = context.aimProcessor.fork(); Vec3 delta = goalDelta; Vec3 motion = context.motion; @@ -1242,7 +1317,8 @@ private List simulate(final SolverContext context, final Vec3 goalDelta, f for (int y = ymin; y < ymax; y++) { for (int z = zmin; z < zmax; z++) { if (!this.passable(x, y, z, context.ignoreLava)) { - return null; + // displacement only covers the ticks before the impact + return stopOnImpact ? displacement : null; } } } From 3fe69125ac95c3641f88cff41d0d20f41ce5a8f8 Mon Sep 17 00:00:00 2001 From: Dek Date: Sun, 30 Aug 2026 15:27:14 -0400 Subject: [PATCH 21/54] add elytraPathLookahead How far ahead of the player the solver looks for a node to aim at was a bare 20 in the middle of solveAngles. At firework speed 20 nodes is not much runway, and raising it is the knob that decides how much the bot cuts corners, so it belongs in the settings. --- src/api/java/baritone/api/Settings.java | 7 +++++++ src/main/java/baritone/process/elytra/ElytraBehavior.java | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/api/java/baritone/api/Settings.java b/src/api/java/baritone/api/Settings.java index 968cf986c7..188d926aa6 100644 --- a/src/api/java/baritone/api/Settings.java +++ b/src/api/java/baritone/api/Settings.java @@ -1445,6 +1445,13 @@ public final class Settings { */ public final Setting elytraPitchRange = new Setting<>(25); + /** + * The number of path nodes ahead of the player that are considered as flight targets. Higher values let the bot + * commit to a further-away node and cut corners, which is smoother at firework speed, but are computationally + * more expensive. + */ + public final Setting elytraPathLookahead = new Setting<>(20); + /** * The minimum speed that the player can drop to (in blocks/tick) before a firework is automatically deployed. */ diff --git a/src/main/java/baritone/process/elytra/ElytraBehavior.java b/src/main/java/baritone/process/elytra/ElytraBehavior.java index 9dc7133ae2..ba1726cb60 100644 --- a/src/main/java/baritone/process/elytra/ElytraBehavior.java +++ b/src/main/java/baritone/process/elytra/ElytraBehavior.java @@ -679,6 +679,7 @@ private Solution solveAngles(final SolverContext context) { final NetherPath path = context.path; final int playerNear = landingMode ? path.size() - 1 : context.playerNear; final Vec3 start = context.start; + final int scanAhead = Math.max(1, Baritone.settings().elytraPathLookahead.value); Solution solution = null; for (int relaxation = 0; relaxation < 3; relaxation++) { // try for a strict solution first, then relax more and more (if we're in a corner or near some blocks, it will have to relax its constraints a bit) @@ -687,7 +688,7 @@ private Solution solveAngles(final SolverContext context) { //int minStep = Math.max(0, playerNear - relaxation); int minStep = playerNear; - for (int i = Math.min(playerNear + 20, path.size() - 1); i >= minStep; i--) { + for (int i = Math.min(playerNear + scanAhead, path.size() - 1); i >= minStep; i--) { if (Thread.interrupted()) return null; // cancelled by the game thread final List> candidates = new ArrayList<>(); for (int dy : heights) { From 892c80c69ac4a6a3af37734aeea72ad7bb58ad8c Mon Sep 17 00:00:00 2001 From: Dek Date: Sun, 30 Aug 2026 15:27:14 -0400 Subject: [PATCH 22/54] take off from flat ground when there is no ledge to walk off elytraAutoJump only knew how to walk to an overhang and step off it. Everywhere that has no overhang within reach - which on 2b2t is most of the overworld and most of a nether highway - it spent a couple of seconds pathfinding and then told the user to go find a cliff. It also had three ways to end up standing still forever: a leftover path segment that blocks the walk off calculation from ever starting, a walk off path that reaches its goal by stepping down instead of falling, and a GoalYLevel that is already satisfied where we stand (landing at exactly y=31 in the nether hit this every time). Add a standing takeoff: jump, open the elytra on the way up the same way the vanilla double tap does, and light a firework the moment it opens, because a glide that starts with no speed a block and a half off the floor only ends one way. The elytra path is computed before leaving the ground, since the handful of ticks between opening the elytra and hitting the floor is no time to compute one. Guarded by elytraStandingTakeoff, capped at three attempts so a bad spot cannot burn a stack of rockets, and each of the stall cases now has a way out. --- src/api/java/baritone/api/Settings.java | 6 + .../java/baritone/process/ElytraProcess.java | 258 +++++++++++++++++- .../process/elytra/ElytraBehavior.java | 57 +++- 3 files changed, 299 insertions(+), 22 deletions(-) diff --git a/src/api/java/baritone/api/Settings.java b/src/api/java/baritone/api/Settings.java index 188d926aa6..b5295bd41d 100644 --- a/src/api/java/baritone/api/Settings.java +++ b/src/api/java/baritone/api/Settings.java @@ -1497,6 +1497,12 @@ public final class Settings { */ public final Setting elytraAutoJump = new Setting<>(false); + /** + * When {@link #elytraAutoJump} can't find a ledge to walk off of, jump on the spot and light a firework the + * moment the elytra opens instead of giving up. Costs one firework per attempt. + */ + public final Setting elytraStandingTakeoff = new Setting<>(true); + /** * The seed used to generate chunks for long distance elytra path-finding in the nether. * Defaults to 2b2t's nether seed. diff --git a/src/main/java/baritone/process/ElytraProcess.java b/src/main/java/baritone/process/ElytraProcess.java index 7ee911cded..d244360ca7 100644 --- a/src/main/java/baritone/process/ElytraProcess.java +++ b/src/main/java/baritone/process/ElytraProcess.java @@ -37,6 +37,7 @@ import baritone.api.utils.RotationUtils; import baritone.api.utils.input.Input; import baritone.pathing.movement.CalculationContext; +import baritone.pathing.movement.MovementHelper; import baritone.pathing.movement.movements.MovementFall; import baritone.process.elytra.*; import baritone.utils.BaritoneProcessHelper; @@ -47,6 +48,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.NonNullList; import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.ServerboundPlayerCommandPacket; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; @@ -84,6 +86,37 @@ public class ElytraProcess extends BaritoneProcessHelper implements IBaritonePro private static final int SHORT_LANDING_COLUMN_HEIGHT = 15; private static final int LONG_LANDING_COLUMN_HEIGHT = 39; private static final long LANDING_SEARCH_BUDGET_NANOS = TimeUnit.MILLISECONDS.toNanos(25); // half a tick + /** + * The shortest drop {@link WalkOffCalculationContext} offers, and therefore the least we have to ask to descend + * for the walk off path to be forced to contain a {@link MovementFall} instead of a flight of stairs. + */ + private static final int TAKEOFF_MIN_FALL_HEIGHT = 8; + /** + * How long to sit in a takeoff state with neither a path nor a calculation running before giving up. Both of + * those are dead ends: nothing in this process starts another calculation, so we would stand still forever. + */ + private static final int TAKEOFF_STALL_TICKS = 60; + /** + * How long a standing takeoff gets to leave the ground and open the elytra before it is written off as a dud + * (a ceiling we didn't account for, knockback, a cobweb, ...). + */ + private static final int STANDING_TAKEOFF_TIMEOUT_TICKS = 40; + /** + * How many standing takeoffs to attempt before giving up. Each one costs a firework, so if this many in a row + * have put us straight back on the ground then something about this spot is wrong and retrying just burns + * rockets. + */ + private static final int MAX_STANDING_TAKEOFFS = 3; + /** + * How many ledges to walk towards before concluding that walking off one is never going to happen. + */ + private static final int MAX_WALK_OFF_ATTEMPTS = 2; + private int takeoffStallTicks; + private int standingTakeoffs; + private int walkOffAttempts; + private int takeoffAirborneTicks; + private boolean walkOffImpossible; + private boolean takeoffBoostPending; private int landingColumnHeight = SHORT_LANDING_COLUMN_HEIGHT; private Set badLandingSpots = new HashSet<>(); private LandingSearchState landingSearchState; @@ -100,6 +133,12 @@ public void onLostControl(boolean destroyNpf) { this.landingSearchState = null; this.reachedGoal = false; this.goal = null; + this.takeoffStallTicks = 0; + this.standingTakeoffs = 0; + this.walkOffAttempts = 0; + this.takeoffAirborneTicks = 0; + this.walkOffImpossible = false; + this.takeoffBoostPending = false; destroyBehaviorAsync(); if (destroyNpf) { destroyNpfContextAsync(); @@ -132,7 +171,8 @@ public void resetState() { } } - private static final String AUTO_JUMP_FAILURE_MSG = "Failed to compute a walking path to a spot to jump off from. Consider starting from a higher location, near an overhang. Or, you can disable elytraAutoJump and just manually begin gliding."; + private static final String TAKEOFF_ADVICE_MSG = "Consider starting from a higher location, near an overhang. Or, you can disable elytraAutoJump and just manually begin gliding."; + private static final String AUTO_JUMP_FAILURE_MSG = "Failed to compute a walking path to a spot to jump off from. " + TAKEOFF_ADVICE_MSG; @Override public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { @@ -170,6 +210,9 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { this.behavior.onTick(); if (calcFailed) { + if (this.state == State.LOCATE_JUMP || this.state == State.GET_TO_JUMP) { + return standingTakeoff(); + } onLostControl(); logDirect(AUTO_JUMP_FAILURE_MSG); return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); @@ -240,6 +283,12 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { } if (ctx.player().isFallFlying()) { + if (this.state == State.TAKEOFF_JUMP) { + // the elytra opened without us having asked for it, pick up from wherever that leaves us + this.state = State.START_FLYING; + this.takeoffStallTicks = 0; + this.takeoffBoostPending = true; + } behavior.landingMode = this.state == State.LANDING; this.goal = null; baritone.getInputOverrideHandler().clearAllKeys(); @@ -250,6 +299,12 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { this.behavior.npfContext.releaseReadLock(); } } + if (this.takeoffBoostPending) { + // a takeoff boost that couldn't be used the moment the elytra opened. after behavior.tick() so + // that it doesn't fight the boost bookkeeping if the solver already decided to use one + this.takeoffBoostPending = false; + this.behavior.useFireworkForTakeoff(); + } return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); } else if (this.state == State.LANDING) { if (ctx.playerMotion().multiply(1, 0, 1).length() > 0.001) { @@ -264,9 +319,12 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { } if (this.state == State.FLYING || this.state == State.START_FLYING) { - this.state = ctx.player().onGround() && Baritone.settings().elytraAutoJump.value - ? State.LOCATE_JUMP - : State.START_FLYING; + if (ctx.player().onGround() && Baritone.settings().elytraAutoJump.value) { + this.state = State.LOCATE_JUMP; + this.takeoffStallTicks = 0; + } else { + this.state = State.START_FLYING; + } } if (this.state == State.LOCATE_JUMP) { @@ -275,11 +333,24 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { onLostControl(); return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); } + if (this.walkOffImpossible) { + // there is nothing to walk off of around here, and we already spent a couple of seconds of + // pathfinding finding that out once + return standingTakeoff(); + } if (this.goal == null) { - this.goal = new GoalYLevel(31); + this.goal = takeoffGoal(); } - final IPathExecutor executor = baritone.getPathingBehavior().getCurrent(); - if (executor != null && executor.getPath().getGoal() == this.goal) { + IPathExecutor executor = baritone.getPathingBehavior().getCurrent(); + if (executor != null && executor.getPath().getGoal() != this.goal) { + // a segment left over from whatever we were doing before takeoff. it can never finish while we're + // pausing pathing, and while it exists secretInternalSetGoalAndPath refuses to start the walk off + // calculation, so neither side of this ever moves unless we drop it here + baritone.getPathingBehavior().secretInternalSegmentCancel(); + executor = null; + } + if (executor != null) { + this.takeoffStallTicks = 0; final IMovement fall = executor.getPath().movements().stream() .filter(movement -> movement instanceof MovementFall) .findFirst().orElse(null); @@ -299,10 +370,14 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { }); this.state = State.PAUSE; } else { - onLostControl(); - logDirect(AUTO_JUMP_FAILURE_MSG); - return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); + return standingTakeoff(); } + } else if (baritone.getPathingBehavior().getInProgress().isPresent()) { + this.takeoffStallTicks = 0; + } else if (++this.takeoffStallTicks > TAKEOFF_STALL_TICKS) { + // no path and nothing being calculated: SET_GOAL_AND_PAUSE has already decided it has nothing to + // do, so the executor we're waiting on is never going to appear + return standingTakeoff(); } return new PathingCommandContext(this.goal, PathingCommandType.SET_GOAL_AND_PAUSE, new WalkOffCalculationContext(baritone)); } @@ -312,6 +387,30 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { return new PathingCommand(null, PathingCommandType.REQUEST_PAUSE); } + if (this.state == State.TAKEOFF_JUMP) { + if (++this.takeoffStallTicks > STANDING_TAKEOFF_TIMEOUT_TICKS) { + return abortTakeoff(ctx.player().onGround() + ? "Couldn't get off the ground. " + : "Got into the air but the elytra wouldn't open. "); + } + baritone.getInputOverrideHandler().clearAllKeys(); + if (ctx.player().onGround()) { + // an ordinary jump. vanilla physics, and the movement packets it produces are what the server + // checks its own idea of onGround against before it will accept the request below + this.takeoffAirborneTicks = 0; + baritone.getInputOverrideHandler().setInputForceState(Input.JUMP, true); + } else if (this.takeoffAirborneTicks++ > 0) { + // the second press of the double tap. openElytra() is what actually opens it, but the client + // reports every change of the key to the server (ServerboundPlayerInputPacket), so the inputs it + // sees are the ones a player who opens an elytra sends: press, release, press + baritone.getInputOverrideHandler().setInputForceState(Input.JUMP, true); + if (openElytra()) { + this.state = State.START_FLYING; + } + } + return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); + } + if (this.state == State.GET_TO_JUMP) { final IPathExecutor executor = baritone.getPathingBehavior().getCurrent(); // TODO 1.21.5: replace `ctx.player().getDeltaMovement().y < -0.377` with `ctx.player().fallDistance > 1.0f` @@ -321,8 +420,23 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { && executor.getPath().movements().get(executor.getPosition()) instanceof MovementFall; if (canStartFlying) { + this.takeoffStallTicks = 0; this.state = State.START_FLYING; } else { + if (executor != null || baritone.getPathingBehavior().getInProgress().isPresent()) { + this.takeoffStallTicks = 0; + } else if (++this.takeoffStallTicks > TAKEOFF_STALL_TICKS) { + // the walk off path ran out without us ever falling off it, either because we reached the goal + // by stepping down or because the segment got cancelled. PathingBehavior won't re-path once + // it considers the goal met, so go pick a new ledge instead of standing here + if (++this.walkOffAttempts >= MAX_WALK_OFF_ATTEMPTS) { + return standingTakeoff(); + } + this.takeoffStallTicks = 0; + this.goal = null; + this.state = State.LOCATE_JUMP; + return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); + } return new PathingCommand(null, PathingCommandType.SET_GOAL_AND_PATH); } } @@ -341,6 +455,127 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); } + /** + * Takes off from where we are standing: jump, open the elytra on the way up, and light a firework the moment + * it opens, since a glide that starts with no speed a block and a half off the floor only ends one way. This + * is what flat terrain leaves us with, where {@link WalkOffCalculationContext} has no ledge to offer and the + * alternative is standing around telling the user to go find a cliff. + */ + private PathingCommand standingTakeoff() { + this.walkOffImpossible = true; + this.goal = null; + this.takeoffStallTicks = 0; + this.takeoffAirborneTicks = 0; + baritone.getPathingBehavior().secretInternalSegmentCancel(); + + if (!Baritone.settings().elytraStandingTakeoff.value) { + return abortTakeoff("There is no spot to jump off from, and elytraStandingTakeoff is off. "); + } + if (this.standingTakeoffs >= MAX_STANDING_TAKEOFFS) { + return abortTakeoff("Took off from here " + this.standingTakeoffs + " times and ended up back on the ground every time. "); + } + final BetterBlockPos feet = ctx.playerFeet(); + if (!MovementHelper.fullyPassable(ctx, feet.above(2)) || !MovementHelper.fullyPassable(ctx, feet.above(3))) { + return abortTakeoff("There is no spot to jump off from, and not enough room above me to jump and open the elytra. "); + } + if (!this.behavior.selectFirework()) { + return abortTakeoff("There is no spot to jump off from, and no fireworks in my hotbar to take off from here with. "); + } + if (this.standingTakeoffs++ == 0) { + logDirect("No spot to jump off from, taking off from here instead."); + } + // the elytra path has to exist before we leave the ground: the behavior does nothing without one, and the + // handful of ticks between opening the elytra and hitting the ground is no time to compute one + this.state = State.PAUSE; + this.behavior.pathManager.pathToDestination(feet).whenComplete((result, ex) -> { + if (ex == null) { + this.state = State.TAKEOFF_JUMP; + return; + } + onLostControl(); + }); + return new PathingCommand(null, PathingCommandType.REQUEST_PAUSE); + } + + /** + * Opens the elytra mid jump, exactly the way {@link net.minecraft.client.player.LocalPlayer} does it when a + * player double taps jump: set the flag locally, then tell the server. Doing it here instead of by toggling + * the jump key means it doesn't hang on the key going up and back down on precisely the right ticks, and it + * is the same thing on the wire either way - the server has already been sent the movement packets from an + * ordinary jump, so it agrees we are off the ground by the time the request reaches it. + * + * @return {@code true} if the elytra is now open + */ + private boolean openElytra() { + if (!ctx.player().tryToStartFallFlying()) { + return false; + } + ctx.player().connection.send(new ServerboundPlayerCommandPacket(ctx.player(), ServerboundPlayerCommandPacket.Action.START_FALL_FLYING)); + // no speed and a block and a half of air underneath: the boost can't wait for the solver to ask for one + // on the next tick. the elytra is open as far as the client is concerned, so the rocket attaches to us + this.takeoffBoostPending = !this.behavior.useFireworkForTakeoff(); + return true; + } + + private PathingCommand abortTakeoff(String reason) { + onLostControl(); + logDirect(reason + TAKEOFF_ADVICE_MSG); + return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); + } + + /** + * Where to walk to in order to take off. + *

+ * {@link WalkOffCalculationContext} only offers single steps and falls of at least + * {@link #TAKEOFF_MIN_FALL_HEIGHT} as ways down, so asking to descend at least that far is what forces the + * path to contain a {@link MovementFall} at all. Aiming deeper than that is what makes the biggest reachable + * drop the cheapest route: a long fall costs a handful of ticks where the same descent by single steps costs + * roughly 8 ticks a block, so the pathfinder will happily walk a long way to an overhang. In the nether the + * lava ocean at y=31 is as deep as it is worth aiming, since anything that reaches it is already more runway + * than we need. + *

+ * The level is always strictly below our feet. A goal we are already standing in is never pathed to at all + * ({@link baritone.behavior.PathingBehavior#secretInternalSetGoalAndPath}), which used to leave the process + * paused forever waiting on an executor that nothing was calculating - most easily hit by landing at exactly + * y=31 in the nether, where the old fixed {@code GoalYLevel(31)} was already satisfied. + */ + private Goal takeoffGoal() { + final int feetY = ctx.playerFeet().y; + final int minY = ctx.world().dimensionType().minY(); + final int deepest = ctx.world().dimension() == Level.NETHER ? minY + 31 : minY; + final int level = Math.min(feetY - 1, Math.max(minY, Math.min(deepest, feetY - TAKEOFF_MIN_FALL_HEIGHT))); + return new GoalDescendTo(level); + } + + /** + * {@link GoalYLevel} is an exact-level goal: it is satisfied only by standing at the level, and asks to climb + * back up when below it. For a takeoff all that matters is getting down, so anything at or below the level + * will do. + */ + private static final class GoalDescendTo implements Goal { + + private final int level; + + private GoalDescendTo(int level) { + this.level = level; + } + + @Override + public boolean isInGoal(int x, int y, int z) { + return y <= this.level; + } + + @Override + public double heuristic(int x, int y, int z) { + return y > this.level ? GoalYLevel.calculate(this.level, y) : 0; + } + + @Override + public String toString() { + return "GoalDescendTo{y<=" + this.level + "}"; + } + } + public void landingSpotIsBad(BetterBlockPos endPos) { badLandingSpots.add(endPos); goingToLandingSpot = false; @@ -509,6 +744,7 @@ public enum State { LOCATE_JUMP("Finding spot to jump off"), PAUSE("Waiting for elytra path"), GET_TO_JUMP("Walking to takeoff"), + TAKEOFF_JUMP("Taking off"), START_FLYING("Begin flying"), FLYING("Flying"), LANDING("Landing"); @@ -562,7 +798,7 @@ public static final class WalkOffCalculationContext extends CalculationContext { public WalkOffCalculationContext(IBaritone baritone) { super(baritone, true); this.allowFallIntoLava = true; - this.minFallHeight = 8; + this.minFallHeight = TAKEOFF_MIN_FALL_HEIGHT; this.maxFallHeightNoWater = 10000; } diff --git a/src/main/java/baritone/process/elytra/ElytraBehavior.java b/src/main/java/baritone/process/elytra/ElytraBehavior.java index ba1726cb60..46da70ed33 100644 --- a/src/main/java/baritone/process/elytra/ElytraBehavior.java +++ b/src/main/java/baritone/process/elytra/ElytraBehavior.java @@ -850,19 +850,54 @@ private void tickUseFireworks(final Vec3 start, final Vec3 goingTo, final boolea && (ctx.player().position().y < goingTo.y - 5 || start.distanceTo(new Vec3(goingTo.x + 0.5, ctx.player().position().y, goingTo.z + 0.5)) > 5) // UGH!!!!!!! && currentSpeed < elytraFireworkSpeed * elytraFireworkSpeed)) ) { - // Prioritize boosting fireworks over regular ones - // TODO: Take the minimum boost time into account? - if (!baritone.getInventoryBehavior().throwaway(true, ElytraBehavior::isBoostingFireworks) && - !baritone.getInventoryBehavior().throwaway(true, ElytraBehavior::isFireworks)) { - logDirect("no fireworks"); - return; - } logVerbose("attempting to use firework" + (forceUseFirework ? " (forced)" : "")); - ctx.playerController().processRightClick(ctx.player(), ctx.world(), InteractionHand.MAIN_HAND); - this.minimumBoostTicks = 10 * (1 + getFireworkBoost(ctx.player().getItemInHand(InteractionHand.MAIN_HAND)).orElse(0)); - this.remainingFireworkTicks = 10; - this.deployedFireworkLastTick = true; + this.useFirework(); + } + } + + /** + * Lights a firework to get off the ground, for an elytra that was opened from a standing jump instead of a + * fall. There is no speed and only a couple of blocks of air to work with, so this can't wait for the solver + * to decide that a boost is warranted. + * + * @return {@code true} if a firework is now burning, {@code false} if we have none to use + */ + public boolean useFireworkForTakeoff() { + if (this.remainingFireworkTicks > 0 || this.getAttachedFirework().isPresent()) { + return true; + } + return this.useFirework(); + } + + /** + * @return {@code true} if a firework was used + */ + private boolean useFirework() { + // the main hand is what processRightClick uses, and selectFirework settles for putting something harmless + // there when the only firework we have is in the off hand, which would right click nothing at all + if (!this.selectFirework() || !isFireworks(ctx.player().getItemInHand(InteractionHand.MAIN_HAND))) { + logDirect("no fireworks"); + return false; } + ctx.playerController().processRightClick(ctx.player(), ctx.world(), InteractionHand.MAIN_HAND); + this.minimumBoostTicks = 10 * (1 + getFireworkBoost(ctx.player().getItemInHand(InteractionHand.MAIN_HAND)).orElse(0)); + this.remainingFireworkTicks = 10; + this.deployedFireworkLastTick = true; + return true; + } + + /** + * Puts a firework in the main hand. Picking one that is already in the hotbar is instant, but bringing one up + * from the rest of the inventory is a window click, and those only happen while stationary, so a takeoff has + * to call this before leaving the ground. + * + * @return {@code true} if the main hand now holds a firework + */ + public boolean selectFirework() { + // Prioritize boosting fireworks over regular ones + // TODO: Take the minimum boost time into account? + return baritone.getInventoryBehavior().throwaway(true, ElytraBehavior::isBoostingFireworks) + || baritone.getInventoryBehavior().throwaway(true, ElytraBehavior::isFireworks); } private final class SolverContext { From e1c56e4a2aef0daf95a5db7467b8bbea114a8ce3 Mon Sep 17 00:00:00 2001 From: Dek Date: Sun, 30 Aug 2026 15:51:32 -0400 Subject: [PATCH 23/54] document the fork and version the release jars The README was still upstream's, down to the dead Travis badge and download links for Minecraft versions this fork does not build. Replace it with what this fork actually is, which versions it covers and what it changes. The release workflow named every jar -SNAPSHOT regardless of the tag, so two releases of the same Minecraft version shipped identically named files. Take the version from the tag, the way the CI build already does, and fail the release rather than publishing an empty one if the build produced no jars. Releases run the tests now. --- .github/workflows/build-release.yml | 28 ++- README.md | 267 +++++++++++++++------------- 2 files changed, 167 insertions(+), 128 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 44fdbd86f7..e32cd6001e 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -17,14 +17,22 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Get Java version - id: java - run: echo "version=$(grep '^java_version=' gradle.properties | cut -d= -f2)" >> $GITHUB_OUTPUT - - - name: Set up JDK ${{ steps.java.outputs.version }} + - name: Read build config + id: cfg + run: | + mc=$(grep '^minecraft_version=' gradle.properties | cut -d= -f2) + version=${GITHUB_REF_NAME#v} + { + echo "java=$(grep '^java_version=' gradle.properties | cut -d= -f2)" + echo "mc=$mc" + echo "version=$version" + echo "modver=${version%-$mc}" + } >> "$GITHUB_OUTPUT" + + - name: Set up JDK ${{ steps.cfg.outputs.java }} uses: actions/setup-java@v4 with: - java-version: ${{ steps.java.outputs.version }} + java-version: ${{ steps.cfg.outputs.java }} distribution: 'zulu' - name: Setup Gradle @@ -34,10 +42,16 @@ jobs: run: chmod +x gradlew - name: Build with Gradle - run: ./gradlew build -x test -Porg.gradle.java.installations.auto-detect=false "-Porg.gradle.java.installations.paths=$JAVA_HOME" + run: > + ./gradlew build + -Pmod_version=${{ steps.cfg.outputs.version }} + -Porg.gradle.java.installations.auto-detect=false + "-Porg.gradle.java.installations.paths=$JAVA_HOME" - name: Release if: startsWith(github.ref, 'refs/tags/v') uses: softprops/action-gh-release@v2 with: + name: Baritone ${{ steps.cfg.outputs.modver }} for Minecraft ${{ steps.cfg.outputs.mc }} files: dist/* + fail_on_unmatched_files: true diff --git a/README.md b/README.md index 65cbf9321d..6423912e63 100644 --- a/README.md +++ b/README.md @@ -1,121 +1,154 @@ # Baritone

- GitHub All Releases + Build + Tests + Release + License

- Minecraft - Minecraft - Minecraft - Minecraft - Minecraft - Minecraft - Minecraft - Minecraft - Minecraft - Minecraft - Minecraft + 1.21.4 + 1.21.5 + 1.21.8 + 1.21.11 + 26.1 + 26.2

-

- Build Status - Release - License - Codacy Badge - Code of Conduct - Known Vulnerabilities - Contributions welcome - Issues - GitHub issues-closed - Pull Requests - GitHub contributors - GitHub commits - Code size - GitHub repo size - Lines of Code - yes -

- -

- Impact integration - Lambda integration - ForgeHax integration - Aristois add-on integration - rootNET integration - Future integration - RusherHack integration -

- -

- forthebadge - forthebadge -

- -A Minecraft pathfinder bot. - -Baritone is the pathfinding system used in [Impact](https://impactclient.net/) since 4.4. [Here's](https://www.youtube.com/watch?v=StquF69-_wI) a (very old!) video I made showing off what it can do. - -[**Baritone Discord Server**](http://discord.gg/s6fRBAUpmr) - -**Quick download links:** - -| Forge | Fabric | NeoForge | -|---------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------| -| [1.12.2 Forge](https://github.com/cabaletta/baritone/releases/download/v1.2.19/baritone-api-forge-1.2.19.jar) | | | -| [1.16.5 Forge](https://github.com/cabaletta/baritone/releases/download/v1.6.5/baritone-api-forge-1.6.5.jar) | [1.16.5 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.6.5/baritone-api-fabric-1.6.5.jar) | | -| [1.17.1 Forge](https://github.com/cabaletta/baritone/releases/download/v1.7.3/baritone-api-forge-1.7.3.jar) | [1.17.1 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.7.3/baritone-api-fabric-1.7.3.jar) | | -| [1.18.2 Forge](https://github.com/cabaletta/baritone/releases/download/v1.8.6/baritone-api-forge-1.8.6.jar) | [1.18.2 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.8.6/baritone-api-fabric-1.8.6.jar) | | -| [1.19.2 Forge](https://github.com/cabaletta/baritone/releases/download/v1.9.4/baritone-api-forge-1.9.4.jar) | [1.19.2 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.9.4/baritone-api-fabric-1.9.4.jar) | | -| [1.19.3 Forge](https://github.com/cabaletta/baritone/releases/download/v1.9.1/baritone-api-forge-1.9.1.jar) | [1.19.3 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.9.1/baritone-api-fabric-1.9.1.jar) | | -| [1.19.4 Forge](https://github.com/cabaletta/baritone/releases/download/v1.9.5/baritone-api-forge-1.9.5.jar) | [1.19.4 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.9.5/baritone-api-fabric-1.9.5.jar) | | -| [1.20.1 Forge](https://github.com/cabaletta/baritone/releases/download/v1.10.3/baritone-api-forge-1.10.3.jar) | [1.20.1 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.10.3/baritone-api-fabric-1.10.3.jar) | | -| [1.20.3 Forge](https://github.com/cabaletta/baritone/releases/download/v1.10.4/baritone-api-forge-1.10.4.jar) | [1.20.3 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.10.4/baritone-api-fabric-1.10.4.jar) | [1.20.3 NeoForge](https://github.com/cabaletta/baritone/releases/download/v1.10.4/baritone-api-neoforge-1.10.4.jar) | -| [1.20.4 Forge](https://github.com/cabaletta/baritone/releases/download/v1.10.4/baritone-api-forge-1.10.4.jar) | [1.20.4 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.10.4/baritone-api-fabric-1.10.4.jar) | [1.20.4 NeoForge](https://github.com/cabaletta/baritone/releases/download/v1.10.4/baritone-api-neoforge-1.10.4.jar) | -| [1.21.1 Forge](https://github.com/cabaletta/baritone/releases/download/v1.11.2/baritone-api-forge-1.11.2.jar) | [1.21.1 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.11.2/baritone-api-fabric-1.11.2.jar) | [1.21.1 NeoForge](https://github.com/cabaletta/baritone/releases/download/v1.11.2/baritone-api-neoforge-1.11.2.jar) | -| [1.21.3 Forge](https://github.com/cabaletta/baritone/releases/download/v1.11.1/baritone-api-forge-1.11.1.jar) | [1.21.3 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.11.1/baritone-api-fabric-1.11.1.jar) | [1.21.3 NeoForge](https://github.com/cabaletta/baritone/releases/download/v1.11.1/baritone-api-neoforge-1.11.1.jar) | -| [1.21.4 Forge](https://github.com/cabaletta/baritone/releases/download/v1.13.1/baritone-api-forge-1.13.1.jar) | [1.21.4 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.13.1/baritone-api-fabric-1.13.1.jar) | [1.21.4 NeoForge](https://github.com/cabaletta/baritone/releases/download/v1.13.1/baritone-api-neoforge-1.13.1.jar) | -| [1.21.5 Forge](https://github.com/cabaletta/baritone/releases/download/v1.14.0/baritone-api-forge-1.14.0.jar) | [1.21.5 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.14.0/baritone-api-fabric-1.14.0.jar) | [1.21.5 NeoForge](https://github.com/cabaletta/baritone/releases/download/v1.14.0/baritone-api-neoforge-1.14.0.jar) | -| [1.21.6 Forge](https://github.com/cabaletta/baritone/releases/download/v1.15.0/baritone-api-forge-1.15.0.jar) | [1.21.6 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.15.0/baritone-api-fabric-1.15.0.jar) | [1.21.6 NeoForge](https://github.com/cabaletta/baritone/releases/download/v1.15.0/baritone-api-neoforge-1.15.0.jar) | -| [1.21.7 Forge](https://github.com/cabaletta/baritone/releases/download/v1.15.0/baritone-api-forge-1.15.0.jar) | [1.21.7 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.15.0/baritone-api-fabric-1.15.0.jar) | [1.21.7 NeoForge](https://github.com/cabaletta/baritone/releases/download/v1.15.0/baritone-api-neoforge-1.15.0.jar) | -| [1.21.8 Forge](https://github.com/cabaletta/baritone/releases/download/v1.15.0/baritone-api-forge-1.15.0.jar) | [1.21.8 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.15.0/baritone-api-fabric-1.15.0.jar) | [1.21.8 NeoForge](https://github.com/cabaletta/baritone/releases/download/v1.15.0/baritone-api-neoforge-1.15.0.jar) | - -**How to immediately get started:** Type `#goto 1000 500` in chat to go to x=1000 z=500. Type `#mine diamond_ore` to mine diamond ore. Type `#stop` to stop. For more, read [the usage page](USAGE.md) and/or watch this [tutorial playlist](https://www.youtube.com/playlist?list=PLnwnJ1qsS7CoQl9Si-RTluuzCo_4Oulpa). Also try `#elytra` for Elytra flying in the Nether using fireworks ([trailer](https://youtu.be/4bGGPo8yiHo), [usage](https://youtu.be/NnSlQi-68eQ)). For help, join the [Baritone Discord Server](http://discord.gg/s6fRBAUpmr). - -For other versions of Minecraft or more complicated situations or for development, see [Installation & setup](SETUP.md). For 1.16.5, [click here](https://www.youtube.com/watch?v=_4eVJ9Qz2J8) and see description. Once Baritone is installed, look [here](USAGE.md) for instructions on how to use it. There's a [showcase video](https://youtu.be/CZkLXWo4Fg4) made by @Adovin#6313 on Baritone which I recommend. For help, join the [Baritone Discord Server](http://discord.gg/s6fRBAUpmr). - -This project is an updated version of [MineBot](https://github.com/leijurv/MineBot/), -the original version of the bot for Minecraft 1.8.9, rebuilt for 1.12.2 onwards. Baritone focuses on reliability and particularly performance (it's over [30x faster](https://github.com/cabaletta/baritone/pull/180#issuecomment-423822928) than MineBot at calculating paths). - -Have committed at least once a day from Aug 1, 2018, to Aug 1, 2019. - -1Leijurv3DWTrGAfmmiTphjhXLvQiHg7K2 +A Minecraft pathfinder bot. This is a fork of [Baritone](https://github.com/cabaletta/baritone) by +[leijurv](https://github.com/leijurv), tracking the Fabric branches of +[MeteorDevelopment/baritone](https://github.com/MeteorDevelopment/baritone), with fixes aimed at the +one thing anarchy servers ask of a pathfinder: travelling a very long way, unattended, without +dying. Everything upstream Baritone does — `#goto`, `#mine`, `#follow`, `#build`, `#farm` — works +here unchanged. + +The fixes are version independent, so every supported Minecraft version gets all of them. + +## Supported versions + +| Minecraft | Branch | Java | Release tag | +|-----------|-----------------------------------------------------|------|-------------------| +| 1.21.4 | [`1.21.4`](https://github.com/dekrom/baritone/tree/1.21.4) | 21 | `v-1.21.4` | +| 1.21.5 | [`1.21.5`](https://github.com/dekrom/baritone/tree/1.21.5) | 21 | `v-1.21.5` | +| 1.21.8 | [`1.21.8`](https://github.com/dekrom/baritone/tree/1.21.8) | 21 | `v-1.21.8` | +| 1.21.11 | [`1.21.11`](https://github.com/dekrom/baritone/tree/1.21.11) | 21 | `v-1.21.11` | +| 26.1 | [`26.1`](https://github.com/dekrom/baritone/tree/26.1) | 25 | `v-26.1` | +| 26.2 | [`26.2`](https://github.com/dekrom/baritone/tree/26.2) | 25 | `v-26.2` | + +Each Minecraft version has its own branch and its own release. Fabric only. + +## Install + +1. Grab the jar for your Minecraft version from [Releases](https://github.com/dekrom/baritone/releases). + - `baritone-api-fabric-*.jar` — use this one if another mod (Meteor Client, an addon, your own + mod) drives Baritone through its API. This is what most people want. + - `baritone-standalone-fabric-*.jar` — everything exposed, for using Baritone on its own. + - `baritone-unoptimized-fabric-*.jar` — not obfuscated. Use it when reporting a bug so the stack + trace is readable. +2. Drop it in `mods/` next to Fabric API. +3. Type `#goto 1000 500` in chat. + +Verify your download against `checksums.txt` on the release. See [SETUP.md](SETUP.md) for anything +more involved, and [USAGE.md](USAGE.md) for the chat commands. + +## What this fork changes + +### Elytra + +`#elytra` is the reason this fork exists. Upstream's implementation is good at flying a computed +path and bad at everything around it: getting into the air, and getting out of trouble once the +world stops matching the path. + +- **Takeoff from anywhere.** `elytraAutoJump` only knew how to walk to an overhang and step off it. + With no overhang in reach — most of the overworld, and most of a nether highway — it spent a + couple of seconds pathfinding and then told you to go find a cliff. It now falls back to jumping + on the spot, opening the elytra on the way up the way a vanilla double tap does, and lighting a + firework the instant it opens. Three ways it could previously stand still forever are fixed as + well, including landing at exactly y=31 in the nether, where the old fixed takeoff goal was + already satisfied where you stood. +- **A survival fallback.** When the solver ran out of options it returned a heading with no pitch, + which in practice means holding the last pitch straight into the terrain. That is exactly the + case that matters: the world changed under the path, a wall appeared, something knocked you off + course. It now simulates the pitch range and flies whatever trajectory stays clear the longest, + preferring the one that ends higher, and forces a firework when nothing else buys enough time. +- **Physics that match the server.** The flight simulation had drifted from + `LivingEntity.updateFallFlyingMovement`: hardcoded gravity instead of the `GRAVITY` attribute, no + Slow Falling, a cosine term from `Mth`'s lookup table with a factor vanilla does not have, a + missing divide-by-zero guard that produced `NaN` when looking straight up or down, and drag as + `float` where vanilla uses `double`. Every tick of error compounds across the horizon the solver + picks a pitch from. +- **Recalculating instead of hoping.** A path node buried in terrain used to be noticed and + ignored. The segment is now recomputed, rejoining the existing path at the last node still in + open air. +- **`elytraPathLookahead`** exposes how far ahead the solver looks for a node to aim at (was a + hardcoded 20). Higher cuts corners harder at firework speed. +- **`elytraStandingTakeoff`** turns the standing takeoff on and off. On by default, capped at three + attempts so a bad spot cannot burn a stack of rockets. + +### Pathfinding correctness + +- Falls were charged one tick of gravity that never happens, because `velocity(0)` is zero. +- Descending onto a block counted one block of fall too many; the fall ends on top of the block. +- `GoalRunAway` folded its bounding box's max corner against the running minimum, so with more than + one position the box collapsed towards the minimum corner. +- The dimension height check compared a y coordinate against a block count, letting the search + climb 64 blocks above the overworld build limit. +- Waterlogged fences, walls, panes, bars, stairs and leaves were treated as plain water and walked + into. +- `fullyPassable` listed the blocks you cannot jump through one at a time, so powder snow, honey, + berry bushes, dripstone and anything in `blocksToAvoid` were considered jumpable. The hitbox is + the same width in the air as on the ground. +- A diagonal's second corner was tested for lava but its magma check read the first corner again. +- `backtrackCostFavoringCoefficient` at 0 or below produced a non-positive action cost, which A\* + rejects, failing the whole calculation instead of favoring nothing. +- `GoalGetToBlock` reported an ETA that never counted down to zero. + +### Pathing thread + +- `ToolSet` read the live inventory from the calculation thread — a data race with the game thread — + and cached break speeds in a plain `HashMap`. The hotbar and selected slot are now snapshotted on + construction and the cache is concurrent. +- `assumeWalkOnWater` was read off the settings mid-calculation, so flipping it made the cost + function inconsistent with the nodes already expanded. It is read once per `BlockStateInterface`. +- `cancelRequested` is written by the game thread and read by the calculation thread, and was not + `volatile`. +- `bestPathSoFar` allocated a `BetterBlockPos` per node on every tick a calculation was running, + before the cheap checks that almost always decide the outcome; `PathExecutor` built a whole + `CalculationContext` per tick to read two fields off it. + +### 26.2 only + +- Nether pathfinder chunk packing wrote solid sections as air, so `#elytra` planned paths straight + through terrain. The 26.2 port had replaced the removed `LevelChunkSection.SECTION_SIZE` (4096, + blocks per section) with a local `16`, and the all-solid fast path then memset 2 bytes instead of + 512. +- Baritone's threadpool threads are daemon threads. As of 26.2 the game waits on non-daemon threads + at shutdown and files a crash report if any are still alive. + +## Building + +Java 21 for the `1.21.x` branches, Java 25 for `26.x` (see the table above). + +```sh +./gradlew build # jars land in dist/, with checksums.txt +./gradlew test +``` -# Getting Started +Tagging `v-` on a version branch builds it and publishes the release. -Here are some links to help to get started: +## Docs - [Features](FEATURES.md) - - [Installation & setup](SETUP.md) - -- [API Javadocs](https://baritone.leijurv.com/) - -- [Settings](https://baritone.leijurv.com/baritone/api/Settings.html#field.detail) - - [Usage (chat control)](USAGE.md) +- [Settings](https://baritone.leijurv.com/baritone/api/Settings.html#field.detail) +- [API Javadocs](https://baritone.leijurv.com/) (upstream) -## Stars over time - -[![Stargazers over time](https://starchart.cc/cabaletta/baritone.svg)](https://starchart.cc/cabaletta/baritone) - -# API - -The API is heavily documented, you can find the Javadocs for the latest release [here](https://baritone.leijurv.com/). -Please note that usage of anything located outside of the `baritone.api` package is not supported by the API release -jar. +## API -Below is an example of basic usage for changing some settings, and then pathing to an X/Z goal. +The API is heavily documented. Anything outside the `baritone.api` package is not supported by the +API release jar. ```java BaritoneAPI.getSettings().allowSprint.value = true; @@ -123,26 +156,18 @@ BaritoneAPI.getSettings().primaryTimeoutMS.value = 2000L; BaritoneAPI.getProvider().getPrimaryBaritone().getCustomGoalProcess().setGoalAndPath(new GoalXZ(10000, 20000)); ``` -# FAQ - -## Can I use Baritone as a library in my custom utility client? - -That's what it's for, sure! (As long as usage complies with the LGPL 3.0 License) - -## How is it so fast? - -Magic. (Hours of [leijurv](https://github.com/leijurv/) enduring excruciating pain) - -### Additional Special Thanks To: - -![YourKit-Logo](https://www.yourkit.com/images/yklogo.png) - -YourKit supports open source projects with innovative and intelligent tools for monitoring and profiling Java and .NET applications. +## Credits -YourKit is the creator of the [YourKit Java Profiler](https://www.yourkit.com/java/profiler/), [YourKit .NET Profiler](https://www.yourkit.com/.net/profiler/), and [YourKit YouMonitor](https://www.yourkit.com/youmonitor/). +Baritone is by [leijurv](https://github.com/leijurv) and +[its contributors](https://github.com/cabaletta/baritone/graphs/contributors); it grew out of +[MineBot](https://github.com/leijurv/MineBot/). The Fabric branches this fork tracks are maintained +by [MeteorDevelopment](https://github.com/MeteorDevelopment/baritone). The elytra pathfinder is +[nether-pathfinder](https://github.com/babbaj/nether-pathfinder) by +[babbaj](https://github.com/babbaj). -We thank them for granting Baritone an OSS license so that we can make our software the best it can be. +For help with Baritone itself, the [Baritone Discord](http://discord.gg/s6fRBAUpmr) is the place to +ask. Issues with *this fork* belong [here](https://github.com/dekrom/baritone/issues). -## Why is it called Baritone? +## License -It's named for FitMC's deep sultry voice. +LGPL-3.0, same as upstream. See [LICENSE](LICENSE). From 5d63ef93dd3ad6925d939fec10b8fa3283d73f44 Mon Sep 17 00:00:00 2001 From: Dek Date: Sun, 30 Aug 2026 16:04:55 -0400 Subject: [PATCH 24/54] readme: the version specific fixes are not on every branch --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6423912e63..9e89e5b884 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ one thing anarchy servers ask of a pathfinder: travelling a very long way, unatt dying. Everything upstream Baritone does — `#goto`, `#mine`, `#follow`, `#build`, `#farm` — works here unchanged. -The fixes are version independent, so every supported Minecraft version gets all of them. +Every fix lands on every supported Minecraft version, except the handful of bugs that only ever +existed on one of them. ## Supported versions From f3a51d47a05fa4fc9cacd6d90091f617a8d685df Mon Sep 17 00:00:00 2001 From: Todarac <3059008+Todarac@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:37:25 -0500 Subject: [PATCH 25/54] Revert client registry access for loot table resolution PR #5058 sourced ServerLevelStub's RegistryAccess from client.level so that modded loot tables would resolve. However, loot tables are populated only by the ReloadableServerRegistries reload performed in load(); a client RegistryAccess never receives them. In game (client.level != null) every loot table lookup therefore threw, and the catch (Throwable) widened by that same PR silently swallowed it, leaving an empty drops list. The visible effect was that #mine stopped picking up items entirely. Revert that half so drops resolve again. The widened catch is kept, so modded blocks whose tables are absent degrade to no known drops instead of crashing, which preserves what #5058 set out to fix. Reported-by: ZacSharp Co-Authored-By: Claude --- src/api/java/baritone/api/utils/BlockOptionalMeta.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/api/java/baritone/api/utils/BlockOptionalMeta.java b/src/api/java/baritone/api/utils/BlockOptionalMeta.java index e0db6b54c8..da085a1df0 100644 --- a/src/api/java/baritone/api/utils/BlockOptionalMeta.java +++ b/src/api/java/baritone/api/utils/BlockOptionalMeta.java @@ -289,9 +289,10 @@ public static ServerLevelStub fastCreate() { @Override public RegistryAccess registryAccess() { - if (client.level != null) { - return client.level.registryAccess(); - } + // Loot tables are populated only by the reloaded server registries built + // in load(). A client RegistryAccess never receives them, so sourcing the + // holder from client.level made every block resolve to zero drops, which + // silently broke #mine item pickup (regression from PR #5058). return registryAccess.join(); } From 9fadf7cf95293d7f6678981e77a939b4a7978373 Mon Sep 17 00:00:00 2001 From: Todarac <3059008+Todarac@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:38:36 -0500 Subject: [PATCH 26/54] Bump 26.2 to 1.19.0 The 26.1 and 26.2 branches both carried mod_version 1.18.0, which would collide on the v1.18.0 tag and produce identical artifact names for two distinct builds. 26.1 keeps 1.18.0; 26.2 moves to 1.19.0, matching the one-version-per-branch scheme used by every previous release. Co-Authored-By: Claude --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6ab8aa48d5..92947ba3ee 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ org.gradle.jvmargs=-Xmx4G available_loaders=fabric,forge,neoforge,tweaker -mod_version=1.18.0 +mod_version=1.19.0 maven_group=baritone archives_base_name=baritone From d4fe4d5553fcf21e0ebe742e0a07511923d69805 Mon Sep 17 00:00:00 2001 From: Dek Date: Tue, 1 Sep 2026 19:55:25 -0400 Subject: [PATCH 27/54] save cached regions atomically CachedRegion.save rewrites a region file in place, from the cache save thread, while everything else is still reading it. The nether pathfinder is the reader that matters: with elytraUseCache on it is handed baritone's own cache directory and parses those files lazily during pathFind, recording each region it has looked at so that it never looks at it again. A read that lands mid rewrite gets a truncated gzip stream, the region is written off as unparseable, and it stays a hole in the world for the rest of that context's life. On a long flight over cached terrain that is what turns into "Failed to compute path to destination" forever. Write to a temp file and rename it over the real one instead. The name carries our pid so that two clients sharing a cache directory cannot corrupt each other's temp file; concurrent saves of the same region then settle as last-complete-writer-wins, which is what the in place rewrite was pretending to do anyway. --- .../java/baritone/cache/CachedRegion.java | 101 ++++++++++-------- 1 file changed, 58 insertions(+), 43 deletions(-) diff --git a/src/main/java/baritone/cache/CachedRegion.java b/src/main/java/baritone/cache/CachedRegion.java index 38b5490101..12df7c4ffa 100644 --- a/src/main/java/baritone/cache/CachedRegion.java +++ b/src/main/java/baritone/cache/CachedRegion.java @@ -27,9 +27,11 @@ import net.minecraft.world.level.dimension.DimensionType; import java.io.*; +import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.util.*; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; @@ -130,61 +132,74 @@ public synchronized final void save(String directory) { } System.out.println("Saving region " + x + "," + z + " to disk " + path); Path regionFile = getRegionFile(path, this.x, this.z); - if (!Files.exists(regionFile)) { - Files.createFile(regionFile); - } - try ( - FileOutputStream fileOut = new FileOutputStream(regionFile.toFile()); - GZIPOutputStream gzipOut = new GZIPOutputStream(fileOut, 16384); - DataOutputStream out = new DataOutputStream(gzipOut) - ) { - out.writeInt(CACHED_REGION_MAGIC); - for (int x = 0; x < 32; x++) { - for (int z = 0; z < 32; z++) { - CachedChunk chunk = this.chunks[x][z]; - if (chunk == null) { - out.write(CHUNK_NOT_PRESENT); - } else { - out.write(CHUNK_PRESENT); - byte[] chunkBytes = chunk.toByteArray(); - out.write(chunkBytes); - // Messy, but fills the empty 0s that should be trailing to fill up the space. - out.write(new byte[chunk.sizeInBytes - chunkBytes.length]); + // Write to a temp file and atomically rename it over the real one. The nether pathfinder + // (and a concurrently running load) reads these files while we save; an in-place rewrite + // hands them a truncated gzip stream and they treat the region as permanently unparseable. + // The pid keeps multiple game instances sharing this cache dir off each other's temp file; + // concurrent saves of the same region then resolve to last-complete-writer-wins. + Path tmpFile = regionFile.resolveSibling(regionFile.getFileName() + "." + ProcessHandle.current().pid() + ".tmp"); + try { + try ( + FileOutputStream fileOut = new FileOutputStream(tmpFile.toFile()); + GZIPOutputStream gzipOut = new GZIPOutputStream(fileOut, 16384); + DataOutputStream out = new DataOutputStream(gzipOut) + ) { + out.writeInt(CACHED_REGION_MAGIC); + for (int x = 0; x < 32; x++) { + for (int z = 0; z < 32; z++) { + CachedChunk chunk = this.chunks[x][z]; + if (chunk == null) { + out.write(CHUNK_NOT_PRESENT); + } else { + out.write(CHUNK_PRESENT); + byte[] chunkBytes = chunk.toByteArray(); + out.write(chunkBytes); + // Messy, but fills the empty 0s that should be trailing to fill up the space. + out.write(new byte[chunk.sizeInBytes - chunkBytes.length]); + } } } - } - for (int x = 0; x < 32; x++) { - for (int z = 0; z < 32; z++) { - if (chunks[x][z] != null) { - for (int i = 0; i < 256; i++) { - out.writeUTF(BlockUtils.blockToString(chunks[x][z].getOverview()[i].getBlock())); + for (int x = 0; x < 32; x++) { + for (int z = 0; z < 32; z++) { + if (chunks[x][z] != null) { + for (int i = 0; i < 256; i++) { + out.writeUTF(BlockUtils.blockToString(chunks[x][z].getOverview()[i].getBlock())); + } } } } - } - for (int x = 0; x < 32; x++) { - for (int z = 0; z < 32; z++) { - if (chunks[x][z] != null) { - Map> locs = chunks[x][z].getRelativeBlocks(); - out.writeShort(locs.entrySet().size()); - for (Map.Entry> entry : locs.entrySet()) { - out.writeUTF(entry.getKey()); - out.writeShort(entry.getValue().size()); - for (BlockPos pos : entry.getValue()) { - out.writeByte((byte) (pos.getZ() << 4 | pos.getX())); - out.writeInt(pos.getY()-dimension.minY()); + for (int x = 0; x < 32; x++) { + for (int z = 0; z < 32; z++) { + if (chunks[x][z] != null) { + Map> locs = chunks[x][z].getRelativeBlocks(); + out.writeShort(locs.entrySet().size()); + for (Map.Entry> entry : locs.entrySet()) { + out.writeUTF(entry.getKey()); + out.writeShort(entry.getValue().size()); + for (BlockPos pos : entry.getValue()) { + out.writeByte((byte) (pos.getZ() << 4 | pos.getX())); + out.writeInt(pos.getY()-dimension.minY()); + } } } } } - } - for (int x = 0; x < 32; x++) { - for (int z = 0; z < 32; z++) { - if (chunks[x][z] != null) { - out.writeLong(chunks[x][z].cacheTimestamp); + for (int x = 0; x < 32; x++) { + for (int z = 0; z < 32; z++) { + if (chunks[x][z] != null) { + out.writeLong(chunks[x][z].cacheTimestamp); + } } } } + try { + Files.move(tmpFile, regionFile, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException ex) { + Files.move(tmpFile, regionFile, StandardCopyOption.REPLACE_EXISTING); + } + } finally { + // nothing to delete once the move succeeded; this is the failed-write path + Files.deleteIfExists(tmpFile); } hasUnsavedChanges = false; System.out.println("Saved region successfully"); From c76ae7b44495582453b73f2680e89aeb7d04f8fa Mon Sep 17 00:00:00 2001 From: Dek Date: Tue, 1 Sep 2026 19:55:31 -0400 Subject: [PATCH 28/54] fix two arithmetic bugs in the above build limit pathfinder generateDirectPath divides 8 by the horizontal distance to the destination to get its step vector, so a destination directly above or below the player made every step NaN and the whole path with it. The fallback for a failed transition compares distanceXZ against 32 to decide whether to aim at the destination itself or at the build limit above it. distanceXZ is a squared distance, so anything past 6 blocks took the first branch and the roof route was never used. --- .../java/baritone/process/elytra/BuildLimitPathFinder.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/baritone/process/elytra/BuildLimitPathFinder.java b/src/main/java/baritone/process/elytra/BuildLimitPathFinder.java index 01624d730f..1961ed5d54 100644 --- a/src/main/java/baritone/process/elytra/BuildLimitPathFinder.java +++ b/src/main/java/baritone/process/elytra/BuildLimitPathFinder.java @@ -108,7 +108,8 @@ public Pair,Boolean> generateTransitionUp(BetterBlockPos st final double deltaZ = destination.getZ() - start.getZ(); final double distance = Math.sqrt(deltaX * deltaX + deltaZ * deltaZ); - final double scale = 8 / distance; + // a destination straight above/below would otherwise make the steps NaN + final double scale = distance > 1.0e-6 ? 8 / distance : 0; final double stepX = deltaX * scale; final double stepZ = deltaZ * scale; @@ -234,7 +235,8 @@ public CompletableFuture pathFindAsync(BlockPos src, BlockPos d boolean success = transition.second(); if(!success) { - BetterBlockPos newDest = distanceXZ > 32 ? new BetterBlockPos(dst) : new BetterBlockPos(dst.getX(), playerCtx.world().getMaxY(), dst.getZ()); + // distanceXZ is squared + BetterBlockPos newDest = distanceXZ > 32 * 32 ? new BetterBlockPos(dst) : new BetterBlockPos(dst.getX(), playerCtx.world().getMaxY(), dst.getZ()); var directPath = generateDirectPath(new BetterBlockPos(src), newDest, 0, 2); return CompletableFuture.completedFuture(new UnpackedSegment(directPath.first().stream(), directPath.second())); } From c2a52eb4be3c581e3c4dcf90e639185d1723683f Mon Sep 17 00:00:00 2001 From: Dek Date: Tue, 1 Sep 2026 19:55:42 -0400 Subject: [PATCH 29/54] never call into a freed pathfinder context destroy() shuts the read and write executors down and waits for them, then calls freeContext. That covers path calculation and chunk packing, and nothing else. The solver thread raytraces through the context outside those executors, and so does the game thread - setPath's clearView runs on whichever thread the path future completed on, with no lock at all. Freeing the context out from under one of those is a use-after-free on native memory, and BlockStateOctreeInterface.chunkPtr caches a raw pointer into it besides. Take the write lock around freeContext, drop the cached chunk pointer, and set a destroyed flag under the same lock. Every native entry point now checks it under the read lock and fails safe: nothing is visible, nothing is passable, every raytrace hits. passable() is the solver's innermost loop and its callers already hold the read lock, so it reads the flag without taking it again. Queueing work after destroy() rejects instead of throwing out of queueForPacking/queueBlockUpdate/queueCacheCulling, and pathFindAsync returns a failed future rather than propagating RejectedExecutionException to a caller that expects a future. Also cut the native path calculation timeout from 10s to 3s. NetherPathfinder's cancel() is a no-op - the JNI sets ctx->cancelFlag while the search loop polls an unrelated file scope global - so that timeout is the only bound on how long destroy() can sit waiting for a search that is never going to finish. Searches that succeed return within a few hundred ms of first progress; only hopeless ones run the clock out. --- .../elytra/NetherPathfinderContext.java | 162 +++++++++++++----- 1 file changed, 123 insertions(+), 39 deletions(-) diff --git a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java index 6cb8564c21..7540ec9176 100644 --- a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java +++ b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java @@ -40,6 +40,7 @@ import java.lang.ref.SoftReference; import java.lang.reflect.Field; import java.nio.file.Path; +import java.util.Arrays; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -74,6 +75,9 @@ public final class NetherPathfinderContext implements IElytraPathFinder { // Visible for access in BlockStateOctreeInterface final long context; + // Set under the write lock before freeContext; every native call checks it under the read lock + // so nothing can touch the context pointer after it has been freed. + private volatile boolean destroyed; private final long seed; // write locked operations private final ExecutorService writeExecutor = Executors.newSingleThreadExecutor(); @@ -99,11 +103,30 @@ public NetherPathfinderContext(long seed, Path cache, Level world) { } public boolean hasChunk(ChunkPos pos) { - return NetherPathfinder.hasChunkFromJava(this.context, pos.x(), pos.z()); + readLock.lock(); + try { + if (this.destroyed) { + return false; + } + return NetherPathfinder.hasChunkFromJava(this.context, pos.x(), pos.z()); + } finally { + readLock.unlock(); + } + } + + /** + * Submits to the write executor, dropping the task if the context is already being torn down. + */ + private void executeWrite(Runnable task) { + try { + this.writeExecutor.execute(task); + } catch (java.util.concurrent.RejectedExecutionException ignored) { + // destroy() has shut the executor down; the cache no longer matters + } } public void queueCacheCulling(int chunkX, int chunkZ, int maxDistanceBlocks) { - this.writeExecutor.execute(() -> { + this.executeWrite(() -> { writeLock.lock(); try { this.boi.chunkPtr = 0L; @@ -116,7 +139,7 @@ public void queueCacheCulling(int chunkX, int chunkZ, int maxDistanceBlocks) { public void queueForPacking(final LevelChunk chunkIn) { final SoftReference ref = new SoftReference<>(chunkIn); - this.writeExecutor.execute(() -> { + this.executeWrite(() -> { // TODO: Prioritize packing recent chunks and/or ones that the path goes through, // and prune the oldest chunks per chunkPackerQueueMaxSize final LevelChunk chunk = ref.get(); @@ -135,7 +158,7 @@ public void queueForPacking(final LevelChunk chunkIn) { } public void queueBlockUpdate(BlockChangeEvent event) { - this.writeExecutor.execute(() -> { + this.executeWrite(() -> { ChunkPos chunkPos = event.getChunkPos(); // not inserting or deleting from the cache hashmap but it would still be bad for this function to race with itself writeLock.lock(); @@ -160,29 +183,40 @@ public CompletableFuture pathFindAsync(final BlockPos src, fina boolean generate = Baritone.settings().elytraPredictTerrain.value && this.dimension == Level.NETHER; Lock l = generate ? writeLock : readLock; ExecutorService exec = generate ? writeExecutor : readExecutor; - return CompletableFuture.supplyAsync(() -> { - l.lock(); - try { - final PathSegment segment = NetherPathfinder.pathFind( - this.context, - adjustedSrc.getX(), adjustedSrc.getY(), adjustedSrc.getZ(), - adjustedDst.getX(), adjustedDst.getY(), adjustedDst.getZ(), - !Baritone.settings().elytraAllowTightSpaces.value, // atleastX4 - false, // refine - 10000, // timeoutMs - !generate, // useAirIfChunkNotLoaded - // TODO: Determine appropriate cost value - 8.0 // fakeChunkCost - ); - if (segment == null) { - throw new PathCalculationException("Path calculation failed"); - } + try { + return CompletableFuture.supplyAsync(() -> { + l.lock(); + try { + if (this.destroyed) { + throw new PathCalculationException("Pathfinder context destroyed"); + } + final PathSegment segment = NetherPathfinder.pathFind( + this.context, + adjustedSrc.getX(), adjustedSrc.getY(), adjustedSrc.getZ(), + adjustedDst.getX(), adjustedDst.getY(), adjustedDst.getZ(), + !Baritone.settings().elytraAllowTightSpaces.value, // atleastX4 + false, // refine + // The native cancel is a no-op (nether-pathfinder sets ctx->cancelFlag but the + // search loop polls an unrelated global), so this timeout is the only bound on + // how long destroy() can be stuck waiting for an in-flight search. Successful + // segments return ~500ms after first progress; only hopeless searches run it out. + 3000, // timeoutMs + !generate, // useAirIfChunkNotLoaded + // TODO: Determine appropriate cost value + 8.0 // fakeChunkCost + ); + if (segment == null) { + throw new PathCalculationException("Path calculation failed"); + } - return new UnpackedSegment(UnpackedSegment.from(segment).collect().stream().map(pos -> pos.above(minY)), segment.finished); - } finally { - l.unlock(); - } - }, exec); + return new UnpackedSegment(UnpackedSegment.from(segment).collect().stream().map(pos -> pos.above(minY)), segment.finished); + } finally { + l.unlock(); + } + }, exec); + } catch (java.util.concurrent.RejectedExecutionException ex) { + return CompletableFuture.failedFuture(new PathCalculationException("Pathfinder context destroyed")); + } } /** @@ -201,7 +235,15 @@ public boolean raytrace(final double startX, final double startY, final double s final double endX, final double endY, final double endZ) { final double adjustedStartY = startY - this.minY; final double adjustedEndY = endY - this.minY; - return NetherPathfinder.isVisible(this.context, NetherPathfinder.CACHE_MISS_SOLID, startX, adjustedStartY, startZ, endX, adjustedEndY, endZ); + readLock.lock(); + try { + if (this.destroyed) { + return false; + } + return NetherPathfinder.isVisible(this.context, NetherPathfinder.CACHE_MISS_SOLID, startX, adjustedStartY, startZ, endX, adjustedEndY, endZ); + } finally { + readLock.unlock(); + } } /** @@ -215,7 +257,15 @@ public boolean raytrace(final double startX, final double startY, final double s public boolean raytrace(final Vec3 start, final Vec3 end) { final Vec3 adjustedStart = start.subtract(0, this.minY, 0); final Vec3 adjustedEnd = end.subtract(0, this.minY, 0); - return NetherPathfinder.isVisible(this.context, NetherPathfinder.CACHE_MISS_SOLID, adjustedStart.x, adjustedStart.y, adjustedStart.z, adjustedEnd.x, adjustedEnd.y, adjustedEnd.z); + readLock.lock(); + try { + if (this.destroyed) { + return false; + } + return NetherPathfinder.isVisible(this.context, NetherPathfinder.CACHE_MISS_SOLID, adjustedStart.x, adjustedStart.y, adjustedStart.z, adjustedEnd.x, adjustedEnd.y, adjustedEnd.z); + } finally { + readLock.unlock(); + } } public boolean raytrace(final int count, final double[] src, final double[] dst, final int visibility) { @@ -228,15 +278,23 @@ public boolean raytrace(final int count, final double[] src, final double[] dst, dst[i] -= this.minY; } - switch (visibility) { - case Visibility.ALL: - return NetherPathfinder.isVisibleMulti(this.context, NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, false) == -1; - case Visibility.NONE: - return NetherPathfinder.isVisibleMulti(this.context, NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, true) == -1; - case Visibility.ANY: - return NetherPathfinder.isVisibleMulti(this.context, NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, true) != -1; - default: - throw new IllegalArgumentException("lol"); + readLock.lock(); + try { + if (this.destroyed) { + return false; + } + switch (visibility) { + case Visibility.ALL: + return NetherPathfinder.isVisibleMulti(this.context, NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, false) == -1; + case Visibility.NONE: + return NetherPathfinder.isVisibleMulti(this.context, NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, true) == -1; + case Visibility.ANY: + return NetherPathfinder.isVisibleMulti(this.context, NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, true) != -1; + default: + throw new IllegalArgumentException("lol"); + } + } finally { + readLock.unlock(); } } @@ -250,10 +308,27 @@ 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); + readLock.lock(); + try { + if (this.destroyed) { + Arrays.fill(hitsOut, true); + return; + } + NetherPathfinder.raytrace(this.context, NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, hitsOut, hitPosOut); + } finally { + readLock.unlock(); + } } + /** + * Callers already hold the read lock (see {@link ElytraBehavior#solveAngles}) - this is the + * solver's innermost loop and taking it again per block would cost more than the call itself. + * {@link #destroyed} is set under the write lock, so it cannot flip while a caller is inside. + */ public boolean passable(int x, int y, int z) { + if (this.destroyed) { + return false; + } return !this.boi.get0(x, y, z); } @@ -274,7 +349,16 @@ public void destroy() { e.printStackTrace(); } - NetherPathfinder.freeContext(this.context); + // The executors are drained, but the solver and render threads still raytrace through this + // context; the write lock keeps freeContext from pulling the memory out from under them. + writeLock.lock(); + try { + this.destroyed = true; + this.boi.chunkPtr = 0L; + NetherPathfinder.freeContext(this.context); + } finally { + writeLock.unlock(); + } } public long getSeed() { From f4fb89ebfd478d6b5bf498ba0bb374ee441c4067 Mon Sep 17 00:00:00 2001 From: Dek Date: Tue, 1 Sep 2026 19:55:55 -0400 Subject: [PATCH 30/54] stop cancelling elytra from freezing the game Cancelling #elytra tears the behavior and the pathfinder context down on the executor and hands the semaphore back when the context is freed. Both halves were wrong. The two teardowns were submitted independently, so the context could be freed while the behavior's solver and its still-queued path futures were still calling into it. Chain them: the context is destroyed only after the behavior is gone. Worse, the next engage went through getNpfContext, which blocks on that semaphore uninterruptibly - on the game thread. A path calculation cannot be cancelled and runs to its timeout, so anything that re-issued a goal shortly after a cancel froze the client for the rest of it. pathTo0 now tries the semaphore, and if the old context is still going down it defers the engage onto the teardown's completion instead of waiting for it. An epoch counter, bumped by every onLostControl and by leaving the world, drops that deferred engage if anything else has happened in the meantime. onLostControl itself is called from the path future's failure handler, which is a pathfinder worker thread, and touches nothing but game thread state. It marshals itself back now. Finally, a context that fails a path calculation usually keeps failing: a region file it could not parse is never retried, so the hole stays. Combined with a caller that re-issues its goal on a timer, that is an endless stream of "Failed to compute path to destination" and no way out short of a relog. Three consecutive failures now discard the context so the next attempt starts from a clean cache - skipped while flying, since resetting mid air would drop the elytra outright. --- .../java/baritone/process/ElytraProcess.java | 118 ++++++++++++++---- .../process/elytra/ElytraBehavior.java | 2 + 2 files changed, 93 insertions(+), 27 deletions(-) diff --git a/src/main/java/baritone/process/ElytraProcess.java b/src/main/java/baritone/process/ElytraProcess.java index d244360ca7..52900b7cf6 100644 --- a/src/main/java/baritone/process/ElytraProcess.java +++ b/src/main/java/baritone/process/ElytraProcess.java @@ -64,8 +64,10 @@ import net.minecraft.world.phys.Vec3; import java.util.*; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import static baritone.api.pathing.movement.ActionCosts.COST_INF; @@ -82,6 +84,15 @@ public class ElytraProcess extends BaritoneProcessHelper implements IBaritonePro private boolean allowAboveBuildLimit; private boolean allowAboveRoof; private final Semaphore npfSema = new Semaphore(1); + private CompletableFuture npfDestroyDone = CompletableFuture.completedFuture(null); + /** + * Bumped by every {@link #onLostControl}; a deferred engage queued while the old context was + * tearing down only fires if nothing else (a cancel, another goal) happened in the meantime. + */ + private int engageEpoch; + // incremented from the pathfinder's worker threads, reset from wherever a path completes + private final AtomicInteger consecutivePathCalcFailures = new AtomicInteger(); + private static final int PATH_CALC_FAILURES_BEFORE_RESET = 3; private static final int SHORT_LANDING_COLUMN_HEIGHT = 15; private static final int LONG_LANDING_COLUMN_HEIGHT = 39; @@ -127,6 +138,13 @@ public void onLostControl() { } public void onLostControl(boolean destroyNpf) { + // Path calculation whenComplete chains land here from the pathfinder's worker threads on + // failure; everything below (behavior handoff, semaphore, epoch) is render-thread state. + if (!ctx.minecraft().isSameThread()) { + ctx.minecraft().execute(() -> this.onLostControl(destroyNpf)); + return; + } + this.engageEpoch++; this.state = State.START_FLYING; // TODO: null state? this.goingToLandingSpot = false; this.landingSpot = null; @@ -139,9 +157,9 @@ public void onLostControl(boolean destroyNpf) { this.takeoffAirborneTicks = 0; this.walkOffImpossible = false; this.takeoffBoostPending = false; - destroyBehaviorAsync(); + CompletableFuture behaviorGone = destroyBehaviorAsync(); if (destroyNpf) { - destroyNpfContextAsync(); + destroyNpfContextAsync(behaviorGone); } } @@ -150,6 +168,30 @@ private ElytraProcess(Baritone baritone) { baritone.getGameEventHandler().registerEventListener(this); } + public void onPathCalculationSuccess() { + this.consecutivePathCalcFailures.set(0); + } + + /** + * A pathfinder context that hit a transient failure while lazily parsing a cached region file + * never retries that region, so once one path calculation fails they usually all do. Persisting + * the context across new goals then turns any external re-command loop into an endless failure + * spam. After a few consecutive failures, throw the context away so the next engage starts clean. + */ + public void onPathCalculationFailure() { + if (this.consecutivePathCalcFailures.incrementAndGet() < PATH_CALC_FAILURES_BEFORE_RESET) { + return; + } + if (ctx.player() != null && ctx.player().isFallFlying()) { + // resetting mid-air would disengage the elytra outright; if the failures persist, + // the grounded retries after landing will trip this + return; + } + this.consecutivePathCalcFailures.set(0); + logDirect("Path calculation failed " + PATH_CALC_FAILURES_BEFORE_RESET + " times in a row, discarding the pathfinder's chunk cache"); + this.onLostControl(); + } + public static IElytraProcess create(final Baritone baritone) { return NetherPathfinderContext.isSupported() ? new ElytraProcess(baritone) @@ -584,14 +626,13 @@ public void landingSpotIsBad(BetterBlockPos endPos) { this.state = State.FLYING; } - private void destroyBehaviorAsync() { + private CompletableFuture destroyBehaviorAsync() { ElytraBehavior behavior = this.behavior; - if (behavior != null) { - this.behavior = null; - Baritone.getExecutor().execute(() -> { - behavior.destroy(); - }); + if (behavior == null) { + return CompletableFuture.completedFuture(null); } + this.behavior = null; + return CompletableFuture.runAsync(behavior::destroy, Baritone.getExecutor()); } @Override @@ -658,11 +699,36 @@ private void pathTo0(BlockPos destination, boolean appendDestination) { return; } this.onLostControl(false); + if (this.npfContext == null) { + if (!this.npfSema.tryAcquire()) { + // The previous context is still tearing down on a worker thread, pinned by an + // uncancellable native path calculation. Blocking here would hang the render + // thread for its whole remaining duration, so finish this engage when the + // teardown completes instead - unless something else took over in the meantime. + final int epoch = this.engageEpoch; + this.npfDestroyDone.whenCompleteAsync((v, ex) -> { + if (this.engageEpoch == epoch) { + this.pathTo0(destination, appendDestination); + } + }, ctx.minecraft()::execute); + return; + } + try { + this.npfContext = new NetherPathfinderContext( + Baritone.settings().elytraNetherSeed.value, + Baritone.settings().elytraUseCache.value ? baritone.getWorldProvider().getCurrentWorld().directory.resolve("cache") : null, + ctx.world() + ); + } catch (Throwable t) { + this.npfSema.release(); + throw t; + } + } this.predictingTerrain = ctx.player().level().dimension() == Level.NETHER && Baritone.settings().elytraPredictTerrain.value; this.allowTight = Baritone.settings().elytraAllowTightSpaces.value; this.allowAboveBuildLimit = Baritone.settings().elytraAllowAboveBuildLimit.value; this.allowAboveRoof = Baritone.settings().elytraAllowAboveRoof.value; - this.behavior = new ElytraBehavior(this.baritone, this, getNpfContext(), destination, appendDestination); + this.behavior = new ElytraBehavior(this.baritone, this, this.npfContext, destination, appendDestination); if (ctx.world() != null) { this.repackChunks(); @@ -764,7 +830,9 @@ public void onRenderPass(RenderEvent event) { @Override public void onWorldEvent(WorldEvent event) { if (event.getWorld() != null && event.getState() == EventState.POST) { - // Exiting the world, just destroy + // Exiting the world, just destroy. A world change also invalidates any engage that was + // deferred while the old context tore down - its destination belongs to the old world. + this.engageEpoch++; destroyBehaviorAsync(); } } @@ -1006,26 +1074,22 @@ private BetterBlockPos advanceHeightmap(BetterBlockPos qPos) { } } - private NetherPathfinderContext getNpfContext() { - if(this.npfContext == null) { - npfSema.acquireUninterruptibly(); - this.npfContext = new NetherPathfinderContext( - Baritone.settings().elytraNetherSeed.value, - Baritone.settings().elytraUseCache.value ? baritone.getWorldProvider().getCurrentWorld().directory.resolve("cache") : null, - ctx.world() - ); - } - return this.npfContext; - } - - private void destroyNpfContextAsync() { + private void destroyNpfContextAsync(CompletableFuture afterBehaviorGone) { NetherPathfinderContext npf = this.npfContext; if (npf != null) { this.npfContext = null; - Baritone.getExecutor().execute(() -> { - npf.destroy(); - npfSema.release(); - }); + // The solver and any still-queued path results keep making native calls until the + // behavior teardown completes, so the context must only be freed after it. + this.npfDestroyDone = afterBehaviorGone.handleAsync((v, ex) -> { + try { + npf.destroy(); + } catch (Throwable t) { + t.printStackTrace(); + } finally { + npfSema.release(); + } + return null; + }, Baritone.getExecutor()); } } } diff --git a/src/main/java/baritone/process/elytra/ElytraBehavior.java b/src/main/java/baritone/process/elytra/ElytraBehavior.java index 46da70ed33..69fc5585d0 100644 --- a/src/main/java/baritone/process/elytra/ElytraBehavior.java +++ b/src/main/java/baritone/process/elytra/ElytraBehavior.java @@ -196,6 +196,7 @@ public CompletableFuture pathToDestination(final BlockPos from) { final long start = System.nanoTime(); return this.path0(from, destinationFixed(), UnaryOperator.identity()) .thenRun(() -> { + process.onPathCalculationSuccess(); final double distance = this.path.get(0).distanceTo(this.path.get(this.path.size() - 1)); if (this.completePath) { logVerbose(String.format("Computed path (%.1f blocks in %.4f seconds)", distance, (System.nanoTime() - start) / 1e9d)); @@ -209,6 +210,7 @@ public CompletableFuture pathToDestination(final BlockPos from) { final Throwable cause = ex.getCause(); if (cause instanceof PathCalculationException) { logDirect("Failed to compute path to destination"); + process.onPathCalculationFailure(); } else { logUnhandledException(cause); } From f55f995c8cc959768d8a8049f088bf6f2d29f9de Mon Sep 17 00:00:00 2001 From: Dek Date: Tue, 1 Sep 2026 19:56:21 -0400 Subject: [PATCH 31/54] readme: the elytra teardown and chunk cache fixes --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index 9e89e5b884..634148324f 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,20 @@ world stops matching the path. hardcoded 20). Higher cuts corners harder at firework speed. - **`elytraStandingTakeoff`** turns the standing takeoff on and off. On by default, capped at three attempts so a bad spot cannot burn a stack of rockets. +- **Cancelling could crash the client.** The nether pathfinder's native context was freed once its + own executors had drained, but the solver and the game thread raytrace through it outside those + executors, and the octree interface caches a raw pointer into it. Running one of those against + freed memory is a segfault, not an exception. It is freed under the lock they hold now, and every + native call is fenced behind a flag that fails safe. + +### Chunk cache + +- Region files were rewritten in place, from the cache save thread, while everything else was + reading them. A read that landed mid-rewrite got a truncated gzip stream and threw away a + 512×512 region's worth of cached terrain. Saves are written to a temp file and renamed over the + old one, so a reader sees either the previous region or the new one. On 26.2 this is what the + nether pathfinder reads with `elytraUseCache` on, and a region it fails to parse is gone for the + rest of the flight. ### Pathfinding correctness @@ -126,6 +140,18 @@ world stops matching the path. 512. - Baritone's threadpool threads are daemon threads. As of 26.2 the game waits on non-daemon threads at shutdown and files a crash report if any are still alive. +- **Cancelling `#elytra` froze the game.** 26.2 keeps one pathfinder context alive across engages, + and re-engaging blocked the game thread on the old one until it was freed. A path calculation + cannot be interrupted — nether-pathfinder's `cancel()` sets a flag its search loop does not read — + so the client hung for whatever the in-flight calculation had left, up to ten seconds. The engage + is deferred onto the teardown now, and the calculation timeout is three seconds. +- **The endless `Failed to compute path to destination`.** That same persistent context is fed + Baritone's region cache, and a region it fails to parse is never retried, so one bad read leaves + a permanent hole in its world and every later calculation fails against it. Three consecutive + failures discard the context so the next attempt starts from a clean cache; skipped while flying, + where a reset would drop the elytra. +- The above-build-limit fallback divided by a horizontal distance of zero, making every step of a + straight-up path `NaN`, and compared a squared distance against an unsquared 32. ## Building From 0a61ecf47e512f4c49226cafa9cf678a2e09dacb Mon Sep 17 00:00:00 2001 From: Dek Date: Fri, 4 Sep 2026 22:02:43 -0400 Subject: [PATCH 32/54] never run a path calculation under the read lock pathFindAsync picked its lock and executor by elytraPredictTerrain: the write lock when the search may generate terrain, the read lock otherwise, on the theory that a search that only reads the chunk cache can share it with the solvers. It cannot. With elytraUseCache on, pathFind parses Baritone's region files into the cache the first time a search touches them, prediction or not, and every insert can rehash the map. The two solvers run getChunkOrDefault under the same read lock, and on the native side that call is a plain find with no mutex of its own. A rehash under a bucket walk is heap corruption, and it showed up as a SIGSEGV in getChunkOrDefault on the render thread, mid flight, with terrain prediction off - the one configuration that used the read lock. A path calculation is always a writer. The prediction flag now only decides whether an unloaded chunk counts as air. The cost is that the render thread's solver skips its tick while a recomputation runs, which is what the prediction path has always done. --- .../process/elytra/NetherPathfinderContext.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java index 7540ec9176..424ed6aa33 100644 --- a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java +++ b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java @@ -181,8 +181,14 @@ public CompletableFuture pathFindAsync(final BlockPos src, fina final BlockPos adjustedSrc = src.below(minY); final BlockPos adjustedDst = dst.below(minY); boolean generate = Baritone.settings().elytraPredictTerrain.value && this.dimension == Level.NETHER; - Lock l = generate ? writeLock : readLock; - ExecutorService exec = generate ? writeExecutor : readExecutor; + // pathFind is always a writer, regardless of terrain prediction: it lazily parses baritone cache + // regions into chunkCache (gated only by checkedRegions, see nether-pathfinder parseBaritoneRegion) + // and generates terrain. It therefore has to exclude the getChunkOrDefault/raytrace lookups the + // solvers run under the read lock. Running it under the read lock instead lets those inserts (and the + // rehash they trigger) race a concurrent lookup's bucket walk, corrupting the map -> SIGSEGV in + // getChunkOrDefault. The prediction flag now only selects useAirIfChunkNotLoaded. + Lock l = writeLock; + ExecutorService exec = writeExecutor; try { return CompletableFuture.supplyAsync(() -> { l.lock(); From 3310b7f846dcb03caf6de040fd7d68693fc84f29 Mon Sep 17 00:00:00 2001 From: Dek Date: Fri, 4 Sep 2026 22:02:43 -0400 Subject: [PATCH 33/54] bound the pathfinder context teardown destroy() waited for the executors without limit. The native cancel is a no-op, so an in-flight search runs to its own timeout, and that timeout does not cover findAir's breadth-first search for a start cube or the region-file parsing a search can trigger. A search wedged in either pinned the teardown forever, and the teardown is what hands npfSema back: every later engage found the semaphore taken, deferred itself onto a completion that never came, and was dropped by the next onLostControl. One hung teardown was #elytra silently dead for the rest of the session, with nothing in the log to say so. Wait six seconds per executor - twice the native timeout - and if a search is still running after that, abandon the context: mark it destroyed so nothing new calls in and return without freeContext, which would be a use-after-free under the running search, and without taking the write lock, which that search holds. The leak is one context per wedged search. --- .../elytra/NetherPathfinderContext.java | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java index 424ed6aa33..4bb6d83fcd 100644 --- a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java +++ b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java @@ -342,17 +342,36 @@ public void cancel() { NetherPathfinder.cancel(this.context); } + // Generous relative to the 3000ms native pathFind timeout, which does not bound findAir's BFS or the + // region-file IO a search can trigger. A normal teardown finishes well inside this; overshooting it + // means a search is genuinely wedged in native code and this context is abandoned rather than freed. + private static final long TEARDOWN_TIMEOUT_MS = 6000; + public void destroy() { this.cancel(); // Ignore anything that was queued up, just shutdown the executor this.readExecutor.shutdownNow(); this.writeExecutor.shutdownNow(); + boolean terminated; try { - while (!this.readExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS)) {} - while (!this.writeExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS)) {} + // The native cancel is a no-op, so shutdownNow only drops queued tasks; an in-flight pathFind + // runs to its own timeout. Bound the wait: a wedged native call must not pin this teardown + // forever, because that never releases npfSema and silently kills every later engage. + terminated = this.readExecutor.awaitTermination(TEARDOWN_TIMEOUT_MS, TimeUnit.MILLISECONDS) + & this.writeExecutor.awaitTermination(TEARDOWN_TIMEOUT_MS, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); + terminated = false; + } + + if (!terminated) { + // A search is still running in native code. freeContext now would be a use-after-free, and the + // write lock is held by that same wedged search (pathFind is a writer), so taking it would + // deadlock. Abandon (leak) this context instead: mark it destroyed so nothing new calls in, and + // let the next engage build a fresh one. The leak is bounded to one context per wedged search. + this.destroyed = true; + return; } // The executors are drained, but the solver and render threads still raytrace through this From 677db2e3685f400515d0052c3c5d3a262b6d3af2 Mon Sep 17 00:00:00 2001 From: Dek Date: Fri, 4 Sep 2026 22:03:43 -0400 Subject: [PATCH 34/54] don't stitch the old path onto an unfinished segment pathRecalcSegment recomputes the path from the player up to a rejoin node and appends the rest of the old path after it. It did that whether or not the search reached the rejoin node. The native search gives up 500ms after its last progress, and a recomputation around an obstacle is often that kind of search: when it stopped short, the result was the new segment, then a straight jump to the rejoin node through whatever lay between, and the old path from there. No obstacle check catches that jump - both of its ends are in open air - so every recomputation ended the same way, and the solver circled the last node it could reach, next to a path drawn through solid ground. An unfinished segment now replaces the path on its own. It is marked incomplete, so attemptNextSegment picks it up from where the search got to, and a recomputation all the way to the destination no longer inherits the previous path's completeness either. --- .../baritone/process/elytra/ElytraBehavior.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/baritone/process/elytra/ElytraBehavior.java b/src/main/java/baritone/process/elytra/ElytraBehavior.java index 69fc5585d0..ced13beba8 100644 --- a/src/main/java/baritone/process/elytra/ElytraBehavior.java +++ b/src/main/java/baritone/process/elytra/ElytraBehavior.java @@ -227,7 +227,18 @@ public CompletableFuture pathRecalcSegment(final OptionalInt upToIncl) { final List after = upToIncl.isPresent() ? this.path.subList(upToIncl.getAsInt() + 1, this.path.size()) : Collections.emptyList(); final boolean complete = this.completePath; - return this.path0(ctx.playerFeet(), upToIncl.isPresent() ? fixDestination(this.path.get(upToIncl.getAsInt())) : destinationFixed(), segment -> segment.append(after.stream(), complete || (segment.isFinished() && !upToIncl.isPresent()))) + return this.path0(ctx.playerFeet(), upToIncl.isPresent() ? fixDestination(this.path.get(upToIncl.getAsInt())) : destinationFixed(), segment -> { + if (!upToIncl.isPresent() || !segment.isFinished()) { + // Either this went all the way to the destination and speaks for itself, or the + // search ran out of time short of the rejoin node. Tacking the old tail onto a + // segment that never reached it would leave a blind jump in the path, straight + // through whatever lies between, that no obstacle check can ever fix: every + // recomputation ends the same way, and the solver just circles the last node it + // can reach. Leave the path unfinished instead and continue from where it got to. + return segment; + } + return segment.append(after.stream(), complete); + }) .whenComplete((result, ex) -> { this.recalculating = false; if (ex != null) { From fa36a056c57a10599f3582e7345c6c2f3a859add Mon Sep 17 00:00:00 2001 From: Dek Date: Fri, 4 Sep 2026 22:03:43 -0400 Subject: [PATCH 35/54] repack the chunks around the player when the cache is wrong Everything the solver does runs against the pathfinder's own copy of the world: the pitch simulation, the hitbox raytraces, the obstacle checks along the path. If that copy disagrees with the client's, the solver aims at terrain it believes is air and nothing notices, because the raytraces that would catch it look at the same copy. Cancelling and re-engaging fixed it, by way of repackChunks, and that was the only thing that did. Two sources of disagreement. A chunk packet the client discards, because it is outside the view distance, still raises the chunk event, and the world hands back its shared empty chunk for it; packing that recorded a chunk of pure air as real terrain, and the path went straight through whatever is actually there. Skip those. The other is the mundane one, a chunk whose packing was missed or a section the block update path never saw, and the fix for that is to notice and re-read: on a horizontal collision, which the simulation never plans, when the cache says we are inside a block while we are plainly flying, and when a recomputation fails, the 3x3 chunks around the player are queued for packing again, at most once every two seconds. --- .../process/elytra/ElytraBehavior.java | 57 ++++++++++++++++++- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/src/main/java/baritone/process/elytra/ElytraBehavior.java b/src/main/java/baritone/process/elytra/ElytraBehavior.java index ced13beba8..8a4a002cd2 100644 --- a/src/main/java/baritone/process/elytra/ElytraBehavior.java +++ b/src/main/java/baritone/process/elytra/ElytraBehavior.java @@ -245,6 +245,7 @@ public CompletableFuture pathRecalcSegment(final OptionalInt upToIncl) { final Throwable cause = ex.getCause(); if (cause instanceof PathCalculationException) { logDirect("Failed to recompute segment"); + repackNearPlayer("segment recomputation failed"); } else { logUnhandledException(cause); } @@ -279,6 +280,7 @@ public void pathNextSegment(final int afterIncl) { final Throwable cause = ex.getCause(); if (cause instanceof PathCalculationException) { logDirect("Failed to compute next segment"); + repackNearPlayer("next segment computation failed"); if (pathStart.distToCenterSqr(ctx.player().position()) < 16 * 16) { logVerbose("Player is near the segment start, therefore repeating this calculation is pointless. Marking as complete"); completePath = true; @@ -354,7 +356,10 @@ private void pathfindAroundObstacles() { // the node closest to us is now inside a block, so the terrain changed since this path was computed final BetterBlockPos feet = ctx.playerFeet(); if (!ElytraBehavior.this.passable(feet.x, feet.y, feet.z, false)) { - return; // we're inside a block ourselves, recalculating from here can't succeed + // we're inside a block ourselves, recalculating from here can't succeed. while flying that + // means the cache is wrong about where we are, so refresh it + repackNearPlayer("the cache has us inside a block"); + return; } int rejoin = rangeEndExcl - 1; while (rejoin > rangeStartIncl && !ElytraBehavior.this.passable(path.get(rejoin).x, path.get(rejoin).y, path.get(rejoin).z, false)) { @@ -501,9 +506,53 @@ public void onRenderPass(RenderEvent event) { public void onChunkEvent(ChunkEvent event) { if (event.isPostPopulate() && this.npfContext != null) { - final LevelChunk chunk = ctx.world().getChunk(event.getX(), event.getZ()); - npfContext.queueForPacking(chunk); + // A chunk packet the client discarded (outside its view range) still raises this event, and the + // world hands back its shared empty chunk for it. Packing that would record a chunk of pure air as + // real terrain, and the path would then be routed straight through whatever is actually there. + final LevelChunk chunk = ctx.world().getChunkSource().getChunk(event.getX(), event.getZ(), false); + if (chunk != null && !chunk.isEmpty()) { + npfContext.queueForPacking(chunk); + } + } + } + + // Wall-clock so that it also bounds the repacks queued from the recomputation callbacks between ticks + private static final long REPACK_NEAR_COOLDOWN_MS = 2000; + private long lastRepackNearMs; + + /** + * Re-reads the chunks around the player into the pathfinder's cache. Everything the solver and the + * obstacle checks do runs against that cache, so if it disagrees with the world (a chunk whose packing was + * missed, a stale region from the on-disk cache that the live chunk never replaced) the solver aims at + * terrain it thinks is air and nothing in here notices - the raytraces look at the same cache. This is the + * fix that cancelling and re-engaging used to apply by accident, through {@code repackChunks}. + */ + private void repackNearPlayer(final String why) { + if (!ctx.minecraft().isSameThread()) { + ctx.minecraft().execute(() -> this.repackNearPlayer(why)); + return; + } + if (ctx.player() == null || ctx.world() == null || this.npfContext == null) { + return; + } + final long now = System.currentTimeMillis(); + if (now - this.lastRepackNearMs < REPACK_NEAR_COOLDOWN_MS) { + return; + } + this.lastRepackNearMs = now; + final ChunkPos center = ctx.player().chunkPosition(); + final ChunkSource chunkSource = ctx.world().getChunkSource(); + int packed = 0; + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + final LevelChunk chunk = chunkSource.getChunk(center.x() + dx, center.z() + dz, false); + if (chunk != null && !chunk.isEmpty()) { + npfContext.queueForPacking(chunk); + packed++; + } + } } + logVerbose("Repacking " + packed + " chunks around the player: " + why); } public void onBlockChange(BlockChangeEvent event) { @@ -620,6 +669,8 @@ public void tick() { if (ctx.player().horizontalCollision) { logVerbose("hbonk"); + // the simulation never flies into anything it knows about, so this is something it didn't + repackNearPlayer("hit something the cache didn't know about"); } if (ctx.player().verticalCollision) { logVerbose("vbonk"); From d2b111c48e87582a52c24ede00adcd7ae1609102 Mon Sep 17 00:00:00 2001 From: Dek Date: Fri, 4 Sep 2026 22:05:57 -0400 Subject: [PATCH 36/54] take off through the first clear cube overhead Where a nether flight ends when the rockets run out is where the next one has to start, and that is a basalt delta crevice or the hollow beside a lava pond as often as not: walls a couple of blocks away on every side, and open sky straight up. The standing takeoff asked for a path from our feet. The pathfinder starts every path from the nearest 4x4x4 cube of air, and its search for one walks straight through walls, so from a hole the first node was very often on the far side of one. The takeoff rocket flew at it, hit the wall, dropped back into the hole, and did that three times before giving up. The path now starts from the first clear cube directly above us, up to 32 blocks up, which is about what a rocket lit straight up is good for. The survival solver is changed to match. With a rocket burning it no longer settles for the first pitch that survives the horizon but takes the one that ends highest, since the boost is the one chance to get above whatever is boxing us in and straight up is where that is most likely to be clear. And it forces a rocket whenever an impact is anywhere inside the horizon instead of within twelve ticks of it, which from the top of a climb out of a hole meant falling most of the way back in first. --- .../java/baritone/process/ElytraProcess.java | 41 ++++++++++++++++++- .../process/elytra/ElytraBehavior.java | 15 +++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/main/java/baritone/process/ElytraProcess.java b/src/main/java/baritone/process/ElytraProcess.java index 52900b7cf6..fb62e6516b 100644 --- a/src/main/java/baritone/process/ElytraProcess.java +++ b/src/main/java/baritone/process/ElytraProcess.java @@ -122,6 +122,12 @@ public class ElytraProcess extends BaritoneProcessHelper implements IBaritonePro * How many ledges to walk towards before concluding that walking off one is never going to happen. */ private static final int MAX_WALK_OFF_ATTEMPTS = 2; + /** + * How far above our feet to look for the first clear 4x4x4 cube when taking off from where we stand. A + * duration-1 rocket lifts a stationary player around 25 blocks straight up before it dies, so a cube any + * further up is out of reach of a vertical takeoff anyway. + */ + private static final int TAKEOFF_MAX_EXIT_HEIGHT = 32; private int takeoffStallTicks; private int standingTakeoffs; private int walkOffAttempts; @@ -526,10 +532,16 @@ private PathingCommand standingTakeoff() { if (this.standingTakeoffs++ == 0) { logDirect("No spot to jump off from, taking off from here instead."); } + // The path starts from the first clear cube straight above us, which is where a climb on the takeoff + // rocket comes out. In a basalt delta crevice or the hollow beside a lava pond the walls are a couple of + // blocks away on every side, and up is the one direction that is reliably clear. Left to itself the + // pathfinder would start from the nearest clear cube to our feet, and its search for one ignores walls: + // from a hole that is very often a cube on the far side of one, which nothing here could ever reach. + final BetterBlockPos exit = takeoffExit(feet); // the elytra path has to exist before we leave the ground: the behavior does nothing without one, and the // handful of ticks between opening the elytra and hitting the ground is no time to compute one this.state = State.PAUSE; - this.behavior.pathManager.pathToDestination(feet).whenComplete((result, ex) -> { + this.behavior.pathManager.pathToDestination(exit != null ? exit : feet).whenComplete((result, ex) -> { if (ex == null) { this.state = State.TAKEOFF_JUMP; return; @@ -539,6 +551,33 @@ private PathingCommand standingTakeoff() { return new PathingCommand(null, PathingCommandType.REQUEST_PAUSE); } + /** + * The centre of the first 4x4x4 cube of air straight above {@code feet}, aligned the way the elytra + * pathfinder aligns its nodes, or {@code null} if there is none within {@link #TAKEOFF_MAX_EXIT_HEIGHT}. + */ + private BetterBlockPos takeoffExit(BetterBlockPos feet) { + final int minY = ctx.world().dimensionType().minY(); + final int top = minY + this.behavior.npfContext.getMaxHeight(); + final int limit = Math.min(feet.y + TAKEOFF_MAX_EXIT_HEIGHT, top - 4); + final int ox = feet.x & ~3; + final int oz = feet.z & ~3; + final BlockPos.MutableBlockPos mut = new BlockPos.MutableBlockPos(); + cubes: + for (int oy = feet.y & ~3; oy <= limit; oy += 4) { + for (int x = ox; x < ox + 4; x++) { + for (int y = oy; y < oy + 4; y++) { + for (int z = oz; z < oz + 4; z++) { + if (!ctx.world().getBlockState(mut.set(x, y, z)).isAir()) { + continue cubes; + } + } + } + } + return new BetterBlockPos(ox + 2, oy + 2, oz + 2); + } + return null; + } + /** * Opens the elytra mid jump, exactly the way {@link net.minecraft.client.player.LocalPlayer} does it when a * player double taps jump: set the flag locally, then tell the server. Doing it here instead of by toggling diff --git a/src/main/java/baritone/process/elytra/ElytraBehavior.java b/src/main/java/baritone/process/elytra/ElytraBehavior.java index 8a4a002cd2..7baf0ff596 100644 --- a/src/main/java/baritone/process/elytra/ElytraBehavior.java +++ b/src/main/java/baritone/process/elytra/ElytraBehavior.java @@ -848,7 +848,11 @@ private Solution solveSurvival(final SolverContext context, final Solution unsol } final int survivedTicks = best.steps.size() - 1; - if (survivedTicks < ticks && survivedTicks <= 12 && !context.boost.isBoosted() && context.hasFireworks) { + // No pitch keeps us clear for the whole horizon: if a rocket lit now would provably do better, light + // one. This used to wait until the impact was a dozen ticks out, which from the top of a climb out of + // a hole meant falling most of the way back in before doing anything about it. + final boolean impactAhead = survivedTicks < ticks; + if (impactAhead && !context.boost.isBoosted() && context.hasFireworks) { final PitchResult boosted = this.solveSurvivalPitch(context, yaw, ticks, 10, 2); if (boosted != null && boosted.steps.size() > best.steps.size() + 2) { final Vec3 last = boosted.steps.get(boosted.steps.size() - 1); @@ -869,12 +873,17 @@ private PitchResult solveSurvivalPitch(final SolverContext context, final float final float currentPitch = ctx.playerRotations().getPitch(); PitchResult best = null; - // scan the climbing side (negative pitch) first; new walls are usually escapable from above + // scan the climbing side (negative pitch) first; new walls are usually escapable from above. With a + // rocket burning, don't settle for the first pitch that survives: take the one that ends highest. The + // boost is the one chance to get above whatever is boxing us in, and straight up is where that is most + // likely to be clear (the hole we just took off from, a crevice, the hollow beside a lava pond). + final boolean preferHeight = ticksBoosted > 0; for (float pitch = currentPitch; pitch >= -90; pitch -= 3) { if (Thread.interrupted()) return null; best = this.betterSurvival(context, direction, pitch, ticks, ticksBoosted, ticksBoostDelay, best); - if (best != null && best.steps.size() - 1 >= ticks) return best; + if (!preferHeight && best != null && best.steps.size() - 1 >= ticks) return best; } + if (best != null && best.steps.size() - 1 >= ticks) return best; for (float pitch = currentPitch + 3; pitch <= 90; pitch += 3) { if (Thread.interrupted()) return null; best = this.betterSurvival(context, direction, pitch, ticks, ticksBoosted, ticksBoostDelay, best); From 5611a7b5205a6a4d8f33769e9f54185ddd6a9989 Mon Sep 17 00:00:00 2001 From: Dek Date: Fri, 4 Sep 2026 22:05:59 -0400 Subject: [PATCH 37/54] light the takeoff rocket the tick after the elytra opens The standing takeoff lit its rocket in the same tick it opened the elytra, on the grounds that a glide starting from a standstill a block and a half up has no time to spare. Logs from 2b2t show what that got us: every standing takeoff opened the elytra and was back on the ground within half a second, with no push from the rocket at all. The server never accepted it. A vanilla client cannot use an item in the tick it starts gliding - its use packets go out before its glide packet, while the elytra is still shut - and the use packet carries a rotation of its own, read off the player as the packet is built, which was the camera's, while the movement packet in the same tick carried the takeoff aim. Two different looks in one tick is exactly what the server's movement checks compare, and the rocket went the way of anything that fails them. So the rocket goes the tick after, once the solver has aimed, and that costs nothing: the rocket takes a round trip to show up regardless. Every use packet now carries the rotation the movement packet is about to, via the new LookBehavior.getRotationForThisTick, by facing it for the duration of the click. The solver had a rule for that round trip which made this worse. A rocket whose entity had not arrived yet counted as no rocket, so the tick after lighting one it planned an unboosted trajectory: a shallow glide, since nothing else goes anywhere without thrust, pointed at the nearest wall. It now assumes a rocket it lit within the last ten ticks has been burning since it lit it, and drops the assumption with the cooldown if the entity never comes. The old two-tick special case and its boost counter are gone with it; they were shorter than 2b2t's round trip anyway. A rocket that never shows up is logged, and so is an elytra the server shuts again while we are still in the air right after a takeoff, since that is the server refusing the glide and worth telling apart from a takeoff that merely went nowhere. --- .../java/baritone/behavior/LookBehavior.java | 13 ++++ .../java/baritone/process/ElytraProcess.java | 29 ++++++++- .../process/elytra/ElytraBehavior.java | 59 +++++++++++++------ 3 files changed, 79 insertions(+), 22 deletions(-) diff --git a/src/main/java/baritone/behavior/LookBehavior.java b/src/main/java/baritone/behavior/LookBehavior.java index 98e12aeff3..bdb8d7e284 100644 --- a/src/main/java/baritone/behavior/LookBehavior.java +++ b/src/main/java/baritone/behavior/LookBehavior.java @@ -158,6 +158,19 @@ public void pig() { } } + /** + * The rotation this tick's movement packet is going to carry: the one {@link #onPlayerUpdate} applies for the + * current target, or the player's own if there is no target or the target leaves the rotation alone. Anything + * else sent this tick that carries a look of its own (a use packet) has to match it exactly, because the + * server-side movement checks compare the two. + */ + public Rotation getRotationForThisTick() { + if (this.target == null || this.target.mode == Target.Mode.NONE) { + return new Rotation(ctx.player().getYRot(), ctx.player().getXRot()); + } + return this.processor.peekRotation(this.target.rotation); + } + public Optional getEffectiveRotation() { if (Baritone.settings().freeLook.value) { return Optional.ofNullable(this.serverRotation); diff --git a/src/main/java/baritone/process/ElytraProcess.java b/src/main/java/baritone/process/ElytraProcess.java index fb62e6516b..cc92605eef 100644 --- a/src/main/java/baritone/process/ElytraProcess.java +++ b/src/main/java/baritone/process/ElytraProcess.java @@ -134,6 +134,12 @@ public class ElytraProcess extends BaritoneProcessHelper implements IBaritonePro private int takeoffAirborneTicks; private boolean walkOffImpossible; private boolean takeoffBoostPending; + /** + * Ticks since a takeoff opened the elytra, or {@code -1} outside a takeoff. Only the server can shut an + * elytra again, so one that is shut while we are still in the air this soon after opening it is a takeoff + * the server refused, which is worth telling the user apart from one that merely didn't get anywhere. + */ + private int takeoffOpenedTicksAgo = -1; private int landingColumnHeight = SHORT_LANDING_COLUMN_HEIGHT; private Set badLandingSpots = new HashSet<>(); private LandingSearchState landingSearchState; @@ -163,6 +169,7 @@ public void onLostControl(boolean destroyNpf) { this.takeoffAirborneTicks = 0; this.walkOffImpossible = false; this.takeoffBoostPending = false; + this.takeoffOpenedTicksAgo = -1; CompletableFuture behaviorGone = destroyBehaviorAsync(); if (destroyNpf) { destroyNpfContextAsync(behaviorGone); @@ -336,6 +343,10 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { this.state = State.START_FLYING; this.takeoffStallTicks = 0; this.takeoffBoostPending = true; + this.takeoffOpenedTicksAgo = 0; + } + if (this.takeoffOpenedTicksAgo >= 0) { + this.takeoffOpenedTicksAgo++; } behavior.landingMode = this.state == State.LANDING; this.goal = null; @@ -366,6 +377,15 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { return new PathingCommand(null, PathingCommandType.REQUEST_PAUSE); } + if (this.takeoffOpenedTicksAgo >= 0) { + // we opened the elytra and it is shut again: on the ground that is a takeoff that didn't get + // anywhere, in the air it is the server refusing the glide, and the takeoff rocket with it + if (!ctx.player().onGround()) { + logDirect("The server closed the elytra " + this.takeoffOpenedTicksAgo + " ticks after takeoff."); + } + this.takeoffOpenedTicksAgo = -1; + } + if (this.state == State.FLYING || this.state == State.START_FLYING) { if (ctx.player().onGround() && Baritone.settings().elytraAutoJump.value) { this.state = State.LOCATE_JUMP; @@ -592,9 +612,12 @@ private boolean openElytra() { return false; } ctx.player().connection.send(new ServerboundPlayerCommandPacket(ctx.player(), ServerboundPlayerCommandPacket.Action.START_FALL_FLYING)); - // no speed and a block and a half of air underneath: the boost can't wait for the solver to ask for one - // on the next tick. the elytra is open as far as the client is concerned, so the rocket attaches to us - this.takeoffBoostPending = !this.behavior.useFireworkForTakeoff(); + // The rocket goes next tick, once the solver has aimed. A vanilla client cannot use an item in the tick + // it starts gliding (its use packets go out before its glide packet, while the elytra is still shut), + // and the use packet carries a look that has to match that tick's movement packet, which the solver + // has not decided yet. The tick costs nothing: the rocket takes a round trip to show up regardless. + this.takeoffBoostPending = true; + this.takeoffOpenedTicksAgo = 0; return true; } diff --git a/src/main/java/baritone/process/elytra/ElytraBehavior.java b/src/main/java/baritone/process/elytra/ElytraBehavior.java index 7baf0ff596..7db56272c7 100644 --- a/src/main/java/baritone/process/elytra/ElytraBehavior.java +++ b/src/main/java/baritone/process/elytra/ElytraBehavior.java @@ -87,6 +87,7 @@ public final class ElytraBehavior implements Helper { public final PathManager pathManager; private final ElytraProcess process; + private static final int FIREWORK_COOLDOWN_TICKS = 10; /** * Remaining cool-down ticks between firework usage */ @@ -105,9 +106,11 @@ public final class ElytraBehavior implements Helper { * Updated every time a firework is automatically used */ private int minimumBoostTicks; - - private boolean deployedFireworkLastTick; - private final int[] nextTickBoostCounter; + /** + * Whether the entity of the last rocket we lit has been seen attached to us. Only for the verbose log: + * a rocket that never shows up is one the server ignored. + */ + private boolean fireworkSeenSinceLit = true; private BlockStateInterface bsi; public final BetterBlockPos destination; @@ -134,7 +137,6 @@ public ElytraBehavior(Baritone baritone, ElytraProcess process, NetherPathfinder this.destination = new BetterBlockPos(destination); this.appendDestination = appendDestination; this.solverExecutor = Executors.newSingleThreadExecutor(); - this.nextTickBoostCounter = new int[2]; this.npfContext = npf; @@ -624,11 +626,17 @@ private void onTick0() { // Certified mojang employee incident if (this.remainingFireworkTicks > 0) { this.remainingFireworkTicks--; + if (this.remainingFireworkTicks == 0 && !this.fireworkSeenSinceLit) { + logVerbose("the rocket lit " + FIREWORK_COOLDOWN_TICKS + " ticks ago never showed up, the server ignored it"); + } } if (this.remainingSetBackTicks > 0) { this.remainingSetBackTicks--; } - if (!this.getAttachedFirework().isPresent()) { + if (this.getAttachedFirework().isPresent()) { + this.fireworkSeenSinceLit = true; + } else if (this.remainingFireworkTicks <= 0) { + // nothing burning, and nothing lit recently enough for its entity to still be on its way from the server this.minimumBoostTicks = 0; } @@ -687,11 +695,6 @@ public void tick() { solution = this.solveAngles(solverContext); } - if (this.deployedFireworkLastTick) { - this.nextTickBoostCounter[solverContext.boost.isBoosted() ? 1 : 0]++; - this.deployedFireworkLastTick = false; - } - final boolean inLava = ctx.player().isInLava(); if (inLava) { baritone.getInputOverrideHandler().setInputForceState(Input.JUMP, true); @@ -952,10 +955,25 @@ private boolean useFirework() { logDirect("no fireworks"); return false; } - ctx.playerController().processRightClick(ctx.player(), ctx.world(), InteractionHand.MAIN_HAND); + // The use packet carries a look of its own, read off the player as it is built, and the server-side + // movement check compares it with the look in this tick's movement packet: the rotation LookBehavior + // is about to apply for the solver's target, not what the player is facing right now. Face that for + // the duration of the click so the two agree; the look behavior sets or restores the real rotation + // later in the tick regardless. + final Rotation wire = this.baritone.getLookBehavior().getRotationForThisTick(); + final float yaw = ctx.player().getYRot(); + final float pitch = ctx.player().getXRot(); + ctx.player().setYRot(wire.getYaw()); + ctx.player().setXRot(wire.getPitch()); + try { + ctx.playerController().processRightClick(ctx.player(), ctx.world(), InteractionHand.MAIN_HAND); + } finally { + ctx.player().setYRot(yaw); + ctx.player().setXRot(pitch); + } this.minimumBoostTicks = 10 * (1 + getFireworkBoost(ctx.player().getItemInHand(InteractionHand.MAIN_HAND)).orElse(0)); - this.remainingFireworkTicks = 10; - this.deployedFireworkLastTick = true; + this.remainingFireworkTicks = FIREWORK_COOLDOWN_TICKS; + this.fireworkSeenSinceLit = false; return true; } @@ -1012,12 +1030,15 @@ public SolverContext(boolean async) { } this.hasFireworks = fireworks; - final Integer fireworkTicksExisted; - if (async && ElytraBehavior.this.deployedFireworkLastTick) { - final int[] counter = ElytraBehavior.this.nextTickBoostCounter; - fireworkTicksExisted = counter[1] > counter[0] ? 0 : null; - } else { - fireworkTicksExisted = ElytraBehavior.this.getAttachedFirework().map(e -> e.tickCount).orElse(null); + Integer fireworkTicksExisted = ElytraBehavior.this.getAttachedFirework().map(e -> e.tickCount).orElse(null); + if (fireworkTicksExisted == null && ElytraBehavior.this.remainingFireworkTicks > 0) { + // A rocket we lit within the cooldown whose entity the server hasn't shown us yet: a round trip, + // which on a busy server is several ticks. Solve as though it had been burning since we lit it. + // The boost follows whatever we aim at once it arrives, and planning an unboosted trajectory in + // the meantime - a shallow glide, since without thrust nothing else gets anywhere - is what + // points a takeoff rocket at the nearest wall. If it never shows up the assumption expires with + // the cooldown. The end-of-tick solve is for the next tick, whose cooldown is one lower. + fireworkTicksExisted = FIREWORK_COOLDOWN_TICKS - ElytraBehavior.this.remainingFireworkTicks + (async ? 1 : 0); } this.boost = new FireworkBoost(fireworkTicksExisted, ElytraBehavior.this.minimumBoostTicks); From 54b3c12a0fe9de6cc8d25a4f48f222c7bc6435fa Mon Sep 17 00:00:00 2001 From: Dek Date: Fri, 4 Sep 2026 22:06:00 -0400 Subject: [PATCH 38/54] climb out of lava on the takeoff rocket An elytra opens in lava - vanilla only refuses in water - but it does nothing there: while we are in a fluid, glide or not, vanilla moves us by the fluid rules, so the wings are inert and a rocket is the only thrust, at a fraction of its usual push. Two things went wrong with that. In the air the solver aimed at the path, and from a lava pond the path leads through the pond's basalt wall, so the rocket spent itself pushing against rock. And on the ground the process had no idea it was in lava at all: the landing logic saw a complete path and went looking for a landing spot, every path it asked for from inside the pool failed on the spot, and it asked for another one next tick, twenty times a second, each one a whole new ElytraBehavior. In lava, everything else waits. ElytraProcess swims up, opens the elytra as soon as we are off the pool floor, and asks for a path from the first clear cube above us if there is none yet. The solver, when its context says we are in lava, skips the path and solves for the steepest climb the simulation keeps clear, with a rocket assumed to be burning and a rocket forced every time; the setback delay and landing mode no longer hold that rocket back, since neither is a reason to keep sitting in lava. The landing search is skipped while we are in it. The BlockStateInterface the solver reads through is now created before the empty-path return in onTick0, since the lava escape solves without a path, and tick() bails if it has not been created yet rather than dereferencing it. --- .../java/baritone/process/ElytraProcess.java | 40 ++++++++++- .../process/elytra/ElytraBehavior.java | 67 +++++++++++++++---- 2 files changed, 92 insertions(+), 15 deletions(-) diff --git a/src/main/java/baritone/process/ElytraProcess.java b/src/main/java/baritone/process/ElytraProcess.java index cc92605eef..e61fe86756 100644 --- a/src/main/java/baritone/process/ElytraProcess.java +++ b/src/main/java/baritone/process/ElytraProcess.java @@ -140,6 +140,7 @@ public class ElytraProcess extends BaritoneProcessHelper implements IBaritonePro * the server refused, which is worth telling the user apart from one that merely didn't get anywhere. */ private int takeoffOpenedTicksAgo = -1; + private boolean lavaPathRequested; private int landingColumnHeight = SHORT_LANDING_COLUMN_HEIGHT; private Set badLandingSpots = new HashSet<>(); private LandingSearchState landingSearchState; @@ -170,6 +171,7 @@ public void onLostControl(boolean destroyNpf) { this.walkOffImpossible = false; this.takeoffBoostPending = false; this.takeoffOpenedTicksAgo = -1; + this.lavaPathRequested = false; CompletableFuture behaviorGone = destroyBehaviorAsync(); if (destroyNpf) { destroyNpfContextAsync(behaviorGone); @@ -264,6 +266,10 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { this.behavior.onTick(); + if (!ctx.player().isFallFlying() && ctx.player().isInLava()) { + return lavaTakeoff(); + } + if (calcFailed) { if (this.state == State.LOCATE_JUMP || this.state == State.GET_TO_JUMP) { return standingTakeoff(); @@ -273,8 +279,11 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); } + // No landing business while in lava. The solver is busy climbing out, and every path the landing search + // asks for from in here starts inside the pool and fails at once, which used to re-issue one every tick. + final boolean inLava = ctx.player().isInLava(); boolean safetyLanding = false; - if (ctx.player().isFallFlying() && shouldLandForSafety()) { + if (ctx.player().isFallFlying() && !inLava && shouldLandForSafety()) { if (Baritone.settings().elytraAllowEmergencyLand.value) { logDirect("Emergency landing - almost out of elytra durability or fireworks"); safetyLanding = true; @@ -282,7 +291,7 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { logDirect("almost out of elytra durability or fireworks, but I'm going to continue since elytraAllowEmergencyLand is false"); } } - if (ctx.player().isFallFlying() && this.state != State.LANDING && (this.behavior.pathManager.isComplete() || safetyLanding)) { + if (ctx.player().isFallFlying() && !inLava && this.state != State.LANDING && (this.behavior.pathManager.isComplete() || safetyLanding)) { final BetterBlockPos last = this.behavior.pathManager.path.getLast(); if (last != null && (last.distToCenterSqr(ctx.player().position()) < (48 * 48) || safetyLanding) && (!goingToLandingSpot || (safetyLanding && this.landingSpot == null))) { if (this.landingSearchState == null) { @@ -571,6 +580,33 @@ private PathingCommand standingTakeoff() { return new PathingCommand(null, PathingCommandType.REQUEST_PAUSE); } + /** + * We are in lava and the elytra is not open. Whatever the ground states were doing can wait: an elytra opens + * in lava (vanilla only refuses in water), and a rocket lit looking straight up is what gets us out. Swimming + * is far too slow, and since vanilla moves us by the fluid rules while we are in one, glide or not, the + * rocket is the only thrust there is and keeps a fraction of its usual push. Pointing it at the pool's wall + * would spend all of that on basalt, so once the elytra is open {@link ElytraBehavior} aims straight up and + * keeps lighting rockets until we are out. + */ + private PathingCommand lavaTakeoff() { + baritone.getPathingBehavior().secretInternalSegmentCancel(); + baritone.getInputOverrideHandler().clearAllKeys(); + // swimming up: slow, but it also lifts us off the pool floor, without which the elytra can't open + baritone.getInputOverrideHandler().setInputForceState(Input.JUMP, true); + if (this.behavior.pathManager.getPath().isEmpty() && !this.lavaPathRequested) { + // nothing to fly along once we're out; the takeoff states would have computed this + this.lavaPathRequested = true; + final BetterBlockPos feet = ctx.playerFeet(); + final BetterBlockPos exit = takeoffExit(feet); + this.behavior.pathManager.pathToDestination(exit != null ? exit : feet); + } + if (!ctx.player().onGround() && openElytra()) { + this.state = State.START_FLYING; + this.takeoffStallTicks = 0; + } + return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); + } + /** * The centre of the first 4x4x4 cube of air straight above {@code feet}, aligned the way the elytra * pathfinder aligns its nodes, or {@code null} if there is none within {@link #TAKEOFF_MAX_EXIT_HEIGHT}. diff --git a/src/main/java/baritone/process/elytra/ElytraBehavior.java b/src/main/java/baritone/process/elytra/ElytraBehavior.java index 7db56272c7..10380263aa 100644 --- a/src/main/java/baritone/process/elytra/ElytraBehavior.java +++ b/src/main/java/baritone/process/elytra/ElytraBehavior.java @@ -647,6 +647,11 @@ private void onTick0() { this.simulationLine = null; this.aimPos = null; + // ctx AND context???? :DDD + // Before the path check: everything the solver simulates goes through passable(), and the lava escape + // solves without a path + this.bsi = new BlockStateInterface(ctx); + final List path = this.pathManager.getPath(); if (path.isEmpty()) { return; @@ -655,8 +660,6 @@ private void onTick0() { return; } - // ctx AND context???? :DDD - this.bsi = new BlockStateInterface(ctx); this.pathManager.tick(); final int playerNear = this.pathManager.getNear(); @@ -670,7 +673,8 @@ private void onTick0() { * Called by {@link baritone.process.ElytraProcess#onTick(boolean, boolean)} when the process is in control and the player is flying */ public void tick() { - if (this.pathManager.getPath().isEmpty()) { + final boolean inLava = ctx.player().isInLava(); + if (this.pathManager.getPath().isEmpty() && !inLava) { return; } trySwapElytra(); @@ -683,6 +687,10 @@ public void tick() { if (ctx.player().verticalCollision) { logVerbose("vbonk"); } + if (this.bsi == null) { + // onTick0 has not run yet (a path calculation has held the lock every tick so far), nothing to solve against + return; + } final SolverContext solverContext = this.new SolverContext(false); this.solveNextTick = true; @@ -695,7 +703,6 @@ public void tick() { solution = this.solveAngles(solverContext); } - final boolean inLava = ctx.player().isInLava(); if (inLava) { baritone.getInputOverrideHandler().setInputForceState(Input.JUMP, true); } @@ -718,7 +725,8 @@ public void tick() { solution.context.start, solution.goingTo, solution.context.boost.isBoosted(), - solution.forceUseFirework || inLava + solution.forceUseFirework || inLava, + inLava ); } @@ -743,6 +751,9 @@ public void onPostTick(TickEvent event) { // calls passable which requires a read lock private Solution solveAngles(final SolverContext context) { + if (context.ignoreLava) { + return this.solveLavaEscape(context); + } final NetherPath path = context.path; final int playerNear = landingMode ? path.size() - 1 : context.playerNear; final Vec3 start = context.start; @@ -831,6 +842,34 @@ private Solution solveAngles(final SolverContext context) { return this.solveSurvival(context, solution); } + /** + * In lava the elytra is inert: vanilla moves us by the fluid rules whenever we are in one, glide or not, so + * the only thrust is the rocket, at a fraction of its usual push, and the pool is walled in a few blocks + * away on every side. Aiming at the path would spend the rocket pushing against basalt. The way out is up: + * the steepest climb the simulation keeps clear, and a rocket, every time. Works without a path, since a + * lava takeoff may still be waiting for its first one. + */ + private Solution solveLavaEscape(final SolverContext context) { + final NetherPath path = context.path; + // yaw is irrelevant to a vertical rocket; face where we are going so that the climb comes out pointed + // along the path once the normal solver takes over + final Vec3 towards = path.isEmpty() + ? Vec3.atCenterOf(this.destination) + : path.getVec(Math.min(context.playerNear + 1, path.size() - 1)); + final float yaw = RotationUtils.calcRotationFromVec3d(context.start, towards, ctx.playerRotations()).getYaw(); + final int ticks = Math.max(5, Baritone.settings().elytraSimulationTicks.value); + // simulate with a rocket burning whether or not one is yet: the escape needs one, and tick() lights it + final int ticksBoosted = context.boost.isBoosted() ? Math.max(1, context.boost.getGuaranteedBoostTicks()) : 10; + final int ticksBoostDelay = context.boost.isBoosted() ? 0 : 2; + final PitchResult best = this.solveSurvivalPitch(context, yaw, ticks, ticksBoosted, ticksBoostDelay); + if (best == null) { + return null; // cancelled by the game thread + } + this.simulationLine = best.steps; + final Vec3 last = best.steps.get(best.steps.size() - 1); + return new Solution(context, new Rotation(yaw, best.pitch), context.start.add(last), true, true); + } + /** * Last resort when no pitch reaches the path (e.g. the terrain changed and the goal is now walled off): * fly the pitch that stays collision-free the longest, biased towards climbing, so that the path @@ -851,13 +890,14 @@ private Solution solveSurvival(final SolverContext context, final Solution unsol } final int survivedTicks = best.steps.size() - 1; - // No pitch keeps us clear for the whole horizon: if a rocket lit now would provably do better, light - // one. This used to wait until the impact was a dozen ticks out, which from the top of a climb out of - // a hole meant falling most of the way back in before doing anything about it. + // No pitch keeps us clear for the whole horizon, or we are in lava: if a rocket lit now would provably + // do better, light one. This used to wait until the impact was a dozen ticks out, which from the top of + // a climb out of a hole meant falling most of the way back in before doing anything about it. final boolean impactAhead = survivedTicks < ticks; - if (impactAhead && !context.boost.isBoosted() && context.hasFireworks) { + if ((impactAhead || context.ignoreLava) && !context.boost.isBoosted() && context.hasFireworks) { final PitchResult boosted = this.solveSurvivalPitch(context, yaw, ticks, 10, 2); - if (boosted != null && boosted.steps.size() > best.steps.size() + 2) { + if (boosted != null && (boosted.steps.size() > best.steps.size() + 2 + || (context.ignoreLava && boosted.steps.size() >= best.steps.size()))) { final Vec3 last = boosted.steps.get(boosted.steps.size() - 1); this.simulationLine = boosted.steps; return new Solution(context, new Rotation(yaw, boosted.pitch), context.start.add(last), true, true); @@ -904,12 +944,13 @@ private PitchResult betterSurvival(final SolverContext context, final Vec3 direc return best == null || score > best.dot ? new PitchResult(pitch, score, steps) : best; } - private void tickUseFireworks(final Vec3 start, final Vec3 goingTo, final boolean isBoosted, final boolean forceUseFirework) { - if (this.remainingSetBackTicks > 0) { + private void tickUseFireworks(final Vec3 start, final Vec3 goingTo, final boolean isBoosted, final boolean forceUseFirework, final boolean inLava) { + // neither the setback delay nor a landing in progress is a reason to keep burning + if (this.remainingSetBackTicks > 0 && !inLava) { logDebug("waiting for elytraFireworkSetbackUseDelay: " + this.remainingSetBackTicks); return; } - if (this.landingMode) { + if (this.landingMode && !inLava) { return; } final boolean useOnDescend = !Baritone.settings().elytraConserveFireworks.value || ctx.player().position().y < goingTo.y + 5; From b1dad68cca325e04a54f2c7357db23e61474cd20 Mon Sep 17 00:00:00 2001 From: Dek Date: Fri, 4 Sep 2026 22:13:05 -0400 Subject: [PATCH 39/54] readme: the takeoff, lava and cache fixes --- README.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/README.md b/README.md index 634148324f..0455a29154 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,43 @@ world stops matching the path. hardcoded 20). Higher cuts corners harder at firework speed. - **`elytraStandingTakeoff`** turns the standing takeoff on and off. On by default, capped at three attempts so a bad spot cannot burn a stack of rockets. +- **Taking off from a hole.** Where a nether flight ends when the rockets run out is where the next + one starts, and in the basalt deltas that is a crevice with walls a couple of blocks away on every + side and open sky straight up. The standing takeoff asked for a path from your feet, and the + pathfinder starts every path from the nearest 4×4×4 cube of air, found by a search that walks + straight through walls — from a hole, very often a cube on the far side of one. The rocket flew at + it, hit the wall and dropped back in. The path now starts from the first clear cube straight + overhead. With a rocket burning, the survival solver takes the pitch that ends highest rather than + the first one that survives, and it forces a rocket as soon as an impact is anywhere in its + horizon instead of a dozen ticks out. +- **The takeoff rocket was never accepted.** It was lit in the same tick the elytra opened, which no + vanilla client can do — its use packet goes out before the glide packet — and it carried the + camera's rotation while that tick's movement packet carried the takeoff aim, two looks the + server's movement checks compare. The rocket goes the tick after now, every use packet carries + the rotation the movement packet will, and the solver assumes a rocket it has just lit is burning + instead of planning an unboosted glide into the nearest wall while the entity is in transit. A + takeoff the server refuses is reported as such. +- **Lava.** An elytra opens in lava but does nothing there — vanilla moves you by the fluid rules + — so the rocket is the only thrust, and it was aimed at a path that leads through the pond's wall + while the landing logic asked for a new path from inside the pool twenty times a second. In lava + the process now swims up, opens the elytra, aims straight up and lights rockets until it is out; + the landing search waits. +- **Unfinished segments.** A recomputation around an obstacle that ran out of time short of its + rejoin node had the old path stitched on anyway, leaving a blind jump through whatever lay + between; every recomputation ended the same way, and the solver circled next to a path drawn + through solid ground. An unfinished segment now stands on its own and is continued from where the + search got to. +- **A cache that disagrees with the world.** Everything the solver does runs against the + pathfinder's copy of the world, and when that copy was wrong nothing noticed, because the + raytraces look at the same copy. A chunk packet the client discarded was packed as a real chunk + made of air, and a chunk whose packing was missed stayed missed until a cancel. Discarded chunks + are skipped now, and a collision, a "we are inside a block" verdict or a failed recomputation + re-reads the chunks around the player. +- **A hung teardown.** The pathfinder context's teardown waited without limit for a native search + that the cancel cannot interrupt. On 26.2, where the teardown hands back the semaphore the next + engage needs, one hung search silently killed every later `#elytra` for the session; elsewhere it + pinned a thread and leaked the context. The wait is bounded now, and a context whose search is + still wedged after it is abandoned rather than freed underneath it. - **Cancelling could crash the client.** The nether pathfinder's native context was freed once its own executors had drained, but the solver and the game thread raytrace through it outside those executors, and the octree interface caches a raw pointer into it. Running one of those against @@ -152,6 +189,10 @@ world stops matching the path. where a reset would drop the elytra. - The above-build-limit fallback divided by a horizontal distance of zero, making every step of a straight-up path `NaN`, and compared a squared distance against an unsquared 32. +- **Segfault in `getChunkOrDefault`.** The native path calculation ran under the read lock whenever + terrain prediction was off, on the theory that it only reads the cache. With `elytraUseCache` it + also parses region files into it, and an insert that rehashes the map under the solvers' lookups + is heap corruption. A path calculation is always a writer now. ## Building From 30176ed90e8a2b8b1a04e0a75e87a5054a2122c9 Mon Sep 17 00:00:00 2001 From: rfresh2 <89827146+rfresh2@users.noreply.github.com> Date: Sun, 18 Jan 2026 17:25:52 -0800 Subject: [PATCH 40/54] 26.3 --- fabric/src/main/resources/fabric.mod.json | 4 +- forge/src/main/resources/META-INF/mods.toml | 2 +- gradle.properties | 10 ++-- .../resources/META-INF/neoforge.mods.toml | 2 +- .../baritone/api/utils/BlockOptionalMeta.java | 9 ++- .../mixins/MixinClientPlayNetHandler.java | 4 +- .../launch/mixins/MixinRenderPipelines.java | 2 +- .../launch/mixins/MixinWorldRenderer.java | 10 ++-- .../command/ExampleBaritoneControl.java | 5 +- .../java/baritone/process/BuilderProcess.java | 2 +- .../java/baritone/process/FarmProcess.java | 15 +---- .../java/baritone/utils/BlockBreakHelper.java | 5 +- .../java/baritone/utils/BlockPlaceHelper.java | 3 +- src/main/java/baritone/utils/IRenderer.java | 60 +++++++++++-------- .../java/baritone/utils/PathRenderer.java | 2 +- .../utils/accessor/IRenderPipelines.java | 2 +- .../baritone/utils/pathing/Avoidance.java | 4 +- 17 files changed, 71 insertions(+), 70 deletions(-) diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json index 76da7a3245..38ed3e5baf 100644 --- a/fabric/src/main/resources/fabric.mod.json +++ b/fabric/src/main/resources/fabric.mod.json @@ -24,8 +24,8 @@ "mixins.baritone.json" ], "depends": { - "fabricloader": ">=0.19.3", - "minecraft": ["26.2"] + "fabricloader": ">=0.19.5", + "minecraft": "~26.3" }, "custom": { "modmenu": { diff --git a/forge/src/main/resources/META-INF/mods.toml b/forge/src/main/resources/META-INF/mods.toml index 5328e7bbf4..c6d1f26722 100644 --- a/forge/src/main/resources/META-INF/mods.toml +++ b/forge/src/main/resources/META-INF/mods.toml @@ -35,6 +35,6 @@ A Minecraft pathfinder bot. modId="minecraft" mandatory=true # This version range declares a minimum of the current minecraft version up to but not including the next major version -versionRange="[26.2]" +versionRange="[26.3, 26.4)" ordering="NONE" side="BOTH" diff --git a/gradle.properties b/gradle.properties index 92947ba3ee..c2d1767cce 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,20 +1,20 @@ org.gradle.jvmargs=-Xmx4G -available_loaders=fabric,forge,neoforge,tweaker +available_loaders=fabric,neoforge,tweaker -mod_version=1.19.0 +mod_version=1.20.0 maven_group=baritone archives_base_name=baritone java_version=25 -minecraft_version=26.2 +minecraft_version=26.3 forge_version=65.0.3 -neoforge_version=11-beta +neoforge_version=6-beta -fabric_version=0.19.3 +fabric_version=0.19.5 nether_pathfinder_version=1.6 diff --git a/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/neoforge/src/main/resources/META-INF/neoforge.mods.toml index 4eec7768ea..567bd7abe5 100644 --- a/neoforge/src/main/resources/META-INF/neoforge.mods.toml +++ b/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -35,7 +35,7 @@ A Minecraft pathfinder bot. modId="minecraft" type="required" # This version range declares a minimum of the current minecraft version up to but not including the next major version -versionRange="[26.2]" +versionRange="[26.3, 26.4)" ordering="NONE" side="BOTH" diff --git a/src/api/java/baritone/api/utils/BlockOptionalMeta.java b/src/api/java/baritone/api/utils/BlockOptionalMeta.java index 0685aa8523..1505a448d5 100644 --- a/src/api/java/baritone/api/utils/BlockOptionalMeta.java +++ b/src/api/java/baritone/api/utils/BlockOptionalMeta.java @@ -59,7 +59,6 @@ import sun.misc.Unsafe; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.*; @@ -306,22 +305,22 @@ public static CompletableFuture load() { // Simplified from {@link net.minecraft.server.WorldLoader#load()} CloseableResourceManager closeableResourceManager = new MultiPackResourceManager( PackType.SERVER_DATA, - List.of(ServerPacksSource.createVanillaPackSource()) + List.of(ServerPacksSource.createVanillaPackSource().fullResources()) ); LayeredRegistryAccess baseLayeredRegistry = RegistryLayer.createRegistryAccess(); List> pendingTags = TagLoader.loadTagsForExistingRegistries( closeableResourceManager, baseLayeredRegistry.getLayer(RegistryLayer.STATIC) ); List> worldGenRegistryLookupList = TagLoader.buildUpdatedLookups( - baseLayeredRegistry.getAccessForLoading(RegistryLayer.WORLDGEN), + baseLayeredRegistry.getAccessForLoading(RegistryLayer.WORLD), pendingTags ); LayeredRegistryAccess layeredRegistryAccess = baseLayeredRegistry.replaceFrom( - RegistryLayer.WORLDGEN, + RegistryLayer.WORLD, RegistryDataLoader.load( closeableResourceManager, worldGenRegistryLookupList, - RegistryDataLoader.WORLDGEN_REGISTRIES, + RegistryDataLoader.WORLD_REGISTRIES, ForkJoinPool.commonPool() ).join() ); diff --git a/src/launch/java/baritone/launch/mixins/MixinClientPlayNetHandler.java b/src/launch/java/baritone/launch/mixins/MixinClientPlayNetHandler.java index 1985b31517..729e397ff0 100644 --- a/src/launch/java/baritone/launch/mixins/MixinClientPlayNetHandler.java +++ b/src/launch/java/baritone/launch/mixins/MixinClientPlayNetHandler.java @@ -108,8 +108,8 @@ private void postHandleChunkData(ClientboundLevelChunkWithLightPacket packetIn, new ChunkEvent( EventState.POST, !packetIn.isSkippable() ? ChunkEvent.Type.POPULATE_FULL : ChunkEvent.Type.POPULATE_PARTIAL, - packetIn.getX(), - packetIn.getZ() + packetIn.x(), + packetIn.z() ) ); } diff --git a/src/launch/java/baritone/launch/mixins/MixinRenderPipelines.java b/src/launch/java/baritone/launch/mixins/MixinRenderPipelines.java index f5a8a6e97e..4f07497a24 100644 --- a/src/launch/java/baritone/launch/mixins/MixinRenderPipelines.java +++ b/src/launch/java/baritone/launch/mixins/MixinRenderPipelines.java @@ -18,7 +18,7 @@ package baritone.launch.mixins; import baritone.utils.accessor.IRenderPipelines; -import com.mojang.blaze3d.pipeline.RenderPipeline; +import com.mojang.renderpearl.api.pipeline.RenderPipeline; import net.minecraft.client.renderer.RenderPipelines; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; diff --git a/src/launch/java/baritone/launch/mixins/MixinWorldRenderer.java b/src/launch/java/baritone/launch/mixins/MixinWorldRenderer.java index 51bb7e3e83..ee93e521dc 100644 --- a/src/launch/java/baritone/launch/mixins/MixinWorldRenderer.java +++ b/src/launch/java/baritone/launch/mixins/MixinWorldRenderer.java @@ -20,13 +20,11 @@ import baritone.api.BaritoneAPI; import baritone.api.IBaritone; import baritone.api.event.events.RenderEvent; -import com.mojang.blaze3d.buffers.GpuBufferSlice; import com.mojang.blaze3d.resource.GraphicsResourceAllocator; import com.mojang.blaze3d.vertex.PoseStack; -import net.minecraft.client.DeltaTracker; +import com.mojang.renderpearl.api.buffers.GpuBufferSlice; import net.minecraft.client.renderer.LevelRenderer; import net.minecraft.client.renderer.state.level.CameraRenderState; -import org.joml.Matrix4fc; import org.joml.Vector4f; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -44,11 +42,11 @@ public class MixinWorldRenderer { method = "render", at = @At("RETURN") ) - private void onStartHand(final GraphicsResourceAllocator allocator, final DeltaTracker deltaTracker, final boolean outline, final CameraRenderState camera, final Matrix4fc modelViewMatrix, final GpuBufferSlice fog, final Vector4f fogColor, final boolean sky, final CallbackInfo ci) { + private void onStartHand(final GraphicsResourceAllocator resourceAllocator, final boolean renderOutline, final CameraRenderState cameraState, final GpuBufferSlice terrainFog, final Vector4f fogColor, final boolean shouldRenderSky, final boolean consistentDepthRequired, final CallbackInfo ci) { for (IBaritone ibaritone : BaritoneAPI.getProvider().getAllBaritones()) { PoseStack poseStack = new PoseStack(); - poseStack.mulPose(modelViewMatrix); - ibaritone.getGameEventHandler().onRenderPass(new RenderEvent(deltaTracker.getGameTimeDeltaPartialTick(false), poseStack, camera.projectionMatrix)); + poseStack.mulPose(cameraState.viewRotationMatrix); + ibaritone.getGameEventHandler().onRenderPass(new RenderEvent(cameraState.cameraEntityPartialTicks, poseStack, cameraState.projectionMatrix)); } } } diff --git a/src/main/java/baritone/command/ExampleBaritoneControl.java b/src/main/java/baritone/command/ExampleBaritoneControl.java index 82f1cc57c3..12d9149f2d 100644 --- a/src/main/java/baritone/command/ExampleBaritoneControl.java +++ b/src/main/java/baritone/command/ExampleBaritoneControl.java @@ -34,13 +34,14 @@ import baritone.command.argument.ArgConsumer; import baritone.command.argument.CommandArguments; import baritone.command.manager.CommandManager; +import com.mojang.blaze3d.Blaze3D; import net.minecraft.ChatFormatting; import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.HoverEvent; import net.minecraft.network.chat.MutableComponent; -import net.minecraft.util.Util; +import java.net.URI; import java.util.List; import java.util.Locale; import java.util.stream.Stream; @@ -96,7 +97,7 @@ public boolean runCommand(String msg) { return false; } else if (msg.trim().equalsIgnoreCase("orderpizza")) { try { - Util.getPlatform().openUri("https://www.dominos.com/en/pages/order/"); + Blaze3D.openUri(URI.create("https://www.dominos.com/en/pages/order/")); } catch (Exception ignored) {} return false; } diff --git a/src/main/java/baritone/process/BuilderProcess.java b/src/main/java/baritone/process/BuilderProcess.java index c1f275001e..ceca32538c 100644 --- a/src/main/java/baritone/process/BuilderProcess.java +++ b/src/main/java/baritone/process/BuilderProcess.java @@ -519,7 +519,7 @@ public int lengthZ() { } // build repeat time layer = 0; - origin = new BlockPos(origin).offset(repeat); + origin = new BlockPos(origin.getX(), origin.getY(), origin.getZ()).offset(repeat); if (!Baritone.settings().buildRepeatSneaky.value) { schematic.reset(); } diff --git a/src/main/java/baritone/process/FarmProcess.java b/src/main/java/baritone/process/FarmProcess.java index fbb7f66a2c..da2ff2fdd7 100644 --- a/src/main/java/baritone/process/FarmProcess.java +++ b/src/main/java/baritone/process/FarmProcess.java @@ -21,8 +21,8 @@ import baritone.api.BaritoneAPI; import baritone.api.pathing.goals.Goal; import baritone.api.pathing.goals.GoalBlock; -import baritone.api.pathing.goals.GoalGetToBlock; import baritone.api.pathing.goals.GoalComposite; +import baritone.api.pathing.goals.GoalGetToBlock; import baritone.api.process.IFarmProcess; import baritone.api.process.PathingCommand; import baritone.api.process.PathingCommandType; @@ -42,16 +42,7 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.AirBlock; -import net.minecraft.world.level.block.BambooStalkBlock; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.block.BonemealableBlock; -import net.minecraft.world.level.block.CactusBlock; -import net.minecraft.world.level.block.CocoaBlock; -import net.minecraft.world.level.block.CropBlock; -import net.minecraft.world.level.block.NetherWartBlock; -import net.minecraft.world.level.block.SugarCaneBlock; +import net.minecraft.world.level.block.*; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.HitResult; @@ -267,7 +258,7 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { } if (state.getBlock() instanceof BonemealableBlock) { BonemealableBlock ig = (BonemealableBlock) state.getBlock(); - if (ig.isValidBonemealTarget(ctx.world(), pos, state) && ig.isBonemealSuccess(ctx.world(), ctx.world().getRandom(), pos, state)) { + if (ig.isValidBonemealTarget(ctx.world(), pos, state, BonemealSource.INTERACTION) && ig.isBonemealSuccess(ctx.world(), ctx.world().getRandom(), pos, state, BonemealSource.INTERACTION)) { bonemealable.add(pos); } } diff --git a/src/main/java/baritone/utils/BlockBreakHelper.java b/src/main/java/baritone/utils/BlockBreakHelper.java index 0c5cf6f000..2b1e10c8df 100644 --- a/src/main/java/baritone/utils/BlockBreakHelper.java +++ b/src/main/java/baritone/utils/BlockBreakHelper.java @@ -21,6 +21,7 @@ import baritone.api.utils.IPlayerContext; import baritone.utils.accessor.IPlayerControllerMP; import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.component.SwingAnimation; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.HitResult; @@ -62,10 +63,10 @@ public void tick(boolean isLeftClick) { if (ctx.playerController().hasBrokenBlock()) { ctx.playerController().syncHeldItem(); ctx.playerController().clickBlock(((BlockHitResult) trace).getBlockPos(), ((BlockHitResult) trace).getDirection()); - ctx.player().swing(InteractionHand.MAIN_HAND); + ctx.player().swing(InteractionHand.MAIN_HAND, SwingAnimation.DEFAULT, false); } else { if (ctx.playerController().onPlayerDamageBlock(((BlockHitResult) trace).getBlockPos(), ((BlockHitResult) trace).getDirection())) { - ctx.player().swing(InteractionHand.MAIN_HAND); + ctx.player().swing(InteractionHand.MAIN_HAND, SwingAnimation.DEFAULT, false); } if (ctx.playerController().hasBrokenBlock()) { // block broken this tick // break delay timer only applies for multi-tick block breaks like vanilla diff --git a/src/main/java/baritone/utils/BlockPlaceHelper.java b/src/main/java/baritone/utils/BlockPlaceHelper.java index ca8d505239..950b59f56e 100644 --- a/src/main/java/baritone/utils/BlockPlaceHelper.java +++ b/src/main/java/baritone/utils/BlockPlaceHelper.java @@ -21,6 +21,7 @@ import baritone.api.utils.IPlayerContext; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; +import net.minecraft.world.item.component.SwingAnimation; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.HitResult; @@ -47,7 +48,7 @@ public void tick(boolean rightClickRequested) { rightClickTimer = Baritone.settings().rightClickSpeed.value - BASE_PLACE_DELAY; for (InteractionHand hand : InteractionHand.values()) { if (ctx.playerController().processRightClickBlock(ctx.player(), ctx.world(), hand, (BlockHitResult) mouseOver) == InteractionResult.SUCCESS) { - ctx.player().swing(hand); + ctx.player().swing(hand, SwingAnimation.DEFAULT, false); return; } if (!ctx.player().getItemInHand(hand).isEmpty() && ctx.playerController().processRightClick(ctx.player(), ctx.world(), hand) == InteractionResult.SUCCESS) { diff --git a/src/main/java/baritone/utils/IRenderer.java b/src/main/java/baritone/utils/IRenderer.java index 4cf39bdac0..9554c394fd 100644 --- a/src/main/java/baritone/utils/IRenderer.java +++ b/src/main/java/baritone/utils/IRenderer.java @@ -22,17 +22,14 @@ import baritone.utils.accessor.IEntityRenderManager; import baritone.utils.accessor.IRenderPipelines; import baritone.utils.accessor.IRenderType; -import com.mojang.blaze3d.pipeline.BlendFunction; -import com.mojang.blaze3d.pipeline.ColorTargetState; -import com.mojang.blaze3d.pipeline.DepthStencilState; -import com.mojang.blaze3d.pipeline.RenderPipeline; -import com.mojang.blaze3d.platform.CompareOp; -import com.mojang.blaze3d.platform.BlendFactor; -import com.mojang.blaze3d.PrimitiveTopology; -import com.mojang.blaze3d.vertex.*; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.renderpearl.api.pipeline.*; import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.client.renderer.BindGroupLayouts; +import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.client.renderer.StagedVertexBuffer; import net.minecraft.client.renderer.blockentity.BeaconRenderer; import net.minecraft.client.renderer.rendertype.RenderSetup; @@ -45,6 +42,8 @@ import net.minecraft.world.phys.Vec3; import java.awt.*; +import java.util.Optional; +import java.util.OptionalDouble; import java.util.function.BiFunction; public interface IRenderer { @@ -64,25 +63,26 @@ public interface IRenderer { .buildSnippet(); RenderPipeline.Snippet BARITONE_BEACON_BEAM_SNIPPET = RenderPipeline.builder(((IRenderPipelines) new RenderPipelines()).getMatricesFogSnippet()) - .withVertexShader("core/rendertype_beacon_beam") - .withFragmentShader("core/rendertype_beacon_beam") - .withBindGroupLayout(BindGroupLayouts.SAMPLER0) - .withVertexBinding(0, DefaultVertexFormat.BLOCK) - .withPrimitiveTopology(PrimitiveTopology.QUADS) - .buildSnippet(); + .withVertexShader("core/rendertype_beacon_beam") + .withFragmentShader("core/rendertype_beacon_beam") + .withBindGroupLayout(BindGroupLayouts.SAMPLER0) + .withVertexBinding(0, DefaultVertexFormat.BLOCK) + .withPrimitiveTopology(PrimitiveTopology.QUADS) + .buildSnippet(); RenderPipeline BEACON_BEAM_OPAQUE = ((IRenderPipelines) new RenderPipelines()).baritone$registerPipeline(RenderPipeline.builder(BARITONE_BEACON_BEAM_SNIPPET) - .withLocation("pipeline/baritone_beacon_beam_opaque") - .withDepthStencilState(new DepthStencilState(CompareOp.ALWAYS_PASS, false)) - .withCull(true) - .build()); + .withLocation("pipeline/baritone_beacon_beam_opaque") + .withColorTargetState(new ColorTargetState(BlendFunction.TRANSLUCENT)) + .withDepthStencilState(new DepthStencilState(CompareOp.ALWAYS_PASS, false)) + .withCull(true) + .build()); RenderPipeline BEACON_BEAM_TRANSLUCENT = ((IRenderPipelines) new RenderPipelines()).baritone$registerPipeline(RenderPipeline.builder(BARITONE_BEACON_BEAM_SNIPPET) - .withLocation("pipeline/baritone_beacon_beam_translucent") - .withColorTargetState(new ColorTargetState(BlendFunction.TRANSLUCENT)) - .withDepthStencilState(new DepthStencilState(CompareOp.ALWAYS_PASS, false)) - .withCull(true) - .build()); + .withLocation("pipeline/baritone_beacon_beam_translucent") + .withColorTargetState(new ColorTargetState(BlendFunction.TRANSLUCENT)) + .withDepthStencilState(new DepthStencilState(CompareOp.ALWAYS_PASS, false)) + .withCull(true) + .build()); RenderType linesWithDepthRenderType = ((IRenderType) RenderTypes.lines()).createRenderType( "renderType/baritone_lines_with_depth", @@ -145,7 +145,17 @@ static void endBuffer(BufferBuilder bufferBuilder, RenderType renderType) { stagedVertexBuffer.upload(); StagedVertexBuffer.ExecuteInfo info = stagedVertexBuffer.getExecuteInfo(draw[0]); if (info != null) { - renderType.prepare().drawFromBuffer(info); + var preparedRenderType = renderType.prepare(); + var renderTarget = Minecraft.getInstance().gameRenderer.mainRenderTarget(); + try (var renderPass = RenderSystem.getDevice().createCommandEncoder().createRenderPass( + () -> "Baritone Render Pass", + renderTarget.getColorTextureView(), + Optional.empty(), + renderTarget.getDepthTextureView(), + OptionalDouble.empty()) + ) { + preparedRenderType.drawFromBuffer(info, renderPass); + } } stagedVertexBuffer.endFrame(); } diff --git a/src/main/java/baritone/utils/PathRenderer.java b/src/main/java/baritone/utils/PathRenderer.java index 6339303e05..3cc75fb7fc 100644 --- a/src/main/java/baritone/utils/PathRenderer.java +++ b/src/main/java/baritone/utils/PathRenderer.java @@ -359,7 +359,7 @@ private static void renderGoalXZBeaconLayer(PoseStack stack, double height, floa stack.translate(0.5D, 0.0D, 0.5D); if (!translucent) { stack.pushPose(); - stack.mulPose(Axis.YP.rotationDegrees(time * 2.25F - 45.0F)); + stack.rotateDegrees(Axis.YP, time * 2.25F - 45.0F); } float v0 = -1.0F + scroll; diff --git a/src/main/java/baritone/utils/accessor/IRenderPipelines.java b/src/main/java/baritone/utils/accessor/IRenderPipelines.java index 35ebca33b9..6ef174f140 100644 --- a/src/main/java/baritone/utils/accessor/IRenderPipelines.java +++ b/src/main/java/baritone/utils/accessor/IRenderPipelines.java @@ -17,7 +17,7 @@ package baritone.utils.accessor; -import com.mojang.blaze3d.pipeline.RenderPipeline; +import com.mojang.renderpearl.api.pipeline.RenderPipeline; public interface IRenderPipelines { RenderPipeline.Snippet getLinesSnippet(); diff --git a/src/main/java/baritone/utils/pathing/Avoidance.java b/src/main/java/baritone/utils/pathing/Avoidance.java index dcaf6e8a57..5e5f171cb5 100644 --- a/src/main/java/baritone/utils/pathing/Avoidance.java +++ b/src/main/java/baritone/utils/pathing/Avoidance.java @@ -23,7 +23,7 @@ import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap; import net.minecraft.core.BlockPos; import net.minecraft.world.entity.Mob; -import net.minecraft.world.entity.monster.EnderMan; +import net.minecraft.world.entity.monster.Enderman; import net.minecraft.world.entity.monster.spider.Spider; import net.minecraft.world.entity.monster.zombie.ZombifiedPiglin; @@ -76,7 +76,7 @@ public static List create(IPlayerContext ctx) { .filter(entity -> entity instanceof Mob) .filter(entity -> (!(entity instanceof Spider)) || ctx.player().getLightLevelDependentMagicValue() < 0.5) .filter(entity -> !(entity instanceof ZombifiedPiglin) || ((ZombifiedPiglin) entity).getLastHurtByMob() != null) - .filter(entity -> !(entity instanceof EnderMan) || ((EnderMan) entity).isCreepy()) + .filter(entity -> !(entity instanceof Enderman) || ((Enderman) entity).isCreepy()) .forEach(entity -> res.add(new Avoidance(entity.blockPosition(), mobCoeff, Baritone.settings().mobAvoidanceRadius.value))); } return res; From e9e84d7d398ef8a2d64a8aaed32940a680301d6d Mon Sep 17 00:00:00 2001 From: rfresh2 <89827146+rfresh2@users.noreply.github.com> Date: Sat, 19 Sep 2026 15:37:32 -0700 Subject: [PATCH 41/54] forge --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index c2d1767cce..d0fae79e42 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ org.gradle.jvmargs=-Xmx4G -available_loaders=fabric,neoforge,tweaker +available_loaders=fabric,forge,neoforge,tweaker mod_version=1.20.0 maven_group=baritone @@ -10,7 +10,7 @@ java_version=25 minecraft_version=26.3 -forge_version=65.0.3 +forge_version=66.0.2 neoforge_version=6-beta From 3c2030fd9f78f8be71a117cc1e2e476b2c4d876b Mon Sep 17 00:00:00 2001 From: rfresh2 <89827146+rfresh2@users.noreply.github.com> Date: Sat, 19 Sep 2026 15:54:46 -0700 Subject: [PATCH 42/54] move player update post event inject target --- src/launch/java/baritone/launch/mixins/MixinMinecraft.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/launch/java/baritone/launch/mixins/MixinMinecraft.java b/src/launch/java/baritone/launch/mixins/MixinMinecraft.java index 37455dbd28..5762e05de3 100644 --- a/src/launch/java/baritone/launch/mixins/MixinMinecraft.java +++ b/src/launch/java/baritone/launch/mixins/MixinMinecraft.java @@ -114,7 +114,7 @@ private void postRunTick(CallbackInfo ci) { method = "tick", at = @At( value = "INVOKE", - target = "net/minecraft/client/multiplayer/ClientLevel.tickEntities()V", + target = "Lnet/minecraft/client/player/LocalPlayer;sendChanges()V", shift = At.Shift.AFTER ) ) From aa9bd4542aae80c192e204d97141d640a9d10a49 Mon Sep 17 00:00:00 2001 From: rfresh2 <89827146+rfresh2@users.noreply.github.com> Date: Sat, 19 Sep 2026 15:58:08 -0700 Subject: [PATCH 43/54] fix mouse button ids --- src/main/java/baritone/utils/GuiClick.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/baritone/utils/GuiClick.java b/src/main/java/baritone/utils/GuiClick.java index fb8410134c..369104a136 100644 --- a/src/main/java/baritone/utils/GuiClick.java +++ b/src/main/java/baritone/utils/GuiClick.java @@ -22,6 +22,7 @@ import baritone.api.pathing.goals.GoalBlock; import baritone.api.utils.BetterBlockPos; import baritone.api.utils.Helper; +import com.mojang.blaze3d.platform.InputConstants; import com.mojang.blaze3d.vertex.BufferBuilder; import com.mojang.blaze3d.vertex.PoseStack; import net.minecraft.ChatFormatting; @@ -92,7 +93,7 @@ public void extractBackground(GuiGraphicsExtractor grapics, int mouseX, int mous @Override public boolean mouseReleased(MouseButtonEvent event) { if (currentMouseOver != null) { //Catch this, or else a click into void will result in a crash - if (event.button() == 0) { + if (event.button() == InputConstants.MOUSE_BUTTON_LEFT) { if (clickStart != null && !clickStart.equals(currentMouseOver)) { BaritoneAPI.getProvider().getPrimaryBaritone().getSelectionManager().removeAllSelections(); BaritoneAPI.getProvider().getPrimaryBaritone().getSelectionManager().addSelection(BetterBlockPos.from(clickStart), BetterBlockPos.from(currentMouseOver)); @@ -107,7 +108,7 @@ public boolean mouseReleased(MouseButtonEvent event) { } else { BaritoneAPI.getProvider().getPrimaryBaritone().getCustomGoalProcess().setGoalAndPath(new GoalBlock(currentMouseOver)); } - } else if (event.button() == 1) { + } else if (event.button() == InputConstants.MOUSE_BUTTON_RIGHT) { BaritoneAPI.getProvider().getPrimaryBaritone().getCustomGoalProcess().setGoalAndPath(new GoalBlock(currentMouseOver.above())); } } From 4b96a99ff55afd6a3365de8181456d74b68100ac Mon Sep 17 00:00:00 2001 From: rfresh2 <89827146+rfresh2@users.noreply.github.com> Date: Sat, 19 Sep 2026 16:02:37 -0700 Subject: [PATCH 44/54] remove indent diff --- src/main/java/baritone/utils/IRenderer.java | 32 ++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/main/java/baritone/utils/IRenderer.java b/src/main/java/baritone/utils/IRenderer.java index 9554c394fd..59d8005a17 100644 --- a/src/main/java/baritone/utils/IRenderer.java +++ b/src/main/java/baritone/utils/IRenderer.java @@ -63,26 +63,26 @@ public interface IRenderer { .buildSnippet(); RenderPipeline.Snippet BARITONE_BEACON_BEAM_SNIPPET = RenderPipeline.builder(((IRenderPipelines) new RenderPipelines()).getMatricesFogSnippet()) - .withVertexShader("core/rendertype_beacon_beam") - .withFragmentShader("core/rendertype_beacon_beam") - .withBindGroupLayout(BindGroupLayouts.SAMPLER0) - .withVertexBinding(0, DefaultVertexFormat.BLOCK) - .withPrimitiveTopology(PrimitiveTopology.QUADS) - .buildSnippet(); + .withVertexShader("core/rendertype_beacon_beam") + .withFragmentShader("core/rendertype_beacon_beam") + .withBindGroupLayout(BindGroupLayouts.SAMPLER0) + .withVertexBinding(0, DefaultVertexFormat.BLOCK) + .withPrimitiveTopology(PrimitiveTopology.QUADS) + .buildSnippet(); RenderPipeline BEACON_BEAM_OPAQUE = ((IRenderPipelines) new RenderPipelines()).baritone$registerPipeline(RenderPipeline.builder(BARITONE_BEACON_BEAM_SNIPPET) - .withLocation("pipeline/baritone_beacon_beam_opaque") - .withColorTargetState(new ColorTargetState(BlendFunction.TRANSLUCENT)) - .withDepthStencilState(new DepthStencilState(CompareOp.ALWAYS_PASS, false)) - .withCull(true) - .build()); + .withLocation("pipeline/baritone_beacon_beam_opaque") + .withColorTargetState(new ColorTargetState(BlendFunction.TRANSLUCENT)) + .withDepthStencilState(new DepthStencilState(CompareOp.ALWAYS_PASS, false)) + .withCull(true) + .build()); RenderPipeline BEACON_BEAM_TRANSLUCENT = ((IRenderPipelines) new RenderPipelines()).baritone$registerPipeline(RenderPipeline.builder(BARITONE_BEACON_BEAM_SNIPPET) - .withLocation("pipeline/baritone_beacon_beam_translucent") - .withColorTargetState(new ColorTargetState(BlendFunction.TRANSLUCENT)) - .withDepthStencilState(new DepthStencilState(CompareOp.ALWAYS_PASS, false)) - .withCull(true) - .build()); + .withLocation("pipeline/baritone_beacon_beam_translucent") + .withColorTargetState(new ColorTargetState(BlendFunction.TRANSLUCENT)) + .withDepthStencilState(new DepthStencilState(CompareOp.ALWAYS_PASS, false)) + .withCull(true) + .build()); RenderType linesWithDepthRenderType = ((IRenderType) RenderTypes.lines()).createRenderType( "renderType/baritone_lines_with_depth", From 9338452a27934b4da8801f8e752b3ab8483ce271 Mon Sep 17 00:00:00 2001 From: Dek Date: Tue, 22 Sep 2026 07:44:13 -0300 Subject: [PATCH 45/54] local work snapshot 2026-09-22 --- docs/mod-algorithm-review-2026-09-21.md | 55 +++ .../AllocatorRegression.java | 58 +++ .../EstimatorRegression.java | 73 ++++ scripts/mod-formula-review/README.md | 21 ++ .../reproduce_highlight_cap.py | 30 ++ scripts/mod-formula-review/test_priority.py | 43 +++ .../mod-formula-review/verify_coordinates.py | 58 +++ scripts/mod-formula-review/verify_maps.py | 106 ++++++ .../mod-formula-review/verify_rendering.py | 26 ++ scripts/proguard.pro | 13 + src/api/java/baritone/api/Settings.java | 43 ++- .../baritone/api/pathing/goals/GoalNear.java | 12 +- .../api/pathing/goals/GoalRunAway.java | 14 +- src/main/java/baritone/cache/CachedChunk.java | 7 + .../java/baritone/cache/CachedRegion.java | 58 ++- src/main/java/baritone/cache/CachedWorld.java | 356 ++++++++++++++---- src/main/java/baritone/cache/ChunkPacker.java | 4 +- .../baritone/cache/RegionRetentionPolicy.java | 74 ++++ src/main/java/baritone/cache/WorldData.java | 6 +- .../java/baritone/cache/WorldProvider.java | 42 ++- .../command/defaults/FindCommand.java | 2 +- .../java/baritone/event/GameEventHandler.java | 8 + .../java/baritone/process/ElytraProcess.java | 32 +- .../cache/RegionRetentionPolicyTest.java | 92 +++++ .../cache/WorldProviderFolderNameTest.java | 66 ++++ .../pathing/goals/GoalDistanceTest.java | 68 ++++ 26 files changed, 1249 insertions(+), 118 deletions(-) create mode 100644 docs/mod-algorithm-review-2026-09-21.md create mode 100644 scripts/mod-formula-review/AllocatorRegression.java create mode 100644 scripts/mod-formula-review/EstimatorRegression.java create mode 100644 scripts/mod-formula-review/README.md create mode 100644 scripts/mod-formula-review/reproduce_highlight_cap.py create mode 100644 scripts/mod-formula-review/test_priority.py create mode 100644 scripts/mod-formula-review/verify_coordinates.py create mode 100644 scripts/mod-formula-review/verify_maps.py create mode 100644 scripts/mod-formula-review/verify_rendering.py create mode 100644 src/main/java/baritone/cache/RegionRetentionPolicy.java create mode 100644 src/test/java/baritone/cache/RegionRetentionPolicyTest.java create mode 100644 src/test/java/baritone/cache/WorldProviderFolderNameTest.java create mode 100644 src/test/java/baritone/pathing/goals/GoalDistanceTest.java diff --git a/docs/mod-algorithm-review-2026-09-21.md b/docs/mod-algorithm-review-2026-09-21.md new file mode 100644 index 0000000000..bff82cc450 --- /dev/null +++ b/docs/mod-algorithm-review-2026-09-21.md @@ -0,0 +1,55 @@ +# Stash-hunting mod algorithm review — 2026-09-21 + +This is a focused source audit of the local Minecraft 26.2 forks: Baritone, XaeroWorldMap, XaeroPlus, XaeroMinimap, Voxy and Sodium. Priority was long-distance navigation, map coordinates/history, detection logic and rendering during travel. It is not an exhaustive audit of every algorithm. Existing local changes were preserved. + +## Applied fixes + +| Component | Confirmed failure | Applied correction | +| --- | --- | --- | +| Baritone `FindCommand` | `#find` passed the player's Y as the cache search's Z center. At Z=1,000,000 and Y=64, it searched around Z=64. | Pass `origin.z`; cached-block searches now center on the player's horizontal position. | +| Baritone `GoalNear` | Squared `int` distances overflow: a point 65,536 blocks away can be accepted by a two-block goal. The radius square can overflow too. | Widen coordinates before subtraction/multiplication and store the squared radius as `long`. | +| Baritone `GoalRunAway` | Distance squares overflow, and casting the requested squared radius to `int` clips large distances and truncates fractional ones. Hashing a goal without a Y constraint also throws. | Preserve the squared radius as `double`, widen coordinate arithmetic and hash nullable Y safely. | +| XaeroPlus chunk/region conversions | Casting a negative fractional coordinate before shifting selects the wrong cell: -0.5 becomes chunk 0 instead of -1. This also affects dimension-projected map centers. | Floor before shifting by 4 or 9. | +| XaeroPlus map cursor and Baritone integration | Overworld→Nether conversion truncates toward zero: -1/8 becomes block 0 instead of -1. | Floor the scaled coordinates consistently for cursor text and both Baritone goal constructors. | +| XaeroWorldMap region sorting, via XaeroPlus | Three distance formulas square integers. A 50,000-region delta or 46,400-chunk delta can overflow into a computed distance of zero, prioritizing distant data incorrectly. | Existing `MixinLeveledRegion` now widens shifts/subtractions and squares doubles in all three methods. | +| XaeroPlus highlight deletion | When no tile selection exists, right-click block coordinates are treated as chunk coordinates. | Convert the fallback coordinates to chunks. Normal selection behavior is unchanged; this is a less common fallback-path fix. | +| Sodium task priority | The close-distance penalty uses `max(distance, 50)` although it should saturate at 50. It penalizes all nearby chunks equally and grows without the intended cap. | Use `min`; close-distance contribution is now 0, 0.3, 0.6, 0.6 at 0, 25, 50 and 512 blocks. Other priority weights are preserved. | +| Sodium duration estimation | A large first batch can leave the shared estimator permanently untrained; constant-size samples can produce zero duration. Old covariance/variance are merged around the wrong mean. | Correct warmup/sample accounting, use the observed mean when variance is zero, and recenter historical moments before merging. Bulk job completion can trigger the large-first-batch case; uploads also benefit from the constant-input and recentering fixes. | +| Voxy contiguous node allocation | With capacity 16, freeing alternating slots and requesting two contiguous slots returns [16,18), outside capacity. Exact fits are rejected, and the mask for 64 slots is zero. | Bound hierarchy searches and candidate ranges, permit exact fits, handle a 64-bit mask, and reject invalid counts. | + +WorldMap is supplied as reference/decompiled source here, so its fix lives in the buildable XaeroPlus integration. No separate Minimap arithmetic defect met the same evidence threshold in the waypoint/distance paths reviewed; no direct Minimap patch was made. + +## Validation + +- Baritone: full offline Java 25 build passed, including 58 JUnit tests; six new goal tests cover overflow, fractional/large radii, multiple origins, height constraints and hashing. Four of those tests failed on the original source before the fixes. +- XaeroPlus: full offline build passed for Fabric and NeoForge; both new Fabric client regression classes compiled. Coordinate harness passed 147,560 assertions. Region-distance harness passed 31,158 comparisons against an independent `hypot`/scaled-coordinate calculation; dimension conversion passed 70 checks. +- Sodium and Voxy: full offline builds passed. Direct production-class regressions cover estimator warmup, constant inputs, deferred batches and shifted means against independently computed weighted least squares. Allocator tests include 80,000 deterministic randomized operations against a boolean-array oracle, exact fits, 64-slot allocations and exhaustion at the 24-bit boundary. Priority method checks cover distance, visibility and task age. +- Runtime Xaero client validation: the default runner could not launch because `xvfb-run` is absent. A temporary Gradle override used the available desktop display. Both new tests completed before the runner entered the existing `XaeroPlusRegionIndexGameTest`; that test timed out starting its temporary dedicated server, whose log says its EULA was not accepted. Thus the new coordinate and applied WorldMap mixin checks ran in Minecraft, while the complete integration suite did not pass. No EULA or repository test-runner setting was changed. Log: `/tmp/xaeroplus-formula-display-test.log`. +- These checks establish arithmetic/build correctness, not improved FPS or detection accuracy on 2b2t. No production profile or stash database was modified. + +Reproducible numerical checks are retained in [scripts/mod-formula-review](../scripts/mod-formula-review/README.md). Baritone tests are in [GoalDistanceTest.java](../src/test/java/baritone/pathing/goals/GoalDistanceTest.java); Xaero tests are in its sibling `fabric/src/gametest` source set. + +## Remaining findings, in priority order + +1. **XaeroPlus highlight cap can hide history after zoom-in.** `ChunkHighlightDatabase` caps queries at 3,000,000 rows, but `ChunkHighlightCacheDimensionHandler` subsequently excludes the entire previous rectangle as if it had loaded completely. A synthetic database of 3,000,001 entries reproduces a missing highlight that remains absent after zooming inward. A fully populated radius-28 window can exceed the limit; the default radius 24 fits below it. Carry complete/truncated status with results and reload incomplete views without excluding them. Keep dirty entries separately when replacing persisted snapshots, and enforce a total resident budget. Removing the SQL cap alone restores the memory problem. The retained SQLite reproduction uses only an in-memory synthetic database. +2. **XaeroPlus portal scans can outlive reset/cancellation.** `PortalSkipDetection` cancels futures without checking interruption inside its loops, shares mutable scan buffers, and queues results without a world/dimension generation check. Old work can overwrite a newer scan after switching worlds/dimensions or disabling the module. Use per-task buffers, cancellation checks and a generation token checked before publishing on the client thread. This needs controlled concurrency tests before changing cache lifecycle behavior. +3. **WorldMap right-click distance mixes dimensions.** The inspected `GuiMap`→`DistanceUtils` path compares displayed-map coordinates with raw player coordinates. An Overworld player at X=8000 looking at Nether X=1000 can see a misleading 7000m. Convert the target into the player's coordinate scale before measuring. This is separate from the fixed cursor and Baritone target rounding. +4. **WorldMap skinny PNG exports can produce a zero dimension.** `PNGExporter` truncates scaled output dimensions and then divides by output height. The arithmetic reproduces a zero height for very wide, one-chunk-high selections. Clamp both dimensions to at least one and validate pixel budgets with widened arithmetic before allocating. +5. **Baritone ETA calculation still enumerates large goal volumes.** The no-argument `GoalNear.heuristic()` and `GoalRunAway.heuristic()` have cubic scans. Large-radius membership is now correct, but that does not make large-radius ETA calculation cheap. Replace enumeration with a validated analytical bound or separately defined ETA estimate. Current widened membership arithmetic is validated for normal Minecraft world coordinates, not every arbitrary integer triple. + +## Detection improvements requiring data + +`PaletteNewChunks` misses unused trailing entries in a linear palette, although it detects mismatched encounter order. Recognizing those entries is a plausible consistency improvement, but both generation and later edits can leave unused palette entries. Validate against labeled saved chunk packets before changing the classification rule or its three-section threshold. + +`OldChunks` and `LiquidNewChunks` infer history from terrain/block content and update patterns; neither establishes generation age with certainty. In particular, the liquid detector accepts an adjacent source without matching its fluid type to the updated flow. A same-fluid check is worth testing against captured updates. No arbitrary scoring weights or detection thresholds were changed. + +Portal scanning is a separate performance opportunity: an eligibility grid plus summed-area or sliding-window counts can identify the same all-eligible squares without repeatedly checking every member. Test equivalence on randomized masks, dimension boundaries and radius 0/1 before replacing the current algorithm. + +## Built artifacts + +- Baritone: `fabric/build/libs/baritone-api-fabric-26.2-SNAPSHOT.jar` +- XaeroPlus: `../xaeroplus/fabric/build/libs/XaeroPlus-2.35.3+fabric-26.2-WM1.45.0-MM26.4.2.jar` +- Sodium: `../sodium/build/mods/sodium-fabric-0.9.2-SNAPSHOT+mc26.2-local.jar` +- Voxy: `../voxy/build/libs/voxy-0.2.19-beta-337b919.jar` + +These are build outputs. Installation into the active game profile was outside this review. diff --git a/scripts/mod-formula-review/AllocatorRegression.java b/scripts/mod-formula-review/AllocatorRegression.java new file mode 100644 index 0000000000..826fb3387d --- /dev/null +++ b/scripts/mod-formula-review/AllocatorRegression.java @@ -0,0 +1,58 @@ +import java.util.Random; +import me.cortex.voxy.common.util.HierarchicalBitSet; + +public class AllocatorRegression { + private static void check(boolean ok, String message) { + if (!ok) throw new AssertionError(message); + } + private static int firstRun(boolean[] used, int count) { + for (int i = 0; i <= used.length - count; i++) { + int j = 0; + while (j < count && !used[i + j]) j++; + if (j == count) return i; + } + return -1; + } + public static void main(String[] args) { + var fragmented = new HierarchicalBitSet(16); + for (int i = 0; i < 16; i++) fragmented.allocateNext(); + for (int i = 0; i < 16; i += 2) fragmented.free(i); + check(fragmented.allocateNextConsecutiveCounted(2) < 0, "fragmentation must not allocate beyond limit"); + check(fragmented.getCount() == 8, "failed allocation must preserve count"); + var exact = new HierarchicalBitSet(16); + check(exact.allocateNextConsecutiveCounted(16) == 0, "exact capacity must be usable"); + var fullWord = new HierarchicalBitSet(128); + fullWord.allocateNext(); fullWord.allocateNext(); fullWord.free(0); + check(fullWord.allocateNextConsecutiveCounted(64) == 2, "64-bit run must not overlap allocated ID 1"); + for (int limit : new int[] {1, 16, 63, 64, 65, 127, 128, 1024}) { + var used = new boolean[limit]; + var allocator = new HierarchicalBitSet(limit); + var random = new Random(limit); + for (int t = 0; t < 10000; t++) { + if (random.nextBoolean()) { + int id = random.nextInt(limit); + check(allocator.free(id) == used[id], "free state mismatch"); + used[id] = false; + } else { + int count = random.nextInt(64) + 1; + int expected = firstRun(used, count); + int actual = allocator.allocateNextConsecutiveCounted(count); + check(expected < 0 ? actual < 0 : expected == actual, "first-fit mismatch: limit=" + limit + " count=" + count + " expected=" + expected + " actual=" + actual); + if (actual >= 0) for (int j = 0; j < count; j++) used[actual + j] = true; + } + int usedCount = 0; + for (int i = 0; i < limit; i++) { + check(allocator.isSet(i) == used[i], "occupancy mismatch"); + if (used[i]) usedCount++; + } + check(allocator.getCount() == usedCount, "count mismatch"); + } + } + // Last free slot in the full 24-bit hierarchy cannot satisfy a two-slot run. + var edge = new HierarchicalBitSet(); + for (int i = 0; i < edge.getLimit(); i++) edge.allocateNext(); + edge.free(edge.getLimit() - 1); edge.free(0); edge.free(2); + check(edge.allocateNextConsecutiveCounted(2) < 0, "full hierarchy exhaustion must not access a nonexistent level"); + System.out.println("PASS Voxy allocator: fragmented limits, exact fit, 64-slot runs, 80,000 randomized operations, full 24-bit boundary"); + } +} diff --git a/scripts/mod-formula-review/EstimatorRegression.java b/scripts/mod-formula-review/EstimatorRegression.java new file mode 100644 index 0000000000..cda38609ef --- /dev/null +++ b/scripts/mod-formula-review/EstimatorRegression.java @@ -0,0 +1,73 @@ +import java.util.ArrayList; +import java.util.List; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.UploadDuration; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.UploadDurationEstimator; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.ExpDecayLinear2DEstimator; +import java.util.HashMap; +import java.util.Map; + +public class EstimatorRegression { + private static void close(long actual, long expected, String label) { + if (Math.abs(actual - expected) > 1) throw new AssertionError(label + ": expected " + expected + ", got " + actual); + } + private static class SmallEstimator extends ExpDecayLinear2DEstimator { + SmallEstimator() { super(0.05, 100, 40, 12345L); } + @Override protected Map createMap() { return new HashMap<>(); } + } + private record Sample(long x, long y, double weight) {} + private static long reference(List points, long input) { + double total=0, meanX=0, meanY=0; + for (var p:points) { total+=p.weight; meanX+=p.weight*p.x; meanY+=p.weight*p.y; } + meanX/=total; meanY/=total; + double covariance=0, variance=0; + for (var p:points) { covariance+=p.weight*(p.x-meanX)*(p.y-meanY); variance+=p.weight*(p.x-meanX)*(p.x-meanX); } + double slope=variance==0?0:Math.max(0,covariance/variance); + return (long)(meanY+slope*(input-meanX)); + } + public static void main(String[] args) { + var varying=new UploadDurationEstimator(); + for(int batch=0;batch<3;batch++) { + for(int i=0;i<101;i++) varying.addData(new UploadDuration(1_000_000L+i*10_000L,1000L+i*10L)); + varying.updateModels(); + close(varying.estimateUploadDuration(1000L),1_000_000L,"oversized first batch "+batch); + } + var constant=new UploadDurationEstimator(); + for(int i=0;i<100;i++) constant.addData(new UploadDuration(1_000_000L,1000L)); + constant.updateModels(); + close(constant.estimateUploadDuration(1000L),1_000_000L,"constant-X initial batch"); + for(int i=0;i<100;i++) constant.addData(new UploadDuration(2_000_000L,2000L)); + constant.updateModels(); + close(constant.estimateUploadDuration(1500L),1_500_000L,"different constant-X batch establishes slope"); + + var warming=new SmallEstimator(); + var points=new ArrayList(); + for(int batch=0;batch<3;batch++) { + for(int i=0;i<40;i++) { + long x=1000L+batch*100L+i; + long y=500_000L+10L*x+batch*10000L; + warming.addData(new UploadDuration(y,x)); points.add(new Sample(x,y,1)); + } + warming.updateModels(); + close(warming.predict(null,1100L),reference(points,1100L),"warmup and target crossing batch "+batch); + } + var shifted=new UploadDurationEstimator(); + var shiftedPoints=new ArrayList(); + for(int batch=0;batch<2;batch++) { + for(int i=0;i<100;i++) { + long x=1000L+batch*1000L+(i%2)*10L; + long y=500_000L+100L*x+batch*250_000L; + shifted.addData(new UploadDuration(y,x)); + shiftedPoints.add(new Sample(x,y,batch==0?19:1)); + } + shifted.updateModels(); + } + for(long input:new long[]{1000,1500,2000}) close(shifted.estimateUploadDuration(input),reference(shiftedPoints,input),"shifted means at "+input); + + var deferred=new UploadDurationEstimator(); + for(int i=0;i<99;i++) deferred.addData(new UploadDuration(1_000_000L,1000L)); + deferred.updateModels(); close(deferred.estimateUploadDuration(1000),100_000L,"retain fallback while below batch minimum"); + deferred.addData(new UploadDuration(1_000_000L,1000L)); deferred.updateModels(); + close(deferred.estimateUploadDuration(1000),1_000_000L,"deferred samples retained"); + System.out.println("PASS Sodium estimator: >100 first batch, constant-X, warmup/target crossing, shifted-mean weighted least-squares reference, deferred samples"); + } +} diff --git a/scripts/mod-formula-review/README.md b/scripts/mod-formula-review/README.md new file mode 100644 index 0000000000..32bec82eb3 --- /dev/null +++ b/scripts/mod-formula-review/README.md @@ -0,0 +1,21 @@ +# Cross-mod arithmetic regressions + +Run from the Baritone repository, with XaeroPlus, Sodium, Voxy and the Xaero 26.2 reference sources in sibling directories. These scripts require the local Java 25 installation. The rendering harness uses the FastUtil 8.5.18 and JSpecify 1.0.0 jars already in the Gradle cache. + +```sh +python3 scripts/mod-formula-review/verify_coordinates.py ../xaeroplus/common/src/main/java/xaeroplus/util/ChunkUtils.java +python3 scripts/mod-formula-review/verify_maps.py +python3 scripts/mod-formula-review/verify_rendering.py +``` + +The coordinate, map and priority harnesses extract methods from the working source. The estimator and allocator harnesses compile the production classes directly. Independent grid-containment, weighted least-squares and boolean-array checks supply the expected results. Temporary compilation files are removed automatically. These checks do not initialize Minecraft or prove runtime mixin compatibility. + +Baritone's six goal regressions run with `./gradlew test` under Java 25. Persistent Xaero client tests are in its `fabric/src/gametest` source set: `XaeroPlusCoordinateGameTest` and `XaeroPlusRegionDistanceGameTest`. + +To reproduce the unresolved highlight-cap issue, run: + +```sh +python3 scripts/mod-formula-review/reproduce_highlight_cap.py +``` + +That script creates an in-memory SQLite database with 3,000,001 synthetic rows. It never opens or changes real map databases. Its expected output is `CONFIRMED`, demonstrating omitted highlights after zooming into a truncated window. diff --git a/scripts/mod-formula-review/reproduce_highlight_cap.py b/scripts/mod-formula-review/reproduce_highlight_cap.py new file mode 100644 index 0000000000..6d2b606d00 --- /dev/null +++ b/scripts/mod-formula-review/reproduce_highlight_cap.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +"""SQLite regression model of the actual row cap and incremental exclusion.""" +import re +import sqlite3 +from pathlib import Path + +root = Path(__file__).resolve().parents[3] +source = (root / 'xaeroplus/common/src/main/java/xaeroplus/feature/highlights/ChunkHighlightDatabase.java').read_text() +cap = int(re.search(r'MAX_ROWS_PER_LOAD = ([\d_]+)', source).group(1).replace('_', '')) +db = sqlite3.connect(':memory:') +db.execute('CREATE TABLE highlights (x INTEGER, z INTEGER, foundTime INTEGER, PRIMARY KEY (x, z)) WITHOUT ROWID') +db.executemany('INSERT INTO highlights VALUES (?, ?, 1)', ((i // 2000 - 1000, i % 2000 - 1000) for i in range(cap + 1))) +# Exactly the existing region-bound arithmetic, for a 32-region-radius window. +previous = (-1024, 1024, -1024, 1024) +query = 'SELECT x,z FROM highlights WHERE x BETWEEN ? AND ? AND z BETWEEN ? AND ? LIMIT ?' +loaded = db.execute('SELECT count(*) FROM (' + query + ')', (*previous, cap)).fetchone()[0] +omitted = db.execute('SELECT x,z FROM highlights ORDER BY x,z LIMIT 1 OFFSET ?', (cap,)).fetchone() +assert loaded == cap +assert omitted is not None +assert db.execute(query, (*previous, cap)).fetchone() != omitted +# Shrink the view around the row the cap omitted; new rectangle is entirely inside old. +cx, cz = omitted[0] >> 5, omitted[1] >> 5 +current = ((cx - 1) << 5, (cx + 1) << 5, (cz - 1) << 5, (cz + 1) << 5) +incremental_query = 'SELECT x,z FROM highlights WHERE x BETWEEN ? AND ? AND z BETWEEN ? AND ? AND NOT (x BETWEEN ? AND ? AND z BETWEEN ? AND ?) LIMIT ?' +incremental = db.execute(incremental_query, (*current, *previous, cap)).fetchall() +full = db.execute(query, (*current, cap)).fetchall() +assert omitted in full +assert omitted not in incremental +assert not incremental +print(f'CONFIRMED: loaded {loaded:,} of {cap + 1:,} rows; omitted {omitted}; zoom-in fetched {len(incremental)} rows, while a complete query finds {len(full)} including the omitted row.') diff --git a/scripts/mod-formula-review/test_priority.py b/scripts/mod-formula-review/test_priority.py new file mode 100644 index 0000000000..9ab8dabaa4 --- /dev/null +++ b/scripts/mod-formula-review/test_priority.py @@ -0,0 +1,43 @@ +from pathlib import Path +import re, subprocess, sys, tempfile +source=Path(sys.argv[1]).read_text() +start=source.index(' private float getSectionPriority(') +pos=source.index('{', start) +depth=1 +end=pos+1 +while depth: + if source[end]=='{': depth+=1 + if source[end]=='}': depth-=1 + end+=1 +method=source[start:end] +constants='\n'.join(re.findall(r' static final float .*?;',source)) +code='''public class PriorityRegression { +'''+constants+''' + private float invMaxDistance = 3.0f / 512.0f; + private long creationTime = 5_000_000_000L; + private int cameraX, cameraY, cameraZ; + private record RenderSection(int getCenterX, int getCenterY, int getCenterZ, long getPendingUpdateSince) {} + private static class ChunkUpdateTypes { static float getPriorityValue(int type) { return type; } } +'''+method+''' + private static void close(float actual, float expected) { + if (Math.abs(actual - expected) > 0.00001f) throw new AssertionError("expected " + expected + ", got " + actual); + } + public static void main(String[] args) { + var test = new PriorityRegression(); + for (int distance : new int[] {0, 25, 50, 100, 512}) { + float expected = 3.0f * distance / 512.0f + (distance < 50 ? 0.6f * distance / 50.0f : 0.6f); + close(test.getSectionPriority(new RenderSection(distance, 0, 0, test.creationTime), 0, false), expected); + } + var distant = new RenderSection(512, 0, 0, test.creationTime); + var near = new RenderSection(100, 0, 0, test.creationTime); + if (test.getSectionPriority(distant, 0, true) >= test.getSectionPriority(near, 0, false)) throw new AssertionError("bounded proximity must preserve the visible-chunk bias"); + close(test.getSectionPriority(distant, 0, true), test.getSectionPriority(distant, 0, false)-3.0f); + close(test.getSectionPriority(new RenderSection(512, 0, 0, 0), 0, false), test.getSectionPriority(distant, 0, false)-1.0f); + System.out.println("PASS Sodium priority: 0/25/50/100/512-block distances, visible-task ordering, waiting-time weight"); + } +} +''' +with tempfile.TemporaryDirectory(prefix='sodium-priority-') as tmp: + p=Path(tmp)/'PriorityRegression.java';p.write_text(code) + subprocess.run(['/usr/lib/jvm/java-25-openjdk/bin/javac','-d',tmp,str(p)],check=True) + subprocess.run(['/usr/lib/jvm/java-25-openjdk/bin/java','-cp',tmp,'PriorityRegression'],check=True) diff --git a/scripts/mod-formula-review/verify_coordinates.py b/scripts/mod-formula-review/verify_coordinates.py new file mode 100644 index 0000000000..fa02f29847 --- /dev/null +++ b/scripts/mod-formula-review/verify_coordinates.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Run the actual two source method bodies in an isolated Java boundary harness.""" +from pathlib import Path +import re +import subprocess +import sys +import tempfile + +source = Path(sys.argv[1]).read_text() +methods = [] +for name in ('coordToChunkCoord', 'coordToRegionCoord'): + match = re.search(r'public static int ' + name + r'\(final double coord\)\s*\{[^}]*\}', source) + if not match: + raise SystemExit(f'Method not found: {name}') + methods.append(match.group()) + +java = ''' +public class CoordinateRegression { +METHODS + static int checks = 0; + static void check(double x) { + int actualChunk = coordToChunkCoord(x); + int actualRegion = coordToRegionCoord(x); + int expectedChunk = (int) Math.floor(x / 16.0); + int expectedRegion = (int) Math.floor(x / 512.0); + checks += 2; + if (actualChunk != expectedChunk || actualRegion != expectedRegion) { + throw new AssertionError("coordinate " + x + ": chunk=" + actualChunk + + " expected=" + expectedChunk + ", region=" + actualRegion + + " expected=" + expectedRegion); + } + } + public static void main(String[] args) { + double[] offsets = {-1, -0.999, -0.5, -0.001, 0, 0.001, 0.5, 0.999, 1}; + for (int boundary = -2048; boundary <= 2048; boundary++) { + for (double offset : offsets) { + check(boundary * 16.0 + offset); + check(boundary * 512.0 + offset); + } + } + for (double edge : new double[] {-29999984.0, 29999984.0}) { + for (double offset : offsets) check(edge + offset); + } + for (double coordinate : new double[] {-4096.25, -128.5, -4, -0.125, 0, 4, 128.5, 4096.25}) { + check(coordinate / 8.0); + check(coordinate * 8.0); + } + System.out.println("PASS: " + checks + " chunk/region boundary assertions"); + } +} +'''.replace('METHODS', '\n'.join(methods)) + +with tempfile.TemporaryDirectory(prefix='xaeroplus-coordinate-regression-') as temporary: + java_path = Path(temporary) / 'CoordinateRegression.java' + java_path.write_text(java) + subprocess.run(['/usr/lib/jvm/java-25-openjdk/bin/javac', str(java_path)], check=True) + result = subprocess.run(['/usr/lib/jvm/java-25-openjdk/bin/java', '-cp', temporary, 'CoordinateRegression']) + raise SystemExit(result.returncode) diff --git a/scripts/mod-formula-review/verify_maps.py b/scripts/mod-formula-review/verify_maps.py new file mode 100644 index 0000000000..0cbf7782f8 --- /dev/null +++ b/scripts/mod-formula-review/verify_maps.py @@ -0,0 +1,106 @@ +from pathlib import Path +import subprocess +import re +import tempfile +temporary = tempfile.TemporaryDirectory(prefix='xaero-distance-regression-') +out = Path(temporary.name) +root = Path(__file__).resolve().parents[3] +source = (root / 'XaeroDecompiled-26.2/worldmap/xaero/map/region/LeveledRegion.java').read_text() +fixed = (root / 'xaeroplus/common/src/main/java/xaeroplus/mixin/client/MixinLeveledRegion.java').read_text() +def method(text, name): + pattern = r'(?:protected int|private void) ' + re.escape(name) + r'\([^)]*\) \{' + found = re.search(pattern, text) + assert found, name + start = found.start() + pos = found.end() + depth = 1 + while depth: + if text[pos] == '{': depth += 1 + elif text[pos] == '}': depth -= 1 + pos += 1 + return text[start:pos] +java = '''import java.util.Random; +public class RegionDistanceRegression { + int regionX, regionZ, level; + static int comparisonX, comparisonZ, comparisonLevel, comparisonLeafX, comparisonLeafZ; + static int checks, baselineFailures; + static class CallbackInfoReturnable { + T value; + void setReturnValue(T v) { value = v; } + } +''' +for name in ['distanceFromPlayer', 'leafDistanceFromPlayer', 'chunkDistanceFromPlayer']: + java += method(source, name) + '\n' + java += method(fixed, 'xaeroplus$' + name) + '\n' +java += ''' + static void verify(RegionDistanceRegression region, int x, int z, int lv, int cx, int cz, int cl, int lx, int lz) { + region.regionX = x; region.regionZ = z; region.level = lv; + comparisonX = cx; comparisonZ = cz; comparisonLevel = cl; + comparisonLeafX = lx; comparisonLeafZ = lz; + CallbackInfoReturnable result = new CallbackInfoReturnable<>(); + region.xaeroplus$distanceFromPlayer(result); + check("region", result.value, + (int) Math.hypot(Math.floor(Math.scalb((double) x, lv - cl)) - cx, + Math.floor(Math.scalb((double) z, lv - cl)) - cz), region.distanceFromPlayer()); + region.xaeroplus$leafDistanceFromPlayer(result); + check("leaf", result.value, + (int) Math.hypot(Math.scalb((double) x, lv) - lx, Math.scalb((double) z, lv) - lz), region.leafDistanceFromPlayer()); + region.xaeroplus$chunkDistanceFromPlayer(result); + check("chunk", result.value, + (int) Math.hypot(Math.scalb((double) x, lv + 5) - cx, Math.scalb((double) z, lv + 5) - cz), region.chunkDistanceFromPlayer()); + } + static void check(String kind, int actual, int expected, int baseline) { + if (actual != expected) throw new AssertionError(kind + ": " + actual + " != " + expected); + checks++; + if (baseline != expected) baselineFailures++; + } + public static void main(String[] args) { + RegionDistanceRegression r = new RegionDistanceRegression(); + verify(r, 50000, 0, 0, 0, 0, 0, 0, 0); + System.out.println("25.6M-block separation, legacy leaf distance=" + r.leafDistanceFromPlayer() + ", fixed=50000 regions"); + verify(r, 1450, 0, 0, 0, 0, 0, 0, 0); + System.out.println("742400-block separation, legacy chunk distance=" + r.chunkDistanceFromPlayer() + ", fixed=46400 chunks"); + // Axis boundary, diagonal overflow, negative coordinate shifts, opposite borders. + for (int v : new int[]{0, 1, -1, 46340, 46341, -46341, 50000, -50000, 58593, -58594, Integer.MAX_VALUE, Integer.MIN_VALUE}) { + for (int lv = 0; lv <= 3; lv++) { + for (int cl = 0; cl <= 3; cl++) { + verify(r, v, -v, lv, 0, 0, cl, 0, 0); + verify(r, v, v, lv, 58593, -58594, cl, -58594, 58593); + } + } + } + Random random = new Random(0x2B2); + for (int i = 0; i < 10000; i++) { + int lv = random.nextInt(4), cl = random.nextInt(4); + verify(r, random.nextInt(117188) - 58594, random.nextInt(117188) - 58594, lv, + random.nextInt(3750000) - 1875000, random.nextInt(3750000) - 1875000, cl, + random.nextInt(117188) - 58594, random.nextInt(117188) - 58594); + } + System.out.println("Passed " + checks + " source-extracted handler checks; legacy formula failures=" + baselineFailures); + } +} +''' +(out / 'RegionDistanceRegression.java').write_text(java) +subprocess.run(['/usr/lib/jvm/java-25-openjdk/bin/javac', '-d', str(out), str(out / 'RegionDistanceRegression.java')], check=True) +subprocess.run(['/usr/lib/jvm/java-25-openjdk/bin/java', '-cp', str(out), 'RegionDistanceRegression'], check=True) +# Use expressions extracted from the proposed edits, not copies of the replacement formula. +baritone = (root / 'xaeroplus/common/src/main/java/xaeroplus/util/BaritoneExecutor.java').read_text() +gui = (root / 'xaeroplus/common/src/main/java/xaeroplus/mixin/client/MixinGuiMap.java').read_text() +expressions = [re.search(r'int goalX = (.*);', baritone).group(1), re.search(r'int x = (.*mouseBlockPosX.*);', gui).group(1)] +java = '''public class DimensionFloorRegression { +static int baritone(int x, double customDimDiv) { return %s; } +static int cursor(int mouseBlockPosX, double dimDiv) { return %s; } +public static void main(String[] args) { + int[] input = {-30000000, -17, -16, -9, -8, -7, -1, 0, 1, 7, 8, 9, 17, 30000000}; + for (int x : input) { + int expected = Math.floorDiv(x, 8); + if (baritone(x, .125) != expected || cursor(x, 8) != expected) throw new AssertionError("divide " + x); + if (baritone(x, 8) != x * 8 || cursor(x, .125) != x * 8) throw new AssertionError("multiply " + x); + if (baritone(x, 1) != x) throw new AssertionError("same dimension " + x); + } + System.out.println("Passed 70 dimension-conversion boundary checks; -1 OW block maps to -1 Nether block"); +}} +''' % tuple(expressions) +(out / 'DimensionFloorRegression.java').write_text(java) +subprocess.run(['/usr/lib/jvm/java-25-openjdk/bin/javac', '-d', str(out), str(out / 'DimensionFloorRegression.java')], check=True) +subprocess.run(['/usr/lib/jvm/java-25-openjdk/bin/java', '-cp', str(out), 'DimensionFloorRegression'], check=True) diff --git a/scripts/mod-formula-review/verify_rendering.py b/scripts/mod-formula-review/verify_rendering.py new file mode 100644 index 0000000000..a9cf6f5adf --- /dev/null +++ b/scripts/mod-formula-review/verify_rendering.py @@ -0,0 +1,26 @@ +"""Compile and exercise the sibling Sodium/Voxy production algorithms.""" +from pathlib import Path +import os +import subprocess +import tempfile + +work = Path(__file__).resolve().parent +root = work.parents[2] +gradle_home = Path(os.environ.get('GRADLE_USER_HOME', Path.home() / '.gradle')) +jar_root = gradle_home / 'caches/modules-2/files-2.1' +classpath = os.pathsep.join(str(next((jar_root / dependency).rglob('*.jar'))) for dependency in ( + 'it.unimi.dsi/fastutil/8.5.18', 'org.jspecify/jspecify/1.0.0')) +java = Path('/usr/lib/jvm/java-25-openjdk/bin') +sodium = root / 'sodium/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk' +subprocess.run(['python3', str(work / 'test_priority.py'), + str(sodium / 'lists/TaskCollectingTree.java')], check=True) +with tempfile.TemporaryDirectory(prefix='rendering-formulas-') as temporary: + sources = [sodium / 'compile/estimation' / (name + '.java') for name in ( + 'Estimator', 'Abstract2DLinearEstimator', 'UploadDuration', + 'UploadDurationEstimator', 'ExpDecayLinear2DEstimator')] + sources.extend((root / 'voxy/src/main/java/me/cortex/voxy/common/util/HierarchicalBitSet.java', + work / 'AllocatorRegression.java', work / 'EstimatorRegression.java')) + subprocess.run([str(java / 'javac'), '-cp', classpath, '-d', temporary, + *map(str, sources)], check=True) + for test in ('AllocatorRegression', 'EstimatorRegression'): + subprocess.run([str(java / 'java'), '-cp', temporary + os.pathsep + classpath, test], check=True) diff --git a/scripts/proguard.pro b/scripts/proguard.pro index dd63b216ba..90e4d8acca 100644 --- a/scripts/proguard.pro +++ b/scripts/proguard.pro @@ -44,6 +44,19 @@ @baritone.KeepName *; } +# XaeroPlus (BaritonePathSync) reflects the elytra path through these members by name; +# Meteor's obfuscated builds rename them and lose the map overlay +-keepclassmembers class baritone.process.ElytraProcess { + baritone.process.elytra.ElytraBehavior behavior; +} +-keepclassmembers class baritone.process.elytra.ElytraBehavior { + baritone.process.elytra.ElytraBehavior$PathManager pathManager; +} +-keepclassmembers class baritone.process.elytra.ElytraBehavior$PathManager { + baritone.process.elytra.NetherPath path; + public baritone.process.elytra.NetherPath getPath(); +} + # setting names are reflected from field names, so keep field names -keepclassmembers class baritone.api.Settings { public ; diff --git a/src/api/java/baritone/api/Settings.java b/src/api/java/baritone/api/Settings.java index b5295bd41d..ded11ff324 100644 --- a/src/api/java/baritone/api/Settings.java +++ b/src/api/java/baritone/api/Settings.java @@ -646,16 +646,47 @@ public final class Settings { public final Setting chunkCaching = new Setting<>(true); /** - * On save, delete from RAM any cached regions that are more than 1024 blocks away from the player + * Evict cached regions from RAM. The cache maintenance thread drops a region once it has been idle (not read + * by pathing or scanning, not written by the packer) for longer than {@link #cachedRegionsIdleSeconds}, and + * drops the least recently used regions whenever the resident cache exceeds {@link #cachedRegionsRamBudgetMB}. + * Regions with unsaved changes are written to disk first and are never evicted before that write succeeded. *

- * Temporarily disabled - *

- * Temporarily reenabled - * - * @see Issue #248 + * false keeps every region touched in this session in RAM. */ public final Setting pruneRegionsFromRAM = new Setting<>(true); + /** + * Upper bound for the cached regions kept in RAM, in MiB. A fully cached region costs about 27 MiB + * (1024 chunks of 24 KiB block data). The bound is soft by one maintenance tick. + */ + public final Setting cachedRegionsRamBudgetMB = new Setting<>(192); + + /** + * A cached region that nothing has read or written for this many seconds is evicted from RAM (after being + * saved if it changed). Regions come back from disk on demand. + */ + public final Setting cachedRegionsIdleSeconds = new Setting<>(180); + + /** + * How often changed regions are written to disk. The elytra nether pathfinder reads the region files, so this + * is also how long newly seen terrain takes to become visible to it. + */ + public final Setting cachedRegionsSaveIntervalSeconds = new Setting<>(30); + + /** + * When pathing asks for a region that is not in RAM, load it from disk in the background so the next path + * calculation sees terrain explored in earlier sessions. Without this only the packer (chunks around the + * player) and explicit scans (#find, explore) ever bring regions into RAM. + */ + public final Setting cachedRegionsLoadOnDemand = new Setting<>(true); + + /** + * Pack a chunk again when it unloads even if no block changed since it was packed on load. Off by default: + * the unload pack doubled the packer's work for an identical result. A chunk that changed while loaded, or + * that is not in the cache (its load-time pack was dropped by a full packer queue), is still packed on unload. + */ + public final Setting repackUnchangedChunksOnUnload = new Setting<>(false); + /** * The chunk packer queue can never grow to larger than this, if it does, the oldest chunks are discarded *

diff --git a/src/api/java/baritone/api/pathing/goals/GoalNear.java b/src/api/java/baritone/api/pathing/goals/GoalNear.java index 351c080216..630856c539 100644 --- a/src/api/java/baritone/api/pathing/goals/GoalNear.java +++ b/src/api/java/baritone/api/pathing/goals/GoalNear.java @@ -29,20 +29,20 @@ public class GoalNear implements Goal, IGoalRenderPos { private final int x; private final int y; private final int z; - private final int rangeSq; + private final long rangeSq; public GoalNear(BlockPos pos, int range) { this.x = pos.getX(); this.y = pos.getY(); this.z = pos.getZ(); - this.rangeSq = range * range; + this.rangeSq = (long) range * range; } @Override public boolean isInGoal(int x, int y, int z) { - int xDiff = x - this.x; - int yDiff = y - this.y; - int zDiff = z - this.z; + long xDiff = (long) x - this.x; + long yDiff = (long) y - this.y; + long zDiff = (long) z - this.z; return xDiff * xDiff + yDiff * yDiff + zDiff * zDiff <= rangeSq; } @@ -105,7 +105,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return (int) BetterBlockPos.longHash(x, y, z) + rangeSq; + return (int) BetterBlockPos.longHash(x, y, z) + Long.hashCode(rangeSq); } @Override diff --git a/src/api/java/baritone/api/pathing/goals/GoalRunAway.java b/src/api/java/baritone/api/pathing/goals/GoalRunAway.java index 4dd225060b..462968f1b4 100644 --- a/src/api/java/baritone/api/pathing/goals/GoalRunAway.java +++ b/src/api/java/baritone/api/pathing/goals/GoalRunAway.java @@ -34,7 +34,7 @@ public class GoalRunAway implements Goal { private final BlockPos[] from; - private final int distanceSq; + private final double distanceSq; private final Integer maintainY; @@ -47,7 +47,7 @@ public GoalRunAway(double distance, Integer maintainY, BlockPos... from) { throw new IllegalArgumentException("Positions to run away from must not be empty"); } this.from = from; - this.distanceSq = (int) (distance * distance); + this.distanceSq = distance * distance; this.maintainY = maintainY; } @@ -57,9 +57,9 @@ public boolean isInGoal(int x, int y, int z) { return false; } for (BlockPos p : from) { - int diffX = x - p.getX(); - int diffZ = z - p.getZ(); - int distSq = diffX * diffX + diffZ * diffZ; + double diffX = (double) x - p.getX(); + double diffZ = (double) z - p.getZ(); + double distSq = diffX * diffX + diffZ * diffZ; if (distSq < distanceSq) { return false; } @@ -143,8 +143,8 @@ public boolean equals(Object o) { @Override public int hashCode() { int hash = Arrays.hashCode(from); - hash = hash * 1196803141 + distanceSq; - hash = hash * -2053788840 + maintainY; + hash = hash * 1196803141 + Double.hashCode(distanceSq); + hash = hash * -2053788840 + Objects.hashCode(maintainY); return hash; } diff --git a/src/main/java/baritone/cache/CachedChunk.java b/src/main/java/baritone/cache/CachedChunk.java index c49394d76e..c2d0c53d6e 100644 --- a/src/main/java/baritone/cache/CachedChunk.java +++ b/src/main/java/baritone/cache/CachedChunk.java @@ -153,6 +153,13 @@ public static int size(int dimension_height) { return 2 * 16 * 16 * dimension_height; } + /** + * Rough heap footprint: the 2-bit block data plus the overview, height map and special-block index. + */ + public int approximateHeapBytes() { + return sizeInBytes + 2_560 + (special == null ? 0 : special.size() * 48); + } + public static int sizeInBytes(int size) { return size / 8; } diff --git a/src/main/java/baritone/cache/CachedRegion.java b/src/main/java/baritone/cache/CachedRegion.java index 12df7c4ffa..7d6edc8aa0 100644 --- a/src/main/java/baritone/cache/CachedRegion.java +++ b/src/main/java/baritone/cache/CachedRegion.java @@ -74,6 +74,16 @@ public final class CachedRegion implements ICachedRegion { */ private boolean hasUnsavedChanges; + /** + * Wall-clock time of the last read or write through CachedWorld; drives idle and LRU eviction + */ + private volatile long lastAccessMs = System.currentTimeMillis(); + + /** + * Set under the region lock when CachedWorld drops this instance from RAM; a late write must go to the live instance + */ + private boolean evicted; + CachedRegion(int x, int z, DimensionType dimension, ResourceKey dimensionId) { this.x = x; this.z = z; @@ -113,9 +123,53 @@ public final ArrayList getLocationsOf(String block) { return res; } - public final synchronized void updateCachedChunk(int chunkX, int chunkZ, CachedChunk chunk) { + /** + * @return false when this instance was evicted from RAM before the write; the caller must look the region up again + */ + public final synchronized boolean updateCachedChunk(int chunkX, int chunkZ, CachedChunk chunk) { + if (evicted) { + return false; + } this.chunks[chunkX][chunkZ] = chunk; hasUnsavedChanges = true; + lastAccessMs = System.currentTimeMillis(); + return true; + } + + final void touch() { + lastAccessMs = System.currentTimeMillis(); + } + + final long getLastAccessMs() { + return lastAccessMs; + } + + final synchronized boolean hasUnsavedChanges() { + return hasUnsavedChanges; + } + + /** + * Marks the region evicted unless it still has unsaved changes (the packer may have written since the last save). + */ + final synchronized boolean tryEvict() { + if (hasUnsavedChanges) { + return false; + } + evicted = true; + return true; + } + + final long approximateHeapBytes() { + long total = 0; + for (int x = 0; x < 32; x++) { + for (int z = 0; z < 32; z++) { + CachedChunk chunk = this.chunks[x][z]; + if (chunk != null) { + total += chunk.approximateHeapBytes(); + } + } + } + return total; } @@ -375,7 +429,7 @@ public final int getZ() { return this.z; } - private static Path getRegionFile(Path cacheDir, int regionX, int regionZ) { + static Path getRegionFile(Path cacheDir, int regionX, int regionZ) { return Paths.get(cacheDir.toString(), "r." + regionX + "." + regionZ + ".bcr"); } } diff --git a/src/main/java/baritone/cache/CachedWorld.java b/src/main/java/baritone/cache/CachedWorld.java index 037cc35f43..474b246215 100644 --- a/src/main/java/baritone/cache/CachedWorld.java +++ b/src/main/java/baritone/cache/CachedWorld.java @@ -18,14 +18,15 @@ package baritone.cache; import baritone.Baritone; -import baritone.api.BaritoneAPI; -import baritone.api.IBaritone; import baritone.api.cache.ICachedWorld; -import baritone.api.cache.IWorldData; import baritone.api.utils.Helper; import com.google.common.cache.CacheBuilder; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongList; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; import net.minecraft.core.BlockPos; import net.minecraft.resources.ResourceKey; import net.minecraft.world.level.ChunkPos; @@ -35,13 +36,27 @@ import java.io.IOException; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; /** + * The RAM tier of the chunk cache. The disk tier (one .bcr per region) keeps everything ever seen; RAM holds + * only regions in use: written by the packer, read by pathing or scanning. One shared daemon thread writes + * changed regions every {@code cachedRegionsSaveIntervalSeconds} and evicts idle or over-budget regions + * ({@link RegionRetentionPolicy}); a region is never evicted while it has unsaved changes. Pathing misses + * pull regions back from disk in the background. + * * @author Brady * @since 8/4/2018 */ @@ -52,16 +67,39 @@ public final class CachedWorld implements ICachedWorld, Helper { */ private static final int REGION_MAX = 30_000_000 / 512 + 1; + private static final ChunkPos PACKER_STOP = new ChunkPos(Integer.MIN_VALUE, Integer.MIN_VALUE); + + private static final long MAINTENANCE_TICK_SECONDS = 5; + + private static final int KNOWN_ABSENT_MAX = 65_536; + + private static final ScheduledExecutorService MAINTENANCE = Executors.newSingleThreadScheduledExecutor( + new ThreadFactoryBuilder().setNameFormat("baritone-cache-maintenance").setDaemon(true).build()); + + private static final ExecutorService LOADER = Executors.newSingleThreadExecutor( + new ThreadFactoryBuilder().setNameFormat("baritone-cache-loader").setDaemon(true).build()); + + private static final Set ACTIVE = ConcurrentHashMap.newKeySet(); + + static { + MAINTENANCE.scheduleWithFixedDelay(CachedWorld::maintainAll, MAINTENANCE_TICK_SECONDS, MAINTENANCE_TICK_SECONDS, TimeUnit.SECONDS); + } + /** * A map of all of the cached regions. */ - private Long2ObjectMap cachedRegions = new Long2ObjectOpenHashMap<>(); + private final Long2ObjectMap cachedRegions = new Long2ObjectOpenHashMap<>(); /** * The directory that the cached region files are saved to */ private final String directory; + /** + * False when the directory could not be created; every disk access is skipped instead of throwing per region + */ + private final boolean diskAvailable; + /** * Queue of positions to pack. Refers to the toPackMap, in that every element of this queue will be a * key in that map. @@ -74,36 +112,66 @@ public final class CachedWorld implements ICachedWorld, Helper { */ private final Map toPackMap = CacheBuilder.newBuilder().softValues().build().asMap(); + /** + * Region ids with a disk load queued on the loader thread. Guarded by this. + */ + private final LongSet loadsInFlight = new LongOpenHashSet(); + + /** + * Region ids known to have no file on disk, so pathing misses stop hitting the disk. Guarded by this. + */ + private final LongSet knownAbsent = new LongOpenHashSet(); + private final DimensionType dimension; private final ResourceKey dimensionId; + private volatile boolean active; + + private long lastSaveNanos = System.nanoTime(); + CachedWorld(Path directory, DimensionType dimension, ResourceKey dimensionId) { - if (!Files.exists(directory)) { - try { - Files.createDirectories(directory); - } catch (IOException ignored) { - } + boolean available = true; + try { + Files.createDirectories(directory); + } catch (IOException | InvalidPathException e) { + available = false; + System.out.println("Baritone cache directory " + directory + " cannot be created (" + e + "); disk caching is disabled for this world"); } + this.diskAvailable = available; this.directory = directory.toString(); this.dimension = dimension; this.dimensionId = dimensionId; System.out.println("Cached world directory: " + directory); + activate(); + } + + /** + * Starts the packer for this world and puts it under maintenance. Idempotent. + */ + final void activate() { + synchronized (this) { + if (active) { + return; + } + active = true; + } + ACTIVE.add(this); Baritone.getExecutor().execute(new PackerThread()); - Baritone.getExecutor().execute(() -> { - try { - Thread.sleep(30000); - while (true) { - // since a region only saves if it's been modified since its last save - // saving every 10 minutes means that once it's time to exit - // we'll only have a couple regions to save - save(); - Thread.sleep(600000); - } - } catch (InterruptedException e) { - e.printStackTrace(); + } + + /** + * Called when the game leaves this world: the packer drains what is queued, then saves and evicts every region. + */ + final void close() { + synchronized (this) { + if (!active) { + return; } - }); + active = false; + } + ACTIVE.remove(this); + toPackQueue.add(PACKER_STOP); } @Override @@ -115,7 +183,7 @@ public final void queueForPacking(LevelChunk chunk) { @Override public final boolean isCached(int blockX, int blockZ) { - CachedRegion region = getRegion(blockX >> 9, blockZ >> 9); + CachedRegion region = peekRegion(blockX >> 9, blockZ >> 9); if (region == null) { return false; } @@ -123,7 +191,7 @@ public final boolean isCached(int blockX, int blockZ) { } public final boolean regionLoaded(int blockX, int blockZ) { - return getRegion(blockX >> 9, blockZ >> 9) != null; + return peekRegion(blockX >> 9, blockZ >> 9) != null; } @Override @@ -158,82 +226,133 @@ public final ArrayList getLocationsOf(String block, int maximum, int c } private void updateCachedChunk(CachedChunk chunk) { - CachedRegion region = getOrCreateRegion(chunk.x >> 5, chunk.z >> 5); - region.updateCachedChunk(chunk.x & 31, chunk.z & 31, chunk); + while (true) { + CachedRegion region = getOrCreateRegion(chunk.x >> 5, chunk.z >> 5); + if (region.updateCachedChunk(chunk.x & 31, chunk.z & 31, chunk)) { + return; + } + // evicted between the lookup and the write; both sides hold the region lock, so retry on the live instance + } } @Override public final void save() { if (!Baritone.settings().chunkCaching.value) { System.out.println("Not saving to disk; chunk caching is disabled."); - allRegions().forEach(region -> { - if (region != null) { - region.removeExpired(); - } - }); // even if we aren't saving to disk, still delete expired old chunks from RAM - prune(); return; } - long start = System.nanoTime() / 1000000L; - allRegions().parallelStream().forEach(region -> { - if (region != null) { - region.save(this.directory); - } - }); - long now = System.nanoTime() / 1000000L; - System.out.println("World save took " + (now - start) + "ms"); - prune(); + int saved = saveDirtyRegions(); + if (saved == 0) { + System.out.println("World save: nothing changed"); + } } /** - * Delete regions that are too far from the player + * @return how many regions had changes and were written */ - private synchronized void prune() { - if (!Baritone.settings().pruneRegionsFromRAM.value) { - return; + private int saveDirtyRegions() { + if (!diskAvailable) { + return 0; } - BlockPos pruneCenter = guessPosition(); + List dirty = new ArrayList<>(); for (CachedRegion region : allRegions()) { - if (region == null) { - continue; + if (region != null && region.hasUnsavedChanges()) { + dirty.add(region); } - int distX = ((region.getX() << 9) + 256) - pruneCenter.getX(); - int distZ = ((region.getZ() << 9) + 256) - pruneCenter.getZ(); - double dist = Math.sqrt(distX * distX + distZ * distZ); - if (dist > 1024) { - logDebug("Deleting cached region from ram"); - cachedRegions.remove(getRegionID(region.getX(), region.getZ())); + } + if (dirty.isEmpty()) { + return 0; + } + long start = System.nanoTime() / 1000000L; + dirty.parallelStream().forEach(region -> region.save(this.directory)); + long now = System.nanoTime() / 1000000L; + System.out.println("World save of " + dirty.size() + " regions took " + (now - start) + "ms"); + return dirty.size(); + } + + private static void maintainAll() { + for (CachedWorld world : ACTIVE) { + try { + world.maintain(); + } catch (Throwable th) { + th.printStackTrace(); } } } + private void maintain() { + long now = System.nanoTime(); + if (now - lastSaveNanos >= Baritone.settings().cachedRegionsSaveIntervalSeconds.value * 1_000_000_000L) { + lastSaveNanos = now; + if (Baritone.settings().chunkCaching.value) { + saveDirtyRegions(); + } else { + allRegions().forEach(region -> { + if (region != null) { + region.removeExpired(); + } + }); // even if we aren't saving to disk, still delete expired old chunks from RAM + } + } + evict(false); + } + /** - * If we are still in this world and dimension, return player feet, otherwise return most recently modified chunk + * Runs on the packer thread after it drained the queue for a closed world. */ - private BlockPos guessPosition() { - for (IBaritone ibaritone : BaritoneAPI.getProvider().getAllBaritones()) { - IWorldData data = ibaritone.getWorldProvider().getCurrentWorld(); - if (data != null && data.getCachedWorld() == this && ibaritone.getPlayerContext().player() != null) { - return ibaritone.getPlayerContext().playerFeet(); - } + private void saveAndEvictAll() { + if (Baritone.settings().chunkCaching.value) { + saveDirtyRegions(); } - CachedChunk mostRecentlyModified = null; - for (CachedRegion region : allRegions()) { - if (region == null) { - continue; - } - CachedChunk ch = region.mostRecentlyModified(); - if (ch == null) { - continue; + if (active) { + // re-entered this world while the old packer was still draining; the live packer's regions stay + return; + } + evict(true); + } + + /** + * Drops regions per {@link RegionRetentionPolicy}. A region that turned dirty since the policy looked at it + * refuses the eviction under its own lock and stays until the next save. + */ + private void evict(boolean everything) { + if (!everything && !Baritone.settings().pruneRegionsFromRAM.value) { + return; + } + List regions = allRegions(); + List candidates = new ArrayList<>(regions.size()); + for (CachedRegion region : regions) { + if (region != null) { + candidates.add(new RegionRetentionPolicy.Candidate( + getRegionID(region.getX(), region.getZ()), + region.getLastAccessMs(), + region.hasUnsavedChanges(), + region.approximateHeapBytes())); } - if (mostRecentlyModified == null || mostRecentlyModified.cacheTimestamp < ch.cacheTimestamp) { - mostRecentlyModified = ch; + } + LongList victims = RegionRetentionPolicy.select( + candidates, + Baritone.settings().cachedRegionsRamBudgetMB.value * 1024L * 1024L, + Baritone.settings().cachedRegionsIdleSeconds.value * 1000L, + System.currentTimeMillis(), + everything); + if (victims.isEmpty()) { + return; + } + int evicted = 0; + synchronized (this) { + for (int i = 0; i < victims.size(); i++) { + long id = victims.getLong(i); + CachedRegion region = cachedRegions.get(id); + if (region != null && region.tryEvict()) { + cachedRegions.remove(id); + evicted++; + } } } - if (mostRecentlyModified == null) { - return new BlockPos(0, 0, 0); + if (evicted > 0) { + logDebug("Evicted " + evicted + " cached regions from RAM"); } - return new BlockPos((mostRecentlyModified.x << 4) + 8, 0, (mostRecentlyModified.z << 4) + 8); } private synchronized List allRegions() { @@ -252,25 +371,89 @@ public final void reloadAllFromDisk() { System.out.println("World load took " + (now - start) + "ms"); } - @Override - public final synchronized CachedRegion getRegion(int regionX, int regionZ) { + private synchronized CachedRegion peekRegion(int regionX, int regionZ) { return cachedRegions.get(getRegionID(regionX, regionZ)); } + /** + * The region if it is in RAM. A miss queues a background load from disk, so a path calculation that reaches + * terrain explored in an earlier session sees it on its next attempt. + */ + @Override + public final CachedRegion getRegion(int regionX, int regionZ) { + long id = getRegionID(regionX, regionZ); + synchronized (this) { + CachedRegion region = cachedRegions.get(id); + if (region != null) { + region.touch(); + return region; + } + if (!diskAvailable + || !Baritone.settings().cachedRegionsLoadOnDemand.value + || knownAbsent.contains(id) + || !loadsInFlight.add(id)) { + return null; + } + } + LOADER.execute(() -> loadFromDisk(regionX, regionZ)); + return null; + } + + private void loadFromDisk(int regionX, int regionZ) { + long id = getRegionID(regionX, regionZ); + try { + if (!Files.exists(CachedRegion.getRegionFile(Paths.get(directory), regionX, regionZ))) { + synchronized (this) { + if (knownAbsent.size() >= KNOWN_ABSENT_MAX) { + knownAbsent.clear(); + } + knownAbsent.add(id); + } + return; + } + CachedRegion region = new CachedRegion(regionX, regionZ, dimension, dimensionId); + region.load(this.directory); + synchronized (this) { + cachedRegions.putIfAbsent(id, region); + } + } catch (Throwable th) { + th.printStackTrace(); + } finally { + synchronized (this) { + loadsInFlight.remove(id); + } + } + } + /** * Returns the region at the specified region coordinates. If a - * region is not found, then a new one is created. + * region is not found, then a new one is created (loaded from disk when a file exists). + * The disk read happens outside the world lock so pathing lookups are not stalled by it. * * @param regionX The region X coordinate * @param regionZ The region Z coordinate * @return The region located at the specified coordinates */ - private synchronized CachedRegion getOrCreateRegion(int regionX, int regionZ) { - return cachedRegions.computeIfAbsent(getRegionID(regionX, regionZ), id -> { - CachedRegion newRegion = new CachedRegion(regionX, regionZ, dimension, dimensionId); - newRegion.load(this.directory); - return newRegion; - }); + private CachedRegion getOrCreateRegion(int regionX, int regionZ) { + long id = getRegionID(regionX, regionZ); + synchronized (this) { + CachedRegion region = cachedRegions.get(id); + if (region != null) { + region.touch(); + return region; + } + } + CachedRegion fresh = new CachedRegion(regionX, regionZ, dimension, dimensionId); + if (diskAvailable) { + fresh.load(this.directory); + } + synchronized (this) { + CachedRegion existing = cachedRegions.putIfAbsent(id, fresh); + knownAbsent.remove(id); + CachedRegion region = existing != null ? existing : fresh; + region.touch(); + return region; + } } public void tryLoadFromDisk(int regionX, int regionZ) { @@ -310,7 +493,14 @@ public void run() { while (true) { try { ChunkPos pos = toPackQueue.take(); + if (pos == PACKER_STOP) { + saveAndEvictAll(); + return; + } LevelChunk chunk = toPackMap.remove(pos); + if (chunk == null) { + continue; // soft reference collected before we got to it + } if (toPackQueue.size() > Baritone.settings().chunkPackerQueueMaxSize.value) { continue; } diff --git a/src/main/java/baritone/cache/ChunkPacker.java b/src/main/java/baritone/cache/ChunkPacker.java index 78bca4b7a8..b5a6de007e 100644 --- a/src/main/java/baritone/cache/ChunkPacker.java +++ b/src/main/java/baritone/cache/ChunkPacker.java @@ -53,7 +53,9 @@ public static CachedChunk pack(LevelChunk chunk) { LevelChunkSection[] chunkInternalStorageArray = chunk.getSections(); for (int y0 = 0; y0 < height / 16; y0++) { LevelChunkSection extendedblockstorage = chunkInternalStorageArray[y0]; - if (extendedblockstorage == null) { + // modern chunks never have null sections; an all-air section still packs to zeros, so skip the + // 4096 block lookups (most of an overworld column above the surface, the nether above the roof) + if (extendedblockstorage == null || extendedblockstorage.hasOnlyAir()) { // any 16x16x16 area that's all air will have null storage // for example, in an ocean biome, with air from y=64 to y=256 // the first 4 extended blocks storages will be full diff --git a/src/main/java/baritone/cache/RegionRetentionPolicy.java b/src/main/java/baritone/cache/RegionRetentionPolicy.java new file mode 100644 index 0000000000..440008173c --- /dev/null +++ b/src/main/java/baritone/cache/RegionRetentionPolicy.java @@ -0,0 +1,74 @@ +/* + * 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.cache; + +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongList; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * Which cached regions to drop from RAM. Pure so it can be unit tested; CachedWorld applies the result under + * its locks and re-checks the dirty flag there. + *

+ * A dirty region is never selected. Clean regions idle for longer than {@code idleMs} are always selected. + * If the resident bytes still exceed the budget, the least recently used clean regions go next, except those + * touched within {@link #GRACE_MS}: the packer or a path calculation is about to use them again. + */ +final class RegionRetentionPolicy { + + static final long GRACE_MS = 2_000; + + record Candidate(long id, long lastAccessMs, boolean dirty, long bytes) {} + + private RegionRetentionPolicy() {} + + static LongList select(List regions, long budgetBytes, long idleMs, long nowMs, boolean everything) { + LongList victims = new LongArrayList(); + long resident = 0; + for (Candidate c : regions) { + resident += c.bytes(); + } + List clean = new ArrayList<>(); + for (Candidate c : regions) { + if (c.dirty()) { + continue; + } + if (everything || nowMs - c.lastAccessMs() > idleMs) { + victims.add(c.id()); + resident -= c.bytes(); + } else { + clean.add(c); + } + } + if (resident <= budgetBytes) { + return victims; + } + clean.sort(Comparator.comparingLong(Candidate::lastAccessMs)); + for (Candidate c : clean) { + if (resident <= budgetBytes || nowMs - c.lastAccessMs() < GRACE_MS) { + break; + } + victims.add(c.id()); + resident -= c.bytes(); + } + return victims; + } +} diff --git a/src/main/java/baritone/cache/WorldData.java b/src/main/java/baritone/cache/WorldData.java index 5ea9739f92..95710431bb 100644 --- a/src/main/java/baritone/cache/WorldData.java +++ b/src/main/java/baritone/cache/WorldData.java @@ -17,7 +17,6 @@ package baritone.cache; -import baritone.Baritone; import baritone.api.cache.ICachedWorld; import baritone.api.cache.IWaypointCollection; import baritone.api.cache.IWorldData; @@ -48,10 +47,7 @@ public class WorldData implements IWorldData { } public void onClose() { - Baritone.getExecutor().execute(() -> { - System.out.println("Started saving the world in a new thread"); - cache.save(); - }); + cache.close(); } @Override diff --git a/src/main/java/baritone/cache/WorldProvider.java b/src/main/java/baritone/cache/WorldProvider.java index 28b2d94f11..b5e67e20eb 100644 --- a/src/main/java/baritone/cache/WorldProvider.java +++ b/src/main/java/baritone/cache/WorldProvider.java @@ -25,11 +25,11 @@ import net.minecraft.resources.Identifier; import net.minecraft.world.level.Level; import net.minecraft.world.level.storage.LevelResource; -import org.apache.commons.lang3.SystemUtils; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; @@ -94,6 +94,8 @@ public final void initWorld(Level world) { synchronized (worldCache) { this.currentWorld = worldCache.computeIfAbsent(worldDataDir, d -> new WorldData(d, world.dimensionType(), world.dimension())); } + // a WorldData is reused across visits; its cache holds no regions and no threads while closed + this.currentWorld.cache.activate(); this.mcWorld = ctx.world(); }); } @@ -147,11 +149,8 @@ private Optional> getSaveDirectories(Level world) { mcWorld = ctx.world(); return Optional.empty(); } - if (SystemUtils.IS_OS_WINDOWS) { - folderName = folderName.replace(":", "_"); - } // TODO: This should probably be in "baritone/servers" - worldDir = baritone.getDirectory().resolve(folderName); + worldDir = baritone.getDirectory().resolve(serverFolderName(baritone.getDirectory(), folderName)); // Just write the readme to the baritone directory instead of each server save in it readmeDir = baritone.getDirectory(); } @@ -159,6 +158,39 @@ private Optional> getSaveDirectories(Level world) { return Optional.of(new Pair<>(worldDir, readmeDir)); } + /** + * Windows, and NTFS mounted with windows_names, reject {@code : * ? " < > |} in a file name, so a direct-IP + * server ("host:port") could never get a cache directory there and every region save and load threw. Names + * are sanitised on every platform now (":" becomes "_", the same name Windows always produced). A directory + * under the raw name that already exists, written by an earlier version on Linux, keeps being used. + */ + static String serverFolderName(Path baritoneDir, String rawName) { + String sanitised = sanitizeFileName(rawName); + if (!sanitised.equals(rawName)) { + try { + if (Files.isDirectory(baritoneDir.resolve(rawName))) { + return rawName; + } + } catch (InvalidPathException ignored) { + // the raw name is not a legal path on this platform, so nothing can exist under it + } + } + return sanitised; + } + + static String sanitizeFileName(String name) { + StringBuilder out = new StringBuilder(name.length()); + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + out.append(c < 0x20 || "\\/:*?\"<>|".indexOf(c) >= 0 ? '_' : c); + } + int end = out.length(); + while (end > 0 && (out.charAt(end - 1) == '.' || out.charAt(end - 1) == ' ')) { + end--; + } + return end == 0 ? "_" : out.substring(0, end); + } + /** * Why does this exist instead of fixing the event? Some mods break the event. Lol. */ diff --git a/src/main/java/baritone/command/defaults/FindCommand.java b/src/main/java/baritone/command/defaults/FindCommand.java index 75d55e94e0..8d5142e926 100644 --- a/src/main/java/baritone/command/defaults/FindCommand.java +++ b/src/main/java/baritone/command/defaults/FindCommand.java @@ -60,7 +60,7 @@ public void execute(String label, IArgConsumer args) throws CommandException { BuiltInRegistries.BLOCK.getKey(block).getPath(), Integer.MAX_VALUE, origin.x, - origin.y, + origin.z, 4 ).stream() ) diff --git a/src/main/java/baritone/event/GameEventHandler.java b/src/main/java/baritone/event/GameEventHandler.java index 8bbdb52d67..54045a70d7 100644 --- a/src/main/java/baritone/event/GameEventHandler.java +++ b/src/main/java/baritone/event/GameEventHandler.java @@ -101,6 +101,14 @@ public void onChunkEvent(ChunkEvent event) { if (event.isPostPopulate() || isPreUnload) { baritone.getWorldProvider().ifWorldLoaded(worldData -> { LevelChunk chunk = world.getChunk(event.getX(), event.getZ()); + if (isPreUnload + && !Baritone.settings().repackUnchangedChunksOnUnload.value + && !chunk.isUnsaved() + && worldData.getCachedWorld().isCached(event.getX() << 4, event.getZ() << 4)) { + // packed on load and no block changed since (the client marks a chunk unsaved on every + // setBlockState): the cache already holds this exact chunk + return; + } worldData.getCachedWorld().queueForPacking(chunk); }); } diff --git a/src/main/java/baritone/process/ElytraProcess.java b/src/main/java/baritone/process/ElytraProcess.java index e61fe86756..d490c2bd3f 100644 --- a/src/main/java/baritone/process/ElytraProcess.java +++ b/src/main/java/baritone/process/ElytraProcess.java @@ -90,6 +90,13 @@ public class ElytraProcess extends BaritoneProcessHelper implements IBaritonePro * tearing down only fires if nothing else (a cancel, another goal) happened in the meantime. */ private int engageEpoch; + /** + * Destination of an engage that is waiting on {@link #npfDestroyDone} for the previous context's + * teardown (see {@link #pathTo0}); null when none is. Keeps the process active and its destination + * visible in the meantime, so the pathing manager keeps ticking us and callers polling for the goal + * don't re-issue it. Cleared wherever {@link #engageEpoch} is bumped, which is what drops the deferral. + */ + private BlockPos deferredDestination; // incremented from the pathfinder's worker threads, reset from wherever a path completes private final AtomicInteger consecutivePathCalcFailures = new AtomicInteger(); private static final int PATH_CALC_FAILURES_BEFORE_RESET = 3; @@ -158,6 +165,7 @@ public void onLostControl(boolean destroyNpf) { return; } this.engageEpoch++; + this.deferredDestination = null; this.state = State.START_FLYING; // TODO: null state? this.goingToLandingSpot = false; this.landingSpot = null; @@ -207,6 +215,17 @@ public void onPathCalculationFailure() { this.onLostControl(); } + /** + * Engaged, but without a behavior: {@link #pathTo0} queued its creation behind the previous pathfinder + * context's teardown, either because a setting change just reset us mid-tick or because we were + * re-engaged before that teardown was done. Hold control until the deferred engage lands instead of + * ticking a behavior we don't have, and let go of whatever keys the old one left forced. + */ + private PathingCommand holdForDeferredEngage() { + baritone.getInputOverrideHandler().clearAllKeys(); + return new PathingCommand(null, PathingCommandType.REQUEST_PAUSE); + } + public static IElytraProcess create(final Baritone baritone) { return NetherPathfinderContext.isSupported() ? new ElytraProcess(baritone) @@ -215,7 +234,7 @@ public static IElytraProcess create(final Baritone baritone) { @Override public boolean isActive() { - return this.behavior != null; + return this.behavior != null || this.deferredDestination != null; } @Override @@ -233,6 +252,9 @@ public void resetState() { @Override public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { + if (this.behavior == null) { + return holdForDeferredEngage(); + } try { final long seedSetting = Baritone.settings().elytraNetherSeed.value; if (seedSetting != this.behavior.npfContext.getSeed()) { @@ -263,6 +285,10 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { logDirect(e.getMessage(), ChatFormatting.RED); return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); } + if (this.behavior == null) { + // one of the resets above tore the context down and had to defer the re-engage + return holdForDeferredEngage(); + } this.behavior.onTick(); @@ -771,7 +797,7 @@ public void repackChunks() { @Override public BlockPos currentDestination() { - return this.behavior != null ? this.behavior.destination : null; + return this.behavior != null ? this.behavior.destination : this.deferredDestination; } @Override @@ -804,6 +830,7 @@ private void pathTo0(BlockPos destination, boolean appendDestination) { // thread for its whole remaining duration, so finish this engage when the // teardown completes instead - unless something else took over in the meantime. final int epoch = this.engageEpoch; + this.deferredDestination = destination; this.npfDestroyDone.whenCompleteAsync((v, ex) -> { if (this.engageEpoch == epoch) { this.pathTo0(destination, appendDestination); @@ -931,6 +958,7 @@ public void onWorldEvent(WorldEvent event) { // Exiting the world, just destroy. A world change also invalidates any engage that was // deferred while the old context tore down - its destination belongs to the old world. this.engageEpoch++; + this.deferredDestination = null; destroyBehaviorAsync(); } } diff --git a/src/test/java/baritone/cache/RegionRetentionPolicyTest.java b/src/test/java/baritone/cache/RegionRetentionPolicyTest.java new file mode 100644 index 0000000000..58b6584cc2 --- /dev/null +++ b/src/test/java/baritone/cache/RegionRetentionPolicyTest.java @@ -0,0 +1,92 @@ +/* + * 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.cache; + +import baritone.cache.RegionRetentionPolicy.Candidate; +import it.unimi.dsi.fastutil.longs.LongList; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class RegionRetentionPolicyTest { + + private static final long MB = 1024 * 1024; + private static final long NOW = 1_000_000_000L; + private static final long IDLE = 180_000; + + @Test + public void dirtyRegionsAreNeverSelected() { + List regions = List.of( + new Candidate(1, NOW - 10 * IDLE, true, 27 * MB), + new Candidate(2, NOW - 10 * IDLE, true, 27 * MB)); + assertTrue(RegionRetentionPolicy.select(regions, 0, IDLE, NOW, false).isEmpty()); + assertTrue(RegionRetentionPolicy.select(regions, 0, IDLE, NOW, true).isEmpty()); + } + + @Test + public void idleCleanRegionsGoRegardlessOfBudget() { + List regions = List.of( + new Candidate(1, NOW - IDLE - 1, false, 27 * MB), + new Candidate(2, NOW - 1_000, false, 27 * MB)); + LongList victims = RegionRetentionPolicy.select(regions, 1024 * MB, IDLE, NOW, false); + assertEquals(1, victims.size()); + assertEquals(1, victims.getLong(0)); + } + + @Test + public void overBudgetEvictsLeastRecentlyUsedFirstUntilUnderBudget() { + List regions = List.of( + new Candidate(1, NOW - 60_000, false, 27 * MB), + new Candidate(2, NOW - 30_000, false, 27 * MB), + new Candidate(3, NOW - 90_000, false, 27 * MB), + new Candidate(4, NOW - 5_000, false, 27 * MB)); + LongList victims = RegionRetentionPolicy.select(regions, 60 * MB, IDLE, NOW, false); + assertEquals(List.of(3L, 1L), victims); + } + + @Test + public void regionsTouchedWithinGraceStayEvenWhenOverBudget() { + List regions = List.of( + new Candidate(1, NOW - 500, false, 27 * MB), + new Candidate(2, NOW - 100, false, 27 * MB)); + assertTrue(RegionRetentionPolicy.select(regions, 0, IDLE, NOW, false).isEmpty()); + } + + @Test + public void dirtyBytesStillCountAgainstTheBudget() { + List regions = List.of( + new Candidate(1, NOW - 10_000, true, 100 * MB), + new Candidate(2, NOW - 10_000, false, 27 * MB)); + LongList victims = RegionRetentionPolicy.select(regions, 110 * MB, IDLE, NOW, false); + assertEquals(List.of(2L), victims); + assertFalse(victims.contains(1L)); + } + + @Test + public void everythingSelectsEveryCleanRegion() { + List regions = List.of( + new Candidate(1, NOW, false, 27 * MB), + new Candidate(2, NOW, true, 27 * MB), + new Candidate(3, NOW - 1, false, 27 * MB)); + assertEquals(List.of(1L, 3L), RegionRetentionPolicy.select(regions, 1024 * MB, IDLE, NOW, true)); + } +} diff --git a/src/test/java/baritone/cache/WorldProviderFolderNameTest.java b/src/test/java/baritone/cache/WorldProviderFolderNameTest.java new file mode 100644 index 0000000000..56a7e7511c --- /dev/null +++ b/src/test/java/baritone/cache/WorldProviderFolderNameTest.java @@ -0,0 +1,66 @@ +/* + * 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.cache; + +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.assertEquals; + +public class WorldProviderFolderNameTest { + + @Test + public void hostnamesAreUntouched() { + assertEquals("2b2t.org", WorldProvider.sanitizeFileName("2b2t.org")); + assertEquals("realms", WorldProvider.sanitizeFileName("realms")); + } + + @Test + public void directIpMatchesWhatWindowsAlwaysProduced() { + assertEquals("217.77.15.230_25571", WorldProvider.sanitizeFileName("217.77.15.230:25571")); + } + + @Test + public void everyWindowsReservedCharacterIsReplaced() { + assertEquals("a_b_c_d_e_f_g_h_i", WorldProvider.sanitizeFileName("a\\b/c:d*e?f\"gi")); + assertEquals("x_y", WorldProvider.sanitizeFileName("x|y")); + assertEquals("tab_", WorldProvider.sanitizeFileName("tab\t")); + } + + @Test + public void trailingDotsAndSpacesAreStripped() { + assertEquals("host_1", WorldProvider.sanitizeFileName("host:1. ")); + assertEquals("_", WorldProvider.sanitizeFileName("...")); + } + + @Test + public void existingRawDirectoryKeepsBeingUsed() throws IOException { + Path dir = Files.createTempDirectory("baritone-folder-test"); + try { + assertEquals("host_25565", WorldProvider.serverFolderName(dir, "host:25565")); + Files.createDirectory(dir.resolve("host:25565")); + assertEquals("host:25565", WorldProvider.serverFolderName(dir, "host:25565")); + } finally { + Files.deleteIfExists(dir.resolve("host:25565")); + Files.delete(dir); + } + } +} diff --git a/src/test/java/baritone/pathing/goals/GoalDistanceTest.java b/src/test/java/baritone/pathing/goals/GoalDistanceTest.java new file mode 100644 index 0000000000..00ba3013a2 --- /dev/null +++ b/src/test/java/baritone/pathing/goals/GoalDistanceTest.java @@ -0,0 +1,68 @@ +package baritone.pathing.goals; + +import baritone.api.pathing.goals.GoalNear; +import baritone.api.pathing.goals.GoalRunAway; +import net.minecraft.core.BlockPos; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class GoalDistanceTest { + + @Test + public void nearDoesNotAcceptDistantPositionsAfterIntegerOverflow() { + GoalNear goal = new GoalNear(new BlockPos(0, 64, 0), 2); + assertTrue(goal.isInGoal(2, 64, 0)); + assertFalse(goal.isInGoal(3, 64, 0)); + for (int distance : new int[]{50_000, 65_536, 30_000_000}) { + assertFalse(goal.isInGoal(distance, 64, 0)); + assertFalse(goal.isInGoal(0, 64, -distance)); + } + } + + @Test + public void nearSupportsRadiiWhoseSquaresExceedAnInteger() { + GoalNear goal = new GoalNear(new BlockPos(-1_000_000, 64, 1_000_000), 50_000); + assertTrue(goal.isInGoal(-1_000_000, 64, 1_000_000)); + assertTrue(goal.isInGoal(-950_000, 64, 1_000_000)); + assertFalse(goal.isInGoal(-949_999, 64, 1_000_000)); + } + + @Test + public void runawayDistanceWorksAcrossTheWorld() { + GoalRunAway goal = new GoalRunAway(100, new BlockPos(0, 64, 0)); + assertFalse(goal.isInGoal(99, 64, 0)); + assertTrue(goal.isInGoal(100, 64, 0)); + assertTrue(goal.isInGoal(50_000, 64, 0)); + assertTrue(goal.isInGoal(0, 64, -65_536)); + assertTrue(new GoalRunAway(100, new BlockPos(-30_000_000, 64, 0)) + .isInGoal(30_000_000, 64, 0)); + } + + @Test + public void runawayPreservesLargeAndFractionalRequestedDistances() { + GoalRunAway goal = new GoalRunAway(100_000, new BlockPos(0, 64, 0)); + assertFalse(goal.isInGoal(99_999, 64, 0)); + assertTrue(goal.isInGoal(100_000, 64, 0)); + assertFalse(new GoalRunAway(1.1, new BlockPos(0, 64, 0)).isInGoal(1, 64, 0)); + } + + @Test + public void runawayStillChecksEveryOriginAndRequestedHeight() { + GoalRunAway goal = new GoalRunAway(10, Integer.valueOf(70), + new BlockPos(0, 64, 0), new BlockPos(100, 64, 0)); + assertFalse(goal.isInGoal(95, 70, 0)); + assertFalse(goal.isInGoal(50, 69, 0)); + assertTrue(goal.isInGoal(50, 70, 0)); + } + + @Test + public void equalRunawayGoalsWithoutHeightHaveEqualHashes() { + GoalRunAway first = new GoalRunAway(1.1, new BlockPos(0, 64, 0)); + GoalRunAway second = new GoalRunAway(1.1, new BlockPos(0, 64, 0)); + assertEquals(first, second); + assertEquals(first.hashCode(), second.hashCode()); + } +} From 9c35132632fa9e1e1e48490b6e825fb21444d3f2 Mon Sep 17 00:00:00 2001 From: Todarac <3059008+Todarac@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:37:25 -0500 Subject: [PATCH 46/54] Revert client registry access for loot table resolution PR #5058 sourced ServerLevelStub's RegistryAccess from client.level so that modded loot tables would resolve. However, loot tables are populated only by the ReloadableServerRegistries reload performed in load(); a client RegistryAccess never receives them. In game (client.level != null) every loot table lookup therefore threw, and the catch (Throwable) widened by that same PR silently swallowed it, leaving an empty drops list. The visible effect was that #mine stopped picking up items entirely. Revert that half so drops resolve again. The widened catch is kept, so modded blocks whose tables are absent degrade to no known drops instead of crashing, which preserves what #5058 set out to fix. Reported-by: ZacSharp Co-Authored-By: Claude --- src/api/java/baritone/api/utils/BlockOptionalMeta.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/api/java/baritone/api/utils/BlockOptionalMeta.java b/src/api/java/baritone/api/utils/BlockOptionalMeta.java index a9cc2caa56..0685aa8523 100644 --- a/src/api/java/baritone/api/utils/BlockOptionalMeta.java +++ b/src/api/java/baritone/api/utils/BlockOptionalMeta.java @@ -281,9 +281,10 @@ public static ServerLevelStub fastCreate() { @Override public RegistryAccess registryAccess() { - if (client.level != null) { - return client.level.registryAccess(); - } + // Loot tables are populated only by the reloaded server registries built + // in load(). A client RegistryAccess never receives them, so sourcing the + // holder from client.level made every block resolve to zero drops, which + // silently broke #mine item pickup (regression from PR #5058). return registryAccess.join(); } From 2713d88bce17ef22a35de9194cde6c047c3cd546 Mon Sep 17 00:00:00 2001 From: Dek Date: Wed, 4 Nov 2026 14:45:20 -0300 Subject: [PATCH 47/54] readme: the cache budget, packing, goal and folder name fixes, and 26.3 --- README.md | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0455a29154..12b0efc158 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ 1.21.11 26.1 26.2 + 26.3

A Minecraft pathfinder bot. This is a fork of [Baritone](https://github.com/cabaletta/baritone) by @@ -36,6 +37,7 @@ existed on one of them. | 1.21.11 | [`1.21.11`](https://github.com/dekrom/baritone/tree/1.21.11) | 21 | `v-1.21.11` | | 26.1 | [`26.1`](https://github.com/dekrom/baritone/tree/26.1) | 25 | `v-26.1` | | 26.2 | [`26.2`](https://github.com/dekrom/baritone/tree/26.2) | 25 | `v-26.2` | +| 26.3 | [`26.3`](https://github.com/dekrom/baritone/tree/26.3) | 25 | `v-26.3` | Each Minecraft version has its own branch and its own release. Fabric only. @@ -128,6 +130,8 @@ world stops matching the path. executors, and the octree interface caches a raw pointer into it. Running one of those against freed memory is a segfault, not an exception. It is freed under the lock they hold now, and every native call is fenced behind a flag that fails safe. +- **The XaeroPlus path overlay.** XaeroPlus finds the elytra path by field name, and the obfuscated + jars renamed those fields, so the path never showed on its map. They keep their names now. ### Chunk cache @@ -137,6 +141,23 @@ world stops matching the path. old one, so a reader sees either the previous region or the new one. On 26.2 this is what the nether pathfinder reads with `elytraUseCache` on, and a region it fails to parse is gone for the rest of the flight. +- **RAM is bounded.** Regions were only dropped from RAM after the ten minute save, and only by + distance from the player, so nothing capped how much of the cache stayed resident on a long trip. + A maintenance thread now saves changed regions every 30 seconds (`cachedRegionsSaveIntervalSeconds`), + drops regions nothing has touched for three minutes (`cachedRegionsIdleSeconds`), and then the + least recently used ones while the cache is over 192 MiB (`cachedRegionsRamBudgetMB`). A region + with unsaved changes is never dropped. A path calculation that reaches a region on disk but not + in RAM loads it back in the background (`cachedRegionsLoadOnDemand`), so terrain seen in an + earlier session counts again. +- **Less packing.** The check meant to skip empty sections tested for `null`, which modern chunks + never are, so every all-air section cost 4096 block lookups to pack to zeros: most of an overworld + column above the surface and the nether above the roof. Those are skipped now, and a chunk that + unloads unchanged since it was packed on load is not packed again + (`repackUnchangedChunksOnUnload` brings that back). +- **Server folder names.** Only Windows had the colon of a `host:port` server replaced, so on an + NTFS drive mounted under Linux such a server never got a cache directory and every region save + failed. Names are sanitised on every platform now, and an existing directory under the old name + keeps being used. ### Pathfinding correctness @@ -155,6 +176,10 @@ world stops matching the path. - `backtrackCostFavoringCoefficient` at 0 or below produced a non-positive action cost, which A\* rejects, failing the whole calculation instead of favoring nothing. - `GoalGetToBlock` reported an ETA that never counted down to zero. +- `GoalNear` and `GoalRunAway` squared `int` coordinate differences, which overflow: a point 65,536 + blocks away could satisfy a two-block `GoalNear`. `GoalRunAway` also truncated its radius and + threw when hashed without a y constraint. +- `#find` searched the cache around the player's y where it meant their z. ### Pathing thread @@ -169,7 +194,7 @@ world stops matching the path. before the cheap checks that almost always decide the outcome; `PathExecutor` built a whole `CalculationContext` per tick to read two fields off it. -### 26.2 only +### 26.2 and 26.3 only - Nether pathfinder chunk packing wrote solid sections as air, so `#elytra` planned paths straight through terrain. The 26.2 port had replaced the removed `LevelChunkSection.SECTION_SIZE` (4096, @@ -193,6 +218,12 @@ world stops matching the path. terrain prediction was off, on the theory that it only reads the cache. With `elytraUseCache` it also parses region files into it, and an insert that rehashes the map under the solvers' lookups is heap corruption. A path calculation is always a writer now. +- **`#elytra` going idle mid-engage.** An engage deferred behind the old context's teardown left the + process without a behavior, so it reported itself inactive and dropped its destination; the + pathing manager stopped ticking it and anything polling for the goal issued it again. It stays + active and keeps its destination until the engage lands. +- **`#mine` stopped picking up items.** Upstream's #5058 resolved loot tables through the client's + registries, which never contain them, so every block had no drops. Reverted, as upstream did. ## Building From 3f7d04993f6aded6ffe799665f3740c141f085cd Mon Sep 17 00:00:00 2001 From: Dek Date: Wed, 4 Nov 2026 15:19:54 -0300 Subject: [PATCH 48/54] evict regions nothing can save A region only turns clean when it is saved, and with chunkCaching off, or no cache directory, nothing ever saves it. Every region the packer wrote stayed dirty, the retention policy skips dirty regions, and the RAM tier grew for the whole session, where the old distance prune dropped them regardless. Unsaved changes only hold a region in RAM now while they can still be written. --- src/api/java/baritone/api/Settings.java | 3 ++- src/main/java/baritone/cache/CachedRegion.java | 5 +++-- src/main/java/baritone/cache/CachedWorld.java | 11 +++++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/api/java/baritone/api/Settings.java b/src/api/java/baritone/api/Settings.java index ded11ff324..aee759aa6d 100644 --- a/src/api/java/baritone/api/Settings.java +++ b/src/api/java/baritone/api/Settings.java @@ -649,7 +649,8 @@ public final class Settings { * Evict cached regions from RAM. The cache maintenance thread drops a region once it has been idle (not read * by pathing or scanning, not written by the packer) for longer than {@link #cachedRegionsIdleSeconds}, and * drops the least recently used regions whenever the resident cache exceeds {@link #cachedRegionsRamBudgetMB}. - * Regions with unsaved changes are written to disk first and are never evicted before that write succeeded. + * Regions with unsaved changes are written to disk first and are never evicted before that write succeeded, + * unless {@link #chunkCaching} is off and nothing will ever write them. *

* false keeps every region touched in this session in RAM. */ diff --git a/src/main/java/baritone/cache/CachedRegion.java b/src/main/java/baritone/cache/CachedRegion.java index 7d6edc8aa0..312a056048 100644 --- a/src/main/java/baritone/cache/CachedRegion.java +++ b/src/main/java/baritone/cache/CachedRegion.java @@ -150,9 +150,10 @@ final synchronized boolean hasUnsavedChanges() { /** * Marks the region evicted unless it still has unsaved changes (the packer may have written since the last save). + * {@code discardUnsaved} drops them anyway, for when nothing is able to save them. */ - final synchronized boolean tryEvict() { - if (hasUnsavedChanges) { + final synchronized boolean tryEvict(boolean discardUnsaved) { + if (hasUnsavedChanges && !discardUnsaved) { return false; } evicted = true; diff --git a/src/main/java/baritone/cache/CachedWorld.java b/src/main/java/baritone/cache/CachedWorld.java index 474b246215..e1a3f66baf 100644 --- a/src/main/java/baritone/cache/CachedWorld.java +++ b/src/main/java/baritone/cache/CachedWorld.java @@ -54,8 +54,8 @@ * The RAM tier of the chunk cache. The disk tier (one .bcr per region) keeps everything ever seen; RAM holds * only regions in use: written by the packer, read by pathing or scanning. One shared daemon thread writes * changed regions every {@code cachedRegionsSaveIntervalSeconds} and evicts idle or over-budget regions - * ({@link RegionRetentionPolicy}); a region is never evicted while it has unsaved changes. Pathing misses - * pull regions back from disk in the background. + * ({@link RegionRetentionPolicy}); a region is never evicted while it has unsaved changes that can still be + * saved. Pathing misses pull regions back from disk in the background. * * @author Brady * @since 8/4/2018 @@ -319,6 +319,9 @@ private void evict(boolean everything) { if (!everything && !Baritone.settings().pruneRegionsFromRAM.value) { return; } + // with chunk caching off or no cache directory nothing ever saves a region, so its unsaved changes would + // pin it in RAM for good; drop them instead, as the old distance prune did + boolean saveable = diskAvailable && Baritone.settings().chunkCaching.value; List regions = allRegions(); List candidates = new ArrayList<>(regions.size()); for (CachedRegion region : regions) { @@ -326,7 +329,7 @@ private void evict(boolean everything) { candidates.add(new RegionRetentionPolicy.Candidate( getRegionID(region.getX(), region.getZ()), region.getLastAccessMs(), - region.hasUnsavedChanges(), + saveable && region.hasUnsavedChanges(), region.approximateHeapBytes())); } } @@ -344,7 +347,7 @@ private void evict(boolean everything) { for (int i = 0; i < victims.size(); i++) { long id = victims.getLong(i); CachedRegion region = cachedRegions.get(id); - if (region != null && region.tryEvict()) { + if (region != null && region.tryEvict(!saveable)) { cachedRegions.remove(id); evicted++; } From 41edbe54e843260c49e2023681bc4795c26fe662 Mon Sep 17 00:00:00 2001 From: Dek Date: Wed, 4 Nov 2026 15:20:27 -0300 Subject: [PATCH 49/54] repack on unload when this load's pack never ran The unload repack was skipped for any chunk the cache held, but that copy can be one loaded from disk from an earlier session: when the load-time pack is dropped (packer queue over chunkPackerQueueMaxSize, or the soft reference collected first) the stale terrain stayed cached. The skip now also needs this load's pack to have run, and none to be still waiting. --- src/api/java/baritone/api/Settings.java | 2 +- src/main/java/baritone/cache/CachedWorld.java | 33 ++++++++++++++++++- .../java/baritone/event/GameEventHandler.java | 3 +- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/api/java/baritone/api/Settings.java b/src/api/java/baritone/api/Settings.java index aee759aa6d..fe14ae197c 100644 --- a/src/api/java/baritone/api/Settings.java +++ b/src/api/java/baritone/api/Settings.java @@ -684,7 +684,7 @@ public final class Settings { /** * Pack a chunk again when it unloads even if no block changed since it was packed on load. Off by default: * the unload pack doubled the packer's work for an identical result. A chunk that changed while loaded, or - * that is not in the cache (its load-time pack was dropped by a full packer queue), is still packed on unload. + * whose load-time pack was dropped or has not run yet, is still packed on unload. */ public final Setting repackUnchangedChunksOnUnload = new Setting<>(false); diff --git a/src/main/java/baritone/cache/CachedWorld.java b/src/main/java/baritone/cache/CachedWorld.java index e1a3f66baf..9d829b4d21 100644 --- a/src/main/java/baritone/cache/CachedWorld.java +++ b/src/main/java/baritone/cache/CachedWorld.java @@ -73,6 +73,8 @@ public final class CachedWorld implements ICachedWorld, Helper { private static final int KNOWN_ABSENT_MAX = 65_536; + private static final int DROPPED_PACKS_MAX = 65_536; + private static final ScheduledExecutorService MAINTENANCE = Executors.newSingleThreadScheduledExecutor( new ThreadFactoryBuilder().setNameFormat("baritone-cache-maintenance").setDaemon(true).build()); @@ -112,6 +114,12 @@ public final class CachedWorld implements ICachedWorld, Helper { */ private final Map toPackMap = CacheBuilder.newBuilder().softValues().build().asMap(); + /** + * Chunks whose latest pack was dropped (packer queue over chunkPackerQueueMaxSize, or the soft reference + * collected first), so whatever the cache holds for them predates that load. + */ + private final Set droppedPacks = ConcurrentHashMap.newKeySet(); + /** * Region ids with a disk load queued on the loader thread. Guarded by this. */ @@ -190,6 +198,19 @@ public final boolean isCached(int blockX, int blockZ) { return region.isCached(blockX & 511, blockZ & 511); } + /** + * Whether the cache holds what the current load of this chunk packed: it is cached, the pack queued when it + * loaded was not dropped, and no pack of it is still waiting. A copy from an earlier load or session does not + * count. + */ + public final boolean holdsPackOfCurrentLoad(int chunkX, int chunkZ) { + ChunkPos pos = new ChunkPos(chunkX, chunkZ); + if (toPackMap.containsKey(pos) || droppedPacks.contains(pos)) { + return false; + } + return isCached(chunkX << 4, chunkZ << 4); + } + public final boolean regionLoaded(int blockX, int blockZ) { return peekRegion(blockX >> 9, blockZ >> 9) != null; } @@ -490,6 +511,13 @@ private boolean isRegionInWorld(int regionX, int regionZ) { return regionX <= REGION_MAX && regionX >= -REGION_MAX && regionZ <= REGION_MAX && regionZ >= -REGION_MAX; } + private void markDropped(ChunkPos pos) { + if (droppedPacks.size() >= DROPPED_PACKS_MAX) { + droppedPacks.clear(); + } + droppedPacks.add(pos); + } + private class PackerThread implements Runnable { public void run() { @@ -502,13 +530,16 @@ public void run() { } LevelChunk chunk = toPackMap.remove(pos); if (chunk == null) { - continue; // soft reference collected before we got to it + markDropped(pos); // soft reference collected before we got to it + continue; } if (toPackQueue.size() > Baritone.settings().chunkPackerQueueMaxSize.value) { + markDropped(pos); continue; } CachedChunk cached = ChunkPacker.pack(chunk); CachedWorld.this.updateCachedChunk(cached); + droppedPacks.remove(pos); //System.out.println("Processed chunk at " + chunk.x + "," + chunk.z); } catch (InterruptedException e) { e.printStackTrace(); diff --git a/src/main/java/baritone/event/GameEventHandler.java b/src/main/java/baritone/event/GameEventHandler.java index 54045a70d7..db42a599e1 100644 --- a/src/main/java/baritone/event/GameEventHandler.java +++ b/src/main/java/baritone/event/GameEventHandler.java @@ -25,6 +25,7 @@ import baritone.api.utils.Helper; import baritone.api.utils.Pair; import baritone.cache.CachedChunk; +import baritone.cache.CachedWorld; import baritone.cache.WorldProvider; import baritone.utils.BlockStateInterface; import net.minecraft.world.level.ChunkPos; @@ -104,7 +105,7 @@ public void onChunkEvent(ChunkEvent event) { if (isPreUnload && !Baritone.settings().repackUnchangedChunksOnUnload.value && !chunk.isUnsaved() - && worldData.getCachedWorld().isCached(event.getX() << 4, event.getZ() << 4)) { + && ((CachedWorld) worldData.getCachedWorld()).holdsPackOfCurrentLoad(event.getX(), event.getZ())) { // packed on load and no block changed since (the client marks a chunk unsaved on every // setBlockState): the cache already holds this exact chunk return; From 521b1a1916b3c244857ffb6ac90b9d302f8b22cc Mon Sep 17 00:00:00 2001 From: ZacSharp <68165024+ZacSharp@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:42:33 +0200 Subject: [PATCH 50/54] Fix Litematica detection (cherry picked from commit 5f736854a12c0e027b4ba189fb3b9e41e25f9f04) --- .../utils/schematic/litematica/LitematicaHelper.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/baritone/utils/schematic/litematica/LitematicaHelper.java b/src/main/java/baritone/utils/schematic/litematica/LitematicaHelper.java index a4b806f15b..b8e1e69813 100644 --- a/src/main/java/baritone/utils/schematic/litematica/LitematicaHelper.java +++ b/src/main/java/baritone/utils/schematic/litematica/LitematicaHelper.java @@ -26,7 +26,6 @@ import fi.dy.masa.litematica.schematic.placement.SchematicPlacement; import fi.dy.masa.litematica.schematic.placement.SubRegionPlacement; import fi.dy.masa.litematica.world.SchematicWorldHandler; -import fi.dy.masa.litematica.world.WorldSchematic; import net.minecraft.core.BlockPos; import net.minecraft.core.Vec3i; import net.minecraft.world.level.Level; @@ -37,6 +36,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.function.Supplier; /** * Helper class that provides access or processes data related to Litmatica schematics. @@ -99,7 +99,11 @@ public static Pair getSchematic(int i) { int minY = Integer.MAX_VALUE; int minZ = Integer.MAX_VALUE; HashMap subRegions = new HashMap<>(); - Level schematicWorld = SchematicWorldHandler.getSchematicWorld(); + + // doing this the normal way class loaded WorldSchematic too early + Supplier supplier = SchematicWorldHandler::getSchematicWorld; + Level schematicWorld = supplier.get(); + for (Map.Entry entry : placement.getEnabledRelativeSubRegionPlacements().entrySet()) { SubRegionPlacement subPlacement = entry.getValue(); Vec3i pos = transform(subPlacement.getPos(), placement.getMirror(), placement.getRotation()); From 2745cb1995bf747e1b5f2511b7b2f6d224850c45 Mon Sep 17 00:00:00 2001 From: pirateben820 Date: Wed, 16 Sep 2026 05:02:45 -0700 Subject: [PATCH 51/54] Compare block state property values with equals, not reference identity sameBlockstate compares each property's value with !=. The values come out of BlockState.getValues() as boxed Comparable, so this is reference identity. It is correct for enum and Boolean properties, whose values are singletons, and for an IntegerProperty it happens to work today because every vanilla integer property stays inside the -128..127 range the JVM's Integer cache interns (the largest, age, tops out at 25). Nothing in the comparison guarantees that, and identity is not what is meant here; equality is. Objects.equals to match the three other comparisons in this file. Plain equals would also be correct: both states share one StateDefinition, so the lookups cannot return null. (cherry picked from commit 4aa223d5f7632ac98e687e5c1c7dd9ccde913904) --- src/main/java/baritone/process/BuilderProcess.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/baritone/process/BuilderProcess.java b/src/main/java/baritone/process/BuilderProcess.java index ceca32538c..2053712a23 100644 --- a/src/main/java/baritone/process/BuilderProcess.java +++ b/src/main/java/baritone/process/BuilderProcess.java @@ -1037,7 +1037,7 @@ private static boolean sameBlockstate(BlockState first, BlockState second) { return first.equals(second); // early return if no properties are being ignored } for (Property prop : first.getProperties()) { - if (first.getValue(prop) != second.getValue(prop) + if (!Objects.equals(first.getValue(prop), second.getValue(prop)) && !(ignoreDirection && ORIENTATION_PROPS.contains(prop)) && !ignoredProps.contains(prop.getName())) { return false; From a98221d06ce03311e1bc91a1555e2847511dc6f5 Mon Sep 17 00:00:00 2001 From: netkek0 Date: Thu, 17 Sep 2026 18:53:13 +0100 Subject: [PATCH 52/54] Write cache and waypoint files atomically CachedRegion and WaypointCollection opened the real file for writing, which truncates it before the new contents arrive, so a save that does not finish leaves a half-written file behind. Waypoint loading catches the resulting exception and ignores it, silently dropping every waypoint of that tag, and a region load logs a stack trace and loses the cache. Write to a sibling .tmp file and move it into place instead, so an interrupted save leaves the previous file intact. The empty region file that save created up front is gone too, since it would itself be left behind as a corrupt file if a first save were cut off. (cherry picked from commit ffa180e0a687053d4f3457c908ad504607023cb6) --- src/main/java/baritone/cache/CacheFiles.java | 42 ++++++++++ .../baritone/cache/WaypointCollection.java | 9 +- .../java/baritone/cache/CacheFilesTest.java | 82 +++++++++++++++++++ 3 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 src/main/java/baritone/cache/CacheFiles.java create mode 100644 src/test/java/baritone/cache/CacheFilesTest.java diff --git a/src/main/java/baritone/cache/CacheFiles.java b/src/main/java/baritone/cache/CacheFiles.java new file mode 100644 index 0000000000..1230a5c3f6 --- /dev/null +++ b/src/main/java/baritone/cache/CacheFiles.java @@ -0,0 +1,42 @@ +/* + * 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.cache; + +import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +final class CacheFiles { + + private CacheFiles() {} + + /** + * Replaces {@code target} with a fully written {@code source} in one step where the file system allows it, so a + * save that gets cut off (a crash, or the JVM exiting while a worker thread is still writing) leaves the previous + * file intact instead of a truncated one. + */ + static void moveIntoPlace(Path source, Path target) throws IOException { + try { + Files.move(source, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.move(source, target, StandardCopyOption.REPLACE_EXISTING); + } + } +} diff --git a/src/main/java/baritone/cache/WaypointCollection.java b/src/main/java/baritone/cache/WaypointCollection.java index 222adfdf66..ae22aa8103 100644 --- a/src/main/java/baritone/cache/WaypointCollection.java +++ b/src/main/java/baritone/cache/WaypointCollection.java @@ -93,8 +93,9 @@ private synchronized void load(Waypoint.Tag tag) { private synchronized void save(Waypoint.Tag tag) { Path fileName = this.directory.resolve(tag.name().toLowerCase() + ".mp4"); + Path tempFile = fileName.resolveSibling(fileName.getFileName() + ".tmp"); try ( - FileOutputStream fileOut = new FileOutputStream(fileName.toFile()); + FileOutputStream fileOut = new FileOutputStream(tempFile.toFile()); BufferedOutputStream bufOut = new BufferedOutputStream(fileOut); DataOutputStream out = new DataOutputStream(bufOut) ) { @@ -107,6 +108,12 @@ private synchronized void save(Waypoint.Tag tag) { out.writeInt(waypoint.getLocation().getY()); out.writeInt(waypoint.getLocation().getZ()); } + } catch (IOException ex) { + ex.printStackTrace(); + return; + } + try { + CacheFiles.moveIntoPlace(tempFile, fileName); } catch (IOException ex) { ex.printStackTrace(); } diff --git a/src/test/java/baritone/cache/CacheFilesTest.java b/src/test/java/baritone/cache/CacheFilesTest.java new file mode 100644 index 0000000000..201f5f230f --- /dev/null +++ b/src/test/java/baritone/cache/CacheFilesTest.java @@ -0,0 +1,82 @@ +/* + * 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.cache; + +import baritone.api.cache.IWaypoint; +import baritone.api.cache.Waypoint; +import baritone.api.utils.BetterBlockPos; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; +import java.util.stream.Stream; + +import static org.junit.Assert.*; + +public class CacheFilesTest { + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + @Test + public void moveIntoPlaceReplacesExistingFile() throws Exception { + Path target = folder.getRoot().toPath().resolve("r.0.0.bcr"); + Path temp = folder.getRoot().toPath().resolve("r.0.0.bcr.tmp"); + Files.write(target, "old".getBytes(StandardCharsets.UTF_8)); + Files.write(temp, "new".getBytes(StandardCharsets.UTF_8)); + + CacheFiles.moveIntoPlace(temp, target); + + assertEquals("new", new String(Files.readAllBytes(target), StandardCharsets.UTF_8)); + assertFalse(Files.exists(temp)); + } + + @Test + public void moveIntoPlaceCreatesMissingFile() throws Exception { + Path target = folder.getRoot().toPath().resolve("home.mp4"); + Path temp = folder.getRoot().toPath().resolve("home.mp4.tmp"); + Files.write(temp, "new".getBytes(StandardCharsets.UTF_8)); + + CacheFiles.moveIntoPlace(temp, target); + + assertEquals("new", new String(Files.readAllBytes(target), StandardCharsets.UTF_8)); + assertFalse(Files.exists(temp)); + } + + @Test + public void waypointsSurviveSaveAndLoad() throws Exception { + Path dir = folder.getRoot().toPath().resolve("waypoints"); + Waypoint home = new Waypoint("base", IWaypoint.Tag.HOME, new BetterBlockPos(1, 64, -2), 1234L); + + new WaypointCollection(dir).addWaypoint(home); + Set loaded = new WaypointCollection(dir).getByTag(IWaypoint.Tag.HOME); + + assertEquals(1, loaded.size()); + IWaypoint waypoint = loaded.iterator().next(); + assertEquals("base", waypoint.getName()); + assertEquals(new BetterBlockPos(1, 64, -2), waypoint.getLocation()); + assertEquals(1234L, waypoint.getCreationTimestamp()); + try (Stream files = Files.list(dir)) { + assertTrue("temporary file left behind", files.noneMatch(p -> p.toString().endsWith(".tmp"))); + } + } +} From ac5625e64f5aa86a03e67924af7cd7e8573c9789 Mon Sep 17 00:00:00 2001 From: Dek Date: Sun, 8 Nov 2026 12:58:16 -0300 Subject: [PATCH 53/54] readme: the waypoint, litematica and builder fixes, and when a dirty region is evicted --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 12b0efc158..bfb62e7075 100644 --- a/README.md +++ b/README.md @@ -141,12 +141,18 @@ world stops matching the path. old one, so a reader sees either the previous region or the new one. On 26.2 this is what the nether pathfinder reads with `elytraUseCache` on, and a region it fails to parse is gone for the rest of the flight. +- Waypoint files were written the same way, in place, and a save cut off mid-write — a crash, or + the game exiting while the file was still being written — left a truncated file that the next + load silently discarded, taking every waypoint of that tag with it. They are written to a temp + file and renamed over the old one too, so an interrupted save keeps the previous waypoints. - **RAM is bounded.** Regions were only dropped from RAM after the ten minute save, and only by distance from the player, so nothing capped how much of the cache stayed resident on a long trip. A maintenance thread now saves changed regions every 30 seconds (`cachedRegionsSaveIntervalSeconds`), drops regions nothing has touched for three minutes (`cachedRegionsIdleSeconds`), and then the least recently used ones while the cache is over 192 MiB (`cachedRegionsRamBudgetMB`). A region - with unsaved changes is never dropped. A path calculation that reaches a region on disk but not + with unsaved changes is kept while they can still be saved; with `chunkCaching` off or no cache + directory nothing ever saves them, so those regions are evicted like any other instead of pinning + the whole session's terrain in RAM. A path calculation that reaches a region on disk but not in RAM loads it back in the background (`cachedRegionsLoadOnDemand`), so terrain seen in an earlier session counts again. - **Less packing.** The check meant to skip empty sections tested for `null`, which modern chunks @@ -181,6 +187,15 @@ world stops matching the path. threw when hashed without a y constraint. - `#find` searched the cache around the player's y where it meant their z. +### Building + +- `#litematica` could report Litematica as missing with it installed: the check for it pulled in one + of Litematica's world classes before the game could load it. +- With `buildIgnoreDirection` or `buildIgnoreProperties` set, the comparison between the placed + block and the schematic checked each property value by reference rather than by value. Right for + every vanilla property by luck — small integers happen to be interned — and nothing guaranteed it + stayed that way; it compares by value now. + ### Pathing thread - `ToolSet` read the live inventory from the calculation thread — a data race with the game thread — From 843427f6ae042b4c13530a66c2c50f8c26975260 Mon Sep 17 00:00:00 2001 From: Dek Date: Sun, 8 Nov 2026 12:58:16 -0300 Subject: [PATCH 54/54] drop the review notes and scripts that are not part of baritone --- docs/mod-algorithm-review-2026-09-21.md | 55 --------- .../AllocatorRegression.java | 58 ---------- .../EstimatorRegression.java | 73 ------------ scripts/mod-formula-review/README.md | 21 ---- .../reproduce_highlight_cap.py | 30 ----- scripts/mod-formula-review/test_priority.py | 43 ------- .../mod-formula-review/verify_coordinates.py | 58 ---------- scripts/mod-formula-review/verify_maps.py | 106 ------------------ .../mod-formula-review/verify_rendering.py | 26 ----- 9 files changed, 470 deletions(-) delete mode 100644 docs/mod-algorithm-review-2026-09-21.md delete mode 100644 scripts/mod-formula-review/AllocatorRegression.java delete mode 100644 scripts/mod-formula-review/EstimatorRegression.java delete mode 100644 scripts/mod-formula-review/README.md delete mode 100644 scripts/mod-formula-review/reproduce_highlight_cap.py delete mode 100644 scripts/mod-formula-review/test_priority.py delete mode 100644 scripts/mod-formula-review/verify_coordinates.py delete mode 100644 scripts/mod-formula-review/verify_maps.py delete mode 100644 scripts/mod-formula-review/verify_rendering.py diff --git a/docs/mod-algorithm-review-2026-09-21.md b/docs/mod-algorithm-review-2026-09-21.md deleted file mode 100644 index bff82cc450..0000000000 --- a/docs/mod-algorithm-review-2026-09-21.md +++ /dev/null @@ -1,55 +0,0 @@ -# Stash-hunting mod algorithm review — 2026-09-21 - -This is a focused source audit of the local Minecraft 26.2 forks: Baritone, XaeroWorldMap, XaeroPlus, XaeroMinimap, Voxy and Sodium. Priority was long-distance navigation, map coordinates/history, detection logic and rendering during travel. It is not an exhaustive audit of every algorithm. Existing local changes were preserved. - -## Applied fixes - -| Component | Confirmed failure | Applied correction | -| --- | --- | --- | -| Baritone `FindCommand` | `#find` passed the player's Y as the cache search's Z center. At Z=1,000,000 and Y=64, it searched around Z=64. | Pass `origin.z`; cached-block searches now center on the player's horizontal position. | -| Baritone `GoalNear` | Squared `int` distances overflow: a point 65,536 blocks away can be accepted by a two-block goal. The radius square can overflow too. | Widen coordinates before subtraction/multiplication and store the squared radius as `long`. | -| Baritone `GoalRunAway` | Distance squares overflow, and casting the requested squared radius to `int` clips large distances and truncates fractional ones. Hashing a goal without a Y constraint also throws. | Preserve the squared radius as `double`, widen coordinate arithmetic and hash nullable Y safely. | -| XaeroPlus chunk/region conversions | Casting a negative fractional coordinate before shifting selects the wrong cell: -0.5 becomes chunk 0 instead of -1. This also affects dimension-projected map centers. | Floor before shifting by 4 or 9. | -| XaeroPlus map cursor and Baritone integration | Overworld→Nether conversion truncates toward zero: -1/8 becomes block 0 instead of -1. | Floor the scaled coordinates consistently for cursor text and both Baritone goal constructors. | -| XaeroWorldMap region sorting, via XaeroPlus | Three distance formulas square integers. A 50,000-region delta or 46,400-chunk delta can overflow into a computed distance of zero, prioritizing distant data incorrectly. | Existing `MixinLeveledRegion` now widens shifts/subtractions and squares doubles in all three methods. | -| XaeroPlus highlight deletion | When no tile selection exists, right-click block coordinates are treated as chunk coordinates. | Convert the fallback coordinates to chunks. Normal selection behavior is unchanged; this is a less common fallback-path fix. | -| Sodium task priority | The close-distance penalty uses `max(distance, 50)` although it should saturate at 50. It penalizes all nearby chunks equally and grows without the intended cap. | Use `min`; close-distance contribution is now 0, 0.3, 0.6, 0.6 at 0, 25, 50 and 512 blocks. Other priority weights are preserved. | -| Sodium duration estimation | A large first batch can leave the shared estimator permanently untrained; constant-size samples can produce zero duration. Old covariance/variance are merged around the wrong mean. | Correct warmup/sample accounting, use the observed mean when variance is zero, and recenter historical moments before merging. Bulk job completion can trigger the large-first-batch case; uploads also benefit from the constant-input and recentering fixes. | -| Voxy contiguous node allocation | With capacity 16, freeing alternating slots and requesting two contiguous slots returns [16,18), outside capacity. Exact fits are rejected, and the mask for 64 slots is zero. | Bound hierarchy searches and candidate ranges, permit exact fits, handle a 64-bit mask, and reject invalid counts. | - -WorldMap is supplied as reference/decompiled source here, so its fix lives in the buildable XaeroPlus integration. No separate Minimap arithmetic defect met the same evidence threshold in the waypoint/distance paths reviewed; no direct Minimap patch was made. - -## Validation - -- Baritone: full offline Java 25 build passed, including 58 JUnit tests; six new goal tests cover overflow, fractional/large radii, multiple origins, height constraints and hashing. Four of those tests failed on the original source before the fixes. -- XaeroPlus: full offline build passed for Fabric and NeoForge; both new Fabric client regression classes compiled. Coordinate harness passed 147,560 assertions. Region-distance harness passed 31,158 comparisons against an independent `hypot`/scaled-coordinate calculation; dimension conversion passed 70 checks. -- Sodium and Voxy: full offline builds passed. Direct production-class regressions cover estimator warmup, constant inputs, deferred batches and shifted means against independently computed weighted least squares. Allocator tests include 80,000 deterministic randomized operations against a boolean-array oracle, exact fits, 64-slot allocations and exhaustion at the 24-bit boundary. Priority method checks cover distance, visibility and task age. -- Runtime Xaero client validation: the default runner could not launch because `xvfb-run` is absent. A temporary Gradle override used the available desktop display. Both new tests completed before the runner entered the existing `XaeroPlusRegionIndexGameTest`; that test timed out starting its temporary dedicated server, whose log says its EULA was not accepted. Thus the new coordinate and applied WorldMap mixin checks ran in Minecraft, while the complete integration suite did not pass. No EULA or repository test-runner setting was changed. Log: `/tmp/xaeroplus-formula-display-test.log`. -- These checks establish arithmetic/build correctness, not improved FPS or detection accuracy on 2b2t. No production profile or stash database was modified. - -Reproducible numerical checks are retained in [scripts/mod-formula-review](../scripts/mod-formula-review/README.md). Baritone tests are in [GoalDistanceTest.java](../src/test/java/baritone/pathing/goals/GoalDistanceTest.java); Xaero tests are in its sibling `fabric/src/gametest` source set. - -## Remaining findings, in priority order - -1. **XaeroPlus highlight cap can hide history after zoom-in.** `ChunkHighlightDatabase` caps queries at 3,000,000 rows, but `ChunkHighlightCacheDimensionHandler` subsequently excludes the entire previous rectangle as if it had loaded completely. A synthetic database of 3,000,001 entries reproduces a missing highlight that remains absent after zooming inward. A fully populated radius-28 window can exceed the limit; the default radius 24 fits below it. Carry complete/truncated status with results and reload incomplete views without excluding them. Keep dirty entries separately when replacing persisted snapshots, and enforce a total resident budget. Removing the SQL cap alone restores the memory problem. The retained SQLite reproduction uses only an in-memory synthetic database. -2. **XaeroPlus portal scans can outlive reset/cancellation.** `PortalSkipDetection` cancels futures without checking interruption inside its loops, shares mutable scan buffers, and queues results without a world/dimension generation check. Old work can overwrite a newer scan after switching worlds/dimensions or disabling the module. Use per-task buffers, cancellation checks and a generation token checked before publishing on the client thread. This needs controlled concurrency tests before changing cache lifecycle behavior. -3. **WorldMap right-click distance mixes dimensions.** The inspected `GuiMap`→`DistanceUtils` path compares displayed-map coordinates with raw player coordinates. An Overworld player at X=8000 looking at Nether X=1000 can see a misleading 7000m. Convert the target into the player's coordinate scale before measuring. This is separate from the fixed cursor and Baritone target rounding. -4. **WorldMap skinny PNG exports can produce a zero dimension.** `PNGExporter` truncates scaled output dimensions and then divides by output height. The arithmetic reproduces a zero height for very wide, one-chunk-high selections. Clamp both dimensions to at least one and validate pixel budgets with widened arithmetic before allocating. -5. **Baritone ETA calculation still enumerates large goal volumes.** The no-argument `GoalNear.heuristic()` and `GoalRunAway.heuristic()` have cubic scans. Large-radius membership is now correct, but that does not make large-radius ETA calculation cheap. Replace enumeration with a validated analytical bound or separately defined ETA estimate. Current widened membership arithmetic is validated for normal Minecraft world coordinates, not every arbitrary integer triple. - -## Detection improvements requiring data - -`PaletteNewChunks` misses unused trailing entries in a linear palette, although it detects mismatched encounter order. Recognizing those entries is a plausible consistency improvement, but both generation and later edits can leave unused palette entries. Validate against labeled saved chunk packets before changing the classification rule or its three-section threshold. - -`OldChunks` and `LiquidNewChunks` infer history from terrain/block content and update patterns; neither establishes generation age with certainty. In particular, the liquid detector accepts an adjacent source without matching its fluid type to the updated flow. A same-fluid check is worth testing against captured updates. No arbitrary scoring weights or detection thresholds were changed. - -Portal scanning is a separate performance opportunity: an eligibility grid plus summed-area or sliding-window counts can identify the same all-eligible squares without repeatedly checking every member. Test equivalence on randomized masks, dimension boundaries and radius 0/1 before replacing the current algorithm. - -## Built artifacts - -- Baritone: `fabric/build/libs/baritone-api-fabric-26.2-SNAPSHOT.jar` -- XaeroPlus: `../xaeroplus/fabric/build/libs/XaeroPlus-2.35.3+fabric-26.2-WM1.45.0-MM26.4.2.jar` -- Sodium: `../sodium/build/mods/sodium-fabric-0.9.2-SNAPSHOT+mc26.2-local.jar` -- Voxy: `../voxy/build/libs/voxy-0.2.19-beta-337b919.jar` - -These are build outputs. Installation into the active game profile was outside this review. diff --git a/scripts/mod-formula-review/AllocatorRegression.java b/scripts/mod-formula-review/AllocatorRegression.java deleted file mode 100644 index 826fb3387d..0000000000 --- a/scripts/mod-formula-review/AllocatorRegression.java +++ /dev/null @@ -1,58 +0,0 @@ -import java.util.Random; -import me.cortex.voxy.common.util.HierarchicalBitSet; - -public class AllocatorRegression { - private static void check(boolean ok, String message) { - if (!ok) throw new AssertionError(message); - } - private static int firstRun(boolean[] used, int count) { - for (int i = 0; i <= used.length - count; i++) { - int j = 0; - while (j < count && !used[i + j]) j++; - if (j == count) return i; - } - return -1; - } - public static void main(String[] args) { - var fragmented = new HierarchicalBitSet(16); - for (int i = 0; i < 16; i++) fragmented.allocateNext(); - for (int i = 0; i < 16; i += 2) fragmented.free(i); - check(fragmented.allocateNextConsecutiveCounted(2) < 0, "fragmentation must not allocate beyond limit"); - check(fragmented.getCount() == 8, "failed allocation must preserve count"); - var exact = new HierarchicalBitSet(16); - check(exact.allocateNextConsecutiveCounted(16) == 0, "exact capacity must be usable"); - var fullWord = new HierarchicalBitSet(128); - fullWord.allocateNext(); fullWord.allocateNext(); fullWord.free(0); - check(fullWord.allocateNextConsecutiveCounted(64) == 2, "64-bit run must not overlap allocated ID 1"); - for (int limit : new int[] {1, 16, 63, 64, 65, 127, 128, 1024}) { - var used = new boolean[limit]; - var allocator = new HierarchicalBitSet(limit); - var random = new Random(limit); - for (int t = 0; t < 10000; t++) { - if (random.nextBoolean()) { - int id = random.nextInt(limit); - check(allocator.free(id) == used[id], "free state mismatch"); - used[id] = false; - } else { - int count = random.nextInt(64) + 1; - int expected = firstRun(used, count); - int actual = allocator.allocateNextConsecutiveCounted(count); - check(expected < 0 ? actual < 0 : expected == actual, "first-fit mismatch: limit=" + limit + " count=" + count + " expected=" + expected + " actual=" + actual); - if (actual >= 0) for (int j = 0; j < count; j++) used[actual + j] = true; - } - int usedCount = 0; - for (int i = 0; i < limit; i++) { - check(allocator.isSet(i) == used[i], "occupancy mismatch"); - if (used[i]) usedCount++; - } - check(allocator.getCount() == usedCount, "count mismatch"); - } - } - // Last free slot in the full 24-bit hierarchy cannot satisfy a two-slot run. - var edge = new HierarchicalBitSet(); - for (int i = 0; i < edge.getLimit(); i++) edge.allocateNext(); - edge.free(edge.getLimit() - 1); edge.free(0); edge.free(2); - check(edge.allocateNextConsecutiveCounted(2) < 0, "full hierarchy exhaustion must not access a nonexistent level"); - System.out.println("PASS Voxy allocator: fragmented limits, exact fit, 64-slot runs, 80,000 randomized operations, full 24-bit boundary"); - } -} diff --git a/scripts/mod-formula-review/EstimatorRegression.java b/scripts/mod-formula-review/EstimatorRegression.java deleted file mode 100644 index cda38609ef..0000000000 --- a/scripts/mod-formula-review/EstimatorRegression.java +++ /dev/null @@ -1,73 +0,0 @@ -import java.util.ArrayList; -import java.util.List; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.UploadDuration; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.UploadDurationEstimator; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.ExpDecayLinear2DEstimator; -import java.util.HashMap; -import java.util.Map; - -public class EstimatorRegression { - private static void close(long actual, long expected, String label) { - if (Math.abs(actual - expected) > 1) throw new AssertionError(label + ": expected " + expected + ", got " + actual); - } - private static class SmallEstimator extends ExpDecayLinear2DEstimator { - SmallEstimator() { super(0.05, 100, 40, 12345L); } - @Override protected Map createMap() { return new HashMap<>(); } - } - private record Sample(long x, long y, double weight) {} - private static long reference(List points, long input) { - double total=0, meanX=0, meanY=0; - for (var p:points) { total+=p.weight; meanX+=p.weight*p.x; meanY+=p.weight*p.y; } - meanX/=total; meanY/=total; - double covariance=0, variance=0; - for (var p:points) { covariance+=p.weight*(p.x-meanX)*(p.y-meanY); variance+=p.weight*(p.x-meanX)*(p.x-meanX); } - double slope=variance==0?0:Math.max(0,covariance/variance); - return (long)(meanY+slope*(input-meanX)); - } - public static void main(String[] args) { - var varying=new UploadDurationEstimator(); - for(int batch=0;batch<3;batch++) { - for(int i=0;i<101;i++) varying.addData(new UploadDuration(1_000_000L+i*10_000L,1000L+i*10L)); - varying.updateModels(); - close(varying.estimateUploadDuration(1000L),1_000_000L,"oversized first batch "+batch); - } - var constant=new UploadDurationEstimator(); - for(int i=0;i<100;i++) constant.addData(new UploadDuration(1_000_000L,1000L)); - constant.updateModels(); - close(constant.estimateUploadDuration(1000L),1_000_000L,"constant-X initial batch"); - for(int i=0;i<100;i++) constant.addData(new UploadDuration(2_000_000L,2000L)); - constant.updateModels(); - close(constant.estimateUploadDuration(1500L),1_500_000L,"different constant-X batch establishes slope"); - - var warming=new SmallEstimator(); - var points=new ArrayList(); - for(int batch=0;batch<3;batch++) { - for(int i=0;i<40;i++) { - long x=1000L+batch*100L+i; - long y=500_000L+10L*x+batch*10000L; - warming.addData(new UploadDuration(y,x)); points.add(new Sample(x,y,1)); - } - warming.updateModels(); - close(warming.predict(null,1100L),reference(points,1100L),"warmup and target crossing batch "+batch); - } - var shifted=new UploadDurationEstimator(); - var shiftedPoints=new ArrayList(); - for(int batch=0;batch<2;batch++) { - for(int i=0;i<100;i++) { - long x=1000L+batch*1000L+(i%2)*10L; - long y=500_000L+100L*x+batch*250_000L; - shifted.addData(new UploadDuration(y,x)); - shiftedPoints.add(new Sample(x,y,batch==0?19:1)); - } - shifted.updateModels(); - } - for(long input:new long[]{1000,1500,2000}) close(shifted.estimateUploadDuration(input),reference(shiftedPoints,input),"shifted means at "+input); - - var deferred=new UploadDurationEstimator(); - for(int i=0;i<99;i++) deferred.addData(new UploadDuration(1_000_000L,1000L)); - deferred.updateModels(); close(deferred.estimateUploadDuration(1000),100_000L,"retain fallback while below batch minimum"); - deferred.addData(new UploadDuration(1_000_000L,1000L)); deferred.updateModels(); - close(deferred.estimateUploadDuration(1000),1_000_000L,"deferred samples retained"); - System.out.println("PASS Sodium estimator: >100 first batch, constant-X, warmup/target crossing, shifted-mean weighted least-squares reference, deferred samples"); - } -} diff --git a/scripts/mod-formula-review/README.md b/scripts/mod-formula-review/README.md deleted file mode 100644 index 32bec82eb3..0000000000 --- a/scripts/mod-formula-review/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Cross-mod arithmetic regressions - -Run from the Baritone repository, with XaeroPlus, Sodium, Voxy and the Xaero 26.2 reference sources in sibling directories. These scripts require the local Java 25 installation. The rendering harness uses the FastUtil 8.5.18 and JSpecify 1.0.0 jars already in the Gradle cache. - -```sh -python3 scripts/mod-formula-review/verify_coordinates.py ../xaeroplus/common/src/main/java/xaeroplus/util/ChunkUtils.java -python3 scripts/mod-formula-review/verify_maps.py -python3 scripts/mod-formula-review/verify_rendering.py -``` - -The coordinate, map and priority harnesses extract methods from the working source. The estimator and allocator harnesses compile the production classes directly. Independent grid-containment, weighted least-squares and boolean-array checks supply the expected results. Temporary compilation files are removed automatically. These checks do not initialize Minecraft or prove runtime mixin compatibility. - -Baritone's six goal regressions run with `./gradlew test` under Java 25. Persistent Xaero client tests are in its `fabric/src/gametest` source set: `XaeroPlusCoordinateGameTest` and `XaeroPlusRegionDistanceGameTest`. - -To reproduce the unresolved highlight-cap issue, run: - -```sh -python3 scripts/mod-formula-review/reproduce_highlight_cap.py -``` - -That script creates an in-memory SQLite database with 3,000,001 synthetic rows. It never opens or changes real map databases. Its expected output is `CONFIRMED`, demonstrating omitted highlights after zooming into a truncated window. diff --git a/scripts/mod-formula-review/reproduce_highlight_cap.py b/scripts/mod-formula-review/reproduce_highlight_cap.py deleted file mode 100644 index 6d2b606d00..0000000000 --- a/scripts/mod-formula-review/reproduce_highlight_cap.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -"""SQLite regression model of the actual row cap and incremental exclusion.""" -import re -import sqlite3 -from pathlib import Path - -root = Path(__file__).resolve().parents[3] -source = (root / 'xaeroplus/common/src/main/java/xaeroplus/feature/highlights/ChunkHighlightDatabase.java').read_text() -cap = int(re.search(r'MAX_ROWS_PER_LOAD = ([\d_]+)', source).group(1).replace('_', '')) -db = sqlite3.connect(':memory:') -db.execute('CREATE TABLE highlights (x INTEGER, z INTEGER, foundTime INTEGER, PRIMARY KEY (x, z)) WITHOUT ROWID') -db.executemany('INSERT INTO highlights VALUES (?, ?, 1)', ((i // 2000 - 1000, i % 2000 - 1000) for i in range(cap + 1))) -# Exactly the existing region-bound arithmetic, for a 32-region-radius window. -previous = (-1024, 1024, -1024, 1024) -query = 'SELECT x,z FROM highlights WHERE x BETWEEN ? AND ? AND z BETWEEN ? AND ? LIMIT ?' -loaded = db.execute('SELECT count(*) FROM (' + query + ')', (*previous, cap)).fetchone()[0] -omitted = db.execute('SELECT x,z FROM highlights ORDER BY x,z LIMIT 1 OFFSET ?', (cap,)).fetchone() -assert loaded == cap -assert omitted is not None -assert db.execute(query, (*previous, cap)).fetchone() != omitted -# Shrink the view around the row the cap omitted; new rectangle is entirely inside old. -cx, cz = omitted[0] >> 5, omitted[1] >> 5 -current = ((cx - 1) << 5, (cx + 1) << 5, (cz - 1) << 5, (cz + 1) << 5) -incremental_query = 'SELECT x,z FROM highlights WHERE x BETWEEN ? AND ? AND z BETWEEN ? AND ? AND NOT (x BETWEEN ? AND ? AND z BETWEEN ? AND ?) LIMIT ?' -incremental = db.execute(incremental_query, (*current, *previous, cap)).fetchall() -full = db.execute(query, (*current, cap)).fetchall() -assert omitted in full -assert omitted not in incremental -assert not incremental -print(f'CONFIRMED: loaded {loaded:,} of {cap + 1:,} rows; omitted {omitted}; zoom-in fetched {len(incremental)} rows, while a complete query finds {len(full)} including the omitted row.') diff --git a/scripts/mod-formula-review/test_priority.py b/scripts/mod-formula-review/test_priority.py deleted file mode 100644 index 9ab8dabaa4..0000000000 --- a/scripts/mod-formula-review/test_priority.py +++ /dev/null @@ -1,43 +0,0 @@ -from pathlib import Path -import re, subprocess, sys, tempfile -source=Path(sys.argv[1]).read_text() -start=source.index(' private float getSectionPriority(') -pos=source.index('{', start) -depth=1 -end=pos+1 -while depth: - if source[end]=='{': depth+=1 - if source[end]=='}': depth-=1 - end+=1 -method=source[start:end] -constants='\n'.join(re.findall(r' static final float .*?;',source)) -code='''public class PriorityRegression { -'''+constants+''' - private float invMaxDistance = 3.0f / 512.0f; - private long creationTime = 5_000_000_000L; - private int cameraX, cameraY, cameraZ; - private record RenderSection(int getCenterX, int getCenterY, int getCenterZ, long getPendingUpdateSince) {} - private static class ChunkUpdateTypes { static float getPriorityValue(int type) { return type; } } -'''+method+''' - private static void close(float actual, float expected) { - if (Math.abs(actual - expected) > 0.00001f) throw new AssertionError("expected " + expected + ", got " + actual); - } - public static void main(String[] args) { - var test = new PriorityRegression(); - for (int distance : new int[] {0, 25, 50, 100, 512}) { - float expected = 3.0f * distance / 512.0f + (distance < 50 ? 0.6f * distance / 50.0f : 0.6f); - close(test.getSectionPriority(new RenderSection(distance, 0, 0, test.creationTime), 0, false), expected); - } - var distant = new RenderSection(512, 0, 0, test.creationTime); - var near = new RenderSection(100, 0, 0, test.creationTime); - if (test.getSectionPriority(distant, 0, true) >= test.getSectionPriority(near, 0, false)) throw new AssertionError("bounded proximity must preserve the visible-chunk bias"); - close(test.getSectionPriority(distant, 0, true), test.getSectionPriority(distant, 0, false)-3.0f); - close(test.getSectionPriority(new RenderSection(512, 0, 0, 0), 0, false), test.getSectionPriority(distant, 0, false)-1.0f); - System.out.println("PASS Sodium priority: 0/25/50/100/512-block distances, visible-task ordering, waiting-time weight"); - } -} -''' -with tempfile.TemporaryDirectory(prefix='sodium-priority-') as tmp: - p=Path(tmp)/'PriorityRegression.java';p.write_text(code) - subprocess.run(['/usr/lib/jvm/java-25-openjdk/bin/javac','-d',tmp,str(p)],check=True) - subprocess.run(['/usr/lib/jvm/java-25-openjdk/bin/java','-cp',tmp,'PriorityRegression'],check=True) diff --git a/scripts/mod-formula-review/verify_coordinates.py b/scripts/mod-formula-review/verify_coordinates.py deleted file mode 100644 index fa02f29847..0000000000 --- a/scripts/mod-formula-review/verify_coordinates.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python3 -"""Run the actual two source method bodies in an isolated Java boundary harness.""" -from pathlib import Path -import re -import subprocess -import sys -import tempfile - -source = Path(sys.argv[1]).read_text() -methods = [] -for name in ('coordToChunkCoord', 'coordToRegionCoord'): - match = re.search(r'public static int ' + name + r'\(final double coord\)\s*\{[^}]*\}', source) - if not match: - raise SystemExit(f'Method not found: {name}') - methods.append(match.group()) - -java = ''' -public class CoordinateRegression { -METHODS - static int checks = 0; - static void check(double x) { - int actualChunk = coordToChunkCoord(x); - int actualRegion = coordToRegionCoord(x); - int expectedChunk = (int) Math.floor(x / 16.0); - int expectedRegion = (int) Math.floor(x / 512.0); - checks += 2; - if (actualChunk != expectedChunk || actualRegion != expectedRegion) { - throw new AssertionError("coordinate " + x + ": chunk=" + actualChunk - + " expected=" + expectedChunk + ", region=" + actualRegion - + " expected=" + expectedRegion); - } - } - public static void main(String[] args) { - double[] offsets = {-1, -0.999, -0.5, -0.001, 0, 0.001, 0.5, 0.999, 1}; - for (int boundary = -2048; boundary <= 2048; boundary++) { - for (double offset : offsets) { - check(boundary * 16.0 + offset); - check(boundary * 512.0 + offset); - } - } - for (double edge : new double[] {-29999984.0, 29999984.0}) { - for (double offset : offsets) check(edge + offset); - } - for (double coordinate : new double[] {-4096.25, -128.5, -4, -0.125, 0, 4, 128.5, 4096.25}) { - check(coordinate / 8.0); - check(coordinate * 8.0); - } - System.out.println("PASS: " + checks + " chunk/region boundary assertions"); - } -} -'''.replace('METHODS', '\n'.join(methods)) - -with tempfile.TemporaryDirectory(prefix='xaeroplus-coordinate-regression-') as temporary: - java_path = Path(temporary) / 'CoordinateRegression.java' - java_path.write_text(java) - subprocess.run(['/usr/lib/jvm/java-25-openjdk/bin/javac', str(java_path)], check=True) - result = subprocess.run(['/usr/lib/jvm/java-25-openjdk/bin/java', '-cp', temporary, 'CoordinateRegression']) - raise SystemExit(result.returncode) diff --git a/scripts/mod-formula-review/verify_maps.py b/scripts/mod-formula-review/verify_maps.py deleted file mode 100644 index 0cbf7782f8..0000000000 --- a/scripts/mod-formula-review/verify_maps.py +++ /dev/null @@ -1,106 +0,0 @@ -from pathlib import Path -import subprocess -import re -import tempfile -temporary = tempfile.TemporaryDirectory(prefix='xaero-distance-regression-') -out = Path(temporary.name) -root = Path(__file__).resolve().parents[3] -source = (root / 'XaeroDecompiled-26.2/worldmap/xaero/map/region/LeveledRegion.java').read_text() -fixed = (root / 'xaeroplus/common/src/main/java/xaeroplus/mixin/client/MixinLeveledRegion.java').read_text() -def method(text, name): - pattern = r'(?:protected int|private void) ' + re.escape(name) + r'\([^)]*\) \{' - found = re.search(pattern, text) - assert found, name - start = found.start() - pos = found.end() - depth = 1 - while depth: - if text[pos] == '{': depth += 1 - elif text[pos] == '}': depth -= 1 - pos += 1 - return text[start:pos] -java = '''import java.util.Random; -public class RegionDistanceRegression { - int regionX, regionZ, level; - static int comparisonX, comparisonZ, comparisonLevel, comparisonLeafX, comparisonLeafZ; - static int checks, baselineFailures; - static class CallbackInfoReturnable { - T value; - void setReturnValue(T v) { value = v; } - } -''' -for name in ['distanceFromPlayer', 'leafDistanceFromPlayer', 'chunkDistanceFromPlayer']: - java += method(source, name) + '\n' - java += method(fixed, 'xaeroplus$' + name) + '\n' -java += ''' - static void verify(RegionDistanceRegression region, int x, int z, int lv, int cx, int cz, int cl, int lx, int lz) { - region.regionX = x; region.regionZ = z; region.level = lv; - comparisonX = cx; comparisonZ = cz; comparisonLevel = cl; - comparisonLeafX = lx; comparisonLeafZ = lz; - CallbackInfoReturnable result = new CallbackInfoReturnable<>(); - region.xaeroplus$distanceFromPlayer(result); - check("region", result.value, - (int) Math.hypot(Math.floor(Math.scalb((double) x, lv - cl)) - cx, - Math.floor(Math.scalb((double) z, lv - cl)) - cz), region.distanceFromPlayer()); - region.xaeroplus$leafDistanceFromPlayer(result); - check("leaf", result.value, - (int) Math.hypot(Math.scalb((double) x, lv) - lx, Math.scalb((double) z, lv) - lz), region.leafDistanceFromPlayer()); - region.xaeroplus$chunkDistanceFromPlayer(result); - check("chunk", result.value, - (int) Math.hypot(Math.scalb((double) x, lv + 5) - cx, Math.scalb((double) z, lv + 5) - cz), region.chunkDistanceFromPlayer()); - } - static void check(String kind, int actual, int expected, int baseline) { - if (actual != expected) throw new AssertionError(kind + ": " + actual + " != " + expected); - checks++; - if (baseline != expected) baselineFailures++; - } - public static void main(String[] args) { - RegionDistanceRegression r = new RegionDistanceRegression(); - verify(r, 50000, 0, 0, 0, 0, 0, 0, 0); - System.out.println("25.6M-block separation, legacy leaf distance=" + r.leafDistanceFromPlayer() + ", fixed=50000 regions"); - verify(r, 1450, 0, 0, 0, 0, 0, 0, 0); - System.out.println("742400-block separation, legacy chunk distance=" + r.chunkDistanceFromPlayer() + ", fixed=46400 chunks"); - // Axis boundary, diagonal overflow, negative coordinate shifts, opposite borders. - for (int v : new int[]{0, 1, -1, 46340, 46341, -46341, 50000, -50000, 58593, -58594, Integer.MAX_VALUE, Integer.MIN_VALUE}) { - for (int lv = 0; lv <= 3; lv++) { - for (int cl = 0; cl <= 3; cl++) { - verify(r, v, -v, lv, 0, 0, cl, 0, 0); - verify(r, v, v, lv, 58593, -58594, cl, -58594, 58593); - } - } - } - Random random = new Random(0x2B2); - for (int i = 0; i < 10000; i++) { - int lv = random.nextInt(4), cl = random.nextInt(4); - verify(r, random.nextInt(117188) - 58594, random.nextInt(117188) - 58594, lv, - random.nextInt(3750000) - 1875000, random.nextInt(3750000) - 1875000, cl, - random.nextInt(117188) - 58594, random.nextInt(117188) - 58594); - } - System.out.println("Passed " + checks + " source-extracted handler checks; legacy formula failures=" + baselineFailures); - } -} -''' -(out / 'RegionDistanceRegression.java').write_text(java) -subprocess.run(['/usr/lib/jvm/java-25-openjdk/bin/javac', '-d', str(out), str(out / 'RegionDistanceRegression.java')], check=True) -subprocess.run(['/usr/lib/jvm/java-25-openjdk/bin/java', '-cp', str(out), 'RegionDistanceRegression'], check=True) -# Use expressions extracted from the proposed edits, not copies of the replacement formula. -baritone = (root / 'xaeroplus/common/src/main/java/xaeroplus/util/BaritoneExecutor.java').read_text() -gui = (root / 'xaeroplus/common/src/main/java/xaeroplus/mixin/client/MixinGuiMap.java').read_text() -expressions = [re.search(r'int goalX = (.*);', baritone).group(1), re.search(r'int x = (.*mouseBlockPosX.*);', gui).group(1)] -java = '''public class DimensionFloorRegression { -static int baritone(int x, double customDimDiv) { return %s; } -static int cursor(int mouseBlockPosX, double dimDiv) { return %s; } -public static void main(String[] args) { - int[] input = {-30000000, -17, -16, -9, -8, -7, -1, 0, 1, 7, 8, 9, 17, 30000000}; - for (int x : input) { - int expected = Math.floorDiv(x, 8); - if (baritone(x, .125) != expected || cursor(x, 8) != expected) throw new AssertionError("divide " + x); - if (baritone(x, 8) != x * 8 || cursor(x, .125) != x * 8) throw new AssertionError("multiply " + x); - if (baritone(x, 1) != x) throw new AssertionError("same dimension " + x); - } - System.out.println("Passed 70 dimension-conversion boundary checks; -1 OW block maps to -1 Nether block"); -}} -''' % tuple(expressions) -(out / 'DimensionFloorRegression.java').write_text(java) -subprocess.run(['/usr/lib/jvm/java-25-openjdk/bin/javac', '-d', str(out), str(out / 'DimensionFloorRegression.java')], check=True) -subprocess.run(['/usr/lib/jvm/java-25-openjdk/bin/java', '-cp', str(out), 'DimensionFloorRegression'], check=True) diff --git a/scripts/mod-formula-review/verify_rendering.py b/scripts/mod-formula-review/verify_rendering.py deleted file mode 100644 index a9cf6f5adf..0000000000 --- a/scripts/mod-formula-review/verify_rendering.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Compile and exercise the sibling Sodium/Voxy production algorithms.""" -from pathlib import Path -import os -import subprocess -import tempfile - -work = Path(__file__).resolve().parent -root = work.parents[2] -gradle_home = Path(os.environ.get('GRADLE_USER_HOME', Path.home() / '.gradle')) -jar_root = gradle_home / 'caches/modules-2/files-2.1' -classpath = os.pathsep.join(str(next((jar_root / dependency).rglob('*.jar'))) for dependency in ( - 'it.unimi.dsi/fastutil/8.5.18', 'org.jspecify/jspecify/1.0.0')) -java = Path('/usr/lib/jvm/java-25-openjdk/bin') -sodium = root / 'sodium/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk' -subprocess.run(['python3', str(work / 'test_priority.py'), - str(sodium / 'lists/TaskCollectingTree.java')], check=True) -with tempfile.TemporaryDirectory(prefix='rendering-formulas-') as temporary: - sources = [sodium / 'compile/estimation' / (name + '.java') for name in ( - 'Estimator', 'Abstract2DLinearEstimator', 'UploadDuration', - 'UploadDurationEstimator', 'ExpDecayLinear2DEstimator')] - sources.extend((root / 'voxy/src/main/java/me/cortex/voxy/common/util/HierarchicalBitSet.java', - work / 'AllocatorRegression.java', work / 'EstimatorRegression.java')) - subprocess.run([str(java / 'javac'), '-cp', classpath, '-d', temporary, - *map(str, sources)], check=True) - for test in ('AllocatorRegression', 'EstimatorRegression'): - subprocess.run([str(java / 'java'), '-cp', temporary + os.pathsep + classpath, test], check=True)