From 9c5a4c88d5aba3a4f97eed1b66855077a88901c9 Mon Sep 17 00:00:00 2001 From: dam <27978131-real_damighty@users.noreply.gitlab.com> Date: Wed, 6 May 2026 21:06:15 +0300 Subject: [PATCH] Use RSMOD LOS in combat movement - Add reusable RSMOD line-of-sight helpers for projectile checks - Avoid hot movement prediction list allocations during combat ticks - Use direct projectile LOS checks for combat movement, swings, and peltables - Reuse loaded LOS windows during ranged and magic approach filtering - Extend pathfinder tests for the direct RSMOD LOS helpers --- .../global/handlers/item/PlayerPeltables.kt | 5 +- .../entity/combat/CombatMovementIntents.kt | 99 ++++----- .../entity/combat/CombatMovementPlanner.kt | 100 +++++++-- .../game/node/entity/combat/CombatPulse.kt | 4 +- .../node/entity/combat/CombatSwingHandler.kt | 26 +-- .../game/world/map/path/RsmodPathfinder.kt | 209 ++++++++++++++++-- .../src/test/kotlin/core/PathfinderTests.kt | 3 + 7 files changed, 328 insertions(+), 118 deletions(-) diff --git a/Server/src/main/content/global/handlers/item/PlayerPeltables.kt b/Server/src/main/content/global/handlers/item/PlayerPeltables.kt index 931a39ef2..648c7e643 100644 --- a/Server/src/main/content/global/handlers/item/PlayerPeltables.kt +++ b/Server/src/main/content/global/handlers/item/PlayerPeltables.kt @@ -7,7 +7,6 @@ import core.game.node.entity.impl.Projectile import core.game.node.entity.player.Player import core.game.node.item.Item import core.game.system.task.Pulse -import core.game.world.map.path.Pathfinder import core.game.world.update.flag.context.Graphics import org.rs09.consts.Items @@ -47,7 +46,7 @@ class PlayerPeltables : InteractionListener { val other = node.asPlayer() - if (!Pathfinder.find(player, other, false, Pathfinder.PROJECTILE).isSuccessful) { + if (!hasLineOfSight(player, other)) { sendDialogue(player, "You can't reach them!") return true } @@ -112,4 +111,4 @@ class PlayerPeltables : InteractionListener { return equipped } -} \ No newline at end of file +} diff --git a/Server/src/main/core/game/node/entity/combat/CombatMovementIntents.kt b/Server/src/main/core/game/node/entity/combat/CombatMovementIntents.kt index 2a8caded8..cc888e6e5 100644 --- a/Server/src/main/core/game/node/entity/combat/CombatMovementIntents.kt +++ b/Server/src/main/core/game/node/entity/combat/CombatMovementIntents.kt @@ -126,18 +126,13 @@ object CombatMovementIntents { if (attacker.locks.isMovementLocked) { return false } - val targetSteps = CombatMovementPlanner.movementStepsThisTick(target) - if (targetSteps == 0) { - return false - } - val predictedTargetLocation = CombatMovementPlanner.predictTargetLocations(target, targetSteps).lastOrNull() - ?: target.location + val predictedTargetLocation = CombatMovementPlanner.predictedMovementLocation(target) ?: return false val borderTiles = CombatMovementPlanner.borderTiles(target, predictedTargetLocation, attacker.size()) val attackTiles = borderTiles.filter { RegionManager.isTeleportPermitted(it) }.ifEmpty { borderTiles } if (attacker.location in attackTiles) { return false } - val projectedAttackerLocation = CombatMovementPlanner.predictTargetLocations(attacker).lastOrNull() + val projectedAttackerLocation = CombatMovementPlanner.predictedMovementLocation(attacker) ?: attacker.location return projectedAttackerLocation !in attackTiles } @@ -185,8 +180,8 @@ object CombatMovementIntents { val candidates = movementDestinationsFor(attacker, target, targetLocation) val standingOnCandidate = candidates.any { it.location == attacker.location } - val projectedAttackerLocation = CombatMovementPlanner.predictTargetLocations(attacker).lastOrNull() - if (projectedAttackerLocation != null && canAttackFrom(attacker, target, projectedAttackerLocation, targetLocation)) { + val projectedAttackerLocation = CombatMovementPlanner.predictedMovementLocation(attacker) ?: attacker.location + if (projectedAttackerLocation != attacker.location && canAttackFrom(attacker, target, projectedAttackerLocation, targetLocation)) { attacker.face(target) projectedLocations[attacker] = projectedAttackerLocation reservedTiles.addAll(occupiedTiles(attacker, projectedAttackerLocation)) @@ -282,10 +277,10 @@ object CombatMovementIntents { if (range <= CombatReach.meleeDistance(attacker)) { return true } - if (CombatMovementPlanner.movementStepsThisTick(target) > 0) { + if (CombatMovementPlanner.hasMovementStepThisTick(target)) { return true } - return attacker.location.getDistance(closestOccupiedTile(target, targetLocation, attacker.location)) > range + return distanceSquaredToClosestOccupiedTile(target, targetLocation, attacker.location) > range * range } private fun playerAttackRangeDestinations( @@ -342,21 +337,21 @@ object CombatMovementIntents { if (!RegionManager.isTeleportPermitted(tile)) { continue } - val closestTargetTile = closestOccupiedTile(target, targetLocation, tile) - if (tile.getDistance(closestTargetTile) <= range) { + if (distanceSquaredToClosestOccupiedTile(target, targetLocation, tile) <= range * range) { tiles.add(tile) } } } tiles.sortWith( - compareBy { it.getDistance(attacker.location) } - .thenBy { it.getDistance(closestOccupiedTile(target, targetLocation, it)) } + compareBy { distanceSquared(it, attacker.location) } + .thenBy { distanceSquaredToClosestOccupiedTile(target, targetLocation, it) } .thenBy { it.x } .thenBy { it.y } ) val attackTiles = ArrayList(minOf(MAX_RANGED_APPROACH_CANDIDATES, tiles.size)) + RsmodPathfinder.loadLineOfSightWindow(targetLocation) for (tile in tiles) { - if (!hasProjectileLineOfSight(tile, attacker.size(), target, targetLocation)) { + if (!hasProjectileLineOfSight(tile, attacker.size(), target, targetLocation, loadWindow = false)) { continue } attackTiles.add(tile) @@ -378,7 +373,7 @@ object CombatMovementIntents { return false } if (distance > 1 && - attackerLocation.getDistance(closestOccupiedTile(target, targetLocation, attackerLocation)) > distance + distanceSquaredToClosestOccupiedTile(target, targetLocation, attackerLocation) > distance * distance ) { return false } @@ -406,7 +401,7 @@ object CombatMovementIntents { if (attacker.properties.combatPulse.style == CombatStyle.MAGIC) 10 else 7 ) } - return attackerLocation.getDistance(closestOccupiedTile(target, targetLocation, attackerLocation)) <= range && + return distanceSquaredToClosestOccupiedTile(target, targetLocation, attackerLocation) <= range * range && hasProjectileLineOfSight(attackerLocation, attacker.size(), target, targetLocation) } @@ -478,42 +473,40 @@ object CombatMovementIntents { attackerSize: Int, target: Entity, targetLocation: Location, - checkClose: Boolean = false + checkClose: Boolean = false, + loadWindow: Boolean = true ): Boolean { - for (sourceX in 0 until attackerSize) { - for (sourceY in 0 until attackerSize) { - val source = attackerLocation.transform(sourceX, sourceY, 0) - for (targetX in 0 until target.size()) { - for (targetY in 0 until target.size()) { - val destination = targetLocation.transform(targetX, targetY, 0) - val path = Pathfinder.PROJECTILE.find( - source, - 1, - destination, - 1, - 1, - 0, - 0, - 0, - false, - RegionManager::getClippingFlag - ) - if (path.isSuccessful && (!checkClose || path.points.size <= 1)) { - return true - } - } - } - } + val maxRaySteps = if (checkClose) 1 else Int.MAX_VALUE + if (!loadWindow) { + return RsmodPathfinder.hasLineOfSightBetweenLoaded( + attackerLocation, + attackerSize, + targetLocation, + target.size(), + maxRaySteps = maxRaySteps + ) } - return false + return RsmodPathfinder.hasLineOfSightBetween( + attackerLocation, + attackerSize, + targetLocation, + target.size(), + maxRaySteps = maxRaySteps + ) } - private fun closestOccupiedTile(entity: Entity, location: Location, from: Location): Location { - return Location.create( - from.x.coerceIn(location.x, location.x + entity.size() - 1), - from.y.coerceIn(location.y, location.y + entity.size() - 1), - location.z - ) + private fun distanceSquared(first: Location, second: Location): Int { + val dx = first.x - second.x + val dy = first.y - second.y + return dx * dx + dy * dy + } + + private fun distanceSquaredToClosestOccupiedTile(entity: Entity, location: Location, from: Location): Int { + val closestX = from.x.coerceIn(location.x, location.x + entity.size() - 1) + val closestY = from.y.coerceIn(location.y, location.y + entity.size() - 1) + val dx = from.x - closestX + val dy = from.y - closestY + return dx * dx + dy * dy } private fun dumbNpcAttackDestinations(attacker: NPC, target: Entity): List { @@ -675,9 +668,7 @@ object CombatMovementIntents { } private fun targetLocationFor(attacker: Entity, target: Entity): Location { - val targetSteps = CombatMovementPlanner.movementStepsThisTick(target) - val predictionSteps = targetSteps.coerceAtMost(movementStepsFor(attacker)) - return CombatMovementPlanner.predictTargetLocations(target, predictionSteps).lastOrNull() ?: target.location + return CombatMovementPlanner.predictedTargetLocation(target, movementStepsFor(attacker)) } private fun movementStepsFor(attacker: Entity): Int { @@ -697,7 +688,7 @@ object CombatMovementIntents { target: Entity, exhaustedLocalApproach: Boolean = false ): Boolean { - return attacker is Player && (exhaustedLocalApproach || CombatMovementPlanner.movementStepsThisTick(target) == 0) + return attacker is Player && (exhaustedLocalApproach || !CombatMovementPlanner.hasMovementStepThisTick(target)) } @JvmStatic diff --git a/Server/src/main/core/game/node/entity/combat/CombatMovementPlanner.kt b/Server/src/main/core/game/node/entity/combat/CombatMovementPlanner.kt index ea4ae2986..301fbdf6d 100644 --- a/Server/src/main/core/game/node/entity/combat/CombatMovementPlanner.kt +++ b/Server/src/main/core/game/node/entity/combat/CombatMovementPlanner.kt @@ -38,7 +38,12 @@ object CombatMovementPlanner { @JvmStatic fun predictedTargetLocation(target: Entity): Location { - return predictTargetLocations(target).lastOrNull() ?: target.location + return predictedTargetLocation(target, 2) + } + + @JvmStatic + fun predictedTargetLocation(target: Entity, maxSteps: Int): Location { + return predictedMovementLocation(target, maxSteps) ?: target.location } @JvmStatic @@ -51,30 +56,58 @@ object CombatMovementPlanner { if (maxSteps <= 0) { return emptyList() } - val queued = movementPoints(target, 2) - if (queued.isEmpty()) { + var first: Point? = null + var second: Point? = null + for (point in target.walkingQueue.queue) { + if (point.direction == null) { + continue + } + if (first == null) { + first = point + } else { + second = point + break + } + } + val firstPoint = first ?: return emptyList() + val steps = movementStepsThisTick(target, firstPoint, second).coerceAtMost(maxSteps) + if (steps <= 0) { return emptyList() } - val steps = if (canMoveTwoStepsThisTick(target, queued)) maxSteps.coerceAtMost(2) else 1 val predicted = ArrayList(steps) - for (i in 0 until steps) { - val point = queued[i] - predicted.add(Location.create(point.x, point.y, target.location.z)) + predicted.add(Location.create(firstPoint.x, firstPoint.y, target.location.z)) + if (steps > 1 && second != null) { + predicted.add(Location.create(second.x, second.y, target.location.z)) } return predicted } @JvmStatic fun movementStepsThisTick(target: Entity): Int { - val queued = movementPoints(target, 2) - if (queued.isEmpty()) { - return 0 - } - return if (canMoveTwoStepsThisTick(target, queued)) { - 2 - } else { - 1 + var first: Point? = null + var second: Point? = null + for (point in target.walkingQueue.queue) { + if (point.direction == null) { + continue + } + if (first == null) { + first = point + } else { + second = point + break + } } + return movementStepsThisTick(target, first ?: return 0, second) + } + + @JvmStatic + fun hasMovementStepThisTick(target: Entity): Boolean { + return firstMovementPoint(target) != null + } + + @JvmStatic + fun predictedMovementLocation(target: Entity): Location? { + return predictedMovementLocation(target, 2) } @JvmStatic @@ -135,22 +168,45 @@ object CombatMovementPlanner { return border.toList() } - private fun movementPoints(target: Entity, limit: Int): List { - val points = ArrayList(limit) + @JvmStatic + fun predictedMovementLocation(target: Entity, maxSteps: Int): Location? { + if (maxSteps <= 0) { + return null + } + var first: Point? = null + var second: Point? = null for (point in target.walkingQueue.queue) { if (point.direction == null) { continue } - points.add(point) - if (points.size >= limit) { + if (first == null) { + first = point + } else { + second = point break } } - return points + val firstPoint = first ?: return null + val steps = movementStepsThisTick(target, firstPoint, second).coerceAtMost(maxSteps) + val point = if (steps > 1 && second != null) second else firstPoint + return Location.create(point.x, point.y, target.location.z) } - private fun canMoveTwoStepsThisTick(target: Entity, queued: List): Boolean { - if (queued.size <= 1 || queued.first().isRunDisabled || !target.walkingQueue.isRunningBoth) { + private fun firstMovementPoint(target: Entity): Point? { + for (point in target.walkingQueue.queue) { + if (point.direction != null) { + return point + } + } + return null + } + + private fun movementStepsThisTick(target: Entity, first: Point, second: Point?): Int { + return if (canMoveTwoStepsThisTick(target, first, second)) 2 else 1 + } + + private fun canMoveTwoStepsThisTick(target: Entity, first: Point, second: Point?): Boolean { + if (second == null || first.isRunDisabled || !target.walkingQueue.isRunningBoth) { return false } return target !is Player || target.settings.runEnergy >= 1.0 diff --git a/Server/src/main/core/game/node/entity/combat/CombatPulse.kt b/Server/src/main/core/game/node/entity/combat/CombatPulse.kt index 2a9ce3295..06f5c8cd1 100644 --- a/Server/src/main/core/game/node/entity/combat/CombatPulse.kt +++ b/Server/src/main/core/game/node/entity/combat/CombatPulse.kt @@ -117,7 +117,7 @@ class CombatPulse( false } else { val timedOut = combatTimeOut++ > entity.properties.combatTimeOut - if (timedOut && entity is Player && CombatMovementPlanner.movementStepsThisTick(victim!!) == 0) { + if (timedOut && entity is Player && !CombatMovementPlanner.hasMovementStepThisTick(victim!!)) { CombatMovementIntents.stopUnreachableCombat(entity) } timedOut @@ -209,7 +209,7 @@ class CombatPulse( return false } if (CombatMovementPlanner.exceedsCombatChaseDistance(attacker, target)) { - if (attacker is Player && CombatMovementPlanner.movementStepsThisTick(target) == 0) { + if (attacker is Player && !CombatMovementPlanner.hasMovementStepThisTick(target)) { CombatMovementIntents.stopUnreachableCombat(attacker) } else { stop() diff --git a/Server/src/main/core/game/node/entity/combat/CombatSwingHandler.kt b/Server/src/main/core/game/node/entity/combat/CombatSwingHandler.kt index 249d070cd..58790729a 100644 --- a/Server/src/main/core/game/node/entity/combat/CombatSwingHandler.kt +++ b/Server/src/main/core/game/node/entity/combat/CombatSwingHandler.kt @@ -13,8 +13,7 @@ import core.game.node.entity.skill.Skills import content.global.skill.summoning.familiar.Familiar import core.api.log import core.api.playGlobalAudio -import core.game.world.map.RegionManager -import core.game.world.map.path.Pathfinder.* +import core.game.world.map.path.RsmodPathfinder import core.game.world.update.flag.context.Animation import core.tools.RandomFunction import core.game.system.config.ItemConfigParser @@ -576,21 +575,14 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { */ @JvmStatic fun isProjectileClipped(entity: Node, victim: Node?, checkClose: Boolean): Boolean { - for(x1 in 0 until entity.size()) { - for(y1 in 0 until entity.size()) { - val src = entity.location.transform(x1, y1, 0) - for(x2 in 0 until victim!!.size()) { - for(y2 in 0 until victim!!.size()) { - val dst = victim!!.location.transform(x2, y2, 0) - val path = PROJECTILE.find(src, 1, dst, 1, 1, 0, 0, 0, false, RegionManager::getClippingFlag) - if(path.isSuccessful && (!checkClose || path.points.size <= 1)) { - return true - } - } - } - } - } - return false + val target = requireNotNull(victim) + return RsmodPathfinder.hasLineOfSightBetween( + entity.location, + entity.size(), + target.location, + target.size(), + maxRaySteps = if (checkClose) 1 else Int.MAX_VALUE + ) } } diff --git a/Server/src/main/core/game/world/map/path/RsmodPathfinder.kt b/Server/src/main/core/game/world/map/path/RsmodPathfinder.kt index a68f498e6..4eec34083 100644 --- a/Server/src/main/core/game/world/map/path/RsmodPathfinder.kt +++ b/Server/src/main/core/game/world/map/path/RsmodPathfinder.kt @@ -5,16 +5,24 @@ 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 : Pathfinder() { +class RsmodPathfinder( + private val maxWaypoints: Int = 25 +) : Pathfinder() { - private val routeFinder = ThreadLocal.withInitial { RouteFinderState() } + private val defaultFinder = ThreadLocal.withInitial { + PathFinder(RegionManager.RSMOD_CLIPPING_FLAGS, SEARCH_MAP_SIZE, RING_BUFFER_SIZE) + } + private val suppliedFinder = ThreadLocal.withInitial { RouteFinderState() } override fun find( start: Location?, @@ -30,16 +38,13 @@ class RsmodPathfinder : Pathfinder() { ): 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) val magnitude = floor(vector.magnitude()) if (magnitude > ServerConstants.MAX_PATHFIND_DISTANCE) { - if (magnitude < 50.0) { + if (canAttempt(source, destination)) { end = source.transform(vector.normalized() * (ServerConstants.MAX_PATHFIND_DISTANCE - 1)) } else { path.isMoveNear = true @@ -47,15 +52,16 @@ class RsmodPathfinder : Pathfinder() { } } - val state = routeFinder.get() - state.loadCollisionWindow(source, supplier) - - val shape = when { - type != 0 -> type - 1 - sizeX != 0 && sizeY != 0 -> 10 - else -> -1 + 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 route = state.finder.findPath( + val route = finder.findPath( level = source.z, srcX = source.x, srcZ = source.y, @@ -67,26 +73,180 @@ class RsmodPathfinder : Pathfinder() { objRot = rotation, objShape = shape, moveNear = near, - blockAccessFlags = walkingFlag + blockAccessFlags = walkingFlag, + maxWaypoints = maxWaypoints ) if (route.failed) { return path } + var currentX = source.x + var currentY = source.y + path.points.add(Point(currentX, currentY)) for (waypoint in route.waypoints) { - path.points.add(Point(waypoint.x, waypoint.z)) + while (currentX != waypoint.x || currentY != waypoint.z) { + currentX += waypoint.x.compareTo(currentX) + currentY += waypoint.z.compareTo(currentY) + path.points.add(Point(currentX, currentY)) + } } path.setSuccesful(true) path.isMoveNear = route.alternative || end != destination return path } - private class RouteFinderState { - val flags = CollisionFlagMap() - val finder = PathFinder(flags, SEARCH_MAP_SIZE, RING_BUFFER_SIZE) + companion object { + @JvmStatic + fun canAttempt(start: Location, dest: Location): Boolean { + val distance = floor(Vector.betweenLocs(start, dest).magnitude()) + return distance < ServerConstants.MAX_PATHFIND_DISTANCE * 2.0 + } - fun loadCollisionWindow(start: Location, supplier: ClipMaskSupplier) { + @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 + ) + } + + @JvmStatic + fun lineOfSight( + start: Location, + dest: Location, + moverSize: Int, + destWidth: Int, + destHeight: Int + ): RayCast { + RegionManager.loadClippingWindow(start, SEARCH_MAP_SIZE) + return lineOfSightLoaded(start, dest, moverSize, destWidth, destHeight) + } + + private fun lineOfSightLoaded( + start: Location, + dest: Location, + moverSize: Int, + destWidth: Int, + destHeight: Int + ): RayCast { + return projectileLineFinder.get().lineOfSight( + level = start.z, + srcX = start.x, + srcZ = start.y, + destX = dest.x, + destZ = dest.y, + srcSize = moverSize, + destWidth = destWidth, + destHeight = destHeight + ) + } + + @JvmStatic + fun hasLineOfSight( + start: Location, + dest: Location, + moverSize: Int, + destWidth: Int, + destHeight: Int, + maxRaySteps: Int = Int.MAX_VALUE + ): Boolean { + val rayCast = lineOfSight(start, dest, moverSize, destWidth, destHeight) + return rayCast.success && rayCast.coordinates.size <= maxRaySteps + } + + @JvmStatic + fun hasLineOfSightBetween( + sourceLocation: Location, + sourceSize: Int, + targetLocation: Location, + targetSize: Int, + maxRaySteps: Int = Int.MAX_VALUE + ): Boolean { + RegionManager.loadClippingWindow(sourceLocation, SEARCH_MAP_SIZE) + return hasLineOfSightBetweenLoaded(sourceLocation, sourceSize, targetLocation, targetSize, maxRaySteps) + } + + @JvmStatic + fun loadLineOfSightWindow(center: Location) { + RegionManager.loadClippingWindow(center, SEARCH_MAP_SIZE) + } + + @JvmStatic + fun hasLineOfSightBetweenLoaded( + sourceLocation: Location, + sourceSize: Int, + targetLocation: Location, + targetSize: Int, + maxRaySteps: Int = Int.MAX_VALUE + ): Boolean { + for (sourceX in 0 until sourceSize) { + for (sourceY in 0 until sourceSize) { + val source = sourceLocation.transform(sourceX, sourceY, 0) + for (targetX in 0 until targetSize) { + for (targetY in 0 until targetSize) { + val destination = targetLocation.transform(targetX, targetY, 0) + val rayCast = lineOfSightLoaded(source, destination, 1, 1, 1) + if (rayCast.success && rayCast.coordinates.size <= maxRaySteps) { + return true + } + } + } + } + } + return false + } + + private fun routeShape(type: Int, sizeX: Int, sizeY: Int): Int = when { + type >= 0 -> type + sizeX != 0 && sizeY != 0 -> 10 + else -> -1 + } + + private val projectileLineFinder = ThreadLocal.withInitial { + LinePathFinder(RegionManager.RSMOD_PROJECTILE_FLAGS) + } + + private fun loadCollisionWindow( + flags: CollisionFlagMap, + 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) { @@ -96,4 +256,13 @@ class RsmodPathfinder : Pathfinder() { } } } + + 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) + } + } } diff --git a/Server/src/test/kotlin/core/PathfinderTests.kt b/Server/src/test/kotlin/core/PathfinderTests.kt index 9bfcfbcd2..20fa3c249 100644 --- a/Server/src/test/kotlin/core/PathfinderTests.kt +++ b/Server/src/test/kotlin/core/PathfinderTests.kt @@ -103,6 +103,9 @@ class PathfinderTests { val blocked = Pathfinder.PROJECTILE.find(start, 1, dest, 0, 0, 0, -1, 0, false, null) Assertions.assertFalse(blocked.isSuccessful) + Assertions.assertFalse(RsmodPathfinder.hasLineOfSightBetween(start, 1, dest, 1)) + RsmodPathfinder.loadLineOfSightWindow(start) + Assertions.assertFalse(RsmodPathfinder.hasLineOfSightBetweenLoaded(start, 1, dest, 1)) } finally { RegionManager.setRsmodFlag(0, 3201, 3200, true, 0) }