From c52de9464d98ea84f536cde769b28d45f16f3053 Mon Sep 17 00:00:00 2001 From: dam <27978131-real_damighty@users.noreply.gitlab.com> Date: Fri, 1 May 2026 14:47:29 +0300 Subject: [PATCH] Fixed combat movement pathing past SmartPathfinder limit Adventure bots could enter combat with NPCs near the edge of the SmartPathfinder truncation window, causing combat movement to try attack tiles at distance 50+ and spam pathfinding exceptions. Skip SMART combat movement destinations that SmartPathfinder cannot attempt, and make the pathfinder cutoff follow max_pathfind_dist instead of a hard-coded 50. Added a regression test for the reported coordinate boundary. --- .../entity/combat/CombatMovementIntents.kt | 6 ++++ .../game/world/map/path/SmartPathfinder.kt | 31 ++++++++++++------- .../src/test/kotlin/core/PathfinderTests.kt | 10 +++++- 3 files changed, 34 insertions(+), 13 deletions(-) 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 dab817cc2..82ab74e25 100644 --- a/Server/src/main/core/game/node/entity/combat/CombatMovementIntents.kt +++ b/Server/src/main/core/game/node/entity/combat/CombatMovementIntents.kt @@ -14,6 +14,7 @@ import core.game.world.map.Point import core.game.world.map.RegionManager import core.game.world.map.path.Path import core.game.world.map.path.Pathfinder +import core.game.world.map.path.SmartPathfinder import core.game.world.repository.Repository import java.util.LinkedHashMap @@ -405,6 +406,11 @@ object CombatMovementIntents { if (attacker.location == destination.location) { return null } + if (destination.pathfinder === Pathfinder.SMART && + !SmartPathfinder.canAttempt(attacker.location, destination.location) + ) { + return null + } val path = Pathfinder.find(attacker, destination.node, destination.allowPartialPath, destination.pathfinder) if (!path.reaches(destination.location) && (!destination.allowPartialPath || path.points.isEmpty())) { diff --git a/Server/src/main/core/game/world/map/path/SmartPathfinder.kt b/Server/src/main/core/game/world/map/path/SmartPathfinder.kt index 2dcae4a9c..1087d318b 100644 --- a/Server/src/main/core/game/world/map/path/SmartPathfinder.kt +++ b/Server/src/main/core/game/world/map/path/SmartPathfinder.kt @@ -71,6 +71,13 @@ internal constructor() : Pathfinder() { */ private var foundPath = false + companion object { + fun canAttempt(start: Location, dest: Location): Boolean { + val distance = kotlin.math.floor(Vector.betweenLocs(start, dest).magnitude()) + return distance < ServerConstants.MAX_PATHFIND_DISTANCE * 2.0 + } + } + /** * Resets the pathfinder. */ @@ -101,16 +108,16 @@ internal constructor() : Pathfinder() { override fun find(start: Location?, moverSize: Int, dest: Location?, sizeX: Int, sizeY: Int, rotation: Int, type: Int, walkingFlag: Int, near: Boolean, clipMaskSupplier: ClipMaskSupplier?): Path { reset() - assert(start != null && dest != null) - var vec = Vector.betweenLocs(start!!, dest!!) + val startLoc = requireNotNull(start) + var end = requireNotNull(dest) + var vec = Vector.betweenLocs(startLoc, end) var mag = kotlin.math.floor(vec.magnitude()) - var end = dest!! if (mag > ServerConstants.MAX_PATHFIND_DISTANCE) { try { - if (mag < 50.0) { //truncate the path if it's realistically long + if (canAttempt(startLoc, end)) { //truncate the path if it's realistically long vec = vec.normalized() * (ServerConstants.MAX_PATHFIND_DISTANCE - 1) - end = start!!.transform(vec) - } else throw Exception("Pathfinding distance exceeds server max! -> " + mag.toString() + " {" + start + "->" + end + "}") + end = startLoc.transform(vec) + } else throw Exception("Pathfinding distance exceeds server max! -> " + mag.toString() + " {" + startLoc + "->" + end + "}") } catch (e: Exception) { val sw = StringWriter() val pw = PrintWriter(sw) @@ -129,12 +136,12 @@ internal constructor() : Pathfinder() { cost[x][y] = 99999999 } } - val z = start!!.z - val location = Location.create(start.regionX - 6 shl 3, start.regionY - 6 shl 3, z) - curX = start.sceneX - curY = start.sceneY - dstX = end!!.getSceneX(start) - dstY = end.getSceneY(start) + val z = startLoc.z + val location = Location.create(startLoc.regionX - 6 shl 3, startLoc.regionY - 6 shl 3, z) + curX = startLoc.sceneX + curY = startLoc.sceneY + dstX = end.getSceneX(startLoc) + dstY = end.getSceneY(startLoc) var attempts: Int var readPosition: Int check(curX, curY, 99, 0) diff --git a/Server/src/test/kotlin/core/PathfinderTests.kt b/Server/src/test/kotlin/core/PathfinderTests.kt index 84f6c640c..c30c69402 100644 --- a/Server/src/test/kotlin/core/PathfinderTests.kt +++ b/Server/src/test/kotlin/core/PathfinderTests.kt @@ -17,6 +17,7 @@ import core.game.node.entity.npc.NPC import core.game.node.entity.player.Player import core.game.world.GameWorld import core.game.world.map.Region +import core.game.world.map.path.SmartPathfinder import core.net.packet.PacketProcessor import core.plugin.ClassScanner import core.plugin.Plugin @@ -26,6 +27,13 @@ import org.rs09.consts.NPCs class PathfinderTests { companion object {init {TestUtils.preTestSetup(); GatheringSkillOptionListeners().defineListeners(); WoodcuttingListener().defineListeners() }; val NPC_TEST_LOC = ServerConstants.HOME_LOCATION!!.transform(2, 10, 0)} + @Test fun smartPathfinderShouldRejectDestinationsAtTheTruncationLimit() { + val start = Location.create(3165, 3218, 0) + + Assertions.assertTrue(SmartPathfinder.canAttempt(start, Location.create(3203, 3186, 0))) + Assertions.assertFalse(SmartPathfinder.canAttempt(start, Location.create(3203, 3185, 0))) + } + @Test fun getOccupiedTilesShouldReturnCorrectSetOfTilesThatAnObjectOccupiesAtAllRotations() { //clay fireplace - 13609 - sizex: 1, sizey: 2 val scenery = Scenery(13609, Location.create(50, 50, 0)) @@ -257,4 +265,4 @@ class PathfinderTests { Assertions.assertEquals(true, npc.location.getDistance(ServerConstants.HOME_LOCATION!!) <= 5) } } -} \ No newline at end of file +}