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

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()))
}
}