using System; using System.Collections.Generic; using System.Linq; using System.Text; 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) { 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 /// A list of locations, or null if calculation failed public static Queue CalculatePath(World world, Location start, Location goal, bool allowUnsafe = false) { Queue result = null; AutoTimeout.Perform(() => { 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) { 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. // 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; } /* ========= 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"); } } } }