From 894674a12f7192d257ba51d7014439273cdc2501 Mon Sep 17 00:00:00 2001 From: dam <27978131-real_damighty@users.noreply.gitlab.com> Date: Wed, 6 May 2026 23:15:07 +0300 Subject: [PATCH] More detailed performance logging, more RSMOD --- .../entity/combat/CombatMovementIntents.kt | 428 +++++++++++++++--- .../game/world/map/path/RsmodPathfinder.kt | 35 ++ .../src/main/core/worker/MajorUpdateWorker.kt | 5 +- 3 files changed, 410 insertions(+), 58 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 cc888e6e5..e99233dc6 100644 --- a/Server/src/main/core/game/node/entity/combat/CombatMovementIntents.kt +++ b/Server/src/main/core/game/node/entity/combat/CombatMovementIntents.kt @@ -16,6 +16,8 @@ import core.game.world.map.path.Path import core.game.world.map.path.Pathfinder import core.game.world.map.path.RsmodPathfinder import java.util.LinkedHashMap +import org.rsmod.game.pathfinder.PathFinder as RsmodRouteFinder +import kotlin.math.sqrt /** * Collects combat movement requests during pulse updates and applies them before @@ -36,9 +38,98 @@ object CombatMovementIntents { get() = node.location } private data class CandidatePath(val steps: List, val projectedLocation: Location) + data class ResolveReport( + val intents: Int, + val candidateCount: Int, + val directPathHits: Int, + val rsmodRouteCalls: Int, + val losCalls: Int, + val slowestIntent: SlowIntent? + ) { + fun summary(): String { + val slowest = slowestIntent?.let { + ", slowestIntent=${it.elapsedMicros}us attacker=${it.attacker} target=${it.target} " + + "candidates=${it.candidateCount} directPathHits=${it.directPathHits} " + + "rsmodRouteCalls=${it.rsmodRouteCalls} losCalls=${it.losCalls}" + } ?: "" + return "combatMovementStats intents=$intents candidates=$candidateCount " + + "directPathHits=$directPathHits rsmodRouteCalls=$rsmodRouteCalls losCalls=$losCalls$slowest" + } + + companion object { + val EMPTY = ResolveReport(0, 0, 0, 0, 0, null) + } + } + + data class SlowIntent( + val attacker: String, + val target: String, + val elapsedMicros: Long, + val candidateCount: Int, + val directPathHits: Int, + val rsmodRouteCalls: Int, + val losCalls: Int + ) + + private class IntentTrace { + var candidateCount = 0 + var directPathHits = 0 + var rsmodRouteCalls = 0 + var losCalls = 0 + } + + private class ResolveStats { + var intents = 0 + var candidateCount = 0 + var directPathHits = 0 + var rsmodRouteCalls = 0 + var losCalls = 0 + private var slowestNanos = Long.MIN_VALUE + private var slowestIntent: SlowIntent? = null + + fun record(intent: Intent, trace: IntentTrace, elapsedNanos: Long) { + intents++ + candidateCount += trace.candidateCount + directPathHits += trace.directPathHits + rsmodRouteCalls += trace.rsmodRouteCalls + losCalls += trace.losCalls + if (elapsedNanos > slowestNanos) { + slowestNanos = elapsedNanos + slowestIntent = SlowIntent( + attacker = describe(intent.attacker), + target = describe(intent.target), + elapsedMicros = elapsedNanos / 1_000, + candidateCount = trace.candidateCount, + directPathHits = trace.directPathHits, + rsmodRouteCalls = trace.rsmodRouteCalls, + losCalls = trace.losCalls + ) + } + } + + fun report(): ResolveReport { + return ResolveReport( + intents = intents, + candidateCount = candidateCount, + directPathHits = directPathHits, + rsmodRouteCalls = rsmodRouteCalls, + losCalls = losCalls, + slowestIntent = slowestIntent + ) + } + + private fun describe(entity: Entity): String { + return when (entity) { + is Player -> "Player(${entity.username}#${entity.index}@${entity.location})" + is NPC -> "NPC(${entity.id}#${entity.index}@${entity.location})" + else -> "${entity.javaClass.simpleName}(#${entity.index}@${entity.location})" + } + } + } private val intents = LinkedHashMap() private val activeMeleeAttackers = LinkedHashMap() + private var lastResolveReport = ResolveReport.EMPTY @JvmStatic fun request(attacker: Entity, target: Entity) { @@ -93,6 +184,7 @@ object CombatMovementIntents { @JvmStatic fun resolve() { + lastResolveReport = ResolveReport.EMPTY if (intents.isEmpty()) { return } @@ -105,9 +197,11 @@ object CombatMovementIntents { val reservedTiles = LinkedHashSet() val projectedLocations = HashMap() + val stats = ResolveStats() for (intent in pending) { - resolve(intent, reservedTiles, projectedLocations) + resolve(intent, reservedTiles, projectedLocations, stats) } + lastResolveReport = stats.report() } @JvmStatic @@ -121,6 +215,11 @@ object CombatMovementIntents { return intents.size } + @JvmStatic + fun lastResolveSummary(): String { + return lastResolveReport.summary() + } + @JvmStatic fun shouldMaintainMeleePressure(attacker: Entity, target: Entity): Boolean { if (attacker.locks.isMovementLocked) { @@ -153,7 +252,23 @@ object CombatMovementIntents { private fun resolve( intent: Intent, reservedTiles: MutableSet, - projectedLocations: MutableMap + projectedLocations: MutableMap, + stats: ResolveStats + ) { + val trace = IntentTrace() + val start = System.nanoTime() + try { + resolveIntent(intent, reservedTiles, projectedLocations, trace) + } finally { + stats.record(intent, trace, System.nanoTime() - start) + } + } + + private fun resolveIntent( + intent: Intent, + reservedTiles: MutableSet, + projectedLocations: MutableMap, + trace: IntentTrace ) { val attacker = intent.attacker val target = intent.target @@ -163,36 +278,54 @@ object CombatMovementIntents { val targetLocation = targetLocationFor(attacker, target) val projectedTargetLocation = projectedLocations[target] - if (projectedTargetLocation != null && canAttackFrom(attacker, target, attacker.location, projectedTargetLocation)) { + if (projectedTargetLocation != null && canAttackFrom(attacker, target, attacker.location, projectedTargetLocation, trace)) { attacker.walkingQueue.reset() attacker.face(target) projectedLocations[attacker] = attacker.location - reservedTiles.addAll(occupiedTiles(attacker, attacker.location)) + reserveOccupiedTiles(reservedTiles, attacker, attacker.location) return } - if (canAttackFrom(attacker, target, attacker.location, targetLocation)) { + if (canAttackFrom(attacker, target, attacker.location, targetLocation, trace)) { attacker.walkingQueue.reset() attacker.face(target) projectedLocations[attacker] = attacker.location - reservedTiles.addAll(occupiedTiles(attacker, attacker.location)) + reserveOccupiedTiles(reservedTiles, attacker, attacker.location) return } - val candidates = movementDestinationsFor(attacker, target, targetLocation) + val pathfinder = pathfinderFor(attacker) + if (shouldUseTargetFootprintRoute(attacker, target, pathfinder)) { + val candidatePath = pathToTargetFootprint(attacker, target, targetLocation, pathfinder, trace) + if (candidatePath != null) { + if (hasReservedOccupiedTile(reservedTiles, attacker, candidatePath.projectedLocation)) { + return + } + walkPath(attacker, candidatePath) + attacker.face(target) + projectedLocations[attacker] = candidatePath.projectedLocation + reserveOccupiedTiles(reservedTiles, attacker, candidatePath.projectedLocation) + return + } + if (shouldStopUnreachableCombat(attacker, target)) { + stopUnreachableCombat(attacker) + } + return + } + + val candidates = movementDestinationsFor(attacker, target, targetLocation, pathfinder, trace) val standingOnCandidate = candidates.any { it.location == attacker.location } val projectedAttackerLocation = CombatMovementPlanner.predictedMovementLocation(attacker) ?: attacker.location - if (projectedAttackerLocation != attacker.location && canAttackFrom(attacker, target, projectedAttackerLocation, targetLocation)) { + if (projectedAttackerLocation != attacker.location && canAttackFrom(attacker, target, projectedAttackerLocation, targetLocation, trace)) { attacker.face(target) projectedLocations[attacker] = projectedAttackerLocation - reservedTiles.addAll(occupiedTiles(attacker, projectedAttackerLocation)) + reserveOccupiedTiles(reservedTiles, attacker, projectedAttackerLocation) return } var blockedByReservation = false for (candidate in candidates) { - val candidatePath = pathTo(attacker, candidate) ?: continue - val projectedTiles = occupiedTiles(attacker, candidatePath.projectedLocation) - if (projectedTiles.any { it in reservedTiles }) { + val candidatePath = pathTo(attacker, candidate, trace) ?: continue + if (hasReservedOccupiedTile(reservedTiles, attacker, candidatePath.projectedLocation)) { blockedByReservation = true continue } @@ -200,7 +333,7 @@ object CombatMovementIntents { walkPath(attacker, candidatePath) attacker.face(target) projectedLocations[attacker] = candidatePath.projectedLocation - reservedTiles.addAll(projectedTiles) + reserveOccupiedTiles(reservedTiles, attacker, candidatePath.projectedLocation) return } if (!blockedByReservation && shouldStopUnreachableCombat(attacker, target, standingOnCandidate)) { @@ -229,7 +362,13 @@ object CombatMovementIntents { return attacker !is NPC || !attacker.isNeverWalks } - private fun canAttackFrom(attacker: Entity, target: Entity, attackerLocation: Location, targetLocation: Location): Boolean { + private fun canAttackFrom( + attacker: Entity, + target: Entity, + attackerLocation: Location, + targetLocation: Location, + trace: IntentTrace? = null + ): Boolean { if (attackerLocation.z != targetLocation.z) { return false } @@ -237,38 +376,59 @@ object CombatMovementIntents { return false } return when (attacker.properties.combatPulse.style) { - CombatStyle.RANGE, CombatStyle.MAGIC -> canAttackFromRange(attacker, target, attackerLocation, targetLocation) - else -> canAttackFromMelee(attacker, target, attackerLocation, targetLocation) + CombatStyle.RANGE, CombatStyle.MAGIC -> canAttackFromRange(attacker, target, attackerLocation, targetLocation, trace) + else -> canAttackFromMelee(attacker, target, attackerLocation, targetLocation, trace) } } - private fun movementDestinationsFor(attacker: Entity, target: Entity, targetLocation: Location): List { - val pathfinder = pathfinderFor(attacker) + private fun movementDestinationsFor( + attacker: Entity, + target: Entity, + targetLocation: Location, + pathfinder: Pathfinder, + trace: IntentTrace + ): List { if (attacker is NPC && pathfinder === Pathfinder.DUMB) { - val destinations = if (occupiedTilesOverlap(attacker, target)) { - CombatMovementPlanner.candidateAttackTiles(attacker, target, targetLocation) + val destinations = ArrayList(4) + if (occupiedTilesOverlap(attacker, target)) { + for (location in CombatMovementPlanner.candidateAttackTiles(attacker, target, targetLocation)) { + destinations.add(MovementDestination(location, pathfinder, allowPartialPath = true)) + } } else { - dumbNpcAttackDestinations(attacker, target) - } - return destinations.map { - MovementDestination(it, pathfinder, allowPartialPath = true) + addDumbNpcAttackDestinations(attacker, target, pathfinder, destinations) } + trace.candidateCount += destinations.size + return destinations } - val destinations = CombatMovementPlanner.candidateAttackTiles(attacker, target, targetLocation).map { - MovementDestination(it, pathfinder, allowPartialPath = false) - } + val attackTiles = CombatMovementPlanner.candidateAttackTiles(attacker, target, targetLocation) if (attacker is Player) { + val rangedTiles = playerAttackRangeDestinations(attacker, target, targetLocation, pathfinder, trace) + val capacity = rangedTiles.size + attackTiles.size + 1 + val destinations = ArrayList(capacity) + destinations.addAll(rangedTiles) + for (location in attackTiles) { + destinations.add(MovementDestination(location, pathfinder, allowPartialPath = false)) + } val targetFallback = if (!occupiedTilesOverlap(attacker, target) && shouldAllowPartialTargetPath(attacker, target, targetLocation) ) { - listOf(MovementDestination(target, pathfinder, allowPartialPath = true)) + MovementDestination(target, pathfinder, allowPartialPath = true) } else { - emptyList() + null } - val rangedDestinations = playerAttackRangeDestinations(attacker, target, targetLocation, pathfinder) - return rangedDestinations + destinations + targetFallback + if (targetFallback != null) { + destinations.add(targetFallback) + } + trace.candidateCount += destinations.size + return destinations } + + val destinations = ArrayList(attackTiles.size) + for (location in attackTiles) { + destinations.add(MovementDestination(location, pathfinder, allowPartialPath = false)) + } + trace.candidateCount += destinations.size return destinations } @@ -287,15 +447,19 @@ object CombatMovementIntents { attacker: Player, target: Entity, targetLocation: Location, - pathfinder: Pathfinder + pathfinder: Pathfinder, + trace: IntentTrace ): List { val range = playerAttackRange(attacker) if (range <= CombatReach.meleeDistance(attacker)) { return emptyList() } - return attackRangeTiles(attacker, target, targetLocation, range).map { - MovementDestination(it, pathfinder, allowPartialPath = false) + val attackTiles = attackRangeTiles(attacker, target, targetLocation, range, trace) + val destinations = ArrayList(attackTiles.size) + for (tile in attackTiles) { + destinations.add(MovementDestination(tile, pathfinder, allowPartialPath = false)) } + return destinations } private fun playerAttackRange(attacker: Player): Int { @@ -325,7 +489,13 @@ object CombatMovementIntents { return distance } - private fun attackRangeTiles(attacker: Player, target: Entity, targetLocation: Location, range: Int): List { + private fun attackRangeTiles( + attacker: Player, + target: Entity, + targetLocation: Location, + range: Int, + trace: IntentTrace + ): List { val tiles = ArrayList() val minX = targetLocation.x - range val maxX = targetLocation.x + target.size() - 1 + range @@ -351,7 +521,7 @@ object CombatMovementIntents { val attackTiles = ArrayList(minOf(MAX_RANGED_APPROACH_CANDIDATES, tiles.size)) RsmodPathfinder.loadLineOfSightWindow(targetLocation) for (tile in tiles) { - if (!hasProjectileLineOfSight(tile, attacker.size(), target, targetLocation, loadWindow = false)) { + if (!hasProjectileLineOfSight(tile, attacker.size(), target, targetLocation, loadWindow = false, trace = trace)) { continue } attackTiles.add(tile) @@ -366,7 +536,8 @@ object CombatMovementIntents { attacker: Entity, target: Entity, attackerLocation: Location, - targetLocation: Location + targetLocation: Location, + trace: IntentTrace? = null ): Boolean { val distance = CombatReach.meleeDistance(attacker) if (distance == 1 && !isAdjacentToTarget(attacker, attackerLocation, target, targetLocation)) { @@ -382,7 +553,8 @@ object CombatMovementIntents { attacker.size(), target, targetLocation, - checkClose = !CombatReach.isUsingHalberd(attacker) + checkClose = !CombatReach.isUsingHalberd(attacker), + trace = trace ) } @@ -390,7 +562,8 @@ object CombatMovementIntents { attacker: Entity, target: Entity, attackerLocation: Location, - targetLocation: Location + targetLocation: Location, + trace: IntentTrace? = null ): Boolean { val range = if (attacker is Player) { playerAttackRange(attacker) @@ -402,7 +575,7 @@ object CombatMovementIntents { ) } return distanceSquaredToClosestOccupiedTile(target, targetLocation, attackerLocation) <= range * range && - hasProjectileLineOfSight(attackerLocation, attacker.size(), target, targetLocation) + hasProjectileLineOfSight(attackerLocation, attacker.size(), target, targetLocation, trace = trace) } private fun isAdjacentToTarget( @@ -474,8 +647,12 @@ object CombatMovementIntents { target: Entity, targetLocation: Location, checkClose: Boolean = false, - loadWindow: Boolean = true + loadWindow: Boolean = true, + trace: IntentTrace? = null ): Boolean { + if (trace != null) { + trace.losCalls++ + } val maxRaySteps = if (checkClose) 1 else Int.MAX_VALUE if (!loadWindow) { return RsmodPathfinder.hasLineOfSightBetweenLoaded( @@ -509,22 +686,114 @@ object CombatMovementIntents { return dx * dx + dy * dy } - private fun dumbNpcAttackDestinations(attacker: NPC, target: Entity): List { - val directions = LinkedHashSet() - directions.add(Direction.getLogicalDirection(target.centerLocation, attacker.centerLocation)) + private fun shouldUseTargetFootprintRoute(attacker: Entity, target: Entity, pathfinder: Pathfinder): Boolean { + if (attacker.properties.combatPulse.style != CombatStyle.MELEE || occupiedTilesOverlap(attacker, target)) { + return false + } + return attacker !is NPC || pathfinder !== Pathfinder.DUMB + } + + private fun pathToTargetFootprint( + attacker: Entity, + target: Entity, + targetLocation: Location, + pathfinder: Pathfinder, + trace: IntentTrace + ): CandidatePath? { + val directDestination = naiveMeleeDestination(attacker, target, targetLocation) + val directPath = directPathTo(attacker, directDestination) + if (directPath != null) { + trace.directPathHits++ + return directPath + } + if (pathfinder === Pathfinder.SMART && !RsmodPathfinder.canAttempt(attacker.location, targetLocation)) { + return null + } + + trace.rsmodRouteCalls++ + val clipMaskSupplier = if (attacker is NPC) { + attacker.behavior?.getClippingSupplier(attacker) + } else { + null + } + val path = pathfinder.find( + attacker.location, + attacker.size(), + targetLocation, + target.size(), + target.size(), + 0, + -1, + 0, + true, + clipMaskSupplier + ) + if (!path.isSuccessful || path.points.isEmpty()) { + return null + } + if (attacker is Player && !path.isMoveNear && isExcessiveCombatDetourToTarget(attacker, target, targetLocation, path)) { + return null + } + val steps = immediateMovementSteps(attacker, path) + if (attacker is Player && path.isMoveNear && !partialPathMovesCloserToTarget(attacker, target, targetLocation, steps)) { + return null + } + if (attacker is Player && steps.any { !RegionManager.isTeleportPermitted(Location.create(it.x, it.y, attacker.location.z)) }) { + return null + } + val projected = steps.lastOrNull()?.let { Location.create(it.x, it.y, attacker.location.z) } ?: return null + return CandidatePath(steps, projected) + } + + private fun naiveMeleeDestination(attacker: Entity, target: Entity, targetLocation: Location): Location { + val destination = RsmodRouteFinder.naiveDestination( + sourceX = attacker.location.x, + sourceZ = attacker.location.y, + sourceWidth = attacker.size(), + sourceHeight = attacker.size(), + targetX = targetLocation.x, + targetZ = targetLocation.y, + targetWidth = target.size(), + targetHeight = target.size() + ) + return Location.create(destination.x, destination.z, targetLocation.z) + } + + private fun addDumbNpcAttackDestinations( + attacker: NPC, + target: Entity, + pathfinder: Pathfinder, + destinations: MutableList + ) { + val directions = ArrayList(3) + addDirection(directions, Direction.getLogicalDirection(target.centerLocation, attacker.centerLocation)) if (attacker.centerLocation.x < target.centerLocation.x) { - directions.add(Direction.WEST) + addDirection(directions, Direction.WEST) } else if (attacker.centerLocation.x > target.centerLocation.x) { - directions.add(Direction.EAST) + addDirection(directions, Direction.EAST) } if (attacker.centerLocation.y < target.centerLocation.y) { - directions.add(Direction.SOUTH) + addDirection(directions, Direction.SOUTH) } else if (attacker.centerLocation.y > target.centerLocation.y) { - directions.add(Direction.NORTH) + addDirection(directions, Direction.NORTH) } - return directions.map { dumbNpcAttackDestination(attacker, target, it) } + for (direction in directions) { + destinations.add( + MovementDestination( + dumbNpcAttackDestination(attacker, target, direction), + pathfinder, + allowPartialPath = true + ) + ) + } + } + + private fun addDirection(directions: MutableList, direction: Direction) { + if (!directions.contains(direction)) { + directions.add(direction) + } } private fun dumbNpcAttackDestination(attacker: NPC, target: Entity, direction: Direction): Location { @@ -558,12 +827,13 @@ object CombatMovementIntents { } } - private fun pathTo(attacker: Entity, destination: MovementDestination): CandidatePath? { + private fun pathTo(attacker: Entity, destination: MovementDestination, trace: IntentTrace): CandidatePath? { if (attacker.location == destination.location) { return null } val directPath = directPathTo(attacker, destination) if (directPath != null) { + trace.directPathHits++ return directPath } if (destination.pathfinder === Pathfinder.SMART && @@ -572,6 +842,7 @@ object CombatMovementIntents { return null } + trace.rsmodRouteCalls++ val path = Pathfinder.find(attacker, destination.node, destination.allowPartialPath, destination.pathfinder) if (!path.reaches(destination.location) && (!destination.allowPartialPath || path.points.isEmpty())) { return null @@ -594,15 +865,22 @@ object CombatMovementIntents { if (attacker.size() != 1 || destination.node !is Location || attacker.location.z != destination.location.z) { return null } + return directPathTo(attacker, destination.location) + } + + private fun directPathTo(attacker: Entity, destination: Location): CandidatePath? { + if (attacker.size() != 1 || attacker.location.z != destination.z) { + return null + } val maxSteps = movementStepsFor(attacker) val steps = ArrayList(maxSteps) var current = attacker.location var distance = 0 - while (current != destination.location) { + while (current != destination) { if (++distance > MAX_DIRECT_COMBAT_PATH_DISTANCE) { return null } - val direction = Direction.getDirection(current, destination.location) ?: return null + val direction = Direction.getDirection(current, destination) ?: return null if (!direction.canMoveFrom(current.z, current.x, current.y, RegionManager::getClippingFlag)) { return null } @@ -622,6 +900,28 @@ object CombatMovementIntents { return CandidatePath(steps, projected) } + private fun isExcessiveCombatDetourToTarget( + attacker: Player, + target: Entity, + targetLocation: Location, + path: Path + ): Boolean { + val pathLength = (path.points.size - 1).coerceAtLeast(0) + val directDistance = sqrt(distanceSquaredToClosestOccupiedTile(target, targetLocation, attacker.location).toDouble()) + return pathLength > directDistance + MAX_PLAYER_COMBAT_PATH_DETOUR + } + + private fun partialPathMovesCloserToTarget( + attacker: Player, + target: Entity, + targetLocation: Location, + steps: List + ): Boolean { + val projected = steps.lastOrNull()?.let { Location.create(it.x, it.y, attacker.location.z) } ?: return false + return distanceSquaredToClosestOccupiedTile(target, targetLocation, projected) < + distanceSquaredToClosestOccupiedTile(target, targetLocation, attacker.location) + } + private fun isExcessiveCombatDetour(attacker: Player, destination: MovementDestination, path: Path): Boolean { val pathLength = (path.points.size - 1).coerceAtLeast(0) val directDistance = attacker.location.getDistance(destination.location) @@ -700,14 +1000,30 @@ object CombatMovementIntents { } } - private fun occupiedTiles(entity: Entity, location: Location): List { - val tiles = ArrayList(entity.size() * entity.size()) + private fun hasReservedOccupiedTile(reservedTiles: Set, entity: Entity, location: Location): Boolean { + if (entity.size() == 1) { + return location in reservedTiles + } for (x in 0 until entity.size()) { for (y in 0 until entity.size()) { - tiles.add(location.transform(x, y, 0)) + if (location.transform(x, y, 0) in reservedTiles) { + return true + } + } + } + return false + } + + private fun reserveOccupiedTiles(reservedTiles: MutableSet, entity: Entity, location: Location) { + if (entity.size() == 1) { + reservedTiles.add(location) + return + } + for (x in 0 until entity.size()) { + for (y in 0 until entity.size()) { + reservedTiles.add(location.transform(x, y, 0)) } } - return tiles } private fun occupiedTilesOverlap(first: Entity, second: Entity): Boolean { 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 4eec34083..a53ffbd17 100644 --- a/Server/src/main/core/game/world/map/path/RsmodPathfinder.kt +++ b/Server/src/main/core/game/world/map/path/RsmodPathfinder.kt @@ -6,6 +6,7 @@ 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.LineValidator import org.rsmod.game.pathfinder.PathFinder import org.rsmod.game.pathfinder.RayCast import org.rsmod.game.pathfinder.collision.CollisionFlagMap @@ -215,6 +216,17 @@ class RsmodPathfinder( targetSize: Int, maxRaySteps: Int = Int.MAX_VALUE ): Boolean { + if (sourceSize == 1 && targetSize == 1) { + if (maxRaySteps == Int.MAX_VALUE) { + return hasSingleTileLineOfSight(sourceLocation, targetLocation) + } + if (maxRaySteps == 1) { + if (manhattanDistance(sourceLocation, targetLocation) > 1) { + return false + } + return hasSingleTileLineOfSight(sourceLocation, targetLocation) + } + } for (sourceX in 0 until sourceSize) { for (sourceY in 0 until sourceSize) { val source = sourceLocation.transform(sourceX, sourceY, 0) @@ -232,12 +244,35 @@ class RsmodPathfinder( return false } + private fun hasSingleTileLineOfSight(sourceLocation: Location, targetLocation: Location): Boolean { + return projectileLineValidator.get().hasLineOfSight( + level = sourceLocation.z, + srcX = sourceLocation.x, + srcZ = sourceLocation.y, + destX = targetLocation.x, + destZ = targetLocation.y, + srcSize = 1, + destWidth = 1, + destHeight = 1 + ) + } + + private fun manhattanDistance(first: Location, second: Location): Int { + val dx = kotlin.math.abs(first.x - second.x) + val dy = kotlin.math.abs(first.y - second.y) + return dx + dy + } + private fun routeShape(type: Int, sizeX: Int, sizeY: Int): Int = when { type >= 0 -> type sizeX != 0 && sizeY != 0 -> 10 else -> -1 } + private val projectileLineValidator = ThreadLocal.withInitial { + LineValidator(RegionManager.RSMOD_PROJECTILE_FLAGS) + } + private val projectileLineFinder = ThreadLocal.withInitial { LinePathFinder(RegionManager.RSMOD_PROJECTILE_FLAGS) } diff --git a/Server/src/main/core/worker/MajorUpdateWorker.kt b/Server/src/main/core/worker/MajorUpdateWorker.kt index 7a2ca8976..b38415e3a 100644 --- a/Server/src/main/core/worker/MajorUpdateWorker.kt +++ b/Server/src/main/core/worker/MajorUpdateWorker.kt @@ -156,17 +156,18 @@ class MajorUpdateWorker { private fun notifyIfCombatMovementTooLong(meleePressureTime: Long, movementResolveTime: Long) { val totalTime = meleePressureTime + movementResolveTime + val resolveSummary = CombatMovementIntents.lastResolveSummary() if (totalTime >= CRITICAL_COMBAT_MOVEMENT_MS) { log( this::class.java, Log.WARN, - "CRITICALLY long combat movement update - requestActiveMeleePressure took $meleePressureTime ms, resolve took $movementResolveTime ms, total $totalTime ms" + "CRITICALLY long combat movement update - requestActiveMeleePressure took $meleePressureTime ms, resolve took $movementResolveTime ms, total $totalTime ms, $resolveSummary" ) } else if (totalTime >= LONG_COMBAT_MOVEMENT_MS) { log( this::class.java, Log.WARN, - "Long combat movement update - requestActiveMeleePressure took $meleePressureTime ms, resolve took $movementResolveTime ms, total $totalTime ms" + "Long combat movement update - requestActiveMeleePressure took $meleePressureTime ms, resolve took $movementResolveTime ms, total $totalTime ms, $resolveSummary" ) } }