feat: add Phase 1 core pathfinding architecture

Implements the new Baritone-inspired A* pathfinding system:
- Core types: PathNode, PathResult, MoveResult, MoveType, ActionCosts
- BinaryHeapOpenSet min-heap for A* open set
- AStarPathFinder with timeout, cancellation, partial path support
- CalculationContext for thread-safe world state snapshots
- MoveHelper for block passability checks
- IGoal interface + GoalBlock, GoalXZ, GoalNear, GoalComposite
- IMove interface + MoveTraverse, MoveDiagonal, MoveAscend, MoveDescend, MoveClimb
- /pathfind command for testing the new pathfinder

Made-with: Cursor
This commit is contained in:
BruceChen 2026-04-11 01:48:10 +08:00
parent 3efe63c54d
commit 1abab20f17
23 changed files with 1222 additions and 0 deletions

View file

@ -0,0 +1,50 @@
using MinecraftClient.Pathing.Core;
namespace MinecraftClient.Pathing.Moves.Impl
{
/// <summary>
/// Jump up 1 block in a cardinal direction.
/// Requires: headroom at (x, y+2, z), body space at dest (y+1, y+2), ground at dest (y).
/// </summary>
public sealed class MoveAscend : IMove
{
public MoveType Type => MoveType.Ascend;
public int XOffset { get; }
public int ZOffset { get; }
public bool DynamicY => false;
public MoveAscend(int xOffset, int zOffset)
{
XOffset = xOffset;
ZOffset = zOffset;
}
public void Calculate(CalculationContext ctx, int x, int y, int z, ref MoveResult result)
{
int destX = x + XOffset;
int destZ = z + ZOffset;
int destY = y + 1;
if (!ctx.CanWalkThrough(x, y + 2, z))
{
result.SetImpossible();
return;
}
if (!ctx.CanWalkThrough(destX, destY, destZ) || !ctx.CanWalkThrough(destX, destY + 1, destZ))
{
result.SetImpossible();
return;
}
if (!ctx.CanWalkOn(destX, y, destZ))
{
result.SetImpossible();
return;
}
double cost = ctx.SprintCost + ctx.JumpPenalty;
result.Set(destX, destY, destZ, cost);
}
}
}

View file

@ -0,0 +1,64 @@
using MinecraftClient.Mapping;
using MinecraftClient.Pathing.Core;
namespace MinecraftClient.Pathing.Moves.Impl
{
/// <summary>
/// Climb up or down a ladder/vine at the current X,Z position.
/// </summary>
public sealed class MoveClimb : IMove
{
public MoveType Type => MoveType.Climb;
public int XOffset => 0;
public int ZOffset => 0;
public bool DynamicY => false;
private readonly bool _up;
public MoveClimb(bool up)
{
_up = up;
}
public void Calculate(CalculationContext ctx, int x, int y, int z, ref MoveResult result)
{
var currentMat = ctx.GetMaterial(x, y, z);
if (!MoveHelper.IsClimbable(currentMat))
{
result.SetImpossible();
return;
}
if (_up)
{
int destY = y + 1;
if (!ctx.CanWalkThrough(x, destY + 1, z))
{
result.SetImpossible();
return;
}
var aboveMat = ctx.GetMaterial(x, destY, z);
if (MoveHelper.IsClimbable(aboveMat) || !ctx.GetMaterial(x, destY, z).IsSolid())
{
result.Set(x, destY, z, ActionCosts.LadderUpOne);
return;
}
result.SetImpossible();
}
else
{
int destY = y - 1;
var belowMat = ctx.GetMaterial(x, destY, z);
if (MoveHelper.IsClimbable(belowMat) || !belowMat.IsSolid())
{
result.Set(x, destY, z, ActionCosts.LadderDownOne);
return;
}
result.SetImpossible();
}
}
}
}

View file

@ -0,0 +1,66 @@
using MinecraftClient.Pathing.Core;
namespace MinecraftClient.Pathing.Moves.Impl
{
/// <summary>
/// Walk off a ledge and drop 1-N blocks in a cardinal direction.
/// Scans downward for a landing spot within MaxFallHeight.
/// </summary>
public sealed class MoveDescend : IMove
{
public MoveType Type => MoveType.Descend;
public int XOffset { get; }
public int ZOffset { get; }
public bool DynamicY => true;
public MoveDescend(int xOffset, int zOffset)
{
XOffset = xOffset;
ZOffset = zOffset;
}
public void Calculate(CalculationContext ctx, int x, int y, int z, ref MoveResult result)
{
int destX = x + XOffset;
int destZ = z + ZOffset;
if (!ctx.CanWalkThrough(destX, y, destZ) || !ctx.CanWalkThrough(destX, y + 1, destZ))
{
result.SetImpossible();
return;
}
for (int fallDist = 1; fallDist <= ctx.MaxFallHeight; fallDist++)
{
int landY = y - fallDist;
if (ctx.CanWalkOn(destX, landY - 1, destZ))
{
if (!ctx.CanWalkThrough(destX, landY, destZ))
{
result.SetImpossible();
return;
}
double cost = ActionCosts.WalkOffBlock + ActionCosts.FallCost(fallDist);
if (MoveHelper.IsHazardous(ctx.GetMaterial(destX, landY - 1, destZ)))
{
result.SetImpossible();
return;
}
result.Set(destX, landY, destZ, cost);
return;
}
if (!ctx.CanWalkThrough(destX, landY, destZ))
{
result.SetImpossible();
return;
}
}
result.SetImpossible();
}
}
}

View file

@ -0,0 +1,54 @@
using MinecraftClient.Pathing.Core;
namespace MinecraftClient.Pathing.Moves.Impl
{
/// <summary>
/// Diagonal walk (1 block in both X and Z, same Y).
/// Checks both intermediate cardinal columns for clearance.
/// </summary>
public sealed class MoveDiagonal : IMove
{
public MoveType Type => MoveType.Diagonal;
public int XOffset { get; }
public int ZOffset { get; }
public bool DynamicY => false;
public MoveDiagonal(int xOffset, int zOffset)
{
XOffset = xOffset;
ZOffset = zOffset;
}
public void Calculate(CalculationContext ctx, int x, int y, int z, ref MoveResult result)
{
int destX = x + XOffset;
int destZ = z + ZOffset;
if (!ctx.CanWalkThrough(destX, y, destZ) || !ctx.CanWalkThrough(destX, y + 1, destZ))
{
result.SetImpossible();
return;
}
if (!ctx.CanWalkOn(destX, y - 1, destZ))
{
result.SetImpossible();
return;
}
if (!ctx.CanWalkThrough(x + XOffset, y, z) || !ctx.CanWalkThrough(x + XOffset, y + 1, z))
{
result.SetImpossible();
return;
}
if (!ctx.CanWalkThrough(x, y, z + ZOffset) || !ctx.CanWalkThrough(x, y + 1, z + ZOffset))
{
result.SetImpossible();
return;
}
result.Set(destX, y, destZ, ctx.SprintCost * ActionCosts.DiagonalMultiplier);
}
}
}

View file

@ -0,0 +1,54 @@
using MinecraftClient.Pathing.Core;
namespace MinecraftClient.Pathing.Moves.Impl
{
/// <summary>
/// Flat cardinal walk (1 block in +/-X or +/-Z, same Y).
/// Checks body+head passable and ground below destination.
/// </summary>
public sealed class MoveTraverse : IMove
{
public MoveType Type => MoveType.Traverse;
public int XOffset { get; }
public int ZOffset { get; }
public bool DynamicY => false;
public MoveTraverse(int xOffset, int zOffset)
{
XOffset = xOffset;
ZOffset = zOffset;
}
public void Calculate(CalculationContext ctx, int x, int y, int z, ref MoveResult result)
{
int destX = x + XOffset;
int destZ = z + ZOffset;
if (!ctx.CanWalkThrough(destX, y, destZ))
{
result.SetImpossible();
return;
}
if (!ctx.CanWalkThrough(destX, y + 1, destZ))
{
result.SetImpossible();
return;
}
if (!ctx.CanWalkOn(destX, y - 1, destZ))
{
result.SetImpossible();
return;
}
double cost = ctx.SprintCost;
var destFloorMat = ctx.GetMaterial(destX, y - 1, destZ);
if (destFloorMat == Mapping.Material.SoulSand)
cost *= 1.0 / Physics.PhysicsConsts.SoulSandSpeedFactor;
result.Set(destX, y, destZ, cost);
}
}
}