More detailed performance logging, more RSMOD

This commit is contained in:
dam 2026-05-06 23:15:07 +03:00
parent 870534be54
commit 894674a12f
No known key found for this signature in database
GPG key ID: 4AF4E722399663FB
3 changed files with 410 additions and 58 deletions

View file

@ -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<Point>, 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<Entity, Intent>()
private val activeMeleeAttackers = LinkedHashMap<Entity, Entity>()
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<Location>()
val projectedLocations = HashMap<Entity, Location>()
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<Location>,
projectedLocations: MutableMap<Entity, Location>
projectedLocations: MutableMap<Entity, Location>,
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<Location>,
projectedLocations: MutableMap<Entity, Location>,
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<MovementDestination> {
val pathfinder = pathfinderFor(attacker)
private fun movementDestinationsFor(
attacker: Entity,
target: Entity,
targetLocation: Location,
pathfinder: Pathfinder,
trace: IntentTrace
): List<MovementDestination> {
if (attacker is NPC && pathfinder === Pathfinder.DUMB) {
val destinations = if (occupiedTilesOverlap(attacker, target)) {
CombatMovementPlanner.candidateAttackTiles(attacker, target, targetLocation)
val destinations = ArrayList<MovementDestination>(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<MovementDestination>(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<MovementDestination>(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<MovementDestination> {
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<MovementDestination>(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<Location> {
private fun attackRangeTiles(
attacker: Player,
target: Entity,
targetLocation: Location,
range: Int,
trace: IntentTrace
): List<Location> {
val tiles = ArrayList<Location>()
val minX = targetLocation.x - range
val maxX = targetLocation.x + target.size() - 1 + range
@ -351,7 +521,7 @@ object CombatMovementIntents {
val attackTiles = ArrayList<Location>(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<Location> {
val directions = LinkedHashSet<Direction>()
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<MovementDestination>
) {
val directions = ArrayList<Direction>(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: 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<Point>(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<Point>
): 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<Location> {
val tiles = ArrayList<Location>(entity.size() * entity.size())
private fun hasReservedOccupiedTile(reservedTiles: Set<Location>, 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<Location>, 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 {

View file

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

View file

@ -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"
)
}
}