diff --git a/MinecraftClient/ChatBot.cs b/MinecraftClient/ChatBot.cs
index f1505ddb..3b37d98b 100644
--- a/MinecraftClient/ChatBot.cs
+++ b/MinecraftClient/ChatBot.cs
@@ -983,13 +983,26 @@ namespace MinecraftClient
///
/// Location to reach
/// Allow possible but unsafe locations thay may hurt the player: lava, cactus...
- /// Allow non-vanilla teleport instead of computing path, but may cause invalid moves and/or trigger anti-cheat plugins
+ /// Allow non-vanilla direct teleport instead of computing path, but may cause invalid moves and/or trigger anti-cheat plugins
+ /// If no valid path can be found, also allow locations within specified distance of destination
+ /// Do not get closer of destination than specified distance
+ /// How long to wait before stopping computation (default: 5 seconds)
+ /// When location is unreachable, computation will reach timeout, then optionally fallback to a close location within maxOffset
/// True if a path has been found
- protected bool MoveToLocation(Mapping.Location location, bool allowUnsafe = false, bool allowDirectTeleport = false)
+ protected bool MoveToLocation(Mapping.Location location, bool allowUnsafe = false, bool allowDirectTeleport = false, int maxOffset = 0, int minOffset = 0, TimeSpan? timeout = null)
{
- return Handler.MoveTo(location, allowUnsafe, allowDirectTeleport);
+ return Handler.MoveTo(location, allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeout);
}
+ ///
+ /// Check if the client is currently processing a Movement.
+ ///
+ /// true if a movement is currently handled
+ protected bool ClientIsMoving()
+ {
+ return Handler.ClientIsMoving();
+ }
+
///
/// Look at the specified location
///
diff --git a/MinecraftClient/Mapping/Movement.cs b/MinecraftClient/Mapping/Movement.cs
index 6c947343..224454cd 100644
--- a/MinecraftClient/Mapping/Movement.cs
+++ b/MinecraftClient/Mapping/Movement.cs
@@ -1,7 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
namespace MinecraftClient.Mapping
{
@@ -129,62 +130,120 @@ namespace MinecraftClient.Mapping
/// Start location
/// Destination location
/// Allow possible but unsafe locations
+ /// If no valid path can be found, also allow locations within specified distance of destination
+ /// Do not get closer of destination than specified distance
+ /// How long to wait before stopping computation
+ /// When location is unreachable, computation will reach timeout, then optionally fallback to a close location within maxOffset
/// A list of locations, or null if calculation failed
- public static Queue CalculatePath(World world, Location start, Location goal, bool allowUnsafe = false)
+ public static Queue CalculatePath(World world, Location start, Location goal, bool allowUnsafe, int maxOffset, int minOffset, TimeSpan timeout)
{
- Queue result = null;
-
- AutoTimeout.Perform(() =>
+ CancellationTokenSource cts = new CancellationTokenSource();
+ Task> pathfindingTask = Task.Factory.StartNew(() => Movement.CalculatePath(world, start, goal, allowUnsafe, maxOffset, minOffset, cts.Token));
+ pathfindingTask.Wait(timeout);
+ if (!pathfindingTask.IsCompleted)
{
- HashSet ClosedSet = new HashSet(); // The set of locations already evaluated.
- HashSet OpenSet = new HashSet(new[] { start }); // The set of tentative nodes to be evaluated, initially containing the start node
- Dictionary Came_From = new Dictionary(); // The map of navigated nodes.
+ cts.Cancel();
+ pathfindingTask.Wait();
+ }
+ return pathfindingTask.Result;
+ }
- Dictionary g_score = new Dictionary(); //:= map with default value of Infinity
- g_score[start] = 0; // Cost from start along best known path.
- // Estimated total cost from start to goal through y.
- Dictionary f_score = new Dictionary(); //:= map with default value of Infinity
- f_score[start] = (int)start.DistanceSquared(goal); //heuristic_cost_estimate(start, goal)
+ ///
+ /// Calculate a path from the start location to the destination location
+ ///
+ ///
+ /// Based on the A* pathfinding algorithm described on Wikipedia
+ ///
+ ///
+ /// Start location
+ /// Destination location
+ /// Allow possible but unsafe locations
+ /// If no valid path can be found, also allow locations within specified distance of destination
+ /// Do not get closer of destination than specified distance
+ /// Token for stopping computation after a certain time
+ /// A list of locations, or null if calculation failed
+ public static Queue CalculatePath(World world, Location start, Location goal, bool allowUnsafe, int maxOffset, int minOffset, CancellationToken ct)
+ {
- while (OpenSet.Count > 0)
+ if (minOffset > maxOffset)
+ throw new ArgumentException("minOffset must be lower or equal to maxOffset", "minOffset");
+
+ Location current = new Location(); // Location that is currently processed
+ Location closestGoal = new Location(); // Closest Location to the goal. Used for approaching if goal can not be reached or was not found.
+ HashSet ClosedSet = new HashSet(); // The set of locations already evaluated.
+ HashSet OpenSet = new HashSet(new[] { start }); // The set of tentative nodes to be evaluated, initially containing the start node
+ Dictionary Came_From = new Dictionary(); // The map of navigated nodes.
+
+ Dictionary g_score = new Dictionary(); //:= map with default value of Infinity
+ g_score[start] = 0; // Cost from start along best known path.
+ // Estimated total cost from start to goal through y.
+ Dictionary f_score = new Dictionary(); //:= map with default value of Infinity
+ f_score[start] = (int)start.DistanceSquared(goal); //heuristic_cost_estimate(start, goal)
+
+ while (OpenSet.Count > 0)
+ {
+ current = //the node in OpenSet having the lowest f_score[] value
+ OpenSet.Select(location => f_score.ContainsKey(location)
+ ? new KeyValuePair(location, f_score[location])
+ : new KeyValuePair(location, int.MaxValue))
+ .OrderBy(pair => pair.Value).First().Key;
+
+ // Only assert a value if it is of actual use later
+ if (maxOffset > 0 && ClosedSet.Count > 0)
+ // Get the block that currently is closest to the goal
+ closestGoal = ClosedSet.OrderBy(checkedLocation => checkedLocation.DistanceSquared(goal)).First();
+
+ // Stop when goal is reached or we are close enough
+ if (current == goal || (minOffset > 0 && current.DistanceSquared(goal) <= minOffset))
+ return ReconstructPath(Came_From, current);
+ else if (ct.IsCancellationRequested)
+ break; // Return if we are cancelled
+
+ OpenSet.Remove(current);
+ ClosedSet.Add(current);
+
+ foreach (Location neighbor in GetAvailableMoves(world, current, allowUnsafe))
{
- Location current = //the node in OpenSet having the lowest f_score[] value
- OpenSet.Select(location => f_score.ContainsKey(location)
- ? new KeyValuePair(location, f_score[location])
- : new KeyValuePair(location, int.MaxValue))
- .OrderBy(pair => pair.Value).First().Key;
- if (current == goal)
- { //reconstruct_path(Came_From, goal)
- List total_path = new List(new[] { current });
- while (Came_From.ContainsKey(current))
- {
- current = Came_From[current];
- total_path.Add(current);
- }
- total_path.Reverse();
- result = new Queue(total_path);
- }
- OpenSet.Remove(current);
- ClosedSet.Add(current);
- foreach (Location neighbor in GetAvailableMoves(world, current, allowUnsafe))
- {
- if (ClosedSet.Contains(neighbor))
- continue; // Ignore the neighbor which is already evaluated.
- int tentative_g_score = g_score[current] + (int)current.DistanceSquared(neighbor); //dist_between(current,neighbor) // length of this path.
- if (!OpenSet.Contains(neighbor)) // Discover a new node
- OpenSet.Add(neighbor);
- else if (tentative_g_score >= g_score[neighbor])
- continue; // This is not a better path.
+ if (ct.IsCancellationRequested)
+ break; // Stop searching for blocks if we are cancelled.
+ if (ClosedSet.Contains(neighbor))
+ continue; // Ignore the neighbor which is already evaluated.
+ int tentative_g_score = g_score[current] + (int)current.DistanceSquared(neighbor); //dist_between(current,neighbor) // length of this path.
+ if (!OpenSet.Contains(neighbor)) // Discover a new node
+ OpenSet.Add(neighbor);
+ else if (tentative_g_score >= g_score[neighbor])
+ continue; // This is not a better path.
- // This path is the best until now. Record it!
- Came_From[neighbor] = current;
- g_score[neighbor] = tentative_g_score;
- f_score[neighbor] = g_score[neighbor] + (int)neighbor.DistanceSquared(goal); //heuristic_cost_estimate(neighbor, goal)
- }
+ // This path is the best until now. Record it!
+ Came_From[neighbor] = current;
+ g_score[neighbor] = tentative_g_score;
+ f_score[neighbor] = g_score[neighbor] + (int)neighbor.DistanceSquared(goal); //heuristic_cost_estimate(neighbor, goal)
}
- }, TimeSpan.FromSeconds(5));
+ }
- return result;
+ // Goal could not be reached. Set the path to the closest location if close enough
+ if (maxOffset == int.MaxValue || goal.DistanceSquared(closestGoal) <= maxOffset)
+ return ReconstructPath(Came_From, closestGoal);
+ else
+ return null;
+ }
+
+ ///
+ /// Helper function for CalculatePath(). Backtrack from goal to start to reconstruct a step-by-step path.
+ ///
+ /// The collection of Locations that leads back to the start
+ /// Endpoint of our later walk
+ /// the path that leads to current from the start position
+ private static Queue ReconstructPath(Dictionary Came_From, Location current)
+ {
+ List total_path = new List(new[] { current });
+ while (Came_From.ContainsKey(current))
+ {
+ current = Came_From[current];
+ total_path.Add(current);
+ }
+ total_path.Reverse();
+ return new Queue(total_path);
}
/* ========= LOCATION PROPERTIES ========= */
diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs
index dfd149f5..94eda171 100644
--- a/MinecraftClient/McClient.cs
+++ b/MinecraftClient/McClient.cs
@@ -1061,8 +1061,12 @@ namespace MinecraftClient
/// Location to reach
/// Allow possible but unsafe locations thay may hurt the player: lava, cactus...
/// Allow non-vanilla direct teleport instead of computing path, but may cause invalid moves and/or trigger anti-cheat plugins
+ /// If no valid path can be found, also allow locations within specified distance of destination
+ /// Do not get closer of destination than specified distance
+ /// How long to wait until the path is evaluated (default: 5 seconds)
+ /// When location is unreachable, computation will reach timeout, then optionally fallback to a close location within maxOffset
/// True if a path has been found
- public bool MoveTo(Location location, bool allowUnsafe = false, bool allowDirectTeleport = false)
+ public bool MoveTo(Location location, bool allowUnsafe = false, bool allowDirectTeleport = false, int maxOffset = 0, int minOffset = 0, TimeSpan? timeout=null)
{
lock (locationLock)
{
@@ -1078,7 +1082,7 @@ namespace MinecraftClient
// Calculate path through pathfinding. Path contains a list of 1-block movement that will be divided into steps
if (Movement.GetAvailableMoves(world, this.location, allowUnsafe).Contains(location))
path = new Queue(new[] { location });
- else path = Movement.CalculatePath(world, this.location, location, allowUnsafe);
+ else path = Movement.CalculatePath(world, this.location, location, allowUnsafe, maxOffset, minOffset, timeout ?? TimeSpan.FromSeconds(5));
return path != null;
}
}
@@ -1847,6 +1851,15 @@ namespace MinecraftClient
DispatchBotEvent(bot => bot.OnRespawn());
}
+ ///
+ /// Check if the client is currently processing a Movement.
+ ///
+ /// true if a movement is currently handled
+ public bool ClientIsMoving()
+ {
+ return terrainAndMovementsEnabled && locationReceived && ((steps != null && steps.Count > 0) || (path != null && path.Count > 0));
+ }
+
///
/// Called when the server sends a new player location,
/// or if a ChatBot whishes to update the player's location.