Skip to content

Ahoy! Baritone can now swim and use boats - #5127

Open
AverWasTaken wants to merge 3 commits into
cabaletta:1.21.4from
AverWasTaken:water-improvements
Open

AverWasTaken wants to merge 3 commits into
cabaletta:1.21.4from
AverWasTaken:water-improvements

Conversation

@AverWasTaken

@AverWasTaken AverWasTaken commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Ahoy! Baritone can now swim and use boats

Branch: water-improvements -> 1.21.4


What this PR does

Two optional ways to actually cross water instead of bobbing along the bottom of it: allowSwimming sprint swims along the waterline with the head out, and allowBoats places a boat from the inventory, rows the path, and takes the boat with it on the far side. Both off by default, both priced into the planner so it picks water routes when they're faster.

Why it should be merged

Baritone treats a lake as nine ticks a block of wading, so it walks around anything bigger than a pond. Swimming brings that to about seven and a boat to under three, and the search gets to use that. Everything is behind settings that default to off, the cost side is a min() in the calculation context, and the boat execution is a self contained state machine the executor hands the tick to, so with the settings off nothing about pathing changes and with them on it's one class to look at if something looks odd.

What changed

Swimming first. Vanilla only enters the swim state with the eye underwater and actively cancels sprint at the surface, which is exactly where the old jump bob kept us, so sprint swimming never engaged on its own. Then even once it did, cutting the jump key at a depth band launched the player out of the water and porpoised. And boats: a boat's velocity keeps nine tenths of itself every tick, so anything that steers on heading error overcorrects and hunts, which took a few evenings of rowing into banks to properly appreciate.

  • allowSwimming: sprint is what keeps the swim state alive, so Movement.update() forces it while in water and a small redirect in updateSwimming latches the state without waiting for gravity. Vertical control is pitch, not jump: with jump held vanilla settles at a vertical speed set purely by look pitch, so a PD on feet height turns into a pitch and the player sits 0.15 under the surface with the head out and the body in. Simulated against the exact update equations first, no overshoot from any depth, settles within a second. Shallow water crawls along the bottom until air runs low, then stands up to breathe. Corners are cut early because water momentum coasts half a block past a bend. SWIM_ONE_BLOCK_COST = 20/2.86.
  • Mining from a swim didn't work at all: the swim look mode returned SERVER for everything so the crosshair never landed on the block, and vanilla mines 5x slower off the ground and 5x again with the eye under. Block interacts get CLIENT like the normal freeLook branch, and shouldSwim() is false while PREPPING so we stand up to mine.
  • allowBoats: with a BoatItem anywhere in the inventory, water nodes a boat can float on are priced at BOAT_ONE_BLOCK_COST = 20/7. "Can float" is surface water with headroom for a rider, no bubble columns in the 3x3 (a whirlpool sinks it, soul sand launches it), and every neighbour at water level open, because the hull is 1.375 wide and the item refuses to place it with a bank beside the node. That last rule also routes A* a block off the banks for free.
  • The crossing itself is BoatTrip, owned by PathExecutor: place the boat out over the lane from the shore (or from the water if the path jumped in), climb in, row, and at the far side punch our own boat so the item drops where we land, wait for the feet to sink back into the water node, and hand the tick back. It keeps pathPosition moving so plan ahead still works, survives splices, and a fresh executor that finds the player already rowing just carries on. boatMinWaterLength (default 8) says how much water is worth the place and scuttle overhead, boatPickup whether to take the boat along.
  • Driving is a small model predictive loop on the exact vanilla boat physics: for every tap it could do (1, 2, 4 or 12 ticks either way, or nothing, with and without forward) roll twelve ticks forward, starting with last tick's choice because the boat ticks before its passenger reads the keys, and keep the one that lands nearest the lane point ahead without leaving the lane, clipping a bank with the hull's box, pointing the wrong way, or fidgeting with the keys. The lane it tracks is the node centers pulled taut, since staircase water paths otherwise turn into a wave the boat follows faithfully. Tuned offline against the same equations: two right angles in about ninety ticks with one clean swing each, and key presses on a straight down from 35 to 5. A single tap is always a ten degree turn in vanilla, so the heading holds to about five degrees at best, honestly surprised me.
  • Boat stretches render as a lane in colorBoatPath (default green), two lines either side of the line the boat actually follows. Refused runs drop back to the normal path color.
  • Debug chat is nautical. "ahoy! we can sail the seven seas!", "land ho!", "we've run aground. abandon ship". You'll live.

Costs changed: water traversal is min(wading, swimming) with allowSwimming and min(that, rowing) where a boat can float with allowBoats. Nothing else in the cost model moved.

Preview

https://www.youtube.com/watch?v=0kdDPz10wvI

Testing

Existing unit tests and the full build (JDK 17) green. Swimming and boats smoke tested in a dev env over a few sessions: lake crossings, rivers with bends, shores a block above the water, falling into water from a ledge, one boat in the inventory, creative and survival pickup, and pausing mid crossing. Known limits: a one wide canal is never boatable, blockFreeLook on means placement mostly fails since the client never turns, and corners at speed still swing about a block wide inside the lane.

