From 138d9d78a14bf7b9e8bd14d7d38b6b83f77b97a9 Mon Sep 17 00:00:00 2001 From: dam <27978131-real_damighty@users.noreply.gitlab.com> Date: Wed, 6 May 2026 18:31:48 +0300 Subject: [PATCH] Some performance fixes --- .../entity/combat/CombatMovementIntents.kt | 137 ++++++++++++++---- .../entity/combat/CombatMovementPlanner.kt | 30 +++- .../game/node/entity/combat/CombatPulse.kt | 4 + 3 files changed, 137 insertions(+), 34 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 3d89849b0..bd6fd3690 100644 --- a/Server/src/main/core/game/node/entity/combat/CombatMovementIntents.kt +++ b/Server/src/main/core/game/node/entity/combat/CombatMovementIntents.kt @@ -14,8 +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 core.game.world.map.path.RsmodPathfinder import java.util.LinkedHashMap /** @@ -25,6 +24,7 @@ import java.util.LinkedHashMap object CombatMovementIntents { private const val MAX_RANGED_APPROACH_CANDIDATES = 128 private const val MAX_PLAYER_COMBAT_PATH_DETOUR = 6.0 + private const val MAX_DIRECT_COMBAT_PATH_DISTANCE = 32 private data class Intent(val attacker: Entity, val target: Entity) private data class MovementDestination( @@ -38,6 +38,7 @@ object CombatMovementIntents { private data class CandidatePath(val steps: List, val projectedLocation: Location) private val intents = LinkedHashMap() + private val activeMeleeAttackers = LinkedHashMap() @JvmStatic fun request(attacker: Entity, target: Entity) { @@ -47,16 +48,46 @@ object CombatMovementIntents { if (attacker is NPC && attacker.isNeverWalks) { return } + trackActiveMelee(attacker, target) intents[attacker] = Intent(attacker, target) } @JvmStatic - fun requestActiveMeleePressure() { - for (attacker in Repository.players) { - requestActiveMeleePressure(attacker) + fun trackActiveMelee(attacker: Entity, target: Entity) { + if (isActiveMeleeAttacker(attacker, target)) { + activeMeleeAttackers[attacker] = target + } else { + activeMeleeAttackers.remove(attacker) } - for (attacker in Repository.renderableNpcs) { - requestActiveMeleePressure(attacker) + } + + @JvmStatic + fun untrack(attacker: Entity?) { + if (attacker == null) { + return + } + intents.remove(attacker) + activeMeleeAttackers.remove(attacker) + } + + @JvmStatic + fun requestActiveMeleePressure() { + if (activeMeleeAttackers.isEmpty()) { + return + } + val iterator = activeMeleeAttackers.entries.iterator() + while (iterator.hasNext()) { + val entry = iterator.next() + val attacker = entry.key + val target = entry.value + if (!isActiveMeleeAttacker(attacker, target)) { + intents.remove(attacker) + iterator.remove() + continue + } + if (shouldMaintainMeleePressure(attacker, target)) { + intents[attacker] = Intent(attacker, target) + } } } @@ -82,6 +113,7 @@ object CombatMovementIntents { @JvmStatic fun clear() { intents.clear() + activeMeleeAttackers.clear() } @JvmStatic @@ -94,11 +126,14 @@ object CombatMovementIntents { if (attacker.locks.isMovementLocked) { return false } - if (CombatMovementPlanner.movementStepsThisTick(target) == 0) { + val targetSteps = CombatMovementPlanner.movementStepsThisTick(target) + if (targetSteps == 0) { return false } - val predictedTargetLocation = CombatMovementPlanner.predictedTargetLocation(target) - val attackTiles = CombatMovementPlanner.candidateAttackTiles(attacker, target, predictedTargetLocation) + val predictedTargetLocation = CombatMovementPlanner.predictTargetLocations(target, targetSteps).lastOrNull() + ?: target.location + val borderTiles = CombatMovementPlanner.borderTiles(target, predictedTargetLocation, attacker.size()) + val attackTiles = borderTiles.filter { RegionManager.isTeleportPermitted(it) }.ifEmpty { borderTiles } if (attacker.location in attackTiles) { return false } @@ -106,15 +141,18 @@ object CombatMovementIntents { return projectedAttackerLocation !in attackTiles } - private fun requestActiveMeleePressure(attacker: Entity) { + private fun isActiveMeleeAttacker(attacker: Entity, target: Entity): Boolean { + if (!attacker.isActive || !target.isActive || attacker.location == null || target.location == null) { + return false + } + if (attacker.locks.isMovementLocked || attacker.location.z != target.location.z) { + return false + } + if (attacker is NPC && attacker.isNeverWalks) { + return false + } val pulse = attacker.properties.combatPulse - val target = pulse.getVictim() ?: return - if (!pulse.isAttacking || pulse.style != CombatStyle.MELEE) { - return - } - if (shouldMaintainMeleePressure(attacker, target)) { - request(attacker, target) - } + return pulse.getVictim() === target && pulse.isAttacking && pulse.style == CombatStyle.MELEE } private fun resolve( @@ -138,7 +176,7 @@ object CombatMovementIntents { return } - val candidates = movementDestinationsFor(attacker, target) + val candidates = movementDestinationsFor(attacker, target, targetLocation) val standingOnCandidate = candidates.any { it.location == attacker.location } if (standingOnCandidate && canAttackFrom(attacker, target, attacker.location, targetLocation)) { attacker.walkingQueue.reset() @@ -209,11 +247,11 @@ object CombatMovementIntents { } } - private fun movementDestinationsFor(attacker: Entity, target: Entity): List { + private fun movementDestinationsFor(attacker: Entity, target: Entity, targetLocation: Location): List { val pathfinder = pathfinderFor(attacker) if (attacker is NPC && pathfinder === Pathfinder.DUMB) { val destinations = if (occupiedTilesOverlap(attacker, target)) { - CombatMovementPlanner.candidateAttackTiles(attacker, target, targetLocationFor(attacker, target)) + CombatMovementPlanner.candidateAttackTiles(attacker, target, targetLocation) } else { dumbNpcAttackDestinations(attacker, target) } @@ -222,7 +260,6 @@ object CombatMovementIntents { } } - val targetLocation = targetLocationFor(attacker, target) val destinations = CombatMovementPlanner.candidateAttackTiles(attacker, target, targetLocation).map { MovementDestination(it, pathfinder, allowPartialPath = false) } @@ -521,8 +558,12 @@ object CombatMovementIntents { if (attacker.location == destination.location) { return null } + val directPath = directPathTo(attacker, destination) + if (directPath != null) { + return directPath + } if (destination.pathfinder === Pathfinder.SMART && - !SmartPathfinder.canAttempt(attacker.location, destination.location) + !RsmodPathfinder.canAttempt(attacker.location, destination.location) ) { return null } @@ -545,6 +586,38 @@ object CombatMovementIntents { return CandidatePath(steps, projected) } + private fun directPathTo(attacker: Entity, destination: MovementDestination): CandidatePath? { + if (attacker.size() != 1 || destination.node !is Location || attacker.location.z != destination.location.z) { + return null + } + val maxSteps = movementStepsFor(attacker) + val steps = ArrayList(maxSteps) + var current = attacker.location + var distance = 0 + while (current != destination.location) { + if (++distance > MAX_DIRECT_COMBAT_PATH_DISTANCE) { + return null + } + val direction = Direction.getDirection(current, destination.location) ?: return null + if (!direction.canMoveFrom(current.z, current.x, current.y, RegionManager::getClippingFlag)) { + return null + } + val next = current.transform(direction) + if (!RegionManager.isTeleportPermitted(next)) { + return null + } + if (steps.size < maxSteps) { + steps.add(Point(next.x, next.y, direction, direction.stepX, direction.stepY)) + } + current = next + } + if (steps.isEmpty()) { + return null + } + val projected = steps.last().let { Location.create(it.x, it.y, attacker.location.z) } + return CandidatePath(steps, projected) + } + 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) @@ -560,17 +633,23 @@ object CombatMovementIntents { if (!isSuccessful || isMoveNear) { return false } - val terminal = points.lastOrNull() ?: return false + val terminal = points.peekLast() ?: return false return terminal.x == destination.x && terminal.y == destination.y } private fun immediateMovementSteps(attacker: Entity, path: Path): List { - val points = path.points.filter { it.x != attacker.location.x || it.y != attacker.location.y } - if (points.isEmpty()) { - return emptyList() + val maxSteps = movementStepsFor(attacker) + val steps = ArrayList(maxSteps) + for (point in path.points) { + if (point.x == attacker.location.x && point.y == attacker.location.y) { + continue + } + steps.add(point) + if (steps.size >= maxSteps) { + break + } } - val steps = movementStepsFor(attacker).coerceAtMost(points.size) - return points.take(steps) + return steps } private fun walkPath(attacker: Entity, path: CandidatePath) { 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 bebb3a721..ea4ae2986 100644 --- a/Server/src/main/core/game/node/entity/combat/CombatMovementPlanner.kt +++ b/Server/src/main/core/game/node/entity/combat/CombatMovementPlanner.kt @@ -3,6 +3,7 @@ package core.game.node.entity.combat import core.ServerConstants import core.game.node.entity.Entity import core.game.node.entity.player.Player +import core.game.world.map.Direction import core.game.world.map.Location import core.game.world.map.Point import core.game.world.map.RegionManager @@ -50,17 +51,22 @@ object CombatMovementPlanner { if (maxSteps <= 0) { return emptyList() } - val queued = movementPoints(target) + val queued = movementPoints(target, 2) if (queued.isEmpty()) { return emptyList() } val steps = if (canMoveTwoStepsThisTick(target, queued)) maxSteps.coerceAtMost(2) else 1 - return queued.take(steps).map { Location.create(it.x, it.y, target.location.z) } + 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)) + } + return predicted } @JvmStatic fun movementStepsThisTick(target: Entity): Int { - val queued = movementPoints(target) + val queued = movementPoints(target, 2) if (queued.isEmpty()) { return 0 } @@ -94,6 +100,10 @@ object CombatMovementPlanner { } private fun canInteractFrom(attacker: Entity, location: Location, target: Entity, targetLocation: Location): Boolean { + if (attacker.size() == 1 && target.size() == 1) { + val direction = Direction.getDirection(location, targetLocation) ?: return false + return direction.canMoveFrom(location.z, location.x, location.y, RegionManager::getClippingFlag) + } return Pathfinder.canInteract( location.x, location.y, @@ -125,8 +135,18 @@ object CombatMovementPlanner { return border.toList() } - private fun movementPoints(target: Entity): List { - return target.walkingQueue.queue.filter { it.direction != null } + private fun movementPoints(target: Entity, limit: Int): List { + val points = ArrayList(limit) + for (point in target.walkingQueue.queue) { + if (point.direction == null) { + continue + } + points.add(point) + if (points.size >= limit) { + break + } + } + return points } private fun canMoveTwoStepsThisTick(target: Entity, queued: List): Boolean { 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 984f09c28..2a9ce3295 100644 --- a/Server/src/main/core/game/node/entity/combat/CombatPulse.kt +++ b/Server/src/main/core/game/node/entity/combat/CombatPulse.kt @@ -216,6 +216,9 @@ class CombatPulse( } return false } + if (style == CombatStyle.MELEE) { + CombatMovementIntents.trackActiveMelee(attacker, target) + } val type = canInteract() if (type == InteractionType.STILL_INTERACT) { if (shouldMaintainMeleePressure()) { @@ -378,6 +381,7 @@ class CombatPulse( override fun stop() { super.stop() + CombatMovementIntents.untrack(entity) entity!!.setAttribute("combat-stop", GameWorld.ticks) if (victim != null) { lastVictim = victim