diff --git a/MinecraftClient/Mapping/CubeFromWorld.cs b/MinecraftClient/Mapping/CubeFromWorld.cs deleted file mode 100644 index 94ee39ee..00000000 --- a/MinecraftClient/Mapping/CubeFromWorld.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace MinecraftClient.Mapping -{ - /// - /// A row of blocks that will be mined - /// - public class Row - { - public readonly List BlocksInRow; - - /// - /// Initialize a row of blocks - /// - /// Enter a list of blocks - public Row(List blocksInRow = null) - { - BlocksInRow = blocksInRow ?? new List(); - } - } - - /// - /// Several rows are summarized in a layer - /// - public class Layer - { - public readonly List RowsInLayer; - - /// - /// Add a new row to this layer - /// - /// enter a row that should be added - /// Index of the last row - public void AddRow(Row givenRow = null) - { - RowsInLayer.Add(givenRow ?? new Row()); - } - - /// - /// Initialize a layer - /// - /// Enter a list of rows - public Layer(List rowInLayer = null) - { - RowsInLayer = rowInLayer ?? new List(); - } - } - - /// - /// Several layers result in a cube - /// - public class Cube - { - public readonly List LayersInCube; - - /// - /// Add a new layer to the cube - /// - /// Enter a layer that should be added - /// Index of the last layer - public void AddLayer(Layer givenLayer = null) - { - LayersInCube.Add(givenLayer ?? new Layer()); - } - - /// - /// Initialize a cube - /// - /// Enter a list of layers - public Cube(List layerInCube = null) - { - LayersInCube = layerInCube ?? new List(); - } - } - - public static class CubeFromWorld - { - /// - /// Creates a cube of blocks out of two coordinates. - /// - /// Start Location - /// Stop Location - /// A cube of blocks consisting of Layers, Rows and single blocks - public static Cube GetBlocksAsCube(World currentWorld, Location startBlock, Location stopBlock, List materialList = null, bool isBlacklist = true) - { - // Initialize cube to mine. - Cube cubeToMine = new Cube(); - - // Get the distance between start and finish as Vector. - Location vectorToStopPosition = stopBlock - startBlock; - - // Initialize Iteration process - int[] iterateX = GetNumbersFromTo(0, Convert.ToInt32(Math.Round(vectorToStopPosition.X))).ToArray(); - int[] iterateY = GetNumbersFromTo(0, Convert.ToInt32(Math.Round(vectorToStopPosition.Y))).ToArray(); - int[] iterateZ = GetNumbersFromTo(0, Convert.ToInt32(Math.Round(vectorToStopPosition.Z))).ToArray(); - - // Iterate through all coordinates relative to the start block. - foreach (int y in iterateY) - { - Layer tempLayer = new Layer(); - foreach (int x in iterateX) - { - Row tempRow = new Row(); - foreach (int z in iterateZ) - { - if (materialList != null && materialList.Count > 0) - { - Location tempLocation = new Location(Math.Round(startBlock.X + x), Math.Round(startBlock.Y + y), Math.Round(startBlock.Z + z)); - Material tempLocationMaterial = currentWorld.GetBlock(tempLocation).Type; - - // XOR - // If blacklist == true and it does not contain the material (false); Add it. - // If blacklist == false (whitelist) and it contains the item (true); Add it. - if (isBlacklist ^ materialList.Contains(tempLocationMaterial)) - { - tempRow.BlocksInRow.Add(tempLocation); - } - } - else - { - tempRow.BlocksInRow.Add(new Location(Math.Round(startBlock.X + x), Math.Round(startBlock.Y + y), Math.Round(startBlock.Z + z))); - } - } - if (tempRow.BlocksInRow.Count > 0) - { - tempLayer.AddRow(tempRow); - } - } - if (tempLayer.RowsInLayer.Count > 0) - { - cubeToMine.AddLayer(tempLayer); - } - } - - return cubeToMine; - } - - /// - /// Get all numbers between from and to. - /// - /// Number to start - /// Number to stop - /// All numbers between the start and stop number, including the stop number - private static List GetNumbersFromTo(int start, int stop) - { - List tempList = new List(); - if (start <= stop) - { - for (int i = start; i <= stop; i++) - { - tempList.Add(i); - } - } - else - { - for (int i = start; i >= stop; i--) - { - tempList.Add(i); - } - } - return tempList; - } - } -} diff --git a/MinecraftClient/Mapping/Direction.cs b/MinecraftClient/Mapping/Direction.cs index 5092e7ff..209d97e2 100644 --- a/MinecraftClient/Mapping/Direction.cs +++ b/MinecraftClient/Mapping/Direction.cs @@ -15,7 +15,13 @@ namespace MinecraftClient.Mapping West = 1, North = 2, East = 3, + Up = 4, - Down = 5 + Down = 5, + + NorthEast = 6, + SouthEast = 7, + SouthWest = 8, + NorthWest = 9, } } diff --git a/MinecraftClient/Mapping/Movement.cs b/MinecraftClient/Mapping/Movement.cs index fc66ca7a..ec63abe0 100644 --- a/MinecraftClient/Mapping/Movement.cs +++ b/MinecraftClient/Mapping/Movement.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -61,7 +60,7 @@ namespace MinecraftClient.Mapping } else { - foreach (Direction dir in new []{ Direction.East, Direction.West, Direction.North, Direction.South }) + 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)); @@ -164,72 +163,77 @@ namespace MinecraftClient.Mapping /// 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) { - + // This is a bad configuration if (minOffset > maxOffset) throw new ArgumentException("minOffset must be lower or equal to maxOffset", "minOffset"); - + + // Round start coordinates for easier calculation + start = new Location(Math.Floor(start.X), Math.Floor(start.Y), Math.Floor(start.Z)); + // We always use distance squared so our limits must also be squared. minOffset *= minOffset; maxOffset *= maxOffset; - 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. + ///---/// + // Prepare variables and datastructures for A* + ///---/// + + // Dictionary that contains the relation between all coordinates and resolves the final path + Dictionary CameFrom = new Dictionary(); + // Create a Binary Heap for all open positions => Allows fast access to Nodes with lowest scores + BinaryHeap openSet = new BinaryHeap(); + // Dictionary to keep track of the G-Score of every location + Dictionary gScoreDict = new Dictionary(); - 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) + // Set start values for variables + openSet.Insert(0, (int)start.DistanceSquared(goal), start); + gScoreDict[start] = 0; + BinaryHeap.Node current = null; - while (OpenSet.Count > 0) + ///---/// + // Start of A* + ///---/// + + // Execute while we have nodes to process and we are not cancelled + while (openSet.Count() > 0 && !ct.IsCancellationRequested) { - 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). - // Sort for h-score (f-score - g-score) to get smallest distance to goal if f-scores are equal - ThenBy(pair => f_score[pair.Key]-g_score[pair.Key]).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(); + // Get the root node of the Binary Heap + // Node with the lowest F-Score or lowest H-Score on tie + current = openSet.GetRootLocation(); - // 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)) + // 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 (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. + return ReconstructPath(CameFrom, current.Location); + } - // 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) + // Discover neighbored blocks + foreach (Location neighbor in GetAvailableMoves(world, current.Location, allowUnsafe)) + { + // If we are cancelled: break + if (ct.IsCancellationRequested) + break; + + // tentative_gScore is the distance from start to the neighbor through current + int tentativeGScore = current.G_score + (int)current.Location.DistanceSquared(neighbor); + + // If the neighbor is not in the gScoreDict OR its current tentativeGScore is lower than the previously saved one: + if (!gScoreDict.ContainsKey(neighbor) || (gScoreDict.ContainsKey(neighbor) && tentativeGScore < gScoreDict[neighbor])) + { + // Save the new relation between the neighbored block and the current one + CameFrom[neighbor] = current.Location; + gScoreDict[neighbor] = tentativeGScore; + + // 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); + } } } - // 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); + //// 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); else return null; } @@ -242,16 +246,196 @@ namespace MinecraftClient.Mapping /// 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 }); + // 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) }); while (Came_From.ContainsKey(current)) { current = Came_From[current]; - total_path.Add(current); + total_path.Add(current + new Location(0.5, 0, 0.5)); } total_path.Reverse(); return new Queue(total_path); } + /// + /// A datastructure to store Locations as Nodes and provide them in sorted and queued order. + /// !!! + /// CAN BE REPLACED WITH PriorityQueue IN .NET-6 + /// https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.priorityqueue-2?view=net-6.0 + /// !!! + /// + public class BinaryHeap + { + /// + /// Represents a location and its attributes + /// + public class Node + { + // Distance to start + public int G_score; + // Distance to Goal + public int H_score; + public int F_score { get { return H_score + G_score; } } + + public Location Location; + + public Node(int g_score, int h_score, Location loc) + { + this.G_score = g_score; + this.H_score = h_score; + Location = loc; + } + } + + // List which contains all nodes in form of a Binary Heap + private List heapList; + // Hashset for quick checks of locations included in the heap + private HashSet locationList; + public Node MinH_ScoreNode; + + public BinaryHeap() + { + heapList = new List(); + locationList = new HashSet(); + MinH_ScoreNode = null; + } + + /// + /// Insert a new location in the heap + /// + /// G-Score of the location + /// H-Score of the location + /// The location + public void Insert(int newG_Score, int newH_Score, Location loc) + { + // Begin at the end of the list + int i = heapList.Count; + + // Temporarily save the node created with the parameters to allow comparisons + Node newNode = new Node(newG_Score, newH_Score, loc); + + // Add new note to the end of the list + heapList.Add(newNode); + locationList.Add(loc); + + // Save node with the smallest H-Score => Distance to goal + if (MinH_ScoreNode == null || newNode.H_score < MinH_ScoreNode.H_score) + MinH_ScoreNode = newNode; + + // There is no need of sorting for one node. + if (i > 0) + { + /// Go up the heap from child to parent and move parent down... + // while we are not looking at the root node AND the new node has better attributes than the parent node ((i - 1) / 2) + while (i > 0 && FirstNodeBetter(newNode /* Current Child */, heapList[(i - 1) / 2] /* Coresponding Parent */)) + { + // Move parent down and replace current child -> New free space is created + heapList[i] = heapList[(i - 1) / 2]; + // Select the next parent to check + i = (i - 1) / 2; + } + + /// Nodes were moved down at position I there is now a free space at the correct position for our new node: + // Insert new node in position + heapList[i] = newNode; + } + } + + /// + /// Obtain the root which represents the node the the best attributes currently + /// + /// node with the best attributes currently + /// + public Node GetRootLocation() + { + // The heap is empty. There is nothing to return. + if (heapList.Count == 0) + { + throw new InvalidOperationException("The heap is empty."); + } + + // Save the root node + Node rootNode = heapList[0]; + locationList.Remove(rootNode.Location); + + // Temporarirly store the last item's value. + Node lastNode = heapList[heapList.Count - 1]; + + // Remove the last value. + heapList.RemoveAt(heapList.Count - 1); + + if (heapList.Count > 0) + { + // Start at the first index. + int currentParentPos = 0; + + /// Go through the heap from root to bottom... + // Continue until the halfway point of the heap. + while (currentParentPos < heapList.Count / 2) + { + // Select the left child of the current parent + int currentChildPos = (2 * currentParentPos) + 1; + + // If the currently selected child is not the last entry of the list AND right child has better attributes + if ((currentChildPos < heapList.Count - 1) && FirstNodeBetter(heapList[currentChildPos + 1], heapList[currentChildPos])) + { + // Select the right child + currentChildPos++; + } + + // If the last item is smaller than both siblings at the + // current height, break. + if (FirstNodeBetter(lastNode, heapList[currentChildPos])) + { + break; + } + + // Move the item at index j up one level. + heapList[currentParentPos] = heapList[currentChildPos]; + // Move index i to the appropriate branch. + currentParentPos = currentChildPos; + } + // Insert the last node into the currently free position + heapList[currentParentPos] = lastNode; + } + + return rootNode; + } + + /// + /// Compares two nodes and evaluates their position to the goal. + /// + /// First node to compare + /// Second node to compare + /// True if the first node has a more promissing position to the goal than the second + private static bool FirstNodeBetter(Node firstNode, Node secondNode) + { + // Is the F_score smaller? + return (firstNode.F_score < secondNode.F_score) || + // If F_score is equal, evaluate the h-score + (firstNode.F_score == secondNode.F_score && firstNode.H_score < secondNode.H_score); + } + + /// + /// Get the size of the heap + /// + /// size of the heap + public int Count() + { + return heapList.Count; + } + + /// + /// Check if the heap contains a node with a certain location + /// + /// Location to check + /// true if a node with the given location is in the heap + public bool ContainsLocation(Location loc) + { + return locationList.Contains(loc); + } + } + /* ========= LOCATION PROPERTIES ========= */ /// @@ -313,22 +497,47 @@ namespace MinecraftClient.Mapping { switch (direction) { + // Move vertical 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(); + + // Move horizontal 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(); + return PlayerFitsHere(world, Move(location, direction)); + + // Move diagonal + case Direction.NorthEast: + return PlayerFitsHere(world, Move(location, Direction.North)) && PlayerFitsHere(world, Move(location, Direction.East)) && PlayerFitsHere(world, Move(location, direction)); + case Direction.SouthEast: + return PlayerFitsHere(world, Move(location, Direction.South)) && PlayerFitsHere(world, Move(location, Direction.East)) && PlayerFitsHere(world, Move(location, direction)); + case Direction.SouthWest: + return PlayerFitsHere(world, Move(location, Direction.South)) && PlayerFitsHere(world, Move(location, Direction.West)) && PlayerFitsHere(world, Move(location, direction)); + case Direction.NorthWest: + return PlayerFitsHere(world, Move(location, Direction.North)) && PlayerFitsHere(world, Move(location, Direction.West)) && PlayerFitsHere(world, Move(location, direction)); + default: throw new ArgumentException("Unknown direction", "direction"); } } + /// + /// Evaluates if a player fits in this location + /// + /// Current world + /// Location to check + /// True if a player is able to stand in this location + public static bool PlayerFitsHere(World world, Location location) + { + return !world.GetBlock(location).Type.IsSolid() + && !world.GetBlock(Move(location, Direction.Up)).Type.IsSolid(); + } + /// /// Get an updated location for moving in the specified direction /// @@ -350,10 +559,13 @@ namespace MinecraftClient.Mapping { switch (direction) { + // Move vertical case Direction.Down: return new Location(0, -1, 0); case Direction.Up: return new Location(0, 1, 0); + + // Move horizontal straight case Direction.East: return new Location(1, 0, 0); case Direction.West: @@ -362,6 +574,17 @@ namespace MinecraftClient.Mapping return new Location(0, 0, 1); case Direction.North: return new Location(0, 0, -1); + + // Move horizontal diagonal + case Direction.NorthEast: + return Move(Direction.North) + Move(Direction.East); + case Direction.SouthEast: + return Move(Direction.South) + Move(Direction.East); + case Direction.SouthWest: + return Move(Direction.South) + Move(Direction.West); + case Direction.NorthWest: + return Move(Direction.North) + Move(Direction.West); + default: throw new ArgumentException("Unknown direction", "direction"); } diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 1aa68340..82276f25 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -60,6 +60,8 @@ namespace MinecraftClient private float playerYaw; private float playerPitch; private double motionY; + public enum MovementType { Sneak, Walk, Sprint} + public int currentMovementSpeed = 4; private int sequenceId; // User for player block synchronization (Aka. digging, placing blocks, etc..) private string host; @@ -344,7 +346,7 @@ namespace MinecraftClient { lock (locationLock) { - for (int i = 0; i < 2; i++) //Needs to run at 20 tps; MCC runs at 10 tps + for (int i = 0; i < currentMovementSpeed; i++) //Needs to run at 20 tps; MCC runs at 10 tps { if (_yaw == null || _pitch == null) { @@ -1125,9 +1127,7 @@ namespace MinecraftClient else { // Calculate path through pathfinding. Path contains a list of 1-block movement that will be divided into steps - if (Movement.GetAvailableMoves(world, this.location, allowUnsafe).Contains(location)) - path = new Queue(new[] { location }); - else path = Movement.CalculatePath(world, this.location, location, allowUnsafe, maxOffset, minOffset, timeout ?? TimeSpan.FromSeconds(5)); + path = Movement.CalculatePath(world, this.location, location, allowUnsafe, maxOffset, minOffset, timeout ?? TimeSpan.FromSeconds(5)); return path != null; } } @@ -1905,6 +1905,49 @@ namespace MinecraftClient return terrainAndMovementsEnabled && locationReceived && ((steps != null && steps.Count > 0) || (path != null && path.Count > 0)); } + /// + /// Get the current goal + /// + /// Current goal of movement. Location.Zero if not set. + public Location GetCurrentMovementGoal() + { + return ClientIsMoving() ? Location.Zero : path.Last(); + } + + /// + /// Cancels the current movement + /// + /// True if there was an active path + public bool CancelMovement() + { + bool success = ClientIsMoving(); + path = null; + return success; + } + + /// + /// Change the amount of sent movement packets per time + /// + /// Set a new walking type + public void SetMovementSpeed(MovementType newSpeed) + { + switch (newSpeed) + { + case MovementType.Sneak: + // https://minecraft.fandom.com/wiki/Sneaking#Effects - Sneaking 1.31m/s + currentMovementSpeed = 2; + break; + case MovementType.Walk: + // https://minecraft.fandom.com/wiki/Walking#Usage - Walking 4.317 m/s + currentMovementSpeed = 4; + break; + case MovementType.Sprint: + // https://minecraft.fandom.com/wiki/Sprinting#Usage - Sprinting 5.612 m/s + currentMovementSpeed = 5; + break; + } + } + /// /// Called when the server sends a new player location, /// or if a ChatBot whishes to update the player's location. diff --git a/MinecraftClient/config/ChatBots/MineCube.cs b/MinecraftClient/config/ChatBots/MineCube.cs index fffb56ec..f2099bdf 100644 --- a/MinecraftClient/config/ChatBots/MineCube.cs +++ b/MinecraftClient/config/ChatBots/MineCube.cs @@ -4,312 +4,490 @@ MCC.LoadBot(new MineCube()); //MCCScript Extensions +//using System.Threading.Tasks; + class MineCube : ChatBot { - public override void Initialize() - { - if (!GetTerrainEnabled()) - { - LogToConsole(Translations.Get("extra.terrainandmovement_required")); - UnloadBot(); - return; - } - RegisterChatBotCommand("mine", "Mine a cube from a to b", "/mine x y z OR /mine x1 y1 z1 x2 y2 z2", EvaluateMineCommand); - RegisterChatBotCommand("mineup", "Walk over a flat cubic platform of blocks and mine everything above you", "/mine x1 y1 z1 x2 y2 z2 (y1 = y2)", EvaluateMineCommand); - LogToConsole("Mining bot created by Daenges."); - } + private CancellationTokenSource cts; + private Task currentMiningTask; + private TimeSpan breakTimeout; + private bool toolHandling; + private int cacheSize; - /// - /// Dig out a 2 Block high cube and let the bot walk through it - /// mining all blocks above it that it can reach. - /// - /// Area that the bot should walk through. (The lower Y coordinate of the 2 high cube.) - public void MineUp(Cube walkingArea) - { - foreach (Layer lay in walkingArea.LayersInCube) - { - foreach (Row r in lay.RowsInLayer) - { - foreach (Location loc in r.BlocksInRow) - { - Location currentLoc = GetCurrentLocation(); + public override void Initialize() + { + if (!GetTerrainEnabled()) + { + LogToConsole(Translations.Get("extra.terrainandmovement_required")); + UnloadBot(); + return; + } - if (MoveToLocation(new Location(loc.X, loc.Y, loc.Z))) - { - while (Math.Round(GetCurrentLocation().Distance(loc)) > 1) - { - Thread.Sleep(200); - } - } - else - { - // This block is not reachable for some reason. - // Keep on going with the next collumn. - LogDebugToConsole("Unable to walk to: " + loc.X.ToString() + " " + (loc.Y).ToString() + " " + loc.Z.ToString()); - continue; - } + currentMiningTask = null; + breakTimeout = TimeSpan.FromSeconds(15); + cacheSize = 10; + toolHandling = true; - for (int height = Convert.ToInt32(Math.Round(currentLoc.Y)) + 2; height < Convert.ToInt32(Math.Round(currentLoc.Y)) + 7; height++) - { - Location mineLocation = new Location(loc.X, height, loc.Z); - Material mineLocationMaterial = GetWorld().GetBlock(mineLocation).Type; + RegisterChatBotCommand("mine", "Mine a cube from a to b", "/mine x y z OR /mine x1 y1 z1 x2 y2 z2", EvaluateMineCommand); + RegisterChatBotCommand("mineup", "Walk over a flat cubic platform of blocks and mine everything above you", "/mine x1 y1 z1 x2 y2 z2 (y1 = y2)", EvaluateMineCommand); + LogToConsole("Mining bot created by Daenges."); + } - // Stop mining process if breaking the next block could endager the bot - // through falling blocks or liquids. - if (IsGravityBlockAbove(mineLocation) || IsSorroundedByLiquid(mineLocation)) { break; } - // Skip this block if it can not be mined. - if (Material2Tool.IsUnbreakable(mineLocationMaterial)) { continue; } + /// + /// Walks in a 2 high area under an area of blocks and mines anything above its head. + /// + /// The current world + /// The start corner of walking + /// The stop corner of walking + /// CancellationToken to stop the task on cancel + public void MineUp(World currentWorld, Location startBlock, Location stopBlock, CancellationToken ct) + { + if (startBlock.Y != stopBlock.Y) + { + LogToConsole("Command FAILED. Both coordinates must be on the same y level."); + } - //DateTime start = DateTime.Now; - // Search this tool in hotbar and select the correct slot - SelectCorrectSlotInHotbar( - // Returns the correct tool for this type - Material2Tool.GetCorrectToolForBlock( - // returns the type of the current block - mineLocationMaterial)); + IEnumerable xLocationRange = GetNumbersFromTo(Convert.ToInt32(Math.Round(startBlock.X)), Convert.ToInt32(Math.Round(stopBlock.X))); + IEnumerable zLocationRange = GetNumbersFromTo(Convert.ToInt32(Math.Round(startBlock.Z)), Convert.ToInt32(Math.Round(stopBlock.Z))); - // Unable to check when breaking is over. - if (DigBlock(mineLocation)) - { - short i = 0; // Maximum wait time of 10 sec. - while (GetWorld().GetBlock(mineLocation).Type != Material.Air && i <= 100) - { - Thread.Sleep(100); - i++; - } - } - else - { - LogDebugToConsole("Unable to break this block: " + mineLocation.ToString()); - } - } - } - } - } - LogToConsole("Finished mining up."); - } + foreach (int currentXLoc in xLocationRange) + { + foreach (int currentZLoc in zLocationRange) + { + Location standLocation = new Location(currentXLoc, startBlock.Y, currentZLoc); - /// - /// Mines out a cube of blocks from top to bottom. - /// - /// The cube that should be mined. - public void Mine(Cube cubeToMine) - { - foreach (Layer lay in cubeToMine.LayersInCube) - { - foreach (Row r in lay.RowsInLayer) - { - foreach (Location loc in r.BlocksInRow) - { - Material locMaterial = GetWorld().GetBlock(loc).Type; - if (!Material2Tool.IsUnbreakable(locMaterial) && !IsSorroundedByLiquid(loc)) - { - if (GetHeadLocation(GetCurrentLocation()).Distance(loc) > 5) - { - // Unable to detect when walking is over and goal is reached. - if (MoveToLocation(new Location(loc.X, loc.Y + 1, loc.Z))) - { - while (GetCurrentLocation().Distance(loc) > 2) - { - Thread.Sleep(200); - } + // Walk to the new location. + waitForMoveToLocation(standLocation, maxOffset: 1); - } - else - { - LogDebugToConsole("Unable to walk to: " + loc.X.ToString() + " " + (loc.Y + 1).ToString() + " " + loc.Z.ToString()); - } - } + for (int height = Convert.ToInt32(startBlock.Y) + 2; height < Convert.ToInt32(startBlock.Y) + 7; height++) + { + if (ct.IsCancellationRequested) + { + currentMiningTask = null; + LogToConsole("Cancellation requested. STOP MINING."); + return; + } - //DateTime start = DateTime.Now; - // Search this tool in hotbar and select the correct slot - SelectCorrectSlotInHotbar( - // Returns the correct tool for this type - Material2Tool.GetCorrectToolForBlock( - // returns the type of the current block - GetWorld().GetBlock(loc).Type)); + Location mineLocation = new Location(currentXLoc, height, currentZLoc); + Material mineLocationMaterial = currentWorld.GetBlock(mineLocation).Type; - // Unable to check when breaking is over. - if (DigBlock(loc)) - { - short i = 0; // Maximum wait time of 10 sec. - while (GetWorld().GetBlock(loc).Type != Material.Air && i <= 100) - { - Thread.Sleep(100); - i++; - } - } - else - { - LogDebugToConsole("Unable to break this block: " + loc.ToString()); - } - } - } - } - } - LogToConsole("Mining finished."); - } + // Stop mining process if breaking the next block could endager the bot + // through falling blocks or liquids. + if (!IsGravitySave(currentWorld, mineLocation) || IsSorroundedByLiquid(currentWorld, mineLocation)) { break; } + // Skip this block if it can not be mined. + if (Material2Tool.IsUnbreakable(mineLocationMaterial)) { continue; } - public Func GetHeadLocation = locFeet => new Location(locFeet.X, locFeet.Y + 1, locFeet.Z); + if (Settings.InventoryHandling && toolHandling) + { + // Search this tool in hotbar and select the correct slot + SelectCorrectSlotInHotbar( + // Returns the correct tool for this type + Material2Tool.GetCorrectToolForBlock( + // returns the type of the current block + mineLocationMaterial)); + } - private void SelectCorrectSlotInHotbar(ItemType[] tools) - { - if (GetInventoryEnabled()) - { - foreach (ItemType tool in tools) - { - int[] tempArray = GetPlayerInventory().SearchItem(tool); - // Check whether an item could be found and make sure that it is in - // a hotbar slot (36-44). - if (tempArray.Length > 0 && tempArray[0] > 35) - { - // Changeslot takes numbers from 0-8 - ChangeSlot(Convert.ToInt16(tempArray[0] - 36)); - break; - } - } - } - else - { - LogToConsole("Activate Inventory Handling."); - } - } + // If we are able to reach the block && break sucessfully sent + if (GetCurrentLocation().EyesLocation().DistanceSquared(mineLocation) <= 25 && DigBlock(mineLocation)) + { + AutoTimeout.Perform(() => + { + while (GetWorld().GetBlock(mineLocation).Type != Material.Air) + { + Thread.Sleep(100); - public bool IsGravityBlockAbove(Location block) - { - World world = GetWorld(); - double blockX = Math.Round(block.X); - double blockY = Math.Round(block.Y); - double blockZ = Math.Round(block.Z); + if (ct.IsCancellationRequested) + break; + } + }, breakTimeout); + } + else + { + LogDebugToConsole("Unable to break this block: " + mineLocation.ToString()); + } + } + } + } + LogToConsole("Finished mining up."); + } - List gravityBlockList = new List(new Material[] { Material.Gravel, Material.Sand, Material.RedSand, Material.Scaffolding, Material.Anvil, }); + /// + /// Mine a cube of blocks from top to bottom between start and stop location + /// + /// The current world + /// The upper corner of the cube to mine + /// The lower corner of the cube to mine + /// CancellationToken to stop the task on cancel + public void Mine(World currentWorld, Location startBlock, Location stopBlock, CancellationToken ct) + { + // Turn the cube around, so the bot always starts from the top. + if (stopBlock.Y > startBlock.Y) + { + Location temp = stopBlock; + stopBlock = startBlock; + startBlock = temp; + } + IEnumerable xLocationRange = GetNumbersFromTo(Convert.ToInt32(Math.Round(startBlock.X)), Convert.ToInt32(Math.Round(stopBlock.X))); + IEnumerable yLocationRange = GetNumbersFromTo(Convert.ToInt32(Math.Round(startBlock.Y)), Convert.ToInt32(Math.Round(stopBlock.Y))); + IEnumerable zLocationRange = GetNumbersFromTo(Convert.ToInt32(Math.Round(startBlock.Z)), Convert.ToInt32(Math.Round(stopBlock.Z))); - var temptype = world.GetBlock(new Location(blockX, blockY + 1, blockZ)).Type; - var tempLoc = gravityBlockList.Contains(world.GetBlock(new Location(blockX, blockY + 1, blockZ)).Type); + foreach (int currentYLoc in yLocationRange) + { + foreach (int currentXLoc in xLocationRange) + { - return - // Block can not fall down on player e.g. Sand, Gravel etc. - gravityBlockList.Contains(world.GetBlock(new Location(blockX, blockY + 1, blockZ)).Type); - } + if (ct.IsCancellationRequested) + { + currentMiningTask = null; + LogToConsole("Cancellation requested. STOP MINING."); + return; + } - public bool IsSorroundedByLiquid(Location block) - { - World world = GetWorld(); - double blockX = Math.Round(block.X); - double blockY = Math.Round(block.Y); - double blockZ = Math.Round(block.Z); + List blocksToMine = null; - List liquidBlockList = new List(new Material[] { Material.Water, Material.Lava, }); + // If the end of the new row is closer than the start, reverse the line and start here + Location currentStandingLoc = GetCurrentLocation(); + Queue currentZLocationRangeQueue = new Queue(currentStandingLoc.DistanceSquared(new Location(currentXLoc, currentYLoc, zLocationRange.Last())) < currentStandingLoc.DistanceSquared(new Location(currentXLoc, currentYLoc, zLocationRange.First())) ? + zLocationRange.Reverse() : + zLocationRange); - return // Liquid can not flow down the hole. Liquid is unable to flow diagonally. - liquidBlockList.Contains(world.GetBlock(new Location(blockX, blockY + 1, blockZ)).Type) || - liquidBlockList.Contains(world.GetBlock(new Location(blockX - 1, blockY, blockZ)).Type) || - liquidBlockList.Contains(world.GetBlock(new Location(blockX + 1, blockY, blockZ)).Type) || - liquidBlockList.Contains(world.GetBlock(new Location(blockX, blockY, blockZ - 1)).Type) || - liquidBlockList.Contains(world.GetBlock(new Location(blockX, blockY, blockZ + 1)).Type); - } + while (!ct.IsCancellationRequested && (currentZLocationRangeQueue.Count > 0 || blocksToMine.Count > 0)) + { + // Evaluate the next blocks to mine, while mining + Task> cacheEval = Task>.Factory.StartNew(() => // Get a new chunk of blocks that can be mined + EvaluateBlocks(currentWorld, currentXLoc, currentYLoc, currentZLocationRangeQueue, ct, cacheSize)); - /// - private string getHelpPage() - { - return - "Usage of the mine bot:\n" + - "/mine OR /mine \n" + - "to excavate a cube of blocks from top to bottom. (2 high area above the cube must be dug free by hand.)\n" + - "/mineup OR /mineup \n" + - "to walk over a quadratic field of blocks and simultaniously mine everything above the head. \n" + - "(Mines up to 5 Blocks, stops if gravel or lava would fall. 2 High area below this must be dug fee by hand.)\n"; - } + // On the first run, we need the task to finish, otherwise we would not have any results + if (blocksToMine != null) + { + // For all blocks in this block chunk + foreach (Location mineLocation in blocksToMine) + { + if (ct.IsCancellationRequested) + break; - /// - /// Evaluates the given command - /// - /// - /// - /// - private string EvaluateMineCommand(string command, string[] args) - { - if (args.Length > 2) - { - Location startBlock; - Location stopBlock; + Location currentLoc = GetCurrentLocation(); + Location currentBlockUnderFeet = new Location(Math.Floor(currentLoc.X), Math.Floor(currentLoc.Y) - 1, Math.Floor(currentLoc.Z)); - if (args.Length > 5) - { - try - { - startBlock = new Location( - double.Parse(args[0]), - double.Parse(args[1]), - double.Parse(args[2]) - ); + // If we are too far away from the mining location + if (currentLoc.EyesLocation().DistanceSquared(mineLocation) > 25) + { + // Walk to the new location + waitForMoveToLocation(mineLocation, maxOffset:3); + } - stopBlock = new Location( - double.Parse(args[3]), - double.Parse(args[4]), - double.Parse(args[5]) - ); + // Prevent falling into danger + if (mineLocation == currentBlockUnderFeet && !Movement.IsSafe(currentWorld, currentBlockUnderFeet)) + waitForMoveToLocation(mineLocation, maxOffset: 4, minOffset:3); - } - catch (Exception e) - { - LogDebugToConsole(e.ToString()); - return "Please enter correct coordinates as numbers.\n" + getHelpPage(); - } - } - else - { - Location tempLoc = GetCurrentLocation(); - startBlock = new Location(Math.Round(tempLoc.X), - Math.Round(tempLoc.Y), - Math.Round(tempLoc.Z)); + // Is inventoryhandling activated? + if (Settings.InventoryHandling && toolHandling) + { + // Search this tool in hotbar and select the correct slot + SelectCorrectSlotInHotbar( + // Returns the correct tool for this type + Material2Tool.GetCorrectToolForBlock( + // returns the type of the current block + currentWorld.GetBlock(mineLocation).Type)); + } - try - { - stopBlock = new Location( - double.Parse(args[0]), - double.Parse(args[1]), - double.Parse(args[2]) - ); - } - catch (Exception e) - { - LogDebugToConsole(e.ToString()); - return "Please enter correct coordinates as numbers.\n" + getHelpPage(); - } - } + // If we are able to reach the block && break sucessfully sent + if (GetCurrentLocation().EyesLocation().DistanceSquared(mineLocation) <= 25 && DigBlock(mineLocation)) + { + // Wait until the block is broken (== Air) + AutoTimeout.Perform(() => + { + while (GetWorld().GetBlock(mineLocation).Type != Material.Air) + { + Thread.Sleep(100); - if (command.Contains("mineup")) - { - if (Math.Round(startBlock.Y) != Math.Round(stopBlock.Y)) - { - return "Both blocks must have the same Y value!\n" + getHelpPage(); - } + if (ct.IsCancellationRequested) + break; + } + }, breakTimeout); + } + else + { + LogDebugToConsole("Unable to break this block: " + mineLocation.ToString()); + } - List materialWhitelist = new List() { Material.Air }; - Thread tempThread = new Thread(() => MineUp(CubeFromWorld.GetBlocksAsCube(GetWorld(), startBlock, stopBlock, materialWhitelist, isBlacklist: false))); - tempThread.Start(); - return "Start mining up."; - } - else - { - // Turn the cube around, so the bot always starts from the top. - if (stopBlock.Y > startBlock.Y) - { - Location temp = stopBlock; - stopBlock = startBlock; - startBlock = temp; - } + } + } - List blacklistedMaterials = new List() { Material.Air, Material.Water, Material.Lava }; - Thread tempThread = new Thread(() => Mine(CubeFromWorld.GetBlocksAsCube(GetWorld(), startBlock, stopBlock, blacklistedMaterials))); - tempThread.Start(); + if (!ct.IsCancellationRequested) + { + // Wait for the block evaluation task to finish (if not already) and save the result + if (!cacheEval.IsCompleted) + { + cacheEval.Wait(); + } + blocksToMine = cacheEval.Result; + } + } + } + } + currentMiningTask = null; + LogToConsole("MINING FINISHED."); + } - return "Start mining cube."; - } - } + /// + /// This function selects a certain amount of minable blocks in a row + /// + /// The current world + /// The current x location of the row + /// The current y location of the row + /// All Z blocks that will be mined + /// CancellationToken to stop the task on cancel + /// Maximum amount of blocks to return + /// + private List EvaluateBlocks(World currentWorld, int xLoc, int yLoc, Queue zLocationQueue, CancellationToken ct, int cacheSize = 10) + { + List blockMiningCache = new List(); + int i = 0; + while (zLocationQueue.Count > 0 && i < cacheSize && !ct.IsCancellationRequested) + { + // Get the block to mine, relative to the startblock of the row + Location mineLocation = new Location(xLoc, yLoc, zLocationQueue.Dequeue()); - return "Invalid command syntax.\n" + getHelpPage(); - } + // Add the current location to the mining cache if it is safe to mine + if (currentWorld.GetBlock(mineLocation).Type != Material.Air && + IsGravitySave(currentWorld, mineLocation) && + !IsSorroundedByLiquid(currentWorld, mineLocation) && + !Material2Tool.IsUnbreakable(currentWorld.GetBlock(mineLocation).Type)) + { + blockMiningCache.Add(mineLocation); + i++; + } + } + return blockMiningCache; + } + + /// + /// Generates a sequence of numbers between a start and a stop number, including both + /// + /// Number to start from + /// Number to end with + /// a sequence of numbers between a start and a stop number, including both + private static IEnumerable GetNumbersFromTo(int start, int stop) + { + return start <= stop ? Enumerable.Range(start, stop - start + 1) : Enumerable.Range(stop, start - stop + 1).Reverse(); + } + + /// + /// Starts walk and waits until the client arrives + /// + /// 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 + /// Do not get closer of destination than specified distance + /// How long to wait before stopping computation (default: 5 seconds) + private void waitForMoveToLocation(Location goal, bool allowUnsafe = false, bool allowDirectTeleport = false, int maxOffset = 0, int minOffset = 0, TimeSpan? timeout = null) + { + if (MoveToLocation(goal, allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeout)) + { + // Wait till the client stops moving + while (ClientIsMoving()) + { + Thread.Sleep(200); + } + } + else + { + LogDebugToConsole("Unable to walk to: " + goal.ToString()); + } + } + + /// + /// Checks all slots of the hotbar for an Item and selects it if found + /// + /// List of items that may be selected, from worst to best + private void SelectCorrectSlotInHotbar(ItemType[] tools) + { + if (GetInventoryEnabled()) + { + foreach (ItemType tool in tools) + { + int[] tempArray = GetPlayerInventory().SearchItem(tool); + // Check whether an item could be found and make sure that it is in + // a hotbar slot (36-44). + if (tempArray.Length > 0 && tempArray[0] > 35) + { + // Changeslot takes numbers from 0-8 + ChangeSlot(Convert.ToInt16(tempArray[0] - 36)); + break; + } + } + } + else + { + LogToConsole("Activate Inventory Handling."); + } + } + + /// + /// Check if mining the current block would update others + /// + /// Current World + /// The block to be checked + /// true if mining the current block would not update others + public bool IsGravitySave(World currentWorld, Location blockToMine) + { + Location currentLoc = GetCurrentLocation(); + Location block = new Location(Math.Round(blockToMine.X), Math.Round(blockToMine.Y), Math.Round(blockToMine.Z)); + List gravityBlockList = new List(new Material[] { Material.Gravel, Material.Sand, Material.RedSand, Material.Scaffolding, Material.Anvil, }); + Func isGravityBlock = (Location blockToCheck) => gravityBlockList.Contains(currentWorld.GetBlock(blockToCheck).Type); + Func isBlockSolid = (Location blockToCheck) => currentWorld.GetBlock(blockToCheck).Type.IsSolid(); + + return + // Block can not fall down on player e.g. Sand, Gravel etc. + !isGravityBlock(Movement.Move(block, Direction.Up)) && + (Movement.Move(currentLoc, Direction.Down) != blockToMine || currentWorld.GetBlock(Movement.Move(currentLoc, Direction.Down, 2)).Type.IsSolid()) && + // Prevent updating flying sand/gravel under player + !isGravityBlock(Movement.Move(block, Direction.Down)) || isBlockSolid(Movement.Move(block, Direction.Down, 2)); + } + + /// + /// Checks if the current block is sorrounded by liquids + /// + /// Current World + /// The block to be checked + /// true if mining the current block results in liquid flow change + public bool IsSorroundedByLiquid(World currentWorld, Location blockToMine) + { + Location block = new Location(Math.Round(blockToMine.X), Math.Round(blockToMine.Y), Math.Round(blockToMine.Z)); + Func isLiquid = (Location blockToCheck) => currentWorld.GetBlock(blockToCheck).Type.IsLiquid(); + + return // Liquid can not flow down the hole. Liquid is unable to flow diagonally. + isLiquid(block) || + isLiquid(Movement.Move(block, Direction.Up)) || + isLiquid(Movement.Move(block, Direction.North)) || + isLiquid(Movement.Move(block, Direction.South)) || + isLiquid(Movement.Move(block, Direction.East)) || + isLiquid(Movement.Move(block, Direction.West)); + } + + /// + /// The Help page for this command. + /// + /// a help page + private string getHelpPage() + { + return + "Usage of the mine bot:\n" + + "/mine OR /mine \n" + + "to excavate a cube of blocks from top to bottom. (There must be a 2 high area of air above the cube you want to mine.)\n" + + "/mineup OR /mineup \n" + + "to walk over a quadratic field of blocks and simultaniously mine everything above the head. \n" + + "(Mines up to 5 Blocks, stops if gravel or lava would fall. There must be a 2 high area of air below the cube you want to mine.)\n" + + "/mine OR /mineup cancel\n" + + "to cancel the current mining process.\n" + + "/mine OR /mineup cachesize\n" + + "to set the current cache size\n" + + "/mine OR /mineup breaktimeout\n" + + "to set the time to wait until a block is broken."; ; + + } + + private string EvaluateMineCommand(string command, string[] args) + { + for (int i = 0; i < args.Length; i++) + { + switch (args[i]) + { + case "breaktimeout": + int temp; + if (int.TryParse(args[i + 1], out temp)) + breakTimeout = TimeSpan.FromMilliseconds(temp); + else return "Please enter a valid number."; + return string.Format("Set the break timout to {0} ms.", breakTimeout); + + case "cachesize": + return int.TryParse(args[i + 1], out cacheSize) ? string.Format("Set cache size to {0} blocks.", cacheSize) : "Please enter a valid number"; + + case "cancel": + cts.Cancel(); + currentMiningTask = null; + return "Cancelled current mining process."; + + case "toolHandling": + toolHandling = !toolHandling; + return string.Format("Tool handling was set to: {0}", toolHandling.ToString()); + } + } + + if (args.Length > 2) + { + Location startBlock; + Location stopBlock; + + if (args.Length > 5) + { + try + { + startBlock = new Location( + double.Parse(args[0]), + double.Parse(args[1]), + double.Parse(args[2]) + ); + + stopBlock = new Location( + double.Parse(args[3]), + double.Parse(args[4]), + double.Parse(args[5]) + ); + + } + catch (Exception e) + { + LogDebugToConsole(e.ToString()); + return "Please enter correct coordinates as numbers.\n" + getHelpPage(); + } + } + else + { + Location tempLoc = GetCurrentLocation(); + startBlock = new Location(Math.Round(tempLoc.X), + Math.Round(tempLoc.Y), + Math.Round(tempLoc.Z)); + + try + { + stopBlock = new Location( + double.Parse(args[0]), + double.Parse(args[1]), + double.Parse(args[2]) + ); + } + catch (Exception e) + { + LogDebugToConsole(e.ToString()); + return "Please enter correct coordinates as numbers.\n" + getHelpPage(); + } + } + + if (currentMiningTask == null) + { + if (command.Contains("mineup")) + { + cts = new CancellationTokenSource(); + + currentMiningTask = Task.Factory.StartNew(() => MineUp(GetWorld(), startBlock, stopBlock, cts.Token)); + return "Start mining up."; + } + else if (command.Contains("mine")) + { + + cts = new CancellationTokenSource(); + + currentMiningTask = Task.Factory.StartNew(() => Mine(GetWorld(), startBlock, stopBlock, cts.Token)); + return "Start mining cube."; + } + } + else return "You are already mining. Cancel it with '/minecancel'"; + } + + return "Invalid command syntax.\n" + getHelpPage(); + } } \ No newline at end of file