Merge branch 'combat-rewrites' into 'master'

Movement rewrite

Closes #1137, #1954, #1959, #2078, #2437, and #2501

See merge request 2009scape/2009scape!2362
This commit is contained in:
dam 2026-08-01 00:01:47 +03:00
commit 1739b39d16
38 changed files with 7831 additions and 2465 deletions

View file

@ -63,6 +63,12 @@
<version>[1.4.0,)</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.rsmod</groupId>
<artifactId>rsmod-pathfinder</artifactId>
<version>4.2.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>

View file

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

View file

@ -1,6 +1,8 @@
package content.global.skill.thieving;
import core.game.event.ResourceProducedEvent;
import core.game.node.entity.combat.CombatPulse;
import core.game.node.entity.combat.CombatReach;
import core.game.node.entity.combat.ImpactHandler;
import core.game.node.entity.skill.SkillPulse;
import core.game.node.entity.skill.Skills;
@ -11,6 +13,8 @@ import core.game.node.scenery.Scenery;
import core.game.node.scenery.SceneryBuilder;
import core.game.world.GameWorld;
import core.game.world.map.RegionManager;
import core.game.world.map.path.Path;
import core.game.world.map.path.Pathfinder;
import core.game.world.update.flag.context.Animation;
import core.tools.RandomFunction;
import core.tools.StringUtils;
@ -156,15 +160,85 @@ public final class StallThiefPulse extends SkillPulse<Scenery> {
player.sendMessage("A higher power smites you");
return false;
}
for (NPC npc : RegionManager.getLocalNpcs(player.getLocation(), 8)) {
if (!npc.getProperties().getCombatPulse().isAttacking() && (npc.getId() == 32 || npc.getId() == 2236)) {
npc.sendChat("Hey! Get your hands off there!");
npc.getProperties().getCombatPulse().attack(player);
return false;
}
NPC guard = findGuardForFailedSteal(player);
if (guard != null) {
guard.sendChat("Hey! Get your hands off there!");
guard.getProperties().getCombatPulse().attack(player);
return false;
}
}
return true;
}
/**
* Finds the guard that busts a failed steal. Guards whose attack can actually
* connect are preferred: NPC combat chase uses dumb pathing, so a guard on the far
* side of a stall (or inside the guardhouse next to the Ardougne market) would
* shout and then silently never reach the player. If no guard can reach, any guard
* within detection range still catches the thief - the steal must not succeed just
* because the witness is boxed in.
* @param player the thieving player.
* @return The catching guard, or {@code null} if no guard is in range.
*/
public static NPC findGuardForFailedSteal(Player player) {
NPC catcher = findCatchingGuard(player);
return catcher != null ? catcher : findWitnessGuard(player);
}
/**
* Finds a guard within detection range whose attack can actually connect.
* @param player the thieving player.
* @return The catching guard, or {@code null} if no guard can reach the player.
*/
private static NPC findCatchingGuard(Player player) {
for (NPC npc : RegionManager.getLocalNpcs(player.getLocation(), 8)) {
if (npc.getProperties().getCombatPulse().isAttacking() || (npc.getId() != 32 && npc.getId() != 2236)) {
continue;
}
if (canReachThief(npc, player)) {
return npc;
}
}
return null;
}
/**
* Finds any guard within detection range that witnesses the steal, even if it
* cannot reach the player. A guard already fighting someone else is too busy to
* notice, but one already (fruitlessly) chasing this player keeps catching them.
* @param player the thieving player.
* @return The witnessing guard, or {@code null} if none is in range.
*/
private static NPC findWitnessGuard(Player player) {
for (NPC npc : RegionManager.getLocalNpcs(player.getLocation(), 8)) {
if (npc.getId() != 32 && npc.getId() != 2236) {
continue;
}
CombatPulse pulse = npc.getProperties().getCombatPulse();
if (pulse.isAttacking() && pulse.getVictim() != player) {
continue;
}
return npc;
}
return null;
}
/**
* Checks whether the guard's attack can actually connect: either it is already in
* melee reach, or its (dumb) chase route reaches the player.
* @param npc the guard.
* @param player the thieving player.
* @return {@code True} if the guard can reach the player.
*/
private static boolean canReachThief(NPC npc, Player player) {
if (CombatReach.canMelee(npc, player, 1)) {
return true;
}
if (npc.getLocks().isMovementLocked()) {
return false;
}
Path path = Pathfinder.find(npc, player, true, Pathfinder.DUMB);
return path.isSuccessful() && !path.isMoveNear();
}
}

View file

