You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
389 lines
8.6 KiB
Kotlin
389 lines
8.6 KiB
Kotlin
package net.idylls.pathos
|
|
|
|
import java.awt.Graphics2D
|
|
import java.awt.Dimension
|
|
import java.awt.Color
|
|
import java.awt.BasicStroke
|
|
import javax.inject.Inject
|
|
|
|
import net.runelite.api.Client
|
|
import net.runelite.api.Perspective
|
|
import net.runelite.api.Point
|
|
|
|
import net.runelite.client.ui.overlay.Overlay
|
|
import net.runelite.client.ui.overlay.OverlayLayer
|
|
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.abs
|
|
|
|
/**
|
|
* Draws an overlay based on the Old School RuneScape pathfinding
|
|
* algorithm. More information can be found here:
|
|
* https://oldschool.runescape.wiki/w/Pathfinding
|
|
*/
|
|
|
|
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>,
|
|
_current: ScenePoint
|
|
): Sequence<ScenePoint> {
|
|
var current = _current
|
|
|
|
return generateSequence({
|
|
val ret = current
|
|
current = when (val next = backEdges.get(current)) {
|
|
null -> return@generateSequence null
|
|
else -> next
|
|
}
|
|
|
|
return@generateSequence ret
|
|
})
|
|
}
|
|
|
|
fun findPath(
|
|
from: ScenePoint,
|
|
to: ScenePoint,
|
|
collisionFlags: Array<IntArray>,
|
|
tiles: Array<Array<Tile?>>,
|
|
): Sequence<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!! })
|
|
}
|
|
|
|
class Overlay
|
|
@Inject constructor(
|
|
val client: Client,
|
|
val config: PathosConfig,
|
|
): net.runelite.client.ui.overlay.Overlay() {
|
|
init {
|
|
this.position = OverlayPosition.DYNAMIC
|
|
this.layer = OverlayLayer.ABOVE_SCENE
|
|
this.priority = OverlayPriority.MED
|
|
}
|
|
|
|
override public fun render(gfx: Graphics2D): Dimension? {
|
|
val player = client.getLocalPlayer()
|
|
if (player == null) {
|
|
return null
|
|
}
|
|
val z = client.plane
|
|
|
|
val collisions = client.collisionMaps
|
|
if (collisions == null) {
|
|
return null
|
|
}
|
|
|
|
val _playerTrueTile = LocalPoint.fromWorld(client, player.worldLocation)
|
|
if (_playerTrueTile == null) {
|
|
return null
|
|
}
|
|
val playerTrueTile = ScenePoint(_playerTrueTile.sceneX, _playerTrueTile.sceneY)
|
|
|
|
val planeCollisions = collisions[z]
|
|
val collisionFlags = planeCollisions.flags
|
|
val tiles = client.scene.tiles[z]
|
|
|
|
val _hoveredTile = client.selectedSceneTile
|
|
|
|
val hoveredPathTiles = if (
|
|
config.highlightHoveredPath()
|
|
&& _hoveredTile != null
|
|
) {
|
|
val hoveredTile = _hoveredTile.sceneLocation
|
|
renderTile(gfx, hoveredTile, Color(0, 0, 255), 1.0)
|
|
|
|
when (val p = findPath(
|
|
playerTrueTile,
|
|
hoveredTile,
|
|
collisionFlags,
|
|
tiles,
|
|
)) {
|
|
null -> emptySequence<ScenePoint>()
|
|
else -> p
|
|
}
|
|
} else {
|
|
emptySequence<ScenePoint>()
|
|
}
|
|
|
|
val destinationPoint = client.getLocalDestinationLocation()
|
|
val destinationPathTiles = if (
|
|
config.highlightDestinationPath()
|
|
&& destinationPoint != null
|
|
) {
|
|
val destinationTile = ScenePoint(
|
|
destinationPoint.sceneX,
|
|
destinationPoint.sceneY,
|
|
)
|
|
when (val p = findPath(
|
|
playerTrueTile,
|
|
destinationTile,
|
|
collisionFlags,
|
|
tiles,
|
|
)) {
|
|
null -> emptySequence<ScenePoint>()
|
|
else -> p
|
|
}
|
|
} else {
|
|
emptySequence<ScenePoint>()
|
|
}
|
|
|
|
paintTiles(
|
|
gfx,
|
|
hoveredPathTiles,
|
|
config.highlightHoveredBorder(),
|
|
config.highlightHoveredBorderWidth(),
|
|
)
|
|
paintTiles(
|
|
gfx,
|
|
destinationPathTiles,
|
|
config.highlightDestinationBorder(),
|
|
config.highlightDestinationBorderWidth(),
|
|
)
|
|
|
|
return null
|
|
}
|
|
|
|
fun paintTiles(
|
|
gfx: Graphics2D,
|
|
tiles: Sequence<ScenePoint>,
|
|
color: Color,
|
|
borderWidth: Double,
|
|
) {
|
|
for (tile in tiles) {
|
|
renderTile(gfx, tile, color, borderWidth)
|
|
}
|
|
}
|
|
|
|
fun renderTile(
|
|
gfx: Graphics2D,
|
|
tile: ScenePoint,
|
|
borderColor: Color,
|
|
borderWidth: Double,
|
|
) {
|
|
var local = LocalPoint.fromScene(tile.getX(), tile.getY())
|
|
var poly = Perspective.getCanvasTilePoly(client, local)
|
|
|
|
OverlayUtil.renderPolygon(gfx, poly, borderColor, Color(0, 0, 0, 0), BasicStroke(borderWidth.toFloat()))
|
|
}
|
|
}
|