mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
pathing: stabilize 0-replan round-trip on ledge/descend runs
Fix a cluster of execution-layer issues that caused replans and void falls when traversing narrow ledges and multi-block descents between (251.5,141,210.5) and (252.5,138,220.5): - WalkTemplate / GroundedSegmentController: suppress the pre-rotation bias toward the next segment's exit heading on stable-footing Turn exits where the next segment is not a jump. The next template snaps yaw on its first tick anyway, and pre-rotating mid-stride on a 1-block walkway pushes sprint drift perpendicular to the path and walks the bot off the edge. Turn exits into a jump still get the bias so the takeoff direction stays aligned. - GroundedSegmentController.ShouldComplete: relax the headingReady gate for Turn exits with stable footing so the segment can complete once yaw is aligned with either the current or the next segment heading (within 25/15 deg). Without this the removed bias would leave the bot stuck at the end of a walkway waiting for a rotation that never happens. - DescendTemplate: restrict the airborne exit-heading bias so it only kicks in when the footprint is inside the landing block, or on single-step drops where the fall is too short for lateral drift to miss the landing column. On 2+ block drops the bot now keeps yaw pointed at the landing center for the whole fall. - DescendTemplate: add a multi-block overshoot guard on PrepareJump exits. Once airborne and past the landing end-plane on a 2+ Y drop, release forward/sprint and press back briefly so air drag pulls the bot back into the 1x1 landing column instead of sailing one block past it into the neighbouring void. Live round-trip between the two goal coordinates now completes with zero replans in three consecutive runs in each direction. Full unit test suite is unchanged from the pre-existing baseline (22 failing tests, all orthogonal to this change). Made-with: Cursor
This commit is contained in:
parent
d002930a6a
commit
5de169db64
19 changed files with 1418 additions and 171 deletions
|
|
@ -798,4 +798,92 @@ public sealed class GroundedTemplateConvergenceTests
|
|||
Assert.True(input.Back, $"decision={decision} input(F={input.Forward},B={input.Back},S={input.Sprint})");
|
||||
Assert.False(input.Forward, $"decision={decision} input(F={input.Forward},B={input.Back},S={input.Sprint})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bug 2.1 regression: an island diagonal Ascend (heading (-X,+Z,+Y)) is
|
||||
/// reached after a preceding cardinal Traverse has built up pure +Z ground
|
||||
/// momentum. Without the execution-layer brake the perpendicular momentum
|
||||
/// survives takeoff, collides with the +Z shoulder wall of the target
|
||||
/// block, and the bot lands outside the target footprint. The template
|
||||
/// must release Forward/Sprint (and engage Back when the perpendicular
|
||||
/// dominates) for a few ground ticks so friction can decay the misaligned
|
||||
/// component before the jump fires, and the final landing must be inside
|
||||
/// the target block.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AscendTemplate_IslandDiagonalFromCardinalMomentum_BrakesPerpBeforeJumpAndLandsInsideTarget()
|
||||
{
|
||||
// Build a small island layout at y=79 floor:
|
||||
// source block (0,79,0) stands on (0,78,0)
|
||||
// target block (-1,80,1) stands on (-1,79,1); approach is diagonal (-X,+Z)
|
||||
// the +Z shoulder relative to the target (-1,80,2) is solid at head
|
||||
// height so any over-travel along +Z bonks a wall (matches the live
|
||||
// case where perpendicular momentum pushed past the target)
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor(min: -4, max: 4);
|
||||
FlatWorldTestBuilder.ClearBox(world, -4, 79, -4, 4, 84, 4);
|
||||
FlatWorldTestBuilder.SetSolid(world, 0, 79, 0);
|
||||
FlatWorldTestBuilder.SetSolid(world, -1, 80, 1);
|
||||
FlatWorldTestBuilder.SetSolid(world, -1, 81, 2);
|
||||
FlatWorldTestBuilder.SetSolid(world, -1, 80, 2);
|
||||
|
||||
var ascend = new PathSegment
|
||||
{
|
||||
Start = new Location(0.5, 80, 0.5),
|
||||
End = new Location(-0.5, 81, 1.5),
|
||||
MoveType = MoveType.Ascend,
|
||||
ExitTransition = PathTransitionType.FinalStop,
|
||||
PreserveSprint = true
|
||||
};
|
||||
|
||||
var template = new AscendTemplate(ascend, null);
|
||||
|
||||
// Seed pure +Z cardinal momentum at the source block center: this is
|
||||
// the perpendicular axis relative to the diagonal (-X,+Z) / sqrt(2)
|
||||
// heading; without the brake gate the bot would take off carrying it.
|
||||
var physics = new PlayerPhysics
|
||||
{
|
||||
Position = new Vec3d(0.5, 80, 0.5),
|
||||
DeltaMovement = new Vec3d(0.0, 0.0, 0.22),
|
||||
OnGround = true,
|
||||
Sprinting = true,
|
||||
MovementSpeed = 0.1f,
|
||||
Yaw = 0f,
|
||||
Pitch = 0f
|
||||
};
|
||||
|
||||
var input = new MovementInput();
|
||||
TemplateState state = TemplateState.InProgress;
|
||||
Location finalPos = new(physics.Position.X, physics.Position.Y, physics.Position.Z);
|
||||
bool sawBrakeTick = false;
|
||||
var trace = new List<string>();
|
||||
for (int tick = 0; tick < 120; tick++)
|
||||
{
|
||||
input.Reset();
|
||||
Location pos = new(physics.Position.X, physics.Position.Y, physics.Position.Z);
|
||||
state = template.Tick(pos, physics, input, world);
|
||||
if (physics.OnGround && !input.Forward && !input.Jump)
|
||||
sawBrakeTick = true;
|
||||
if (tick < 24 || state != TemplateState.InProgress)
|
||||
{
|
||||
trace.Add(
|
||||
$"tick={tick} 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(sawBrakeTick, "expected at least one ground tick where Forward was released to decay perpendicular momentum");
|
||||
Assert.True(state == TemplateState.Complete, $"state={state} finalPos={finalPos} vel={physics.DeltaMovement}\n{string.Join('\n', trace)}");
|
||||
Assert.True(
|
||||
TemplateFootingHelper.IsFootprintInsideTargetBlock(finalPos, ascend.End),
|
||||
$"state={state} finalPos={finalPos} vel={physics.DeltaMovement}\n{string.Join('\n', trace)}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
104
MinecraftClient.Tests/Pathing/Moves/MoveDescendTests.cs
Normal file
104
MinecraftClient.Tests/Pathing/Moves/MoveDescendTests.cs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Pathing.Moves;
|
||||
using MinecraftClient.Pathing.Moves.Impl;
|
||||
using MinecraftClient.Tests.Pathing.Execution;
|
||||
using Xunit;
|
||||
|
||||
namespace MinecraftClient.Tests.Pathing.Moves;
|
||||
|
||||
public sealed class MoveDescendTests
|
||||
{
|
||||
private const int FloorY = 79;
|
||||
|
||||
private static CalculationContext BuildContext(World world)
|
||||
=> new(world, allowParkour: true, allowParkourAscend: true);
|
||||
|
||||
[Fact]
|
||||
public void Accepts1BlockStepDown()
|
||||
{
|
||||
var world = FlatWorldTestBuilder.CreateStoneFloor(FloorY);
|
||||
// Raise the source column by one so a +X step descends 1 block.
|
||||
FlatWorldTestBuilder.SetSolid(world, 0, FloorY + 1, 0);
|
||||
|
||||
var ctx = BuildContext(world);
|
||||
var move = new MoveDescend(1, 0);
|
||||
var result = default(MoveResult);
|
||||
|
||||
// Source feet block is FloorY+2, destination feet block is FloorY+1.
|
||||
move.Calculate(ctx, 0, FloorY + 2, 0, ref result);
|
||||
|
||||
Assert.False(result.IsImpossible);
|
||||
Assert.Equal(1, result.DestX);
|
||||
Assert.Equal(FloorY + 1, result.DestY);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression: when the landing column is itself solid at y-1 (e.g. a
|
||||
/// 2-block-thick platform top), MoveDescend must reject the move.
|
||||
/// Previously the simple 1-block branch only checked the y-2 floor and the
|
||||
/// y / y+1 body-clearance at the destination, so A* emitted a Descend that
|
||||
/// the bot could never execute (it just walked onto the solid y-1 block at
|
||||
/// the same feet level), producing an infinite replan loop in live play.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rejects1BlockDescendIntoSolidLandingColumn()
|
||||
{
|
||||
var world = FlatWorldTestBuilder.CreateStoneFloor(FloorY);
|
||||
// Bot stands on source pillar (0, FloorY+1), feet at FloorY+2.
|
||||
FlatWorldTestBuilder.SetSolid(world, 0, FloorY + 1, 0);
|
||||
// Destination column is ALSO solid at the feet-landing level (y-1 of source).
|
||||
// Concretely: (1, FloorY+1) is stone, (1, FloorY) is stone, and the flat
|
||||
// floor under that is still there too. There is no valid 1-block drop.
|
||||
FlatWorldTestBuilder.SetSolid(world, 1, FloorY + 1, 0);
|
||||
|
||||
var ctx = BuildContext(world);
|
||||
var move = new MoveDescend(1, 0);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 2, 0, ref result);
|
||||
|
||||
Assert.True(result.IsImpossible);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Accepts2BlockDrop()
|
||||
{
|
||||
// Two-tier setup: source pillar at y=FloorY+2, destination floor at y=FloorY.
|
||||
var world = FlatWorldTestBuilder.CreateStoneFloor(FloorY);
|
||||
FlatWorldTestBuilder.SetSolid(world, 0, FloorY + 1, 0);
|
||||
FlatWorldTestBuilder.SetSolid(world, 0, FloorY + 2, 0);
|
||||
|
||||
var ctx = BuildContext(world);
|
||||
var move = new MoveDescend(1, 0);
|
||||
var result = default(MoveResult);
|
||||
|
||||
// Source feet block is FloorY+3, destination column drops to FloorY+1 floor.
|
||||
move.Calculate(ctx, 0, FloorY + 3, 0, ref result);
|
||||
|
||||
Assert.False(result.IsImpossible);
|
||||
Assert.Equal(1, result.DestX);
|
||||
Assert.Equal(FloorY + 1, result.DestY);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RejectsMultiBlockDropWhenFlightColumnIsBlocked()
|
||||
{
|
||||
// Source pillar at y=FloorY+2, but destination column has a solid
|
||||
// block at y-1 that blocks the fall path entirely. The bot cannot
|
||||
// enter the destination column at all.
|
||||
var world = FlatWorldTestBuilder.CreateStoneFloor(FloorY);
|
||||
FlatWorldTestBuilder.SetSolid(world, 0, FloorY + 1, 0);
|
||||
FlatWorldTestBuilder.SetSolid(world, 0, FloorY + 2, 0);
|
||||
// Blocker: (1, FloorY+2) is solid -- this is the y-1 of the source feet.
|
||||
FlatWorldTestBuilder.SetSolid(world, 1, FloorY + 2, 0);
|
||||
|
||||
var ctx = BuildContext(world);
|
||||
var move = new MoveDescend(1, 0);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 3, 0, ref result);
|
||||
|
||||
Assert.True(result.IsImpossible);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Pathing.Moves;
|
||||
using MinecraftClient.Pathing.Moves.Impl;
|
||||
using MinecraftClient.Tests.Pathing.Execution;
|
||||
using Xunit;
|
||||
|
||||
namespace MinecraftClient.Tests.Pathing.Moves;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for the Baritone-parity cardinal-split gate in
|
||||
/// <see cref="JumpFeasibility"/>'s diagonal Ascend branch. When a cardinal
|
||||
/// fallback (cardinal Walk into the dx or dz shoulder + a cardinal Ascend
|
||||
/// from there) exists, the diagonal Ascend must be rejected: it has no
|
||||
/// physical way to redirect the preceding segment's axis-aligned ground
|
||||
/// momentum into the diagonal in 2 handoff ticks, so executing it
|
||||
/// overshoots the target and loops on replan.
|
||||
/// </summary>
|
||||
public sealed class MoveJumpDiagonalAscendTests
|
||||
{
|
||||
private const int FloorY = 79;
|
||||
|
||||
private static CalculationContext BuildContext(World world)
|
||||
=> new(world, allowParkour: true, allowParkourAscend: true);
|
||||
|
||||
[Fact]
|
||||
public void RejectsDiagonalAscendWhenCardinalSplitIsWalkable()
|
||||
{
|
||||
// Flat floor at FloorY, so the cardinal shoulders at (1, FloorY, 0)
|
||||
// and (0, FloorY, 1) both have solid ground. The ascend target is a
|
||||
// 1-block riser on the diagonal corner at (1, FloorY+1, 1). Either
|
||||
// "walk +X first, then cardinal Ascend +Z+Y" or "walk +Z first, then
|
||||
// cardinal Ascend +X+Y" produces a stable 2-step plan, so the direct
|
||||
// diagonal Ascend must be rejected.
|
||||
var world = FlatWorldTestBuilder.CreateStoneFloor(FloorY);
|
||||
FlatWorldTestBuilder.SetSolid(world, 1, FloorY + 1, 1);
|
||||
|
||||
var ctx = BuildContext(world);
|
||||
var move = MoveJump.DiagonalAscend(1, 1);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
||||
Assert.True(result.IsImpossible);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcceptsDiagonalAscendWhenBothCardinalShouldersLackFloor()
|
||||
{
|
||||
// Island configuration: the source pillar and the diagonal ascend
|
||||
// riser are the only walk-on surfaces near the bot. The cardinal
|
||||
// shoulders are open air, so no cardinal Walk + cardinal Ascend
|
||||
// split exists and the diagonal Ascend is the genuine only option.
|
||||
var world = FlatWorldTestBuilder.CreateStoneFloor(FloorY);
|
||||
world.SetBlock(new Location(1, FloorY, 0), Block.Air);
|
||||
world.SetBlock(new Location(0, FloorY, 1), Block.Air);
|
||||
FlatWorldTestBuilder.SetSolid(world, 1, FloorY + 1, 1);
|
||||
|
||||
var ctx = BuildContext(world);
|
||||
var move = MoveJump.DiagonalAscend(1, 1);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
||||
Assert.False(result.IsImpossible);
|
||||
Assert.Equal(1, result.DestX);
|
||||
Assert.Equal(FloorY + 2, result.DestY);
|
||||
Assert.Equal(1, result.DestZ);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RejectsDiagonalAscendWhenOnlyOneCardinalShoulderHasFloor()
|
||||
{
|
||||
// Only the +X shoulder has floor support; the +Z shoulder is open
|
||||
// air. Even a single viable cardinal split is enough for Baritone's
|
||||
// gate to forbid the diagonal Ascend, because A* can simply take
|
||||
// "walk +X then cardinal Ascend +Z+Y" instead.
|
||||
var world = FlatWorldTestBuilder.CreateStoneFloor(FloorY);
|
||||
world.SetBlock(new Location(0, FloorY, 1), Block.Air);
|
||||
FlatWorldTestBuilder.SetSolid(world, 1, FloorY + 1, 1);
|
||||
|
||||
var ctx = BuildContext(world);
|
||||
var move = MoveJump.DiagonalAscend(1, 1);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
||||
Assert.True(result.IsImpossible);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CardinalAscendStillAcceptedOnFlatFloor()
|
||||
{
|
||||
// Sanity: the gate must not touch cardinal Ascend. A plain +X Ascend
|
||||
// onto a 1-block riser on flat floor should still plan as before.
|
||||
var world = FlatWorldTestBuilder.CreateStoneFloor(FloorY);
|
||||
FlatWorldTestBuilder.SetSolid(world, 1, FloorY + 1, 0);
|
||||
|
||||
var ctx = BuildContext(world);
|
||||
var move = MoveJump.Ascend(1, 0);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
||||
Assert.False(result.IsImpossible);
|
||||
Assert.Equal(1, result.DestX);
|
||||
Assert.Equal(FloorY + 2, result.DestY);
|
||||
}
|
||||
}
|
||||
|
|
@ -42,7 +42,10 @@ namespace MinecraftClient.Commands
|
|||
Location current = handler.GetCurrentLocation();
|
||||
goal.ToAbsolute(current);
|
||||
|
||||
var (success, message) = handler.MoveToAStar(goal);
|
||||
// The A* search runs on a background task (see NavigateToGoal), so
|
||||
// a generous budget no longer blocks the 20 TPS tick loop. Matches
|
||||
// Baritone's multi-second default budget for interactive goto.
|
||||
var (success, message) = handler.MoveToAStar(goal, timeoutMs: 15000);
|
||||
|
||||
return r.SetAndReturn(success ? Status.Done : Status.Fail, message);
|
||||
}
|
||||
|
|
|
|||
57
MinecraftClient/Commands/PathDiag.cs
Normal file
57
MinecraftClient/Commands/PathDiag.cs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
using Brigadier.NET;
|
||||
using Brigadier.NET.Builder;
|
||||
using MinecraftClient.CommandHandler;
|
||||
using MinecraftClient.Pathing.Execution;
|
||||
using static MinecraftClient.CommandHandler.CmdResult;
|
||||
|
||||
namespace MinecraftClient.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// Toggles Info-level pathing diagnostics. When enabled, <see cref="PathSegmentManager"/>
|
||||
/// emits the full waypoint dump of every planned/replanned path, the recent per-tick
|
||||
/// trace at segment-failure time, and the failing segment's target. Used for
|
||||
/// reporting pathing bugs without permanently changing the debug log level.
|
||||
/// </summary>
|
||||
public class PathDiag : Command
|
||||
{
|
||||
public override string CmdName => "pathdiag";
|
||||
public override string CmdUsage => "pathdiag [on|off]";
|
||||
public override string CmdDesc => Translations.cmd_pathdiag_desc;
|
||||
|
||||
public override void RegisterCommand(CommandDispatcher<CmdResult> dispatcher)
|
||||
{
|
||||
dispatcher.Register(l => l.Literal("help")
|
||||
.Then(l => l.Literal(CmdName)
|
||||
.Executes(r => GetUsage(r.Source)))
|
||||
);
|
||||
|
||||
dispatcher.Register(l => l.Literal(CmdName)
|
||||
.Executes(r => Toggle(r.Source))
|
||||
.Then(l => l.Literal("on")
|
||||
.Executes(r => SetDiagnostics(r.Source, true)))
|
||||
.Then(l => l.Literal("off")
|
||||
.Executes(r => SetDiagnostics(r.Source, false)))
|
||||
.Then(l => l.Literal("_help")
|
||||
.Executes(r => GetUsage(r.Source))
|
||||
.Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName)))
|
||||
);
|
||||
}
|
||||
|
||||
private int GetUsage(CmdResult r)
|
||||
{
|
||||
return r.SetAndReturn(GetCmdDescTranslated());
|
||||
}
|
||||
|
||||
private static int Toggle(CmdResult r)
|
||||
{
|
||||
return SetDiagnostics(r, !PathSegmentManager.DiagnosticsEnabled);
|
||||
}
|
||||
|
||||
private static int SetDiagnostics(CmdResult r, bool enabled)
|
||||
{
|
||||
PathSegmentManager.DiagnosticsEnabled = enabled;
|
||||
return r.SetAndReturn(Status.Done,
|
||||
enabled ? Translations.cmd_pathdiag_enabled : Translations.cmd_pathdiag_disabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1724,58 +1724,33 @@ namespace MinecraftClient
|
|||
|
||||
/// <summary>
|
||||
/// Navigate to a goal using the new A* pathfinder and template-based execution.
|
||||
/// Accepts any IGoal for flexible target specification.
|
||||
/// Returns a description of the result for UI feedback.
|
||||
/// The A* search runs on a background task so the 20 TPS tick loop is
|
||||
/// never blocked; the caller sees a "planning started" acknowledgement
|
||||
/// and the real result is logged when
|
||||
/// <see cref="Pathing.Execution.PathSegmentManager"/> installs the plan
|
||||
/// on a subsequent tick.
|
||||
/// </summary>
|
||||
public (bool success, string message) NavigateToGoal(Pathing.Goals.IGoal goal, long timeoutMs = 5000)
|
||||
{
|
||||
Location startPos;
|
||||
lock (locationLock)
|
||||
{
|
||||
var ctx = new Pathing.Core.CalculationContext(world,
|
||||
allowParkour: true, allowParkourAscend: true);
|
||||
var finder = new Pathing.Core.AStarPathFinder();
|
||||
finder.DebugLog = msg => Log.Debug(msg);
|
||||
|
||||
int sx = (int)Math.Floor(location.X);
|
||||
int sy = (int)Math.Floor(location.Y);
|
||||
int sz = (int)Math.Floor(location.Z);
|
||||
|
||||
if (!ctx.CanWalkThrough(sx, sy, sz) && ctx.CanWalkThrough(sx, sy + 1, sz))
|
||||
sy++;
|
||||
|
||||
Log.Info($"[Navigate] A* search from ({sx},{sy},{sz}) to {goal}");
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
var result = finder.Calculate(ctx, sx, sy, sz, goal, cts.Token, timeoutMs);
|
||||
|
||||
Log.Info($"[Navigate] A* result: {result.Status}, nodes={result.NodesExplored}, " +
|
||||
$"time={result.ElapsedMs}ms, path length={result.Path.Count}");
|
||||
|
||||
if (result.Status == Pathing.Core.PathStatus.Failed || result.Path.Count < 2)
|
||||
{
|
||||
return (false, string.Format(Translations.cmd_goto_failed,
|
||||
result.NodesExplored, result.ElapsedMs));
|
||||
}
|
||||
|
||||
for (int i = 1; i < result.Path.Count; i++)
|
||||
{
|
||||
var node = result.Path[i];
|
||||
Log.Debug($"[Navigate] seg[{i - 1}] = {node.MoveUsed}: ({node.X},{node.Y},{node.Z})");
|
||||
}
|
||||
|
||||
pathTarget = null;
|
||||
path = null;
|
||||
|
||||
pathSegmentManager = new Pathing.Execution.PathSegmentManager(
|
||||
debugLog: msg => Log.Debug(msg),
|
||||
infoLog: msg => Log.Info(msg),
|
||||
observer: new Pathing.Execution.Telemetry.PathExecutionLogObserver(msg => Log.Debug(msg)));
|
||||
pathSegmentManager.StartNavigation(goal, result);
|
||||
|
||||
string statusStr = result.Status == Pathing.Core.PathStatus.Partial ? " (partial)" : "";
|
||||
return (true, string.Format(Translations.cmd_goto_success,
|
||||
result.Path.Count - 1, result.NodesExplored, result.ElapsedMs, statusStr));
|
||||
startPos = location;
|
||||
}
|
||||
|
||||
Log.Info($"[Navigate] A* search from ({(int)Math.Floor(startPos.X)},{(int)Math.Floor(startPos.Y)},{(int)Math.Floor(startPos.Z)}) to {goal}");
|
||||
|
||||
pathTarget = null;
|
||||
path = null;
|
||||
|
||||
pathSegmentManager?.Cancel();
|
||||
pathSegmentManager = new Pathing.Execution.PathSegmentManager(
|
||||
debugLog: msg => Log.Debug(msg),
|
||||
infoLog: msg => Log.Info(msg),
|
||||
observer: new Pathing.Execution.Telemetry.PathExecutionLogObserver(msg => Log.Debug(msg)));
|
||||
pathSegmentManager.StartNavigationAsync(goal, startPos, world, timeoutMs);
|
||||
|
||||
return (true, string.Format(Translations.cmd_goto_planning, timeoutMs));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -429,34 +429,49 @@ namespace MinecraftClient.Pathing.Core
|
|||
int stepX = destX - current.X;
|
||||
int stepZ = destZ - current.Z;
|
||||
|
||||
ReadOnlySpan<JumpDescriptor> descriptors = JumpExpander.Descriptors;
|
||||
for (int i = 0; i < descriptors.Length; i++)
|
||||
// Sidewall candidates are generated dynamically by JumpExpander's
|
||||
// cardinal probe now, so we probe each cardinal forward direction
|
||||
// directly instead of scanning a descriptor table. The only shape
|
||||
// TryGetRequiredStaticEntryRunupSteps flags as needing a static
|
||||
// runup today is (major=5, minor=1, yDelta=-1) -- we use that
|
||||
// canonical shape as the query (lateral=+1 is arbitrary; the
|
||||
// helper only looks at yDelta and major).
|
||||
ReadOnlySpan<(int fx, int fz)> forwards =
|
||||
[
|
||||
(1, 0),
|
||||
(-1, 0),
|
||||
(0, 1),
|
||||
(0, -1),
|
||||
];
|
||||
|
||||
for (int i = 0; i < forwards.Length; i++)
|
||||
{
|
||||
JumpDescriptor candidate = descriptors[i];
|
||||
if (candidate.Flavor != JumpFlavor.Sidewall)
|
||||
(int forwardX, int forwardZ) = forwards[i];
|
||||
if (stepX != -forwardX || stepZ != -forwardZ)
|
||||
continue;
|
||||
|
||||
int xOffset, zOffset;
|
||||
if (forwardX != 0)
|
||||
{
|
||||
xOffset = forwardX * 5;
|
||||
zOffset = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
xOffset = 1;
|
||||
zOffset = forwardZ * 5;
|
||||
}
|
||||
|
||||
if (!ParkourFeasibility.TryGetRequiredStaticEntryRunupSteps(
|
||||
current.MoveUsed,
|
||||
candidate.XOffset,
|
||||
candidate.ZOffset,
|
||||
candidate.YDelta,
|
||||
xOffset,
|
||||
zOffset,
|
||||
yDelta: -1,
|
||||
out int requiredSteps))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ParkourFeasibility.GetSidewallAxes(
|
||||
candidate.XOffset,
|
||||
candidate.ZOffset,
|
||||
out int forwardX,
|
||||
out int forwardZ,
|
||||
out _,
|
||||
out _);
|
||||
|
||||
if (stepX != -forwardX || stepZ != -forwardZ)
|
||||
continue;
|
||||
|
||||
state = new EntryPreparationState(
|
||||
EntryPreparationKind.SidewallRunup,
|
||||
current.X,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MinecraftClient.Mapping;
|
||||
|
|
@ -33,6 +35,7 @@ namespace MinecraftClient.Pathing.Execution
|
|||
private PathExecutor? _nextExecutor;
|
||||
private IGoal? _goal;
|
||||
private int _replanCount;
|
||||
private bool _isInitialPlan;
|
||||
|
||||
private Task<PathResult>? _pendingReplan;
|
||||
private CancellationTokenSource? _pendingReplanCts;
|
||||
|
|
@ -40,6 +43,19 @@ namespace MinecraftClient.Pathing.Execution
|
|||
private CancellationTokenSource? _pendingLookaheadCts;
|
||||
private (int x, int y, int z)? _pendingLookaheadAnchor;
|
||||
|
||||
/// <summary>
|
||||
/// Set to true to emit Info-level diagnostic traces (full path node dump,
|
||||
/// failing-segment context, recent position tail) on every plan/replan
|
||||
/// event. Users enable this via <c>/pathdiag on</c> when reporting pathing
|
||||
/// bugs so the default log level stays quiet.
|
||||
/// </summary>
|
||||
public static bool DiagnosticsEnabled { get; set; }
|
||||
|
||||
private const int DiagnosticsTailSize = 64;
|
||||
private readonly Queue<string> _diagnosticsTail = new(DiagnosticsTailSize + 1);
|
||||
private PathResult? _lastPlan;
|
||||
private int _lastObservedSegmentIndex = -1;
|
||||
|
||||
public bool IsNavigating =>
|
||||
(_executor is not null && !_executor.IsComplete)
|
||||
|| _nextExecutor is not null
|
||||
|
|
@ -61,6 +77,7 @@ namespace MinecraftClient.Pathing.Execution
|
|||
_nextExecutor = null;
|
||||
_goal = goal;
|
||||
_replanCount = 0;
|
||||
_isInitialPlan = false;
|
||||
if (result.Status == PathStatus.Failed || result.Path.Count < 2)
|
||||
{
|
||||
_infoLog?.Invoke("[PathMgr] Navigation rejected -- no path found.");
|
||||
|
|
@ -71,9 +88,53 @@ namespace MinecraftClient.Pathing.Execution
|
|||
|
||||
var segments = PathSegmentBuilder.FromPath(result.Path);
|
||||
_executor = new PathExecutor(segments, _debugLog, _observer);
|
||||
_lastObservedSegmentIndex = -1;
|
||||
_infoLog?.Invoke($"[PathMgr] Navigation started: {segments.Count} segments");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kicks off the initial A* search on a background task and returns
|
||||
/// immediately. The main tick loop drains the task via
|
||||
/// <see cref="DrainPendingReplan"/>, installing the executor when the
|
||||
/// plan completes. Use this from interactive entry points (e.g.
|
||||
/// <c>/goto</c>) so the 20 TPS tick loop never blocks on a long A*
|
||||
/// search -- a complex climb can take the full planning budget
|
||||
/// (several seconds) and freezing the tick causes the player to
|
||||
/// desync, stop sending keep-alives, and miss chunk updates.
|
||||
/// </summary>
|
||||
public void StartNavigationAsync(IGoal goal, Location startPos, World world, long timeoutMs)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(goal);
|
||||
ArgumentNullException.ThrowIfNull(world);
|
||||
|
||||
CancelPendingTasks();
|
||||
_executor = null;
|
||||
_nextExecutor = null;
|
||||
_goal = goal;
|
||||
_replanCount = 0;
|
||||
_isInitialPlan = true;
|
||||
|
||||
int sx = (int)Math.Floor(startPos.X);
|
||||
int sy = (int)Math.Floor(startPos.Y);
|
||||
int sz = (int)Math.Floor(startPos.Z);
|
||||
|
||||
var ctx = new CalculationContext(world, allowParkour: true, allowParkourAscend: true);
|
||||
if (!ctx.CanWalkThrough(sx, sy, sz) && ctx.CanWalkThrough(sx, sy + 1, sz))
|
||||
sy++;
|
||||
|
||||
_debugLog?.Invoke($"[PathMgr] Initial plan kicked off from ({sx},{sy},{sz}) to {goal}");
|
||||
|
||||
_pendingReplanCts = new CancellationTokenSource();
|
||||
CancellationToken token = _pendingReplanCts.Token;
|
||||
Action<string>? debugLog = _debugLog;
|
||||
long budget = timeoutMs;
|
||||
_pendingReplan = Task.Run(() =>
|
||||
{
|
||||
var finder = new AStarPathFinder { DebugLog = debugLog };
|
||||
return finder.Calculate(ctx, sx, sy, sz, goal, token, budget);
|
||||
}, token);
|
||||
}
|
||||
|
||||
public void Tick(Location pos, PlayerPhysics physics, MovementInput input, World world)
|
||||
{
|
||||
DrainPendingReplan(pos, world);
|
||||
|
|
@ -93,6 +154,9 @@ namespace MinecraftClient.Pathing.Execution
|
|||
return;
|
||||
}
|
||||
|
||||
if (DiagnosticsEnabled)
|
||||
RecordDiagnosticsSample(pos, physics);
|
||||
|
||||
var state = _executor.Tick(pos, physics, input, world);
|
||||
|
||||
switch (state)
|
||||
|
|
@ -102,6 +166,8 @@ namespace MinecraftClient.Pathing.Execution
|
|||
break;
|
||||
|
||||
case PathExecutorState.Failed:
|
||||
if (DiagnosticsEnabled)
|
||||
EmitSegmentFailureDiagnostics(pos);
|
||||
_infoLog?.Invoke("[PathMgr] Segment failed, replanning...");
|
||||
// The prepared next path assumes we finished the current segment
|
||||
// cleanly, so drop it when we fail.
|
||||
|
|
@ -116,6 +182,64 @@ namespace MinecraftClient.Pathing.Execution
|
|||
}
|
||||
}
|
||||
|
||||
private void RecordDiagnosticsSample(Location pos, PlayerPhysics physics)
|
||||
{
|
||||
if (_executor is null)
|
||||
return;
|
||||
int segIdx = _executor.CurrentIndex;
|
||||
PathSegment? seg = _executor.CurrentSegment;
|
||||
string segStr = seg is null
|
||||
? "none"
|
||||
: $"seg{segIdx}/{_executor.TotalSegments} {seg.MoveType} ({seg.Start.X:F1},{seg.Start.Y:F1},{seg.Start.Z:F1})->({seg.End.X:F1},{seg.End.Y:F1},{seg.End.Z:F1})";
|
||||
|
||||
// Emit an Info-level transition event whenever the executor steps to a
|
||||
// new segment so the caller can reconstruct the full execution timeline
|
||||
// without relying on the bounded tail buffer. Resets on plan install.
|
||||
if (segIdx != _lastObservedSegmentIndex)
|
||||
{
|
||||
_lastObservedSegmentIndex = segIdx;
|
||||
_infoLog?.Invoke(
|
||||
$"[PathDiag] seg->{segIdx}/{_executor.TotalSegments} pos=({pos.X:F2},{pos.Y:F2},{pos.Z:F2}) yaw={physics.Yaw:F1} vy={physics.DeltaMovement.Y:F3} og={physics.OnGround} " +
|
||||
(seg is null ? "none" : $"{seg.MoveType} ({seg.Start.X:F1},{seg.Start.Y:F1},{seg.Start.Z:F1})->({seg.End.X:F1},{seg.End.Y:F1},{seg.End.Z:F1}) exit={seg.ExitTransition}"));
|
||||
}
|
||||
|
||||
_diagnosticsTail.Enqueue(
|
||||
$"pos=({pos.X:F2},{pos.Y:F2},{pos.Z:F2}) yaw={physics.Yaw:F1} vy={physics.DeltaMovement.Y:F3} vx={physics.DeltaMovement.X:F3} vz={physics.DeltaMovement.Z:F3} og={physics.OnGround} {segStr}");
|
||||
while (_diagnosticsTail.Count > DiagnosticsTailSize)
|
||||
_diagnosticsTail.Dequeue();
|
||||
}
|
||||
|
||||
private void EmitSegmentFailureDiagnostics(Location pos)
|
||||
{
|
||||
if (_executor is null)
|
||||
return;
|
||||
PathSegment? seg = _executor.CurrentSegment;
|
||||
int segIdx = _executor.CurrentIndex;
|
||||
_infoLog?.Invoke($"[PathDiag] Failure context: pos=({pos.X:F2},{pos.Y:F2},{pos.Z:F2}) failingSeg={segIdx}/{_executor.TotalSegments} " +
|
||||
(seg is null ? "seg=<none>" : $"seg={seg.MoveType} ({seg.Start.X:F1},{seg.Start.Y:F1},{seg.Start.Z:F1})->({seg.End.X:F1},{seg.End.Y:F1},{seg.End.Z:F1}) exit={seg.ExitTransition}"));
|
||||
if (_diagnosticsTail.Count > 0)
|
||||
{
|
||||
_infoLog?.Invoke($"[PathDiag] Recent tick trace (last {_diagnosticsTail.Count}):");
|
||||
int i = 0;
|
||||
foreach (string line in _diagnosticsTail)
|
||||
_infoLog?.Invoke($"[PathDiag] t-{_diagnosticsTail.Count - i++ - 1}: {line}");
|
||||
}
|
||||
}
|
||||
|
||||
private void EmitPathDumpDiagnostics(string label, PathResult result, int startIdx = 0)
|
||||
{
|
||||
if (!DiagnosticsEnabled)
|
||||
return;
|
||||
_infoLog?.Invoke($"[PathDiag] {label}: {result.Path.Count} waypoints, status={result.Status}, nodes={result.NodesExplored}, time={result.ElapsedMs}ms");
|
||||
int count = result.Path.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var node = result.Path[i];
|
||||
string move = i == 0 ? "Start" : node.MoveUsed.ToString();
|
||||
_infoLog?.Invoke($"[PathDiag] [{startIdx + i:D2}] {move,-22} ({node.X},{node.Y},{node.Z})");
|
||||
}
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
if (_executor is not null || _pendingReplan is not null || _nextExecutor is not null)
|
||||
|
|
@ -254,10 +378,16 @@ namespace MinecraftClient.Pathing.Execution
|
|||
_pendingReplanCts = null;
|
||||
cts?.Dispose();
|
||||
|
||||
bool isInitial = _isInitialPlan;
|
||||
_isInitialPlan = false;
|
||||
|
||||
if (task.IsFaulted || task.IsCanceled)
|
||||
{
|
||||
_infoLog?.Invoke("[PathMgr] Replan task failed or was cancelled.");
|
||||
_observer?.OnReplanFailed(_replanCount, pos);
|
||||
_infoLog?.Invoke(isInitial
|
||||
? "[PathMgr] Initial plan failed or was cancelled."
|
||||
: "[PathMgr] Replan task failed or was cancelled.");
|
||||
if (!isInitial)
|
||||
_observer?.OnReplanFailed(_replanCount, pos);
|
||||
_goal = null;
|
||||
_executor = null;
|
||||
_nextExecutor = null;
|
||||
|
|
@ -283,8 +413,11 @@ namespace MinecraftClient.Pathing.Execution
|
|||
|
||||
if (result.Status == PathStatus.Failed || result.Path.Count < 2)
|
||||
{
|
||||
_observer?.OnReplanFailed(_replanCount, pos);
|
||||
_infoLog?.Invoke("[PathMgr] Replan failed -- no path found.");
|
||||
if (!isInitial)
|
||||
_observer?.OnReplanFailed(_replanCount, pos);
|
||||
_infoLog?.Invoke(isInitial
|
||||
? $"[PathMgr] No path found (nodes={result.NodesExplored}, time={result.ElapsedMs}ms)."
|
||||
: "[PathMgr] Replan failed -- no path found.");
|
||||
_executor = null;
|
||||
_nextExecutor = null;
|
||||
_goal = null;
|
||||
|
|
@ -292,10 +425,25 @@ namespace MinecraftClient.Pathing.Execution
|
|||
}
|
||||
|
||||
var segments = PathSegmentBuilder.FromPath(result.Path);
|
||||
_observer?.OnReplanSucceeded(_replanCount, segments);
|
||||
_executor = new PathExecutor(segments, _debugLog, _observer);
|
||||
_nextExecutor = null;
|
||||
_infoLog?.Invoke($"[PathMgr] Replanned: {segments.Count} segments (replan #{_replanCount})");
|
||||
_lastPlan = result;
|
||||
_diagnosticsTail.Clear();
|
||||
_lastObservedSegmentIndex = -1;
|
||||
if (isInitial)
|
||||
{
|
||||
_executor = new PathExecutor(segments, _debugLog, _observer);
|
||||
_nextExecutor = null;
|
||||
string partial = result.Status == PathStatus.Partial ? " (partial)" : "";
|
||||
_infoLog?.Invoke($"[PathMgr] Navigation started: {segments.Count} segments, nodes={result.NodesExplored}, time={result.ElapsedMs}ms{partial}");
|
||||
EmitPathDumpDiagnostics("Initial plan", result);
|
||||
}
|
||||
else
|
||||
{
|
||||
_observer?.OnReplanSucceeded(_replanCount, segments);
|
||||
_executor = new PathExecutor(segments, _debugLog, _observer);
|
||||
_nextExecutor = null;
|
||||
_infoLog?.Invoke($"[PathMgr] Replanned: {segments.Count} segments (replan #{_replanCount})");
|
||||
EmitPathDumpDiagnostics($"Replan #{_replanCount} plan", result);
|
||||
}
|
||||
}
|
||||
|
||||
private void MaybeStartLookahead(World world)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,20 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
private const double EdgeCloseDistance = 1.2;
|
||||
private const double LateralAlignmentTolerance = 0.2;
|
||||
|
||||
// Diagonal-ascend velocity alignment constants. Live-server
|
||||
// regression: when A* routes through an "island" diagonal 1-block
|
||||
// riser whose preceding segment delivered axis-aligned ground
|
||||
// momentum (e.g. a cardinal Traverse along +Z landing at the foot of
|
||||
// a -X+Z+Y riser), the 1-tick sprint-jump boost cannot redirect the
|
||||
// perpendicular component onto the diagonal and the bot overshoots
|
||||
// the target along the cardinal axis. Before firing Jump we hold
|
||||
// Forward/Sprint off for up to a small window so ground friction can
|
||||
// decay the perpendicular component; if we have not aligned within
|
||||
// the window we take off anyway so the bot never stalls on the
|
||||
// source block indefinitely.
|
||||
private const double DiagonalTakeoffMaxPerpVelocity = 0.08;
|
||||
private const int DiagonalTakeoffMaxBrakeTicks = 6;
|
||||
|
||||
public Location ExpectedStart { get; }
|
||||
public Location ExpectedEnd { get; }
|
||||
|
||||
|
|
@ -29,6 +43,7 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
private Location _lastPos;
|
||||
private int _stuckTicks;
|
||||
private bool _initiatedJump;
|
||||
private int _diagonalBrakeTicks;
|
||||
|
||||
public AscendTemplate(PathSegment segment, PathSegment? nextSegment)
|
||||
{
|
||||
|
|
@ -57,7 +72,16 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
float targetYaw = TemplateHelper.CalculateYaw(dx, dz);
|
||||
float targetPitch = TemplateHelper.CalculatePitch(dx, dy, dz);
|
||||
if (!groundedPrepareJumpHandoff)
|
||||
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, targetYaw);
|
||||
{
|
||||
// Snap yaw on the first tick so we don't drift sideways while
|
||||
// rotating from a stale orientation (e.g. after a teleport or a
|
||||
// sharp turn transition). The Ascend template also already gates
|
||||
// forward input on headingReady below, but snapping removes one
|
||||
// source of wasted ticks for narrow 1-block staircases.
|
||||
physics.Yaw = _tickCount == 1
|
||||
? targetYaw
|
||||
: TemplateHelper.SmoothYaw(physics.Yaw, targetYaw);
|
||||
}
|
||||
physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch);
|
||||
float headingPenalty = YawDifference(physics.Yaw, targetYaw);
|
||||
bool headingReady = headingPenalty <= 8.0;
|
||||
|
|
@ -75,17 +99,69 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
bool laterallyAligned = sideDist <= LateralAlignmentTolerance;
|
||||
|
||||
bool jumpReady;
|
||||
if (HasHeadBonkClear(world))
|
||||
if (diagonalAscend)
|
||||
{
|
||||
// Diagonal Ascend only reaches this path when the search
|
||||
// layer has cleared the move (cardinal split not
|
||||
// feasible). The source-center to target-center distance
|
||||
// is ~sqrt(2) blocks, so the cardinal closeToEdge /
|
||||
// sideDist gates below never fire and would stall the
|
||||
// jump indefinitely; the bot must leap from the source
|
||||
// block center as soon as its heading is aligned with
|
||||
// the diagonal AND the horizontal velocity is close to
|
||||
// the diagonal direction. If the bot arrives with strong
|
||||
// cardinal momentum from a preceding Traverse (the
|
||||
// common case for wall-shoulder islands), fire one or
|
||||
// more ground ticks with Forward/Sprint released so
|
||||
// friction can decay the perpendicular component before
|
||||
// takeoff. Without this the preserved cardinal momentum
|
||||
// leaks the landing footprint off the target block.
|
||||
if (!headingReady)
|
||||
{
|
||||
jumpReady = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
double diagLen = Math.Sqrt(
|
||||
(double)_segment.HeadingX * _segment.HeadingX
|
||||
+ (double)_segment.HeadingZ * _segment.HeadingZ);
|
||||
double dirX = _segment.HeadingX / diagLen;
|
||||
double dirZ = _segment.HeadingZ / diagLen;
|
||||
double vx = physics.DeltaMovement.X;
|
||||
double vz = physics.DeltaMovement.Z;
|
||||
double perpMag = Math.Abs(vx * dirZ - vz * dirX);
|
||||
if (perpMag > DiagonalTakeoffMaxPerpVelocity
|
||||
&& _diagonalBrakeTicks < DiagonalTakeoffMaxBrakeTicks)
|
||||
{
|
||||
// Suppress this tick's acceleration so vanilla
|
||||
// ground friction (~0.546/tick) alone decays the
|
||||
// perpendicular component, and nudge the back
|
||||
// input if the velocity is dominantly in the
|
||||
// perpendicular direction - the back-input vector
|
||||
// is along -yaw which is the reverse of the
|
||||
// diagonal, cancelling the perpendicular faster
|
||||
// than friction alone for high-speed entries.
|
||||
input.Forward = false;
|
||||
input.Sprint = false;
|
||||
double along = vx * dirX + vz * dirZ;
|
||||
if (perpMag > Math.Abs(along))
|
||||
input.Back = true;
|
||||
_diagonalBrakeTicks++;
|
||||
jumpReady = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
jumpReady = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (HasHeadBonkClear(world))
|
||||
{
|
||||
// Vertical head-room above the source block is clear, so starting the
|
||||
// jump early is safe and actually makes the short hop more reliable
|
||||
// (matches Baritone's "headBonkClear" shortcut).
|
||||
jumpReady = headingReady;
|
||||
}
|
||||
else if (diagonalAscend)
|
||||
{
|
||||
jumpReady = headingReady;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Mirror Baritone's gate: only jump when close to the riser and
|
||||
|
|
|
|||
|
|
@ -62,13 +62,46 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
float targetPitch = TemplateHelper.CalculatePitch(dx, dy, dz);
|
||||
physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch);
|
||||
|
||||
// Snap yaw on the first tick to avoid a few ticks of sideways drift
|
||||
// when the bot enters this segment with a stale orientation (e.g.
|
||||
// just after a teleport or after a turn). Ledge-adjacent descends
|
||||
// cannot tolerate drift without falling off the wrong side.
|
||||
if (_tickCount == 1)
|
||||
physics.Yaw = targetYaw;
|
||||
|
||||
if (physics.OnGround && Math.Abs(dy) < (_hasFallen ? 1.0 : 0.6))
|
||||
{
|
||||
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(_segment, _nextSegment, pos, physics, world);
|
||||
bool onOrPastTarget = TemplateFootingHelper.IsFootprintInsideTargetBlock(pos, ExpectedEnd)
|
||||
|| TemplateHelper.HasReachedSegmentEndPlane(pos, _segment);
|
||||
|
||||
// Fallback: after a diagonal descend landing the bot can end
|
||||
// up on a support block that is not yet the target block
|
||||
// (footprint still off the landing column). The braking
|
||||
// planner reads "remaining <= coastStop + lead" and returns
|
||||
// Coast, which zeroes every input - if the bot has already
|
||||
// come to rest this means the segment hangs forever and the
|
||||
// pathing manager replans. When we are stopped, not inside
|
||||
// the target block, and not being asked to brake, walk
|
||||
// toward the target instead of coasting so the landing
|
||||
// resolves in one tick-window.
|
||||
if (!decision.HoldBack
|
||||
&& !TemplateFootingHelper.IsFootprintInsideTargetBlock(pos, ExpectedEnd)
|
||||
&& horizDistSq > 0.01
|
||||
&& TemplateHelper.GetHorizontalSpeed(physics) < 0.03)
|
||||
{
|
||||
float walkYaw = TemplateHelper.CalculateYaw(dx, dz);
|
||||
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, walkYaw);
|
||||
input.Forward = true;
|
||||
input.Sprint = _needsSprint;
|
||||
|
||||
if (GroundedSegmentController.ShouldComplete(_segment, pos, physics))
|
||||
return TemplateState.Complete;
|
||||
return TemplateState.InProgress;
|
||||
}
|
||||
|
||||
if (horizDistSq > 0.01 && !decision.HoldBack)
|
||||
{
|
||||
bool onOrPastTarget = TemplateFootingHelper.IsFootprintInsideTargetBlock(pos, ExpectedEnd)
|
||||
|| TemplateHelper.HasReachedSegmentEndPlane(pos, _segment);
|
||||
float groundedYaw = onOrPastTarget
|
||||
? TemplateHelper.GetExitHeadingYaw(_segment)
|
||||
: TemplateHelper.ShouldBiasTowardExitHeading(pos, _segment)
|
||||
|
|
@ -96,8 +129,27 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
{
|
||||
bool onOrPastTarget = TemplateFootingHelper.IsFootprintInsideTargetBlock(pos, ExpectedEnd)
|
||||
|| TemplateHelper.HasReachedSegmentEndPlane(pos, _segment);
|
||||
bool biasTowardExitInAir = onOrPastTarget
|
||||
|| (_hasFallen && TemplateHelper.ShouldBiasTowardExitHeading(pos, _segment, distanceThreshold: 1.5));
|
||||
// Airborne bias toward the exit heading is only safe when
|
||||
// the bot has effectively finished the current segment's
|
||||
// horizontal travel: either the footprint is inside the
|
||||
// landing block, or the vertical drop is small enough that
|
||||
// lateral drift cannot miss the 1x1 landing column. For
|
||||
// multi-block drops the bot is in the air for 8+ ticks;
|
||||
// rotating yaw mid-fall (e.g. after crossing the end plane
|
||||
// but still 1-2 blocks above landing) pushes sprint/walk
|
||||
// momentum perpendicular to the segment and drifts the bot
|
||||
// off the landing column into the void. Keep yaw pointed
|
||||
// at the landing center through the whole fall on multi-Y
|
||||
// descends; GroundedSegmentController rotates yaw once the
|
||||
// bot is actually standing on the landing column.
|
||||
double segmentYDrop = _segment.Start.Y - _segment.End.Y;
|
||||
bool isSingleStepDescend = segmentYDrop <= 1.0;
|
||||
bool footInsideTarget = TemplateFootingHelper.IsFootprintInsideTargetBlock(pos, ExpectedEnd);
|
||||
bool biasTowardExitInAir = footInsideTarget
|
||||
|| (isSingleStepDescend
|
||||
&& (onOrPastTarget
|
||||
|| (_hasFallen
|
||||
&& TemplateHelper.ShouldBiasTowardExitHeading(pos, _segment, distanceThreshold: 1.5))));
|
||||
float airborneYaw = biasTowardExitInAir
|
||||
? TemplateHelper.GetExitHeadingYaw(_segment)
|
||||
: targetYaw;
|
||||
|
|
@ -117,7 +169,30 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
else
|
||||
{
|
||||
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(_segment, _nextSegment, pos, physics, world);
|
||||
if (_segment.ExitHints.AllowAirBrake)
|
||||
// Multi-block descend overshoot guard: when the
|
||||
// fall spans 2+ Y blocks, sprint momentum will
|
||||
// carry the bot roughly one extra horizontal
|
||||
// block past the planned landing. If the next
|
||||
// segment prepares a jump (PrepareJump exit) the
|
||||
// bot MUST land inside the planned 1x1 landing
|
||||
// column so the jump takeoff has a valid footing;
|
||||
// overshooting drops into the void or onto a
|
||||
// block 1-2 tiers below, breaking the jump.
|
||||
// Once airborne and past the landing end-plane,
|
||||
// release forward input so sprint momentum decays
|
||||
// via air drag over the final 1-2 ticks of fall,
|
||||
// pulling the bot back into the landing column.
|
||||
bool riskyOvershoot = _hasFallen
|
||||
&& segmentYDrop >= 2.0
|
||||
&& onOrPastTarget
|
||||
&& _segment.ExitTransition == PathTransitionType.PrepareJump;
|
||||
if (riskyOvershoot)
|
||||
{
|
||||
input.Forward = false;
|
||||
input.Sprint = false;
|
||||
input.Back = true;
|
||||
}
|
||||
else if (_segment.ExitHints.AllowAirBrake)
|
||||
{
|
||||
TemplateHelper.ApplyDecision(input, decision);
|
||||
if (decision.HoldForward && _needsSprint)
|
||||
|
|
|
|||
|
|
@ -25,13 +25,36 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
return;
|
||||
}
|
||||
|
||||
if (TemplateHelper.ShouldBiasTowardExitHeading(pos, segment))
|
||||
TemplateHelper.FaceExitHeading(physics, segment);
|
||||
|
||||
// Compute the braking decision first so rotation and input stay
|
||||
// consistent. Applying the exit-heading bias while we are still
|
||||
// braking causes the Back input (which acts opposite to yaw) to
|
||||
// push the bot perpendicular to the segment line, which on narrow
|
||||
// 1-block walkways turns into a side-off-the-edge step. Stay on
|
||||
// the segment heading for as long as we are braking and only let
|
||||
// the bias rotate us once the brake has released.
|
||||
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(segment, nextSegment, pos, physics, world);
|
||||
TemplateHelper.ApplyDecision(input, decision);
|
||||
if (decision.HoldBack)
|
||||
{
|
||||
TemplateHelper.FaceSegmentHeading(physics, segment);
|
||||
return;
|
||||
}
|
||||
|
||||
// On stable-footing Turn exits (the next segment is a walk-like
|
||||
// move, not a jump) the next template snaps yaw instantly on
|
||||
// its first tick, so pre-rotating here is unnecessary. On
|
||||
// narrow 1-block walkways the bias combined with along-segment
|
||||
// momentum pushes the bot perpendicular to the walkway and
|
||||
// walks it off the edge (the bot sprint-drifts diagonally
|
||||
// while yaw rotates ~45 deg mid-stride). For Turn exits into
|
||||
// a jump (RequireJumpReady) we still need to align yaw before
|
||||
// takeoff, so keep the bias there.
|
||||
bool suppressBiasForSafeTurn = segment.ExitTransition == PathTransitionType.Turn
|
||||
&& segment.ExitHints.RequireStableFooting
|
||||
&& !segment.ExitHints.RequireJumpReady;
|
||||
if (!suppressBiasForSafeTurn
|
||||
&& TemplateHelper.ShouldBiasTowardExitHeading(pos, segment))
|
||||
TemplateHelper.FaceExitHeading(physics, segment);
|
||||
}
|
||||
|
||||
internal static bool ShouldComplete(PathSegment segment, Location pos, PlayerPhysics physics)
|
||||
|
|
@ -61,8 +84,26 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
}
|
||||
|
||||
double exitSpeed = TemplateHelper.ProjectHorizontalSpeedAlongHint(physics, segment);
|
||||
bool headingReady = TemplateHelper.HeadingPenaltyDegrees(physics.Yaw, segment)
|
||||
<= (segment.ExitHints.RequireJumpReady ? 8.0 : 15.0);
|
||||
// On Turn exits we deliberately do NOT pre-rotate yaw toward the
|
||||
// next segment's heading (see Apply() above). The next segment's
|
||||
// template snaps yaw on its first tick, so measuring heading
|
||||
// readiness against the exit heading here would deadlock the
|
||||
// handoff (bot is still facing segment heading, would never pass
|
||||
// the 15 deg gate). Measure against segment heading for Turn
|
||||
// exits with stable footing where yaw will be snapped anyway.
|
||||
bool headingReady;
|
||||
if (segment.ExitTransition == PathTransitionType.Turn
|
||||
&& segment.ExitHints.RequireStableFooting
|
||||
&& !segment.ExitHints.RequireJumpReady)
|
||||
{
|
||||
headingReady = TemplateHelper.HeadingPenaltyDegrees(physics.Yaw, segment.HeadingX, segment.HeadingZ) <= 25.0
|
||||
|| TemplateHelper.HeadingPenaltyDegrees(physics.Yaw, segment) <= 15.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
headingReady = TemplateHelper.HeadingPenaltyDegrees(physics.Yaw, segment)
|
||||
<= (segment.ExitHints.RequireJumpReady ? 8.0 : 15.0);
|
||||
}
|
||||
|
||||
if (!headingReady)
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
private int _tickCount;
|
||||
private Location _lastPos;
|
||||
private int _stuckTicks;
|
||||
private int _airborneTicks;
|
||||
|
||||
public WalkTemplate(PathSegment segment, PathSegment? nextSegment)
|
||||
{
|
||||
|
|
@ -35,11 +36,42 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
double dx = ExpectedEnd.X - pos.X;
|
||||
double dz = ExpectedEnd.Z - pos.Z;
|
||||
double dy = ExpectedEnd.Y - pos.Y;
|
||||
float targetYaw = TemplateHelper.ShouldBiasTowardExitHeading(pos, _segment)
|
||||
? TemplateHelper.GetExitHeadingYaw(_segment)
|
||||
: TemplateHelper.CalculateYaw(dx, dz);
|
||||
// While approaching the end, steer via pos->end so lateral drift self-
|
||||
// corrects. Once the center has entered the target block the pos->end
|
||||
// vector becomes tiny/negative and flips yaw by ~180 degrees, which
|
||||
// fights GroundedSegmentController's exit-heading rotation and locks
|
||||
// yaw at a local equilibrium (e.g. 333 deg on a 1,1 diagonal) where
|
||||
// HeadingPenalty never drops below the 8 deg ShouldComplete gate.
|
||||
// Fall back to the stable quantized segment heading once inside the
|
||||
// target block so the completion check and exit rotation converge.
|
||||
// Skip the exit-heading bias on stable-footing Turn exits: it
|
||||
// rotates yaw mid-segment while the bot still has along-segment
|
||||
// momentum, which on a 1-block walkway drifts the bot
|
||||
// perpendicular and walks it off the edge. The next segment's
|
||||
// template snaps yaw on its first tick, so nothing is lost by
|
||||
// deferring the rotation. Keep the bias when the next segment
|
||||
// is a jump (RequireJumpReady): we need yaw aligned before
|
||||
// takeoff or the jump direction will be off.
|
||||
bool suppressBiasForSafeTurn = _segment.ExitTransition == PathTransitionType.Turn
|
||||
&& _segment.ExitHints.RequireStableFooting
|
||||
&& !_segment.ExitHints.RequireJumpReady;
|
||||
float targetYaw;
|
||||
if (!suppressBiasForSafeTurn
|
||||
&& TemplateHelper.ShouldBiasTowardExitHeading(pos, _segment))
|
||||
targetYaw = TemplateHelper.GetExitHeadingYaw(_segment);
|
||||
else if (TemplateFootingHelper.IsCenterInsideTargetBlock(pos, _segment.End))
|
||||
targetYaw = TemplateHelper.CalculateYaw(_segment.HeadingX, _segment.HeadingZ);
|
||||
else
|
||||
targetYaw = TemplateHelper.CalculateYaw(dx, dz);
|
||||
float targetPitch = TemplateHelper.CalculatePitch(dx, dy, dz);
|
||||
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, targetYaw);
|
||||
// Snap yaw on the first tick so we don't push forward input while the
|
||||
// bot is still rotating from whatever yaw it had before this segment
|
||||
// started (e.g. a random post-teleport orientation). Baritone-style:
|
||||
// the server accepts instant yaw updates and the narrow 1-block lanes
|
||||
// in parkour courses don't tolerate 3 ticks of sideways drift.
|
||||
physics.Yaw = _tickCount == 1
|
||||
? targetYaw
|
||||
: TemplateHelper.SmoothYaw(physics.Yaw, targetYaw);
|
||||
physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch);
|
||||
|
||||
GroundedSegmentController.Apply(_segment, _nextSegment, pos, physics, input, world);
|
||||
|
|
@ -51,6 +83,15 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
_stuckTicks = movedSq < 0.0005 ? _stuckTicks + 1 : 0;
|
||||
_lastPos = pos;
|
||||
|
||||
// Walk/Diagonal is a grounded move: if the bot is airborne for more
|
||||
// than a handful of ticks the platform is gone beneath us (e.g. we
|
||||
// rotated toward an exit heading on a narrow 1-block walkway and
|
||||
// stepped off the edge). Fail fast so the replanner can recover
|
||||
// before gravity carries the bot 10+ blocks out of position.
|
||||
_airborneTicks = physics.OnGround ? 0 : _airborneTicks + 1;
|
||||
if (_airborneTicks > 8)
|
||||
return TemplateState.Failed;
|
||||
|
||||
int maxTicks = _segment.ExitTransition switch
|
||||
{
|
||||
PathTransitionType.ContinueStraight => 100,
|
||||
|
|
|
|||
|
|
@ -35,6 +35,19 @@ namespace MinecraftClient.Pathing.Moves.Impl
|
|||
return;
|
||||
}
|
||||
|
||||
// The landing feet column must also be passable. Without this, a descend
|
||||
// into a column whose y-1 block is solid (e.g. a 2-block thick platform top
|
||||
// where (destX,y-1) is stone and (destX,y-2) is also stone) would be
|
||||
// accepted: the solid y-2 floor satisfies CanWalkOn, the y/y+1 body space
|
||||
// satisfies the step, but physically the bot just walks onto the solid
|
||||
// y-1 block at the same feet level and the Descend template waits forever
|
||||
// for a drop that can never happen -- producing an infinite replan loop.
|
||||
if (!ctx.CanWalkThrough(destX, y - 1, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't descend from ladder/vine (unreliable)
|
||||
Material fromDown = ctx.GetMaterial(x, y - 1, z);
|
||||
if (fromDown.CanBeClimbedOn())
|
||||
|
|
|
|||
|
|
@ -7,34 +7,49 @@ namespace MinecraftClient.Pathing.Moves;
|
|||
|
||||
/// <summary>
|
||||
/// Dynamic expander for every move in the jump family (Walk, Step,
|
||||
/// SprintJump, Sidewall). Iterates a declarative descriptor table and calls
|
||||
/// <see cref="JumpFeasibility.Evaluate"/> for each entry without allocating
|
||||
/// an IMove object per direction.
|
||||
/// SprintJump, Sidewall). Walk, Step, diagonal SprintJump and Sidewall are
|
||||
/// still driven by a declarative descriptor table that calls
|
||||
/// <see cref="JumpFeasibility.Evaluate"/> one entry at a time. Cardinal
|
||||
/// SprintJumps are produced by <see cref="ProbeCardinal"/>, a Baritone-style
|
||||
/// near-to-far scan that emits at most one candidate per direction -- letting
|
||||
/// A* re-probe from each landing instead of enumerating every (distance,
|
||||
/// yDelta) combination.
|
||||
///
|
||||
/// The hot path hoists per-node guards (AllowParkour, head clearance,
|
||||
/// takeoff material, adjacent-wall presence) and precomputes an 8-direction
|
||||
/// "first-step has no floor" table so entire descriptor groups can be
|
||||
/// rejected in O(1) before touching <see cref="JumpFeasibility"/>. Ordinary
|
||||
/// ground-walking nodes skip all ~170 jump descriptors this way; nodes
|
||||
/// without any adjacent wall skip all 112 sidewall descriptors.
|
||||
/// ground-walking nodes skip every jump descriptor this way; nodes without
|
||||
/// any adjacent wall skip all sidewall descriptors.
|
||||
/// </summary>
|
||||
public sealed class JumpExpander : IMoveExpander
|
||||
{
|
||||
/// <summary>
|
||||
/// Extra slots in the neighbor buffer for the 4 cardinal probes. Each
|
||||
/// probe may emit up to one sprint-jump candidate per yDelta (4 total)
|
||||
/// plus up to one sidewall candidate per (lateral sign, yDelta) (8
|
||||
/// total), so 4 directions * (4 + 8) = 48 slots. The probe almost never
|
||||
/// fills every slot; this is a generous upper bound that keeps the
|
||||
/// neighbor buffer stack-allocated.
|
||||
/// </summary>
|
||||
private const int CardinalProbeSlots = 48;
|
||||
|
||||
private static readonly JumpDescriptor[] _descriptors = BuildDescriptors();
|
||||
|
||||
public int MaxNeighbors => _descriptors.Length;
|
||||
public int MaxNeighbors => _descriptors.Length + CardinalProbeSlots;
|
||||
|
||||
public int Expand(CalculationContext ctx, int x, int y, int z, Span<MoveNeighbor> buffer)
|
||||
{
|
||||
int count = 0;
|
||||
MoveResult result = default;
|
||||
|
||||
// ---- Per-node preconditions (shared by every SprintJump + Sidewall descriptor) ----
|
||||
// These are the first checks JumpFeasibility.Evaluate* would make. Hoisting
|
||||
// them once turns ~170 method calls per node into one branch in the hot path.
|
||||
// ---- Per-node preconditions shared by every jump-family move ----
|
||||
// These are the first checks JumpFeasibility.Evaluate* would make.
|
||||
// Hoisting them once turns many method calls per node into one
|
||||
// branch in the hot path. "canSprintTakeoff" gates both the
|
||||
// descriptor loop's SprintJump entries and all four cardinal probes.
|
||||
bool jumpFamilyAllowed = ctx.AllowParkour && ctx.CanSprint;
|
||||
bool canSprintTakeoff = false;
|
||||
bool hasAdjacentWall = false;
|
||||
if (jumpFamilyAllowed)
|
||||
{
|
||||
Material standingOn = ctx.GetMaterial(x, y - 1, z);
|
||||
|
|
@ -43,9 +58,6 @@ public sealed class JumpExpander : IMoveExpander
|
|||
!standingOn.CanBeClimbedOn()
|
||||
&& !atFeet.IsLiquid()
|
||||
&& ctx.CanWalkThrough(x, y + 2, z);
|
||||
|
||||
if (canSprintTakeoff)
|
||||
hasAdjacentWall = HasAnyAdjacentWall(ctx, x, y, z);
|
||||
}
|
||||
|
||||
// ---- Per-direction gap table (SprintJump only) ----
|
||||
|
|
@ -88,9 +100,8 @@ public sealed class JumpExpander : IMoveExpander
|
|||
}
|
||||
break;
|
||||
case JumpFlavor.Sidewall:
|
||||
if (!canSprintTakeoff || !hasAdjacentWall)
|
||||
continue;
|
||||
break;
|
||||
// Sidewall candidates are produced by ProbeCardinal now.
|
||||
continue;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
@ -104,23 +115,254 @@ public sealed class JumpExpander : IMoveExpander
|
|||
if (count < buffer.Length)
|
||||
buffer[count++] = new MoveNeighbor(result, type);
|
||||
}
|
||||
|
||||
// ---- Cardinal SprintJump probes (Baritone-style near-to-far scan) ----
|
||||
// Each cardinal direction probes distance 2..5 and emits at most one
|
||||
// candidate (the closest feasible landing). A* re-probes from that
|
||||
// landing to discover longer variants, which keeps the frontier small
|
||||
// while preserving reachability.
|
||||
if (canSprintTakeoff)
|
||||
{
|
||||
ProbeCardinal(ctx, x, y, z, +1, 0, directionGapOpen, buffer, ref count, ref result);
|
||||
ProbeCardinal(ctx, x, y, z, -1, 0, directionGapOpen, buffer, ref count, ref result);
|
||||
ProbeCardinal(ctx, x, y, z, 0, +1, directionGapOpen, buffer, ref count, ref result);
|
||||
ProbeCardinal(ctx, x, y, z, 0, -1, directionGapOpen, buffer, ref count, ref result);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Conservative O(1) short-circuit for the Sidewall family. Every sidewall
|
||||
/// descriptor needs a solid block one lateral step from the takeoff at
|
||||
/// <c>y</c> or <c>y+1</c>, i.e. at a cardinal neighbor. If all four
|
||||
/// cardinal neighbors at both heights are walk-through, there is no wall
|
||||
/// to cling to and all 112 sidewall descriptors can be skipped without
|
||||
/// calling <see cref="JumpFeasibility"/>.
|
||||
/// Scans a single cardinal direction <c>(fx, fz)</c> for both sprint-jump
|
||||
/// and sidewall landings. The forward air corridor is swept once
|
||||
/// (Baritone-style monotonic scan with early break on obstruction) and
|
||||
/// every feasible landing shape shares that sweep. Per-(lateral, yDelta)
|
||||
/// sidewall candidates use the same <c>i</c> iteration to locate their
|
||||
/// landing on the lateral column, so a single O(5) scan replaces the
|
||||
/// ~8 + 16 static descriptor entries this direction used to need.
|
||||
///
|
||||
/// Instead of emitting the closest valid landing (Baritone's choice),
|
||||
/// the probe records the farthest valid landing per shape bucket and
|
||||
/// emits one candidate each. Preferring the longer jump keeps A*'s path
|
||||
/// cost low and avoids chains of short d=2 parkour jumps that MCC's
|
||||
/// template can overshoot when sprint momentum is carried over.
|
||||
/// </summary>
|
||||
private static bool HasAnyAdjacentWall(CalculationContext ctx, int x, int y, int z)
|
||||
private static void ProbeCardinal(
|
||||
CalculationContext ctx,
|
||||
int x, int y, int z,
|
||||
int fx, int fz,
|
||||
ReadOnlySpan<bool> directionGapOpen,
|
||||
Span<MoveNeighbor> buffer,
|
||||
ref int count,
|
||||
ref MoveResult result)
|
||||
{
|
||||
return !ctx.CanWalkThrough(x + 1, y, z) || !ctx.CanWalkThrough(x + 1, y + 1, z)
|
||||
|| !ctx.CanWalkThrough(x - 1, y, z) || !ctx.CanWalkThrough(x - 1, y + 1, z)
|
||||
|| !ctx.CanWalkThrough(x, y, z + 1) || !ctx.CanWalkThrough(x, y + 1, z + 1)
|
||||
|| !ctx.CanWalkThrough(x, y, z - 1) || !ctx.CanWalkThrough(x, y + 1, z - 1);
|
||||
// If the first step has a floor, a cheaper Walk move covers this
|
||||
// direction already (Baritone: "don't parkour if we could just
|
||||
// traverse"). Use the precomputed gap table.
|
||||
int firstStepIdx = ((fx + 1) * 3) + (fz + 1);
|
||||
if (!directionGapOpen[firstStepIdx])
|
||||
return;
|
||||
|
||||
// The first step's column (y, y+1) must be passable; without it the
|
||||
// player hits a wall before leaving the takeoff block. (y+2 over the
|
||||
// takeoff itself is guaranteed by canSprintTakeoff.)
|
||||
int sx1 = x + fx;
|
||||
int sz1 = z + fz;
|
||||
if (!ctx.CanWalkThrough(sx1, y, sz1) || !ctx.CanWalkThrough(sx1, y + 1, sz1))
|
||||
return;
|
||||
|
||||
// Lateral unit vectors perpendicular to (fx, fz). Positive and
|
||||
// negative sides are tracked independently so the wall presence
|
||||
// short-circuit applies per side.
|
||||
int lxP, lzP, lxN, lzN;
|
||||
if (fx != 0)
|
||||
{
|
||||
lxP = 0; lzP = +1;
|
||||
lxN = 0; lzN = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
lxP = +1; lzP = 0;
|
||||
lxN = -1; lzN = 0;
|
||||
}
|
||||
|
||||
// Sidewall needs a solid block immediately lateral to the takeoff
|
||||
// (step=0 in HasSidewallArcClearance). If that cell is walk-through
|
||||
// at both y and y+1, no sidewall candidate from this takeoff can
|
||||
// succeed along that lateral sign.
|
||||
bool wallP = !ctx.CanWalkThrough(x + lxP, y, z + lzP)
|
||||
|| !ctx.CanWalkThrough(x + lxP, y + 1, z + lzP);
|
||||
bool wallN = !ctx.CanWalkThrough(x + lxN, y, z + lzN)
|
||||
|| !ctx.CanWalkThrough(x + lxN, y + 1, z + lzN);
|
||||
|
||||
// Farthest valid i for each sprint-jump shape.
|
||||
int bestAscend = 0;
|
||||
int bestFlat = 0;
|
||||
int bestDescend1 = 0;
|
||||
int bestDescend2 = 0;
|
||||
|
||||
// Farthest valid i per (lateral sign, yDelta) for sidewall.
|
||||
// yDelta indices: 0=+1, 1=0, 2=-1, 3=-2.
|
||||
int bestSwP0 = 0, bestSwP1 = 0, bestSwP2 = 0, bestSwP3 = 0;
|
||||
int bestSwN0 = 0, bestSwN1 = 0, bestSwN2 = 0, bestSwN3 = 0;
|
||||
|
||||
const int MaxJumpDistance = 5;
|
||||
for (int i = 2; i <= MaxJumpDistance; i++)
|
||||
{
|
||||
int dx = x + fx * i;
|
||||
int dz = z + fz * i;
|
||||
|
||||
// Shared head-height air corridor. If blocked the whole arc is
|
||||
// interrupted; every larger i is also unreachable for both
|
||||
// sprint jump and sidewall.
|
||||
if (!ctx.CanWalkThrough(dx, y + 1, dz) || !ctx.CanWalkThrough(dx, y + 2, dz))
|
||||
break;
|
||||
|
||||
if (!ctx.CanWalkThrough(dx, y, dz))
|
||||
{
|
||||
// Foot-height is blocked. Only sprint-jump ascend is
|
||||
// potentially viable here, and only for i <= 3. Sidewall's
|
||||
// HasSidewallArcClearance requires a clear forward column
|
||||
// at every step, so no sidewall candidate survives past
|
||||
// this obstruction either.
|
||||
if (i <= 3 && ctx.CanWalkOn(dx, y, dz))
|
||||
bestAscend = i;
|
||||
break;
|
||||
}
|
||||
|
||||
// Foot-height is clear; record the best forward-axis landing.
|
||||
if (ctx.CanWalkOn(dx, y - 1, dz))
|
||||
bestFlat = i;
|
||||
else if (ctx.CanWalkOn(dx, y - 2, dz))
|
||||
bestDescend1 = i;
|
||||
else if (ctx.CanWalkOn(dx, y - 3, dz))
|
||||
bestDescend2 = i;
|
||||
|
||||
// Sidewall candidates land on the lateral column. The forward
|
||||
// corridor has already been validated above; HasSidewallArc-
|
||||
// Clearance's wall-depth and outside-lateral checks are deferred
|
||||
// to EvaluateSidewall.
|
||||
if (wallP)
|
||||
TrackSidewallCandidates(ctx, dx, y, dz, lxP, lzP, i,
|
||||
ref bestSwP0, ref bestSwP1, ref bestSwP2, ref bestSwP3);
|
||||
if (wallN)
|
||||
TrackSidewallCandidates(ctx, dx, y, dz, lxN, lzN, i,
|
||||
ref bestSwN0, ref bestSwN1, ref bestSwN2, ref bestSwN3);
|
||||
}
|
||||
|
||||
// Emit sprint-jump bests (MoveType.Parkour).
|
||||
if (bestAscend > 0)
|
||||
TryEmitSprintJump(ctx, x, y, z, fx * bestAscend, fz * bestAscend, +1, buffer, ref count, ref result);
|
||||
if (bestFlat > 0)
|
||||
TryEmitSprintJump(ctx, x, y, z, fx * bestFlat, fz * bestFlat, 0, buffer, ref count, ref result);
|
||||
if (bestDescend1 > 0)
|
||||
TryEmitSprintJump(ctx, x, y, z, fx * bestDescend1, fz * bestDescend1, -1, buffer, ref count, ref result);
|
||||
if (bestDescend2 > 0)
|
||||
TryEmitSprintJump(ctx, x, y, z, fx * bestDescend2, fz * bestDescend2, -2, buffer, ref count, ref result);
|
||||
|
||||
// Emit sidewall bests, one candidate per (lateral sign, yDelta).
|
||||
EmitSidewallIfAny(ctx, x, y, z, fx, fz, lxP, lzP, +1, bestSwP0, buffer, ref count, ref result);
|
||||
EmitSidewallIfAny(ctx, x, y, z, fx, fz, lxP, lzP, 0, bestSwP1, buffer, ref count, ref result);
|
||||
EmitSidewallIfAny(ctx, x, y, z, fx, fz, lxP, lzP, -1, bestSwP2, buffer, ref count, ref result);
|
||||
EmitSidewallIfAny(ctx, x, y, z, fx, fz, lxP, lzP, -2, bestSwP3, buffer, ref count, ref result);
|
||||
|
||||
EmitSidewallIfAny(ctx, x, y, z, fx, fz, lxN, lzN, +1, bestSwN0, buffer, ref count, ref result);
|
||||
EmitSidewallIfAny(ctx, x, y, z, fx, fz, lxN, lzN, 0, bestSwN1, buffer, ref count, ref result);
|
||||
EmitSidewallIfAny(ctx, x, y, z, fx, fz, lxN, lzN, -1, bestSwN2, buffer, ref count, ref result);
|
||||
EmitSidewallIfAny(ctx, x, y, z, fx, fz, lxN, lzN, -2, bestSwN3, buffer, ref count, ref result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cheap per-<c>i</c> pre-check for sidewall candidates. Updates the
|
||||
/// per-yDelta "farthest valid i" buckets whenever the lateral landing
|
||||
/// column matches the y offset. The expensive full feasibility check
|
||||
/// (<see cref="ParkourFeasibility.HasSidewallArcClearance"/> etc.) is
|
||||
/// still performed by <see cref="JumpFeasibility.EvaluateSidewall"/>
|
||||
/// on emission; this pre-check just filters out trivially-impossible
|
||||
/// iterations so Evaluate runs at most 8 times per direction.
|
||||
/// </summary>
|
||||
private static void TrackSidewallCandidates(
|
||||
CalculationContext ctx,
|
||||
int dx, int y, int dz,
|
||||
int lateralX, int lateralZ,
|
||||
int i,
|
||||
ref int bestPlus1,
|
||||
ref int bestFlat,
|
||||
ref int bestMinus1,
|
||||
ref int bestMinus2)
|
||||
{
|
||||
int lx = dx + lateralX;
|
||||
int lz = dz + lateralZ;
|
||||
|
||||
// yDelta = +1 (ascend). Only meaningful for i <= 3.
|
||||
if (i <= 3
|
||||
&& ctx.CanWalkOn(lx, y, lz)
|
||||
&& ctx.CanWalkThrough(lx, y + 1, lz)
|
||||
&& ctx.CanWalkThrough(lx, y + 2, lz))
|
||||
{
|
||||
bestPlus1 = i;
|
||||
}
|
||||
|
||||
// Destination column body clearance at flat/descend heights.
|
||||
if (!ctx.CanWalkThrough(lx, y, lz) || !ctx.CanWalkThrough(lx, y + 1, lz))
|
||||
return;
|
||||
|
||||
if (ctx.CanWalkOn(lx, y - 1, lz))
|
||||
bestFlat = i;
|
||||
else if (ctx.CanWalkOn(lx, y - 2, lz))
|
||||
bestMinus1 = i;
|
||||
else if (ctx.CanWalkOn(lx, y - 3, lz))
|
||||
bestMinus2 = i;
|
||||
}
|
||||
|
||||
private static void EmitSidewallIfAny(
|
||||
CalculationContext ctx,
|
||||
int x, int y, int z,
|
||||
int fx, int fz,
|
||||
int lateralX, int lateralZ,
|
||||
int yDelta,
|
||||
int bestI,
|
||||
Span<MoveNeighbor> buffer,
|
||||
ref int count,
|
||||
ref MoveResult result)
|
||||
{
|
||||
if (bestI <= 0)
|
||||
return;
|
||||
|
||||
int xOffset = fx * bestI + lateralX;
|
||||
int zOffset = fz * bestI + lateralZ;
|
||||
JumpDescriptor desc = new(xOffset, zOffset, yDelta, JumpFlavor.Sidewall);
|
||||
result.Cost = 0;
|
||||
JumpFeasibility.Evaluate(ctx, x, y, z, desc, ref result);
|
||||
if (result.IsImpossible)
|
||||
return;
|
||||
|
||||
if (count < buffer.Length)
|
||||
buffer[count++] = new MoveNeighbor(result, MoveType.Parkour);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a cardinal <see cref="JumpFlavor.SprintJump"/> descriptor for
|
||||
/// the probed shape and delegates to <see cref="JumpFeasibility.Evaluate"/>.
|
||||
/// The descriptor table and this probe share a single source of truth for
|
||||
/// run-up, flight path, overshoot, cost, and entry preparation.
|
||||
/// </summary>
|
||||
private static void TryEmitSprintJump(
|
||||
CalculationContext ctx,
|
||||
int x, int y, int z,
|
||||
int xOffset, int zOffset, int yDelta,
|
||||
Span<MoveNeighbor> buffer,
|
||||
ref int count,
|
||||
ref MoveResult result)
|
||||
{
|
||||
JumpDescriptor desc = new(xOffset, zOffset, yDelta, JumpFlavor.SprintJump);
|
||||
result.Cost = 0;
|
||||
JumpFeasibility.Evaluate(ctx, x, y, z, desc, ref result);
|
||||
if (result.IsImpossible)
|
||||
return;
|
||||
|
||||
if (count < buffer.Length)
|
||||
buffer[count++] = new MoveNeighbor(result, MoveType.Parkour);
|
||||
}
|
||||
|
||||
private static MoveType DeriveMoveType(JumpDescriptor d) => d.Flavor switch
|
||||
|
|
@ -160,29 +402,8 @@ public sealed class JumpExpander : IMoveExpander
|
|||
}
|
||||
}
|
||||
|
||||
// Cardinal parkour (flat / +1 / -1 / -2)
|
||||
foreach (int dx in offsets)
|
||||
{
|
||||
for (int d = 2; d <= 5; d++)
|
||||
list.Add(new JumpDescriptor(dx * d, 0, 0, JumpFlavor.SprintJump));
|
||||
for (int d = 2; d <= 3; d++)
|
||||
list.Add(new JumpDescriptor(dx * d, 0, 1, JumpFlavor.SprintJump));
|
||||
for (int d = 2; d <= 5; d++)
|
||||
list.Add(new JumpDescriptor(dx * d, 0, -1, JumpFlavor.SprintJump));
|
||||
for (int d = 2; d <= 5; d++)
|
||||
list.Add(new JumpDescriptor(dx * d, 0, -2, JumpFlavor.SprintJump));
|
||||
}
|
||||
foreach (int dz in offsets)
|
||||
{
|
||||
for (int d = 2; d <= 5; d++)
|
||||
list.Add(new JumpDescriptor(0, dz * d, 0, JumpFlavor.SprintJump));
|
||||
for (int d = 2; d <= 3; d++)
|
||||
list.Add(new JumpDescriptor(0, dz * d, 1, JumpFlavor.SprintJump));
|
||||
for (int d = 2; d <= 5; d++)
|
||||
list.Add(new JumpDescriptor(0, dz * d, -1, JumpFlavor.SprintJump));
|
||||
for (int d = 2; d <= 5; d++)
|
||||
list.Add(new JumpDescriptor(0, dz * d, -2, JumpFlavor.SprintJump));
|
||||
}
|
||||
// Cardinal parkour is handled dynamically by ProbeCardinal; only the
|
||||
// diagonal SprintJump variants remain as static descriptors.
|
||||
|
||||
// Diagonal parkour
|
||||
foreach (int dx in offsets)
|
||||
|
|
@ -205,37 +426,19 @@ public sealed class JumpExpander : IMoveExpander
|
|||
}
|
||||
}
|
||||
|
||||
// Sidewall parkour
|
||||
foreach (int dx in offsets)
|
||||
{
|
||||
foreach (int dz in offsets)
|
||||
{
|
||||
foreach (int distance in new[] { 2, 3, 4, 5 })
|
||||
{
|
||||
list.Add(new JumpDescriptor(dx, dz * distance, 0, JumpFlavor.Sidewall));
|
||||
list.Add(new JumpDescriptor(dx * distance, dz, 0, JumpFlavor.Sidewall));
|
||||
|
||||
if (distance <= 3)
|
||||
{
|
||||
list.Add(new JumpDescriptor(dx, dz * distance, 1, JumpFlavor.Sidewall));
|
||||
list.Add(new JumpDescriptor(dx * distance, dz, 1, JumpFlavor.Sidewall));
|
||||
}
|
||||
|
||||
list.Add(new JumpDescriptor(dx, dz * distance, -1, JumpFlavor.Sidewall));
|
||||
list.Add(new JumpDescriptor(dx * distance, dz, -1, JumpFlavor.Sidewall));
|
||||
list.Add(new JumpDescriptor(dx, dz * distance, -2, JumpFlavor.Sidewall));
|
||||
list.Add(new JumpDescriptor(dx * distance, dz, -2, JumpFlavor.Sidewall));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sidewall parkour is produced by ProbeCardinal alongside cardinal
|
||||
// sprint jumps -- the probe shares a single forward-corridor scan
|
||||
// with the sprint-jump candidates and emits a sidewall candidate
|
||||
// whenever a lateral wall supports it.
|
||||
|
||||
return list.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read-only snapshot of the descriptor table used by this expander. Exposed
|
||||
/// for callers that need to enumerate the jump family directly (e.g. A*'s
|
||||
/// sidewall-runup preparation logic).
|
||||
/// Read-only snapshot of the descriptor table used by this expander.
|
||||
/// Contains only moves that are enumerated statically (Walk, Step,
|
||||
/// diagonal SprintJump); cardinal SprintJump and Sidewall are produced
|
||||
/// dynamically by <see cref="ProbeCardinal"/>.
|
||||
/// </summary>
|
||||
public static ReadOnlySpan<JumpDescriptor> Descriptors => _descriptors;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,6 +165,27 @@ internal static class JumpFeasibility
|
|||
return;
|
||||
}
|
||||
|
||||
// Baritone-parity gate (MovementDiagonal.cost @197-200): when either
|
||||
// cardinal shoulder also has solid ground below (i.e. the bot could
|
||||
// walk that way first and then do a plain cardinal Ascend), refuse
|
||||
// the diagonal Ascend. Executing a diagonal Ascend requires the
|
||||
// bot's ground-speed momentum to already point along the diagonal at
|
||||
// the moment of takeoff; when the preceding segment is a cardinal
|
||||
// Walk the momentum is axis-aligned and the 2-tick yaw/input rotation
|
||||
// during the handoff cannot redirect enough horizontal motion, so the
|
||||
// bot consistently overshoots the target block. Forcing A* to spend
|
||||
// the extra ~0.4 cost of a cardinal Walk + cardinal Ascend pair
|
||||
// eliminates that execution failure while still leaving true "only
|
||||
// reachable diagonally" setups (no cardinal floor support) on the
|
||||
// table for scenarios that explicitly test the diagonal Step graph.
|
||||
bool cardinalWalkableViaX = pathViaX && ctx.CanWalkOn(x + dx, y - 1, z);
|
||||
bool cardinalWalkableViaZ = pathViaZ && ctx.CanWalkOn(x, y - 1, z + dz);
|
||||
if (cardinalWalkableViaX || cardinalWalkableViaZ)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
double diagCost = ctx.SprintCost * ActionCosts.DiagonalMultiplier + ctx.JumpPenalty;
|
||||
result.Set(destX, destY, destZ, diagCost);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -284,7 +284,16 @@ internal static class ParkourFeasibility
|
|||
int major = Math.Max(Math.Abs(xOffset), Math.Abs(zOffset));
|
||||
int insideWallDepth = 0;
|
||||
|
||||
for (int step = 0; step < 2; step++)
|
||||
// Probe up to MaxProbeDepth cells along the forward axis at the lateral
|
||||
// column to measure how thick the inner wall is. A 1- or 2-thick wall
|
||||
// was the original supported case; thicker walls (3) still let the
|
||||
// sidewall arc play out because the wall only provides lateral support
|
||||
// during the sprint-jump — the player brushes the wall longer but the
|
||||
// forward reach is unchanged. Walls thicker than MaxProbeDepth are
|
||||
// rejected because they either bury the landing column or leave no
|
||||
// open air for the arc to complete.
|
||||
const int MaxProbeDepth = 3;
|
||||
for (int step = 0; step < MaxProbeDepth; step++)
|
||||
{
|
||||
int wx = x + lateralX + (forwardX * step);
|
||||
int wz = z + lateralZ + (forwardZ * step);
|
||||
|
|
@ -293,7 +302,7 @@ internal static class ParkourFeasibility
|
|||
insideWallDepth++;
|
||||
}
|
||||
|
||||
if (insideWallDepth is < 1 or > 2)
|
||||
if (insideWallDepth is < 1 or > MaxProbeDepth)
|
||||
return false;
|
||||
|
||||
for (int step = 1; step <= major; step++)
|
||||
|
|
|
|||
|
|
@ -3528,6 +3528,42 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Planning path in background (budget: {0}ms)... result will appear in the log.
|
||||
/// </summary>
|
||||
internal static string cmd_goto_planning {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.goto.planning", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to toggle verbose pathing diagnostics (full plan dump, failure trace).
|
||||
/// </summary>
|
||||
internal static string cmd_pathdiag_desc {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.pathdiag.desc", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Pathing diagnostics enabled. Run /pathdiag off to disable.
|
||||
/// </summary>
|
||||
internal static string cmd_pathdiag_enabled {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.pathdiag.enabled", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Pathing diagnostics disabled.
|
||||
/// </summary>
|
||||
internal static string cmd_pathdiag_disabled {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.pathdiag.disabled", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to [PathMetric] routeStart segments={0}.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1252,6 +1252,18 @@ Change EnableEmoji=false in the settings if the display is confusing.</value>
|
|||
<data name="cmd.goto.failed" xml:space="preserve">
|
||||
<value>No path found ({0} nodes explored in {1}ms)</value>
|
||||
</data>
|
||||
<data name="cmd.goto.planning" xml:space="preserve">
|
||||
<value>Planning path in background (budget: {0}ms)... result will appear in the log.</value>
|
||||
</data>
|
||||
<data name="cmd.pathdiag.desc" xml:space="preserve">
|
||||
<value>toggle verbose pathing diagnostics (full plan dump, failure trace).</value>
|
||||
</data>
|
||||
<data name="cmd.pathdiag.enabled" xml:space="preserve">
|
||||
<value>Pathing diagnostics enabled. Run /pathdiag off to disable.</value>
|
||||
</data>
|
||||
<data name="cmd.pathdiag.disabled" xml:space="preserve">
|
||||
<value>Pathing diagnostics disabled.</value>
|
||||
</data>
|
||||
<data name="pathing.metric.route_start" xml:space="preserve">
|
||||
<value>[PathMetric] routeStart segments={0}</value>
|
||||
</data>
|
||||
|
|
|
|||
|
|
@ -428,6 +428,19 @@
|
|||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 2,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 2,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "ascend",
|
||||
|
|
@ -441,6 +454,19 @@
|
|||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 12,
|
||||
"max_reach": 3,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 2,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "ascend",
|
||||
|
|
@ -480,6 +506,19 @@
|
|||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 1,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 2,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "ascend",
|
||||
|
|
@ -493,6 +532,19 @@
|
|||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 12,
|
||||
"max_reach": 2,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 2,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
|
|
@ -532,6 +584,19 @@
|
|||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 4,
|
||||
"delta_y": -2.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 2,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
|
|
@ -545,6 +610,19 @@
|
|||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 12,
|
||||
"max_reach": 5,
|
||||
"delta_y": -2.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 2,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
|
|
@ -584,6 +662,19 @@
|
|||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 1,
|
||||
"max_reach": 4,
|
||||
"delta_y": -1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 2,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
|
|
@ -597,6 +688,19 @@
|
|||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 2,
|
||||
"max_mm": 12,
|
||||
"max_reach": 5,
|
||||
"delta_y": -1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 2,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
|
|
@ -649,6 +753,19 @@
|
|||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 2,
|
||||
"delta_y": -2.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 2,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
|
|
@ -662,6 +779,19 @@
|
|||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 2,
|
||||
"max_reach": 3,
|
||||
"delta_y": -2.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 2,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
|
|
@ -675,6 +805,19 @@
|
|||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 3,
|
||||
"max_mm": 12,
|
||||
"max_reach": 4,
|
||||
"delta_y": -2.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 2,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
|
|
@ -714,6 +857,19 @@
|
|||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 2,
|
||||
"delta_y": -1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 2,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
|
|
@ -727,6 +883,19 @@
|
|||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 12,
|
||||
"max_reach": 3,
|
||||
"delta_y": -1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 2,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "flat",
|
||||
|
|
@ -766,6 +935,19 @@
|
|||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "flat",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 3,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 2,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "flat",
|
||||
|
|
@ -779,6 +961,19 @@
|
|||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "flat",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 12,
|
||||
"max_reach": 4,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 2,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "flat",
|
||||
|
|
@ -818,6 +1013,19 @@
|
|||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "flat",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 1,
|
||||
"max_reach": 2,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 2,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "flat",
|
||||
|
|
@ -830,5 +1038,18 @@
|
|||
"ceiling_height": null,
|
||||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "flat",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 2,
|
||||
"max_mm": 12,
|
||||
"max_reach": 3,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 2,
|
||||
"notes": ""
|
||||
}
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue