From 8ece75acc3836a13c2876c8a4be98656c5a9c958 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 12 Apr 2026 00:03:43 +0800 Subject: [PATCH] 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 --- MinecraftClient/Commands/Pathfind.cs | 96 +------------------ MinecraftClient/McClient.cs | 62 ++++++++---- .../Pathing/Execution/PathSegmentManager.cs | 1 + MinecraftClient/Pathing/Moves/MoveHelper.cs | 2 +- MinecraftClient/Scripting/ChatBot.cs | 30 ++++++ 5 files changed, 78 insertions(+), 113 deletions(-) diff --git a/MinecraftClient/Commands/Pathfind.cs b/MinecraftClient/Commands/Pathfind.cs index 50eade55..8f8bd306 100644 --- a/MinecraftClient/Commands/Pathfind.cs +++ b/MinecraftClient/Commands/Pathfind.cs @@ -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); } } } diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index cb52b7ff..4f91d4a1 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -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 } /// - /// 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. /// - 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 } } + /// + /// 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. + /// + 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); + } + /// /// Send a chat message or command to the server /// @@ -3436,7 +3451,16 @@ namespace MinecraftClient /// Current goal of movement. Location.Zero if not set. 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; } /// @@ -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 diff --git a/MinecraftClient/Pathing/Execution/PathSegmentManager.cs b/MinecraftClient/Pathing/Execution/PathSegmentManager.cs index c6492cc0..1582dd4a 100644 --- a/MinecraftClient/Pathing/Execution/PathSegmentManager.cs +++ b/MinecraftClient/Pathing/Execution/PathSegmentManager.cs @@ -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? debugLog = null, Action? infoLog = null) { diff --git a/MinecraftClient/Pathing/Moves/MoveHelper.cs b/MinecraftClient/Pathing/Moves/MoveHelper.cs index c9396461..63fc2c40 100644 --- a/MinecraftClient/Pathing/Moves/MoveHelper.cs +++ b/MinecraftClient/Pathing/Moves/MoveHelper.cs @@ -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; diff --git a/MinecraftClient/Scripting/ChatBot.cs b/MinecraftClient/Scripting/ChatBot.cs index 45236fda..851f22b5 100644 --- a/MinecraftClient/Scripting/ChatBot.cs +++ b/MinecraftClient/Scripting/ChatBot.cs @@ -1246,6 +1246,18 @@ namespace MinecraftClient.Scripting return Handler.MoveTo(location, allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeout); } + /// + /// Navigate to a goal using A* pathfinding with template-based execution. + /// Supports GoalBlock, GoalXZ, GoalNear, GoalComposite for flexible targeting. + /// + /// Target goal (GoalBlock, GoalNear, GoalXZ, etc.) + /// Maximum pathfinding computation time in milliseconds + /// Tuple of (success, descriptive message) + protected (bool success, string message) NavigateTo(Pathing.Goals.IGoal goal, long timeoutMs = 5000) + { + return Handler.NavigateToGoal(goal, timeoutMs); + } + /// /// Check if the client is currently processing a Movement. /// @@ -1255,6 +1267,24 @@ namespace MinecraftClient.Scripting return Handler.ClientIsMoving(); } + /// + /// Cancel the current movement, stopping both legacy and A* pathfinding. + /// + /// true if there was an active movement that was cancelled + protected bool CancelMovement() + { + return Handler.CancelMovement(); + } + + /// + /// Get the current movement goal location. + /// Returns Location.Zero if no movement is active. + /// + protected Location GetCurrentMovementGoal() + { + return Handler.GetCurrentMovementGoal(); + } + /// /// Look at the specified location ///