@ -235,6 +235,7 @@ class ServerConstants {
arrayOf(Location.create(2722, 4886, 0), "quest the golem 1"),
arrayOf(Location.create(2704, 5349, 0), "dorgeshuun", "dorg"),
arrayOf(Location.create(2711, 10132, 0), "brine rats"),
arrayOf(Location.create(2591, 4320, 0), "puro puro", "puro-puro", "puropuro", "puro", "impling maze"),
arrayOf(Location.create(2328, 3677, 0), "piscatoris"),
arrayOf(Location.create(2660, 3158, 0), "fishing trawler", "trawler"),
arrayOf(Location.create(2800, 3667, 0), "mountain camp"),

View file

@ -162,6 +162,29 @@ object InteractionListeners {
return destinationOverrides["$type:$option"]
}
private fun getOptionHandlerDestination(id: Int, option: String, node: Node): ((Entity, Node) -> Location?)? {
val handlers = ArrayList<OptionHandler>(2)
Option.defaultHandler(node, id, option)?.let { handlers.add(it) }
node.interaction?.options
?.firstOrNull { it != null && it.name.equals(option, ignoreCase = true) }
?.handler
?.takeIf { it !in handlers }
?.let { handlers.add(it) }
if (handlers.isEmpty()) {
return null
}
return { entity, target ->
handlers.firstNotNullOfOrNull { it.getDestination(entity, target) }
}
}
private fun getDestinationOverride(type: Int, id: Int, option: String, node: Node): ((Entity, Node) -> Location?)? {
return getOverride(type, id, option)
?: getOverride(type, node.id)
?: getOverride(type, option.toLowerCase())
?: getOptionHandlerDestination(id, option, node)
}
@JvmStatic
fun run(id: Int, player: Player, node: Node, isEquip: Boolean): Boolean{
player.scripts.removeWeakScripts()
@ -255,7 +278,7 @@ object InteractionListeners {
return true
}
val destOverride = getOverride(type.ordinal, id, option) ?: getOverride(type.ordinal,node.id) ?: getOverride(type.ordinal,option.toLowerCase())
val destOverride = getDestinationOverride(type.ordinal, id, option, node)
if(type != IntType.ITEM && !isInstant(method)) {
if(player.locks.isMovementLocked) return false

File diff suppressed because it is too large Load diff

View file

@ -253,15 +253,14 @@ class ScriptProcessor(val entity: Entity) {
is Scenery -> {
val basicPath = Pathfinder.find(entity, interactTarget)
val path = basicPath.points.lastOrNull()
if (basicPath.isMoveNear) {
target.location
return
}
if (path == null) {
if (!basicPath.isSuccessful) {
clearScripts(entity)
return
}
Location.create(path.x, path.y, entity.location.z)
if (basicPath.isMoveNear) {
return
}
path?.let { Location.create(it.x, it.y, entity.location.z) } ?: entity.location
}
is GroundItem -> DestinationFlag.ITEM.getDestination(entity, interactTarget)
else -> target.location

View file

@ -280,7 +280,7 @@ public abstract class Entity extends Node {
impactHandler.getImpactQueue().clear();
impactHandler.setDisabledTicks(10);
timers.onEntityDeath();
removeAttribute("combat-time");
clearCombatDeathState(killer);
face(null);
//Check if it's a Loar shade and transform back into the shadow version.
if(this.getId() == 1240 || this.getId() == 1241){
@ -288,6 +288,37 @@ public abstract class Entity extends Node {
}
}
private void clearCombatDeathState(Entity killer) {
Object attacker = getAttribute("combat-attacker");
Object aggressor = getAttribute("aggressor");
properties.getCombatPulse().stop();
removeAttribute("combat-time");
removeAttribute("combat-attacker");
removeAttribute("aggressor");
clearCombatReference(killer);
if (attacker instanceof Entity) {
clearCombatReference((Entity) attacker);
}
if (aggressor instanceof Entity) {
clearCombatReference((Entity) aggressor);
}
}
private void clearCombatReference(Entity entity) {
if (entity == null) {
return;
}
if (entity.getAttribute("combat-attacker") == this) {
entity.removeAttribute("combat-attacker");
}
if (entity.getAttribute("aggressor") == this) {
entity.removeAttribute("aggressor");
}
if (entity.getProperties().getCombatPulse().getVictim() == this) {
entity.getProperties().getCombatPulse().stop();
}
}
/**
* Updates the location of an entity.
* @param last the last location.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,240 @@
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
import core.game.world.map.path.Pathfinder
/** Plans combat-specific chase targets without mutating walking queues. */
object CombatMovementPlanner {
data class Plan(
val targetLocation: Location,
val attackTile: Location?,
)
@JvmStatic
fun plan(attacker: Entity, target: Entity): Plan {
val targetLocation = predictedTargetLocation(target)
return Plan(
targetLocation = targetLocation,
attackTile = chooseTargetBorderTile(attacker, target, targetLocation),
)
}
@JvmStatic
fun exceedsCombatChaseDistance(attacker: Entity, target: Entity): Boolean {
if (attacker.location.z != target.location.z) {
return true
}
val targetTile = target.getClosestOccupiedTile(attacker.location)
return attacker.location.getDistance(targetTile) >
ServerConstants.MAX_PATHFIND_DISTANCE * 2.0
}
@JvmStatic
fun predictedTargetLocation(target: Entity): Location {
return predictedTargetLocation(target, 2)
}
@JvmStatic
fun predictedTargetLocation(target: Entity, maxSteps: Int): Location {
return predictedMovementLocation(target, maxSteps) ?: target.location
}
@JvmStatic
fun predictTargetLocations(target: Entity): List<Location> {
return predictTargetLocations(target, movementStepsThisTick(target))
}
@JvmStatic
fun predictTargetLocations(target: Entity, maxSteps: Int): List<Location> {
if (maxSteps <= 0) {
return emptyList()
}
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)
val predicted = ArrayList<Location>(steps)
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 {
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
fun chooseTargetBorderTile(attacker: Entity, target: Entity): Location? {
return chooseTargetBorderTile(attacker, target, predictedTargetLocation(target))
}
@JvmStatic
fun chooseTargetBorderTile(
attacker: Entity,
target: Entity,
targetLocation: Location,
): Location? {
return candidateAttackTiles(attacker, target, targetLocation).firstOrNull()
}
@JvmStatic
fun candidateAttackTiles(
attacker: Entity,
target: Entity,
targetLocation: Location,
): List<Location> {
val candidates = borderTiles(target, targetLocation, attacker.size())
val walkable = candidates.filter { RegionManager.isTeleportPermitted(it) }
val attackable = walkable.filter { canInteractFrom(attacker, it, target, targetLocation) }
return (attackable.ifEmpty { walkable.ifEmpty { candidates } }).sortedWith(
compareBy<Location> {
it.getDistance(attacker.location)
}
.thenBy { it.x }
.thenBy { it.y }
)
}
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,
attacker.size(),
targetLocation.x,
targetLocation.y,
target.size(),
target.size(),
0,
targetLocation.z,
) { z, x, y ->
RegionManager.getClippingFlag(z, x, y)
}
}
@JvmStatic
fun borderTiles(target: Entity, targetLocation: Location, attackerSize: Int): List<Location> {
val targetSize = target.size()
val border = LinkedHashSet<Location>()
val plane = targetLocation.z
val minOffset = -attackerSize + 1
val maxOffset = targetSize - 1
for (offset in minOffset..maxOffset) {
border.add(
Location.create(targetLocation.x - attackerSize, targetLocation.y + offset, plane)
)
border.add(
Location.create(targetLocation.x + targetSize, targetLocation.y + offset, plane)
)
border.add(
Location.create(targetLocation.x + offset, targetLocation.y - attackerSize, plane)
)
border.add(
Location.create(targetLocation.x + offset, targetLocation.y + targetSize, plane)
)
}
return border.toList()
}
@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
}
if (first == null) {
first = point
} else {
second = point
break
}
}
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 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
}
}

View file

@ -3,7 +3,6 @@ package core.game.node.entity.combat
import content.global.ame.RandomEventNPC
import content.global.handlers.item.equipment.special.SalamanderSwingHandler
import core.game.container.impl.EquipmentContainer
import core.game.interaction.MovementPulse
import core.game.node.Node
import core.game.node.entity.Entity
import core.game.node.entity.combat.equipment.WeaponInterface
@ -17,7 +16,6 @@ import core.game.world.GameWorld
import core.game.world.update.flag.context.Animation
import core.tools.RandomFunction
import core.api.*
import core.game.interaction.DestinationFlag
import core.game.system.timer.impl.*
/**
@ -93,11 +91,6 @@ class CombatPulse(
*/
private var combatTimeOut = 0
/**
* The movement handling pulse.
*/
private val movement: MovementPulse
/**
* The last attack sent.
*/
@ -120,9 +113,15 @@ class CombatPulse(
return true
}
if (!interactable()) {
return if (entity.walkingQueue.isMoving) {
return if (entity.walkingQueue.isMoving || entity.walkingQueue.hasPath()) {
false
} else combatTimeOut++ > entity.properties.combatTimeOut
} else {
val timedOut = combatTimeOut++ > entity.properties.combatTimeOut
if (timedOut && entity is Player && !CombatMovementPlanner.hasMovementStepThisTick(victim!!)) {
CombatMovementIntents.stopUnreachableCombat(entity)
}
timedOut
}
}
combatTimeOut = 0
entity.face(victim)
@ -195,29 +194,44 @@ class CombatPulse(
* @return `True` if so.
*/
private fun interactable(): Boolean {
if (victim == null) {
return false
}
if (entity is NPC && victim is Player && entity.isHidden(victim as Player?)) {
val attacker = entity ?: return false
val target = victim ?: return false
if (attacker is NPC && target is Player && attacker.isHidden(target)) {
stop()
return false
}
if (victim is NPC && entity is Player && (victim as NPC).isHidden(entity as Player?)) {
if (target is NPC && attacker is Player && target.isHidden(attacker)) {
stop()
return false
}
if (entity is NPC && !entity.asNpc().canStartCombat(victim)) {
if (attacker is NPC && !attacker.asNpc().canStartCombat(target)) {
stop()
return false
}
if (CombatMovementPlanner.exceedsCombatChaseDistance(attacker, target)) {
if (attacker is Player && !CombatMovementPlanner.hasMovementStepThisTick(target)) {
CombatMovementIntents.stopUnreachableCombat(attacker)
} else {
stop()
}
return false
}
if (style == CombatStyle.MELEE) {
CombatMovementIntents.trackActiveMelee(attacker, target)
}
val type = canInteract()
if (type == InteractionType.STILL_INTERACT) {
if (style == CombatStyle.MELEE && !attacker.locks.isMovementLocked &&
CombatMovementIntents.shouldMaintainMeleePressure(attacker, target)
) {
CombatMovementIntents.request(attacker, target)
}
return true
}
if (entity == null || victim == null || entity.locks.isMovementLocked) {
if (attacker.locks.isMovementLocked) {
return false
}
movement.updatePath()
CombatMovementIntents.request(attacker, target)
return type == InteractionType.MOVE_INTERACT
}
@ -300,8 +314,10 @@ class CombatPulse(
victim.scripts.removeWeakScripts()
}
if (!isAttacking) {
if (!isAttacking)
entity.pulseManager.run(this)
if (style == CombatStyle.MELEE) {
CombatMovementIntents.trackActiveMelee(entity, victim)
}
}
@ -311,8 +327,6 @@ class CombatPulse(
*/
fun setVictim(victim: Node?) {
super.addNodeCheck(1, victim)
movement.setLast(null)
movement.setDestination(victim)
this.victim = victim as Entity?
combatTimeOut = 0
}
@ -362,6 +376,7 @@ class CombatPulse(
override fun stop() {
super.stop()
CombatMovementIntents.untrack(entity)
entity!!.setAttribute("combat-stop", GameWorld.ticks)
if (victim != null) {
lastVictim = victim
@ -472,11 +487,4 @@ class CombatPulse(
}
}
init {
movement = object : MovementPulse(entity, null) {
override fun pulse(): Boolean {
return false
}
}
}
}

View file

@ -0,0 +1,217 @@
package core.game.node.entity.combat
import core.game.container.impl.EquipmentContainer
import core.game.node.entity.Entity
import core.game.node.entity.npc.NPC
import core.game.node.entity.player.Player
import core.game.world.map.Direction
import core.game.world.map.Location
import core.game.world.map.RegionManager.getClippingFlag
import core.game.world.map.path.Pathfinder
import core.game.world.map.path.Pathfinder.*
import core.game.world.map.path.RsmodPathfinder
/** Shared combat reach calculations. */
object CombatReach {
private const val BORK_LEGION_ID = 7135
@JvmStatic
fun isUsingHalberd(entity: Entity): Boolean {
if (entity is Player) {
val weapon = entity.equipment[EquipmentContainer.SLOT_WEAPON]
if (weapon != null) {
return weapon.id in 3190..3204 || weapon.id == 6599
}
} else if (entity is NPC) {
return entity.id == 8612
}
return false
}
@JvmStatic
fun meleeDistance(entity: Entity): Int {
return if (hasExtendedMeleeReach(entity)) 2 else 1
}
@JvmStatic
fun hasExtendedMeleeReach(entity: Entity): Boolean {
return isUsingHalberd(entity) || entity is NPC && entity.id == BORK_LEGION_ID
}
@JvmStatic
fun canMelee(entity: Entity, victim: Entity?, distance: Int): Boolean {
val e = entity.location
if (victim == null) {
return false
}
if (entity.id == BORK_LEGION_ID && entity.location.withinDistance(victim.location, 2)) {
return true
}
if (occupiedAreasOverlap(entity, victim)) {
return false
}
val x = victim.location.x
val y = victim.location.y
val size = entity.size()
if (distance == 1) {
val victimSize = victim.size()
fun adjacent(ex: Int, ey: Int): Boolean =
Pathfinder.isStandingIn(ex, ey, 1, 1, x, y, victimSize, victimSize)
for (i in 0 until size) {
if (adjacent(e.x - 1, e.y + i)) return true
if (adjacent(e.x + size, e.y + i)) return true
if (adjacent(e.x + i, e.y - 1)) return true
if (adjacent(e.x + i, e.y + size)) return true
}
return victim.getSwingHandler(false).type == CombatStyle.MELEE &&
e.withinDistance(
victim.location,
1,
) &&
victim.properties.combatPulse.getVictim() === entity &&
entity.index < victim.index
}
return entity.centerLocation.withinDistance(
victim.centerLocation,
distance + (size shr 1) + (victim.size() shr 1),
)
}
@JvmStatic
fun hasMeleeReach(
attackerLocation: Location,
attackerSize: Int,
targetLocation: Location,
targetSize: Int,
): Boolean {
return RsmodPathfinder.canReach(
attackerLocation.x,
attackerLocation.y,
attackerSize,
targetLocation.x,
targetLocation.y,
targetSize,
targetSize,
0,
-1,
0,
attackerLocation.z,
null,
)
}
private fun occupiedAreasOverlap(first: Entity, second: Entity): Boolean {
return Pathfinder.isStandingIn(
first.location.x,
first.location.y,
first.size(),
first.size(),
second.location.x,
second.location.y,
second.size(),
second.size(),
)
}
@JvmStatic
fun combatDistance(entity: Entity, victim: Entity, rawDistance: Int): Int {
var distance = rawDistance
if (entity is NPC && entity.definition.combatDistance > 0) {
distance = entity.definition.combatDistance
}
return (entity.size() shr 1) + (victim.size() shr 1) + distance
}
@JvmStatic
fun canReach(entity: Entity, victim: Entity, distance: Int): Boolean {
return victim.centerLocation.withinDistance(entity.centerLocation, distance)
}
@JvmStatic
fun canStepTowards(entity: Entity, victim: Entity): InteractionType {
val closestVictimTile = victim.getClosestOccupiedTile(entity.location)
val closestEntityTile = entity.getClosestOccupiedTile(closestVictimTile)
val dir =
closestEntityTile.deriveDirection(closestVictimTile)
?: return InteractionType.STILL_INTERACT
var next = closestEntityTile
// A fixed-direction walk can pass beside an oblique target without converging, so
// limit the skipped gap to the number of steps available from the starting distance.
val maxSkipSteps = next.getDistance(closestVictimTile).toInt() + 1
for (i in 0 until maxSkipSteps) {
if (next.getDistance(closestVictimTile) <= 3) {
break
}
next = next.transform(dir)
}
if (next.getDistance(closestVictimTile) > 3) {
return InteractionType.STILL_INTERACT
}
var result = InteractionType.STILL_INTERACT
val maxIterations = next.getDistance(closestVictimTile).toInt()
for (i in 0 until maxIterations) {
next = next.transform(dir)
result = checkStepInterval(dir, next)
if (result == InteractionType.NO_INTERACT) {
break
}
}
return result
}
private fun checkStepInterval(dir: Direction, next: Location): InteractionType {
val components = next.getStepComponents(dir)
when (dir) {
Direction.NORTH ->
if (getClippingFlag(next) and PREVENT_NORTH != 0) return InteractionType.NO_INTERACT
Direction.EAST ->
if (getClippingFlag(next) and PREVENT_EAST != 0) return InteractionType.NO_INTERACT
Direction.SOUTH ->
if (getClippingFlag(next) and PREVENT_SOUTH != 0) return InteractionType.NO_INTERACT
Direction.WEST ->
if (getClippingFlag(next) and PREVENT_WEST != 0) return InteractionType.NO_INTERACT
Direction.NORTH_EAST -> {
if (
getClippingFlag(components[0]) and PREVENT_EAST != 0 ||
getClippingFlag(components[1]) and PREVENT_NORTH != 0 ||
getClippingFlag(next) and PREVENT_NORTHEAST != 0
)
return InteractionType.NO_INTERACT
}
Direction.NORTH_WEST -> {
if (
getClippingFlag(components[0]) and PREVENT_WEST != 0 ||
getClippingFlag(components[1]) and PREVENT_NORTH != 0 ||
getClippingFlag(next) and PREVENT_NORTHWEST != 0
)
return InteractionType.NO_INTERACT
}
Direction.SOUTH_EAST -> {
if (
getClippingFlag(components[0]) and PREVENT_EAST != 0 ||
getClippingFlag(components[1]) and PREVENT_SOUTH != 0 ||
getClippingFlag(next) and PREVENT_SOUTHEAST != 0
)
return InteractionType.NO_INTERACT
}
Direction.SOUTH_WEST -> {
if (
getClippingFlag(components[0]) and PREVENT_WEST != 0 ||
getClippingFlag(components[1]) and PREVENT_SOUTH != 0 ||
getClippingFlag(next) and PREVENT_SOUTHWEST != 0
)
return InteractionType.NO_INTERACT
}
}
return InteractionType.STILL_INTERACT
}
}

View file

@ -1,51 +1,47 @@
package core.game.node.entity.combat
import content.global.skill.summoning.familiar.Familiar
import core.api.log
import core.api.playGlobalAudio
import core.game.component.Component
import core.game.container.impl.EquipmentContainer
import core.game.node.Node
import core.game.node.entity.Entity
import core.game.node.entity.combat.equipment.*
import core.game.node.entity.combat.equipment.ArmourSet
import core.game.node.entity.combat.equipment.DegradableEquipment
import core.game.node.entity.combat.equipment.WeaponInterface
import core.game.node.entity.npc.NPC
import core.game.node.entity.player.Player
import core.game.node.entity.player.link.audio.Audio
import core.game.node.entity.player.link.prayer.PrayerType
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.Direction
import core.game.world.map.Location
import core.game.world.map.RegionManager
import core.game.world.map.RegionManager.getClippingFlag
import core.game.world.map.path.Pathfinder
import core.game.world.map.path.Pathfinder.*
import core.game.world.update.flag.context.Animation
import core.tools.RandomFunction
import core.game.system.config.ItemConfigParser
import core.game.world.map.path.RsmodPathfinder
import core.game.world.update.flag.context.Animation
import core.tools.Log
import core.tools.RandomFunction
import org.rs09.consts.Sounds
import java.util.*
import kotlin.math.floor
/**
* Handles a combat swing.
*
* @author Emperor
* @author Ceikry - Kotlin refactoring, general cleanup
* @author Player Name - converted `flags` to ArrayList
*/
abstract class CombatSwingHandler(var type: CombatStyle?) {
var flags: ArrayList<SwingHandlerFlag> = ArrayList(SwingHandlerFlag.values().size)
constructor(type: CombatStyle?, vararg flags: SwingHandlerFlag) : this(type) {
this.flags = arrayListOf(*flags)
}
/**
* The mapping of the special attack handlers.
*/
/** The mapping of the special attack handlers. */
private var specialHandlers: MutableMap<Int, CombatSwingHandler?>? = null
/**
* Starts the combat swing.
*
* @param entity The attacking entity.
* @param victim The victim.
* @param state The battle state instance.
@ -55,6 +51,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
/**
* Handles the impact of the combat swing (victim getting hit).
*
* @param entity The attacking entity.
* @param victim The victim.
* @param state The battle state instance.
@ -63,6 +60,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
/**
* Visualizes the impact itself (end animation, end GFX, ...)
*
* @param entity The attacking entity.
* @param victim The victim.
* @param state The battle state instance.
@ -71,6 +69,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
/**
* Calculates the maximum accuracy of the entity.
*
* @param entity The entity.
* @return The maximum accuracy value.
*/
@ -78,6 +77,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
/**
* Calculates the maximum strength of the entity.
*
* @param entity The entity.
* @param victim The victim.
* @param modifier The modifier.
@ -87,6 +87,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
/**
* Calculates the maximum defence of the entity.
*
* @param victim The entity.
* @param attacker The entity to defend against.
* @return The maximum defence value.
@ -95,6 +96,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
/**
* Gets the void set multiplier.
*
* @param e The entity.
* @param skillId The skill id.
* @return The multiplier.
@ -103,6 +105,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
/**
* Visualizes the combat swing (start animation, GFX, projectile, ...)
*
* @param entity The attacking entity.
* @param victim The victim.
* @param state The battle state instance.
@ -113,6 +116,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
/**
* Method called when the impact method got called.
*
* @param entity The attacking entity.
* @param victim The victim.
* @param state The battle state.
@ -139,6 +143,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
/**
* Gets the currently worn armour set, if any.
*
* @param e The entity.
* @return The armour set worn.
*/
@ -148,6 +153,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
/**
* Checks if the hit will be accurate.
*
* @param entity The entity.
* @param victim The victim.
* @return `True` if the hit is accurate.
@ -158,6 +164,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
/**
* Checks if the hit will be accurate.
*
* @param entity The entity.
* @param victim The victim.
* @param style The combat style used.
@ -169,6 +176,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
/**
* Checks if the hit will be accurate.
*
* @param entity The entity.
* @param victim The victim.
* @param style The combat style (null to ignore prayers).
@ -176,26 +184,38 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
* @param defenceMod The defence modifier.
* @return `True` if the hit is accurate.
*/
fun isAccurateImpact(entity: Entity?, victim: Entity?, style: CombatStyle?, accuracyMod: Double, defenceMod: Double): Boolean {
fun isAccurateImpact(
entity: Entity?,
victim: Entity?,
style: CombatStyle?,
accuracyMod: Double,
defenceMod: Double,
): Boolean {
var mod = 1.0
if (victim == null || style == null) {
return false
}
if (victim is Player && entity is Familiar && victim.prayer[PrayerType.PROTECT_FROM_SUMMONING]) {
if (
victim is Player &&
entity is Familiar &&
victim.prayer[PrayerType.PROTECT_FROM_SUMMONING]
) {
mod = 0.0
}
val attack = calculateAccuracy(entity) * accuracyMod * mod
val defence = calculateDefence(victim, entity) * defenceMod
val chance: Double = if (attack > defence) {
1 - ((defence + 2) / (2 * (attack + 1)))
} else {
attack / (2 * (defence + 1))
}
val chance: Double =
if (attack > defence) {
1 - ((defence + 2) / (2 * (attack + 1)))
} else {
attack / (2 * (defence + 1))
}
return Math.random() < chance
}
/**
* Checks if the entity can execute a combat swing at the victim.
*
* @param entity The entity.
* @param victim The victim.
* @return `True` if so.
@ -206,6 +226,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
/**
* Checks if the victim can be attacked by the entity.
*
* @param entity The attacking entity.
* @param victim The entity being attacked.
* @return `True` if so.
@ -215,15 +236,20 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
return InteractionType.NO_INTERACT
}
if (type == CombatStyle.MELEE) {
if (type == CombatStyle.MELEE && !CombatReach.hasExtendedMeleeReach(entity)) {
val stepType = canStepTowards(entity, victim)
if (stepType != InteractionType.STILL_INTERACT) return stepType
}
val comp = entity.getAttribute("autocast_component",null) as Component?
if((comp != null || type == CombatStyle.MAGIC) && (entity.properties.autocastSpell == null || entity.properties.autocastSpell.spellId == 0) && entity is Player){
val comp = entity.getAttribute("autocast_component", null) as Component?
if (
(comp != null || type == CombatStyle.MAGIC) &&
(entity.properties.autocastSpell == null ||
entity.properties.autocastSpell.spellId == 0) &&
entity is Player
) {
val weapEx = entity.getExtension<Any>(WeaponInterface::class.java) as WeaponInterface?
if(comp != null){
if (comp != null) {
entity.interfaceManager.close(comp)
entity.interfaceManager.openTab(weapEx)
entity.properties.combatPulse.stop()
@ -234,7 +260,12 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
entity.debug("Adjusting attack style")
}
if (entity.location == victim.location) {
return if (entity is Player && victim is Player && entity.clientIndex < victim.clientIndex && victim.properties.combatPulse.getVictim() === entity) {
return if (
entity is Player &&
victim is Player &&
entity.clientIndex < victim.clientIndex &&
victim.properties.combatPulse.getVictim() === entity
) {
InteractionType.STILL_INTERACT
} else InteractionType.NO_INTERACT
}
@ -248,81 +279,12 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
}
protected fun canStepTowards(entity: Entity, victim: Entity): InteractionType {
val closestVictimTile = victim.getClosestOccupiedTile(entity.location)
val closestEntityTile = entity.getClosestOccupiedTile(closestVictimTile)
val dir = closestEntityTile.deriveDirection(closestVictimTile)
?: return InteractionType.STILL_INTERACT //if we cannot derive a direction, it's because both tiles are the same, so hand off control to the main logic which already handles this case
var next = closestEntityTile
//Skip the initial gap in distance if it exists, because standard pathfinding would already stop us before this point if something was between us and the NPC or vice versa.
//A fixed-direction walk can only arrive within its starting distance in steps; on oblique approaches it
//passes beside the target and never converges, so the walk is hard-capped at that many steps.
val maxSkipSteps = next.getDistance(closestVictimTile).toInt() + 1
for (i in 0 until maxSkipSteps) {
if (next.getDistance(closestVictimTile) <= 3) break
next = next.transform(dir)
}
if (next.getDistance(closestVictimTile) > 3) return InteractionType.STILL_INTERACT //never converged (oblique approach), so defer to the range checks instead
var result: InteractionType = InteractionType.STILL_INTERACT
val maxIterations = next.getDistance(closestVictimTile).toInt()
for (i in 0 until maxIterations) { //step towards the target tile, checking if anything would obstruct us on the way, and immediately breaking + returning if it does.
next = next.transform(dir)
result = checkStepInterval(dir, next)
if (result == InteractionType.NO_INTERACT) break
}
return result
}
private fun checkStepInterval(
dir: Direction,
next: Location
): InteractionType {
val components = next.getStepComponents(dir)
when (dir) {
Direction.NORTH -> if (getClippingFlag(next) and PREVENT_NORTH != 0) return InteractionType.NO_INTERACT
Direction.EAST -> if (getClippingFlag(next) and PREVENT_EAST != 0) return InteractionType.NO_INTERACT
Direction.SOUTH -> if (getClippingFlag(next) and PREVENT_SOUTH != 0) return InteractionType.NO_INTERACT
Direction.WEST -> if (getClippingFlag(next) and PREVENT_WEST != 0) return InteractionType.NO_INTERACT
Direction.NORTH_EAST -> {
if (getClippingFlag(components[0]) and PREVENT_EAST != 0
|| getClippingFlag(components[1]) and PREVENT_NORTH != 0
|| getClippingFlag(next) and PREVENT_NORTHEAST != 0
) return InteractionType.NO_INTERACT
}
Direction.NORTH_WEST -> {
if (getClippingFlag(components[0]) and PREVENT_WEST != 0
|| getClippingFlag(components[1]) and PREVENT_NORTH != 0
|| getClippingFlag(next) and PREVENT_NORTHWEST != 0
) return InteractionType.NO_INTERACT
}
Direction.SOUTH_EAST -> {
if (getClippingFlag(components[0]) and PREVENT_EAST != 0
|| getClippingFlag(components[1]) and PREVENT_SOUTH != 0
|| getClippingFlag(next) and PREVENT_SOUTHEAST != 0
) return InteractionType.NO_INTERACT
}
Direction.SOUTH_WEST -> {
if (getClippingFlag(components[0]) and PREVENT_WEST != 0
|| getClippingFlag(components[1]) and PREVENT_SOUTH != 0
|| getClippingFlag(next) and PREVENT_SOUTHWEST != 0
) return InteractionType.NO_INTERACT
}
}
return InteractionType.STILL_INTERACT
return CombatReach.canStepTowards(entity, victim)
}
/**
* Gets the dragonfire message.
*
* @param protection The protection value.
* @param fireName The fire breath name.
* @return The message to send.
@ -345,6 +307,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
/**
* Visualizes the audio.
*
* @param entity the entity.
* @param victim the victim.
* @param state the state.
@ -354,7 +317,11 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
val styleIndex = entity.settings.attackStyleIndex
if (state.weapon != null && state.weapon.item != null) {
val weapon = state.weapon.item
val audios = weapon.definition.getConfiguration<Array<Audio>>(ItemConfigParser.ATTACK_AUDIO, null)
val audios =
weapon.definition.getConfiguration<Array<Audio>>(
ItemConfigParser.ATTACK_AUDIO,
null,
)
if (audios != null) {
var audio: Audio? = null
if (styleIndex < audios.size) {
@ -366,7 +333,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
playGlobalAudio(entity.location, audio.id)
}
} else if (type == CombatStyle.MELEE) {
//plays a punching sound when no weapon is equipped
// plays a punching sound when no weapon is equipped
playGlobalAudio(entity.location, Sounds.HUMAN_ATTACK_2564)
}
} else if (entity is NPC) {
@ -378,24 +345,20 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
/**
* Gets the combat distance.
*
* @param e The entity.
* @param v The victim.
* @param rawDistance The distance.
* @return The actual distance used for combat.
*/
open fun getCombatDistance(e: Entity, v: Entity, rawDistance: Int): Int {
var distance = rawDistance
if (e is NPC) {
if (e.definition.combatDistance > 0) {
distance = e.definition.combatDistance
}
}
return (e.size() shr 1) + (v.size() shr 1) + distance
return CombatReach.combatDistance(e, v, rawDistance)
}
/**
* Formats the hit for the victim. (called as
* victim.getSwingHandler(false).formatHit(victim, hit))
* Formats the hit for the victim. (called as victim.getSwingHandler(false).formatHit(victim,
* hit))
*
* @param victim The entity receiving the hit.
* @param rawHit The hit to format.
* @return The formatted hit.
@ -413,6 +376,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
/**
* Adjusts the battle state object for this combat swing.
*
* @param entity The attacking entity.
* @param victim The victim.
* @param state The battle state.
@ -425,8 +389,11 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
}
entity.sendImpact(state)
victim.checkImpact(state)
//Prevents lumbridge dummies from dying (true to how rs3 / 2009scape in 2009 does it)
if (victim.id == 4474 && type == CombatStyle.MAGIC || victim.id == 7891 && type == CombatStyle.MELEE) {
// Prevents lumbridge dummies from dying (true to how rs3 / 2009scape in 2009 does it)
if (
victim.id == 4474 && type == CombatStyle.MAGIC ||
victim.id == 7891 && type == CombatStyle.MELEE
) {
EXPERIENCE_MOD = 0.1
victim.fullRestore()
if (state.estimatedHit >= 15) {
@ -439,7 +406,8 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
if (victim.id == 757) {
EXPERIENCE_MOD = 0.01
}
// Recursively adjustBattleState targets so that multi-target attacks have protection prayers applied.
// Recursively adjustBattleState targets so that multi-target attacks have protection
// prayers applied.
if (state.targets != null && state.targets.isNotEmpty()) {
if (!(state.targets.size == 1 && state.targets[0] == state)) {
for (s in state.targets) {
@ -467,7 +435,10 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
}
}
if (victim is NPC) {
if (victim.properties.protectStyle != null && state.style == victim.properties.protectStyle) {
if (
victim.properties.protectStyle != null &&
state.style == victim.properties.protectStyle
) {
state.neutralizeHits()
}
}
@ -475,21 +446,31 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
/**
* Adds the experience for the current combat swing.
*
* @param entity The attacking entity.
* @param victim The victim.
* @param state The battle state.
*/
open fun addExperience(entity: Entity?, victim: Entity?, state: BattleState?) {
if (entity == null || (victim is Player && entity is Player && entity.asPlayer().ironmanManager.isIronman)) {
if (
entity == null ||
(victim is Player && entity is Player && entity.asPlayer().ironmanManager.isIronman)
) {
return
}
var player: Player
var attStyle: Int
when(entity)
{
is Familiar -> {player = entity.owner; attStyle = entity.attackStyle}
is Player -> {player = entity; attStyle = entity.properties.attackStyle.style}
when (entity) {
is Familiar -> {
player = entity.owner
attStyle = entity.attackStyle
}
is Player -> {
player = entity
attStyle = entity.properties.attackStyle.style
}
else -> return
}
if (victim is NPC) EXPERIENCE_MOD *= victim.behavior.getXpMultiplier(victim, player)
@ -538,20 +519,26 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
}
/**
* Hook for operations that conceptually happen during swing but could mess with experience granting logic if they
* happened earlier.
* Hook for operations that conceptually happen during swing but could mess with experience
* granting logic if they happened earlier.
*/
open fun postSwing(entity: Entity?, victim: Entity?, state: BattleState?) {}
/**
* Gets the formated hit.
*
* @param attacker The attacking entity.
* @param victim The victim.
* @param state The battle state.
* @param rawHit The hit to format.
* @return The formated hit.
*/
protected open fun getFormattedHit(attacker: Entity, victim: Entity, state: BattleState, rawHit: Int): Int {
protected open fun getFormattedHit(
attacker: Entity,
victim: Entity,
state: BattleState,
rawHit: Int,
): Int {
var hit = rawHit
hit = attacker.getFormattedHit(state, hit).toInt()
if (victim is Player) {
@ -582,7 +569,11 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
}
if (attacker is Player) {
val player = attacker.asPlayer()
if (player.equipment[3] != null && player.equipment[3].id == 14726 && state.style == CombatStyle.MAGIC) {
if (
player.equipment[3] != null &&
player.equipment[3].id == 14726 &&
state.style == CombatStyle.MAGIC
) {
hit += (hit.toDouble() * 0.15).toInt()
}
}
@ -596,6 +587,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
/**
* Gets the default animation of the entity.
*
* @param e The entity.
* @param style The combat style.
* @return The attack animation.
@ -610,6 +602,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
/**
* Registers a special attack handler.
*
* @param itemId The item id.
* @param handler The combat swing handler.
* @return `True` if succesful.
@ -619,7 +612,17 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
specialHandlers = HashMap()
}
if (specialHandlers!!.containsKey(itemId)) {
log(this::class.java, Log.ERR, "Already contained special attack handler for item " + itemId + " - [old=" + specialHandlers!![itemId]!!::class.java.simpleName + ", new=" + handler.javaClass.simpleName + "].")
log(
this::class.java,
Log.ERR,
"Already contained special attack handler for item " +
itemId +
" - [old=" +
specialHandlers!![itemId]!!::class.java.simpleName +
", new=" +
handler.javaClass.simpleName +
"].",
)
return false
}
return specialHandlers!!.put(itemId, handler) == null
@ -627,9 +630,9 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
/**
* Gets the special attack handler for the given item id.
*
* @param itemId The item id.
* @return The special attack handler, or `null` if this item has no
* special attack handler.
* @return The special attack handler, or `null` if this item has no special attack handler.
*/
fun getSpecial(itemId: Int): CombatSwingHandler? {
if (specialHandlers == null) {
@ -639,41 +642,30 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
}
companion object {
/**
* The amount of experience to get per hit.
*/
@JvmField
var EXPERIENCE_MOD = 4.0
/** The amount of experience to get per hit. */
@JvmField
var EXPERIENCE_MOD = 4.0
/**
* Checks if a projectile can be fired from the node location to the victim
* location.
* Checks if a projectile can be fired from the node location to the victim location.
*
* @param entity The node.
* @param victim The victim.
* @param checkClose If we are checking for a melee attack rather than a
* projectile.
* @param checkClose If we are checking for a melee attack rather than a projectile.
* @return `True` if so.
*/
@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
@JvmStatic
fun isProjectileClipped(entity: Node, victim: Node?, checkClose: Boolean): Boolean {
val target = requireNotNull(victim)
return RsmodPathfinder.hasLineOfSightBetween(
entity.location,
entity.size(),
target.location,
target.size(),
maxRaySteps = if (checkClose) 1 else Int.MAX_VALUE,
)
}
}
}
enum class SwingHandlerFlag {
@ -681,5 +673,5 @@ enum class SwingHandlerFlag {
IGNORE_STAT_BOOSTS_ACCURACY,
IGNORE_PRAYER_BOOSTS_DAMAGE,
IGNORE_PRAYER_BOOSTS_ACCURACY,
IGNORE_STAT_REDUCTION
IGNORE_STAT_REDUCTION,
}

View file

@ -26,9 +26,9 @@ open class MagicSwingHandler (vararg flags: SwingHandlerFlag)
}
var distance = 10
var type = InteractionType.STILL_INTERACT
var goodRange = victim.centerLocation.withinDistance(entity.centerLocation, getCombatDistance(entity, victim, distance))
var goodRange = CombatReach.canReach(entity, victim, getCombatDistance(entity, victim, distance))
if (victim.walkingQueue.isMoving && !goodRange) {
goodRange = victim.centerLocation.withinDistance(entity.centerLocation, getCombatDistance(entity, victim, ++distance))
goodRange = CombatReach.canReach(entity, victim, getCombatDistance(entity, victim, ++distance))
type = InteractionType.MOVE_INTERACT
}
if (goodRange && isAttackable(entity, victim) != InteractionType.NO_INTERACT) {

View file

@ -2,12 +2,9 @@ package core.game.node.entity.combat
import content.global.skill.skillcapeperks.SkillcapePerks
import content.global.skill.slayer.SlayerEquipmentFlags
import content.global.skill.slayer.SlayerManager
import content.global.skill.slayer.Tasks
import content.global.skill.summoning.SummoningPouch
import core.api.*
import core.api.EquipmentSlot
import core.game.container.impl.EquipmentContainer
import core.game.node.entity.Entity
import core.game.node.entity.combat.equipment.ArmourSet
import core.game.node.entity.combat.equipment.Weapon
@ -15,50 +12,165 @@ import core.game.node.entity.combat.equipment.WeaponInterface
import core.game.node.entity.npc.NPC
import core.game.node.entity.player.Player
import core.game.node.entity.skill.Skills
import core.game.world.map.path.Pathfinder
import core.game.world.map.Direction
import core.game.world.map.Location
import core.game.world.map.RegionManager
import core.tools.RandomFunction
import org.rs09.consts.Items
import kotlin.math.ceil
import kotlin.math.floor
/**
* Handles a melee combat swing.
*
* @author Emperor
* @author Ceikry, Kotlin conversion + cleanup
*/
open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
/**
* Constructs a new `MeleeSwingHandler` {@Code Object}.
*/
open class MeleeSwingHandler(vararg flags: SwingHandlerFlag)
/** Constructs a new `MeleeSwingHandler` {@Code Object}. */
: CombatSwingHandler(CombatStyle.MELEE, *flags) {
override fun canSwing(entity : Entity, victim : Entity) : InteractionType? {
//Credits wolfenzi, https://www.rune-server.ee/2009scape-development/rs2-server/snippets/608720-arios-hybridding-improve.html
var distance = if (usingHalberd(entity)) 2 else 1
var type = InteractionType.STILL_INTERACT
var goodRange = canMelee(entity, victim, distance)
if (!goodRange && victim.properties.combatPulse.getVictim() !== entity && victim.walkingQueue.isMoving && entity.size() == 1) {
type = InteractionType.MOVE_INTERACT
distance += if (entity.walkingQueue.isRunningBoth) 2 else 1
goodRange = canMelee(entity, victim, distance)
}
if (!isProjectileClipped(entity, victim, !usingHalberd(entity))) {
return InteractionType.NO_INTERACT
}
val isRunning = entity.walkingQueue.runDir != -1
val enemyRunning = victim.walkingQueue.runDir != -1
// THX 4 fix tom <333.
if (super.canSwing(entity, victim) != InteractionType.NO_INTERACT) {
val maxDistance = if (isRunning) if (enemyRunning) 3 else 4 else 2
if (entity.walkingQueue.isMoving && entity.location.getDistance(victim.location) <= maxDistance && goodRange) {
return type
} else if (goodRange) {
if (canStepTowards(entity, victim) == InteractionType.NO_INTERACT) return InteractionType.NO_INTERACT
if (type == InteractionType.STILL_INTERACT) entity.walkingQueue.reset()
return type
}
}
return InteractionType.NO_INTERACT
}
override fun canSwing(entity: Entity, victim: Entity): InteractionType? {
// Credits wolfenzi,
// https://www.rune-server.ee/2009scape-development/rs2-server/snippets/608720-arios-hybridding-improve.html
var distance = CombatReach.meleeDistance(entity)
var type = InteractionType.STILL_INTERACT
var goodRange = CombatReach.canMelee(entity, victim, distance)
if (
!goodRange &&
victim.properties.combatPulse.getVictim() !== entity &&
victim.walkingQueue.isMoving &&
entity.size() == 1
) {
type = InteractionType.MOVE_INTERACT
distance += if (entity.walkingQueue.isRunningBoth) 2 else 1
goodRange = CombatReach.canMelee(entity, victim, distance)
}
if (!hasMeleeLineOfSight(entity, victim, type)) {
return InteractionType.NO_INTERACT
}
val isRunning = entity.walkingQueue.runDir != -1
val enemyRunning = victim.walkingQueue.runDir != -1
// THX 4 fix tom <333.
if (super.canSwing(entity, victim) != InteractionType.NO_INTERACT) {
val maxDistance = if (isRunning) if (enemyRunning) 3 else 4 else 2
if (
entity.walkingQueue.isMoving &&
entity.location.getDistance(victim.location) <= maxDistance &&
goodRange
) {
return type
} else if (goodRange) {
if (
!CombatReach.hasExtendedMeleeReach(entity) &&
canStepTowards(entity, victim) == InteractionType.NO_INTERACT
)
return InteractionType.NO_INTERACT
if (type == InteractionType.STILL_INTERACT) entity.walkingQueue.reset()
return type
}
}
return InteractionType.NO_INTERACT
}
private fun hasMeleeLineOfSight(
entity: Entity,
victim: Entity,
type: InteractionType,
): Boolean {
if (CombatReach.hasExtendedMeleeReach(entity)) {
return isProjectileClipped(entity, victim, false)
}
if (
CombatReach.hasMeleeReach(
entity.location,
entity.size(),
victim.location,
victim.size(),
)
) {
return true
}
if (type != InteractionType.MOVE_INTERACT) {
return false
}
val predictedVictimLocation =
CombatMovementPlanner.predictedMovementLocation(victim) ?: return false
val projectedEntityLocation =
projectedMeleeChaseLocation(entity, victim, predictedVictimLocation) ?: return false
if (!isAdjacentTo(entity, projectedEntityLocation, victim, predictedVictimLocation)) {
return false
}
return CombatReach.hasMeleeReach(
projectedEntityLocation,
entity.size(),
predictedVictimLocation,
victim.size(),
)
}
private fun projectedMeleeChaseLocation(
entity: Entity,
victim: Entity,
victimLocation: Location,
): Location? {
if (entity.size() != 1 || entity.location.z != victimLocation.z) {
return null
}
val maxSteps =
if (
entity is Player &&
entity.walkingQueue.isRunningBoth &&
entity.settings.runEnergy >= 1.0
)
2
else 1
var current = entity.location
var steps = 0
while (steps < maxSteps && !isAdjacentTo(entity, current, victim, victimLocation)) {
val direction = Direction.getDirection(current, victimLocation) ?: 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
}
current = next
steps++
}
return current
}
private fun isAdjacentTo(
entity: Entity,
entityLocation: Location,
victim: Entity,
victimLocation: Location,
): Boolean {
val entityMinX = entityLocation.x
val entityMaxX = entityLocation.x + entity.size()
val entityMinY = entityLocation.y
val entityMaxY = entityLocation.y + entity.size()
val victimMinX = victimLocation.x
val victimMaxX = victimLocation.x + victim.size()
val victimMinY = victimLocation.y
val victimMaxY = victimLocation.y + victim.size()
val xOverlaps = entityMinX < victimMaxX && entityMaxX > victimMinX
val yOverlaps = entityMinY < victimMaxY && entityMaxY > victimMinY
if (xOverlaps && yOverlaps) {
return false
}
val xTouches = entityMaxX == victimMinX || victimMaxX == entityMinX
val yTouches = entityMaxY == victimMinY || victimMaxY == entityMinY
return (xTouches && yOverlaps) || (yTouches && xOverlaps)
}
override fun swing(entity: Entity?, victim: Entity?, state: BattleState?): Int {
var hit = 0
@ -69,18 +181,29 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
if (entity!!.properties.armourSet == ArmourSet.VERAC && RandomFunction.roll(4)) {
state.armourEffect = ArmourSet.VERAC
}
if (state.armourEffect == ArmourSet.VERAC || isAccurateImpact(entity, victim, CombatStyle.MELEE)) {
if (
state.armourEffect == ArmourSet.VERAC ||
isAccurateImpact(entity, victim, CombatStyle.MELEE)
) {
var max = calculateHit(entity, victim, 1.0)
if (victim != null) {
if (entity is NPC && state.armourEffect == ArmourSet.VERAC && victim.hasProtectionPrayer(CombatStyle.MELEE)) max = max * 2 / 3
if (
entity is NPC &&
state.armourEffect == ArmourSet.VERAC &&
victim.hasProtectionPrayer(CombatStyle.MELEE)
)
max = max * 2 / 3
}
state.maximumHit = max
hit = RandomFunction.random(max + 1)
}
state.estimatedHit = hit
if(victim != null) {
if (state.estimatedHit > victim.skills.lifepoints) state.estimatedHit = victim.skills.lifepoints
if (state.estimatedHit + state.secondaryHit > victim.skills.lifepoints) state.secondaryHit -= ((state.estimatedHit + state.secondaryHit) - victim.skills.lifepoints)
if (victim != null) {
if (state.estimatedHit > victim.skills.lifepoints)
state.estimatedHit = victim.skills.lifepoints
if (state.estimatedHit + state.secondaryHit > victim.skills.lifepoints)
state.secondaryHit -=
((state.estimatedHit + state.secondaryHit) - victim.skills.lifepoints)
}
return 1
}
@ -123,7 +246,7 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
damage = 20
}
if (damage > -1 && RandomFunction.random(10) < 4) {
applyPoison (victim, entity, damage)
applyPoison(victim, entity, damage)
}
}
} else if (entity is NPC) {
@ -131,26 +254,36 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
val damage = entity.poisonSeverity()
if (poisonous && damage > -1 && RandomFunction.random(10) < 4) {
applyPoison (victim, entity, damage)
applyPoison(victim, entity, damage)
}
}
super.adjustBattleState(entity, victim, state)
}
override fun calculateAccuracy(entity: Entity?): Int {
//formula taken from wiki: https://oldschool.runescape.wiki/w/Damage_per_second/Melee#Step_six:_Calculate_the_hit_chance Yes I know it's old school. It's the best resource we have for potentially authentic formulae.
// formula taken from wiki:
// https://oldschool.runescape.wiki/w/Damage_per_second/Melee#Step_six:_Calculate_the_hit_chance Yes I know it's old school. It's the best resource we have for potentially authentic formulae.
entity ?: return 0
val styleAttackBonus = entity.properties.bonuses[entity.properties.attackStyle.bonusType] + 64
val styleAttackBonus =
entity.properties.bonuses[entity.properties.attackStyle.bonusType] + 64
when (entity) {
is Player -> {
var effectiveAttackLevel = entity.skills.getLevel(Skills.ATTACK).toDouble()
if(!flags.contains(SwingHandlerFlag.IGNORE_PRAYER_BOOSTS_ACCURACY))
effectiveAttackLevel = floor(effectiveAttackLevel + (entity.prayer.getSkillBonus(Skills.ATTACK) * effectiveAttackLevel))
if(entity.properties.attackStyle.style == WeaponInterface.STYLE_ACCURATE) effectiveAttackLevel += 3
else if(entity.properties.attackStyle.style == WeaponInterface.STYLE_CONTROLLED) effectiveAttackLevel += 1
if (!flags.contains(SwingHandlerFlag.IGNORE_PRAYER_BOOSTS_ACCURACY))
effectiveAttackLevel =
floor(
effectiveAttackLevel +
(entity.prayer.getSkillBonus(Skills.ATTACK) * effectiveAttackLevel)
)
if (entity.properties.attackStyle.style == WeaponInterface.STYLE_ACCURATE)
effectiveAttackLevel += 3
else if (entity.properties.attackStyle.style == WeaponInterface.STYLE_CONTROLLED)
effectiveAttackLevel += 1
effectiveAttackLevel += 8
if(SkillcapePerks.isActive(SkillcapePerks.PRECISION_STRIKES, entity)){ //Attack skillcape perk
if (
SkillcapePerks.isActive(SkillcapePerks.PRECISION_STRIKES, entity)
) { // Attack skillcape perk
effectiveAttackLevel += 6
}
effectiveAttackLevel *= getSetMultiplier(entity, Skills.ATTACK)
@ -163,14 +296,27 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
// attack bonus for specialized equipments (salve amulets, slayer equips)
val amuletId = getItemFromEquipment(entity, EquipmentSlot.NECK)?.id ?: 0
if ((amuletId == Items.SALVE_AMULET_4081 || amuletId == Items.SALVE_AMULETE_10588) && checkUndead(victimName)) {
if (
(amuletId == Items.SALVE_AMULET_4081 ||
amuletId == Items.SALVE_AMULETE_10588) && checkUndead(victimName)
) {
effectiveAttackLevel *= if (amuletId == Items.SALVE_AMULET_4081) 1.15 else 1.2
} else if (getSlayerTask(entity)?.let { task ->
} else if (
getSlayerTask(entity)?.let { task ->
val victimId = entity.properties.combatPulse?.getVictim()?.id ?: 0
task.ids.contains(victimId) || (task == Tasks.KALPHITES && (victimId == 1158)) // Kalphite Queen phase 1
} == true) {
effectiveAttackLevel *= SlayerEquipmentFlags.getDamAccBonus(entity) //Slayer Helm/ Black Mask/ Slayer cape
if (getSlayerTask(entity)?.dragon == true && inEquipment(entity, Items.DRAGON_SLAYER_GLOVES_12862))
task.ids.contains(victimId) ||
(task == Tasks.KALPHITES &&
(victimId == 1158)) // Kalphite Queen phase 1
} == true
) {
effectiveAttackLevel *=
SlayerEquipmentFlags.getDamAccBonus(
entity
) // Slayer Helm/ Black Mask/ Slayer cape
if (
getSlayerTask(entity)?.dragon == true &&
inEquipment(entity, Items.DRAGON_SLAYER_GLOVES_12862)
)
effectiveAttackLevel *= 1.1
}
@ -183,8 +329,6 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
}
return 0
}
override fun calculateHit(entity: Entity?, victim: Entity?, modifier: Double): Int {
@ -194,28 +338,43 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
when (entity) {
is Player -> {
var effectiveStrengthLevel = entity.skills.getLevel(Skills.STRENGTH).toDouble()
if(!flags.contains(SwingHandlerFlag.IGNORE_PRAYER_BOOSTS_DAMAGE))
effectiveStrengthLevel = floor(effectiveStrengthLevel + (entity.prayer.getSkillBonus(Skills.STRENGTH) * effectiveStrengthLevel))
if(entity.properties.attackStyle.style == WeaponInterface.STYLE_AGGRESSIVE) effectiveStrengthLevel += 3
else if (entity.properties.attackStyle.style == WeaponInterface.STYLE_CONTROLLED) effectiveStrengthLevel += 1
if (!flags.contains(SwingHandlerFlag.IGNORE_PRAYER_BOOSTS_DAMAGE))
effectiveStrengthLevel =
floor(
effectiveStrengthLevel +
(entity.prayer.getSkillBonus(Skills.STRENGTH) *
effectiveStrengthLevel)
)
if (entity.properties.attackStyle.style == WeaponInterface.STYLE_AGGRESSIVE)
effectiveStrengthLevel += 3
else if (entity.properties.attackStyle.style == WeaponInterface.STYLE_CONTROLLED)
effectiveStrengthLevel += 1
effectiveStrengthLevel += 8
effectiveStrengthLevel *= getSetMultiplier(entity, Skills.STRENGTH)
effectiveStrengthLevel = floor(effectiveStrengthLevel)
if (!flags.contains(SwingHandlerFlag.IGNORE_STAT_BOOSTS_DAMAGE))
effectiveStrengthLevel *= styleStrengthBonus
else effectiveStrengthLevel *= 64
if (getSlayerTask(entity)?.let { task ->
val victimId = entity.properties.combatPulse?.getVictim()?.id ?: 0
task.ids.contains(victimId) || (task == Tasks.KALPHITES && (victimId == 1158)) // Kalphite Queen phase 1
} == true) {
effectiveStrengthLevel *= SlayerEquipmentFlags.getDamAccBonus(entity) //Slayer Helm/ Black Mask/ Slayer cape
if (
getSlayerTask(entity)?.let { task ->
val victimId = entity.properties.combatPulse?.getVictim()?.id ?: 0
task.ids.contains(victimId) ||
(task == Tasks.KALPHITES &&
(victimId == 1158)) // Kalphite Queen phase 1
} == true
) {
effectiveStrengthLevel *=
SlayerEquipmentFlags.getDamAccBonus(
entity
) // Slayer Helm/ Black Mask/ Slayer cape
}
return (floor((0.5 + (effectiveStrengthLevel / 640.0))) * modifier).toInt()
}
is NPC -> {
val strengthLevel = entity.skills.getLevel(Skills.STRENGTH) + 9
return (floor((0.5 + (strengthLevel * styleStrengthBonus / 640.0))) * modifier).toInt()
return (floor((0.5 + (strengthLevel * styleStrengthBonus / 640.0))) * modifier)
.toInt()
}
}
@ -223,17 +382,28 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
}
override fun calculateDefence(victim: Entity?, attacker: Entity?): Int {
//authentic formula, taken from OSRS wiki: https://oldschool.runescape.wiki/w/Damage_per_second/Melee#Step_five:_Calculate_the_Defence_roll
// authentic formula, taken from OSRS wiki:
// https://oldschool.runescape.wiki/w/Damage_per_second/Melee#Step_five:_Calculate_the_Defence_roll
victim ?: return 0
attacker ?: return 0
val styleDefenceBonus = victim.properties.bonuses[attacker.properties.attackStyle.bonusType + 5] + 64
val styleDefenceBonus =
victim.properties.bonuses[attacker.properties.attackStyle.bonusType + 5] + 64
when (victim) {
is Player -> {
var effectiveDefenceLevel = victim.skills.getLevel(Skills.DEFENCE).toDouble()
effectiveDefenceLevel = floor(effectiveDefenceLevel + (victim.prayer.getSkillBonus(Skills.DEFENCE) * effectiveDefenceLevel))
if (victim.properties.attackStyle.style == WeaponInterface.STYLE_DEFENSIVE || victim.properties.attackStyle.style == WeaponInterface.STYLE_LONG_RANGE) effectiveDefenceLevel += 3
else if (victim.properties.attackStyle.style == WeaponInterface.STYLE_CONTROLLED) effectiveDefenceLevel += 1
effectiveDefenceLevel =
floor(
effectiveDefenceLevel +
(victim.prayer.getSkillBonus(Skills.DEFENCE) * effectiveDefenceLevel)
)
if (
victim.properties.attackStyle.style == WeaponInterface.STYLE_DEFENSIVE ||
victim.properties.attackStyle.style == WeaponInterface.STYLE_LONG_RANGE
)
effectiveDefenceLevel += 3
else if (victim.properties.attackStyle.style == WeaponInterface.STYLE_CONTROLLED)
effectiveDefenceLevel += 1
effectiveDefenceLevel += 8
effectiveDefenceLevel *= getSetMultiplier(victim, Skills.DEFENCE)
effectiveDefenceLevel *= familiarDefenceBonus(victim)
@ -283,12 +453,23 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
/**
* Check to see whether an NPC is classified as undead.
*
* @param name
* @return true if so
*/
private fun checkUndead(name: String): Boolean {
return (name == "Zombie" || name.contains("rmoured") || name == "Ankou" || name == "Crawling Hand" || name == "Banshee" || name == "Ghost" || name == "Ghast" || name == "Mummy" || name.contains("Revenant")
|| name == "Skeleton" || name == "Zogre" || name == "Spiritual Mage")
return (name == "Zombie" ||
name.contains("rmoured") ||
name == "Ankou" ||
name == "Crawling Hand" ||
name == "Banshee" ||
name == "Ghost" ||
name == "Ghast" ||
name == "Mummy" ||
name.contains("Revenant") ||
name == "Skeleton" ||
name == "Zogre" ||
name == "Spiritual Mage")
}
/**
@ -298,11 +479,12 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
*/
private fun familiarDefenceBonus(e: Entity?): Double {
if (e !is Player) return 1.0
val fam = try {
e.familiarManager?.familiar
} catch (ex: Exception) {
null
} ?: return 1.0
val fam =
try {
e.familiarManager?.familiar
} catch (ex: Exception) {
null
} ?: return 1.0
return when (fam.pouchId) {
SummoningPouch.IRON_TITAN_POUCH.pouchId -> 1.10
SummoningPouch.STEEL_TITAN_POUCH.pouchId -> 1.15
@ -311,61 +493,15 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
}
companion object {
/**
* Checks if the entity is using a halberd.
* @param entity The entity.
* @return `True` if so.
*/
private fun usingHalberd(entity: Entity): Boolean {
if (entity is Player) {
val weapon = entity.equipment[EquipmentContainer.SLOT_WEAPON]
if (weapon != null) {
return weapon.id in 3190..3204 || weapon.id == 6599
}
} else if (entity is NPC) {
return entity.id == 8612
}
return false
}
/**
* Checks if the entity can execute a melee swing from its current location.
*
* @param entity The attacking entity.
* @param victim The victim.
* @return `True` if so.
*/
fun canMelee(entity: Entity, victim: Entity?, distance: Int): Boolean {
val e = entity.location
if (victim == null) {
return false
}
if (entity.id == 7135 && entity.location.withinDistance(victim.location, 2)) {
return true
}
val x = victim.location.x
val y = victim.location.y
val size = entity.size()
if (distance == 1) {
for (i in 0 until size) {
if (Pathfinder.isStandingIn(e.x - 1, e.y + i, 1, 1, x, y, victim.size(), victim.size())) {
return true
}
if (Pathfinder.isStandingIn(e.x + size, e.y + i, 1, 1, x, y, victim.size(), victim.size())) {
return true
}
if (Pathfinder.isStandingIn(e.x + i, e.y - 1, 1, 1, x, y, victim.size(), victim.size())) {
return true
}
if (Pathfinder.isStandingIn(e.x + i, e.y + size, 1, 1, x, y, victim.size(), victim.size())) {
return true
}
}
if (e == victim.location) {
return true
}
return victim.getSwingHandler(false).type == CombatStyle.MELEE && e.withinDistance(victim.location, 1) && victim.properties.combatPulse.getVictim() === entity && entity.index < victim.index
}
return entity.centerLocation.withinDistance(victim.centerLocation, distance + (size shr 1) + (victim.size() shr 1))
return CombatReach.canMelee(entity, victim, distance)
}
}
}

View file

@ -53,10 +53,10 @@ open class RangeSwingHandler (vararg flags: SwingHandlerFlag) : CombatSwingHandl
distance = 10
}
}
var goodRange = victim.centerLocation.withinDistance(entity.centerLocation, getCombatDistance(entity, victim, distance))
var goodRange = CombatReach.canReach(entity, victim, getCombatDistance(entity, victim, distance))
var type = InteractionType.STILL_INTERACT
if (victim.walkingQueue.isMoving && !goodRange) {
goodRange = victim.centerLocation.withinDistance(entity.centerLocation, getCombatDistance(entity, victim, ++distance))
goodRange = CombatReach.canReach(entity, victim, getCombatDistance(entity, victim, ++distance))
type = InteractionType.MOVE_INTERACT
}
if (goodRange && super.canSwing(entity, victim) != InteractionType.NO_INTERACT) {

View file

@ -64,6 +64,21 @@ public final class WalkingQueue {
public ArrayList<GroundItem> routeItems = new ArrayList<GroundItem>();
/**
* Clears the route markers created by ::drawroute.
*/
private void clearRouteItems() {
if (!(entity instanceof Player) || routeItems.isEmpty()) {
return;
}
for (GroundItem item : routeItems) {
if (item != null) {
RegionManager.getRegionPlane(item.getLocation()).remove(item);
}
}
routeItems.clear();
}
/**
* Constructs a new {@code WalkingQueue} {@code Object}.
* @param entity The entity.
@ -92,17 +107,9 @@ public final class WalkingQueue {
if (hasTimerActive(entity, "frozen"))
return;
Point point = walkingQueue.poll();
boolean drawPath = entity.getAttribute("routedraw", false);
if (point == null) {
updateRunEnergy(false);
if (isPlayer && drawPath) {
for (GroundItem item : routeItems) {
if (item != null) {
RegionManager.getRegionPlane(item.getLocation()).remove(item);
}
}
routeItems.clear();
}
clearRouteItems();
return;
}
if (isPlayer && ((Player) entity).getSettings().getRunEnergy() < 1.0) {
@ -236,6 +243,7 @@ public final class WalkingQueue {
*/
public boolean updateTeleport() {
if (entity.getProperties().getTeleportLocation() != null) {
entity.getProperties().getCombatPulse().stop();
reset(false);
entity.setLocation(entity.getProperties().getTeleportLocation());
entity.getProperties().setTeleportLocation(null);
@ -374,13 +382,19 @@ public final class WalkingQueue {
}
return running;
}
/**
* Checks if the entity has a path to walk.
*
* @return {@code True} if so.
*/
public boolean hasPath() {
return !walkingQueue.isEmpty();
for (Point point : walkingQueue) {
if (point.getDirection() != null) {
return true;
}
}
return false;
}
/**
@ -412,6 +426,7 @@ public final class WalkingQueue {
);
}
clearRouteItems();
walkingQueue.clear();
walkingQueue.add(new Point(loc.getX(), loc.getY()));
this.running = running;

View file

@ -21,8 +21,11 @@ import content.global.skill.slayer.Tasks;
import content.global.skill.summoning.familiar.Familiar;
import core.game.world.map.Direction;
import core.game.world.map.Location;
import core.game.world.map.Point;
import core.game.world.map.RegionManager;
import core.game.world.map.build.DynamicRegion;
import core.game.world.map.path.ClipMaskSupplier;
import core.game.world.map.path.Path;
import core.game.world.map.path.Pathfinder;
import core.game.world.update.flag.context.Animation;
import core.game.world.update.flag.context.Graphics;
@ -466,25 +469,22 @@ public class NPC extends Entity {
return;
if (!getLocks().isInteractionLocked()) {
if (!getLocks().isMovementLocked()) {
int effectiveWalkRadius = getWalkRadius();
if (
!pathBoundMovement
&& walkRadius > 0
&& walkRadius <= 20
&& !getLocation().withinDistance(getProperties().getSpawnLocation(), (int)(walkRadius * 1.5))
&& effectiveWalkRadius > 0
&& effectiveWalkRadius <= 20
&& !getLocation().withinDistance(getProperties().getSpawnLocation(), getSpawnReturnDistance(effectiveWalkRadius))
&& !getAttribute("no-spawn-return", false)
)
{
MovementPulse current = getAttribute("return-to-spawn-pulse");
if (current != null && current.isRunning()) return;
if(!isNeverWalks()){
if(walkRadius == 0)
walkRadius = 3;
}
if (aggressiveHandler != null) {
aggressiveHandler.setPauseTicks(walkRadius + 1);
aggressiveHandler.setPauseTicks(effectiveWalkRadius + 1);
}
nextWalk = GameWorld.getTicks() + walkRadius + 1;
nextWalk = GameWorld.getTicks() + effectiveWalkRadius + 1;
getLocks().lockMovement(100);
getImpactHandler().setDisabledTicks(100);
setAttribute("return-to-spawn", true);
@ -530,15 +530,130 @@ public class NPC extends Entity {
setNextWalk();
Location l = getMovementDestination();
if (canMove(l)) {
if((Boolean) definition.getHandlers().getOrDefault("water_npc",false)){
Pathfinder.findWater(this,l,true,Pathfinder.DUMB).walk(this);
} else {
Pathfinder.find(this, l, true, Pathfinder.DUMB).walk(this);
}
Path path = pathBoundMovement ? findMovementPath(l) : findRandomMovementPath(l);
path.walk(this);
}
return false;
}
private Path findMovementPath(Location destination) {
if (isWaterNPC()) {
return Pathfinder.findWater(this, destination, true, Pathfinder.DUMB);
}
return Pathfinder.find(this, destination, true, Pathfinder.DUMB);
}
private Path findRandomMovementPath(Location destination) {
if (isWaterNPC() || size() != 1) {
Path path = findMovementPath(destination);
return isRandomMovementPathWithinBounds(path) ? path : new Path();
}
return findDirectRandomMovementPath(destination);
}
private Path findDirectRandomMovementPath(Location destination) {
Path path = new Path();
path.setSuccesful(true);
Location current = getLocation();
int maxSteps = Math.min(14, Math.max(1, getSpawnReturnDistance(Math.max(1, getWalkRadius()))));
boolean usedSidestep = false;
for (int steps = 0; !current.equals(destination) && steps < maxSteps; steps++) {
Direction direction = Direction.getDirection(current, destination);
Direction stepDirection = chooseRandomMovementStep(current, direction, usedSidestep);
if (stepDirection == null) {
path.setSuccesful(false);
path.setMoveNear(!path.getPoints().isEmpty());
break;
}
Location next = current.transform(stepDirection);
if (!isWithinRandomMovementBounds(next)) {
path.setSuccesful(false);
path.setMoveNear(!path.getPoints().isEmpty());
break;
}
if (stepDirection != direction) {
usedSidestep = true;
}
path.getPoints().add(new Point(next.getX(), next.getY(), stepDirection, stepDirection.getStepX(), stepDirection.getStepY()));
current = next;
}
if (!current.equals(destination) && !path.getPoints().isEmpty()) {
path.setMoveNear(true);
}
return path;
}
private Direction chooseRandomMovementStep(Location current, Direction direction, boolean usedSidestep) {
if (direction == null) {
return null;
}
Location next = current.transform(direction);
if (isWithinRandomMovementBounds(next) && canTakeRandomMovementStep(current, direction)) {
return direction;
}
if (usedSidestep) {
return null;
}
for (Direction sidestep : sidestepDirections(direction)) {
next = current.transform(sidestep);
if (isWithinRandomMovementBounds(next) && canTakeRandomMovementStep(current, sidestep)) {
return sidestep;
}
}
return null;
}
private Direction[] sidestepDirections(Direction direction) {
switch (direction) {
case NORTH:
case SOUTH:
return new Direction[] { Direction.EAST, Direction.WEST };
case EAST:
case WEST:
return new Direction[] { Direction.NORTH, Direction.SOUTH };
case NORTH_EAST:
return new Direction[] { Direction.EAST, Direction.NORTH };
case SOUTH_EAST:
return new Direction[] { Direction.EAST, Direction.SOUTH };
case SOUTH_WEST:
return new Direction[] { Direction.WEST, Direction.SOUTH };
case NORTH_WEST:
return new Direction[] { Direction.WEST, Direction.NORTH };
default:
return new Direction[0];
}
}
private boolean canTakeRandomMovementStep(Location current, Direction direction) {
ClipMaskSupplier clipMaskSupplier = behavior != null ? behavior.getClippingSupplier(this) : null;
if (clipMaskSupplier == null) {
clipMaskSupplier = RegionManager::getClippingFlag;
}
return direction.canMoveFrom(current.getZ(), current.getX(), current.getY(), clipMaskSupplier);
}
private boolean isRandomMovementPathWithinBounds(Path path) {
for (Point point : path.getPoints()) {
if (!isWithinRandomMovementBounds(Location.create(point.getX(), point.getY(), getLocation().getZ()))) {
return false;
}
}
return true;
}
private boolean isWithinRandomMovementBounds(Location location) {
int walkRadius = getWalkRadius();
return walkRadius <= 0 || location.withinDistance(getProperties().getSpawnLocation(), getSpawnReturnDistance(walkRadius));
}
private int getSpawnReturnDistance(int walkRadius) {
return (int) (walkRadius * 1.5);
}
private boolean isWaterNPC() {
return (Boolean) definition.getHandlers().getOrDefault("water_npc", false);
}
public int getNextWalk() {
return nextWalk;
}

View file

@ -3,7 +3,6 @@ package core.game.node.entity.npc
import core.game.node.item.Item
import core.api.ContentInterface
import core.game.node.entity.Entity
import core.game.world.map.RegionManager
import core.game.node.entity.player.Player
import core.game.node.entity.combat.BattleState
import core.game.node.entity.combat.CombatStyle
@ -24,12 +23,6 @@ open class NPCBehavior(vararg val ids: Int = intArrayOf()) : ContentInterface {
}
}
object StandardClipMaskSupplier : ClipMaskSupplier {
override fun getClippingFlag (z: Int, x: Int, y: Int) : Int {
return RegionManager.getClippingFlag(z,x,y)
}
}
/**
* Called every tick, before the base NPC tick() method.
* @param self the NPC instance this behavior belongs to
@ -144,7 +137,7 @@ open class NPCBehavior(vararg val ids: Int = intArrayOf()) : ContentInterface {
* Called by pathfinding code to determine the clipping mask supplier this NPC should use. You can use this to ignore water, etc.
*/
open fun getClippingSupplier(self: NPC) : ClipMaskSupplier? {
return StandardClipMaskSupplier
return null
}
/**

View file

@ -88,7 +88,6 @@ object ServerConfigParser {
wild_pvp_enabled = data.getBoolean("world.wild_pvp_enabled"),
jad_practice_enabled = data.getBoolean("world.jad_practice_enabled"),
ge_announcement_limit = data.getLong("world.ge_announcement_limit", 500L).toInt(),
smartpathfinder_bfs = data.getBoolean("world.smartpathfinder_bfs", false),
enable_castle_wars = data.getBoolean("world.enable_castle_wars", false),
message_model = data.getString("world.motw_identifier").toInt(),
message_string = data.getString("world.motw_text").replace("@name", ServerConstants.SERVER_NAME)

View file

@ -81,7 +81,6 @@ class GameSettings
var wild_pvp_enabled: Boolean,
var jad_practice_enabled: Boolean,
var ge_announcement_limit: Int,
var smartpathfinder_bfs: Boolean,
var enable_castle_wars: Boolean,
/**"Lobby" interface
@ -135,7 +134,6 @@ class GameSettings
val wild_pvp_enabled = if(data.containsKey("wild_pvp_enabled")) data["wild_pvp_enabled"] as Boolean else true
val jad_practice_enabled = if(data.containsKey("jad_practice_enabled")) data["jad_practice_enabled"] as Boolean else true
val ge_announcement_limit = data["ge_announcement_limit"].toString().toInt()
val smartpathfinder_bfs = if(data.containsKey("smartpathfinder_bfs")) data["smartpathfinder_bfs"] as Boolean else false
val enable_castle_wars = if(data.containsKey("enable_castle_wars")) data["enable_castle_wars"] as Boolean else false
val allow_token_purchase = data["allow_token_purchase"] as Boolean
val message_of_the_week_identifier = data["message_of_the_week_identifier"].toString().toInt()
@ -165,7 +163,6 @@ class GameSettings
wild_pvp_enabled,
jad_practice_enabled,
ge_announcement_limit,
smartpathfinder_bfs,
enable_castle_wars,
message_of_the_week_identifier,
message_of_the_week_text

View file

@ -6,10 +6,12 @@ import core.game.node.entity.Entity
import core.game.node.entity.npc.NPC
import core.game.node.entity.player.Player
import core.game.node.scenery.Scenery
import core.game.world.GameWorld
import core.game.world.map.zone.ZoneBorders
import core.tools.Log
import core.tools.RandomFunction
import core.tools.SystemLogger
import org.rsmod.game.pathfinder.collision.CollisionFlagMap
import java.util.*
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
@ -24,8 +26,11 @@ object RegionManager {
* The region cache mapping.
*/
private val REGION_CACHE: MutableMap<Int, Region> = HashMap()
private val RSMOD_PROJECTILE_REGIONS = HashSet<Int>()
@JvmStatic val CLIPPING_FLAGS = HashMap<Int, Array<Int>>()
@JvmStatic val PROJECTILE_FLAGS = HashMap<Int, Array<Int>>()
@JvmStatic val RSMOD_CLIPPING_FLAGS = CollisionFlagMap()
@JvmStatic val RSMOD_PROJECTILE_FLAGS = CollisionFlagMap()
public val LOCK = ReentrantLock()
@ -125,16 +130,89 @@ object RegionManager {
@JvmStatic
fun getFlags(regionId: Int, projectile: Boolean) : Array<Int> {
return if (projectile)
PROJECTILE_FLAGS.getOrPut (regionId) {Array(16384){0}}
else
return if (projectile) {
initialiseRsmodProjectileRegion(regionId)
PROJECTILE_FLAGS.getOrPut(regionId) { Array(16384) { 0 } }
} else {
CLIPPING_FLAGS.getOrPut (regionId) {Array(16384){-1}}
}
}
@JvmStatic
fun resetFlags(regionId: Int) {
PROJECTILE_FLAGS.put (regionId, Array(16384){0})
CLIPPING_FLAGS.put (regionId, Array(16384){-1})
resetRsmodFlags(regionId)
}
@JvmStatic
fun setRsmodFlag(z: Int, x: Int, y: Int, projectile: Boolean, flag: Int) {
val flags = if (projectile) RSMOD_PROJECTILE_FLAGS else RSMOD_CLIPPING_FLAGS
flags[x, y, z] = flag
}
@JvmStatic
fun loadClippingWindow(center: Location, size: Int) {
val ensured = ENSURED_WINDOW_REGIONS.get()
if (ensured.tick != GameWorld.ticks) {
ensured.regions.clear()
ensured.tick = GameWorld.ticks
}
val minX = center.x - (size / 2)
val minY = center.y - (size / 2)
val maxX = minX + size - 1
val maxY = minY + size - 1
for (regionX in (minX shr 6)..(maxX shr 6)) {
for (regionY in (minY shr 6)..(maxY shr 6)) {
val regionId = (regionX shl 8) or regionY
if (!ensured.regions.add(regionId)) {
continue
}
Region.load(forId(regionId))
initialiseRsmodProjectileRegion(regionId)
}
}
}
/**
* Pathfinding probes load the same clipping window many times per tick; regions
* cannot transition to unloaded between probes within one tick, so each thread only
* needs to ensure a region once per tick instead of taking the region lock per probe.
*/
private class EnsuredWindowRegions {
var tick = -1
val regions = HashSet<Int>()
}
private val ENSURED_WINDOW_REGIONS = ThreadLocal.withInitial { EnsuredWindowRegions() }
private fun resetRsmodFlags(regionId: Int) {
val baseX = (regionId shr 8) shl 6
val baseY = (regionId and 0xFF) shl 6
for (z in 0 until 4) {
for (x in baseX until baseX + 64 step 8) {
for (y in baseY until baseY + 64 step 8) {
RSMOD_CLIPPING_FLAGS.deallocateIfPresent(x, y, z)
val projectileFlags = RSMOD_PROJECTILE_FLAGS.allocateIfAbsent(x, y, z)
projectileFlags.fill(0)
}
}
}
}
private fun initialiseRsmodProjectileRegion(regionId: Int) {
if (!RSMOD_PROJECTILE_REGIONS.add(regionId)) {
return
}
val baseX = (regionId shr 8) shl 6
val baseY = (regionId and 0xFF) shl 6
for (z in 0 until 4) {
for (x in baseX until baseX + 64 step 8) {
for (y in baseY until baseY + 64 step 8) {
RSMOD_PROJECTILE_FLAGS.allocateIfAbsent(x, y, z).fill(0)
}
}
}
}
/**

View file

@ -179,7 +179,9 @@ public final class RegionFlags {
public void addFlag(int x, int y, int clipdata) {
int current = getFlag(x, y);
Pair<Integer, Integer> indices = getFlagIndex(x, y);
RegionManager.getFlags(indices.getFirst(), projectile)[indices.getSecond()] = max(0, current) | clipdata;
int updated = max(0, current) | clipdata;
RegionManager.getFlags(indices.getFirst(), projectile)[indices.getSecond()] = updated;
RegionManager.setRsmodFlag(plane, baseX + x, baseY + y, projectile, updated);
}
public void removeFlag(int x, int y, int clipdata) {
@ -189,16 +191,19 @@ public final class RegionFlags {
current = max(0, current) & ~clipdata;
RegionManager.getFlags(indices.getFirst(), projectile)[indices.getSecond()] = current;
RegionManager.setRsmodFlag(plane, baseX + x, baseY + y, projectile, current);
}
public void clearFlag(int x, int y) {
Pair<Integer, Integer> indices = getFlagIndex(x, y);
RegionManager.getFlags(indices.getFirst(), projectile)[indices.getSecond()] = 0;
RegionManager.setRsmodFlag(plane, baseX + x, baseY + y, projectile, 0);
}
public void invalidateFlag(int x, int y) {
Pair<Integer, Integer> indices = getFlagIndex(x, y);
RegionManager.getFlags(indices.getFirst(), projectile)[indices.getSecond()] = -1;
RegionManager.setRsmodFlag(plane, baseX + x, baseY + y, projectile, -1);
}
/**
@ -534,4 +539,4 @@ public final class RegionFlags {
public void setLandscape(boolean[][] landscape) {
this.landscape = landscape;
}
}
}

View file

@ -3,39 +3,35 @@ package core.game.world.map.path;
import core.game.world.map.Direction;
import core.game.world.map.Location;
import core.game.world.map.Point;
import core.game.world.map.RegionManager;
import java.util.ArrayList;
import java.util.List;
/**
* A pathfinder implementation used for an easy path, where the pathfinder won't
* find a way around clipped objects.. <br> This is used for NPC combat
* following, NPC random movement, etc.
* @author Emperor
* Pathfinder for simple local movement. It walks directly toward the
* destination and only tries the horizontal/vertical alternatives of a blocked
* diagonal step; it does not search around obstacles.
*/
public final class DumbPathfinder extends Pathfinder {
/**
* If a path can be found.
*/
private boolean found;
/**
* The plane.
*/
private int z;
/**
* The x-coordinate.
*/
private int x;
/**
* The y-coordinate.
*/
private int y;
@Override
public Path find(Location start, int size, Location end, int sizeX, int sizeY, int rotation, int type, int walkingFlag, boolean near, ClipMaskSupplier clipMaskSupplier) {
public Path find(Location start,
int size,
Location end,
int sizeX,
int sizeY,
int rotation,
int type,
int walkingFlag,
boolean near,
ClipMaskSupplier clipMaskSupplier) {
ClipMaskSupplier supplier = clipMaskSupplier != null ? clipMaskSupplier : RegionManager::getClippingFlag;
Path path = new Path();
z = start.getZ();
x = start.getX();
@ -44,20 +40,19 @@ public final class DumbPathfinder extends Pathfinder {
path.setSuccesful(true);
while (x != end.getX() || y != end.getY()) {
Direction[] directions = getDirection(x, y, end);
if (type != 0) {
if ((type < 5 || type == 10) && canDoorInteract(x, y, size, end.getX(), end.getY(), type - 1, rotation, z, clipMaskSupplier)) {
if (type >= 0) {
if ((type < 5 || type == 9) && canDoorInteract(x, y, size, end.getX(), end.getY(), type, rotation, z, supplier)) {
break;
}
if (type < 10 && canDecorationInteract(x, y, size, end.getX(), end.getY(), type - 1, rotation, z, clipMaskSupplier)) {
if (type < 10 && canDecorationInteract(x, y, size, end.getX(), end.getY(), rotation, type, z, supplier)) {
break;
}
}
if (sizeX != 0 && sizeY != 0) {
if (canInteract(x, y, size, end.getX(), end.getY(), sizeX, sizeY, walkingFlag, z, clipMaskSupplier)) {
if (canInteract(x, y, size, end.getX(), end.getY(), sizeX, sizeY, walkingFlag, z, supplier)) {
break;
}
if (directions.length > 1) { // Ensures we approach the location
// correctly (non-diagonal).
if (directions.length > 1) {
Direction dir = directions[0];
if (x + dir.getStepX() == end.getX() && y + dir.getStepY() == end.getY()) {
directions[0] = directions[directions.length - 1];
@ -67,11 +62,11 @@ public final class DumbPathfinder extends Pathfinder {
}
found = true;
if (size < 2) {
checkSingleTraversal(points, clipMaskSupplier, directions);
checkSingleTraversal(points, supplier, directions);
} else if (size == 2) {
checkDoubleTraversal(points, clipMaskSupplier, directions);
checkDoubleTraversal(points, supplier, directions);
} else {
checkVariableTraversal(points, directions, size, clipMaskSupplier);
checkVariableTraversal(points, directions, size, supplier);
}
if (!found) {
path.setMoveNear(x != start.getX() || y != start.getY());
@ -79,22 +74,10 @@ public final class DumbPathfinder extends Pathfinder {
break;
}
}
if (!points.isEmpty()) {
Direction last = null;
for (int i = 0; i < points.size() - 1; i++) {
Point p = points.get(i);
path.getPoints().add(p);
}
path.getPoints().add(points.get(points.size() - 1));
}
path.getPoints().addAll(points);
return path;
}
/**
* Checks traversal for a size 1 entity.
* @param points The points list.
* @param directions The directions.
*/
private void checkSingleTraversal(List<Point> points, ClipMaskSupplier clipMaskSupplier, Direction... directions) {
for (Direction dir : directions) {
found = true;
@ -108,7 +91,12 @@ public final class DumbPathfinder extends Pathfinder {
y++;
break;
case NORTH_EAST:
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y) & PREVENT_EAST) != 0 || (clipMaskSupplier.getClippingFlag(z, x, y + 1) & PREVENT_NORTH) != 0 || (clipMaskSupplier.getClippingFlag(z, x + 1, y + 1) & PREVENT_NORTHEAST) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y) & PREVENT_EAST) != 0 || (clipMaskSupplier.getClippingFlag(z,
x,
y + 1) & PREVENT_NORTH) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x + 1,
y + 1) & PREVENT_NORTHEAST) != 0) {
found = false;
break;
}
@ -125,7 +113,12 @@ public final class DumbPathfinder extends Pathfinder {
x++;
break;
case SOUTH_EAST:
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y) & PREVENT_EAST) != 0 || (clipMaskSupplier.getClippingFlag(z, x, y - 1) & PREVENT_SOUTH) != 0 || (clipMaskSupplier.getClippingFlag(z, x + 1, y - 1) & PREVENT_SOUTHEAST) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y) & PREVENT_EAST) != 0 || (clipMaskSupplier.getClippingFlag(z,
x,
y - 1) & PREVENT_SOUTH) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x + 1,
y - 1) & PREVENT_SOUTHEAST) != 0) {
found = false;
break;
}
@ -142,7 +135,12 @@ public final class DumbPathfinder extends Pathfinder {
y--;
break;
case SOUTH_WEST:
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & PREVENT_WEST) != 0 || (clipMaskSupplier.getClippingFlag(z, x, y - 1) & PREVENT_SOUTH) != 0 || (clipMaskSupplier.getClippingFlag(z, x - 1, y - 1) & PREVENT_SOUTHWEST) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & PREVENT_WEST) != 0 || (clipMaskSupplier.getClippingFlag(z,
x,
y - 1) & PREVENT_SOUTH) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x - 1,
y - 1) & PREVENT_SOUTHWEST) != 0) {
found = false;
break;
}
@ -159,7 +157,12 @@ public final class DumbPathfinder extends Pathfinder {
x--;
break;
case NORTH_WEST:
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & PREVENT_WEST) != 0 || (clipMaskSupplier.getClippingFlag(z, x, y + 1) & PREVENT_NORTH) != 0 || (clipMaskSupplier.getClippingFlag(z, x - 1, y + 1) & PREVENT_NORTHWEST) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & PREVENT_WEST) != 0 || (clipMaskSupplier.getClippingFlag(z,
x,
y + 1) & PREVENT_NORTH) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x - 1,
y + 1) & PREVENT_NORTHWEST) != 0) {
found = false;
break;
}
@ -173,18 +176,15 @@ public final class DumbPathfinder extends Pathfinder {
}
}
}
/**
* Checks traversal for a size 1 entity.
* @param points The points list.
* @param directions The directions.
*/
private void checkDoubleTraversal(List<Point> points, ClipMaskSupplier clipMaskSupplier, Direction... directions) {
for (Direction dir : directions) {
found = true;
switch (dir) {
case NORTH:
if ((clipMaskSupplier.getClippingFlag(z, x, y + 2) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z, x + 1, y + 2) & 0x12c01e0) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x, y + 2) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + 1,
y + 2) & 0x12c01e0) != 0) {
found = false;
break;
}
@ -192,7 +192,12 @@ public final class DumbPathfinder extends Pathfinder {
y++;
break;
case NORTH_EAST:
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y + 2) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z, x + 2, y + 2) & 0x12c01e0) != 0 || (clipMaskSupplier.getClippingFlag(z, x + 2, y + 1) & 0x12c0183) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y + 2) & 0x12c01f8) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + 2,
y + 2) & 0x12c01e0) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x + 2,
y + 1) & 0x12c01e3) != 0) {
found = false;
break;
}
@ -201,7 +206,9 @@ public final class DumbPathfinder extends Pathfinder {
y++;
break;
case EAST:
if ((clipMaskSupplier.getClippingFlag(z, x + 2, y) & 0x12c0183) != 0 || (clipMaskSupplier.getClippingFlag(z, x + 2, y + 1) & 0x12c01e0) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x + 2, y) & 0x12c0183) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + 2,
y + 1) & 0x12c01e0) != 0) {
found = false;
break;
}
@ -209,7 +216,12 @@ public final class DumbPathfinder extends Pathfinder {
x++;
break;
case SOUTH_EAST:
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, x + 2, y) & 0x12c01e0) != 0 || (clipMaskSupplier.getClippingFlag(z, x + 2, y - 1) & 0x12c0183) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y - 1) & 0x12c018f) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + 2,
y) & 0x12c01e3) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x + 2,
y - 1) & 0x12c0183) != 0) {
found = false;
break;
}
@ -218,7 +230,9 @@ public final class DumbPathfinder extends Pathfinder {
y--;
break;
case SOUTH:
if ((clipMaskSupplier.getClippingFlag(z, x, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, x + 1, y - 1) & 0x12c0183) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + 1,
y - 1) & 0x12c0183) != 0) {
found = false;
break;
}
@ -226,7 +240,12 @@ public final class DumbPathfinder extends Pathfinder {
y--;
break;
case SOUTH_WEST:
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, x - 1, y) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z, x, y - 1) & 0x12c0183) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z,
x - 1,
y) & 0x12c013e) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x,
y - 1) & 0x12c018f) != 0) {
found = false;
break;
}
@ -235,7 +254,9 @@ public final class DumbPathfinder extends Pathfinder {
y--;
break;
case WEST:
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, x - 1, y + 1) & 0x12c0138) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z,
x - 1,
y + 1) & 0x12c0138) != 0) {
found = false;
break;
}
@ -243,7 +264,12 @@ public final class DumbPathfinder extends Pathfinder {
x--;
break;
case NORTH_WEST:
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, x - 1, y + 2) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z, x, y + 2) & 0x12c01e0) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + 1) & 0x12c013e) != 0 || (clipMaskSupplier.getClippingFlag(z,
x - 1,
y + 2) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x,
y + 2) & 0x12c01f8) != 0) {
found = false;
break;
}
@ -257,19 +283,16 @@ public final class DumbPathfinder extends Pathfinder {
}
}
}
/**
* Checks traversal for variable size entities.
* @param points The points list.
* @param directions The directions to check.
* @param size The mover size.
*/
private void checkVariableTraversal(List<Point> points, Direction[] directions, int size, ClipMaskSupplier clipMaskSupplier) {
for (Direction dir : directions) {
found = true;
roar: switch (dir) {
roar:
switch (dir) {
case NORTH:
if ((clipMaskSupplier.getClippingFlag(z, x, y + size) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z, x + (size - 1), y + size) & 0x12c01e0) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x, y + size) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + (size - 1),
y + size) & 0x12c01e0) != 0) {
found = false;
break;
}
@ -283,12 +306,19 @@ public final class DumbPathfinder extends Pathfinder {
y++;
break;
case NORTH_EAST:
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y + size) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z, x + size, y + size) & 0x12c01e0) != 0 || (clipMaskSupplier.getClippingFlag(z, x + size, y + 1) & 0x12c0183) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y + size) & 0x12c01f8) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + size,
y + size) & 0x12c01e0) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x + size,
y + 1) & 0x12c01e3) != 0) {
found = false;
break;
}
for (int i = 1; i < size - 1; i++) {
if ((clipMaskSupplier.getClippingFlag(z, x + (i + 1), y + size) & 0x12c01f8) != 0 || (clipMaskSupplier.getClippingFlag(z, x + size, y + (i + 1)) & 0x12c01e3) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x + (i + 1), y + size) & 0x12c01f8) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + size,
y + (i + 1)) & 0x12c01e3) != 0) {
found = false;
break roar;
}
@ -298,7 +328,9 @@ public final class DumbPathfinder extends Pathfinder {
y++;
break;
case EAST:
if ((clipMaskSupplier.getClippingFlag(z, x + size, y) & 0x12c0183) != 0 || (clipMaskSupplier.getClippingFlag(z, x + size, y + (size - 1)) & 0x12c01e0) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x + size, y) & 0x12c0183) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + size,
y + (size - 1)) & 0x12c01e0) != 0) {
found = false;
break;
}
@ -312,12 +344,19 @@ public final class DumbPathfinder extends Pathfinder {
x++;
break;
case SOUTH_EAST:
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, x + size, y + (size - 2)) & 0x12c01e0) != 0 || (clipMaskSupplier.getClippingFlag(z, x + size, y - 1) & 0x12c0183) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y - 1) & 0x12c018f) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + size,
y + (size - 2)) & 0x12c01e3) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x + size,
y - 1) & 0x12c0183) != 0) {
found = false;
break;
}
for (int i = 1; i < size - 1; i++) {
if ((clipMaskSupplier.getClippingFlag(z, x + size, y + (i - 1)) & 0x12c01e3) != 0 || (clipMaskSupplier.getClippingFlag(z, x + (i + 1), y - 1) & 0x12c018f) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x + size, y + (i - 1)) & 0x12c01e3) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + (i + 1),
y - 1) & 0x12c018f) != 0) {
found = false;
break roar;
}
@ -327,7 +366,9 @@ public final class DumbPathfinder extends Pathfinder {
y--;
break;
case SOUTH:
if ((clipMaskSupplier.getClippingFlag(z, x, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, x + (size - 1), y - 1) & 0x12c0183) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + (size - 1),
y - 1) & 0x12c0183) != 0) {
found = false;
break;
}
@ -341,12 +382,19 @@ public final class DumbPathfinder extends Pathfinder {
y--;
break;
case SOUTH_WEST:
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + (size - 2)) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z, x - 1, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, x + (size - 2), y - 1) & 0x12c0183) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + (size - 2)) & 0x12c013e) != 0 || (clipMaskSupplier.getClippingFlag(z,
x - 1,
y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x + (size - 2),
y - 1) & 0x12c018f) != 0) {
found = false;
break;
}
for (int i = 1; i < size - 1; i++) {
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + (i - 1)) & 0x12c013e) != 0 || (clipMaskSupplier.getClippingFlag(z, x + (i - 1), y - 1) & 0x12c018f) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + (i - 1)) & 0x12c013e) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + (i - 1),
y - 1) & 0x12c018f) != 0) {
found = false;
break roar;
}
@ -356,7 +404,9 @@ public final class DumbPathfinder extends Pathfinder {
y--;
break;
case WEST:
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, x - 1, y + (size - 1)) & 0x12c0138) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z,
x - 1,
y + (size - 1)) & 0x12c0138) != 0) {
found = false;
break;
}
@ -370,12 +420,19 @@ public final class DumbPathfinder extends Pathfinder {
x--;
break;
case NORTH_WEST:
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, x - 1, y + size) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z, x, y + size) & 0x12c01e0) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + 1) & 0x12c013e) != 0 || (clipMaskSupplier.getClippingFlag(z,
x - 1,
y + size) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x,
y + size) & 0x12c01f8) != 0) {
found = false;
break;
}
for (int i = 1; i < size - 1; i++) {
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + (i + 1)) & 0x12c013e) != 0 || (clipMaskSupplier.getClippingFlag(z, x + (i - 1), y + size) & 0x12c01f8) != 0) {
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + (i + 1)) & 0x12c013e) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + i,
y + size) & 0x12c01f8) != 0) {
found = false;
break roar;
}
@ -390,38 +447,32 @@ public final class DumbPathfinder extends Pathfinder {
}
}
}
/**
* Gets the direction.
* @param end The end direction.
* @return The direction.
*/
private static Direction[] getDirection(int startX, int startY, Location end) {
int endX = end.getX();
int endY = end.getY();
if (startX == endX) {
if (startY > endY) {
return new Direction[] { Direction.SOUTH };
return new Direction[]{Direction.SOUTH};
} else if (startY < endY) {
return new Direction[] { Direction.NORTH };
return new Direction[]{Direction.NORTH};
}
} else if (startY == endY) {
if (startX > endX) {
return new Direction[] { Direction.WEST };
return new Direction[]{Direction.WEST};
}
return new Direction[] { Direction.EAST };
return new Direction[]{Direction.EAST};
} else {
if (startX < endX && startY < endY) {
return new Direction[] { Direction.NORTH_EAST, Direction.EAST, Direction.NORTH };
return new Direction[]{Direction.NORTH_EAST, Direction.EAST, Direction.NORTH};
} else if (startX < endX && startY > endY) {
return new Direction[] { Direction.SOUTH_EAST, Direction.EAST, Direction.SOUTH };
return new Direction[]{Direction.SOUTH_EAST, Direction.EAST, Direction.SOUTH};
} else if (startX > endX && startY < endY) {
return new Direction[] { Direction.NORTH_WEST, Direction.WEST, Direction.NORTH };
return new Direction[]{Direction.NORTH_WEST, Direction.WEST, Direction.NORTH};
} else if (startX > endX && startY > endY) {
return new Direction[] { Direction.SOUTH_WEST, Direction.WEST, Direction.SOUTH };
return new Direction[]{Direction.SOUTH_WEST, Direction.WEST, Direction.SOUTH};
}
}
return new Direction[0];
}
}

