Skip to content

Commit 708815f

Browse files
DaengesORelio
andauthored
Improve pathfinding capabilities (#1999)
* Add `ClientIsMoving()` API to determine if currently walking/falling * Improve `MoveToLocation()` performance and allow approaching location Co-authored-by: ORelio <[email protected]>
1 parent aeca6a8 commit 708815f

File tree

3 files changed

+137
-52
lines changed

3 files changed

+137
-52
lines changed

MinecraftClient/ChatBot.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -983,13 +983,26 @@ protected Mapping.Location GetCurrentLocation()
983983
/// </summary>
984984
/// <param name="location">Location to reach</param>
985985
/// <param name="allowUnsafe">Allow possible but unsafe locations thay may hurt the player: lava, cactus...</param>
986-
/// <param name="allowDirectTeleport">Allow non-vanilla teleport instead of computing path, but may cause invalid moves and/or trigger anti-cheat plugins</param>
986+
/// <param name="allowDirectTeleport">Allow non-vanilla direct teleport instead of computing path, but may cause invalid moves and/or trigger anti-cheat plugins</param>
987+
/// <param name="maxOffset">If no valid path can be found, also allow locations within specified distance of destination</param>
988+
/// <param name="minOffset">Do not get closer of destination than specified distance</param>
989+
/// <param name="timeout">How long to wait before stopping computation (default: 5 seconds)</param>
990+
/// <remarks>When location is unreachable, computation will reach timeout, then optionally fallback to a close location within maxOffset</remarks>
987991
/// <returns>True if a path has been found</returns>
988-
protected bool MoveToLocation(Mapping.Location location, bool allowUnsafe = false, bool allowDirectTeleport = false)
992+
protected bool MoveToLocation(Mapping.Location location, bool allowUnsafe = false, bool allowDirectTeleport = false, int maxOffset = 0, int minOffset = 0, TimeSpan? timeout = null)
989993
{
990-
return Handler.MoveTo(location, allowUnsafe, allowDirectTeleport);
994+
return Handler.MoveTo(location, allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeout);
991995
}
992996

997+
/// <summary>
998+
/// Check if the client is currently processing a Movement.
999+
/// </summary>
1000+
/// <returns>true if a movement is currently handled</returns>
1001+
protected bool ClientIsMoving()
1002+
{
1003+
return Handler.ClientIsMoving();
1004+
}
1005+
9931006
/// <summary>
9941007
/// Look at the specified location
9951008
/// </summary>

MinecraftClient/Mapping/Movement.cs

Lines changed: 106 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4-
using System.Text;
4+
using System.Threading;
5+
using System.Threading.Tasks;
56

67
namespace MinecraftClient.Mapping
78
{
@@ -129,62 +130,120 @@ public static Queue<Location> Move2Steps(Location start, Location goal, ref doub
129130
/// <param name="start">Start location</param>
130131
/// <param name="goal">Destination location</param>
131132
/// <param name="allowUnsafe">Allow possible but unsafe locations</param>
133+
/// <param name="maxOffset">If no valid path can be found, also allow locations within specified distance of destination</param>
134+
/// <param name="minOffset">Do not get closer of destination than specified distance</param>
135+
/// <param name="timeout">How long to wait before stopping computation</param>
136+
/// <remarks>When location is unreachable, computation will reach timeout, then optionally fallback to a close location within maxOffset</remarks>
132137
/// <returns>A list of locations, or null if calculation failed</returns>
133-
public static Queue<Location> CalculatePath(World world, Location start, Location goal, bool allowUnsafe = false)
138+
public static Queue<Location> CalculatePath(World world, Location start, Location goal, bool allowUnsafe, int maxOffset, int minOffset, TimeSpan timeout)
134139
{
135-
Queue<Location> result = null;
140+
CancellationTokenSource cts = new CancellationTokenSource();
141+
Task<Queue<Location>> pathfindingTask = Task.Factory.StartNew(() => Movement.CalculatePath(world, start, goal, allowUnsafe, maxOffset, minOffset, cts.Token));
142+
pathfindingTask.Wait(timeout);
143+
if (!pathfindingTask.IsCompleted)
144+
{
145+
cts.Cancel();
146+
pathfindingTask.Wait();
147+
}
148+
return pathfindingTask.Result;
149+
}
150+
151+
/// <summary>
152+
/// Calculate a path from the start location to the destination location
153+
/// </summary>
154+
/// <remarks>
155+
/// Based on the A* pathfinding algorithm described on Wikipedia
156+
/// </remarks>
157+
/// <see href="https://en.wikipedia.org/wiki/A*_search_algorithm#Pseudocode"/>
158+
/// <param name="start">Start location</param>
159+
/// <param name="goal">Destination location</param>
160+
/// <param name="allowUnsafe">Allow possible but unsafe locations</param>
161+
/// <param name="maxOffset">If no valid path can be found, also allow locations within specified distance of destination</param>
162+
/// <param name="minOffset">Do not get closer of destination than specified distance</param>
163+
/// <param name="ct">Token for stopping computation after a certain time</param>
164+
/// <returns>A list of locations, or null if calculation failed</returns>
165+
public static Queue<Location> CalculatePath(World world, Location start, Location goal, bool allowUnsafe, int maxOffset, int minOffset, CancellationToken ct)
166+
{
167+
168+
if (minOffset > maxOffset)
169+
throw new ArgumentException("minOffset must be lower or equal to maxOffset", "minOffset");
170+
171+
Location current = new Location(); // Location that is currently processed
172+
Location closestGoal = new Location(); // Closest Location to the goal. Used for approaching if goal can not be reached or was not found.
173+
HashSet<Location> ClosedSet = new HashSet<Location>(); // The set of locations already evaluated.
174+
HashSet<Location> OpenSet = new HashSet<Location>(new[] { start }); // The set of tentative nodes to be evaluated, initially containing the start node
175+
Dictionary<Location, Location> Came_From = new Dictionary<Location, Location>(); // The map of navigated nodes.
176+
177+
Dictionary<Location, int> g_score = new Dictionary<Location, int>(); //:= map with default value of Infinity
178+
g_score[start] = 0; // Cost from start along best known path.
179+
// Estimated total cost from start to goal through y.
180+
Dictionary<Location, int> f_score = new Dictionary<Location, int>(); //:= map with default value of Infinity
181+
f_score[start] = (int)start.DistanceSquared(goal); //heuristic_cost_estimate(start, goal)
136182

137-
AutoTimeout.Perform(() =>
183+
while (OpenSet.Count > 0)
138184
{
139-
HashSet<Location> ClosedSet = new HashSet<Location>(); // The set of locations already evaluated.
140-
HashSet<Location> OpenSet = new HashSet<Location>(new[] { start }); // The set of tentative nodes to be evaluated, initially containing the start node
141-
Dictionary<Location, Location> Came_From = new Dictionary<Location, Location>(); // The map of navigated nodes.
185+
current = //the node in OpenSet having the lowest f_score[] value
186+
OpenSet.Select(location => f_score.ContainsKey(location)
187+
? new KeyValuePair<Location, int>(location, f_score[location])
188+
: new KeyValuePair<Location, int>(location, int.MaxValue))
189+
.OrderBy(pair => pair.Value).First().Key;
142190

143-
Dictionary<Location, int> g_score = new Dictionary<Location, int>(); //:= map with default value of Infinity
144-
g_score[start] = 0; // Cost from start along best known path.
145-
// Estimated total cost from start to goal through y.
146-
Dictionary<Location, int> f_score = new Dictionary<Location, int>(); //:= map with default value of Infinity
147-
f_score[start] = (int)start.DistanceSquared(goal); //heuristic_cost_estimate(start, goal)
191+
// Only assert a value if it is of actual use later
192+
if (maxOffset > 0 && ClosedSet.Count > 0)
193+
// Get the block that currently is closest to the goal
194+
closestGoal = ClosedSet.OrderBy(checkedLocation => checkedLocation.DistanceSquared(goal)).First();
148195

149-
while (OpenSet.Count > 0)
196+
// Stop when goal is reached or we are close enough
197+
if (current == goal || (minOffset > 0 && current.DistanceSquared(goal) <= minOffset))
198+
return ReconstructPath(Came_From, current);
199+
else if (ct.IsCancellationRequested)
200+
break; // Return if we are cancelled
201+
202+
OpenSet.Remove(current);
203+
ClosedSet.Add(current);
204+
205+
foreach (Location neighbor in GetAvailableMoves(world, current, allowUnsafe))
150206
{
151-
Location current = //the node in OpenSet having the lowest f_score[] value
152-
OpenSet.Select(location => f_score.ContainsKey(location)
153-
? new KeyValuePair<Location, int>(location, f_score[location])
154-
: new KeyValuePair<Location, int>(location, int.MaxValue))
155-
.OrderBy(pair => pair.Value).First().Key;
156-
if (current == goal)
157-
{ //reconstruct_path(Came_From, goal)
158-
List<Location> total_path = new List<Location>(new[] { current });
159-
while (Came_From.ContainsKey(current))
160-
{
161-
current = Came_From[current];
162-
total_path.Add(current);
163-
}
164-
total_path.Reverse();
165-
result = new Queue<Location>(total_path);
166-
}
167-
OpenSet.Remove(current);
168-
ClosedSet.Add(current);
169-
foreach (Location neighbor in GetAvailableMoves(world, current, allowUnsafe))
170-
{
171-
if (ClosedSet.Contains(neighbor))
172-
continue; // Ignore the neighbor which is already evaluated.
173-
int tentative_g_score = g_score[current] + (int)current.DistanceSquared(neighbor); //dist_between(current,neighbor) // length of this path.
174-
if (!OpenSet.Contains(neighbor)) // Discover a new node
175-
OpenSet.Add(neighbor);
176-
else if (tentative_g_score >= g_score[neighbor])
177-
continue; // This is not a better path.
207+
if (ct.IsCancellationRequested)
208+
break; // Stop searching for blocks if we are cancelled.
209+
if (ClosedSet.Contains(neighbor))
210+
continue; // Ignore the neighbor which is already evaluated.
211+
int tentative_g_score = g_score[current] + (int)current.DistanceSquared(neighbor); //dist_between(current,neighbor) // length of this path.
212+
if (!OpenSet.Contains(neighbor)) // Discover a new node
213+
OpenSet.Add(neighbor);
214+
else if (tentative_g_score >= g_score[neighbor])
215+
continue; // This is not a better path.
178216

179-
// This path is the best until now. Record it!
180-
Came_From[neighbor] = current;
181-
g_score[neighbor] = tentative_g_score;
182-
f_score[neighbor] = g_score[neighbor] + (int)neighbor.DistanceSquared(goal); //heuristic_cost_estimate(neighbor, goal)
183-
}
217+
// This path is the best until now. Record it!
218+
Came_From[neighbor] = current;
219+
g_score[neighbor] = tentative_g_score;
220+
f_score[neighbor] = g_score[neighbor] + (int)neighbor.DistanceSquared(goal); //heuristic_cost_estimate(neighbor, goal)
184221
}
185-
}, TimeSpan.FromSeconds(5));
222+
}
186223

187-
return result;
224+
// Goal could not be reached. Set the path to the closest location if close enough
225+
if (maxOffset == int.MaxValue || goal.DistanceSquared(closestGoal) <= maxOffset)
226+
return ReconstructPath(Came_From, closestGoal);
227+
else
228+
return null;
229+
}
230+
231+
/// <summary>
232+
/// Helper function for CalculatePath(). Backtrack from goal to start to reconstruct a step-by-step path.
233+
/// </summary>
234+
/// <param name="Came_From">The collection of Locations that leads back to the start</param>
235+
/// <param name="current">Endpoint of our later walk</param>
236+
/// <returns>the path that leads to current from the start position</returns>
237+
private static Queue<Location> ReconstructPath(Dictionary<Location, Location> Came_From, Location current)
238+
{
239+
List<Location> total_path = new List<Location>(new[] { current });
240+
while (Came_From.ContainsKey(current))
241+
{
242+
current = Came_From[current];
243+
total_path.Add(current);
244+
}
245+
total_path.Reverse();
246+
return new Queue<Location>(total_path);
188247
}
189248

190249
/* ========= LOCATION PROPERTIES ========= */

MinecraftClient/McClient.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,8 +1061,12 @@ public Dictionary<string, string> GetOnlinePlayersWithUUID()
10611061
/// <param name="location">Location to reach</param>
10621062
/// <param name="allowUnsafe">Allow possible but unsafe locations thay may hurt the player: lava, cactus...</param>
10631063
/// <param name="allowDirectTeleport">Allow non-vanilla direct teleport instead of computing path, but may cause invalid moves and/or trigger anti-cheat plugins</param>
1064+
/// <param name="maxOffset">If no valid path can be found, also allow locations within specified distance of destination</param>
1065+
/// <param name="minOffset">Do not get closer of destination than specified distance</param>
1066+
/// <param name="timeout">How long to wait until the path is evaluated (default: 5 seconds)</param>
1067+
/// <remarks>When location is unreachable, computation will reach timeout, then optionally fallback to a close location within maxOffset</remarks>
10641068
/// <returns>True if a path has been found</returns>
1065-
public bool MoveTo(Location location, bool allowUnsafe = false, bool allowDirectTeleport = false)
1069+
public bool MoveTo(Location location, bool allowUnsafe = false, bool allowDirectTeleport = false, int maxOffset = 0, int minOffset = 0, TimeSpan? timeout=null)
10661070
{
10671071
lock (locationLock)
10681072
{
@@ -1078,7 +1082,7 @@ public bool MoveTo(Location location, bool allowUnsafe = false, bool allowDirect
10781082
// Calculate path through pathfinding. Path contains a list of 1-block movement that will be divided into steps
10791083
if (Movement.GetAvailableMoves(world, this.location, allowUnsafe).Contains(location))
10801084
path = new Queue<Location>(new[] { location });
1081-
else path = Movement.CalculatePath(world, this.location, location, allowUnsafe);
1085+
else path = Movement.CalculatePath(world, this.location, location, allowUnsafe, maxOffset, minOffset, timeout ?? TimeSpan.FromSeconds(5));
10821086
return path != null;
10831087
}
10841088
}
@@ -1847,6 +1851,15 @@ public void OnRespawn()
18471851
DispatchBotEvent(bot => bot.OnRespawn());
18481852
}
18491853

1854+
/// <summary>
1855+
/// Check if the client is currently processing a Movement.
1856+
/// </summary>
1857+
/// <returns>true if a movement is currently handled</returns>
1858+
public bool ClientIsMoving()
1859+
{
1860+
return terrainAndMovementsEnabled && locationReceived && ((steps != null && steps.Count > 0) || (path != null && path.Count > 0));
1861+
}
1862+
18501863
/// <summary>
18511864
/// Called when the server sends a new player location,
18521865
/// or if a ChatBot whishes to update the player's location.

0 commit comments

Comments
 (0)