From 38939ac9718ec981fc21d77d75fc4926e6ed5614 Mon Sep 17 00:00:00 2001 From: GeChallengeM <52377234+GeChallengeM@users.noreply.github.com> Date: Sun, 1 Aug 2021 18:15:15 +0200 Subject: [PATCH] Add pathfinding API --- .../src/main/java/net/runelite/api/Tile.java | 8 + .../net/runelite/api/coords/WorldPoint.java | 87 ++++++ .../java/net/runelite/mixins/RSTileMixin.java | 252 ++++++++++++++++++ 3 files changed, 347 insertions(+) diff --git a/runelite-api/src/main/java/net/runelite/api/Tile.java b/runelite-api/src/main/java/net/runelite/api/Tile.java index c9473da415..4dde93cce8 100644 --- a/runelite-api/src/main/java/net/runelite/api/Tile.java +++ b/runelite-api/src/main/java/net/runelite/api/Tile.java @@ -140,6 +140,14 @@ public interface Tile extends Locatable */ boolean hasLineOfSightTo(Tile other); + /** + * Computes and returns the path from this tile to another. + * + * @param other the other tile + * @return the checkpoint tiles of the path, or null if no path found + */ + List pathTo(Tile other); + /** * Get all the ground items for this tile * diff --git a/runelite-api/src/main/java/net/runelite/api/coords/WorldPoint.java b/runelite-api/src/main/java/net/runelite/api/coords/WorldPoint.java index 4a1e9dd8de..f393476c96 100644 --- a/runelite-api/src/main/java/net/runelite/api/coords/WorldPoint.java +++ b/runelite-api/src/main/java/net/runelite/api/coords/WorldPoint.java @@ -34,6 +34,7 @@ import net.runelite.api.Client; import static net.runelite.api.Constants.CHUNK_SIZE; import static net.runelite.api.Constants.REGION_SIZE; import net.runelite.api.Perspective; +import net.runelite.api.Tile; /** * A three-dimensional point representing the coordinate of a Tile. @@ -436,4 +437,90 @@ public class WorldPoint { return position & (REGION_SIZE - 1); } + + /** + * Determine the checkpoint tiles of a server-sided path from this WorldPoint to another WorldPoint. + *

+ * The checkpoint tiles of a path are the "corner tiles" of a path and determine the path completely. + * + * Note that true server-sided pathfinding uses collisiondata of the 128x128 area around this WorldPoint, + * while the client only has access to collisiondata within the 104x104 loaded area. + * This means that the results would differ in case the server's path goes near (or over) the border of the loaded area. + * + * @param client The client to compare in + * @param other The other WorldPoint to compare with + * @return Returns the checkpoint tiles of the path + */ + public List pathTo(Client client, WorldPoint other) + { + if (plane != other.getPlane()) + { + return null; + } + + LocalPoint sourceLp = LocalPoint.fromWorld(client, x, y); + LocalPoint targetLp = LocalPoint.fromWorld(client, other.getX(), other.getY()); + if (sourceLp == null || targetLp == null) + { + return null; + } + + int thisX = sourceLp.getSceneX(); + int thisY = sourceLp.getSceneY(); + int otherX = targetLp.getSceneX(); + int otherY = targetLp.getSceneY(); + + Tile[][][] tiles = client.getScene().getTiles(); + Tile sourceTile = tiles[plane][thisX][thisY]; + + Tile targetTile = tiles[plane][otherX][otherY]; + List checkpointTiles = sourceTile.pathTo(targetTile); + if (checkpointTiles == null) + { + return null; + } + List checkpointWPs = new ArrayList<>(); + for (Tile checkpointTile : checkpointTiles) + { + if (checkpointTile == null) { + break; + } + checkpointWPs.add(checkpointTile.getWorldLocation()); + } + return checkpointWPs; + } + + /** + * Gets the path distance from this point to a WorldPoint. + *

+ * If the other point is unreachable, this method will return {@link Integer#MAX_VALUE}. + * + * @param client + * @param other + * @return Returns the path distance + */ + public int distanceToPath(Client client, WorldPoint other) + { + List checkpointWPs = this.pathTo(client, other); + if (checkpointWPs == null) + { + // No path found + return Integer.MAX_VALUE; + } + + WorldPoint destinationPoint = checkpointWPs.get(checkpointWPs.size() - 1); + if (other.getX() != destinationPoint.getX() || other.getY() != destinationPoint.getY()) + { + // Path found but not to the requested tile + return Integer.MAX_VALUE; + } + WorldPoint Point1 = this; + int distance = 0; + for (WorldPoint Point2 : checkpointWPs) + { + distance += Point1.distanceTo2D(Point2); + Point1 = Point2; + } + return distance; + } } diff --git a/runelite-mixins/src/main/java/net/runelite/mixins/RSTileMixin.java b/runelite-mixins/src/main/java/net/runelite/mixins/RSTileMixin.java index 7bdb8dc79d..8c72217cf6 100644 --- a/runelite-mixins/src/main/java/net/runelite/mixins/RSTileMixin.java +++ b/runelite-mixins/src/main/java/net/runelite/mixins/RSTileMixin.java @@ -236,6 +236,258 @@ public abstract class RSTileMixin implements RSTile return true; } + @Inject + @Override + public List pathTo(Tile other) + { + + int z = this.getPlane(); + if (z != other.getPlane()) + { + return null; + } + + CollisionData[] collisionData = client.getCollisionMaps(); + if (collisionData == null) + { + return null; + } + + int[][] directions = new int[128][128]; + int[][] distances = new int[128][128]; + int[] bufferX = new int[4096]; + int[] bufferY = new int[4096]; + + // Initialise directions and distances + for (int i = 0; i < 128; ++i) + { + for (int j = 0; j < 128; ++j) + { + directions[i][j] = 0; + distances[i][j] = Integer.MAX_VALUE; + } + } + + Point p1 = this.getSceneLocation(); + Point p2 = other.getSceneLocation(); + + int middleX = p1.getX(); + int middleY = p1.getY(); + int currentX = middleX; + int currentY = middleY; + int offsetX = 64; + int offsetY = 64; + // Initialise directions and distances for starting tile + directions[offsetX][offsetY] = 99; + distances[offsetX][offsetY] = 0; + int index1 = 0; + bufferX[0] = currentX; + int index2 = 1; + bufferY[0] = currentY; + int[][] collisionDataFlags = collisionData[z].getFlags(); + + boolean isReachable = false; + + while (index1 != index2) + { + currentX = bufferX[index1]; + currentY = bufferY[index1]; + index1 = index1 + 1 & 4095; + // currentX is for the local coordinate while currentMapX is for the index in the directions and distances arrays + int currentMapX = currentX - middleX + offsetX; + int currentMapY = currentY - middleY + offsetY; + if ((currentX == p2.getX()) && (currentY == p2.getY())) + { + isReachable = true; + break; + } + + int currentDistance = distances[currentMapX][currentMapY] + 1; + if (currentMapX > 0 && directions[currentMapX - 1][currentMapY] == 0 && (collisionDataFlags[currentX - 1][currentY] & 19136776) == 0) + { + // Able to move 1 tile west + bufferX[index2] = currentX - 1; + bufferY[index2] = currentY; + index2 = index2 + 1 & 4095; + directions[currentMapX - 1][currentMapY] = 2; + distances[currentMapX - 1][currentMapY] = currentDistance; + } + + if (currentMapX < 127 && directions[currentMapX + 1][currentMapY] == 0 && (collisionDataFlags[currentX + 1][currentY] & 19136896) == 0) + { + // Able to move 1 tile east + bufferX[index2] = currentX + 1; + bufferY[index2] = currentY; + index2 = index2 + 1 & 4095; + directions[currentMapX + 1][currentMapY] = 8; + distances[currentMapX + 1][currentMapY] = currentDistance; + } + + if (currentMapY > 0 && directions[currentMapX][currentMapY - 1] == 0 && (collisionDataFlags[currentX][currentY - 1] & 19136770) == 0) + { + // Able to move 1 tile south + bufferX[index2] = currentX; + bufferY[index2] = currentY - 1; + index2 = index2 + 1 & 4095; + directions[currentMapX][currentMapY - 1] = 1; + distances[currentMapX][currentMapY - 1] = currentDistance; + } + + if (currentMapY < 127 && directions[currentMapX][currentMapY + 1] == 0 && (collisionDataFlags[currentX][currentY + 1] & 19136800) == 0) + { + // Able to move 1 tile north + bufferX[index2] = currentX; + bufferY[index2] = currentY + 1; + index2 = index2 + 1 & 4095; + directions[currentMapX][currentMapY + 1] = 4; + distances[currentMapX][currentMapY + 1] = currentDistance; + } + + if (currentMapX > 0 && currentMapY > 0 && directions[currentMapX - 1][currentMapY - 1] == 0 && (collisionDataFlags[currentX - 1][currentY - 1] & 19136782) == 0 && (collisionDataFlags[currentX - 1][currentY] & 19136776) == 0 && (collisionDataFlags[currentX][currentY - 1] & 19136770) == 0) + { + // Able to move 1 tile south-west + bufferX[index2] = currentX - 1; + bufferY[index2] = currentY - 1; + index2 = index2 + 1 & 4095; + directions[currentMapX - 1][currentMapY - 1] = 3; + distances[currentMapX - 1][currentMapY - 1] = currentDistance; + } + + if (currentMapX < 127 && currentMapY > 0 && directions[currentMapX + 1][currentMapY - 1] == 0 && (collisionDataFlags[currentX + 1][currentY - 1] & 19136899) == 0 && (collisionDataFlags[currentX + 1][currentY] & 19136896) == 0 && (collisionDataFlags[currentX][currentY - 1] & 19136770) == 0) + { + // Able to move 1 tile north-west + bufferX[index2] = currentX + 1; + bufferY[index2] = currentY - 1; + index2 = index2 + 1 & 4095; + directions[currentMapX + 1][currentMapY - 1] = 9; + distances[currentMapX + 1][currentMapY - 1] = currentDistance; + } + + if (currentMapX > 0 && currentMapY < 127 && directions[currentMapX - 1][currentMapY + 1] == 0 && (collisionDataFlags[currentX - 1][currentY + 1] & 19136824) == 0 && (collisionDataFlags[currentX - 1][currentY] & 19136776) == 0 && (collisionDataFlags[currentX][currentY + 1] & 19136800) == 0) + { + // Able to move 1 tile south-east + bufferX[index2] = currentX - 1; + bufferY[index2] = currentY + 1; + index2 = index2 + 1 & 4095; + directions[currentMapX - 1][currentMapY + 1] = 6; + distances[currentMapX - 1][currentMapY + 1] = currentDistance; + } + + if (currentMapX < 127 && currentMapY < 127 && directions[currentMapX + 1][currentMapY + 1] == 0 && (collisionDataFlags[currentX + 1][currentY + 1] & 19136992) == 0 && (collisionDataFlags[currentX + 1][currentY] & 19136896) == 0 && (collisionDataFlags[currentX][currentY + 1] & 19136800) == 0) + { + // Able to move 1 tile north-east + bufferX[index2] = currentX + 1; + bufferY[index2] = currentY + 1; + index2 = index2 + 1 & 4095; + directions[currentMapX + 1][currentMapY + 1] = 12; + distances[currentMapX + 1][currentMapY + 1] = currentDistance; + } + } + if (!isReachable) + { + // Try find a different reachable tile in the 21x21 area around the target tile, as close as possible to the target tile + int upperboundDistance = Integer.MAX_VALUE; + int pathLength = Integer.MAX_VALUE; + int checkRange = 10; + int approxDestinationX = p2.getX(); + int approxDestinationY = p2.getY(); + for (int i = approxDestinationX - checkRange; i <= checkRange + approxDestinationX; ++i) + { + for (int j = approxDestinationY - checkRange; j <= checkRange + approxDestinationY; ++j) + { + int currentMapX = i - middleX + offsetX; + int currentMapY = j - middleY + offsetY; + if (currentMapX >= 0 && currentMapY >= 0 && currentMapX < 128 && currentMapY < 128 && distances[currentMapX][currentMapY] < 100) + { + int deltaX = 0; + if (i < approxDestinationX) + { + deltaX = approxDestinationX - i; + } + else if (i > approxDestinationX) + { + deltaX = i - (approxDestinationX); + } + + int deltaY = 0; + if (j < approxDestinationY) + { + deltaY = approxDestinationY - j; + } + else if (j > approxDestinationY) + { + deltaY = j - (approxDestinationY); + } + + int distanceSquared = deltaX * deltaX + deltaY * deltaY; + if (distanceSquared < upperboundDistance || distanceSquared == upperboundDistance && distances[currentMapX][currentMapY] < pathLength) + { + upperboundDistance = distanceSquared; + pathLength = distances[currentMapX][currentMapY]; + currentX = i; + currentY = j; + } + } + } + } + if (upperboundDistance == Integer.MAX_VALUE) + { + // No path found + return null; + } + } + + // Getting path from directions and distances + bufferX[0] = currentX; + bufferY[0] = currentY; + int index = 1; + int directionNew; + int directionOld; + for (directionNew = directionOld = directions[currentX - middleX + offsetX][currentY - middleY + offsetY]; p1.getX() != currentX || p1.getY() != currentY; directionNew = directions[currentX - middleX + offsetX][currentY - middleY + offsetY]) + { + if (directionNew != directionOld) + { + // "Corner" of the path --> new checkpoint tile + directionOld = directionNew; + bufferX[index] = currentX; + bufferY[index++] = currentY; + } + + if ((directionNew & 2) != 0) + { + ++currentX; + } + else if ((directionNew & 8) != 0) + { + --currentX; + } + + if ((directionNew & 1) != 0) + { + ++currentY; + } + else if ((directionNew & 4) != 0) + { + --currentY; + } + } + + int checkpointTileNumber = 1; + Tile[][][] tiles = client.getScene().getTiles(); + List checkpointTiles = new ArrayList<>(); + while (index-- > 0) + { + checkpointTiles.add(tiles[this.getPlane()][bufferX[index]][bufferY[index]]); + if (checkpointTileNumber == 25) + { + // Pathfinding only supports up to the 25 first checkpoint tiles + break; + } + checkpointTileNumber++; + } + return checkpointTiles; + } + @Inject @Override public List getGroundItems()