RSMod routefinder

This commit is contained in:
dam 2026-05-05 21:25:56 +03:00
parent 0c196aa9e3
commit e85f3a6812
No known key found for this signature in database
GPG key ID: 4AF4E722399663FB
2 changed files with 19 additions and 260 deletions

View file

@ -5,24 +5,16 @@ import core.api.utils.Vector
import core.game.world.map.Location
import core.game.world.map.Point
import core.game.world.map.RegionManager
import org.rsmod.game.pathfinder.LinePathFinder
import org.rsmod.game.pathfinder.PathFinder
import org.rsmod.game.pathfinder.RayCast
import org.rsmod.game.pathfinder.collision.CollisionFlagMap
import org.rsmod.game.pathfinder.reach.ReachStrategy
import kotlin.math.floor
private const val SEARCH_MAP_SIZE = 128
private const val RING_BUFFER_SIZE = 4096
class RsmodPathfinder(
private val maxWaypoints: Int = 25
) : Pathfinder() {
class RsmodPathfinder : Pathfinder() {
private val defaultFinder = ThreadLocal.withInitial {
PathFinder(RegionManager.RSMOD_CLIPPING_FLAGS, SEARCH_MAP_SIZE, RING_BUFFER_SIZE)
}
private val suppliedFinder = ThreadLocal.withInitial { RouteFinderState() }
private val routeFinder = ThreadLocal.withInitial { RouteFinderState() }
override fun find(
start: Location?,
@ -38,6 +30,9 @@ class RsmodPathfinder(
): Path {
val source = requireNotNull(start)
val destination = requireNotNull(dest)
val supplier = clipMaskSupplier ?: ClipMaskSupplier { z, x, y ->
RegionManager.getClippingFlag(z, x, y)
}
val path = Path()
var end = destination
val vector = Vector.betweenLocs(source, destination)
@ -52,16 +47,15 @@ class RsmodPathfinder(
}
}
val shape = routeShape(type, sizeX, sizeY)
val finder = if (clipMaskSupplier == null) {
RegionManager.loadClippingWindow(source, SEARCH_MAP_SIZE)
defaultFinder.get()
} else {
val state = suppliedFinder.get()
state.loadCollisionWindow(source, clipMaskSupplier)
state.finder
val state = routeFinder.get()
state.loadCollisionWindow(source, supplier)
val shape = when {
type != 0 -> type - 1
sizeX != 0 && sizeY != 0 -> 10
else -> -1
}
val route = finder.findPath(
val route = state.finder.findPath(
level = source.z,
srcX = source.x,
srcZ = source.y,
@ -73,8 +67,7 @@ class RsmodPathfinder(
objRot = rotation,
objShape = shape,
moveNear = near,
blockAccessFlags = walkingFlag,
maxWaypoints = maxWaypoints
blockAccessFlags = walkingFlag
)
if (route.failed) {
@ -89,82 +82,11 @@ class RsmodPathfinder(
return path
}
companion object {
@JvmStatic
fun canReach(
srcX: Int,
srcY: Int,
moverSize: Int,
destX: Int,
destY: Int,
destWidth: Int,
destHeight: Int,
rotation: Int,
type: Int,
walkingFlag: Int,
z: Int,
clipMaskSupplier: ClipMaskSupplier?
): Boolean {
val flags = if (clipMaskSupplier == null) {
RegionManager.loadClippingWindow(Location.create(srcX, srcY, z), SEARCH_MAP_SIZE)
RegionManager.RSMOD_CLIPPING_FLAGS
} else {
CollisionFlagMap().also {
loadCollisionWindow(
flags = it,
start = Location.create(srcX, srcY, z),
supplier = clipMaskSupplier
)
}
}
return ReachStrategy.reached(
flags = flags,
level = z,
srcX = srcX,
srcZ = srcY,
destX = destX,
destZ = destY,
destWidth = if (destWidth == 0) 1 else destWidth,
destHeight = if (destHeight == 0) 1 else destHeight,
srcSize = moverSize,
objRot = rotation,
objShape = routeShape(type, destWidth, destHeight),
blockAccessFlags = walkingFlag
)
}
private class RouteFinderState {
val flags = CollisionFlagMap()
val finder = PathFinder(flags, SEARCH_MAP_SIZE, RING_BUFFER_SIZE)
@JvmStatic
fun lineOfSight(
start: Location,
dest: Location,
moverSize: Int,
destWidth: Int,
destHeight: Int
): RayCast {
RegionManager.loadClippingWindow(start, SEARCH_MAP_SIZE)
return LinePathFinder(RegionManager.RSMOD_PROJECTILE_FLAGS).lineOfSight(
level = start.z,
srcX = start.x,
srcZ = start.y,
destX = dest.x,
destZ = dest.y,
srcSize = moverSize,
destWidth = destWidth,
destHeight = destHeight
)
}
private fun routeShape(type: Int, sizeX: Int, sizeY: Int): Int = when {
type >= 0 -> type
sizeX != 0 && sizeY != 0 -> 10
else -> -1
}
private fun loadCollisionWindow(
flags: CollisionFlagMap,
start: Location,
supplier: ClipMaskSupplier
) {
fun loadCollisionWindow(start: Location, supplier: ClipMaskSupplier) {
val baseX = start.x - (SEARCH_MAP_SIZE / 2)
val baseY = start.y - (SEARCH_MAP_SIZE / 2)
for (x in baseX until baseX + SEARCH_MAP_SIZE) {
@ -174,13 +96,4 @@ class RsmodPathfinder(
}
}
}
private class RouteFinderState {
val flags = CollisionFlagMap()
val finder = PathFinder(flags, SEARCH_MAP_SIZE, RING_BUFFER_SIZE)
fun loadCollisionWindow(start: Location, supplier: ClipMaskSupplier) {
loadCollisionWindow(flags, start, supplier)
}
}
}

View file

@ -1,7 +1,6 @@
package core
import TestUtils
import content.global.handlers.scenery.BankBoothListener
import content.global.skill.gather.GatheringSkillOptionListeners
import content.global.skill.gather.woodcutting.WoodcuttingListener
import core.api.log
@ -25,18 +24,9 @@ import core.plugin.ClassScanner
import core.plugin.Plugin
import core.tools.Log
import org.rs09.consts.NPCs
import org.rs09.consts.Scenery as SceneryIds
class PathfinderTests {
companion object {
init {
TestUtils.preTestSetup()
GatheringSkillOptionListeners().defineListeners()
WoodcuttingListener().defineListeners()
BankBoothListener().defineListeners()
}
val NPC_TEST_LOC = ServerConstants.HOME_LOCATION!!.transform(2, 10, 0)
}
companion object {init {TestUtils.preTestSetup(); GatheringSkillOptionListeners().defineListeners(); WoodcuttingListener().defineListeners() }; val NPC_TEST_LOC = ServerConstants.HOME_LOCATION!!.transform(2, 10, 0)}
@Test fun getOccupiedTilesShouldReturnCorrectSetOfTilesThatAnObjectOccupiesAtAllRotations() {
//clay fireplace - 13609 - sizex: 1, sizey: 2
@ -75,150 +65,6 @@ class PathfinderTests {
)
}
@Test fun dumbPathfinderShouldUseRsmodRouting() {
val start = Location.create(3200, 3200, 0)
val dest = Location.create(3202, 3200, 0)
val blockedMiddle = ClipMaskSupplier { _, x, y ->
if (x == 3201 && y == 3200) 0x100 else 0
}
val path = Pathfinder.DUMB.find(start, 1, dest, 0, 0, 0, -1, 0, false, blockedMiddle)
Assertions.assertTrue(path.isSuccessful)
Assertions.assertTrue(path.points.isNotEmpty())
}
@Test fun projectilePathfinderShouldUseRsmodLineOfSightFlags() {
val start = Location.create(3200, 3200, 0)
val dest = Location.create(3202, 3200, 0)
RegionManager.loadClippingWindow(start, 128)
try {
RegionManager.setRsmodFlag(0, 3201, 3200, true, 0x20000)
val blocked = Pathfinder.PROJECTILE.find(start, 1, dest, 0, 0, 0, -1, 0, false, null)
Assertions.assertFalse(blocked.isSuccessful)
} finally {
RegionManager.setRsmodFlag(0, 3201, 3200, true, 0)
}
}
@Test fun metadataSceneryInteractionShouldTriggerWhenAlreadyAtRsmodApproachTile() {
TestUtils.getMockPlayer("bankBoothApproach").use { p ->
val (booth, approach) = findReachableBankBoothFixture()
p.location = approach
val alreadyAtPath = Pathfinder.find(p, booth)
Assertions.assertTrue(alreadyAtPath.isSuccessful)
Assertions.assertFalse(alreadyAtPath.isMoveNear)
Assertions.assertTrue(InteractionListeners.run(booth.id, IntType.SCENERY, "bank", p, booth))
TestUtils.advanceTicks(10, false)
Assertions.assertTrue(p.bank.isOpen)
}
}
@Test fun metadataSceneryCollectShouldTriggerWhenAlreadyAtRsmodApproachTile() {
TestUtils.getMockPlayer("bankBoothCollectApproach").use { p ->
val (booth, approach) = findReachableBankBoothFixture()
p.location = approach
var collected = false
InteractionListeners.addMetadata(
booth.id,
IntType.SCENERY,
arrayOf("collect"),
InteractionListener.InteractionMetadata({ _, _, _ ->
collected = true
true
}, 1, false)
)
try {
Assertions.assertTrue(InteractionListeners.run(booth.id, IntType.SCENERY, "collect", p, booth))
TestUtils.advanceTicks(10, false)
Assertions.assertTrue(collected)
} finally {
BankBoothListener().defineListeners()
}
}
}
@Test fun directObjectMovementPulseShouldTriggerWhenAlreadyAtRsmodApproachTile() {
TestUtils.getMockPlayer("objectPulseApproach").use { p ->
val tree = RegionManager.getObject(0, 2720, 3475, 1307)
?: throw AssertionError("Expected test tree object.")
val approach = findReachableApproachTile(tree)
var pulsed = false
p.location = approach
GameWorld.Pulser.submit(object : MovementPulse(p, tree) {
override fun pulse(): Boolean {
pulsed = true
return true
}
})
TestUtils.advanceTicks(3, false)
Assertions.assertTrue(pulsed)
}
}
@Test fun entityMovementPulseShouldTriggerWhenDestinationOverrideIsAlreadyReached() {
TestUtils.getMockPlayer("bankerOverrideApproach").use { p ->
val npc = NPC.create(0, NPC_TEST_LOC)
npc.isNeverWalks = true
npc.init()
p.location = ServerConstants.HOME_LOCATION
var pulsed = false
GameWorld.Pulser.submit(object : MovementPulse(p, npc, DestinationFlag.ENTITY, { _, _ -> p.location }) {
override fun pulse(): Boolean {
pulsed = true
return true
}
})
TestUtils.advanceTicks(3, false)
Assertions.assertTrue(pulsed)
}
}
private fun findReachableBankBoothFixture(): Pair<Scenery, Location> {
val base = ServerConstants.HOME_LOCATION!!.transform(8, 8, 0)
for (rotation in 0..3) {
val booth = Scenery(SceneryIds.BANK_BOOTH_2213, base, 10, rotation)
runCatching { findReachableApproachTile(booth) }
.getOrNull()
?.let { return booth to it }
}
throw AssertionError("Could not find a reachable synthetic bank booth fixture.")
}
private fun findReachableApproachTile(scenery: Scenery): Location {
for (radius in 1..8) {
for (x in scenery.location.x - radius..scenery.location.x + radius) {
for (y in scenery.location.y - radius..scenery.location.y + radius) {
val start = Location.create(x, y, scenery.location.z)
if (!RegionManager.isTeleportPermitted(start)) {
continue
}
val path = Pathfinder.find(start, scenery)
if (!path.isSuccessful || path.isMoveNear) {
continue
}
val point = path.points.lastOrNull()
val approach = Location.create(point?.x ?: start.x, point?.y ?: start.y, start.z)
val check = Pathfinder.find(approach, scenery)
if (check.isSuccessful && !check.isMoveNear) {
return approach
}
}
}
}
throw AssertionError("Could not find a reachable approach tile for $scenery.")
}
@Test fun movementPulseShouldStopEarlyIfNextToATileOccupiedByTargetObject() {
val start = Location.create(2731, 3481)
val dest = RegionManager.getObject(0, 2720, 3475, 1307)