@AverWasTaken AverWasTaken changed the title Add allowSwimming and allowBoats for crossing water Ahoy! Baritone can now swim and use boats Sep 17, 2026
baritone crosses water by bobbing along the bottom at wading speed,
which is nine ticks a block. this sprint swims along the surface
instead, about seven, and prices water that way in the planner.

sprint is what enters and keeps the vanilla swim state, so
Movement.update() forces it while in water. vanilla only starts
sprinting from the key on the ground or with the eye under, and cancels
it at the surface, which is exactly where the old bob kept us, so a
small redirect in updateSwimming latches the state the same tick
instead of waiting for gravity to pull the eye down.

vertical control is pitch, not the jump key. with jump held vanilla
settles at a vertical speed set purely by the look pitch (about 25
degrees down hovers, level rises, steeper sinks), so a PD hold on feet
height turns into a pitch and the player sits 0.15 under the surface,
head out for air, body in for the swim state. cutting the jump key at a
depth band used to launch us clean out of the water and porpoise, since
the momentum after a rise carries four times the last tick's speed.
tuned against the exact update equations: no overshoot from any depth,
settles within a second. shallow water crawls the bottom until air runs
low, then stands up to breathe until it's full.

bends get turned into early. water only eats a fifth of the momentum a
tick, so arriving at a corner at swim speed coasts half a block through
it. braking flickers the swim state (vanilla drops sprint the moment W
stops counting as forward), so instead aim at the block after the bend
once the corner is inside coasting distance and let the momentum be the
entry angle.

mining from a swim didn't work at all: the swim look mode returned
SERVER for everything, so the crosshair never landed on the block, and
vanilla mines 5x slower off the ground and 5x again with the eye under.
block interacts get CLIENT like the normal freeLook branch, and
shouldSwim() is false while PREPPING so we stand up to mine.
with a boat in the inventory the planner prices open water at rowing
speed and the executor takes it from there: on the last block of shore
(or floating in the water, if the path jumped in) it places the boat out
over the lane, climbs in, rows the water nodes, and at the far side
punches its own boat so the item drops right where it lands, then
carries on on foot. boatMinWaterLength says how much water is worth the
place and scuttle overhead, boatPickup whether to take the boat along.

water only counts when the boat actually fits: surface water, headroom
for a rider, no bubble columns nearby, and every neighbour at water
level open, because the hull is 1.375 wide and the item refuses to
place it (and a moving one scrapes) with a bank beside the node. that
also routes A* a block off the banks. the first block off the shore gets
a pass on its neighbours since the bank's always there.

driving is a little model predictive loop on the exact vanilla boat
physics: for every tap it could do, roll the next twelve ticks and keep
the one that lands nearest the lane point ahead without leaving the
lane, clipping a bank, pointing the wrong way or fidgeting with the
keys. anything steering on heading alone hunts, nine tenths of the
velocity carries over every tick. tuned offline against the same
equations. the lane it tracks is the node centers pulled taut, since
the staircase water paths otherwise turn into a wave.

the trip owns the executor tick while it runs, keeps pathPosition
moving so plan ahead still works, survives splices, and a fresh
executor that finds the player already rowing just carries on. boat
stretches render as a lane in colorBoatPath.
@ZacSharp

ZacSharp commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

This pr is a little bigger and I was testing other stuff anyway, so some play testing first this time

Did you break A*? My bet would be that boats are faster than sprinting, so the heuristic no longer underestimates. boat_path
Boat trace rendering doesn't look all that great in turns curved_trace

Also not yet sure what to think about the boat code. Boats feel kinda useless, so 1k+ loc is a lot, but it does look darn cool and does speed up ocean crossings.

rowing was priced at 2.857 a block and costHeuristic is 3.563, so the
heuristic overestimated anywhere near water and A* went on scenic
detours across the lake. boat water now costs at least costHeuristic.
still less than half of swimming so it gets picked all the same.

the lane edges were drawn per segment with per segment normals, which
gaps on the outside of a turn and folds into a bow tie on the inside.
now each side is one connected line off averaged normals, and edge
points that end up closer to the centerline than the lane is wide get
skipped so the inside just cuts the corner.
@AverWasTaken

Copy link
Copy Markdown
Contributor Author

yep you called it on A*. rowing was priced at 20/7 = 2.857 a block and costHeuristic is 3.563, so the heuristic overestimated anywhere near water and the search went on a scenic tour of the lake. boat water now costs max(BOAT_ONE_BLOCK_COST, costHeuristic), so nothing in the graph is cheaper than the heuristic thinks a block is. it's still under half of swimming (6.99) so boats get picked all the same, it just can't prefer a detour over water to a straight sprint anymore, which is honestly fine.

on the trace: each segment was drawing its own pair of offset lines with its own normal, so the outside of a turn gapped and the inside folded over itself into that bow tie. each side is now one connected line off averaged normals, and edge points that land closer to the centerline than the lane is wide get dropped, so the inside of a tight turn just cuts the corner.

both in 3926456.

on the size, fair. most of it is BoatTrip, which is one self contained file hooked in at the top of PathExecutor.onTick, and it's all behind allowBoats which defaults to false. with the setting off the only thing that runs is a boolean check. if you'd rather it get split out of this PR so swimming can land on its own i can do that too.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants