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.