View file

@ -5,11 +5,11 @@ import core.game.node.entity.Entity;
import core.game.node.entity.npc.NPC;
import core.game.node.item.GroundItem;
import core.game.node.scenery.Scenery;
import core.game.world.map.Direction;
import core.game.world.map.Location;
import core.game.world.map.RegionManager;
public abstract class Pathfinder {
public static final int PREVENT_NORTH = 0x12c0120;
public static final int PREVENT_EAST = 0x12c0180;
public static final int PREVENT_NORTHEAST = 0x12c01e0;
@ -18,182 +18,140 @@ public abstract class Pathfinder {
public static final int PREVENT_WEST = 0x12c0108;
public static final int PREVENT_SOUTHWEST = 0x12c010e;
public static final int PREVENT_NORTHWEST = 0x12c0138;
/**
* The smart path finder.
*/
public static final SmartPathfinder SMART = new SmartPathfinder();
public static final Pathfinder SMART = new RsmodPathfinder();
/**
* The dumb path finder.
*/
public static final DumbPathfinder DUMB = new DumbPathfinder();
public static final Pathfinder DUMB = new DumbPathfinder();
/**
* The projectile path finder.
*/
public static final ProjectilePathfinder PROJECTILE = new ProjectilePathfinder();
/**
* The south direction flag.
*/
public static final int SOUTH_FLAG = 0x1;
/**
* The west direction flag.
*/
public static final int WEST_FLAG = 0x2;
/**
* The north direction flag.
*/
public static final int NORTH_FLAG = 0x4;
/**
* The east direction flag.
*/
public static final int EAST_FLAG = 0x8;
/**
* The south-west direction flag.
*/
public static final int SOUTH_WEST_FLAG = SOUTH_FLAG | WEST_FLAG;
/**
* The north-west direction flag.
*/
public static final int NORTH_WEST_FLAG = NORTH_FLAG | WEST_FLAG;
/**
* The south-east direction flag.
*/
public static final int SOUTH_EAST_FLAG = SOUTH_FLAG | EAST_FLAG;
/**
* The north-east direction flag.
*/
public static final int NORTH_EAST_FLAG = NORTH_FLAG | EAST_FLAG;
public static int flagForDirection(Direction d) {
switch(d) {
case NORTH_WEST: return NORTH_WEST_FLAG;
case NORTH: return NORTH_FLAG;
case NORTH_EAST: return NORTH_EAST_FLAG;
case WEST: return WEST_FLAG;
case EAST: return EAST_FLAG;
case SOUTH_WEST: return SOUTH_WEST_FLAG;
case SOUTH: return SOUTH_FLAG;
case SOUTH_EAST: return SOUTH_EAST_FLAG;
default: return 0;
}
}
public static final Pathfinder PROJECTILE = new RsmodProjectilePathfinder();
/**
* Finds a path from the location to the end location.
* @param location The start location.
* @param size The mover size.
* @param end The end location.
* @param sizeX The x-size of the destination node.
* @param sizeY The y-size of the destination node.
* @param rotation The object rotation.
* @param type The object type.
*
* @param location The start location.
* @param size The mover size.
* @param end The end location.
* @param sizeX The x-size of the destination node.
* @param sizeY The y-size of the destination node.
* @param rotation The object rotation.
* @param type The object type.
* @param walkingFlag The object walking flag.
* @param near If we should find the nearest location if a path can't be
* found.
* @param near If we should find the nearest location if a path can't be
* found.
* @return The path.
*/
public abstract Path find(Location location, int size, Location end, int sizeX, int sizeY, int rotation, int type, int walkingFlag, boolean near, ClipMaskSupplier clipMaskSupplier);
public abstract Path find(Location location,
int size,
Location end,
int sizeX,
int sizeY,
int rotation,
int type,
int walkingFlag,
boolean near,
ClipMaskSupplier clipMaskSupplier);
/**
* Finds a path from the start location to the end location.
* @param mover The moving entity.
*
* @param mover The moving entity.
* @param destination The destination node.
* @return The path.
*/
public static Path find(Entity mover, Node destination) {
return find(mover, destination, true, SMART);
}
/**
* Finds a path from the start location to the end location.
* @param mover The moving entity.
*
* @param mover The moving entity.
* @param destination The destination node.
* @param near If we should move near the end location, if we can't reach
* it.
* @param finder The pathfinder to use.
* @param near If we should move near the end location, if we can't reach
* it.
* @param finder The pathfinder to use.
* @return The path.
*/
public static Path find(Entity mover, Node destination, boolean near, Pathfinder finder) {
ClipMaskSupplier cms = null;
if (mover instanceof NPC) {
cms = ((NPC) mover).behavior.getClippingSupplier(((NPC) mover));
}
if (cms == null)
cms = RegionManager::getClippingFlag;
ClipMaskSupplier cms = null;
if (mover instanceof NPC) {
NPC npc = (NPC) mover;
cms = npc.behavior != null ? npc.behavior.getClippingSupplier(npc) : null;
}
return find(mover.getLocation(), mover.size(), destination, near, finder, cms);
}
public static Path findWater(Entity mover, Node destination, boolean near, Pathfinder finder){
return find(mover.getLocation(),mover.size(),destination,near,finder, RegionManager::getWaterClipFlag);
public static Path findWater(Entity mover, Node destination, boolean near, Pathfinder finder) {
return find(mover.getLocation(), mover.size(), destination, near, finder, RegionManager::getWaterClipFlag);
}
public static Path find(Entity mover, Node destination, boolean near, Pathfinder finder, ClipMaskSupplier clipMaskSupplier) {
return find(mover.getLocation(), mover.size(), destination, near, finder, clipMaskSupplier);
}
/**
* Finds a path from the start location to the end location.
*
* @param destination The destination node.
* @return The path.
*/
public static Path find(Location start, Node destination) {
return find(start, destination, true, SMART);
}
public static Path find(Location start, Node destination, int moverSize) {
return find(start, moverSize, destination, true, SMART, RegionManager::getClippingFlag);
}
public static Path find(Location start, Node destination, int moverSize) {
return find(start, moverSize, destination, true, SMART, null);
}
/**
* Finds a path from the start location to the end location.
*
* @param destination The destination node.
* @param near If we should move near the end location, if we can't reach
* it.
* @param finder The pathfinder to use.
* @param near If we should move near the end location, if we can't reach
* it.
* @param finder The pathfinder to use.
* @return The path.
*/
public static Path find(Location start, Node destination, boolean near, Pathfinder finder) {
return find(start, 1, destination, near, finder, RegionManager::getClippingFlag);
return find(start, 1, destination, near, finder, null);
}
/**
* Finds a path from the start location to the end location.
*
* @param destination The destination node.
* @param near If we should move near the end location, if we can't reach
* it.
* @param finder The pathfinder to use.
* @param near If we should move near the end location, if we can't reach
* it.
* @param finder The pathfinder to use.
* @return The path.
*/
public static Path find(Location start, int moverSize, Node destination, boolean near, Pathfinder finder, ClipMaskSupplier clipMaskSupplier) {
if (destination instanceof Scenery) {
Scenery object = (Scenery) destination;
Scenery object = getRouteScenery((Scenery) destination);
int type = object.getType();
int rotation = object.getRotation();
if (type == 10 || type == 11 || type == 22) {
int sizeX = object.getDefinition().sizeX;
int sizeY = object.getDefinition().sizeY;
if (rotation % 2 != 0) {
sizeX = object.getDefinition().sizeY;
sizeY = object.getDefinition().sizeX;
}
int walkingFlag = object.getDefinition().getWalkingFlag();
if (rotation != 0) {
walkingFlag = (walkingFlag << rotation & 0xf) + (walkingFlag >> 4 - rotation);
}
return finder.find(start, moverSize, destination.getLocation(), sizeX, sizeY, 0, 0, walkingFlag, near, clipMaskSupplier);
return finder.find(start,
moverSize,
object.getLocation(),
object.getDefinition().sizeX,
object.getDefinition().sizeY,
rotation,
type,
object.getDefinition().getWalkingFlag(),
near,
clipMaskSupplier);
}
return finder.find(start, moverSize, destination.getLocation(), 0, 0, rotation, 1 + type, 0, near, clipMaskSupplier);
return finder.find(start, moverSize, object.getLocation(), 0, 0, rotation, type, 0, near, clipMaskSupplier);
}
int size = 0;
if (destination instanceof Entity) {
@ -201,393 +159,83 @@ public abstract class Pathfinder {
} else if (destination instanceof GroundItem && !RegionManager.isTeleportPermitted(destination.getLocation())) {
size = 1;
}
return finder.find(start, moverSize, destination.getLocation(), size, size, 0, 0, 0, near, clipMaskSupplier);
return finder.find(start, moverSize, destination.getLocation(), size, size, 0, -1, 0, near, clipMaskSupplier);
}
private static Scenery getRouteScenery(Scenery object) {
Scenery wrapper = object.getWrapper();
if (wrapper == object) {
return object;
}
if (getFootprintArea(wrapper) <= getFootprintArea(object)) {
return object;
}
return wrapper;
}
private static int getFootprintArea(Scenery object) {
return object.getDefinition().sizeX * object.getDefinition().sizeY;
}
/**
* Checks if interaction with decoration is possible.
* @param curX The current x-coordinate in viewport.
* @param curY The current y-coordinate in viewport.
* @param size The mover size.
* @param destX The destination x-coordinate in viewport.
* @param destY The destination y-coordinate in viewport.
* @param type The object type.
*
* @param curX The current x-coordinate in viewport.
* @param curY The current y-coordinate in viewport.
* @param size The mover size.
* @param destX The destination x-coordinate in viewport.
* @param destY The destination y-coordinate in viewport.
* @param type The object type.
* @param rotation The object rotation.
* @return {@code True} if so.
*/
public static boolean canDecorationInteract(int curX, int curY, int size, int destX, int destY, int rotation, int type, int z, ClipMaskSupplier clipMaskSupplier) {
if (size != 1) {
if (destX >= curX && destX <= (curX + size) - 1 && destY <= (destY + size) - 1) {
return true;
}
} else if (destX == curX && curY == destY) {
return true;
}
if (size == 1) {
int flag = clipMaskSupplier.getClippingFlag(z, curX, curY);
if (type == 6 || type == 7) {
if (type == 7) {
rotation = rotation + 2 & 0x3;
}
if (rotation == 0) {
if (curX == 1 + destX && curY == destY && (0x80 & flag) == 0) {
return true;
}
if (destX == curX && curY == destY - 1 && (flag & 0x2) == 0) {
return true;
}
} else if (rotation == 1) {
if (curX == destX - 1 && curY == destY && (0x8 & flag) == 0) {
return true;
}
if (curX == destX && curY == destY - 1 && (flag & 0x2) == 0) {
return true;
}
} else if (rotation == 2) {
if (destX - 1 == curX && destY == curY && (flag & 0x8) == 0) {
return true;
}
if (destX == curX && destY + 1 == curY && (0x20 & flag) == 0) {
return true;
}
} else if (rotation == 3) {
if (destX + 1 == curX && curY == destY && (0x80 & flag) == 0) {
return true;
}
if (destX == curX && curY == destY + 1 && (0x20 & flag) == 0) {
return true;
}
}
}
if (type == 8) {
if (destX == curX && curY == destY + 1 && (flag & 0x20) == 0) {
return true;
}
if (destX == curX && -1 + destY == curY && (0x2 & flag) == 0) {
return true;
}
if (curX == destX - 1 && curY == destY && (0x8 & flag) == 0) {
return true;
}
if (curX == destX + 1 && curY == destY && (flag & 0x80) == 0) {
return true;
}
}
} else {
int cornerX = curX + size - 1;
int cornerY = curY + size - 1;
if (type == 6 || type == 7) {
if (type == 7) {
rotation = 0x3 & 2 + rotation;
}
if (rotation == 0) {
if (destX + 1 == curX && destY >= curY && destY <= cornerY && (clipMaskSupplier.getClippingFlag(z, curX, destY) & 0x80) == 0) {
return true;
}
if (destX >= curX && destX <= cornerX && destY - size == curY && (0x2 & clipMaskSupplier.getClippingFlag(z, destX, cornerY)) == 0) {
return true;
}
} else if (rotation == 1) {
if (-size + destX == curX && destY >= curY && cornerY >= destY && (clipMaskSupplier.getClippingFlag(z, cornerX, destY) & 0x8) == 0) {
return true;
}
if (curX <= destX && cornerX >= destX && -size + destY == curY && (clipMaskSupplier.getClippingFlag(z, destX, cornerY) & 0x2) == 0) {
return true;
}
} else if (rotation == 2) {
if (curX == destX - size && curY <= destY && destY <= cornerY && (0x8 & clipMaskSupplier.getClippingFlag(z, cornerX, destY)) == 0) {
return true;
}
if (curX <= destX && cornerX >= destX && destY + 1 == curY && (0x20 & clipMaskSupplier.getClippingFlag(z, destX, curY)) == 0) {
return true;
}
} else if (rotation == 3) {
if (1 + destX == curX && curY <= destY && destY <= cornerY && (0x80 & clipMaskSupplier.getClippingFlag(z, curX, destY)) == 0) {
return true;
}
if (destX >= curX && destX <= cornerX && 1 + destY == curY && (clipMaskSupplier.getClippingFlag(z, destX, curY) & 0x20) == 0) {
return true;
}
}
}
if (type == 8) {
if (curX <= destX && destX <= cornerX && 1 + destY == curY && (clipMaskSupplier.getClippingFlag(z, destX, curY) & 0x20) == 0) {
return true;
}
if (curX <= destX && destX <= cornerX && curY == -size + destY && (0x2 & clipMaskSupplier.getClippingFlag(z, destX, cornerY)) == 0) {
return true;
}
if (curX == -size + destX && destY >= curY && destY <= cornerY && (0x8 & clipMaskSupplier.getClippingFlag(z, cornerX, destY)) == 0) {
return true;
}
if (1 + destX == curX && curY <= destY && cornerY >= destY && (clipMaskSupplier.getClippingFlag(z, curX, destY) & 0x80) == 0) {
return true;
}
}
}
return false;
public static boolean canDecorationInteract(int curX,
int curY,
int size,
int destX,
int destY,
int rotation,
int type,
int z,
ClipMaskSupplier clipMaskSupplier) {
return RsmodPathfinder.canReach(curX, curY, size, destX, destY, 1, 1, rotation, type, 0, z, clipMaskSupplier);
}
/**
* Checks if interaction with a door is possible.
* @param curX The current x-coordinate in viewport.
* @param curY The current y-coordinate in viewport.
* @param size The mover size.
* @param destX The destination x-coordinate in viewport.
* @param destY The destination y-coordinate in viewport.
* @param type The object type.
*
* @param curX The current x-coordinate in viewport.
* @param curY The current y-coordinate in viewport.
* @param size The mover size.
* @param destX The destination x-coordinate in viewport.
* @param destY The destination y-coordinate in viewport.
* @param type The object type.
* @param rotation The object rotation.
* @return {@code True} if so.
*/
public static boolean canDoorInteract(int curX, int curY, int size, int destX, int destY, int type, int rotation, int z, ClipMaskSupplier clipMaskSupplier) {
if (size != 1) {
if (destX >= curX && destX <= size + curX - 1 && destY <= destY + size - 1) {
return true;
}
} else if (curX == destX && destY == curY) {
return true;
}
if (size == 1) {
if (type == 0) {
if (rotation == 0) {
if (curX == destX - 1 && destY == curY) {
return true;
}
if (destX == curX && 1 + destY == curY && (0x12c0120 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
return true;
}
if (curX == destX && destY - 1 == curY && (clipMaskSupplier.getClippingFlag(z, curX, curY) & 0x12c0102) == 0) {
return true;
}
} else if (rotation == 1) {
if (curX == destX && destY + 1 == curY) {
return true;
}
if (curX == destX - 1 && curY == destY && (0x12c0108 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
return true;
}
if (curX == 1 + destX && destY == curY && (0x12c0180 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
return true;
}
} else if (rotation == 2) {
if (1 + destX == curX && destY == curY) {
return true;
}
if (destX == curX && 1 + destY == curY && (0x12c0120 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
return true;
}
if (curX == destX && curY == destY - 1 && (clipMaskSupplier.getClippingFlag(z, curX, curY) & 0x12c0102) == 0) {
return true;
}
} else if (rotation == 3) {
if (curX == destX && -1 + destY == curY) {
return true;
}
if (curX == -1 + destX && destY == curY && (0x12c0108 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
return true;
}
if (curX == 1 + destX && destY == curY && (clipMaskSupplier.getClippingFlag(z, curX, curY) & 0x12c0180) == 0) {
return true;
}
}
} else if (type == 2) {
if (rotation == 0) {
if (destX - 1 == curX && curY == destY) {
return true;
}
if (destX == curX && curY == 1 + destY) {
return true;
}
if (curX == destX + 1 && curY == destY && (0x12c0180 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
return true;
}
if (curX == destX && destY - 1 == curY && (clipMaskSupplier.getClippingFlag(z, curX, curY) & 0x12c0102) == 0) {
return true;
}
} else if (rotation == 1) {
if (curX == destX - 1 && curY == destY && (0x12c0108 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
return true;
}
if (curX == destX && curY == 1 + destY) {
return true;
}
if (1 + destX == curX && curY == destY) {
return true;
}
if (curX == destX && destY - 1 == curY && (clipMaskSupplier.getClippingFlag(z, curX, curY) & 0x12c0102) == 0) {
return true;
}
} else if (rotation == 2) {
if (destX - 1 == curX && destY == curY && (0x12c0108 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
return true;
}
if (destX == curX && 1 + destY == curY && (0x12c0120 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
return true;
}
if (1 + destX == curX && curY == destY) {
return true;
}
if (curX == destX && curY == destY - 1) {
return true;
}
} else if (rotation == 3) {
if (destX - 1 == curX && curY == destY) {
return true;
}
if (destX == curX && curY == destY + 1 && (0x12c0120 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
return true;
}
if (curX == 1 + destX && curY == destY && (clipMaskSupplier.getClippingFlag(z, curX, curY) & 0x12c0180) == 0) {
return true;
}
if (destX == curX && destY - 1 == curY) {
return true;
}
}
} else if (type == 9) {
if (curX == destX && curY == destY + 1 && (clipMaskSupplier.getClippingFlag(z, curX, curY) & 0x20) == 0) {
return true;
}
if (curX == destX && curY == destY - 1 && (clipMaskSupplier.getClippingFlag(z, curX, curY) & 0x2) == 0) {
return true;
}
if (curX == destX - 1 && curY == destY && (0x8 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
return true;
}
if (destX + 1 == curX && curY == destY && (0x80 & clipMaskSupplier.getClippingFlag(z, curX, curY)) == 0) {
return true;
}
}
} else {
int cornerX = curX - (1 - size);
int cornerY = -1 + curY + size;
if (type == 0) {
if (rotation == 0) {
if (destX - size == curX && destY >= curY && destY <= cornerY) {
return true;
}
if (destX >= curX && cornerX >= destX && curY == 1 + destY && (clipMaskSupplier.getClippingFlag(z, destX, curY) & 0x12c0120) == 0) {
return true;
}
if (destX >= curX && cornerX >= destX && destY - size == curY && (clipMaskSupplier.getClippingFlag(z, destX, cornerY) & 0x12c0102) == 0) {
return true;
}
} else if (rotation == 1) {
if (destX >= curX && cornerX >= destX && destY + 1 == curY) {
return true;
}
if (curX == -size + destX && destY >= curY && cornerY >= destY && (0x12c0108 & clipMaskSupplier.getClippingFlag(z, cornerX, destY)) == 0) {
return true;
}
if (curX == 1 + destX && destY >= curY && cornerY >= destY && (clipMaskSupplier.getClippingFlag(z, curX, destY) & 0x12c0180) == 0) {
return true;
}
} else if (rotation == 2) {
if (curX == 1 + destX && curY <= destY && destY <= cornerY) {
return true;
}
if (curX <= destX && cornerX >= destX && destY + 1 == curY && (0x12c0120 & clipMaskSupplier.getClippingFlag(z, destX, curY)) == 0) {
return true;
}
if (destX >= curX && destX <= cornerX && destY - size == curY && (0x12c0102 & clipMaskSupplier.getClippingFlag(z, destX, cornerY)) == 0) {
return true;
}
} else if (rotation == 3) {
if (curX <= destX && destX <= cornerX && curY == -size + destY) {
return true;
}
if (-size + destX == curX && curY <= destY && destY <= cornerY && (clipMaskSupplier.getClippingFlag(z, cornerX, destY) & 0x12c0108) == 0) {
return true;
}
if (1 + destX == curX && curY <= destY && cornerY >= destY && (clipMaskSupplier.getClippingFlag(z, curX, destY) & 0x12c0180) == 0) {
return true;
}
}
}
if (type == 2) {
if (rotation == 0) {
if (destX - size == curX && curY <= destY && destY <= cornerY) {
return true;
}
if (curX <= destX && destX <= cornerX && curY == 1 + destY) {
return true;
}
if (curX == 1 + destX && curY <= destY && destY <= cornerY && (0x12c0180 & clipMaskSupplier.getClippingFlag(z, curX, destY)) == 0) {
return true;
}
if (curX <= destX && cornerX >= destX && -size + destY == curY && (clipMaskSupplier.getClippingFlag(z, destX, cornerY) & 0x12c0102) == 0) {
return true;
}
} else if (rotation == 1) {
if (-size + destX == curX && destY >= curY && destY <= cornerY && (clipMaskSupplier.getClippingFlag(z, cornerX, destY) & 0x12c0108) == 0) {
return true;
}
if (destX >= curX && cornerX >= destX && curY == 1 + destY) {
return true;
}
if (destX + 1 == curX && curY <= destY && destY <= cornerY) {
return true;
}
if (destX >= curX && cornerX >= destX && destY + -size == curY && (0x12c0102 & clipMaskSupplier.getClippingFlag(z, destX, cornerY)) == 0) {
return true;
}
} else if (rotation == 2) {
if (curX == destX - size && curY <= destY && cornerY >= destY && (clipMaskSupplier.getClippingFlag(z, cornerX, destY) & 0x12c0108) == 0) {
return true;
}
if (destX >= curX && destX <= cornerX && 1 + destY == curY && (0x12c0120 & clipMaskSupplier.getClippingFlag(z, destX, curY)) == 0) {
return true;
}
if (1 + destX == curX && destY >= curY && cornerY >= destY) {
return true;
}
if (curX <= destX && destX <= cornerX && curY == -size + destY) {
return true;
}
} else if (rotation == 3) {
if (destX + -size == curX && destY >= curY && destY <= cornerY) {
return true;
}
if (curX <= destX && cornerX >= destX && curY == 1 + destY && (clipMaskSupplier.getClippingFlag(z, destX, curY) & 0x12c0120) == 0) {
return true;
}
if (1 + destX == curX && destY >= curY && cornerY >= destY && (0x12c0180 & clipMaskSupplier.getClippingFlag(z, curX, destY)) == 0) {
return true;
}
if (destX >= curX && destX <= cornerX && curY == -size + destY) {
return true;
}
}
}
if (type == 9) {
if (destX >= curX && destX <= cornerX && curY == 1 + destY && (clipMaskSupplier.getClippingFlag(z, destX, curY) & 0x12c0120) == 0) {
return true;
}
if (destX >= curX && cornerX >= destX && curY == -size + destY && (0x12c0102 & clipMaskSupplier.getClippingFlag(z, destX, cornerY)) == 0) {
return true;
}
if (-size + destX == curX && destY >= curY && cornerY >= destY && (0x12c0108 & clipMaskSupplier.getClippingFlag(z, cornerX, destY)) == 0) {
return true;
}
if (curX == destX + 1 && destY >= curY && cornerY >= destY && (clipMaskSupplier.getClippingFlag(z, curX, destY) & 0x12c0180) == 0) {
return true;
}
}
}
return false;
public static boolean canDoorInteract(int curX,
int curY,
int size,
int destX,
int destY,
int type,
int rotation,
int z,
ClipMaskSupplier clipMaskSupplier) {
return RsmodPathfinder.canReach(curX, curY, size, destX, destY, 1, 1, rotation, type, 0, z, clipMaskSupplier);
}
/**
* Checks if the mover is standing on the destination.
* @param x The current x-location (in viewport).
* @param y The current y-location (in viewport).
*
* @param x The current x-location (in viewport).
* @param y The current y-location (in viewport).
* @param moverSizeX The mover x size.
* @param moverSizeY The mover y size.
* @param destX The destination x-location in viewport.
* @param destY The destination y-location in viewport.
* @param sizeX The destination node x-size.
* @param sizeY The destination node y-size.
* @param destX The destination x-location in viewport.
* @param destY The destination y-location in viewport.
* @param sizeX The destination node x-size.
* @param sizeY The destination node y-size.
* @return {@code True} if so.
*/
public static boolean isStandingIn(int x, int y, int moverSizeX, int moverSizeY, int destX, int destY, int sizeX, int sizeY) {
@ -599,120 +247,52 @@ public abstract class Pathfinder {
}
return true;
}
/**
* Checks if interaction is possible from the current location.
* @param x The current x-location (in viewport).
* @param y The current y-location (in viewport).
*
* @param x The current x-location (in viewport).
* @param y The current y-location (in viewport).
* @param moverSize The mover size.
* @param destX The destination x-location in viewport.
* @param destY The destination y-location in viewport.
* @param sizeX The destination node x-size.
* @param sizeY The destination node y-size.
* @param walkFlag The walking flag.
* @param destX The destination x-location in viewport.
* @param destY The destination y-location in viewport.
* @param sizeX The destination node x-size.
* @param sizeY The destination node y-size.
* @param walkFlag The walking flag.
* @return {@code True} if so.
*/
public static boolean canInteract(int x, int y, int moverSize, int destX, int destY, int sizeX, int sizeY, int walkFlag, int z, ClipMaskSupplier clipMaskSupplier) {
if (moverSize > 1) {
return isStandingIn(x, y, moverSize, moverSize, destX, destY, sizeX, sizeY) || canInteractSized(x, y, moverSize, moverSize, destX, destY, sizeX, sizeY, walkFlag, z);
}
int flag = clipMaskSupplier.getClippingFlag(z, x, y);
int cornerX = destX + sizeX - 1;
int cornerY = destY + sizeY - 1;
if (destX <= x && cornerX >= x && y >= destY && y <= cornerY) {
return true;
}
if (x == destX - 1 && destY <= y && y <= cornerY && (0x8 & flag) == 0 && (0x8 & walkFlag) == 0) {
return true;
}
if (x == cornerX + 1 && destY <= y && cornerY >= y && (flag & 0x80) == 0 && (0x2 & walkFlag) == 0) {
return true;
}
if (y == destY - 1 && destX <= x && cornerX >= x && (0x2 & flag) == 0 && (0x4 & walkFlag) == 0) {
return true;
}
if (y == cornerY + 1 && destX <= x && cornerX >= x && (flag & 0x20) == 0 && (0x1 & walkFlag) == 0) {
return true;
}
return false;
public static boolean canInteract(int x,
int y,
int moverSize,
int destX,
int destY,
int sizeX,
int sizeY,
int walkFlag,
int z,
ClipMaskSupplier clipMaskSupplier) {
return RsmodPathfinder.canReach(x, y, moverSize, destX, destY, sizeX, sizeY, 0, -1, walkFlag, z, clipMaskSupplier);
}
/**
* Checks if interaction is possible from the current location.
*
* @param destX The destination x-location in viewport.
* @param destY The destination y-location in viewport.
* @param sizeX The destination node x-size.
* @param sizeY The destination node y-size.
* @return {@code True} if so.
*/
public static boolean canInteractSized(int curX, int curY, int moverSizeX, int moverSizeY, int destX, int destY, int sizeX, int sizeY, int walkingFlag, int z) {
int fromCornerY = curY + moverSizeY;
int fromCornerX = curX + moverSizeX;
int toCornerX = sizeX + destX;
int toCornerY = sizeY + destY;
if (destX <= curX && curX < toCornerX) {
if (destY == fromCornerY && (walkingFlag & 0x4) == 0) {
int x = curX;
for (int endX = toCornerX < fromCornerX ? toCornerX : fromCornerX; endX > x; x++) {
if ((RegionManager.getClippingFlag(z, x, -1 + fromCornerY) & 0x2) == 0) {
return true;
}
}
} else if (toCornerY == curY && (walkingFlag & 0x1) == 0) {
int x = curX;
for (int endX = fromCornerX <= toCornerX ? fromCornerX : toCornerX; x < endX; x++) {
if ((RegionManager.getClippingFlag(z, x, curY) & 0x20) == 0) {
return true;
}
}
}
} else if (destX < fromCornerX && toCornerX >= fromCornerX) {
if (fromCornerY == destY && (0x4 & walkingFlag) == 0) {
for (int x = destX; fromCornerX > x; x++) {
if ((RegionManager.getClippingFlag(z, x, -1 + (fromCornerY)) & 0x2) == 0) {
return true;
}
}
} else if (toCornerY == curY && (0x1 & walkingFlag) == 0) {
for (int x = destX; fromCornerX > x; x++) {
if ((RegionManager.getClippingFlag(z, x, curY) & 0x20) == 0) {
return true;
}
}
}
} else if (curY < destY || curY >= toCornerY) {
if (fromCornerY > destY && toCornerY >= fromCornerY) {
if (fromCornerX == destX && (walkingFlag & 0x8) == 0) {
for (int y = destY; y < fromCornerY; y++) {
if ((RegionManager.getClippingFlag(z, -1 + fromCornerX, y) & 0x8) == 0) {
return true;
}
}
} else if (curX == toCornerX && (0x2 & walkingFlag) == 0) {
for (int y = destY; fromCornerY > y; y++) {
if ((RegionManager.getClippingFlag(z, curX, y) & 0x80) == 0) {
return true;
}
}
}
}
} else if (destX != fromCornerX || (0x8 & walkingFlag) != 0) {
if (curX == toCornerX && (walkingFlag & 0x2) == 0) {
int y = curY;
for (int endY = fromCornerY <= toCornerY ? fromCornerY : toCornerY; y < endY; y++) {
if ((0x80 & RegionManager.getClippingFlag(z, curX, y)) == 0) {
return true;
}
}
}
} else {
int y = curY;
for (int endY = fromCornerY > toCornerY ? toCornerY : fromCornerY; endY > y; y++) {
if ((RegionManager.getClippingFlag(z, fromCornerX - 1, y) & 0x8) == 0) {
return true;
}
}
}
return false;
public static boolean canInteractSized(int curX,
int curY,
int moverSizeX,
int moverSizeY,
int destX,
int destY,
int sizeX,
int sizeY,
int walkingFlag,
int z) {
return RsmodPathfinder.canReach(curX, curY, moverSizeX, destX, destY, sizeX, sizeY, 0, -1, walkingFlag, z, null);
}
}

View file

@ -1,200 +0,0 @@
package core.game.world.map.path;
import core.game.world.map.Direction;
import core.game.world.map.Location;
import core.game.world.map.Point;
import core.game.world.map.RegionManager;
import java.util.ArrayList;
import java.util.List;
/**
* A pathfinder implementation used for checking projectile paths.
* @author Emperor
*/
public final class ProjectilePathfinder extends Pathfinder {
/**
* If a path can be found.
*/
private boolean found;
/**
* The plane.
*/
private int z;
/**
* The x-coordinate.
*/
private int x;
/**
* The y-coordinate.
*/
private int y;
@Override
public Path find(Location start, int size, Location end, int sizeX, int sizeY, int rotation, int type, int walkingFlag, boolean near, ClipMaskSupplier clipMaskSupplier) {
Path path = new Path();
z = start.getZ();
x = start.getX();
y = start.getY();
List<Point> points = new ArrayList<>(20);
path.setSuccesful(true);
while (x != end.getX() || y != end.getY()) {
Direction[] directions = getDirection(x, y, end);
found = true;
checkSingleTraversal(points, directions);
if (!found) {
path.setMoveNear(x != start.getX() || y != start.getY());
path.setSuccesful(false);
break;
}
}
if (!points.isEmpty()) {
for (int i = 0; i < points.size() - 1; i++) {
Point p = points.get(i);
if (p.getDirection() != null) {
path.getPoints().add(p);
}
}
path.getPoints().add(points.get(points.size() - 1));
}
return path;
}
/**
* Checks traversal for a size 1 entity.
*
* @param points The points list.
* @param directions The directions.
*/
private void checkSingleTraversal(List<Point> points, Direction... directions) {
dir:
for (Direction dir : directions) {
found = true;
switch (dir) {
case NORTH:
if (flagged(z, x, y + 1, 0x12c0120)) {
found = false;
break dir;
}
points.add(new Point(x, y + 1, dir));
y++;
break;
case NORTH_EAST:
if (flagged(z, x + 1, y, 0x12c0180)
|| flagged(z, x, y + 1, 0x12c0120)
|| flagged(z, x + 1, y + 1, 0x12c01e0)) {
found = false;
break dir;
}
points.add(new Point(x + 1, y + 1, dir));
x++;
y++;
break;
case EAST:
if (flagged(z, x + 1, y, 0x12c0180)) {
found = false;
break dir;
}
points.add(new Point(x + 1, y, dir));
x++;
break;
case SOUTH_EAST:
if (flagged(z, x + 1, y, 0x12c0180)
|| flagged(z, x, y - 1, 0x12c0102)
|| flagged(z, x + 1, y - 1, 0x12c0183)) {
found = false;
break dir;
}
points.add(new Point(x + 1, y - 1, dir));
x++;
y--;
break;
case SOUTH:
if (flagged(z, x, y - 1, 0x12c0102)) {
found = false;
break dir;
}
points.add(new Point(x, y - 1, dir));
y--;
break;
case SOUTH_WEST:
if (flagged(z, x - 1, y, 0x12c0108)
|| flagged(z, x, y - 1, 0x12c0102)
|| flagged(z, x - 1, y - 1, 0x12c010e)) {
found = false;
break dir;
}
points.add(new Point(x - 1, y - 1, dir));
x--;
y--;
break;
case WEST:
if (flagged(z, x - 1, y, 0x12c0108)) {
found = false;
break dir;
}
points.add(new Point(x - 1, y, dir));
x--;
break;
case NORTH_WEST:
if (flagged(z, x - 1, y, 0x12c0108)
|| flagged(z, x, y + 1, 0x12c0120)
|| flagged(z, x - 1, y + 1, 0x12c0138)) {
found = false;
break dir;
}
points.add(new Point(x - 1, y + 1, dir));
x--;
y++;
break;
}
if (found) {
break;
}
}
}
private static boolean flagged(int z, int x, int y, int pFlagMask) {
int pFlag = RegionManager.getProjectileFlag(z, x, y);
return (pFlag & pFlagMask) != 0 || (pFlag & 0x20000) != 0 || (RegionManager.getClippingFlag(z, x, y) & 0x20000) != 0;
}
/**
* Gets the direction.
* @param startX The startX.
* @param startY The startY.
* @param end The end direction.
* @return The direction.
*/
private static Direction[] getDirection(int startX, int startY, Location end) {
int endX = end.getX();
int endY = end.getY();
if (startX == endX) {
if (startY > endY) {
return new Direction[] { Direction.SOUTH };
} else if (startY < endY) {
return new Direction[] { Direction.NORTH };
}
} else if (startY == endY) {
if (startX > endX) {
return new Direction[] { Direction.WEST };
}
return new Direction[] { Direction.EAST };
} else {
if (startX < endX && startY < endY) {
return new Direction[] { Direction.NORTH_EAST, Direction.EAST, Direction.NORTH };
} else if (startX < endX && startY > endY) {
return new Direction[] { Direction.SOUTH_EAST, Direction.EAST, Direction.SOUTH };
} else if (startX > endX && startY < endY) {
return new Direction[] { Direction.NORTH_WEST, Direction.WEST, Direction.NORTH };
} else if (startX > endX && startY > endY) {
return new Direction[] { Direction.SOUTH_WEST, Direction.WEST, Direction.SOUTH };
}
}
return new Direction[0];
}
}

View file

@ -0,0 +1,361 @@
package core.game.world.map.path
import core.ServerConstants
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.LineValidator
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(private val maxWaypoints: Int = 25) : Pathfinder() {
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?,
moverSize: Int,
dest: Location?,
sizeX: Int,
sizeY: Int,
rotation: Int,
type: Int,
walkingFlag: Int,
near: Boolean,
clipMaskSupplier: ClipMaskSupplier?,
): Path {
val source = requireNotNull(start)
val destination = requireNotNull(dest)
val path = Path()
var end = destination
val vector = Vector.betweenLocs(source, destination)
val magnitude = floor(vector.magnitude())
if (magnitude > ServerConstants.MAX_PATHFIND_DISTANCE) {
if (canAttempt(source, destination)) {
end =
source.transform(
vector.normalized() * (ServerConstants.MAX_PATHFIND_DISTANCE - 1)
)
} else {
path.isMoveNear = true
return path
}
}
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 =
finder.findPath(
level = source.z,
srcX = source.x,
srcZ = source.y,
destX = end.x,
destZ = end.y,
srcSize = moverSize,
destWidth = if (sizeX == 0) 1 else sizeX,
destHeight = if (sizeY == 0) 1 else sizeY,
objRot = rotation,
objShape = shape,
moveNear = near,
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) {
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
}
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
}
@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 {
if (clipMaskSupplier == null) {
RegionManager.loadClippingWindow(Location.create(srcX, srcY, z), SEARCH_MAP_SIZE)
return ReachStrategy.reached(
flags = RegionManager.RSMOD_CLIPPING_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,
)
}
// Reach checks only read tiles within the source/destination rectangles and
// their cross-axis combinations, so loading just their padded bounding box
// into a reused map produces the same reads as a freshly allocated full map.
val flags = suppliedReachFlags.get()
val destSize =
maxOf(if (destWidth == 0) 1 else destWidth, if (destHeight == 0) 1 else destHeight)
val minX = maxOf(0, minOf(srcX, destX) - 1)
val minY = maxOf(0, minOf(srcY, destY) - 1)
val maxX = maxOf(srcX + moverSize, destX + destSize) + 1
val maxY = maxOf(srcY + moverSize, destY + destSize) + 1
try {
for (x in minX..maxX) {
for (y in minY..maxY) {
flags[x, y, z] = clipMaskSupplier.getClippingFlag(z, x, y)
}
}
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,
)
} finally {
for (zoneX in (minX shr 3)..(maxX shr 3)) {
for (zoneY in (minY shr 3)..(maxY shr 3)) {
flags.deallocateIfPresent(zoneX shl 3, zoneY shl 3, z)
}
}
}
}
@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.coerceAtLeast(1),
destHeight = destHeight.coerceAtLeast(1),
)
}
@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 {
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)
for (targetX in 0 until targetSize) {
for (targetY in 0 until targetSize) {
if (maxRaySteps == 1) {
// A ray between distinct tiles emits a coordinate per axis
// step, so tiles further than one orthogonal step apart can
// never satisfy a single-step ray.
val manhattan =
kotlin.math.abs(source.x - (targetLocation.x + targetX)) +
kotlin.math.abs(source.y - (targetLocation.y + targetY))
if (manhattan > 1) {
continue
}
}
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 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 suppliedReachFlags = ThreadLocal.withInitial { CollisionFlagMap() }
private val projectileLineValidator = ThreadLocal.withInitial {
LineValidator(RegionManager.RSMOD_PROJECTILE_FLAGS)
}
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) {
for (y in baseY until baseY + SEARCH_MAP_SIZE) {
flags[x, y, start.z] = supplier.getClippingFlag(start.z, x, y)
}
}
}
}
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)
}
}
}

View file

@ -0,0 +1,40 @@
package core.game.world.map.path
import core.game.world.map.Location
import core.game.world.map.Point
class RsmodProjectilePathfinder : Pathfinder() {
override fun find(
start: Location?,
size: Int,
end: Location?,
sizeX: Int,
sizeY: Int,
rotation: Int,
type: Int,
walkingFlag: Int,
near: Boolean,
clipMaskSupplier: ClipMaskSupplier?,
): Path {
val source = requireNotNull(start)
val destination = requireNotNull(end)
val rayCast =
RsmodPathfinder.lineOfSight(
start = source,
dest = destination,
moverSize = size,
destWidth = sizeX,
destHeight = sizeY,
)
val path = Path()
for (coordinate in rayCast.coordinates) {
path.points.add(Point(coordinate.x, coordinate.z))
}
if (!rayCast.success) {
path.isMoveNear = rayCast.alternative
return path
}
path.setSuccesful(true)
return path
}
}

View file

@ -1,591 +0,0 @@
package core.game.world.map.path
import core.game.world.GameWorld
import core.game.world.map.Direction
import core.game.world.map.Location
import core.game.world.map.Point
import core.tools.*
import core.api.*
import core.api.utils.Vector
import core.ServerConstants
import java.util.Comparator
import java.util.PriorityQueue
import java.io.*
import javax.imageio.ImageIO
import java.awt.image.BufferedImage
class SmartPathfinder
/**
* Constructs a new `SmartPathfinder` `Object`.
*/
internal constructor() : Pathfinder() {
/**
* The x-queue.
*/
private var queueX: IntArray = intArrayOf(0)
/**
* The y-queue.
*/
private var queueY: IntArray = intArrayOf(0)
/**
* The "via" array.
*/
private var via: Array<IntArray> = Array(104) { IntArray(104) }
/**
* The cost array.
*/
private var cost: Array<IntArray> = Array(104) { IntArray(104) }
/**
* The current writing position.
*/
private var writePathPosition = 0
/**
* The current x-coordinate.
*/
private var curX = 0
/**
* The current y-coordinate.
*/
private var curY = 0
/**
* The destination x-coordinate.
*/
private var dstX = 0
/**
* The destination y-coordinate.
*/
private var dstY = 0
/**
* If a path was found.
*/
private var foundPath = false
/**
* Resets the pathfinder.
*/
fun reset() {
queueX = IntArray(4096)
queueY = IntArray(4096)
via = Array(104) { IntArray(104) }
cost = Array(104) { IntArray(104) }
writePathPosition = 0
}
/**
* Checks a tile.
* @param x The x-coordinate.
* @param y The y-coordinate.
* @param dir The direction.
* @param currentCost The current cost.
*/
fun check(x: Int, y: Int, dir: Int, currentCost: Int, diagonalPenalty: Int = 0) {
if(cost[x][y] > currentCost + diagonalPenalty) {
queueX[writePathPosition] = x
queueY[writePathPosition] = y
via[x][y] = dir
cost[x][y] = currentCost + diagonalPenalty
writePathPosition = writePathPosition + 1 and 0xfff
}
}
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!!)
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
vec = vec.normalized() * (ServerConstants.MAX_PATHFIND_DISTANCE - 1)
end = start!!.transform(vec)
} else throw Exception("Pathfinding distance exceeds server max! -> " + mag.toString() + " {" + start + "->" + end + "}")
} catch (e: Exception) {
val sw = StringWriter()
val pw = PrintWriter(sw)
e.printStackTrace(pw)
log(this::class.java, Log.FINE, sw.toString())
val p = Path()
p.isMoveNear = true
return p
}
}
val path = Path()
foundPath = false
for (x in 0..103) {
for (y in 0..103) {
via[x][y] = 0
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)
var attempts: Int
var readPosition: Int
check(curX, curY, 99, 0)
try {
if (moverSize < 2) {
if(GameWorld.settings?.smartpathfinder_bfs ?: false) {
checkSingleTraversal(end, sizeX, sizeY, type, rotation, walkingFlag, location, clipMaskSupplier!!)
} else {
checkSingleTraversalAstar(end, sizeX, sizeY, type, rotation, walkingFlag, location, clipMaskSupplier!!)
}
} else if (moverSize == 2) {
checkDoubleTraversal(end, sizeX, sizeY, type, rotation, walkingFlag, location, clipMaskSupplier!!)
} else {
checkVariableTraversal(end, moverSize, sizeX, sizeY, type, rotation, walkingFlag, location, clipMaskSupplier!!)
}
} catch (e: Exception) {}
var debugImg = if(false) { BufferedImage(4*104+2, 104, BufferedImage.TYPE_INT_RGB) } else { null }
if(debugImg != null) {
for(y in 0 until 104) {
for(x in 0 until 104) {
debugImg.setRGB(x, 103-y, via[x][y] * (((1 shl 24)-1)/12))
val c = Math.min(4*Math.min(cost[x][y], 64), 255)
debugImg.setRGB(105+x, 103-y, (c shl 16) or (c shl 8) or c)
debugImg.setRGB(2*105+x, 103-y, clipMaskSupplier!!.getClippingFlag(location.z, location.x + x, location.y + y))
}
}
}
if (!foundPath) {
if (near) {
var fullCost = 1000
var thisCost = 100
val depth = 10
for (x in dstX - depth..dstX + depth) {
for (y in dstY - depth..dstY + depth) {
if (x >= 0 && y >= 0 && x < 104 && y < 104 && cost[x][y] < 100) {
var diffX = 0
if (x < dstX) {
diffX = dstX - x
} else if (x > dstX + sizeX - 1) {
diffX = x - (dstX + sizeX - 1)
}
var diffY = 0
if (y < dstY) {
diffY = dstY - y
} else if (y > dstY + sizeY - 1) {
diffY = y - (dstY + sizeY - 1)
}
val totalCost = diffX * diffX + diffY * diffY
if (totalCost < fullCost || totalCost == fullCost && cost[x][y] < thisCost) {
fullCost = totalCost
thisCost = cost[x][y]
curX = x
curY = y
}
}
}
}
if (fullCost == 1000) {
return path
}
path.isMoveNear = true
}
}
readPosition = 0
queueX[readPosition] = curX
queueY[readPosition++] = curY
var previousDirection: Int
attempts = 0
var directionFlag = via[curX][curY].also { previousDirection = it }
while (curX != start.sceneX || curY != start.sceneY) {
if (++attempts > queueX.size) {
return path
}
previousDirection = directionFlag
queueX[readPosition] = curX
queueY[readPosition++] = curY
if (directionFlag and WEST_FLAG != 0) {
curX++
} else if (directionFlag and EAST_FLAG != 0) {
curX--
}
if (directionFlag and SOUTH_FLAG != 0) {
curY++
} else if (directionFlag and NORTH_FLAG != 0) {
curY--
}
if(debugImg != null) {
debugImg.setRGB(3*105+curX, 103-curY, 0x0000ff)
}
directionFlag = via[curX][curY]
}
if(debugImg != null) {
debugImg.setRGB(3*105+start.sceneX, 103-start.sceneY, 0xff0000)
debugImg.setRGB(3*105+dstX, 103-dstY, 0x00ff00)
if(GameWorld.settings?.smartpathfinder_bfs ?: false) {
ImageIO.write(debugImg, "png", File(String.format("bfs_%04d_%04d_%04d_%04d.png", start.x, start.y, end.x, end.y)))
} else {
ImageIO.write(debugImg, "png", File(String.format("astar_%04d_%04d_%04d_%04d.png", start.x, start.y, end.x, end.y)))
}
}
val size = readPosition--
var absX = location.x + queueX[readPosition]
var absY = location.y + queueY[readPosition]
path.points.add(Point(absX, absY))
for (i in 1 until size) {
readPosition--
absX = location.x + queueX[readPosition]
absY = location.y + queueY[readPosition]
path.points.add(Point(absX, absY))
}
path.setSuccesful(true)
if (end != dest)
path.isMoveNear = true
return path
}
class UIntAsPointComparator(val end: Location) : Comparator<UInt> {
override fun compare(p: UInt, q: UInt): Int {
val pc: UInt = (p and 0x00ff0000u) shr 16
val px: UInt = (p and 0x0000ff00u) shr 8
val py: UInt = (p and 0x000000ffu)
val qc: UInt = (q and 0x00ff0000u) shr 16
val qx: UInt = (q and 0x0000ff00u) shr 8
val qy: UInt = (q and 0x000000ffu)
//val dp = pc.toInt() + Math.abs(end.sceneX - (px.toInt())) + Math.abs(end.sceneY - (py.toInt()))
//val dq = qc.toInt() + Math.abs(end.sceneX - (qx.toInt())) + Math.abs(end.sceneY - (qy.toInt()))
val dp = pc.toDouble() + Math.max(Math.abs(end.sceneX - px.toInt()), Math.abs(end.sceneY - py.toInt())).toDouble()
val dq = qc.toDouble() + Math.max(Math.abs(end.sceneX - qx.toInt()), Math.abs(end.sceneY - qy.toInt())).toDouble()
if(dp < dq) {
return -1
} else if(dq < dp) {
return 1
} else {
return 0
}
}
override fun equals(other: Any?): Boolean {
if(other is UIntAsPointComparator) {
return end == other.end
} else {
return false
}
}
override fun hashCode(): Int {
return end.hashCode()
}
}
private fun checkSingleTraversalAstar(end: Location, sizeX: Int, sizeY: Int, type: Int, rotation: Int, walkingFlag: Int, location: Location, clipMaskSupplier: ClipMaskSupplier) {
val z = location.z
var queue = PriorityQueue(4096, UIntAsPointComparator(end))
queue.add(((curX.toUInt()) shl 8) or (curY.toUInt()))
while(!foundPath && !queue.isEmpty()) {
val point = queue.poll()
val curCost = ((point and 0xff0000u) shr 16).toInt()
curX = ((point and 0x0000ff00u) shr 8).toInt()
curY = (point and 0x000000ffu).toInt()
val absX = location.x + curX
val absY = location.y + curY
if (curX == dstX && curY == dstY) {
foundPath = true
break
}
if (type != 0) {
if ((type < 5 || type == 10) && canDoorInteract(absX, absY, 1, end.x, end.y, type - 1, rotation, z, clipMaskSupplier)) {
foundPath = true
break
}
if (type < 10 && canDecorationInteract(absX, absY, 1, end.x, end.y, type - 1, rotation, z, clipMaskSupplier)) {
foundPath = true
break
}
}
if (sizeX != 0 && sizeY != 0 && canInteract(absX, absY, 1, end.x, end.y, sizeX, sizeY, walkingFlag, z, clipMaskSupplier)) {
foundPath = true
break
}
val newCost = curCost + 1
//val orthogonalsFirst = arrayOf(Direction.EAST, Direction.NORTH, Direction.WEST, Direction.SOUTH, Direction.NORTH_EAST, Direction.NORTH_WEST, Direction.SOUTH_WEST, Direction.SOUTH_EAST)
val orthogonalsFirst = arrayOf(Direction.SOUTH, Direction.WEST, Direction.NORTH, Direction.EAST, Direction.SOUTH_WEST, Direction.NORTH_WEST, Direction.SOUTH_EAST, Direction.NORTH_EAST)
//val orthogonalsFirst = arrayOf(Direction.SOUTH, Direction.WEST, Direction.NORTH, Direction.EAST)
//for(dir in Direction.values()) {
for(dir in orthogonalsFirst) {
val newSceneX: Int = curX + dir.stepX
val newSceneY: Int = curY + dir.stepY
if(0 <= newSceneX && newSceneX < 104 && 0 <= newSceneY && newSceneY < 104 && via[newSceneX][newSceneY] == 0) {
if(dir.canMoveFrom(z, absX, absY, clipMaskSupplier)) {
val diagonalPenalty = Math.abs(dir.stepX) + Math.abs(dir.stepY) - 1
val flag = flagForDirection(dir)
check(newSceneX, newSceneY, flag, newCost, diagonalPenalty)
if(via[newSceneX][newSceneY] == flag) {
queue.add(((newCost + diagonalPenalty).toUInt() shl 16) or (newSceneX.toUInt() shl 8) or newSceneY.toUInt())
}
}
}
}
}
}
/**
* Checks possible traversal for a size 1 entity.
* @param end The destination location.
* @param sizeX The x-size of the destination.
* @param sizeY The y-size of the destination.
* @param type The object type.
* @param rotation The object rotation.
* @param walkingFlag The walking flag.
* @param location The viewport location.
*/
private fun checkSingleTraversal(end: Location, sizeX: Int, sizeY: Int, type: Int, rotation: Int, walkingFlag: Int, location: Location, clipMaskSupplier: ClipMaskSupplier) {
var readPosition = 0
val z = location.z
while (writePathPosition != readPosition) {
curX = queueX[readPosition]
curY = queueY[readPosition]
readPosition = readPosition + 1 and 0xfff
if (curX == dstX && curY == dstY) {
foundPath = true
break
}
try {
val absX = location.x + curX
val absY = location.y + curY
if (type != 0) {
if ((type < 5 || type == 10) && canDoorInteract(absX, absY, 1, end.x, end.y, type - 1, rotation, z, clipMaskSupplier)) {
foundPath = true
break
}
if (type < 10 && canDecorationInteract(absX, absY, 1, end.x, end.y, type - 1, rotation, z, clipMaskSupplier)) {
foundPath = true
break
}
}
if (sizeX != 0 && sizeY != 0 && canInteract(absX, absY, 1, end.x, end.y, sizeX, sizeY, walkingFlag, z, clipMaskSupplier)) {
foundPath = true
break
}
val thisCost = cost[curX][curY] + 1
if (curY > 0 && via[curX][curY - 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY - 1) and 0x12c0102 == 0) {
check(curX, curY - 1, SOUTH_FLAG, thisCost)
}
if (curX > 0 && via[curX - 1][curY] == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY) and 0x12c0108 == 0) {
check(curX - 1, curY, WEST_FLAG, thisCost)
}
if (curY < 103 && via[curX][curY + 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY + 1) and 0x12c0120 == 0) {
check(curX, curY + 1, NORTH_FLAG, thisCost)
}
if (curX < 103 && via[curX + 1][curY] == 0 && clipMaskSupplier.getClippingFlag(z, absX + 1, absY) and 0x12c0180 == 0) {
check(curX + 1, curY, EAST_FLAG, thisCost)
}
if (curX > 0 && curY > 0 && via[curX - 1][curY - 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY - 1) and 0x12c010e == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY) and 0x12c0108 == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY - 1) and 0x12c0102 == 0) {
check(curX - 1, curY - 1, SOUTH_WEST_FLAG, thisCost)
}
if (curX > 0 && curY < 103 && via[curX - 1][curY + 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY + 1) and 0x12c0138 == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY) and 0x12c0108 == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY + 1) and 0x12c0120 == 0) {
check(curX - 1, curY + 1, NORTH_WEST_FLAG, thisCost)
}
if (curX < 103 && curY > 0 && via[curX + 1][curY - 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX + 1, absY - 1) and 0x12c0183 == 0 && clipMaskSupplier.getClippingFlag(z, absX + 1, absY) and 0x12c0180 == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY - 1) and 0x12c0102 == 0) {
check(curX + 1, curY - 1, SOUTH_EAST_FLAG, thisCost)
}
if (curX < 103 && curY < 103 && via[curX + 1][curY + 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX + 1, absY + 1) and 0x12c01e0 == 0 && clipMaskSupplier.getClippingFlag(z, absX + 1, absY) and 0x12c0180 == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY + 1) and 0x12c0120 == 0) {
check(curX + 1, curY + 1, NORTH_EAST_FLAG, thisCost)
}
} catch (e: Exception) {
// e.printStackTrace()println("curX " + curX + " curY" + curY + " via " + via[curX + 1] + via[curY + 1])
}
}
}
/**
* Checks possible traversal for a size 2 entity.
* @param end The destination location.
* @param sizeX The x-size of the destination.
* @param sizeY The y-size of the destination.
* @param type The object type.
* @param rotation The object rotation.
* @param walkingFlag The walking flag.
* @param location The viewport location.
*/
private fun checkDoubleTraversal(end: Location, sizeX: Int, sizeY: Int, type: Int, rotation: Int, walkingFlag: Int, location: Location, clipMaskSupplier: ClipMaskSupplier) {
var readPosition = 0
val z = location.z
while (writePathPosition != readPosition) {
curX = queueX[readPosition]
curY = queueY[readPosition]
readPosition = readPosition + 1 and 0xfff
if (curX == dstX && curY == dstY) {
foundPath = true
break
}
val absX = location.x + curX
val absY = location.y + curY
if (type != 0) {
if ((type < 5 || type == 10) && canDoorInteract(absX, absY, 2, end.x, end.y, type - 1, rotation, z, clipMaskSupplier)) {
foundPath = true
break
}
if (type < 10 && canDecorationInteract(absX, absY, 2, end.x, end.y, type - 1, rotation, z, clipMaskSupplier)) {
foundPath = true
break
}
}
if (sizeX != 0 && sizeY != 0 && canInteract(absX, absY, 2, end.x, end.y, sizeX, sizeY, walkingFlag, z, clipMaskSupplier)) {
foundPath = true
break
}
val thisCost = cost[curX][curY] + 1
if (curY > 0 && via[curX][curY - 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY - 1) and 0x12c010e == 0 && clipMaskSupplier.getClippingFlag(z, absX + 1, absY - 1) and 0x12c0183 == 0) {
check(curX, curY - 1, SOUTH_FLAG, thisCost)
}
if (curX > 0 && via[curX - 1][curY] == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY) and 0x12c010e == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY + 1) and 0x12c0138 == 0) {
check(curX - 1, curY, WEST_FLAG, thisCost)
}
if (curY < 102 && via[curX][curY + 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY + 2) and 0x12c0138 == 0 && clipMaskSupplier.getClippingFlag(z, absX + 1, absY + 2) and 0x12c01e0 == 0) {
check(curX, curY + 1, NORTH_FLAG, thisCost)
}
if (curX < 102 && via[curX + 1][curY] == 0 && clipMaskSupplier.getClippingFlag(z, absX + 2, absY) and 0x12c0183 == 0 && clipMaskSupplier.getClippingFlag(z, absX + 2, absY + 1) and 0x12c01e0 == 0) {
check(curX + 1, curY, EAST_FLAG, thisCost)
}
if (curX > 0 && curY > 0 && via[curX - 1][curY - 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY - 1) and 0x12c010e == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY) and 0x12c0138 == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY - 1) and 0x12c0183 == 0) {
check(curX - 1, curY - 1, SOUTH_WEST_FLAG, thisCost)
}
if (curX > 0 && curY < 102 && via[curX - 1][curY + 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY + 1) and 0x12c010e == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY + 2) and 0x12c0138 == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY + 2) and 0x12c01e0 == 0) {
check(curX - 1, curY + 1, NORTH_WEST_FLAG, thisCost)
}
if (curX < 102 && curY > 0 && via[curX + 1][curY - 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX + 1, absY - 1) and 0x12c010e == 0 && clipMaskSupplier.getClippingFlag(z, absX + 2, absY) and 0x12c01e0 == 0 && clipMaskSupplier.getClippingFlag(z, absX + 2, absY - 1) and 0x12c0183 == 0) {
check(curX + 1, curY - 1, SOUTH_EAST_FLAG, thisCost)
}
if (curX < 102 && curY < 102 && via[curX + 1][curY + 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX + 1, absY + 2) and 0x12c0138 == 0 && clipMaskSupplier.getClippingFlag(z, absX + 2, absY + 2) and 0x12c01e0 == 0 && clipMaskSupplier.getClippingFlag(z, absX + 2, absY + 1) and 0x12c0183 == 0) {
check(curX + 1, curY + 1, NORTH_EAST_FLAG, thisCost)
}
}
}
/**
* Checks possible traversal for any sized entity.
* @param end The destination location.
* @param size The mover size.
* @param sizeX The x-size of the destination.
* @param sizeY The y-size of the destination.
* @param type The object type.
* @param rotation The object rotation.
* @param walkingFlag The walking flag.
* @param location The viewport location.
*/
private fun checkVariableTraversal(end: Location, size: Int, sizeX: Int, sizeY: Int, type: Int, rotation: Int, walkingFlag: Int, location: Location, clipMaskSupplier: ClipMaskSupplier) {
var readPosition = 0
val z = location.z
main@ while (writePathPosition != readPosition) {
curX = queueX[readPosition]
curY = queueY[readPosition]
readPosition = readPosition + 1 and 0xfff
if (curX == dstX && curY == dstY) {
foundPath = true
break
}
val absX = location.x + curX
val absY = location.y + curY
if (type != 0) {
if ((type < 5 || type == 10) && canDoorInteract(absX, absY, size, end.x, end.y, type - 1, rotation, z, clipMaskSupplier)) {
foundPath = true
break
}
if (type < 10 && canDecorationInteract(absX, absY, size, end.x, end.y, type - 1, rotation, z, clipMaskSupplier)) {
foundPath = true
break
}
}
if (sizeX != 0 && sizeY != 0 && canInteract(absX, absY, size, end.x, end.y, sizeX, sizeY, walkingFlag, z, clipMaskSupplier)) {
foundPath = true
break
}
val thisCost = cost[curX][curY] + 1
south@ do {
if (curY > 0 && via[curX][curY - 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY - 1) and 0x12c010e == 0 && clipMaskSupplier.getClippingFlag(z, absX + (size - 1), absY - 1) and 0x12c0183 == 0) {
for (i in 1 until size - 1) {
if (clipMaskSupplier.getClippingFlag(z, absX + i, absY - 1) and 0x12c018f != 0) {
break@south
}
}
check(curX, curY - 1, SOUTH_FLAG, thisCost)
}
} while (false)
west@ do {
if (curX > 0 && via[curX - 1][curY] == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY) and 0x12c010e == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY + (size - 1)) and 0x12c0138 == 0) {
for (i in 1 until size - 1) {
if (clipMaskSupplier.getClippingFlag(z, absX - 1, absY + i) and 0x12c013e != 0) {
break@west
}
}
check(curX - 1, curY, WEST_FLAG, thisCost)
}
} while (false)
north@ do {
if (curY < 102 && via[curX][curY + 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY + size) and 0x12c0138 == 0 && clipMaskSupplier.getClippingFlag(z, absX + (size - 1), absY + size) and 0x12c01e0 == 0) {
for (i in 1 until size - 1) {
if (clipMaskSupplier.getClippingFlag(z, absX + i, absY + size) and 0x12c01f8 != 0) {
break@north
}
}
check(curX, curY + 1, NORTH_FLAG, thisCost)
}
} while (false)
east@ do {
if (curX < 102 && via[curX + 1][curY] == 0 && clipMaskSupplier.getClippingFlag(z, absX + size, absY) and 0x12c0183 == 0 && clipMaskSupplier.getClippingFlag(z, absX + size, absY + (size - 1)) and 0x12c01e0 == 0) {
for (i in 1 until size - 1) {
if (clipMaskSupplier.getClippingFlag(z, absX + size, absY + i) and 0x12c01e3 != 0) {
break@east
}
}
check(curX + 1, curY, EAST_FLAG, thisCost)
}
} while (false)
southWest@ do {
if (curX > 0 && curY > 0 && via[curX - 1][curY - 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY + (size - 2)) and 0x12c0138 == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY - 1) and 0x12c010e == 0 && clipMaskSupplier.getClippingFlag(z, absX + (size - 2), absY - 1) and 0x12c0183 == 0) {
for (i in 1 until size - 1) {
if (clipMaskSupplier.getClippingFlag(z, absX - 1, absY + (i - 1)) and 0x12c013e != 0 || clipMaskSupplier.getClippingFlag(z, absX + (i - 1), absY - 1) and 0x12c018f != 0) {
break@southWest
}
}
check(curX - 1, curY - 1, SOUTH_WEST_FLAG, thisCost)
}
} while (false)
northWest@ do {
if (curX > 0 && curY < 102 && via[curX - 1][curY + 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY + 1) and 0x12c010e == 0 && clipMaskSupplier.getClippingFlag(z, absX - 1, absY + size) and 0x12c0138 == 0 && clipMaskSupplier.getClippingFlag(z, absX, absY + size) and 0x12c01e0 == 0) {
for (i in 1 until size - 1) {
if (clipMaskSupplier.getClippingFlag(z, absX - 1, absY + (i + 1)) and 0x12c013e != 0 || clipMaskSupplier.getClippingFlag(z, absX + (i - 1), absY + size) and 0x12c01f8 != 0) {
break@northWest
}
}
check(curX - 1, curY + 1, NORTH_WEST_FLAG, thisCost)
}
} while (false)
southEast@ do {
if (curX < 102 && curY > 0 && via[curX + 1][curY - 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX + 1, absY - 1) and 0x12c010e == 0 && clipMaskSupplier.getClippingFlag(z, absX + size, absY - 1) and 0x12c0183 == 0 && clipMaskSupplier.getClippingFlag(z, absX + size, absY + (size - 2)) and 0x12c01e0 == 0) {
for (i in 1 until size - 1) {
if (clipMaskSupplier.getClippingFlag(z, absX + size, absY + (i - 1)) and 0x12c01e3 != 0 || clipMaskSupplier.getClippingFlag(z, absX + (i + 1), absY - 1) and 0x12c018f != 0) {
break@southEast
}
}
check(curX + 1, curY - 1, SOUTH_EAST_FLAG, thisCost)
}
} while (false)
if (curX < 102 && curY < 102 && via[curX + 1][curY + 1] == 0 && clipMaskSupplier.getClippingFlag(z, absX + 1, absY + size) and 0x12c0138 == 0 && clipMaskSupplier.getClippingFlag(z, absX + size, absY + size) and 0x12c01e0 == 0 && clipMaskSupplier.getClippingFlag(z, absX + size, absY + 1) and 0x12c0183 == 0) {
for (i in 1 until size - 1) {
if (clipMaskSupplier.getClippingFlag(z, absX + (i + 1), absY + size) and 0x12c01f8 != 0 || clipMaskSupplier.getClippingFlag(z, absX + size, absY + (i + 1)) and 0x12c01e3 != 0) {
continue@main
}
}
check(curX + 1, curY + 1, NORTH_EAST_FLAG, thisCost)
}
}
}
}

View file

@ -296,7 +296,9 @@ class EasterEvent : WorldEvent("easter"), TickListener, InteractionListener, Log
val dir = dirs[RandomFunction.random(dirs.size)]
var loc = player.location.transform(dir, 3)
val path = Pathfinder.find(player, loc)
loc = Location.create(path.points.last.x, path.points.last.y, loc.z)
path.points.lastOrNull()?.let {
loc = Location.create(it.x, it.y, loc.z)
}
GroundItemManager.create(Item(eggs.random()), loc, player)
sendMessage(player, colorize("%RAn egg has appeared nearby."))
}
@ -317,4 +319,4 @@ class EasterEvent : WorldEvent("easter"), TickListener, InteractionListener, Log
WeightedItem(Items.DRAGON_IMPLING_JAR_11256, 1, 1, 0.005)
)
}
}
}

View file

@ -71,6 +71,12 @@ object PacketProcessor {
}
}
@JvmStatic fun clearQueue() {
synchronized(queueLock) {
queue.clear()
}
}
@JvmStatic fun processQueue() {
synchronized(queueLock) {
if (queue.isEmpty()) {

View file

@ -5,6 +5,7 @@ import core.ServerConstants
import core.ServerStore
import core.api.log
import core.api.submitWorldPulse
import core.game.node.entity.combat.CombatMovementIntents
import core.game.system.task.Pulse
import core.game.world.GameWorld
import core.game.world.repository.Repository
@ -20,6 +21,7 @@ import java.lang.Long.max
import java.text.SimpleDateFormat
import java.util.*
import kotlin.system.exitProcess
import kotlin.system.measureTimeMillis
/**
* Handles the running of pulses and writing of masks, etc
@ -118,6 +120,13 @@ class MajorUpdateWorker {
GameWorld.Pulser.updateAll()
}
GameWorld.tickListeners.forEach { it.tick() }
val meleePressureTime = measureTimeMillis {
CombatMovementIntents.requestActiveMeleePressure()
}
val movementResolveTime = measureTimeMillis {
CombatMovementIntents.resolve()
}
notifyIfCombatMovementTooLong(meleePressureTime, movementResolveTime)
sequence.start()
sequence.run()
@ -138,6 +147,29 @@ 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, $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, $resolveSummary"
)
}
}
companion object {
private const val LONG_COMBAT_MOVEMENT_MS = 50L
private const val CRITICAL_COMBAT_MOVEMENT_MS = 100L
}
fun start() {
if (!started) {
running = true

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,209 @@
package content
import TestUtils
import core.ServerConstants
import core.game.node.entity.combat.CombatMovementIntents
import core.game.node.entity.combat.equipment.WeaponInterface
import core.game.node.entity.player.Player
import core.game.node.entity.skill.Skills
import core.game.world.GameWorld
import core.game.world.map.Location
import core.game.world.repository.Repository
import core.game.world.update.UpdateSequence
import core.net.packet.PacketProcessor
import core.tools.LogLevel
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import java.util.concurrent.TimeUnit
@Disabled // ENABLE LOCALLY ONLY
class CombatPerformanceTests {
init {
TestUtils.preTestSetup()
}
@Test
fun serverTickStaysWithinBudgetWithLiveSizedCombatLoad() {
withQuietPerformanceLogs {
var load: CombatLoad? = null
try {
load = createCombatLoad()
assertEquals(REAL_PLAYER_COUNT, load.players.count { !it.isArtificial })
assertEquals(BOT_PLAYER_COUNT, load.players.count { it.isArtificial })
repeat(WARMUP_TICKS) {
measureCombatTick(load, it)
}
val durations =
LongArray(MEASURED_TICKS) { tick ->
measureCombatTick(load, tick + WARMUP_TICKS)
}
val sorted = durations.sorted()
val p90Index = ((sorted.size * 9 + 9) / 10 - 1).coerceIn(0, sorted.lastIndex)
val p90 = sorted[p90Index]
val max = sorted.last()
val durationText = durations.joinToString(prefix = "[", postfix = "]")
assertTrue(
p90 <= HEADROOM_TICK_BUDGET_MILLIS,
"650-player combat p90 tick time should leave room for slower live hardware. " +
"durations=${durationText}ms, p90=${p90}ms, " +
"budget=${HEADROOM_TICK_BUDGET_MILLIS}ms",
)
assertTrue(
max <= LIVE_TICK_BUDGET_MILLIS,
"650-player combat tick should remain under the live 600ms server tick budget. " +
"durations=${durationText}ms, max=${max}ms",
)
} finally {
load?.close()
}
}
}
private fun <T> withQuietPerformanceLogs(action: () -> T): T {
val previousLogLevel = ServerConstants.LOG_LEVEL
ServerConstants.LOG_LEVEL = LogLevel.CAUTIOUS
try {
return action()
} finally {
ServerConstants.LOG_LEVEL = previousLogLevel
}
}
private fun createCombatLoad(): CombatLoad {
val players = ArrayList<Player>(TOTAL_PLAYER_COUNT)
val previousWildPvp = GameWorld.settings!!.wild_pvp_enabled
for (i in 0 until TOTAL_PLAYER_COUNT) {
val player = TestUtils.getMockPlayer("combat_perf_$i", isBot = i >= REAL_PLAYER_COUNT)
players.add(player)
configureMeleePlayer(player)
}
val pairs =
players.chunked(2).mapIndexed { index, pair ->
CombatPair(pair[0], pair[1], pairOrigin(index))
}
val load = CombatLoad(players, pairs, previousWildPvp)
GameWorld.settings!!.wild_pvp_enabled = true
load.resetPairPositionsAndMovement(0)
for (pair in pairs) {
pair.first.attack(pair.second)
pair.second.attack(pair.first)
}
return load
}
private fun measureCombatTick(load: CombatLoad, tick: Int): Long {
PacketProcessor.clearQueue()
load.resetPairPositionsAndMovement(tick)
load.requestAllCombatMovement()
assertEquals(
TOTAL_PLAYER_COUNT,
CombatMovementIntents.pendingCount(),
"The performance fixture should exercise one combat movement intent per loaded player.",
)
val start = System.nanoTime()
GameWorld.majorUpdateWorker.handleTickActions(false)
return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)
}
private fun configureMeleePlayer(player: Player) {
player.properties.attackStyle =
WeaponInterface.AttackStyle(
WeaponInterface.STYLE_AGGRESSIVE,
WeaponInterface.BONUS_CRUSH,
)
player.properties.combatPulse.updateStyle()
player.properties.combatLevel = 126
player.skills.setStaticLevel(Skills.HITPOINTS, 10_000)
player.skills.lifepoints = 10_000
player.settings.runEnergy = 100.0
player.settings.setRunToggled(true)
player.skullManager.isWilderness = true
player.skullManager.level = 50
}
private data class CombatPair(
val first: Player,
val second: Player,
val origin: Location,
)
private class CombatLoad(
val players: List<Player>,
private val pairs: List<CombatPair>,
private val previousWildPvp: Boolean,
) : AutoCloseable {
fun resetPairPositionsAndMovement(tick: Int) {
val direction = if (tick % 2 == 0) 1 else -1
for (pair in pairs) {
val firstLocation = pair.origin
val secondLocation = pair.origin.transform(1, 0, 0)
place(pair.first, firstLocation)
place(pair.second, secondLocation)
queueRun(pair.first, firstLocation.transform(-8 * direction, 0, 0))
queueRun(pair.second, secondLocation.transform(8 * direction, 0, 0))
}
}
fun requestAllCombatMovement() {
CombatMovementIntents.clear()
for (pair in pairs) {
CombatMovementIntents.request(pair.first, pair.second)
CombatMovementIntents.request(pair.second, pair.first)
}
}
override fun close() {
CombatMovementIntents.clear()
for (player in players.asReversed()) {
player.pulseManager.clear()
player.walkingQueue.reset()
player.isActive = false
player.setPlaying(false)
Repository.removePlayer(player)
UpdateSequence.renderablePlayers.remove(player)
}
GameWorld.Pulser.updateAll()
UpdateSequence.renderablePlayers.sync()
PacketProcessor.clearQueue()
GameWorld.settings!!.wild_pvp_enabled = previousWildPvp
}
private fun place(player: Player, location: Location) {
player.location = location
player.walkingQueue.reset()
}
private fun queueRun(player: Player, destination: Location) {
player.walkingQueue.reset(true)
player.walkingQueue.addPath(destination.x, destination.y)
}
}
private companion object {
const val REAL_PLAYER_COUNT = 150
const val BOT_PLAYER_COUNT = 500
const val TOTAL_PLAYER_COUNT = REAL_PLAYER_COUNT + BOT_PLAYER_COUNT
const val WARMUP_TICKS = 3
const val MEASURED_TICKS = 10
const val LIVE_TICK_BUDGET_MILLIS = 600L
const val HEADROOM_TICK_BUDGET_MILLIS = 350L
fun pairOrigin(index: Int): Location {
val columns = 25
val column = index % columns
val row = index / columns
return Location.create(3200 + column * 12, 3600 + row * 6, 0)
}
}
}

View file

@ -2,15 +2,20 @@ package content
import TestUtils
import content.global.handlers.item.equipment.special.ChinchompaSwingHandler
import core.ServerConstants
import core.api.EquipmentSlot
import core.game.container.impl.EquipmentContainer.updateBonuses
import core.game.interaction.IntType
import core.game.interaction.InteractionListeners
import core.game.node.entity.combat.CombatMovementIntents
import core.game.node.entity.combat.CombatMovementPlanner
import core.game.node.entity.combat.CombatReach
import core.game.node.entity.combat.MagicSwingHandler
import core.game.node.entity.combat.MeleeSwingHandler
import core.game.node.entity.combat.RangeSwingHandler
import core.game.node.entity.combat.SwingHandlerFlag
import core.game.node.entity.combat.equipment.WeaponInterface
import core.game.node.entity.npc.NPC
import core.game.node.entity.player.link.prayer.PrayerType
import core.game.node.entity.skill.Skills
import core.game.node.item.Item
@ -135,4 +140,89 @@ class CombatTests {
Assertions.assertEquals(damageBaseline, handler.calculateHit(p, p, 1.0))
}
}
}
@Test
fun combatReachUsesOccupiedTilesForLargeMeleeTargets() {
TestUtils.getMockPlayer("combatReachLargeTarget").use { attacker ->
val origin = ServerConstants.HOME_LOCATION!!.transform(32, 32, 0)
val victim = NPC.create(100, origin.transform(1, 0, 0))
victim.setSize(2)
attacker.location = origin
Assertions.assertTrue(CombatReach.canMelee(attacker, victim, 1))
Assertions.assertTrue(MeleeSwingHandler.canMelee(attacker, victim, 1))
}
}
@Test
fun combatMovementPlannerPredictsRunningTargetSteps() {
TestUtils.getMockPlayer("combatPlannerRunner").use { target ->
val origin = ServerConstants.HOME_LOCATION!!.transform(32, 32, 0)
target.location = origin
target.walkingQueue.reset(true)
target.walkingQueue.addPath(origin.x + 3, origin.y)
Assertions.assertEquals(
listOf(origin.transform(1, 0, 0), origin.transform(2, 0, 0)),
CombatMovementPlanner.predictTargetLocations(target)
)
Assertions.assertEquals(origin.transform(2, 0, 0), CombatMovementPlanner.predictedTargetLocation(target))
}
}
@Test
fun combatMovementPlannerTreatsZeroEnergyRunningPlayerAsWalking() {
TestUtils.getMockPlayer("combatPlannerNoEnergyRunner").use { target ->
val origin = ServerConstants.HOME_LOCATION!!.transform(32, 32, 0)
target.location = origin
target.settings.runEnergy = 0.0
target.walkingQueue.reset(true)
target.walkingQueue.addPath(origin.x + 3, origin.y)
Assertions.assertEquals(1, CombatMovementPlanner.movementStepsThisTick(target))
Assertions.assertEquals(
listOf(origin.transform(1, 0, 0)),
CombatMovementPlanner.predictTargetLocations(target)
)
Assertions.assertEquals(origin.transform(1, 0, 0), CombatMovementPlanner.predictedTargetLocation(target))
}
}
@Test
fun combatMovementPlannerChoosesClosestTargetBorderTile() {
TestUtils.getMockPlayer("combatPlannerAttacker").use { attacker ->
val origin = ServerConstants.HOME_LOCATION!!.transform(32, 32, 0)
val victim = NPC.create(100, origin.transform(4, 0, 0))
victim.setSize(2)
attacker.location = origin
Assertions.assertEquals(
origin.transform(3, 0, 0),
CombatMovementPlanner.chooseTargetBorderTile(attacker, victim, victim.location)
)
}
}
@Test
fun combatMovementIntentResolverAppliesPendingMeleePathBeforeEntityMovement() {
TestUtils.getMockPlayer("combatIntentAttacker").use { attacker ->
TestUtils.getMockPlayer("combatIntentVictim").use { victim ->
val origin = ServerConstants.HOME_LOCATION!!.transform(32, 32, 0)
attacker.location = origin
victim.location = origin.transform(4, 0, 0)
attacker.properties.attackStyle = WeaponInterface.AttackStyle(
WeaponInterface.STYLE_AGGRESSIVE,
WeaponInterface.BONUS_CRUSH
)
attacker.properties.combatPulse.updateStyle()
attacker.attack(victim)
CombatMovementIntents.request(attacker, victim)
CombatMovementIntents.resolve()
Assertions.assertTrue(attacker.walkingQueue.hasPath())
Assertions.assertEquals(0, CombatMovementIntents.pendingCount())
}
}
}
}

View file

@ -0,0 +1,266 @@
package content
import MockSession
import TestUtils
import content.global.skill.thieving.StallThiefPulse
import content.global.skill.thieving.ThievingOptionPlugin
import core.game.node.entity.Entity
import core.game.node.entity.combat.CombatMovementIntents
import core.game.node.entity.npc.NPC
import core.game.node.entity.player.Player
import core.game.node.entity.skill.Skills
import core.game.world.map.Location
import core.game.world.map.RegionManager
import core.game.world.map.path.Pathfinder
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
/**
* Regression tests for the Ardougne market stall mechanic: a failed steal must never
* fail silently. Any guard in range busts the steal; a guard whose attack can actually
* connect is preferred over one boxed in behind a stall or inside the guardhouse, so
* the shout is followed by a real attack whenever possible.
*/
class StallGuardReactionTests {
init {
TestUtils.preTestSetup()
}
private fun findBakersStall(): core.game.node.scenery.Scenery {
core.game.world.map.Region.load(RegionManager.forId(10547))
val bakerIds = setOf(2561, 6163, 34384)
for (x in 2650..2680) {
for (y in 3295..3325) {
val obj = RegionManager.getObject(0, x, y) ?: continue
if (obj.id in bakerIds) {
return obj
}
}
}
throw AssertionError("No baker's stall found in the Ardougne market square.")
}
private fun adjacentWalkableTile(stall: core.game.node.scenery.Scenery): Location {
val base = stall.location
val candidates = ArrayList<Location>()
for (dx in -1..2) {
for (dy in -1..2) {
if (dx in 0..1 && dy in 0..1) continue
candidates.add(base.transform(dx, dy, 0))
}
}
return candidates.firstOrNull { RegionManager.isTeleportPermitted(it) }
?: throw AssertionError("No walkable tile adjacent to the stall at $base.")
}
private fun place(entity: Entity, location: Location) {
entity.location = location
RegionManager.move(entity)
entity.walkingQueue.reset()
}
private fun assertGuardAttacks(guard: NPC, player: Player, scenario: String) {
TestUtils.advanceTicks(30, false)
assertTrue(
player.inCombat(),
"[$scenario] Player should have been hit by the guard. " +
"guard=${guard.location}, attacking=${guard.properties.combatPulse.isAttacking}, " +
"player=${player.location}, ${CombatMovementIntents.lastResolveSummary()}",
)
}
private val movementBlockFlag =
Pathfinder.PREVENT_NORTH or
Pathfinder.PREVENT_EAST or
Pathfinder.PREVENT_SOUTH or
Pathfinder.PREVENT_WEST
/** Finds an origin whose east and north arms (4 tiles each) are fully walkable. */
private fun openCrossOrigin(): Location {
val start = Location.create(3200, 3600, 0)
for (dy in -16..16) {
for (dx in -16..16) {
val candidate = start.transform(dx, dy, 0)
val armsOpen = (0..4).all {
RegionManager.isTeleportPermitted(candidate.transform(it, 0, 0)) &&
RegionManager.isTeleportPermitted(candidate.transform(0, it, 0))
}
if (armsOpen) {
return candidate
}
}
}
throw AssertionError("No open cross-shaped area found near $start.")
}
@Test
fun blockedGuardStillBustsTheStealButReachableGuardIsPreferred() {
TestUtils.getMockPlayer("stall_guard_selection").use { player ->
val origin = openCrossOrigin()
place(player, origin)
// Wall column two tiles east of the player, between them and the guard.
val wall = (-2..2).map { origin.transform(2, it, 0) }
val blockedGuard = NPC.create(32, origin.transform(4, 0, 0))
try {
wall.forEach { RegionManager.addClippingFlag(it.z, it.x, it.y, false, movementBlockFlag) }
blockedGuard.init()
place(blockedGuard, origin.transform(4, 0, 0))
assertSame(
blockedGuard,
StallThiefPulse.findGuardForFailedSteal(player),
"A guard whose chase route is blocked must still bust the steal.",
)
val reachableGuard = NPC.create(32, origin.transform(0, 4, 0))
try {
reachableGuard.init()
place(reachableGuard, origin.transform(0, 4, 0))
assertSame(
reachableGuard,
StallThiefPulse.findGuardForFailedSteal(player),
"The reachable guard should be picked over the blocked one.",
)
} finally {
reachableGuard.clear()
}
} finally {
wall.forEach { RegionManager.removeClippingFlag(it.z, it.x, it.y, false, movementBlockFlag) }
blockedGuard.clear()
}
}
}
@Test
fun guardBusyFightingSomeoneElseDoesNotBustTheSteal() {
TestUtils.getMockPlayer("stall_guard_busy").use { player ->
TestUtils.getMockPlayer("stall_guard_other_victim").use { other ->
val origin = openCrossOrigin()
place(player, origin)
place(other, origin.transform(4, 1, 0))
val guard = NPC.create(32, origin.transform(4, 0, 0))
try {
guard.init()
place(guard, origin.transform(4, 0, 0))
guard.properties.combatPulse.attack(other)
assertNull(
StallThiefPulse.findGuardForFailedSteal(player),
"A guard busy fighting someone else should not notice the thief.",
)
guard.properties.combatPulse.stop()
assertSame(
guard,
StallThiefPulse.findGuardForFailedSteal(player),
"Once free, the same guard should bust the thief again.",
)
} finally {
guard.clear()
CombatMovementIntents.clear()
}
}
}
}
@Test
fun ardougneGuardShouldAttackPlayerCaughtStealing() {
TestUtils.getMockPlayer("stall_thief").use { player ->
val stall = findBakersStall()
place(player, adjacentWalkableTile(stall))
player.skills.setStaticLevel(Skills.HITPOINTS, 99)
player.skills.lifepoints = 5000
player.properties.isRetaliating = false
val guardSpawn = Location.create(2661, 3309, 0)
val guard = NPC.create(32, guardSpawn)
guard.init()
try {
place(guard, guardSpawn)
val caughtBy = StallThiefPulse.findGuardForFailedSteal(player)
assertNotNull(caughtBy, "A guard should be found near the stall.")
caughtBy!!.sendChat("Hey! Get your hands off there!")
caughtBy.properties.combatPulse.attack(player)
assertGuardAttacks(caughtBy, player, "open-path stall=${stall.location}")
} finally {
guard.clear()
CombatMovementIntents.clear()
}
}
}
@Test
fun walkingGuardShouldStillAttackWhenCaught() {
TestUtils.getMockPlayer("stall_thief_walker").use { player ->
val stall = findBakersStall()
place(player, adjacentWalkableTile(stall))
player.skills.setStaticLevel(Skills.HITPOINTS, 99)
player.skills.lifepoints = 5000
player.properties.isRetaliating = false
val guardSpawn = Location.create(2661, 3309, 0)
val guard = NPC.create(32, guardSpawn)
guard.init()
try {
// Guard is mid-wander when the player gets caught.
guard.walkingQueue.reset(false)
guard.walkingQueue.addPath(guardSpawn.x - 3, guardSpawn.y)
TestUtils.advanceTicks(1, false)
guard.sendChat("Hey! Get your hands off there!")
guard.properties.combatPulse.attack(player)
assertGuardAttacks(guard, player, "walking-guard stall=${stall.location}")
} finally {
guard.clear()
CombatMovementIntents.clear()
}
}
}
/**
* End-to-end: clicking Steal-from must start the thieving attempt from every
* walkable tile around the stall. A tile from which the click silently does
* nothing (no attempt, no message) reproduces the reported bug.
*/
@Test
fun stealFromClickShouldStartAttemptFromEveryNearbyTile() {
ThievingOptionPlugin().newInstance(null)
TestUtils.getMockPlayer("stall_click_thief").use { player ->
val stall = findBakersStall()
player.skills.setStaticLevel(Skills.THIEVING, 99)
player.skills.setLevel(Skills.THIEVING, 99)
player.skills.setStaticLevel(Skills.HITPOINTS, 99)
player.properties.isRetaliating = false
val optIndex = stall.interaction.options.indexOfFirst {
it != null && it.name.equals("steal-from", ignoreCase = true)
}
assertTrue(optIndex >= 0, "Stall has no Steal-from option: ${stall.interaction.options?.map { it?.name }}")
val base = stall.location
val silent = ArrayList<Location>()
var attempts = 0
for (dx in -2..3) {
for (dy in -2..3) {
if (dx in 0..1 && dy in 0..1) continue // stall footprint
val tile = base.transform(dx, dy, 0)
if (!RegionManager.isTeleportPermitted(tile)) continue
place(player, tile)
player.removeAttribute("thieveDelay")
player.removeAttribute("combat-time")
player.properties.combatPulse.stop()
player.inventory.clear()
player.skills.lifepoints = 5000
attempts++
val session = player.session as MockSession
session.receivedPackets.clear()
TestUtils.simulateInteraction(player, stall, optIndex)
TestUtils.advanceTicks(10, false)
if (player.getAttribute<Any?>("thieveDelay", null) == null) {
silent.add(tile)
}
TestUtils.advanceTicks(10, false) // let locks/stall respawn settle
}
}
assertTrue(attempts > 0, "No walkable tiles found around the stall.")
assertTrue(
silent.isEmpty(),
"Steal-from click did nothing from ${silent.size}/$attempts tiles: $silent (stall=$base)",
)
}
}
}

View file

@ -1,53 +1,653 @@
package core
import TestUtils
import content.global.handlers.npc.NPCTalkListener
import content.global.handlers.scenery.BankBoothListener
import content.global.skill.gather.GatheringSkillOptionListeners
import content.global.skill.gather.woodcutting.WoodcuttingListener
import content.region.misthalin.varrock.dialogue.GrandExchangeClerk
import content.region.misthalin.varrock.handlers.GrandExchangePlugin
import core.api.log
import core.cache.def.impl.NPCDefinition
import core.game.dialogue.DialogueInterpreter
import core.game.interaction.*
import core.game.node.scenery.Scenery
import core.game.world.map.Location
import core.game.world.map.RegionManager
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import core.game.node.Node
import core.game.node.entity.impl.PulseType
import core.game.node.entity.npc.NPC
import core.game.node.entity.player.Player
import core.game.node.scenery.Scenery
import core.game.world.GameWorld
import core.game.world.map.Region
import core.net.packet.PacketProcessor
import core.plugin.ClassScanner
import core.game.world.map.Direction
import core.game.world.map.Location
import core.game.world.map.RegionManager
import core.game.world.map.path.ClipMaskSupplier
import core.game.world.map.path.Pathfinder
import core.game.world.map.path.RsmodPathfinder
import core.plugin.Plugin
import core.tools.Log
import org.rs09.consts.NPCs
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.rs09.consts.Scenery as SceneryIds
class PathfinderTests {
companion object {init {TestUtils.preTestSetup(); GatheringSkillOptionListeners().defineListeners(); WoodcuttingListener().defineListeners() }; val NPC_TEST_LOC = ServerConstants.HOME_LOCATION!!.transform(2, 10, 0)}
companion object {
init {
TestUtils.preTestSetup()
GatheringSkillOptionListeners().defineListeners()
WoodcuttingListener().defineListeners()
BankBoothListener().defineListeners()
}
@Test fun getOccupiedTilesShouldReturnCorrectSetOfTilesThatAnObjectOccupiesAtAllRotations() {
val NPC_TEST_LOC = ServerConstants.HOME_LOCATION!!.transform(2, 10, 0)
}
@Test
fun rsmodPathfinderShouldRejectDestinationsAtTheTruncationLimit() {
val start = Location.create(3165, 3218, 0)
Assertions.assertTrue(RsmodPathfinder.canAttempt(start, Location.create(3203, 3186, 0)))
Assertions.assertFalse(RsmodPathfinder.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))
scenery.rotation = 0
val occupiedAt0 = scenery.occupiedTiles.toTypedArray()
Assertions.assertArrayEquals(arrayOf(Location.create(50, 50), Location.create(50,51)), occupiedAt0)
Assertions.assertArrayEquals(arrayOf(Location.create(50, 50), Location.create(50, 51)), occupiedAt0)
scenery.rotation = 1
val occupiedAt1 = scenery.occupiedTiles.toTypedArray()
Assertions.assertArrayEquals(arrayOf(Location.create(50,50), Location.create(51,50)), occupiedAt1)
Assertions.assertArrayEquals(arrayOf(Location.create(50, 50), Location.create(51, 50)), occupiedAt1)
scenery.rotation = 2
val occupiedAt2 = scenery.occupiedTiles.toTypedArray()
Assertions.assertArrayEquals(arrayOf(Location.create(50,50), Location.create(50,49)), occupiedAt2)
Assertions.assertArrayEquals(arrayOf(Location.create(50, 50), Location.create(50, 49)), occupiedAt2)
scenery.rotation = 3
val occupiedAt3 = scenery.occupiedTiles.toTypedArray()
Assertions.assertArrayEquals(arrayOf(Location.create(50,50), Location.create(49,50)), occupiedAt3)
Assertions.assertArrayEquals(arrayOf(Location.create(50, 50), Location.create(49, 50)), occupiedAt3)
}
@Test fun movementPulseShouldStopEarlyIfNextToATileOccupiedByTargetObject() {
@Test
fun smartPathfinderShouldRespectSuppliedClipMask() {
val start = Location.create(3200, 3200, 0)
val dest = Location.create(3202, 3200, 0)
val blockedDestination = ClipMaskSupplier { _, x, y ->
if (x == dest.x && y == dest.y) 0x100 else 0
}
val path = Pathfinder.SMART.find(start, 1, dest, 1, 1, 0, 0, 0, true, blockedDestination)
Assertions.assertTrue(path.isSuccessful)
Assertions.assertEquals(
Location.create(3201, 3200, 0), Location.create(path.points.last.x, path.points.last.y, 0)
)
}
@Test
fun dumbPathfinderShouldNotRouteAroundBlockedCardinalStep() {
val start = Location.create(3200, 3200, 0)
val dest = Location.create(3202, 3200, 0)
val blockedMiddle = ClipMaskSupplier { _, x, y ->
if (x == 3201 && y == 3200) movementBlockFlag else 0
}
val path = Pathfinder.DUMB.find(start, 1, dest, 0, 0, 0, -1, 0, false, blockedMiddle)
Assertions.assertFalse(path.isSuccessful)
Assertions.assertTrue(path.points.isEmpty())
}
@Test
fun dumbPathfinderShouldUseAxisFallbackForBlockedDiagonalStep() {
val start = Location.create(3200, 3200, 0)
val dest = Location.create(3202, 3202, 0)
val blockedNorth = ClipMaskSupplier { _, x, y ->
if (x == 3200 && y == 3201) movementBlockFlag else 0
}
val path = Pathfinder.DUMB.find(start, 1, dest, 0, 0, 0, -1, 0, false, blockedNorth)
Assertions.assertTrue(path.isSuccessful)
Assertions.assertTrue(path.points.isNotEmpty())
Assertions.assertEquals(3201, path.points.first.x)
Assertions.assertEquals(3200, path.points.first.y)
}
@Test
fun walkingQueueHasPathShouldIgnoreResetAnchor() {
TestUtils.getMockPlayer("walkingQueueAnchor").use { player ->
val start = Location.create(3200, 3200, 0)
player.location = start
player.walkingQueue.reset()
Assertions.assertFalse(player.walkingQueue.hasPath())
player.walkingQueue.addPath(start.x + 1, start.y)
Assertions.assertTrue(player.walkingQueue.hasPath())
}
}
@Test
fun projectilePathfinderShouldUseRsmodLineOfSightFlags() {
val start = Location.create(3200, 3200, 0)
val dest = Location.create(3202, 3200, 0)
RegionManager.loadClippingWindow(start, 128)
try {
RegionManager.setRsmodFlag(0, 3201, 3200, true, 0x20000)
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)
}
}
@Test
fun projectilePathfinderShouldTreatZeroSizedLocationDestinationsAsOneTile() {
val start = Location.create(3204, 3204, 0)
val destinations =
listOf(
Location.create(3202, 3204, 0),
Location.create(3204, 3202, 0),
)
val fixtureTiles =
(3201..3204).flatMap { x ->
(3201..3204).map { y -> Location.create(x, y, 0) }
}
RegionManager.loadClippingWindow(start, 128)
val originalFlags = fixtureTiles.associateWith {
RegionManager.getProjectileFlag(it.z, it.x, it.y)
}
try {
for (tile in fixtureTiles) {
RegionManager.setRsmodFlag(tile.z, tile.x, tile.y, true, 0)
}
for (destination in destinations) {
val path =
Pathfinder.PROJECTILE.find(
start,
1,
destination,
0,
0,
0,
-1,
0,
false,
null,
)
Assertions.assertTrue(path.isSuccessful)
Assertions.assertEquals(
destination,
Location.create(path.points.last.x, path.points.last.y, start.z),
"A west/south projectile ray must stop on its zero-sized Location destination.",
)
}
} finally {
for ((tile, flag) in originalFlags) {
RegionManager.setRsmodFlag(tile.z, tile.x, tile.y, true, flag)
}
}
}
@Test
fun metadataSceneryInteractionShouldTriggerWhenAlreadyAtRsmodApproachTile() {
TestUtils.getMockPlayer("bankBoothApproach").use { p ->
val (booth, approach) = findReachableBankBoothFixture()
p.location = approach
val alreadyAtPath = Pathfinder.find(p, booth)
Assertions.assertTrue(alreadyAtPath.isSuccessful)
Assertions.assertFalse(alreadyAtPath.isMoveNear)
Assertions.assertTrue(InteractionListeners.run(booth.id, IntType.SCENERY, "bank", p, booth))
TestUtils.advanceTicks(10, false)
Assertions.assertTrue(p.bank.isOpen)
}
}
@Test
fun metadataSceneryCollectShouldTriggerWhenAlreadyAtRsmodApproachTile() {
TestUtils.getMockPlayer("bankBoothCollectApproach").use { p ->
val (booth, approach) = findReachableBankBoothFixture()
p.location = approach
var collected = false
InteractionListeners.addMetadata(
booth.id, IntType.SCENERY, arrayOf("collect"), InteractionListener.InteractionMetadata({ _, _, _ ->
collected = true
true
}, 1, false)
)
try {
Assertions.assertTrue(InteractionListeners.run(booth.id, IntType.SCENERY, "collect", p, booth))
TestUtils.advanceTicks(10, false)
Assertions.assertTrue(collected)
} finally {
BankBoothListener().defineListeners()
}
}
}
@Test
fun directObjectMovementPulseShouldTriggerWhenAlreadyAtRsmodApproachTile() {
TestUtils.getMockPlayer("objectPulseApproach").use { p ->
val tree =
RegionManager.getObject(0, 2720, 3475, 1307) ?: throw AssertionError("Expected test tree object.")
val approach = findReachableApproachTile(tree)
var pulsed = false
p.location = approach
GameWorld.Pulser.submit(object : MovementPulse(p, tree) {
override fun pulse(): Boolean {
pulsed = true
return true
}
})
TestUtils.advanceTicks(3, false)
Assertions.assertTrue(pulsed)
}
}
@Test
fun pathfinderShouldUseWrapperFootprintWhenSceneryChildIsSmallerThanWrapper() {
TestUtils.getMockPlayer("taverleyPatchChildPath").use { p ->
val wrapper = RegionManager.getObject(0, 2935, 3437, 8388)
?: throw AssertionError("Expected Taverley tree patch wrapper.")
val child = wrapper.getChild(p)
val start = Location.create(2936, 3440, 0)
Assertions.assertEquals(8395, child.id)
Assertions.assertTrue(wrapper.definition.sizeX * wrapper.definition.sizeY > child.definition.sizeX * child.definition.sizeY)
val path = Pathfinder.find(start, child)
val last = path.points.lastOrNull() ?: throw AssertionError("Expected a path point.")
val approach = Location.create(last.x, last.y, start.z)
Assertions.assertTrue(path.isSuccessful)
Assertions.assertFalse(path.isMoveNear)
Assertions.assertEquals(start, approach)
}
}
@Test
fun entityMovementPulseShouldTriggerWhenDestinationOverrideIsAlreadyReached() {
TestUtils.getMockPlayer("bankerOverrideApproach").use { p ->
val npc = NPC.create(0, NPC_TEST_LOC)
npc.isNeverWalks = true
npc.init()
p.location = ServerConstants.HOME_LOCATION
var pulsed = false
GameWorld.Pulser.submit(object : MovementPulse(p, npc, DestinationFlag.ENTITY, { _, _ -> p.location }) {
override fun pulse(): Boolean {
pulsed = true
return true
}
})
TestUtils.advanceTicks(3, false)
Assertions.assertTrue(pulsed)
}
}
@Test
fun movingEntityMovementPulseShouldNotInteractFromDiagonalTile() {
TestUtils.getMockPlayer("movingNpcDiagonalApproach").use { p ->
val origin = Location.create(3200, 3600, 0)
val npc = NPC.create(0, origin.transform(1, 1, 0))
npc.init()
p.location = origin
p.settings.runEnergy = 100.0
p.settings.setRunToggled(true)
npc.walkingQueue.reset(false)
npc.walkingQueue.addPath(origin.transform(4, 1, 0).x, origin.transform(4, 1, 0).y)
var pulseLocation: Location? = null
var pulseTargetLocation: Location? = null
try {
GameWorld.Pulser.submit(object : MovementPulse(p, npc) {
override fun pulse(): Boolean {
pulseLocation = p.location
pulseTargetLocation = npc.location
return true
}
})
TestUtils.advanceTicks(1, false)
Assertions.assertNull(
pulseLocation, "A moving entity interaction must not trigger from a diagonal non-interaction tile."
)
repeat(8) {
if (pulseLocation == null) {
TestUtils.advanceTicks(1, false)
}
}
val actualPulseLocation = pulseLocation
?: throw AssertionError("Expected the movement pulse to eventually reach the moving NPC.")
val actualTargetLocation = pulseTargetLocation
?: throw AssertionError("Expected target location to be captured when the pulse fired.")
Assertions.assertTrue(
Pathfinder.canInteract(
actualPulseLocation.x,
actualPulseLocation.y,
p.size(),
actualTargetLocation.x,
actualTargetLocation.y,
npc.size(),
npc.size(),
0,
actualPulseLocation.z,
null
), "Entity interaction must fire only from a currently valid interaction tile."
)
} finally {
npc.clear()
}
}
}
@Test
fun entityOptionHandlerShouldNotInteractFromNpcQueuedDestination() {
TestUtils.getMockPlayer("queuedNpcPredictionGuard").use { p ->
val origin = Location.create(3200, 3600, 0)
val npc = NPC.create(0, origin.transform(0, 1, 0))
npc.init()
p.location = origin.transform(1, 0, 0)
npc.walkingQueue.reset(false)
npc.walkingQueue.addPath(origin.transform(4, 1, 0).x, origin.transform(4, 1, 0).y)
val optionHandler = object : OptionHandler() {
override fun newInstance(_arg: Any?): Plugin<Any> {
return this
}
override fun handle(_player: Player?, _node: Node?, _option: String?): Boolean {
return true
}
}
var pulseLocation: Location? = null
var pulseTargetLocation: Location? = null
try {
GameWorld.Pulser.submit(object : MovementPulse(p, npc, optionHandler) {
override fun pulse(): Boolean {
pulseLocation = p.location
pulseTargetLocation = npc.location
return true
}
})
TestUtils.advanceTicks(1, false)
Assertions.assertNull(
pulseLocation,
"Option-handler entity interaction must not fire from a tile that only reaches the NPC's queued destination."
)
repeat(8) {
if (pulseLocation == null) {
TestUtils.advanceTicks(1, false)
}
}
val actualPulseLocation = pulseLocation
?: throw AssertionError("Expected the movement pulse to eventually reach the moving NPC.")
val actualTargetLocation = pulseTargetLocation
?: throw AssertionError("Expected target location to be captured when the pulse fired.")
Assertions.assertTrue(
Pathfinder.canInteract(
actualPulseLocation.x,
actualPulseLocation.y,
p.size(),
actualTargetLocation.x,
actualTargetLocation.y,
npc.size(),
npc.size(),
0,
actualPulseLocation.z,
null
), "Entity interaction must fire only from a currently valid interaction tile."
)
} finally {
npc.clear()
}
}
}
@Test
fun entityMovementPulseShouldIgnoreMissingTargetLocation() {
TestUtils.getMockPlayer("missingTargetLocationGuard").use { p ->
val npc = NPC.create(0, NPC_TEST_LOC)
npc.init()
val originalNpcLocation = npc.location
var pulsed = false
try {
npc.location = null
val pulse = object : MovementPulse(p, npc) {
override fun pulse(): Boolean {
pulsed = true
return true
}
}
Assertions.assertFalse(pulse.update())
Assertions.assertFalse(pulsed)
} finally {
npc.location = originalNpcLocation
npc.clear()
}
}
}
@Test
fun entityMovementPulseShouldIgnoreMissingMoverLocation() {
TestUtils.getMockPlayer("missingMoverLocationGuard").use { p ->
val npc = NPC.create(0, NPC_TEST_LOC)
npc.init()
val locationField = Node::class.java.getDeclaredField("location")
locationField.isAccessible = true
val originalPlayerLocation = p.location
var pulsed = false
try {
locationField.set(p, null)
val pulse = object : MovementPulse(p, npc) {
override fun pulse(): Boolean {
pulsed = true
return true
}
}
Assertions.assertFalse(pulse.update())
Assertions.assertFalse(pulsed)
} finally {
locationField.set(p, originalPlayerLocation)
npc.clear()
}
}
}
@Test
fun runEnabledEntityMovementPulseShouldCatchWalkingNpcMovingDirectlyAway() {
TestUtils.getMockPlayer("runNpcInteractionChaser").use { p ->
val origin = openHorizontalInteractionOrigin()
val npc = NPC.create(0, origin.transform(4, 0, 0))
npc.init()
p.location = origin
p.settings.runEnergy = 100.0
p.settings.setRunToggled(true)
npc.walkingQueue.reset(false)
npc.walkingQueue.addPath(origin.transform(12, 0, 0).x, origin.transform(12, 0, 0).y)
var pulseLocation: Location? = null
var pulseTargetLocation: Location? = null
try {
GameWorld.Pulser.submit(object : MovementPulse(p, npc) {
override fun pulse(): Boolean {
pulseLocation = p.location
pulseTargetLocation = npc.location
return true
}
})
repeat(6) {
if (pulseLocation == null) {
TestUtils.advanceTicks(1, false)
}
}
val actualPulseLocation = pulseLocation ?: throw AssertionError(
"Run-enabled player should catch the walking NPC before it stops. " + "player=${p.location}, npc=${npc.location}, queue=${p.walkingQueue.queue}"
)
val actualTargetLocation = pulseTargetLocation
?: throw AssertionError("Expected target location to be captured when the pulse fired.")
Assertions.assertNotEquals(
origin.transform(12, 0, 0),
actualTargetLocation,
"The interaction should not wait until the NPC finishes walking away."
)
Assertions.assertTrue(
Pathfinder.canInteract(
actualPulseLocation.x,
actualPulseLocation.y,
p.size(),
actualTargetLocation.x,
actualTargetLocation.y,
npc.size(),
npc.size(),
0,
actualPulseLocation.z,
null
), "Entity interaction must fire from a currently valid interaction tile."
)
} finally {
npc.clear()
}
}
}
@Test
fun interactionListenerShouldUseOptionHandlerDestinationWhenNoListenerDestinationOverride() {
val npc = NPC.create(0, NPC_TEST_LOC)
npc.isNeverWalks = true
npc.init()
var listenerRan = false
var optionHandlerRan = false
val optionName = "listener-custom-destination"
val option = Option(optionName, 4)
val destinationHandler = object : OptionHandler() {
override fun newInstance(arg: Any?): Plugin<Any> {
NPCDefinition.forId(0).handlers["option:$optionName"] = this
return this
}
override fun handle(player: Player?, node: Node?, option: String?): Boolean {
optionHandlerRan = true
return true
}
override fun getDestination(n: Node, node: Node): Location {
return n.location
}
}
destinationHandler.newInstance(null)
option.handler = destinationHandler
npc.interaction.set(option)
InteractionListeners.add(0, IntType.NPC.ordinal, arrayOf(optionName)) { _, _ ->
listenerRan = true
true
}
TestUtils.getMockPlayer("listenerOptionDestination").use { p ->
p.location = ServerConstants.HOME_LOCATION
TestUtils.simulateInteraction(p, npc, 4)
TestUtils.advanceTicks(3, false)
Assertions.assertTrue(listenerRan)
Assertions.assertFalse(optionHandlerRan)
Assertions.assertEquals(ServerConstants.HOME_LOCATION, p.location)
}
}
@Test
fun genericTalkToShouldOpenGrandExchangeClerkDialogueFromCounterApproachTile() {
GrandExchangePlugin().newInstance(null)
if (!DialogueInterpreter.contains(6528)) {
GrandExchangeClerk().init()
}
if (InteractionListeners.get("talk-to", IntType.NPC.ordinal) == null) {
NPCTalkListener().defineListeners()
}
val clerk = NPC.create(6528, Location.create(3165, 3491, 0), Direction.NORTH)
clerk.isNeverWalks = true
clerk.init()
try {
TestUtils.getMockPlayer("geClerkTalk").use { p ->
p.location = Location.create(3165, 3492, 0)
TestUtils.simulateInteraction(p, clerk, 0)
TestUtils.advanceTicks(3, false)
Assertions.assertNotNull(p.dialogueInterpreter.dialogue)
Assertions.assertEquals(GrandExchangeClerk::class.java, p.dialogueInterpreter.dialogue.javaClass)
Assertions.assertEquals(Location.create(3165, 3492, 0), p.location)
}
} finally {
clerk.clear()
}
}
private fun findReachableBankBoothFixture(): Pair<Scenery, Location> {
val base = ServerConstants.HOME_LOCATION!!.transform(8, 8, 0)
for (rotation in 0..3) {
val booth = Scenery(SceneryIds.BANK_BOOTH_2213, base, 10, rotation)
runCatching { findReachableApproachTile(booth) }.getOrNull()?.let { return booth to it }
}
throw AssertionError("Could not find a reachable synthetic bank booth fixture.")
}
private fun findReachableApproachTile(scenery: Scenery): Location {
for (radius in 1..8) {
for (x in scenery.location.x - radius..scenery.location.x + radius) {
for (y in scenery.location.y - radius..scenery.location.y + radius) {
val start = Location.create(x, y, scenery.location.z)
if (!RegionManager.isTeleportPermitted(start)) {
continue
}
val path = Pathfinder.find(start, scenery)
if (!path.isSuccessful || path.isMoveNear) {
continue
}
val point = path.points.lastOrNull()
val approach = Location.create(point?.x ?: start.x, point?.y ?: start.y, start.z)
val check = Pathfinder.find(approach, scenery)
if (check.isSuccessful && !check.isMoveNear) {
return approach
}
}
}
}
throw AssertionError("Could not find a reachable approach tile for $scenery.")
}
@Test
fun movementPulseShouldStopEarlyIfNextToATileOccupiedByTargetObject() {
val start = Location.create(2731, 3481)
val dest = RegionManager.getObject(0, 2720, 3475, 1307)
val p = TestUtils.getMockPlayer("treefindtest")
@ -59,15 +659,17 @@ class PathfinderTests {
Assertions.assertEquals(Location.create(2722, 3475, 0), p.location)
}
@Test fun movementInteractionShouldTrigger() {
@Test
fun movementInteractionShouldTrigger() {
val npc = NPC.create(0, NPC_TEST_LOC)
npc.init()
var intListenerRan = false
InteractionListeners.add(0, IntType.NPC.ordinal, arrayOf("testoptlistener"), method = {player: Player, node: Node ->
intListenerRan = true
return@add true
})
InteractionListeners.add(
0, IntType.NPC.ordinal, arrayOf("testoptlistener"), method = { player: Player, node: Node ->
intListenerRan = true
return@add true
})
var pluginRan = false
val option = Option("testoption", 4)
@ -77,6 +679,7 @@ class PathfinderTests {
NPCDefinition.forId(0).handlers["option:testoption"] = this
return this
}
override fun handle(player: Player?, node: Node?, option: String?): Boolean {
pluginRan = true
return true
@ -87,7 +690,7 @@ class PathfinderTests {
npc.interaction.set(option2)
option.handler = testHandler
TestUtils.getMockPlayer("interactionTest").use {p ->
TestUtils.getMockPlayer("interactionTest").use { p ->
p.location = ServerConstants.HOME_LOCATION
TestUtils.simulateInteraction(p, npc, 0)
TestUtils.advanceTicks(10, false)
@ -99,8 +702,9 @@ class PathfinderTests {
}
}
@Test fun entityMovingToStationaryNPCShouldNotIdleIndefinitely() {
TestUtils.getMockPlayer("idlenpcdest").use {p ->
@Test
fun entityMovingToStationaryNPCShouldNotIdleIndefinitely() {
TestUtils.getMockPlayer("idlenpcdest").use { p ->
val startLoc = ServerConstants.HOME_LOCATION
p.location = startLoc
val npc = NPC.create(0, NPC_TEST_LOC)
@ -116,8 +720,9 @@ class PathfinderTests {
}
}
@Test fun entityTargetMovementPulseShouldNotStopOnSameTileAsEntity() {
TestUtils.getMockPlayer("entitystoptest").use {p ->
@Test
fun entityTargetMovementPulseShouldNotStopOnSameTileAsEntity() {
TestUtils.getMockPlayer("entitystoptest").use { p ->
p.location = ServerConstants.HOME_LOCATION
val npc = NPC.create(0, NPC_TEST_LOC)
npc.isNeverWalks = true
@ -133,7 +738,8 @@ class PathfinderTests {
}
}
@Test fun entityTargetMovementPulseWithExplicitParamsShouldNotStopOnSameTile() {
@Test
fun entityTargetMovementPulseWithExplicitParamsShouldNotStopOnSameTile() {
TestUtils.getMockPlayer("entitystoptest2").use { p ->
p.location = ServerConstants.HOME_LOCATION
val npc = NPC.create(0, NPC_TEST_LOC)
@ -150,7 +756,8 @@ class PathfinderTests {
}
}
@Test fun doubleMovementPulseToEntityShouldNotStopOnSameTile() {
@Test
fun doubleMovementPulseToEntityShouldNotStopOnSameTile() {
TestUtils.getMockPlayer("entitystoptest3").use { p ->
p.location = ServerConstants.HOME_LOCATION
val npc = NPC.create(0, NPC_TEST_LOC)
@ -173,12 +780,14 @@ class PathfinderTests {
}
}
@Test fun simulatedInteractionPacketWithMovementFromPluginShouldNotEndOnSameTile() {
@Test
fun simulatedInteractionPacketWithMovementFromPluginShouldNotEndOnSameTile() {
val testHandler = object : OptionHandler() {
override fun newInstance(arg: Any?): Plugin<Any> {
NPCDefinition.forId(0).handlers["option:testoption"] = this
return this
}
override fun handle(player: Player?, node: Node?, option: String?): Boolean {
log(this::class.java, Log.ERR, "Interaction triggered")
return true
@ -202,14 +811,16 @@ class PathfinderTests {
}
}
@Test fun simulatedInteractionPacketWithMovementFromListenerShouldNotEndOnSameTile() {
@Test
fun simulatedInteractionPacketWithMovementFromListenerShouldNotEndOnSameTile() {
val npc = NPC.create(0, NPC_TEST_LOC)
npc.isNeverWalks = true
npc.init()
InteractionListeners.add(0, IntType.NPC.ordinal, arrayOf("testoptlistener2"), method = {player: Player, node: Node ->
return@add true
})
InteractionListeners.add(
0, IntType.NPC.ordinal, arrayOf("testoptlistener2"), method = { player: Player, node: Node ->
return@add true
})
val opt = Option("testoptlistener2", 1)
npc.interaction.set(opt)
@ -223,7 +834,8 @@ class PathfinderTests {
}
}
@Test fun npcShouldReliablyReturnToSpawnLocationIfTooFar() {
@Test
fun npcShouldReliablyReturnToSpawnLocationIfTooFar() {
//spawn a player into the area just to make sure it ticks...
TestUtils.getMockPlayer("areatest").use { p ->
val npc = NPC(1, Location.create(3240, 3226, 0))
@ -239,7 +851,106 @@ class PathfinderTests {
}
}
@Test fun npcShouldReliablyReturnToSpawnEvenIfRegionUnloaded() {
@Test
fun npcReturnToSpawnShouldUseOverriddenWalkRadius() {
TestUtils.getMockPlayer("overriddenRadiusReturn").use {
val spawn = ServerConstants.HOME_LOCATION!!
val npc = object : NPC(1, spawn.transform(5, 0, 0)) {
override fun getWalkRadius(): Int {
return 3
}
}
npc.isWalks = true
npc.isNeverWalks = false
npc.init()
npc.properties.spawnLocation = spawn
try {
npc.handleTickActions()
Assertions.assertEquals(true, npc.getAttribute("return-to-spawn", false))
} finally {
npc.clear()
}
}
}
@Test
fun randomWalkingNpcShouldUseSideStepWhenDirectNorthTileIsBlocked() {
val origin = Location.create(3200, 3600, 0)
val blocked = origin.transform(0, 1, 0)
val destination = origin.transform(0, 2, 0)
val sidesteps = setOf(
origin.transform(-1, 0, 0), origin.transform(1, 0, 0)
)
val npc = FixedDestinationNPC(origin, destination, 3)
npc.isWalks = true
npc.isNeverWalks = false
npc.init()
npc.properties.spawnLocation = origin
RegionManager.addClippingFlag(blocked.z, blocked.x, blocked.y, false, movementBlockFlag)
try {
npc.resetWalk()
repeat(20) {
if (!npc.walkingQueue.hasPath()) {
npc.handleTickActions()
}
}
Assertions.assertTrue(
npc.walkingQueue.hasPath(),
"Random-walking NPCs should use an open east/west sidestep when the direct north tile is blocked."
)
npc.walkingQueue.update()
Assertions.assertTrue(
npc.location in sidesteps,
"Random-walking NPC should only take a local sidestep, not route through the blocked north tile. " + "npc=${npc.location}"
)
} finally {
RegionManager.removeClippingFlag(blocked.z, blocked.x, blocked.y, false, movementBlockFlag)
npc.clear()
}
}
@Test
fun randomWalkingNpcShouldNotFullyRouteAroundBlockedLocalDestination() {
val origin = Location.create(3200, 3600, 0)
val blocked = origin.transform(1, 0, 0)
val destination = origin.transform(2, 0, 0)
val sidesteps = setOf(
origin.transform(0, -1, 0), origin.transform(0, 1, 0)
)
val npc = FixedDestinationNPC(origin, destination, 3)
npc.isWalks = true
npc.isNeverWalks = false
npc.init()
npc.properties.spawnLocation = origin
RegionManager.addClippingFlag(blocked.z, blocked.x, blocked.y, false, movementBlockFlag)
try {
npc.resetWalk()
repeat(20) {
if (!npc.walkingQueue.hasPath()) {
npc.handleTickActions()
}
}
Assertions.assertTrue(
npc.walkingQueue.hasPath(),
"Random-walking NPCs should use a local sidestep instead of taking an RSMOD detour."
)
npc.walkingQueue.update()
Assertions.assertTrue(
npc.location in sidesteps,
"Random-walking NPC should not fully route around a clipped boundary tile. npc=${npc.location}"
)
Assertions.assertNotEquals(destination, npc.location)
} finally {
RegionManager.removeClippingFlag(blocked.z, blocked.x, blocked.y, false, movementBlockFlag)
npc.clear()
}
}
@Test
fun npcShouldReliablyReturnToSpawnEvenIfRegionUnloaded() {
//spawn a player into the area just to make sure it ticks...
TestUtils.getMockPlayer("areaunloadtest").use { p ->
val npc = NPC(1, Location.create(3240, 3226, 0))
@ -257,4 +968,45 @@ class PathfinderTests {
Assertions.assertEquals(true, npc.location.getDistance(ServerConstants.HOME_LOCATION!!) <= 5)
}
}
}
private class FixedDestinationNPC(
location: Location, private val destination: Location, private val radius: Int
) : NPC(1, location) {
override fun getMovementDestination(): Location {
return destination
}
override fun getWalkRadius(): Int {
return radius
}
}
private val movementBlockFlag =
Pathfinder.PREVENT_NORTH or Pathfinder.PREVENT_EAST or Pathfinder.PREVENT_SOUTH or Pathfinder.PREVENT_WEST
private fun openHorizontalInteractionOrigin(): Location {
val start = Location.create(3200, 3600, 0)
for (dy in -16..16) {
for (dx in -16..16) {
val candidate = start.transform(dx, dy, 0)
if ((0..12).all { RegionManager.isTeleportPermitted(candidate.transform(it, 0, 0)) } && (0..11).all {
Pathfinder.canInteract(
candidate.x + it,
candidate.y,
1,
candidate.x + it + 1,
candidate.y,
1,
1,
0,
candidate.z,
null
)
}) {
return candidate
}
}
}
throw AssertionError("No open horizontal interaction test line found near $start.")
}
}

View file

@ -0,0 +1,59 @@
package core
import TestUtils
import core.game.world.map.Location
import core.game.world.map.RegionManager
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
class WalkingQueueTests {
companion object {
init {
TestUtils.preTestSetup()
}
}
@Test fun resetClearsDrawRouteMarkersBeforeQueuingNewDestination() {
TestUtils.getMockPlayer("drawRouteReset").use { player ->
val start = Location.create(3200, 3200, 0)
player.location = start
player.setAttribute("routedraw", true)
val queue = player.walkingQueue
queue.reset()
queue.addPath(start.x + 3, start.y)
val firstRouteItemLocation = queue.routeItems.firstOrNull()?.location
?: throw AssertionError("Expected the first route to draw route markers.")
Assertions.assertNotNull(
RegionManager.getRegionPlane(firstRouteItemLocation).getItem(
DRAW_ROUTE_ITEM_ID,
firstRouteItemLocation,
player
)
)
try {
queue.reset()
queue.addPath(start.x, start.y + 3)
Assertions.assertNull(
RegionManager.getRegionPlane(firstRouteItemLocation).getItem(
DRAW_ROUTE_ITEM_ID,
firstRouteItemLocation,
player
),
"The first route marker should be removed when a new movement destination resets the queue."
)
Assertions.assertTrue(
queue.routeItems.any { it.location != firstRouteItemLocation },
"Expected the second route to draw its own markers."
)
} finally {
queue.reset()
}
}
}
}
private const val DRAW_ROUTE_ITEM_ID = 13444