mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
pathing: async replan + template success/failure alignment
Move PathSegmentManager's Replan to Task.Run so the main tick only reads results and swaps executors, and introduce a _nextExecutor pre-planning slot so upcoming segments can prepare while the current one finishes. Relax per-tick yaw/pitch rate limiting: allow instantaneous snapping before jump ticks (Baritone does this and servers do not kick for it). Align jump-template success/failure contracts with Baritone: - Success key shifts from "speed squared" to "feet-on-target block". - Failure window widened to the ~200 tick range. - AscendTemplate gets a headBonkClear + edge/side proximity precondition so launches only happen from a safe takeoff. Expose an initialMomentumTicks option on TemplateSimulationRunner so follow-up sidewall scenarios can warm up physics before a template starts. Made-with: Cursor
This commit is contained in:
parent
e23037a897
commit
95b20d9d1c
6 changed files with 554 additions and 48 deletions
|
|
@ -6,9 +6,9 @@ namespace MinecraftClient.Tests.Pathing.Execution;
|
|||
|
||||
internal static class TemplateSimulationRunner
|
||||
{
|
||||
internal static PlayerPhysics CreateGroundedPhysics(Location start, float yaw)
|
||||
internal static PlayerPhysics CreateGroundedPhysics(Location start, float yaw, int initialMomentumTicks = 0)
|
||||
{
|
||||
return new PlayerPhysics
|
||||
var physics = new PlayerPhysics
|
||||
{
|
||||
Position = new Vec3d(start.X, start.Y, start.Z),
|
||||
DeltaMovement = Vec3d.Zero,
|
||||
|
|
@ -17,6 +17,11 @@ internal static class TemplateSimulationRunner
|
|||
Yaw = yaw,
|
||||
Pitch = 0f
|
||||
};
|
||||
|
||||
if (initialMomentumTicks > 0)
|
||||
ApplyInitialGroundMomentum(physics, initialMomentumTicks);
|
||||
|
||||
return physics;
|
||||
}
|
||||
|
||||
internal static TemplateState Run(IActionTemplate template, PlayerPhysics physics, World world, int maxTicks, out Location finalPos)
|
||||
|
|
@ -39,4 +44,32 @@ internal static class TemplateSimulationRunner
|
|||
finalPos = new Location(physics.Position.X, physics.Position.Y, physics.Position.Z);
|
||||
return state;
|
||||
}
|
||||
|
||||
private static void ApplyInitialGroundMomentum(PlayerPhysics physics, int ticks)
|
||||
{
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor();
|
||||
var seeded = new PlayerPhysics
|
||||
{
|
||||
Position = new Vec3d(0.5, 80, 0.5),
|
||||
DeltaMovement = Vec3d.Zero,
|
||||
OnGround = true,
|
||||
MovementSpeed = physics.MovementSpeed,
|
||||
Yaw = physics.Yaw,
|
||||
Pitch = physics.Pitch
|
||||
};
|
||||
var input = new MovementInput
|
||||
{
|
||||
Forward = true,
|
||||
Sprint = true
|
||||
};
|
||||
|
||||
for (int tick = 0; tick < ticks; tick++)
|
||||
{
|
||||
seeded.ApplyInput(input);
|
||||
seeded.Tick(world);
|
||||
}
|
||||
|
||||
physics.DeltaMovement = seeded.DeltaMovement;
|
||||
physics.Sprinting = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ namespace MinecraftClient.Pathing.Execution
|
|||
public int TotalTicks => _totalTicks;
|
||||
public PathSegment? CurrentSegment =>
|
||||
_currentIndex < _segments.Count ? _segments[_currentIndex] : null;
|
||||
public PathSegment? LastSegment =>
|
||||
_segments.Count > 0 ? _segments[^1] : null;
|
||||
|
||||
public PathExecutor(List<PathSegment> segments, Action<string>? debugLog = null, IPathExecutionObserver? observer = null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Pathing.Execution.Telemetry;
|
||||
|
|
@ -11,19 +12,39 @@ namespace MinecraftClient.Pathing.Execution
|
|||
/// <summary>
|
||||
/// Top-level navigation controller. Holds a PathExecutor, monitors its progress,
|
||||
/// and triggers replanning on failure or deviation.
|
||||
///
|
||||
/// Replans and look-ahead plans run on a background Task (Baritone equivalent:
|
||||
/// findPathInNewThread). The main tick only reads task state: either applies a
|
||||
/// completed plan or installs the next one. A look-ahead plan is speculatively
|
||||
/// started as the current executor nears the end of its segment list so that the
|
||||
/// next executor is ready to splice in without a user-visible pause.
|
||||
/// </summary>
|
||||
public sealed class PathSegmentManager
|
||||
{
|
||||
private PathExecutor? _executor;
|
||||
private IGoal? _goal;
|
||||
private int _replanCount;
|
||||
private const int MaxReplans = 5;
|
||||
private const int ReplanTimeoutMs = 3000;
|
||||
private const int LookaheadTriggerSegmentsRemaining = 2;
|
||||
|
||||
private readonly Action<string>? _debugLog;
|
||||
private readonly Action<string>? _infoLog;
|
||||
private readonly IPathExecutionObserver? _observer;
|
||||
|
||||
public bool IsNavigating => _executor is not null && !_executor.IsComplete;
|
||||
private PathExecutor? _executor;
|
||||
private PathExecutor? _nextExecutor;
|
||||
private IGoal? _goal;
|
||||
private int _replanCount;
|
||||
|
||||
private Task<PathResult>? _pendingReplan;
|
||||
private CancellationTokenSource? _pendingReplanCts;
|
||||
private Task<PathResult>? _pendingLookahead;
|
||||
private CancellationTokenSource? _pendingLookaheadCts;
|
||||
private (int x, int y, int z)? _pendingLookaheadAnchor;
|
||||
|
||||
public bool IsNavigating =>
|
||||
(_executor is not null && !_executor.IsComplete)
|
||||
|| _nextExecutor is not null
|
||||
|| _pendingReplan is not null;
|
||||
|
||||
public int ReplanCount => _replanCount;
|
||||
public IGoal? Goal => _goal;
|
||||
|
||||
|
|
@ -36,6 +57,8 @@ namespace MinecraftClient.Pathing.Execution
|
|||
|
||||
public void StartNavigation(IGoal goal, PathResult result)
|
||||
{
|
||||
CancelPendingTasks();
|
||||
_nextExecutor = null;
|
||||
_goal = goal;
|
||||
_replanCount = 0;
|
||||
if (result.Status == PathStatus.Failed || result.Path.Count < 2)
|
||||
|
|
@ -53,51 +76,100 @@ namespace MinecraftClient.Pathing.Execution
|
|||
|
||||
public void Tick(Location pos, PlayerPhysics physics, MovementInput input, World world)
|
||||
{
|
||||
DrainPendingReplan(pos, world);
|
||||
DrainPendingLookahead();
|
||||
|
||||
if (_executor is null)
|
||||
{
|
||||
// Navigation is still alive if we are waiting on a background plan to
|
||||
// come back. The next tick will promote it into _executor.
|
||||
if (_pendingReplan is not null || _nextExecutor is not null)
|
||||
{
|
||||
TryPromoteNextExecutor();
|
||||
input.Reset();
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var state = _executor.Tick(pos, physics, input, world);
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case PathExecutorState.Complete:
|
||||
if (_goal is not null)
|
||||
{
|
||||
int px = (int)Math.Floor(pos.X);
|
||||
int py = (int)Math.Floor(pos.Y);
|
||||
int pz = (int)Math.Floor(pos.Z);
|
||||
if (!_goal.IsInGoal(px, py, pz))
|
||||
{
|
||||
_infoLog?.Invoke("[PathMgr] Planned route ended before reaching goal, replanning...");
|
||||
Replan(pos, world);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_observer?.OnNavigationCompleted(_executor.TotalTicks);
|
||||
_infoLog?.Invoke("[PathMgr] Navigation complete!");
|
||||
_executor = null;
|
||||
_goal = null;
|
||||
HandleExecutorComplete(pos, world, input);
|
||||
break;
|
||||
|
||||
case PathExecutorState.Failed:
|
||||
_infoLog?.Invoke("[PathMgr] Segment failed, replanning...");
|
||||
Replan(pos, world);
|
||||
// The prepared next path assumes we finished the current segment
|
||||
// cleanly, so drop it when we fail.
|
||||
DiscardLookahead();
|
||||
_nextExecutor = null;
|
||||
StartReplanAsync(pos, world);
|
||||
break;
|
||||
|
||||
case PathExecutorState.InProgress:
|
||||
MaybeStartLookahead(world);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
if (_executor is not null)
|
||||
if (_executor is not null || _pendingReplan is not null || _nextExecutor is not null)
|
||||
{
|
||||
_infoLog?.Invoke("[PathMgr] Navigation cancelled.");
|
||||
_executor = null;
|
||||
_goal = null;
|
||||
}
|
||||
|
||||
CancelPendingTasks();
|
||||
_executor = null;
|
||||
_nextExecutor = null;
|
||||
_goal = null;
|
||||
}
|
||||
|
||||
private void Replan(Location pos, World world)
|
||||
private void HandleExecutorComplete(Location pos, World world, MovementInput input)
|
||||
{
|
||||
if (_goal is not null)
|
||||
{
|
||||
int px = (int)Math.Floor(pos.X);
|
||||
int py = (int)Math.Floor(pos.Y);
|
||||
int pz = (int)Math.Floor(pos.Z);
|
||||
if (!_goal.IsInGoal(px, py, pz))
|
||||
{
|
||||
if (TryPromoteNextExecutor())
|
||||
{
|
||||
_debugLog?.Invoke("[PathMgr] Spliced to prepared next segment chain.");
|
||||
return;
|
||||
}
|
||||
|
||||
_infoLog?.Invoke("[PathMgr] Planned route ended before reaching goal, replanning...");
|
||||
StartReplanAsync(pos, world);
|
||||
input.Reset();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_observer?.OnNavigationCompleted(_executor!.TotalTicks);
|
||||
_infoLog?.Invoke("[PathMgr] Navigation complete!");
|
||||
CancelPendingTasks();
|
||||
_executor = null;
|
||||
_nextExecutor = null;
|
||||
_goal = null;
|
||||
}
|
||||
|
||||
private bool TryPromoteNextExecutor()
|
||||
{
|
||||
if (_nextExecutor is null)
|
||||
return false;
|
||||
|
||||
_executor = _nextExecutor;
|
||||
_nextExecutor = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void StartReplanAsync(Location pos, World world)
|
||||
{
|
||||
_replanCount++;
|
||||
_observer?.OnReplanStarted(_replanCount, pos);
|
||||
|
|
@ -105,7 +177,9 @@ namespace MinecraftClient.Pathing.Execution
|
|||
{
|
||||
_observer?.OnReplanFailed(_replanCount, pos);
|
||||
_infoLog?.Invoke($"[PathMgr] Giving up after {MaxReplans} replans.");
|
||||
CancelPendingTasks();
|
||||
_executor = null;
|
||||
_nextExecutor = null;
|
||||
_goal = null;
|
||||
return;
|
||||
}
|
||||
|
|
@ -116,28 +190,93 @@ namespace MinecraftClient.Pathing.Execution
|
|||
return;
|
||||
}
|
||||
|
||||
_debugLog?.Invoke($"[PathMgr] Replan #{_replanCount} from ({pos.X:F2},{pos.Y:F2},{pos.Z:F2})");
|
||||
|
||||
var ctx = new CalculationContext(world, allowParkour: true, allowParkourAscend: true);
|
||||
var finder = new AStarPathFinder();
|
||||
finder.DebugLog = _debugLog;
|
||||
// Once we decide to replan, the currently-installed executor is discarded;
|
||||
// the new plan will replace it. We keep the executor reference until the
|
||||
// plan returns so IsNavigating reflects that work is in flight.
|
||||
if (_pendingReplan is not null)
|
||||
return;
|
||||
|
||||
int sx = (int)Math.Floor(pos.X);
|
||||
int sy = (int)Math.Floor(pos.Y);
|
||||
int sz = (int)Math.Floor(pos.Z);
|
||||
|
||||
var ctx = new CalculationContext(world, allowParkour: true, allowParkourAscend: true);
|
||||
if (!ctx.CanWalkThrough(sx, sy, sz) && ctx.CanWalkThrough(sx, sy + 1, sz))
|
||||
sy++;
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
var result = finder.Calculate(ctx, sx, sy, sz, _goal, cts.Token, 3000);
|
||||
IGoal goal = _goal;
|
||||
_debugLog?.Invoke($"[PathMgr] Replan #{_replanCount} kicked off from ({pos.X:F2},{pos.Y:F2},{pos.Z:F2})");
|
||||
|
||||
_pendingReplanCts = new CancellationTokenSource();
|
||||
CancellationToken token = _pendingReplanCts.Token;
|
||||
_pendingReplan = Task.Run(() =>
|
||||
{
|
||||
var finder = new AStarPathFinder { DebugLog = _debugLog };
|
||||
return finder.Calculate(ctx, sx, sy, sz, goal, token, ReplanTimeoutMs);
|
||||
}, token);
|
||||
|
||||
// Clear the current executor so IsNavigating stays true via the pending
|
||||
// task branch. This prevents the main client from believing navigation
|
||||
// ended between "plan completes" and "plan gets installed".
|
||||
_executor = null;
|
||||
}
|
||||
|
||||
private void DrainPendingReplan(Location pos, World world)
|
||||
{
|
||||
if (_pendingReplan is null)
|
||||
return;
|
||||
|
||||
// When the current executor has been cleared (we are waiting for a plan
|
||||
// to install), give the background task a short budget to finish. The
|
||||
// player is standing still anyway, so trading a few ms of pause for
|
||||
// installing the new plan immediately is a clear win over letting the
|
||||
// tick return with _executor == null. This also makes tests with a
|
||||
// tight Tick poll loop deterministic: Task.Run needs the caller to
|
||||
// yield at some point so the thread-pool worker can complete.
|
||||
if (!_pendingReplan.IsCompleted && _executor is null && _nextExecutor is null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_pendingReplan.Wait(20);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Exceptions are inspected via Task.IsFaulted below.
|
||||
}
|
||||
}
|
||||
|
||||
if (!_pendingReplan.IsCompleted)
|
||||
return;
|
||||
|
||||
Task<PathResult> task = _pendingReplan;
|
||||
_pendingReplan = null;
|
||||
var cts = _pendingReplanCts;
|
||||
_pendingReplanCts = null;
|
||||
cts?.Dispose();
|
||||
|
||||
if (task.IsFaulted || task.IsCanceled)
|
||||
{
|
||||
_infoLog?.Invoke("[PathMgr] Replan task failed or was cancelled.");
|
||||
_observer?.OnReplanFailed(_replanCount, pos);
|
||||
_goal = null;
|
||||
_executor = null;
|
||||
_nextExecutor = null;
|
||||
return;
|
||||
}
|
||||
|
||||
PathResult result = task.Result;
|
||||
|
||||
int sx = (int)Math.Floor(pos.X);
|
||||
int sy = (int)Math.Floor(pos.Y);
|
||||
int sz = (int)Math.Floor(pos.Z);
|
||||
bool alreadyInGoal = _goal is not null && (_goal.IsInGoal(sx, sy, sz)
|
||||
|| (result.Path.Count == 1 && _goal.IsInGoal(result.Path[0].X, result.Path[0].Y, result.Path[0].Z)));
|
||||
|
||||
bool alreadyInGoal = _goal.IsInGoal(sx, sy, sz)
|
||||
|| (result.Path.Count == 1 && _goal.IsInGoal(result.Path[0].X, result.Path[0].Y, result.Path[0].Z));
|
||||
if (alreadyInGoal)
|
||||
{
|
||||
_infoLog?.Invoke("[PathMgr] Navigation complete!");
|
||||
_executor = null;
|
||||
_nextExecutor = null;
|
||||
_goal = null;
|
||||
return;
|
||||
}
|
||||
|
|
@ -147,6 +286,7 @@ namespace MinecraftClient.Pathing.Execution
|
|||
_observer?.OnReplanFailed(_replanCount, pos);
|
||||
_infoLog?.Invoke("[PathMgr] Replan failed -- no path found.");
|
||||
_executor = null;
|
||||
_nextExecutor = null;
|
||||
_goal = null;
|
||||
return;
|
||||
}
|
||||
|
|
@ -154,7 +294,95 @@ 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})");
|
||||
}
|
||||
|
||||
private void MaybeStartLookahead(World world)
|
||||
{
|
||||
if (_pendingLookahead is not null || _nextExecutor is not null || _goal is null || _executor is null)
|
||||
return;
|
||||
|
||||
int total = _executor.TotalSegments;
|
||||
int current = _executor.CurrentIndex;
|
||||
if (total - current > LookaheadTriggerSegmentsRemaining)
|
||||
return;
|
||||
|
||||
// Anchor the lookahead at the final segment's end; that is where execution
|
||||
// will arrive if the current executor finishes without drift.
|
||||
PathSegment? lastSegment = _executor.LastSegment;
|
||||
if (lastSegment is null)
|
||||
return;
|
||||
|
||||
int ax = (int)Math.Floor(lastSegment.End.X);
|
||||
int ay = (int)Math.Floor(lastSegment.End.Y);
|
||||
int az = (int)Math.Floor(lastSegment.End.Z);
|
||||
|
||||
if (_goal.IsInGoal(ax, ay, az))
|
||||
return;
|
||||
|
||||
// Avoid re-planning from the same anchor repeatedly.
|
||||
if (_pendingLookaheadAnchor is { } prev && prev == (ax, ay, az))
|
||||
return;
|
||||
|
||||
var ctx = new CalculationContext(world, allowParkour: true, allowParkourAscend: true);
|
||||
IGoal goal = _goal;
|
||||
_debugLog?.Invoke($"[PathMgr] Lookahead plan kicked off from ({ax},{ay},{az})");
|
||||
|
||||
_pendingLookaheadCts = new CancellationTokenSource();
|
||||
CancellationToken token = _pendingLookaheadCts.Token;
|
||||
_pendingLookaheadAnchor = (ax, ay, az);
|
||||
_pendingLookahead = Task.Run(() =>
|
||||
{
|
||||
var finder = new AStarPathFinder { DebugLog = _debugLog };
|
||||
return finder.Calculate(ctx, ax, ay, az, goal, token, ReplanTimeoutMs);
|
||||
}, token);
|
||||
}
|
||||
|
||||
private void DrainPendingLookahead()
|
||||
{
|
||||
if (_pendingLookahead is null || !_pendingLookahead.IsCompleted)
|
||||
return;
|
||||
|
||||
Task<PathResult> task = _pendingLookahead;
|
||||
_pendingLookahead = null;
|
||||
var cts = _pendingLookaheadCts;
|
||||
_pendingLookaheadCts = null;
|
||||
cts?.Dispose();
|
||||
|
||||
if (task.IsFaulted || task.IsCanceled)
|
||||
return;
|
||||
|
||||
PathResult result = task.Result;
|
||||
if (result.Status == PathStatus.Failed || result.Path.Count < 2)
|
||||
return;
|
||||
|
||||
// The lookahead should continue from the anchor point. If the live
|
||||
// executor is still running, splice the prepared plan as _nextExecutor
|
||||
// so it can take over without a wait.
|
||||
var segments = PathSegmentBuilder.FromPath(result.Path);
|
||||
_nextExecutor = new PathExecutor(segments, _debugLog, _observer);
|
||||
_debugLog?.Invoke($"[PathMgr] Lookahead ready: {segments.Count} segments spliced into _nextExecutor");
|
||||
}
|
||||
|
||||
private void DiscardLookahead()
|
||||
{
|
||||
_pendingLookaheadCts?.Cancel();
|
||||
_pendingLookaheadCts?.Dispose();
|
||||
_pendingLookaheadCts = null;
|
||||
_pendingLookahead = null;
|
||||
_pendingLookaheadAnchor = null;
|
||||
}
|
||||
|
||||
private void CancelPendingTasks()
|
||||
{
|
||||
_pendingReplanCts?.Cancel();
|
||||
_pendingReplanCts?.Dispose();
|
||||
_pendingReplanCts = null;
|
||||
_pendingReplan = null;
|
||||
|
||||
DiscardLookahead();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,19 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
/// <summary>
|
||||
/// Jump up 1 block while moving 1 block in a cardinal direction.
|
||||
/// Faces destination, sprints forward, and jumps when on ground.
|
||||
///
|
||||
/// Follows Baritone's MovementAscend.updateState gating:
|
||||
/// - jump immediately when headBonkClear (no low-ceiling hazard above source)
|
||||
/// - otherwise wait until close to the destination edge (flatDistToNext <= 1.2)
|
||||
/// and laterally lined up (sideDist <= 0.2) before firing the jump
|
||||
/// This avoids bonking the ceiling on short staircases and avoids jumping while
|
||||
/// still too far away (which causes the short-hop to stall against the riser).
|
||||
/// </summary>
|
||||
public sealed class AscendTemplate : IActionTemplate
|
||||
{
|
||||
private const double EdgeCloseDistance = 1.2;
|
||||
private const double LateralAlignmentTolerance = 0.2;
|
||||
|
||||
public Location ExpectedStart { get; }
|
||||
public Location ExpectedEnd { get; }
|
||||
|
||||
|
|
@ -55,13 +65,45 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
input.Forward = !turnInPlace;
|
||||
input.Sprint = !turnInPlace;
|
||||
|
||||
bool diagonalAscend = _segment.HeadingX != 0 && _segment.HeadingZ != 0;
|
||||
bool jumpReady = headingReady
|
||||
&& (diagonalAscend || TemplateHelper.RemainingDistanceAlongSegment(pos, _segment) <= 1.05);
|
||||
if (physics.OnGround && dy > 0.1 && jumpReady)
|
||||
if (physics.OnGround && dy > 0.1)
|
||||
{
|
||||
input.Jump = true;
|
||||
_initiatedJump = true;
|
||||
bool diagonalAscend = _segment.HeadingX != 0 && _segment.HeadingZ != 0;
|
||||
double flatDistToNext = TemplateHelper.RemainingDistanceAlongSegment(pos, _segment);
|
||||
double sideDist = TemplateHelper.LateralOffsetFromSegmentLine(pos, _segment);
|
||||
|
||||
bool closeToEdge = flatDistToNext <= EdgeCloseDistance;
|
||||
bool laterallyAligned = sideDist <= LateralAlignmentTolerance;
|
||||
|
||||
bool jumpReady;
|
||||
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
|
||||
// laterally lined up; otherwise we end up banging the side of the
|
||||
// block without gaining height.
|
||||
jumpReady = headingReady && closeToEdge && laterallyAligned;
|
||||
}
|
||||
|
||||
if (jumpReady)
|
||||
{
|
||||
// Snap rotation to the target direction on the takeoff tick so
|
||||
// the sprint-jump boost goes along the segment line regardless of
|
||||
// how many ticks the smoothing had to consume. Baritone sets
|
||||
// rotation directly every tick and the server accepts it.
|
||||
physics.Yaw = targetYaw;
|
||||
input.Jump = true;
|
||||
_initiatedJump = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (physics.OnGround && Math.Abs(dy) < 0.2)
|
||||
|
|
@ -76,12 +118,48 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
_stuckTicks = (movedSq < 0.0005 && movedY < 0.001) ? _stuckTicks + 1 : 0;
|
||||
_lastPos = pos;
|
||||
|
||||
if (_stuckTicks > 40 || _tickCount > 80)
|
||||
// Baritone tolerates up to 200 ticks (MAX_TICKS_AWAY) before abandoning a
|
||||
// movement. We mirror that budget so the template does not fail spuriously
|
||||
// during normal run-up / jump / landing settle flows.
|
||||
if (_stuckTicks > 120 || _tickCount > 200)
|
||||
return TemplateState.Failed;
|
||||
|
||||
return TemplateState.InProgress;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when no solid block sits two cells above the source ascent position
|
||||
/// in any cardinal direction the player might nick while rising. Mirrors
|
||||
/// Baritone's MovementAscend.headBonkClear.
|
||||
/// </summary>
|
||||
private bool HasHeadBonkClear(World world)
|
||||
{
|
||||
int sx = (int)Math.Floor(ExpectedStart.X);
|
||||
int sy = (int)Math.Floor(ExpectedStart.Y);
|
||||
int sz = (int)Math.Floor(ExpectedStart.Z);
|
||||
|
||||
// Directly above the source block and each cardinal neighbour at head
|
||||
// height must be walkable-through so the player never catches a corner.
|
||||
if (!IsWalkThroughAt(world, sx, sy + 2, sz))
|
||||
return false;
|
||||
|
||||
int[] dx = { 1, -1, 0, 0 };
|
||||
int[] dz = { 0, 0, 1, -1 };
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
if (!IsWalkThroughAt(world, sx + dx[i], sy + 2, sz + dz[i]))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsWalkThroughAt(World world, int x, int y, int z)
|
||||
{
|
||||
Block block = world.GetBlock(new Location(x, y, z));
|
||||
return !block.Type.IsSolid();
|
||||
}
|
||||
|
||||
private static float YawDifference(float current, float target)
|
||||
{
|
||||
float delta = target - current;
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
private bool _leftGround;
|
||||
private bool _carriedGroundEntry;
|
||||
private bool _releaseForwardLatched;
|
||||
private readonly SidewallParkourController? _sidewallController;
|
||||
|
||||
private const float YawToleranceDeg = 5f;
|
||||
|
||||
|
|
@ -45,12 +46,18 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
double dx = segment.End.X - segment.Start.X;
|
||||
double dz = segment.End.Z - segment.Start.Z;
|
||||
_horizDist = Math.Sqrt(dx * dx + dz * dz);
|
||||
|
||||
if (segment.ParkourProfile == ParkourProfile.Sidewall)
|
||||
_sidewallController = new SidewallParkourController(segment, nextSegment);
|
||||
}
|
||||
|
||||
public TemplateState Tick(Location pos, PlayerPhysics physics, MovementInput input, World world)
|
||||
{
|
||||
_tickCount++;
|
||||
|
||||
if (_sidewallController is not null)
|
||||
return _sidewallController.Tick(pos, physics, input, world);
|
||||
|
||||
double dx = ExpectedEnd.X - pos.X;
|
||||
double dz = ExpectedEnd.Z - pos.Z;
|
||||
double dy = ExpectedEnd.Y - pos.Y;
|
||||
|
|
@ -119,10 +126,17 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
else
|
||||
minApproachDistance = 0.0;
|
||||
|
||||
bool yawAligned = yawDelta < YawToleranceDeg;
|
||||
bool posReady = approachProgress >= minApproachDistance;
|
||||
if (yawAligned && posReady)
|
||||
// Baritone snaps rotation to the exact target every tick and
|
||||
// the server accepts it without disconnection. For our smooth
|
||||
// yaw model we only need it at the takeoff tick: snap yaw the
|
||||
// moment we are positionally ready, so the jump vector is
|
||||
// aligned regardless of how many ticks we had to turn. This
|
||||
// prevents mis-jumps when the approach was short and the
|
||||
// 35 deg/tick smoothing had not finished by the jump tick.
|
||||
if (posReady)
|
||||
{
|
||||
physics.Yaw = targetYaw;
|
||||
input.Jump = true;
|
||||
_phase = Phase.Airborne;
|
||||
}
|
||||
|
|
@ -134,7 +148,10 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
input.Forward = !turnInPlace;
|
||||
input.Sprint = !turnInPlace;
|
||||
}
|
||||
if (_tickCount > 40)
|
||||
// Widen the approach/run-up budget. With snap-to-target yaw the
|
||||
// player aligns in a single tick, but servers may still need a few
|
||||
// extra ticks of sprint acceleration on long jumps.
|
||||
if (_tickCount > 80)
|
||||
return TemplateState.Failed;
|
||||
break;
|
||||
|
||||
|
|
@ -237,18 +254,43 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
{
|
||||
return TemplateState.Complete;
|
||||
}
|
||||
|
||||
// Baritone-style lenient success: the moment the player is standing
|
||||
// on the target block (floor center matches), the jump is done. We
|
||||
// keep the stricter settle checks above as the primary path because
|
||||
// they capture momentum continuity for downstream segments, but the
|
||||
// lenient check prevents spurious failures when the player touches
|
||||
// down slightly off-center or with a small residual slide.
|
||||
if (_segment.ExitTransition != PathTransitionType.ContinueStraight
|
||||
&& physics.OnGround
|
||||
&& LandedInsideTargetBlock(pos))
|
||||
{
|
||||
return TemplateState.Complete;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (pos.Y < ExpectedEnd.Y - 4.0)
|
||||
return TemplateState.Failed;
|
||||
|
||||
if (_tickCount > 60)
|
||||
// Baritone's MAX_TICKS_AWAY is 200 ticks (10 seconds) before it gives up
|
||||
// on a single movement. Short 60-tick windows were too tight for jumps
|
||||
// that include a run-up, a long airtime, and landing drag settling.
|
||||
if (_tickCount > 200)
|
||||
return TemplateState.Failed;
|
||||
|
||||
return TemplateState.InProgress;
|
||||
}
|
||||
|
||||
private bool LandedInsideTargetBlock(Location pos)
|
||||
{
|
||||
if (!TemplateFootingHelper.IsFootprintInsideTargetBlock(pos, ExpectedEnd))
|
||||
return false;
|
||||
|
||||
// Floor Y must match the target block. Accept anywhere within the block.
|
||||
return Math.Floor(pos.Y + 1.0E-4) == Math.Floor(ExpectedEnd.Y + 1.0E-4);
|
||||
}
|
||||
|
||||
private bool IsPastTarget(Location pos)
|
||||
{
|
||||
double dirX = ExpectedEnd.X - ExpectedStart.X;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Physics;
|
||||
|
||||
namespace MinecraftClient.Pathing.Execution.Templates
|
||||
|
|
@ -10,6 +11,11 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
private const float MaxYawStepPerTick = 35f;
|
||||
private const float MaxPitchStepPerTick = 25f;
|
||||
|
||||
// Callers that want to force an immediate alignment (e.g. right before
|
||||
// firing a jump so the takeoff vector matches the target) can use these
|
||||
// "snap" overloads instead of SmoothYaw/SmoothPitch.
|
||||
internal static float SnapStep => 360f;
|
||||
|
||||
internal static float CalculateYaw(double dx, double dz)
|
||||
{
|
||||
float yaw = (float)(-Math.Atan2(dx, dz) / Math.PI * 180.0);
|
||||
|
|
@ -89,6 +95,12 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
physics.Yaw = SmoothYaw(physics.Yaw, headingYaw);
|
||||
}
|
||||
|
||||
internal static void FaceExitHeading(PlayerPhysics physics, PathSegment segment, PathSegment? nextSegment)
|
||||
{
|
||||
float headingYaw = GetExitHeadingYaw(segment, nextSegment);
|
||||
physics.Yaw = SmoothYaw(physics.Yaw, headingYaw);
|
||||
}
|
||||
|
||||
internal static void ApplyDecision(MovementInput input, TransitionBrakingDecision decision)
|
||||
{
|
||||
input.Forward = decision.HoldForward;
|
||||
|
|
@ -137,6 +149,73 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
return dx * segment.HeadingX + dz * segment.HeadingZ;
|
||||
}
|
||||
|
||||
internal static void GetApproachHeading(PathSegment segment, out int headingX, out int headingZ)
|
||||
{
|
||||
if (segment.ParkourProfile == ParkourProfile.Sidewall)
|
||||
{
|
||||
double dx = Math.Abs(segment.End.X - segment.Start.X);
|
||||
double dz = Math.Abs(segment.End.Z - segment.Start.Z);
|
||||
|
||||
if (dx > dz)
|
||||
{
|
||||
headingX = segment.HeadingX;
|
||||
headingZ = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
headingX = 0;
|
||||
headingZ = segment.HeadingZ;
|
||||
}
|
||||
|
||||
if (headingX != 0 || headingZ != 0)
|
||||
return;
|
||||
}
|
||||
|
||||
headingX = segment.HeadingX;
|
||||
headingZ = segment.HeadingZ;
|
||||
}
|
||||
|
||||
internal static float GetApproachYaw(PathSegment segment)
|
||||
{
|
||||
GetApproachHeading(segment, out int headingX, out int headingZ);
|
||||
return CalculateYaw(headingX, headingZ);
|
||||
}
|
||||
|
||||
internal static double ProgressAlongApproach(Location pos, PathSegment segment)
|
||||
{
|
||||
GetApproachHeading(segment, out int headingX, out int headingZ);
|
||||
return ((pos.X - segment.Start.X) * headingX) + ((pos.Z - segment.Start.Z) * headingZ);
|
||||
}
|
||||
|
||||
internal static float GetSidewallTakeoffYaw(PathSegment segment)
|
||||
{
|
||||
float approachYaw = GetApproachYaw(segment);
|
||||
float targetYaw = CalculateYaw(segment.End.X - segment.Start.X, segment.End.Z - segment.Start.Z);
|
||||
|
||||
double major = Math.Max(Math.Abs(segment.End.X - segment.Start.X), Math.Abs(segment.End.Z - segment.Start.Z));
|
||||
float blend = major switch
|
||||
{
|
||||
<= 2.0 => 0.55f,
|
||||
<= 3.0 => 0.52f,
|
||||
<= 4.0 => 0.50f,
|
||||
_ => 0.48f
|
||||
};
|
||||
|
||||
if (major >= 4.0)
|
||||
blend += 0.04f;
|
||||
|
||||
if (segment.End.Y > segment.Start.Y)
|
||||
blend = Math.Max(0.40f, blend - 0.06f);
|
||||
else if (segment.End.Y < segment.Start.Y)
|
||||
{
|
||||
blend = Math.Min(0.62f, blend + 0.06f);
|
||||
if (major <= 2.0)
|
||||
blend = Math.Min(0.66f, blend + 0.05f);
|
||||
}
|
||||
|
||||
return InterpolateYaw(approachYaw, targetYaw, blend);
|
||||
}
|
||||
|
||||
internal static double LateralOffsetFromSegmentLine(Location pos, PathSegment segment)
|
||||
{
|
||||
GetNormalizedSegmentDirection(segment, out double dirX, out double dirZ);
|
||||
|
|
@ -160,6 +239,18 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
return RemainingDistanceAlongSegment(pos, segment) <= distanceThreshold;
|
||||
}
|
||||
|
||||
internal static bool ShouldBiasTowardExitHeading(Location pos, PathSegment segment, PathSegment? nextSegment, double distanceThreshold = 0.35)
|
||||
{
|
||||
GetExitHeading(segment, nextSegment, out int headingX, out int headingZ);
|
||||
if ((headingX == 0 && headingZ == 0)
|
||||
|| (headingX == segment.HeadingX && headingZ == segment.HeadingZ))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return RemainingDistanceAlongSegment(pos, segment) <= distanceThreshold;
|
||||
}
|
||||
|
||||
internal static bool IsSettledOnTargetBlock(Location pos, Location target, PlayerPhysics physics,
|
||||
double speedThresholdSq = 0.0016)
|
||||
{
|
||||
|
|
@ -221,6 +312,12 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
return CalculateYaw(headingX, headingZ);
|
||||
}
|
||||
|
||||
internal static float GetExitHeadingYaw(PathSegment segment, PathSegment? nextSegment)
|
||||
{
|
||||
GetExitHeading(segment, nextSegment, out int headingX, out int headingZ);
|
||||
return CalculateYaw(headingX, headingZ);
|
||||
}
|
||||
|
||||
internal static void GetExitHeading(PathSegment segment, out int headingX, out int headingZ)
|
||||
{
|
||||
headingX = segment.ExitHints.DesiredHeadingX;
|
||||
|
|
@ -233,6 +330,20 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
}
|
||||
}
|
||||
|
||||
internal static void GetExitHeading(PathSegment segment, PathSegment? nextSegment, out int headingX, out int headingZ)
|
||||
{
|
||||
if (nextSegment is not null
|
||||
&& nextSegment.MoveType == MoveType.Parkour
|
||||
&& nextSegment.ParkourProfile == ParkourProfile.Sidewall)
|
||||
{
|
||||
GetApproachHeading(nextSegment, out headingX, out headingZ);
|
||||
if (headingX != 0 || headingZ != 0)
|
||||
return;
|
||||
}
|
||||
|
||||
GetExitHeading(segment, out headingX, out headingZ);
|
||||
}
|
||||
|
||||
internal static PlayerPhysics ClonePhysicsForPlanning(PlayerPhysics physics)
|
||||
{
|
||||
return new PlayerPhysics
|
||||
|
|
@ -280,5 +391,17 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
dirX /= len;
|
||||
dirZ /= len;
|
||||
}
|
||||
|
||||
private static float InterpolateYaw(float from, float to, float factor)
|
||||
{
|
||||
float delta = to - from;
|
||||
while (delta > 180f) delta -= 360f;
|
||||
while (delta < -180f) delta += 360f;
|
||||
|
||||
float result = from + (delta * factor);
|
||||
while (result < 0f) result += 360f;
|
||||
while (result >= 360f) result -= 360f;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue