diff --git a/MinecraftClient/Command.cs b/MinecraftClient/Command.cs index b97c4374..62909f62 100644 --- a/MinecraftClient/Command.cs +++ b/MinecraftClient/Command.cs @@ -80,7 +80,7 @@ namespace MinecraftClient /// Argument array or empty array if no arguments public static string[] getArgs(string command) { - string[] args = getArg(command).Split(' '); + string[] args = getArg(command).Split(' ', StringSplitOptions.RemoveEmptyEntries); if (args.Length == 1 && args[0] == "") { return new string[] { }; diff --git a/MinecraftClient/Commands/Move.cs b/MinecraftClient/Commands/Move.cs index c7a2c61c..7fa718b0 100644 --- a/MinecraftClient/Commands/Move.cs +++ b/MinecraftClient/Commands/Move.cs @@ -90,21 +90,27 @@ namespace MinecraftClient.Commands { 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); + Location current = handler.GetCurrentLocation(), currentCenter = new Location(current).ConvertToCenter(); + + double x = args[0].StartsWith('~') ? current.X + (args[0].Length > 1 ? double.Parse(args[0][1..]) : 0) : double.Parse(args[0]); + double y = args[1].StartsWith('~') ? current.Y + (args[1].Length > 1 ? double.Parse(args[1][1..]) : 0) : double.Parse(args[1]); + double z = args[2].StartsWith('~') ? current.Z + (args[2].Length > 1 ? double.Parse(args[2][1..]) : 0) : double.Parse(args[2]); + Location goal = new(x, y, z); ChunkColumn? chunkColumn = handler.GetWorld().GetChunkColumn(goal); if (chunkColumn == null || chunkColumn.FullyLoaded == false) return Translations.Get("cmd.move.chunk_not_loaded"); - Location current = handler.GetCurrentLocation(); - handler.MoveTo(current.ToCenter(), allowDirectTeleport: true); - - if (handler.MoveTo(goal, allowUnsafe: takeRisk)) + if (takeRisk || Movement.PlayerFitsHere(handler.GetWorld(), goal)) + { + if (current.DistanceSquared(goal) <= 1.5) + handler.MoveTo(goal, allowDirectTeleport: true); + else if (!handler.MoveTo(goal, allowUnsafe: takeRisk)) + return takeRisk ? Translations.Get("cmd.move.fail", goal) : Translations.Get("cmd.move.suggestforce", goal); return Translations.Get("cmd.move.walk", goal, current); - else return takeRisk ? Translations.Get("cmd.move.fail", goal) : Translations.Get("cmd.move.suggestforce", goal); + } + else + return Translations.Get("cmd.move.suggestforce", goal); } catch (FormatException) { return GetCmdDescTranslated(); } } diff --git a/MinecraftClient/Mapping/Location.cs b/MinecraftClient/Mapping/Location.cs index c5232330..d7972b43 100644 --- a/MinecraftClient/Mapping/Location.cs +++ b/MinecraftClient/Mapping/Location.cs @@ -47,6 +47,16 @@ namespace MinecraftClient.Mapping Z = z; } + /// + /// Create a new location + /// + public Location(Location loc) + { + X = loc.X; + Y = loc.Y; + Z = loc.Z; + } + /// /// Create a new location /// @@ -67,7 +77,7 @@ namespace MinecraftClient.Mapping /// Round coordinates /// /// itself - public Location ToFloor() + public Location ConvertToFloor() { this.X = Math.Floor(this.X); this.Y = Math.Floor(this.Y); @@ -79,7 +89,7 @@ namespace MinecraftClient.Mapping /// Get the center coordinates /// /// itself - public Location ToCenter() + public Location ConvertToCenter() { this.X = Math.Floor(this.X) + 0.5; this.Z = Math.Floor(this.Z) + 0.5; diff --git a/MinecraftClient/Mapping/Movement.cs b/MinecraftClient/Mapping/Movement.cs index be573c9e..97af6359 100644 --- a/MinecraftClient/Mapping/Movement.cs +++ b/MinecraftClient/Mapping/Movement.cs @@ -168,8 +168,9 @@ namespace MinecraftClient.Mapping throw new ArgumentException("minOffset must be lower or equal to maxOffset", "minOffset"); // Round start coordinates for easier calculation - start.ToFloor(); - goal.ToFloor(); + Location startCenter = new Location(start).ConvertToCenter(); + Location startLower = new Location(start).ConvertToFloor(); + Location goalLower = new Location(goal).ConvertToFloor(); // We always use distance squared so our limits must also be squared. minOffset *= minOffset; @@ -187,8 +188,8 @@ namespace MinecraftClient.Mapping Dictionary gScoreDict = new Dictionary(); // Set start values for variables - openSet.Insert(0, (int)start.DistanceSquared(goal), start); - gScoreDict[start] = 0; + openSet.Insert(0, (int)startLower.DistanceSquared(goalLower), startLower); + gScoreDict[startLower] = 0; BinaryHeap.Node current = null; ///---/// @@ -203,9 +204,9 @@ namespace MinecraftClient.Mapping current = openSet.GetRootLocation(); // Return if goal found and no maxOffset was given OR current node is between minOffset and maxOffset - if ((current.Location == goal && maxOffset <= 0) || (maxOffset > 0 && current.H_score >= minOffset && current.H_score <= maxOffset)) + if ((current.Location == goalLower && maxOffset <= 0) || (maxOffset > 0 && current.H_score >= minOffset && current.H_score <= maxOffset)) { - return ReconstructPath(CameFrom, current.Location); + return ReconstructPath(CameFrom, current.Location, startCenter, goal); } // Discover neighbored blocks @@ -227,14 +228,14 @@ namespace MinecraftClient.Mapping // If this location is not already included in the Binary Heap: save it if (!openSet.ContainsLocation(neighbor)) - openSet.Insert(tentativeGScore, (int)neighbor.DistanceSquared(goal), neighbor); + openSet.Insert(tentativeGScore, (int)neighbor.DistanceSquared(goalLower), neighbor); } } } //// Goal could not be reached. Set the path to the closest location if close enough if (current != null && (maxOffset == int.MaxValue || openSet.MinH_ScoreNode.H_score <= maxOffset)) - return ReconstructPath(CameFrom, openSet.MinH_ScoreNode.Location); + return ReconstructPath(CameFrom, openSet.MinH_ScoreNode.Location, startCenter, goal); else return null; } @@ -245,15 +246,29 @@ namespace MinecraftClient.Mapping /// 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) + private static Queue ReconstructPath(Dictionary Came_From, Location current, Location startCenter, Location end) { // Add 0.5 to walk over the middle of a block and avoid collisions - List total_path = new List(new[] { current + new Location(0.5, 0, 0.5) }); + Location center = new(0.5, 0, 0.5); + + List total_path = new(); + + // Move from the center of the block to the final position + if (current != end && current.DistanceSquared(end) <= 1.5) + total_path.Add(end); + + // Generate intermediate paths + total_path.Add(current + center); while (Came_From.ContainsKey(current)) { current = Came_From[current]; - total_path.Add(current + new Location(0.5, 0, 0.5)); + total_path.Add(current + center); } + + // Move to the center of the block first + if (current != startCenter && current.DistanceSquared(startCenter) <= 1.5) + total_path.Add(startCenter); + total_path.Reverse(); return new Queue(total_path); } diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index a0bb0069..d126f61d 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -1102,7 +1102,7 @@ namespace MinecraftClient /// /// Move to the specified location /// - /// Location to reach + /// 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 @@ -1110,21 +1110,21 @@ namespace MinecraftClient /// 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, int maxOffset = 0, int minOffset = 0, TimeSpan? timeout = null) + public bool MoveTo(Location goal, bool allowUnsafe = false, bool allowDirectTeleport = false, int maxOffset = 0, int minOffset = 0, TimeSpan? timeout = null) { lock (locationLock) { if (allowDirectTeleport) { // 1-step path to the desired location without checking anything - UpdateLocation(location, location); // Update yaw and pitch to look at next step - handler.SendLocationUpdate(location, Movement.IsOnGround(world, location), _yaw, _pitch); + UpdateLocation(goal, goal); // Update yaw and pitch to look at next step + handler.SendLocationUpdate(goal, Movement.IsOnGround(world, goal), _yaw, _pitch); return true; } else { // Calculate path through pathfinding. Path contains a list of 1-block movement that will be divided into steps - path = Movement.CalculatePath(world, this.location, location, allowUnsafe, maxOffset, minOffset, timeout ?? TimeSpan.FromSeconds(5)); + path = Movement.CalculatePath(world, this.location, goal, allowUnsafe, maxOffset, minOffset, timeout ?? TimeSpan.FromSeconds(5)); return path != null; } }