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
///