mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
feat: complete Phase 4 McClient integration for A* pathfinding
- Fix MoveHelper.IsOpenGate: MangroveWood -> MangroveFenceGate - Fix ResetStateForTransfer to cancel and clear pathSegmentManager - Fix GetCurrentMovementGoal to return correct goal during A* navigation - Fix SetMovementSpeed(Sneak) speed value consistency (2 -> 1) - Migrate /pathfind command to use MoveToAStar + PathSegmentManager - Add NavigateToGoal(IGoal) to McClient for flexible goal navigation - Refactor MoveToAStar to delegate to NavigateToGoal - Add ChatBot API: NavigateTo, CancelMovement, GetCurrentMovementGoal - Expose PathSegmentManager.Goal property for external goal inspection Made-with: Cursor
This commit is contained in:
parent
53082d387e
commit
8ece75acc3
5 changed files with 78 additions and 113 deletions
|
|
@ -1,12 +1,7 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Brigadier.NET;
|
||||
using Brigadier.NET.Builder;
|
||||
using MinecraftClient.CommandHandler;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Pathing.Goals;
|
||||
using static MinecraftClient.CommandHandler.CmdResult;
|
||||
|
||||
namespace MinecraftClient.Commands
|
||||
|
|
@ -38,7 +33,7 @@ namespace MinecraftClient.Commands
|
|||
return r.SetAndReturn(GetCmdDescTranslated());
|
||||
}
|
||||
|
||||
private int DoPathfind(CmdResult r, Location goal)
|
||||
private static int DoPathfind(CmdResult r, Location goal)
|
||||
{
|
||||
McClient handler = CmdResult.currentHandler!;
|
||||
if (!handler.GetTerrainEnabled())
|
||||
|
|
@ -47,94 +42,9 @@ namespace MinecraftClient.Commands
|
|||
Location current = handler.GetCurrentLocation();
|
||||
goal.ToAbsolute(current);
|
||||
|
||||
int startX = (int)Math.Floor(current.X);
|
||||
int startY = (int)Math.Floor(current.Y);
|
||||
int startZ = (int)Math.Floor(current.Z);
|
||||
int goalX = (int)Math.Floor(goal.X);
|
||||
int goalY = (int)Math.Floor(goal.Y);
|
||||
int goalZ = (int)Math.Floor(goal.Z);
|
||||
var (success, message) = handler.MoveToAStar(goal, timeoutMs: 10000);
|
||||
|
||||
handler.Log.Info($"[Pathfind] Planning from ({startX},{startY},{startZ}) to ({goalX},{goalY},{goalZ})");
|
||||
|
||||
var ctx = new CalculationContext(
|
||||
handler.GetWorld(),
|
||||
canSprint: true,
|
||||
maxFallHeight: 3);
|
||||
|
||||
var finder = new AStarPathFinder();
|
||||
finder.DebugLog = msg => handler.Log.Info(msg);
|
||||
|
||||
var goalObj = new GoalBlock(goalX, goalY, goalZ);
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
var result = finder.Calculate(ctx, startX, startY, startZ, goalObj, cts.Token, timeoutMs: 10000);
|
||||
|
||||
handler.Log.Info($"[Pathfind] Result: {result.Status}, {result.Path.Count} nodes, " +
|
||||
$"{result.NodesExplored} explored, {result.ElapsedMs}ms");
|
||||
|
||||
if (result.Path.Count > 1)
|
||||
{
|
||||
handler.Log.Info("[Pathfind] Path waypoints:");
|
||||
for (int i = 0; i < result.Path.Count; i++)
|
||||
{
|
||||
var n = result.Path[i];
|
||||
handler.Log.Info($" [{i}] ({n.X},{n.Y},{n.Z}) via {n.MoveUsed}");
|
||||
}
|
||||
|
||||
handler.Log.Info("[Pathfind] Beginning movement along path...");
|
||||
FollowPath(handler, result);
|
||||
}
|
||||
else
|
||||
{
|
||||
handler.Log.Warn("[Pathfind] No path found!");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
handler.Log.Warn($"[Pathfind] Exception: {ex.Message}");
|
||||
}
|
||||
});
|
||||
|
||||
return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_pathfind_started, goalX, goalY, goalZ));
|
||||
}
|
||||
|
||||
private static void FollowPath(McClient handler, PathResult result)
|
||||
{
|
||||
for (int i = 1; i < result.Path.Count; i++)
|
||||
{
|
||||
var node = result.Path[i];
|
||||
var target = new Location(node.X + 0.5, node.Y, node.Z + 0.5);
|
||||
|
||||
handler.Log.Info($"[Pathfind] Moving to waypoint [{i}/{result.Path.Count - 1}]: ({node.X},{node.Y},{node.Z}) via {node.MoveUsed}");
|
||||
|
||||
bool success = handler.MoveTo(target, allowUnsafe: true, allowDirectTeleport: false, timeout: TimeSpan.FromSeconds(10));
|
||||
if (!success)
|
||||
{
|
||||
handler.Log.Warn($"[Pathfind] Sub-path failed for waypoint [{i}], using direct move");
|
||||
handler.MoveTo(target, allowUnsafe: true, allowDirectTeleport: true);
|
||||
}
|
||||
|
||||
int maxWaitTicks = 200;
|
||||
int waited = 0;
|
||||
while (handler.ClientIsMoving() && waited < maxWaitTicks)
|
||||
{
|
||||
Thread.Sleep(50);
|
||||
waited++;
|
||||
}
|
||||
|
||||
var cur = handler.GetCurrentLocation();
|
||||
double dx = cur.X - target.X;
|
||||
double dz = cur.Z - target.Z;
|
||||
double horizDist = Math.Sqrt(dx * dx + dz * dz);
|
||||
|
||||
handler.Log.Info($"[Pathfind] Waypoint [{i}] done, pos=({cur.X:F2},{cur.Y:F2},{cur.Z:F2}), dist={horizDist:F2}");
|
||||
}
|
||||
|
||||
handler.Log.Info("[Pathfind] Path execution complete!");
|
||||
return r.SetAndReturn(success ? Status.Done : Status.Fail, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -567,6 +567,8 @@ namespace MinecraftClient
|
|||
isUnderSlab = false;
|
||||
path = null;
|
||||
pathTarget = null;
|
||||
pathSegmentManager?.Cancel();
|
||||
pathSegmentManager = null;
|
||||
_yaw = null;
|
||||
_pitch = null;
|
||||
LastDigPosition = null;
|
||||
|
|
@ -1716,11 +1718,11 @@ namespace MinecraftClient
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigate to a goal using the new A* pathfinder.
|
||||
/// Runs the search, converts the result into the legacy Queue path, and starts movement.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public (bool success, string message) MoveToAStar(Location goal, long timeoutMs = 5000)
|
||||
public (bool success, string message) NavigateToGoal(Pathing.Goals.IGoal goal, long timeoutMs = 5000)
|
||||
{
|
||||
lock (locationLock)
|
||||
{
|
||||
|
|
@ -1736,21 +1738,12 @@ namespace MinecraftClient
|
|||
if (!ctx.CanWalkThrough(sx, sy, sz) && ctx.CanWalkThrough(sx, sy + 1, sz))
|
||||
sy++;
|
||||
|
||||
int gx = (int)Math.Floor(goal.X);
|
||||
int gy = (int)Math.Floor(goal.Y);
|
||||
int gz = (int)Math.Floor(goal.Z);
|
||||
|
||||
if (!ctx.CanWalkThrough(gx, gy, gz) && ctx.CanWalkThrough(gx, gy + 1, gz))
|
||||
gy++;
|
||||
|
||||
Log.Info($"[Goto] A* search from ({sx},{sy},{sz}) to ({gx},{gy},{gz}) " +
|
||||
$"[raw pos=({location.X:F2},{location.Y:F2},{location.Z:F2})]");
|
||||
Log.Info($"[Navigate] A* search from ({sx},{sy},{sz}) to {goal}");
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
var pathGoal = new Pathing.Goals.GoalBlock(gx, gy, gz);
|
||||
var result = finder.Calculate(ctx, sx, sy, sz, pathGoal, cts.Token, timeoutMs);
|
||||
var result = finder.Calculate(ctx, sx, sy, sz, goal, cts.Token, timeoutMs);
|
||||
|
||||
Log.Info($"[Goto] A* result: {result.Status}, nodes={result.NodesExplored}, " +
|
||||
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)
|
||||
|
|
@ -1762,7 +1755,7 @@ namespace MinecraftClient
|
|||
for (int i = 1; i < result.Path.Count; i++)
|
||||
{
|
||||
var node = result.Path[i];
|
||||
Log.Debug($"[Goto] seg[{i - 1}] = {node.MoveUsed}: ({node.X},{node.Y},{node.Z})");
|
||||
Log.Debug($"[Navigate] seg[{i - 1}] = {node.MoveUsed}: ({node.X},{node.Y},{node.Z})");
|
||||
}
|
||||
|
||||
pathTarget = null;
|
||||
|
|
@ -1771,7 +1764,7 @@ namespace MinecraftClient
|
|||
pathSegmentManager = new Pathing.Execution.PathSegmentManager(
|
||||
debugLog: msg => Log.Debug(msg),
|
||||
infoLog: msg => Log.Info(msg));
|
||||
pathSegmentManager.StartNavigation(pathGoal, result);
|
||||
pathSegmentManager.StartNavigation(goal, result);
|
||||
|
||||
string statusStr = result.Status == Pathing.Core.PathStatus.Partial ? " (partial)" : "";
|
||||
return (true, string.Format(Translations.cmd_goto_success,
|
||||
|
|
@ -1779,6 +1772,28 @@ namespace MinecraftClient
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigate to a block location using the new A* pathfinder and template-based execution.
|
||||
/// Convenience overload that creates a GoalBlock from the location.
|
||||
/// Returns a description of the result for UI feedback.
|
||||
/// </summary>
|
||||
public (bool success, string message) MoveToAStar(Location goal, long timeoutMs = 5000)
|
||||
{
|
||||
int gx = (int)Math.Floor(goal.X);
|
||||
int gy = (int)Math.Floor(goal.Y);
|
||||
int gz = (int)Math.Floor(goal.Z);
|
||||
|
||||
lock (locationLock)
|
||||
{
|
||||
var ctx = new Pathing.Core.CalculationContext(world);
|
||||
if (!ctx.CanWalkThrough(gx, gy, gz) && ctx.CanWalkThrough(gx, gy + 1, gz))
|
||||
gy++;
|
||||
}
|
||||
|
||||
var pathGoal = new Pathing.Goals.GoalBlock(gx, gy, gz);
|
||||
return NavigateToGoal(pathGoal, timeoutMs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a chat message or command to the server
|
||||
/// </summary>
|
||||
|
|
@ -3436,7 +3451,16 @@ namespace MinecraftClient
|
|||
/// <returns>Current goal of movement. Location.Zero if not set.</returns>
|
||||
public Location GetCurrentMovementGoal()
|
||||
{
|
||||
return (ClientIsMoving() || path is null) ? Location.Zero : path.Last();
|
||||
if (pathSegmentManager is not null && pathSegmentManager.IsNavigating)
|
||||
{
|
||||
if (pathSegmentManager.Goal is Pathing.Goals.GoalBlock gb)
|
||||
return new Location(gb.X + 0.5, gb.Y, gb.Z + 0.5);
|
||||
}
|
||||
|
||||
if (path is not null && path.Count > 0)
|
||||
return path.Last();
|
||||
|
||||
return Location.Zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -3463,7 +3487,7 @@ namespace MinecraftClient
|
|||
{
|
||||
case MovementType.Sneak:
|
||||
// https://minecraft.wiki/w/Sneaking#Effects - Sneaking 1.31m/s
|
||||
Config.Main.Advanced.MovementSpeed = 2;
|
||||
Config.Main.Advanced.MovementSpeed = 1;
|
||||
break;
|
||||
case MovementType.Walk:
|
||||
// https://minecraft.wiki/w/Walking#Usage - Walking 4.317 m/s
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ namespace MinecraftClient.Pathing.Execution
|
|||
|
||||
public bool IsNavigating => _executor is not null && !_executor.IsComplete;
|
||||
public int ReplanCount => _replanCount;
|
||||
public IGoal? Goal => _goal;
|
||||
|
||||
public PathSegmentManager(Action<string>? debugLog = null, Action<string>? infoLog = null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ namespace MinecraftClient.Pathing.Moves
|
|||
{
|
||||
return mat is Material.AcaciaFenceGate or Material.BirchFenceGate
|
||||
or Material.CrimsonFenceGate or Material.DarkOakFenceGate
|
||||
or Material.JungleFenceGate or Material.MangroveWood
|
||||
or Material.JungleFenceGate or Material.MangroveFenceGate
|
||||
or Material.OakFenceGate or Material.SpruceFenceGate
|
||||
or Material.WarpedFenceGate or Material.CherryFenceGate
|
||||
or Material.BambooFenceGate or Material.PaleOakFenceGate;
|
||||
|
|
|
|||
|
|
@ -1246,6 +1246,18 @@ namespace MinecraftClient.Scripting
|
|||
return Handler.MoveTo(location, allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeout);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigate to a goal using A* pathfinding with template-based execution.
|
||||
/// Supports GoalBlock, GoalXZ, GoalNear, GoalComposite for flexible targeting.
|
||||
/// </summary>
|
||||
/// <param name="goal">Target goal (GoalBlock, GoalNear, GoalXZ, etc.)</param>
|
||||
/// <param name="timeoutMs">Maximum pathfinding computation time in milliseconds</param>
|
||||
/// <returns>Tuple of (success, descriptive message)</returns>
|
||||
protected (bool success, string message) NavigateTo(Pathing.Goals.IGoal goal, long timeoutMs = 5000)
|
||||
{
|
||||
return Handler.NavigateToGoal(goal, timeoutMs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the client is currently processing a Movement.
|
||||
/// </summary>
|
||||
|
|
@ -1255,6 +1267,24 @@ namespace MinecraftClient.Scripting
|
|||
return Handler.ClientIsMoving();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancel the current movement, stopping both legacy and A* pathfinding.
|
||||
/// </summary>
|
||||
/// <returns>true if there was an active movement that was cancelled</returns>
|
||||
protected bool CancelMovement()
|
||||
{
|
||||
return Handler.CancelMovement();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the current movement goal location.
|
||||
/// Returns Location.Zero if no movement is active.
|
||||
/// </summary>
|
||||
protected Location GetCurrentMovementGoal()
|
||||
{
|
||||
return Handler.GetCurrentMovementGoal();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Look at the specified location
|
||||
/// </summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue