diff --git a/MinecraftClient/Commands/Move.cs b/MinecraftClient/Commands/Move.cs new file mode 100644 index 00000000..25ab8e16 --- /dev/null +++ b/MinecraftClient/Commands/Move.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using MinecraftClient.Mapping; + +namespace MinecraftClient.Commands +{ + public class Move : Command + { + public override string CMDName { get { return "move"; } } + public override string CMDDesc { get { return "move : walk or start walking."; } } + + public override string Run(McTcpClient handler, string command) + { + if (Settings.TerrainAndMovements) + { + string[] args = getArgs(command); + if (args.Length == 1) + { + string dirStr = getArg(command).Trim().ToLower(); + Direction direction; + switch (dirStr) + { + case "up": direction = Direction.Up; break; + case "down": direction = Direction.Down; break; + case "east": direction = Direction.East; break; + case "west": direction = Direction.West; break; + case "north": direction = Direction.North; break; + case "south": direction = Direction.South; break; + default: return "Unknown direction '" + dirStr + "'."; + } + if (Movement.CanMove(handler.GetWorld(), handler.GetCurrentLocation(), direction)) + { + handler.MoveTo(Movement.Move(handler.GetCurrentLocation(), direction)); + return "Moving " + dirStr + '.'; + } + else return "Cannot move in that direction."; + } + else if (args.Length == 3) + { + try + { + int x = int.Parse(args[0]); + int y = int.Parse(args[1]); + int z = int.Parse(args[2]); + Location goal = new Location(x, y, z); + if (handler.MoveTo(goal)) + return "Walking to " + goal; + return "Failed to compute path to " + goal; + } + catch (FormatException) { return CMDDesc; } + } + else return CMDDesc; + } + else return "Please enable terrainandmovements in config to use this command."; + } + } +} diff --git a/MinecraftClient/Mapping/Direction.cs b/MinecraftClient/Mapping/Direction.cs new file mode 100644 index 00000000..5092e7ff --- /dev/null +++ b/MinecraftClient/Mapping/Direction.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MinecraftClient.Mapping +{ + /// + /// Represents a unit movement in the world + /// + /// + public enum Direction + { + South = 0, + West = 1, + North = 2, + East = 3, + Up = 4, + Down = 5 + } +} diff --git a/MinecraftClient/Mapping/Location.cs b/MinecraftClient/Mapping/Location.cs index e55c5a88..ed683134 100644 --- a/MinecraftClient/Mapping/Location.cs +++ b/MinecraftClient/Mapping/Location.cs @@ -127,6 +127,28 @@ namespace MinecraftClient.Mapping } } + /// + /// Get a squared distance to the specified location + /// + /// Other location for computing distance + /// Distance to the specified location, without using a square root + public double DistanceSquared(Location location) + { + return ((X - location.X) * (X - location.X)) + + ((Y - location.Y) * (Y - location.Y)) + + ((Z - location.Z) * (Z - location.Z)); + } + + /// + /// Get exact distance to the specified location + /// + /// Other location for computing distance + /// Distance to the specified location, with square root so lower performances + public double Distance(Location location) + { + return Math.Sqrt(DistanceSquared(location)); + } + /// /// Compare two locations. Locations are equals if the integer part of their coordinates are equals. /// @@ -156,7 +178,7 @@ namespace MinecraftClient.Mapping /// /// Location representation as ulong - public ulong GetLongRepresentation() + public ulong GetLong() { return ((((ulong)X) & 0x3FFFFFF) << 38) | ((((ulong)Y) & 0xFFF) << 26) | (((ulong)Z) & 0x3FFFFFF); } @@ -166,7 +188,7 @@ namespace MinecraftClient.Mapping /// /// Location represented by the ulong - public static Location FromLongRepresentation(ulong location) + public static Location FromLong(ulong location) { int x = (int)(location >> 38); int y = (int)((location >> 26) & 0xFFF); @@ -186,7 +208,7 @@ namespace MinecraftClient.Mapping /// First location to compare /// Second location to compare /// TRUE if the locations are equals - public static bool operator == (Location loc1, Location loc2) + public static bool operator ==(Location loc1, Location loc2) { if (loc1 == null && loc2 == null) return true; @@ -201,7 +223,7 @@ namespace MinecraftClient.Mapping /// First location to compare /// Second location to compare /// TRUE if the locations are equals - public static bool operator != (Location loc1, Location loc2) + public static bool operator !=(Location loc1, Location loc2) { if (loc1 == null && loc2 == null) return true; @@ -219,7 +241,7 @@ namespace MinecraftClient.Mapping /// First location to sum /// Second location to sum /// Sum of the two locations - public static Location operator + (Location loc1, Location loc2) + public static Location operator +(Location loc1, Location loc2) { return new Location ( @@ -229,6 +251,57 @@ namespace MinecraftClient.Mapping ); } + /// + /// Substract a location to another + /// + /// + /// Thrown if one of the provided location is null + /// + /// First location + /// Location to substract to the first one + /// Sum of the two locations + public static Location operator -(Location loc1, Location loc2) + { + return new Location + ( + loc1.X - loc2.X, + loc1.Y - loc2.Y, + loc1.Z - loc2.Z + ); + } + + /// + /// Multiply a location by a scalar value + /// + /// Location to multiply + /// Scalar value + /// Product of the location and the scalar value + public static Location operator *(Location loc, double val) + { + return new Location + ( + loc.X * val, + loc.Y * val, + loc.Z * val + ); + } + + /// + /// Divide a location by a scalar value + /// + /// Location to divide + /// Scalar value + /// Result of the division + public static Location operator /(Location loc, double val) + { + return new Location + ( + loc.X / val, + loc.Y / val, + loc.Z / val + ); + } + /// /// DO NOT USE. Defined to comply with C# requirements requiring a GetHashCode() when overriding Equals() or == /// diff --git a/MinecraftClient/Mapping/Material.cs b/MinecraftClient/Mapping/Material.cs index c506ef94..005b0b5e 100644 --- a/MinecraftClient/Mapping/Material.cs +++ b/MinecraftClient/Mapping/Material.cs @@ -342,5 +342,24 @@ return false; } } + + /// + /// Check if the provided material is a liquid a player can swim into + /// + /// Material to test + /// True if the material is a liquid + public static bool IsLiquid(this Material m) + { + switch (m) + { + case Material.Water: + case Material.StationaryWater: + case Material.Lava: + case Material.StationaryLava: + return true; + default: + return false; + } + } } } diff --git a/MinecraftClient/Mapping/Movement.cs b/MinecraftClient/Mapping/Movement.cs new file mode 100644 index 00000000..ac963f84 --- /dev/null +++ b/MinecraftClient/Mapping/Movement.cs @@ -0,0 +1,264 @@ +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 + /// Updated location after applying gravity + public static Location HandleGravity(World world, Location location) + { + Location onFoots = new Location(location.X, Math.Floor(location.Y), location.Z); + Location belowFoots = Move(location, Direction.Down); + if (!IsOnGround(world, location) && !IsSwimming(world, location)) + location = Move2Steps(location, belowFoots).Dequeue(); + else if (!(world.GetBlock(onFoots).Type.IsSolid())) + location = Move2Steps(location, onFoots).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 + /// Amount of steps by block + /// A list of locations corresponding to the requested steps + public static Queue Move2Steps(Location start, Location goal, int stepsByBlock = 8) + { + if (stepsByBlock <= 0) + stepsByBlock = 1; + + 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) + { + HashSet ClosedSet = new HashSet(); // The set of locations already evaluated. + HashSet OpenSet = new HashSet(); // 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) + Queue total_path = new Queue(new Location[] { current }); + while (Came_From.ContainsKey(current)) + { + current = Came_From[current]; + total_path.Enqueue(current); + } + return 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) + } + } + + return null; + } + + /* ========= 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(); + } + + /// + /// 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"); + } + } + } +} diff --git a/MinecraftClient/McTcpClient.cs b/MinecraftClient/McTcpClient.cs index 7a6da5f3..a4750e93 100644 --- a/MinecraftClient/McTcpClient.cs +++ b/MinecraftClient/McTcpClient.cs @@ -33,6 +33,8 @@ namespace MinecraftClient private object locationLock = new object(); private World world = new World(); + private Queue steps; + private Queue path; private Location location; private string host; @@ -369,6 +371,23 @@ namespace MinecraftClient UpdateLocation(location, false); } + /// + /// Move to the specified location + /// + /// Location to reach + /// Allow possible but unsafe locations + /// True if a path has been found + public bool MoveTo(Location location, bool allowUnsafe = false) + { + lock (locationLock) + { + if (Movement.GetAvailableMoves(world, this.location, allowUnsafe).Contains(location)) + path = new Queue(new[] { location }); + else path = Movement.CalculatePath(world, this.location, location, allowUnsafe); + return path != null; + } + } + /// /// Received some text from the server /// @@ -453,15 +472,15 @@ namespace MinecraftClient { lock (locationLock) { - Location onFoots = new Location(location.X, Math.Floor(location.Y), location.Z); - Location belowFoots = location + new Location(0, -1, 0); - Block blockOnFoots = world.GetBlock(onFoots); - Block blockBelowFoots = world.GetBlock(belowFoots); - handler.SendLocationUpdate(location, blockBelowFoots.Type.IsSolid()); - if (!blockBelowFoots.Type.IsSolid()) - location = belowFoots; - else if (!blockOnFoots.Type.IsSolid()) - location = onFoots; + for (int i = 0; i < 2; i++) //Needs to run at 20 tps; MCC runs at 10 tps + { + if (steps != null && steps.Count > 0) + location = steps.Dequeue(); + else if (path != null && path.Count > 0) + steps = Movement.Move2Steps(location, path.Dequeue()); + else location = Movement.HandleGravity(world, location); + handler.SendLocationUpdate(location, Movement.IsOnGround(world, location)); + } } } } diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj index dbff60b7..c0a631c9 100644 --- a/MinecraftClient/MinecraftClient.csproj +++ b/MinecraftClient/MinecraftClient.csproj @@ -86,6 +86,7 @@ + @@ -117,7 +118,9 @@ + + diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index 6d395676..5bbd7da5 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -215,7 +215,7 @@ namespace MinecraftClient.Protocol.Handlers break; case 0x23: //Block Change if (Settings.TerrainAndMovements) - handler.GetWorld().SetBlock(Location.FromLongRepresentation(readNextULong(packetData)), new Block((ushort)readNextVarInt(packetData))); + handler.GetWorld().SetBlock(Location.FromLong(readNextULong(packetData)), new Block((ushort)readNextVarInt(packetData))); break; case 0x26: //Map Chunk Bulk if (Settings.TerrainAndMovements) diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index 437cbbab..90b3689d 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -179,7 +179,7 @@ namespace MinecraftClient case "scriptcache": CacheScripts = str2bool(argValue); break; case "showsystemmessages": DisplaySystemMessages = str2bool(argValue); break; case "showxpbarmessages": DisplayXPBarMessages = str2bool(argValue); break; - case "handleterrainandmovements": TerrainAndMovements = str2bool(argValue); break; + case "terrainandmovements": TerrainAndMovements = str2bool(argValue); break; case "botowners": Bots_Owners.Clear(); @@ -409,7 +409,7 @@ namespace MinecraftClient + "chatbotlogfile= #leave empty for no logfile\r\n" + "showsystemmessages=true #system messages for server ops\r\n" + "showxpbarmessages=true #messages displayed above xp bar\r\n" - + "handleterrainandmovements=false #requires quite more ram\r\n" + + "terrainandmovements=false #uses more ram, cpu, bandwidth\r\n" + "accountlist=accounts.txt\r\n" + "serverlist=servers.txt\r\n" + "playerheadicon=true\r\n"