feat: tighten parkour reliability checks

This commit is contained in:
BruceChen 2026-04-12 21:33:40 +08:00
parent 6b449cc72a
commit 0e0fc06b72
7 changed files with 987 additions and 25 deletions

View file

@ -29,6 +29,7 @@ namespace MinecraftClient.Pathing.Execution.Templates
private readonly double _horizDist;
private int _tickCount;
private Phase _phase = Phase.Approach;
private bool _airReleaseCommitted;
private bool _leftGround;
private const float YawToleranceDeg = 5f;
@ -103,7 +104,11 @@ namespace MinecraftClient.Pathing.Execution.Templates
_leftGround = true;
bool pastTarget = IsPastTarget(pos);
bool releaseInAir = TransitionBrakingPlanner.ShouldReleaseForwardInAir(_segment, _nextSegment, pos, physics);
bool releaseInAir = ShouldReleaseInAir(pos, physics, world);
if (_segment.ExitTransition == PathTransitionType.LandingRecovery && releaseInAir)
_airReleaseCommitted = true;
if (_airReleaseCommitted)
releaseInAir = true;
if (releaseInAir || pastTarget)
{
@ -138,7 +143,8 @@ namespace MinecraftClient.Pathing.Execution.Templates
return TemplateState.Complete;
if (_segment.ExitTransition != PathTransitionType.ContinueStraight
&& TemplateHelper.IsSettledAtEnd(pos, ExpectedEnd, physics, horizThresholdSq: 0.0025))
&& physics.OnGround
&& TemplateHelper.IsSettledOnTargetBlock(pos, ExpectedEnd, physics))
{
return TemplateState.Complete;
}
@ -169,6 +175,80 @@ namespace MinecraftClient.Pathing.Execution.Templates
return dot > 0.0;
}
private bool ShouldReleaseInAir(Location pos, PlayerPhysics physics, World world)
{
if (TransitionBrakingPlanner.ShouldReleaseForwardInAir(_segment, _nextSegment, pos, physics))
return true;
if (_segment.ExitTransition == PathTransitionType.ContinueStraight || physics.OnGround)
return false;
Location? landingIfHolding = PredictLandingPosition(physics, world, holdForward: true, holdSprint: true);
Location? landingIfReleased = PredictLandingPosition(physics, world, holdForward: false, holdSprint: false);
if (landingIfHolding is null || landingIfReleased is null)
return false;
bool holdingStaysInside = TemplateFootingHelper.IsFootprintInsideTargetBlock(landingIfHolding.Value, ExpectedEnd);
bool releasingStaysInside = TemplateFootingHelper.IsFootprintInsideTargetBlock(landingIfReleased.Value, ExpectedEnd);
if (_segment.ExitTransition == PathTransitionType.LandingRecovery && !holdingStaysInside)
return true;
return !holdingStaysInside && releasingStaysInside;
}
private Location? PredictLandingPosition(PlayerPhysics physics, World world, bool holdForward, bool holdSprint)
{
PlayerPhysics sim = ClonePhysics(physics);
var input = new MovementInput
{
Forward = holdForward,
Sprint = holdSprint
};
for (int tick = 0; tick < 16; tick++)
{
sim.ApplyInput(input);
sim.Tick(world);
if (sim.OnGround)
return new Location(sim.Position.X, sim.Position.Y, sim.Position.Z);
}
return null;
}
private static PlayerPhysics ClonePhysics(PlayerPhysics physics)
{
return new PlayerPhysics
{
Position = physics.Position,
DeltaMovement = physics.DeltaMovement,
Yaw = physics.Yaw,
Pitch = physics.Pitch,
OnGround = physics.OnGround,
HorizontalCollision = physics.HorizontalCollision,
VerticalCollision = physics.VerticalCollision,
VerticalCollisionBelow = physics.VerticalCollisionBelow,
FallDistance = physics.FallDistance,
StuckSpeedMultiplier = physics.StuckSpeedMultiplier,
Xxa = physics.Xxa,
Zza = physics.Zza,
Yya = physics.Yya,
Jumping = physics.Jumping,
Sprinting = physics.Sprinting,
Sneaking = physics.Sneaking,
CreativeFlying = physics.CreativeFlying,
InWater = physics.InWater,
IsUnderWater = physics.IsUnderWater,
InLava = physics.InLava,
OnClimbable = physics.OnClimbable,
HasSlowFalling = physics.HasSlowFalling,
HasLevitation = physics.HasLevitation,
LevitationAmplifier = physics.LevitationAmplifier,
MovementSpeed = physics.MovementSpeed
};
}
private static float YawDifference(float current, float target)
{
float delta = target - current;

View file

@ -1,6 +1,7 @@
using System;
using MinecraftClient.Mapping;
using MinecraftClient.Pathing.Core;
using MinecraftClient.Pathing.Moves;
namespace MinecraftClient.Pathing.Moves.Impl
{
@ -65,6 +66,12 @@ namespace MinecraftClient.Pathing.Moves.Impl
return;
}
if (!ParkourFeasibility.HasRunUp(ctx, x, y, z, XOffset, ZOffset, _yDelta))
{
result.SetImpossible();
return;
}
int destX = x + XOffset;
int destZ = z + ZOffset;
int destY = y + _yDelta;
@ -141,33 +148,23 @@ namespace MinecraftClient.Pathing.Moves.Impl
}
}
// For diagonal parkour, the player's AABB (0.6 wide) must clear both
// cardinal neighbors at the start. A wall on either side will clip the
// AABB during the initial sprint, preventing enough X or Z velocity to
// reach the target. Require BOTH cardinal exits to be passable.
if (xAbs > 0 && zAbs > 0)
if (!ParkourFeasibility.HasDiagonalShoulderClearance(ctx, x, y, z, XOffset, ZOffset))
{
bool canExitViaX = ctx.CanWalkThrough(x + xSign, y, z) &&
ctx.CanWalkThrough(x + xSign, y + 1, z);
bool canExitViaZ = ctx.CanWalkThrough(x, y, z + zSign) &&
ctx.CanWalkThrough(x, y + 1, z + zSign);
if (!canExitViaX || !canExitViaZ)
{
result.SetImpossible();
return;
}
result.SetImpossible();
return;
}
// Overshoot safety: after landing, player continues moving.
// The block(s) past the destination in the jump direction must be passable.
int overX = destX + xSign;
int overZ = destZ + zSign;
if (!ctx.CanWalkThrough(overX, destY, overZ) ||
!ctx.CanWalkThrough(overX, destY + 1, overZ))
if (!ParkourFeasibility.HasCardinalSideClearance(ctx, x, y, z, XOffset, ZOffset))
{
// Wall right after landing - risk of collision. Still allow but add cost.
// (Baritone rejects this, but we allow with penalty since the template
// will decelerate anyway.)
result.SetImpossible();
return;
}
if (!ParkourFeasibility.HasLandingOvershootClearance(
ctx, destX, destY, destZ, xSign, zSign))
{
result.SetImpossible();
return;
}
// Cost model following Baritone:

View file

@ -0,0 +1,104 @@
using System;
using MinecraftClient.Pathing.Core;
namespace MinecraftClient.Pathing.Moves;
internal static class ParkourFeasibility
{
public static bool HasRunUp(
CalculationContext ctx,
int x,
int y,
int z,
int xOffset,
int zOffset,
int yDelta)
{
double horiz = Math.Sqrt(xOffset * xOffset + zOffset * zOffset);
double threshold = yDelta > 0 ? 2.5 : 3.5;
if (horiz < threshold)
return true;
int backX = x - Math.Sign(xOffset);
int backZ = z - Math.Sign(zOffset);
if (!ctx.CanWalkOn(backX, y - 1, backZ))
return false;
return IsColumnPassable(ctx, backX, y, backZ);
}
public static bool HasDiagonalShoulderClearance(
CalculationContext ctx,
int x,
int y,
int z,
int xOffset,
int zOffset)
{
if (xOffset == 0 || zOffset == 0)
return true;
return IsColumnPassable(ctx, x + Math.Sign(xOffset), y, z)
&& IsColumnPassable(ctx, x, y, z + Math.Sign(zOffset));
}
public static bool HasLandingOvershootClearance(
CalculationContext ctx,
int destX,
int destY,
int destZ,
int xSign,
int zSign)
{
if (xSign == 0 && zSign == 0)
return true;
return IsColumnPassable(ctx, destX + xSign, destY, destZ + zSign);
}
public static bool HasCardinalSideClearance(
CalculationContext ctx,
int x,
int y,
int z,
int xOffset,
int zOffset)
{
if ((xOffset == 0) == (zOffset == 0))
return true;
if (xOffset != 0)
{
int xSign = Math.Sign(xOffset);
for (int step = 1; step <= Math.Abs(xOffset); step++)
{
int gx = x + xSign * step;
if (!IsColumnPassable(ctx, gx, y, z - 1)
|| !IsColumnPassable(ctx, gx, y, z + 1))
{
return false;
}
}
return true;
}
int zSign = Math.Sign(zOffset);
for (int step = 1; step <= Math.Abs(zOffset); step++)
{
int gz = z + zSign * step;
if (!IsColumnPassable(ctx, x - 1, y, gz)
|| !IsColumnPassable(ctx, x + 1, y, gz))
{
return false;
}
}
return true;
}
private static bool IsColumnPassable(CalculationContext ctx, int x, int y, int z)
{
return ctx.CanWalkThrough(x, y, z)
&& ctx.CanWalkThrough(x, y + 1, z);
}
}