Repository navigation
Conversation
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.
As of mc 26.2 the game awaits non-daemon threads on stop and crash reports if any are still alive.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 <mc>-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.
PR cabaletta#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 cabaletta#5058 set out to fix. Reported-by: ZacSharp Co-Authored-By: Claude <noreply@anthropic.com>
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
PR cabaletta#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 cabaletta#5058 set out to fix. Reported-by: ZacSharp Co-Authored-By: Claude <noreply@anthropic.com>
# Conflicts: # gradle.properties
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.
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.
(cherry picked from commit 5f73685)
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 4aa223d)
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 ffa180e)
…region is evicted
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
cabaletta's
26.3branch (cabaletta#5130) with the Meteor changes commit cherry-picked on top, same layout as the26.2and26.1branches here. The only conflict wasmod_version, now26.3-SNAPSHOT.Opened against
26.2because there is no26.3branch in this repo yet; feel free to retarget once it exists.Built with
./gradlew build, published to mavenLocal and tested in a 26.3 dev client together with the meteor-client 26.3 update (PR follows): Baritone loads, the mixin audit passes, and it initialises in a world and saves its cache.