From c1831b2730d05c509c2116f3a41a2d97538585c1 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Mon, 27 Apr 2026 13:30:14 +0000 Subject: [PATCH] pathing: lock Descend air-yaw and shortcut Ascend->Turn landing Two coupled fixes for live 258<->237 / 237<->244 routes that previously hit 5 replans and gave up on Ascend exit=Turn segments. DescendTemplate: when biasTowardExitInAir is false on a multi-block diagonal descend, the per-tick targetYaw rotates as the bot drifts past the landing column mid-fall (e.g. dx=-1 dz=-1 drop yaw cycles 135 -> 90 -> 0 -> 315 over six air ticks). With Forward held, that rotating yaw pushes air-control momentum perpendicular to the planned trajectory, sliding the bot ~0.5 m off the landing onto an adjacent block one tier below. Lock airborne yaw to the segment's start->end heading for those descends so air drift stays aligned with the diagonal. AscendTemplate: add a post-landing completion shortcut for non-FinalStop exits. Once the jump arc puts the bot back on ground at the target's elevation with its center inside the target column, hand off to the next template (which snaps yaw on its first tick). Holding the segment runs both AscendTemplate's top-level yaw smoothing toward a moving targetYaw AND GroundedSegmentController's segment/exit-heading rotation each tick. The competing yaw targets oscillate the bot ~80 ticks until it walks off the 1-block landing's edge and the segment fails. Mirrors the existing PrepareJump completion gate. FinalStop is excluded so the last segment still uses IsSettledAtEnd to detect a true stop. Live verification on 1.21.11 (round-trip 244<->237 plus the originally failing 237->244 route): four navigations, 0 replans, all 25/41/24/41 segments completed. Made-with: Cursor --- .../GroundedTemplateConvergenceTests.cs | 117 ++++++++++++++++++ .../Execution/Templates/AscendTemplate.cs | 31 +++++ .../Execution/Templates/DescendTemplate.cs | 28 ++++- 3 files changed, 173 insertions(+), 3 deletions(-) diff --git a/MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs b/MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs index cabc5d8e..f8f4b69c 100644 --- a/MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs +++ b/MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs @@ -886,4 +886,121 @@ public sealed class GroundedTemplateConvergenceTests TemplateFootingHelper.IsFootprintInsideTargetBlock(finalPos, ascend.End), $"state={state} finalPos={finalPos} vel={physics.DeltaMovement}\n{string.Join('\n', trace)}"); } + + /// + /// Live regression: B->A route segment 9/41 Ascend (243.5,97,181.5)-> + /// (244.5,98,182.5) exit=Turn, fed by a Descend->PrepareJump handoff. + /// The Ascend starts mid-air with cardinal +Z momentum and yaw aimed + /// at the diagonal target. Without a post-landing completion shortcut + /// the bot lands hot (footprint partially outside target) and the + /// AscendTemplate top-level yaw smoothing toward targetYaw fights the + /// GroundedSegmentController exit-heading rotation, oscillating yaw + /// each tick until the bot drifts off the 1-block landing's edge and + /// the segment fails. With the shortcut the segment completes the + /// instant the bot lands with center inside the target column, which + /// hands off cleanly to the next template (Traverse) that snaps yaw + /// on its first tick. + /// + [Fact] + public void AscendTemplate_TurnExit_FromCarriedAirMomentum_CompletesOnTargetColumnLanding() + { + World world = FlatWorldTestBuilder.CreateStoneFloor(min: -4, max: 6); + FlatWorldTestBuilder.ClearBox(world, -4, 80, -4, 6, 86, 6); + // Source block (0,79,0) is the Descend's landing column; the bot + // arrives mid-jump above it. + FlatWorldTestBuilder.SetSolid(world, 0, 79, 0); + // Target block (1,80,1) (NE diagonal +1y) is the Ascend's landing. + FlatWorldTestBuilder.SetSolid(world, 1, 80, 1); + // Next-segment landing column (Traverse east from target). + FlatWorldTestBuilder.SetSolid(world, 2, 80, 1); + // Walkable shoulder block beneath the bot's overshooting +Z + // footprint so the bot doesn't drop into a 2-block hole on + // landing (matches the live world geometry where a wide platform + // existed at the target's elevation). + FlatWorldTestBuilder.SetSolid(world, 1, 80, 2); + + var ascend = new PathSegment + { + Start = new Location(0.5, 80, 0.5), + End = new Location(1.5, 81, 1.5), + MoveType = MoveType.Ascend, + ExitTransition = PathTransitionType.Turn, + ExitHints = new PathTransitionHints( + DesiredHeadingX: 1, + DesiredHeadingZ: 0, + MinExitSpeed: 0.0, + MaxExitSpeed: 0.05, + RequireStableFooting: true, + RequireGrounded: true, + RequireJumpReady: false, + AllowAirBrake: true, + HorizonTicks: 12), + PreserveSprint = true + }; + var next = new PathSegment + { + Start = new Location(1.5, 81, 1.5), + End = new Location(2.5, 81, 1.5), + MoveType = MoveType.Traverse, + ExitTransition = PathTransitionType.FinalStop + }; + + var template = new AscendTemplate(ascend, next); + + // Seed mid-arc state mirroring the live PathDiag tick-trace at the + // first observed tick of seg9/41: bot already airborne with rising + // vy, cardinal +Z momentum from the preceding Descend, and yaw + // pointing roughly NE (the AscendTemplate snapped yaw on the + // takeoff tick). + var physics = new PlayerPhysics + { + Position = new Vec3d(0.7, 80.42, 0.92), + DeltaMovement = new Vec3d(0.0, 0.333, 0.145), + OnGround = false, + Sprinting = true, + MovementSpeed = 0.1f, + Yaw = 311f, + Pitch = 0f + }; + + var input = new MovementInput(); + TemplateState state = TemplateState.InProgress; + Location finalPos = new(physics.Position.X, physics.Position.Y, physics.Position.Z); + var trace = new List(); + int elapsedTicks = 0; + for (; elapsedTicks < 80; elapsedTicks++) + { + input.Reset(); + Location pos = new(physics.Position.X, physics.Position.Y, physics.Position.Z); + state = template.Tick(pos, physics, input, world); + if (elapsedTicks < 30 || state != TemplateState.InProgress) + { + trace.Add( + $"tick={elapsedTicks} state={state} pos={pos} yaw={physics.Yaw:F1} vel={physics.DeltaMovement} " + + $"onGround={physics.OnGround} input(F={input.Forward},B={input.Back},J={input.Jump},S={input.Sprint})"); + } + if (state != TemplateState.InProgress) + { + finalPos = new Location(physics.Position.X, physics.Position.Y, physics.Position.Z); + break; + } + + physics.ApplyInput(input); + physics.Tick(world); + finalPos = new Location(physics.Position.X, physics.Position.Y, physics.Position.Z); + } + + Assert.True( + state == TemplateState.Complete, + $"state={state} elapsed={elapsedTicks} finalPos={finalPos} vel={physics.DeltaMovement}\n{string.Join('\n', trace)}"); + Assert.True( + physics.OnGround, + $"state={state} finalPos={finalPos} vel={physics.DeltaMovement}"); + Assert.True( + TemplateFootingHelper.IsCenterInsideTargetBlock(finalPos, ascend.End), + $"finalPos={finalPos} target={ascend.End}\n{string.Join('\n', trace)}"); + Assert.True( + elapsedTicks <= 30, + $"completion took {elapsedTicks} ticks; expected post-landing shortcut to fire promptly\n{string.Join('\n', trace)}"); + } } diff --git a/MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs b/MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs index dedb7818..5cede71f 100644 --- a/MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs +++ b/MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs @@ -43,6 +43,7 @@ namespace MinecraftClient.Pathing.Execution.Templates private Location _lastPos; private int _stuckTicks; private bool _initiatedJump; + private bool _hasBeenAirborne; private int _diagonalBrakeTicks; public AscendTemplate(PathSegment segment, PathSegment? nextSegment) @@ -182,8 +183,38 @@ namespace MinecraftClient.Pathing.Execution.Templates } } + if (!physics.OnGround) + _hasBeenAirborne = true; + if (physics.OnGround && Math.Abs(dy) < 0.2) { + // Post-landing shortcut: once the Ascend's jump arc has put the + // bot back on ground at the target's elevation with its center + // inside the target column, the segment has done its job. Hand + // off to the next template (which snaps yaw on its first tick) + // instead of trying to brake or settle to stable footing. + // + // Holding onto the segment here re-runs both the AscendTemplate + // top-level yaw smoothing toward targetYaw (a moving bearing as + // the bot drifts past End) AND GroundedSegmentController's + // segment/exit-heading rotation each tick. The two competing + // yaw targets (e.g. yaw=233 anti-velocity vs yaw=315 segment + // heading vs yaw=270 exit heading on a Descend->PrepareJump-> + // Ascend->Turn chain) oscillate the bot ~80 ticks until it + // walks off the 1-block landing's edge and the segment fails. + // Mirrors the existing "Ascend completes on PrepareJump as + // soon as center is inside the target block" gate in + // GroundedSegmentController.ShouldComplete. FinalStop is + // excluded because the last segment must come to rest at + // the goal: hand it back to GroundedSegmentController, + // which uses IsSettledAtEnd to detect a true stop. + if (_hasBeenAirborne + && _segment.ExitTransition != PathTransitionType.FinalStop + && TemplateFootingHelper.IsCenterInsideTargetBlock(pos, _segment.End)) + { + return TemplateState.Complete; + } + GroundedSegmentController.Apply(_segment, _nextSegment, pos, physics, input, world); if (GroundedSegmentController.ShouldComplete(_segment, pos, physics)) return TemplateState.Complete; diff --git a/MinecraftClient/Pathing/Execution/Templates/DescendTemplate.cs b/MinecraftClient/Pathing/Execution/Templates/DescendTemplate.cs index 3cee5861..e481e361 100644 --- a/MinecraftClient/Pathing/Execution/Templates/DescendTemplate.cs +++ b/MinecraftClient/Pathing/Execution/Templates/DescendTemplate.cs @@ -150,9 +150,31 @@ namespace MinecraftClient.Pathing.Execution.Templates && (onOrPastTarget || (_hasFallen && TemplateHelper.ShouldBiasTowardExitHeading(pos, _segment, distanceThreshold: 1.5)))); - float airborneYaw = biasTowardExitInAir - ? TemplateHelper.GetExitHeadingYaw(_segment) - : targetYaw; + float airborneYaw; + if (biasTowardExitInAir) + { + airborneYaw = TemplateHelper.GetExitHeadingYaw(_segment); + } + else if (!isSingleStepDescend) + { + // Multi-block descend: target-tracking yaw rotates as + // the bot drifts past the landing column mid-fall (e.g. + // a diagonal 3 c2c drop with dx=-1, dz=-1 starts at + // yaw=135, the relative bearing to End flips through + // 90 -> 0 -> 315 in 6 air ticks). With Forward input + // held, the rotating yaw pushes air-control momentum + // perpendicular to the planned trajectory, drifting + // the bot ~0.5 m past the landing column and onto an + // adjacent block one tier below. Lock airborne yaw to + // the segment's start->end heading so air drift stays + // aligned with the planned diagonal; the GroundedSegment + // controller takes over once the bot is on the landing. + airborneYaw = TemplateHelper.CalculateYaw(_segment.HeadingX, _segment.HeadingZ); + } + else + { + airborneYaw = targetYaw; + } physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, airborneYaw); if (_hasFallen || YawDifference(physics.Yaw, airborneYaw) <= PreDropYawToleranceDeg)