Compare commits

...

3 Commits

@ -16,8 +16,6 @@ import net.runelite.client.ui.overlay.OverlayPosition
import net.runelite.client.ui.overlay.OverlayPriority
import net.runelite.client.ui.overlay.OverlayUtil
import net.runelite.api.coords.LocalPoint
import net.runelite.api.CollisionDataFlag
import net.runelite.api.Tile
import kotlin.math.min
@ -30,291 +28,6 @@ import kotlin.math.min
const val SCENE_WIDTH = 128
const val SCENE_BUF_LEN = SCENE_WIDTH * SCENE_WIDTH
typealias ScenePoint = Point
typealias CollisionFlags = Int
fun buildPath(
backEdges: Map<ScenePoint, ScenePoint>,
end: ScenePoint
): List<ScenePoint> {
val list = mutableListOf<ScenePoint>()
var current: ScenePoint? = end
while (current != null) {
list.add(current)
current = backEdges.get(current)
}
list.reverse()
return list
}
fun findPath(
from: ScenePoint,
to: ScenePoint,
collisionFlags: Array<IntArray>,
tiles: Array<Array<Tile?>>,
): List<ScenePoint>? {
// A basic breadth-first search, taking into account OSRS movement
// rules
val seen = mutableSetOf<ScenePoint>(from)
val queue = ArrayDeque<ScenePoint>()
queue.add(from)
val backEdges = mutableMapOf<ScenePoint, ScenePoint>()
while (!queue.isEmpty()) {
val current = queue.removeFirst()
if (current == to) {
return buildPath(backEdges, current)
}
val unvisitedNeighbors = neighbors(
current,
{ sp ->
if (
(sp.getX() < 0)
|| (sp.getY() < 0)
|| (sp.getX() >= SCENE_WIDTH)
|| (sp.getY() >= SCENE_WIDTH)
) {
return@neighbors null
}
if (tiles[sp.getX()][sp.getY()] == null) {
return@neighbors null
}
return@neighbors collisionFlags[sp.getX()][sp.getY()]
},
).filter({ n -> !seen.contains(n) })
for (n in unvisitedNeighbors) {
backEdges.put(n, current)
queue.add(n)
seen.add(n)
}
}
return null
}
fun sceneX(t: Tile) = t.sceneLocation.x
fun sceneY(t: Tile) = t.sceneLocation.y
fun sceneCoords(t: Tile) = ScenePoint(t.sceneLocation.getX(), t.sceneLocation.getY())
fun canMoveCardinal(
from: CollisionFlags,
to: CollisionFlags,
direction: Direction,
): Boolean {
if ((to and (
CollisionDataFlag.BLOCK_MOVEMENT_FULL
or CollisionDataFlag.BLOCK_MOVEMENT_OBJECT
)) > 0) {
return false
}
if ((from and direction.collisionFlag) > 0) {
return false
}
return true
}
/**
* the adjDir params should be the direction to move from the adjacent
* tile into the diagonal tile. e.g. If you are trying to move south-west
* and adj1 params the southern tile, adjDir1 should be west (because you
* move south, then west)
*/
fun canMoveDiagonal(
from: CollisionFlags,
to: CollisionFlags,
direction: Direction,
adjOk1: Boolean,
adjFlags1: CollisionFlags,
adjDir1: Direction,
adjOk2: Boolean,
adjFlags2: CollisionFlags,
adjDir2: Direction,
): Boolean {
return (
adjOk1
and adjOk2
and canMoveCardinal(adjFlags1, to, adjDir1)
and canMoveCardinal(adjFlags2, to, adjDir2)
and canMoveCardinal(from, to, direction)
)
}
enum class Direction(
val dx: Int,
val dy: Int,
val collisionFlag: CollisionFlags,
) {
WEST(-1, 0, CollisionDataFlag.BLOCK_MOVEMENT_WEST),
EAST(1, 0, CollisionDataFlag.BLOCK_MOVEMENT_EAST),
SOUTH(0, -1, CollisionDataFlag.BLOCK_MOVEMENT_SOUTH),
NORTH(0, 1, CollisionDataFlag.BLOCK_MOVEMENT_NORTH),
SOUTH_WEST(-1, -1, CollisionDataFlag.BLOCK_MOVEMENT_SOUTH_WEST),
SOUTH_EAST(1, -1, CollisionDataFlag.BLOCK_MOVEMENT_SOUTH_EAST),
NORTH_WEST(-1, 1, CollisionDataFlag.BLOCK_MOVEMENT_NORTH_WEST),
NORTH_EAST(1, 1, CollisionDataFlag.BLOCK_MOVEMENT_NORTH_EAST),
}
fun neighbors(
current: ScenePoint,
collisionFlags: (ScenePoint) -> CollisionFlags?,
): Sequence<ScenePoint> {
val spd = { sp: ScenePoint, d: Direction ->
ScenePoint(sp.getX() + d.dx, sp.getY() + d.dy)
}
val cf = { d: Direction -> collisionFlags(spd(current, d)) }
val currentFlags = collisionFlags(current)!!
data class CanMoveCardinal(
val canMove: Boolean,
val flags: CollisionFlags,
)
val checkCardinal = c@{ d: Direction ->
val flags = when (val f = cf(d)) {
null -> return@c null
else -> f
}
return@c CanMoveCardinal(
canMoveCardinal(currentFlags, flags, d),
flags,
)
}
val west = checkCardinal(Direction.WEST)
val east = checkCardinal(Direction.EAST)
val south = checkCardinal(Direction.SOUTH)
val north = checkCardinal(Direction.NORTH)
val checkDiagonal = c@{
d: Direction,
cmc1: CanMoveCardinal?,
cmc1Dir: Direction,
cmc2: CanMoveCardinal?,
cmc2Dir: Direction,
->
if (cmc1 == null) {
return@c false
}
if (cmc2 == null) {
return@c false
}
val diagonalFlags = when (val f = cf(d)) {
null -> return@c false
else -> f
}
return@c canMoveDiagonal(
currentFlags,
diagonalFlags,
d,
cmc1.canMove,
cmc1.flags,
cmc1Dir,
cmc2.canMove,
cmc2.flags,
cmc2Dir,
)
}
val southWest = checkDiagonal(
Direction.SOUTH_WEST,
south, Direction.WEST,
west, Direction.SOUTH,
)
val southEast = checkDiagonal(
Direction.SOUTH_EAST,
south, Direction.EAST,
east, Direction.SOUTH,
)
val northWest = checkDiagonal(
Direction.NORTH_WEST,
north, Direction.WEST,
west, Direction.NORTH,
)
val northEast = checkDiagonal(
Direction.NORTH_EAST,
north, Direction.EAST,
east, Direction.NORTH,
)
val e = { b: Boolean, d: Direction ->
if (b) { spd(current, d) } else { null }
}
return sequenceOf(
e(west?.canMove ?: false, Direction.WEST),
e(east?.canMove ?: false, Direction.EAST),
e(south?.canMove ?: false, Direction.SOUTH),
e(north?.canMove ?: false, Direction.NORTH),
e(southWest, Direction.SOUTH_WEST),
e(southEast, Direction.SOUTH_EAST),
e(northWest, Direction.NORTH_WEST),
e(northEast, Direction.NORTH_EAST),
).filter({ n -> n != null }).map({ n -> n!! })
}
fun checkpointTiles(tiles: List<ScenePoint>): Set<ScenePoint> {
when (tiles.size) {
0 -> return emptySet()
1 -> return setOf(tiles[0])
2 -> {
return setOf(tiles[0], tiles[1])
}
}
val delta = { a: ScenePoint, b: ScenePoint ->
Pair(a.getX() - b.getX(), a.getY() - b.getY())
}
val checkpoints = mutableSetOf<ScenePoint>()
var idx = 0
var currentDelta = Pair(0, 0)
while (idx < tiles.size) {
val a = tiles[idx - 1]
val b = tiles[idx]
val d = delta(a, b)
if (!d.equals(currentDelta)) {
checkpoints.add(a)
currentDelta = d
}
idx += 1
}
return checkpoints
}
fun runningPath(tiles: List<ScenePoint>): List<ScenePoint> {
val list = mutableListOf<ScenePoint>()
var current: Int = 0
while (current < tiles.size) {
list.add(tiles[current])
current += 2
}
if ((tiles.size > 1) && ((tiles.size % 2) == 0)) {
list.add(tiles.last())
}
return list
}
class Overlay
@Inject constructor(
@ -350,16 +63,17 @@ class Overlay
val tiles = client.scene.tiles[z]
val _hoveredTile = client.selectedSceneTile
val hoveredTarget = getHoveredTarget(client)
val hoveredPathTiles = if (
config.highlightHoveredPath()
&& _hoveredTile != null
&& hoveredTarget != null
) {
val hoveredTile = _hoveredTile.sceneLocation
val requestedTiles = hoveredTarget.getRequestedTiles()
findPath(
findShortestPathAll(
playerTrueTile,
hoveredTile,
requestedTiles,
collisionFlags,
tiles,
)

@ -0,0 +1,310 @@
package net.idylls.pathos
import net.runelite.api.Point
import net.runelite.api.CollisionDataFlag
import net.runelite.api.Tile as RLTile
import kotlin.math.abs
typealias ScenePoint = Point
typealias CollisionFlags = Int
fun sceneCoords(t: RLTile) = ScenePoint(t.sceneLocation.getX(), t.sceneLocation.getY())
fun buildPath(
backEdges: Map<ScenePoint, ScenePoint>,
end: ScenePoint
): List<ScenePoint> {
val list = mutableListOf<ScenePoint>()
var current: ScenePoint? = end
while (current != null) {
list.add(current)
current = backEdges.get(current)
}
list.reverse()
return list
}
fun findShortestPathAll(
from: ScenePoint,
to: Set<ScenePoint>,
collisionFlags: Array<IntArray>,
tiles: Array<Array<RLTile?>>,
): List<ScenePoint>? {
return to
.mapNotNull({ to_ -> findPath(from, to_, collisionFlags, tiles) })
.minByOrNull({ p ->
// minimize zig-zaggedness (manhattan distance) when choosing
// between multiple possible minimal paths
p.windowed(2).fold(0, { acc, w ->
val (a, b) = w
acc + abs(a.x - b.x) + abs(a.y - b.y)
}) + p.size
})
}
fun findPath(
from: ScenePoint,
to: ScenePoint,
collisionFlags: Array<IntArray>,
tiles: Array<Array<RLTile?>>,
): List<ScenePoint>? {
// A basic breadth-first search, taking into account OSRS movement
// rules
val seen = mutableSetOf<ScenePoint>(from)
val queue = ArrayDeque<ScenePoint>()
queue.add(from)
val backEdges = mutableMapOf<ScenePoint, ScenePoint>()
while (!queue.isEmpty()) {
val current = queue.removeFirst()
if (current == to) {
return buildPath(backEdges, current)
}
val unvisitedNeighbors = neighbors(
current,
{ sp ->
if (
(sp.getX() < 0)
|| (sp.getY() < 0)
|| (sp.getX() >= SCENE_WIDTH)
|| (sp.getY() >= SCENE_WIDTH)
) {
return@neighbors null
}
if (tiles[sp.getX()][sp.getY()] == null) {
return@neighbors null
}
return@neighbors collisionFlags[sp.getX()][sp.getY()]
},
).filter({ n -> !seen.contains(n) })
for (n in unvisitedNeighbors) {
backEdges.put(n, current)
queue.add(n)
seen.add(n)
}
}
// If there is not a path to the requested tile, try to find the
// nearest pathable tile.
val dist = { sp: ScenePoint ->
val dx = sp.getX() - to.getX()
val dy = sp.getY() - to.getY()
(dx * dx) + (dy * dy)
}
val candidates = ArrayList(backEdges.keys)
candidates.add(from)
val candidateDistances = candidates.map({ sp ->
Pair(sp, dist(sp))
})
val minDistance = candidateDistances.minByOrNull({ c -> c.second })
if (minDistance == null) {
return null
}
val minimalCandidates = candidateDistances.filter({ c ->
c.second == minDistance.second
})
val minPath = minimalCandidates.mapNotNull({ c ->
buildPath(backEdges, c.first)
}).minWithOrNull({ a, b -> a.size - b.size })
return minPath
}
fun sceneX(t: RLTile) = t.sceneLocation.x
fun sceneY(t: RLTile) = t.sceneLocation.y
fun canMoveCardinal(
from: CollisionFlags,
to: CollisionFlags,
direction: Direction,
): Boolean {
if ((to and (
CollisionDataFlag.BLOCK_MOVEMENT_FULL
or CollisionDataFlag.BLOCK_MOVEMENT_OBJECT
)) > 0) {
return false
}
if ((from and direction.collisionFlag) > 0) {
return false
}
return true
}
/**
* the adjDir params should be the direction to move from the adjacent
* tile into the diagonal tile. e.g. If you are trying to move south-west
* and adj1 params the southern tile, adjDir1 should be west (because you
* move south, then west)
*/
fun canMoveDiagonal(
from: CollisionFlags,
to: CollisionFlags,
direction: Direction,
adjOk1: Boolean,
adjFlags1: CollisionFlags,
adjDir1: Direction,
adjOk2: Boolean,
adjFlags2: CollisionFlags,
adjDir2: Direction,
): Boolean {
return (
adjOk1
and adjOk2
and canMoveCardinal(adjFlags1, to, adjDir1)
and canMoveCardinal(adjFlags2, to, adjDir2)
and canMoveCardinal(from, to, direction)
)
}
enum class Direction(
val dx: Int,
val dy: Int,
val collisionFlag: CollisionFlags,
) {
WEST(-1, 0, CollisionDataFlag.BLOCK_MOVEMENT_WEST),
EAST(1, 0, CollisionDataFlag.BLOCK_MOVEMENT_EAST),
SOUTH(0, -1, CollisionDataFlag.BLOCK_MOVEMENT_SOUTH),
NORTH(0, 1, CollisionDataFlag.BLOCK_MOVEMENT_NORTH),
SOUTH_WEST(-1, -1, CollisionDataFlag.BLOCK_MOVEMENT_SOUTH_WEST),
SOUTH_EAST(1, -1, CollisionDataFlag.BLOCK_MOVEMENT_SOUTH_EAST),
NORTH_WEST(-1, 1, CollisionDataFlag.BLOCK_MOVEMENT_NORTH_WEST),
NORTH_EAST(1, 1, CollisionDataFlag.BLOCK_MOVEMENT_NORTH_EAST),
}
fun neighbors(
current: ScenePoint,
collisionFlags: (ScenePoint) -> CollisionFlags?,
): Sequence<ScenePoint> {
val spd = { sp: ScenePoint, d: Direction ->
ScenePoint(sp.getX() + d.dx, sp.getY() + d.dy)
}
val cf = { d: Direction -> collisionFlags(spd(current, d)) }
val currentFlags = collisionFlags(current)!!
data class CanMoveCardinal(
val canMove: Boolean,
val flags: CollisionFlags,
)
val checkCardinal = c@{ d: Direction ->
val flags = when (val f = cf(d)) {
null -> return@c null
else -> f
}
return@c CanMoveCardinal(
canMoveCardinal(currentFlags, flags, d),
flags,
)
}
val west = checkCardinal(Direction.WEST)
val east = checkCardinal(Direction.EAST)
val south = checkCardinal(Direction.SOUTH)
val north = checkCardinal(Direction.NORTH)
val checkDiagonal = c@{
d: Direction,
cmc1: CanMoveCardinal?,
cmc1Dir: Direction,
cmc2: CanMoveCardinal?,
cmc2Dir: Direction,
->
if (cmc1 == null) {
return@c false
}
if (cmc2 == null) {
return@c false
}
val diagonalFlags = when (val f = cf(d)) {
null -> return@c false
else -> f
}
return@c canMoveDiagonal(
currentFlags,
diagonalFlags,
d,
cmc1.canMove,
cmc1.flags,
cmc1Dir,
cmc2.canMove,
cmc2.flags,
cmc2Dir,
)
}
val southWest = checkDiagonal(
Direction.SOUTH_WEST,
south, Direction.WEST,
west, Direction.SOUTH,
)
val southEast = checkDiagonal(
Direction.SOUTH_EAST,
south, Direction.EAST,
east, Direction.SOUTH,
)
val northWest = checkDiagonal(
Direction.NORTH_WEST,
north, Direction.WEST,
west, Direction.NORTH,
)
val northEast = checkDiagonal(
Direction.NORTH_EAST,
north, Direction.EAST,
east, Direction.NORTH,
)
val e = { b: Boolean, d: Direction ->
if (b) { spd(current, d) } else { null }
}
return sequenceOf(
e(west?.canMove ?: false, Direction.WEST),
e(east?.canMove ?: false, Direction.EAST),
e(south?.canMove ?: false, Direction.SOUTH),
e(north?.canMove ?: false, Direction.NORTH),
e(southWest, Direction.SOUTH_WEST),
e(southEast, Direction.SOUTH_EAST),
e(northWest, Direction.NORTH_WEST),
e(northEast, Direction.NORTH_EAST),
).filter({ n -> n != null }).map({ n -> n!! })
}
fun runningPath(tiles: List<ScenePoint>): List<ScenePoint> {
val list = mutableListOf<ScenePoint>()
var current: Int = 0
while (current < tiles.size) {
list.add(tiles[current])
current += 2
}
if ((tiles.size > 1) && ((tiles.size % 2) == 0)) {
list.add(tiles.last())
}
return list
}

@ -0,0 +1,130 @@
package net.idylls.pathos
import net.runelite.api.Client
import net.runelite.api.MenuAction
import net.runelite.api.Point
import net.runelite.api.TileObject
import net.runelite.api.GameObject
import net.runelite.api.Actor as RLActor
import net.runelite.api.Tile as RLTile
sealed class Target {
class Actor(val actor: RLActor) : Target()
class Tile(val tile: RLTile) : Target()
class Object(val obj: TileObject) : Target()
fun getRequestedTiles(): Set<ScenePoint> {
val meleeTiles = { x: Int, y: Int ->
val points = mutableSetOf<ScenePoint>()
if (x > 0) {
points.add(Point(x - 1, y))
}
if (y > 0) {
points.add(Point(x, y - 1))
}
if (x < 128) {
points.add(Point(x + 1, y))
}
if (y < 128) {
points.add(Point(x, y + 1))
}
points
}
return when (this) {
is Tile -> setOf(this.tile.sceneLocation)
is Actor -> {
val lp = this.actor.localLocation
meleeTiles(lp.sceneX, lp.sceneY)
}
is Object -> when (this.obj) {
is GameObject -> {
val min = this.obj.sceneMinLocation
val max = this.obj.sceneMaxLocation
val points = mutableSetOf<ScenePoint>()
for (y in min.y..max.y) {
for (x in min.x..max.x) {
points.add(Point(x, y))
}
}
points
}
else -> {
val lp = this.obj.localLocation
setOf(Point(lp.sceneX, lp.sceneY))
}
}
}
}
}
fun findTileObject(client: Client, x: Int, y: Int, id: Int): TileObject? {
val scene = client.scene
val tiles = scene.tiles
val plane = tiles[client.plane]
if (x < 0 || y < 0 || x > plane.size || y > plane[0].size) {
return null
}
val tile = tiles[client.plane][x][y] ?: return null
val objects = tile.gameObjects
objects.find({ g -> g?.id == id})?.let { return it }
tile.wallObject?.let {
if (it.id == id) {
return it
}
}
tile.decorativeObject?.let {
if (it.id == id) {
return it
}
}
tile.groundObject?.let {
if (it.id == id) {
return it
}
}
return null
}
fun getHoveredTarget(client: Client): Target? {
val entries = client.menuEntries
if (entries.size == 0) {
throw Error("Entries should never be 0")
}
// TODO: Handle menu open
val entry = entries[entries.size - 1]
if (entry.type == MenuAction.CANCEL) {
return null
}
if (entry.type == MenuAction.WALK) {
return client.selectedSceneTile?.let { Target.Tile(it) }
}
entry.actor?.let { return Target.Actor(it) }
val tileObject = findTileObject(
client,
entry.param0,
entry.param1,
entry.identifier,
) ?: return null
return Target.Object(tileObject)
}
Loading…
Cancel
Save