using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace MinecraftClient.Mapping { /// /// Allows moving through a Minecraft world /// public static class Movement { /* ========= PATHFINDING METHODS ========= */ /// /// Handle movements due to gravity /// /// World the player is currently located in /// Location the player is currently at /// Current vertical motion speed /// Updated location after applying gravity public static Location HandleGravity(World world, Location location, ref double motionY) { if (Settings.GravityEnabled) { Location onFoots = new Location(location.X, Math.Floor(location.Y), location.Z); Location belowFoots = Move(location, Direction.Down); if (location.Y > Math.Truncate(location.Y) + 0.0001) { belowFoots = location; belowFoots.Y = Math.Truncate(location.Y); } if (!IsOnGround(world, location) && !IsSwimming(world, location)) { while (!IsOnGround(world, belowFoots) && belowFoots.Y >= 1) belowFoots = Move(belowFoots, Direction.Down); location = Move2Steps(location, belowFoots, ref motionY, true).Dequeue(); } else if (!(world.GetBlock(onFoots).Type.IsSolid())) location = Move2Steps(location, onFoots, ref motionY, true).Dequeue(); } return location; } /// /// Return a list of possible moves for the player /// /// World the player is currently located in /// Location the player is currently at /// Allow possible but unsafe locations /// A list of new locations the player can move to public static IEnumerable GetAvailableMoves(World world, Location location, bool allowUnsafe = false) { List availableMoves = new List(); if (IsOnGround(world, location) || IsSwimming(world, location)) { foreach (Direction dir in Enum.GetValues(typeof(Direction))) if (CanMove(world, location, dir) && (allowUnsafe || IsSafe(world, Move(location, dir)))) availableMoves.Add(Move(location, dir)); } else { foreach (Direction dir in new []{ Direction.East, Direction.West, Direction.North, Direction.South }) if (CanMove(world, location, dir) && IsOnGround(world, Move(location, dir)) && (allowUnsafe || IsSafe(world, Move(location, dir)))) availableMoves.Add(Move(location, dir)); availableMoves.Add(Move(location, Direction.Down)); } return availableMoves; } /// /// Decompose a single move from a block to another into several steps /// /// /// Allows moving by little steps instead or directly moving between blocks, /// which would be rejected by anti-cheat plugins anyway. /// /// Start location /// Destination location /// Current vertical motion speed /// Specify if performing falling steps /// Amount of steps by block /// A list of locations corresponding to the requested steps public static Queue Move2Steps(Location start, Location goal, ref double motionY, bool falling = false, int stepsByBlock = 8) { if (stepsByBlock <= 0) stepsByBlock = 1; if (falling) { //Use MC-Like falling algorithm double Y = start.Y; Queue fallSteps = new Queue(); fallSteps.Enqueue(start); double motionPrev = motionY; motionY -= 0.08D; motionY *= 0.9800000190734863D; Y += motionY; if (Y < goal.Y) return new Queue(new[] { goal }); else return new Queue(new[] { new Location(start.X, Y, start.Z) }); } else { //Regular MCC moving algorithm motionY = 0; //Reset motion speed double totalStepsDouble = start.Distance(goal) * stepsByBlock; int totalSteps = (int)Math.Ceiling(totalStepsDouble); Location step = (goal - start) / totalSteps; if (totalStepsDouble >= 1) { Queue movementSteps = new Queue(); for (int i = 1; i <= totalSteps; i++) movementSteps.Enqueue(start + step * i); return movementSteps; } else return new Queue(new[] { 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 /// 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, int maxOffset, int minOffset, TimeSpan timeout) { 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) { cts.Cancel(); pathfindingTask.Wait(); } return pathfindingTask.Result; } /// /// 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) { 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)) { 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) } } // 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 ========= */ /// /// Check if the specified location is on the ground /// /// World for performing check /// Location to check /// True if the specified location is on the ground public static bool IsOnGround(World world, Location location) { return world.GetBlock(Move(location, Direction.Down)).Type.IsSolid() && (location.Y <= Math.Truncate(location.Y) + 0.0001); } /// /// Check if the specified location implies swimming /// /// World for performing check /// Location to check /// True if the specified location implies swimming public static bool IsSwimming(World world, Location location) { return world.GetBlock(location).Type.IsLiquid(); } /// /// Check if the specified location is safe /// /// World for performing check /// Location to check /// True if the destination location won't directly harm the player public static bool IsSafe(World world, Location location) { return //No block that can harm the player !world.GetBlock(location).Type.CanHarmPlayers() && !world.GetBlock(Move(location, Direction.Up)).Type.CanHarmPlayers() && !world.GetBlock(Move(location, Direction.Down)).Type.CanHarmPlayers() //No fall from a too high place && (world.GetBlock(Move(location, Direction.Down)).Type.IsSolid() || world.GetBlock(Move(location, Direction.Down, 2)).Type.IsSolid() || world.GetBlock(Move(location, Direction.Down, 3)).Type.IsSolid()) //Not an underwater location && !(world.GetBlock(Move(location, Direction.Up)).Type.IsLiquid()); } /* ========= SIMPLE MOVEMENTS ========= */ /// /// Check if the player can move in the specified direction /// /// World the player is currently located in /// Location the player is currently at /// Direction the player is moving to /// True if the player can move in the specified direction public static bool CanMove(World world, Location location, Direction direction) { switch (direction) { case Direction.Down: return !IsOnGround(world, location); case Direction.Up: return (IsOnGround(world, location) || IsSwimming(world, location)) && !world.GetBlock(Move(Move(location, Direction.Up), Direction.Up)).Type.IsSolid(); case Direction.East: case Direction.West: case Direction.South: case Direction.North: return !world.GetBlock(Move(location, direction)).Type.IsSolid() && !world.GetBlock(Move(Move(location, direction), Direction.Up)).Type.IsSolid(); default: throw new ArgumentException("Unknown direction", "direction"); } } /// /// Get an updated location for moving in the specified direction /// /// Current location /// Direction to move to /// Distance, in blocks /// Updated location public static Location Move(Location location, Direction direction, int length = 1) { return location + Move(direction) * length; } /// /// Get a location delta for moving in the specified direction /// /// Direction to move to /// A location delta for moving in that direction public static Location Move(Direction direction) { switch (direction) { case Direction.Down: return new Location(0, -1, 0); case Direction.Up: return new Location(0, 1, 0); case Direction.East: return new Location(1, 0, 0); case Direction.West: return new Location(-1, 0, 0); case Direction.South: return new Location(0, 0, 1); case Direction.North: return new Location(0, 0, -1); default: throw new ArgumentException("Unknown direction", "direction"); } } } }