Refactored components of the ScriptAPI to more efficiently utilize pathfinding

Implemented a global limit on pathfinding distance (server.max_pathfind_dist)
Pathfinding limit is enforced at the packet level as well as elsewhere (ContentAPI, etc)
This commit is contained in:
Ceikry
2023-04-24 14:16:59 +00:00
committed by Ryan
parent 5aded2b349
commit 4b20cdd001
15 changed files with 369 additions and 326 deletions
+1 -1
View File
@@ -285,7 +285,7 @@ exceptions:
- 'Throwable'
allowedExceptionNameRegex: '_|(ignore|expected).*'
TooGenericExceptionThrown:
active: true
active: false
exceptionNames:
- 'Error'
- 'Exception'
@@ -97,9 +97,9 @@ class LobsterCatcher : Script() {
state = State.FISHING
} else {
if (bot.location.x < 2837) {
Pathfinder.find(bot, Location.create(2837, 3435, 0)).walk(bot)
scriptAPI.walkTo(Location.create(2837, 3435, 0))
} else {
Pathfinder.find(bot, Location.create(2854, 3427, 0)).walk(bot)
scriptAPI.walkTo(Location.create(2854, 3427, 0))
}
}
}
+3
View File
@@ -278,5 +278,8 @@ class ServerConstants {
@JvmField
var ENABLE_GLOBALCHAT = false
@JvmField
var MAX_PATHFIND_DISTANCE = 25
}
}
+8 -4
View File
@@ -70,8 +70,10 @@ import core.game.world.repository.Repository
import core.game.consumable.*
import core.tools.Log
import core.tools.tick
import core.ServerConstants
import core.api.utils.Vector
import java.util.regex.*
import kotlin.math.absoluteValue
import kotlin.math.*
/**
* Gets a skilling tool which the player has the level to use and is in their inventory.
@@ -1770,15 +1772,17 @@ fun getServerConfig(): Toml {
}
fun getPathableRandomLocalCoordinate(target: Entity, radius: Int, center: Location, maxAttempts: Int = 3): Location {
val swCorner = center.transform(-radius, -radius, center.z)
val neCorner = center.transform(radius, radius, center.z)
var maxRadius = Vector.deriveWithEqualComponents(ServerConstants.MAX_PATHFIND_DISTANCE.toDouble()).x - 1
var effectiveRadius = min(radius, maxRadius.toInt())
val swCorner = center.transform(-effectiveRadius, -effectiveRadius, center.z)
val neCorner = center.transform(effectiveRadius, effectiveRadius, center.z)
val borders = ZoneBorders(swCorner.x, swCorner.y, neCorner.x, neCorner.y, center.z)
var attempts = maxAttempts
var success: Boolean
while (attempts-- > 0) {
val dest = borders.randomLoc
val path = Pathfinder.find(target, dest)
val path = Pathfinder.find(center, dest, target.size())
success = path.isSuccessful && !path.isMoveNear
if (success) return dest
}
+20
View File
@@ -25,11 +25,31 @@ class Vector (val x: Double, val y: Double) {
return Vector(this.x * other, this.y * other)
}
operator fun plus (other: Vector) : Vector {
return Vector(this.x + other.x, this.y + other.y)
}
operator fun minus (other: Vector) : Vector {
return Vector(this.x - other.x, this.y - other.y)
}
override fun toString() : String {
return "{$x,$y}"
}
fun invert() : Vector {
return -this
}
companion object {
@JvmStatic fun betweenLocs (from: Location, to: Location) : Vector {
val xDiff = to.x - from.x
val yDiff = to.y - from.y
return Vector (xDiff.toDouble(), yDiff.toDouble())
}
@JvmStatic fun deriveWithEqualComponents (magnitude: Double) : Vector {
var sideLength = sqrt(magnitude.pow(2.0) / 2)
return Vector(sideLength, sideLength)
}
}
}
@@ -40,7 +40,7 @@ class GeneralBotCreator {
randomDelay -= 1
return false
}
if (!botScript.bot.pulseManager.hasPulseRunning()) {
if (!botScript.bot.pulseManager.hasPulseRunning() && botScript.bot.scripts.getActiveScript() == null) {
/*if (ticks++ >= RandomFunction.random(90000,120000)) {
AIPlayer.deregister(botScript.bot.uid)
+54 -99
View File
@@ -49,6 +49,8 @@ import java.util.concurrent.CountDownLatch
import kotlin.math.max
import kotlin.math.pow
import kotlin.math.sqrt
import core.ServerConstants
import core.api.utils.Vector
class ScriptAPI(private val bot: Player) {
val GRAPHICSUP = Graphics(1576)
@@ -86,24 +88,6 @@ class ScriptAPI(private val bot: Player) {
if(!InteractionListeners.run(node.id, type, option, bot, node)) node.interaction.handle(bot, opt)
}
/**
* Gets the nearest node with name entityName
* @param entityName the name of the node to look for
* @return the nearest node with a matching name or null
* @author Ceikry
*/
fun getNearestNode(entityName: String): Node? {
var entity: Node? = null
var minDistance = Double.MAX_VALUE
for (node in RegionManager.forId(bot.location.regionId).planes[bot.location.z].entities) {
if (node != null && node.name == entityName && distance(bot, node) < minDistance && !Pathfinder.find(bot, node).isMoveNear) {
entity = node
minDistance = distance(bot, node)
}
}
return entity
}
fun sendChat(message: String) {
bot.sendChat(message)
bot.updateMasks.register(ChatFlag(ChatMessage(bot, message, 0, 0)))
@@ -115,33 +99,11 @@ class ScriptAPI(private val bot: Player) {
* @return the nearest node with a matching name or null
* @author Ceikry
*/
fun getNearestNodeFromList(acceptedNames: List<String>, isObject: Boolean): Node? {
if (isObject) {
var entity: Node? = null
var minDistance = Double.MAX_VALUE
for (objects in RegionManager.forId(bot.location.regionId).planes[bot.location.z].objects) {
for(e in objects) {
val name = e?.name
if (e != null && acceptedNames.contains(name) && distance(bot, e) < minDistance && !Pathfinder.find(bot, e).isMoveNear && e.isActive) {
entity = e
minDistance = distance(bot, e)
}
}
}
return if(entity == null) null else entity as Scenery
} else {
var entity: Node? = null
var minDistance = Double.MAX_VALUE
for (e in RegionManager.forId(bot.location.regionId).planes[bot.location.z].entities) {
val name = e?.name
if (e != null && acceptedNames.contains(name) && distance(bot, e) < minDistance && !Pathfinder.find(bot, e).isMoveNear) {
entity = e
minDistance = distance(bot, e)
}
}
return entity
}
if (isObject)
return processEvaluationList(RegionManager.forId(bot.location.regionId).planes[bot.location.z].objectList, acceptedName = acceptedNames)
else
return processEvaluationList(RegionManager.forId(bot.location.regionId).planes[bot.location.z].entities, acceptedName = acceptedNames)
}
/**
@@ -151,30 +113,21 @@ class ScriptAPI(private val bot: Player) {
* @return the closest node with matching id or null.
* @author Ceikry
*/
fun getNearestNode(id: Int, `object`: Boolean): Node? {
if (`object`) {
var entity: Node? = null
var minDistance = Double.MAX_VALUE
for (objects in RegionManager.forId(bot.location.regionId).planes[bot.location.z].objects) {
for(e in objects) {
if (e != null && e.id == id && distance(bot, e) < minDistance && !Pathfinder.find(bot, e).isMoveNear && e.isActive) {
entity = e
minDistance = distance(bot, e)
}
}
}
return if(entity == null) null else entity as Scenery
} else {
var entity: Node? = null
var minDistance = Double.MAX_VALUE
for (e in RegionManager.forId(bot.location.regionId).planes[bot.location.z].entities) {
if (e != null && e.id == id && distance(bot, e) < minDistance && !Pathfinder.find(bot, e).isMoveNear) {
entity = e
minDistance = distance(bot, e)
}
}
return entity
fun getNearestNode(id: Int, isObject: Boolean): Node? {
if (isObject)
return processEvaluationList(RegionManager.forId(bot.location.regionId).planes[bot.location.z].objectList, acceptedId = id)
else
return processEvaluationList(RegionManager.forId(bot.location.regionId).planes[bot.location.z].entities, acceptedId = id)
}
/**
* Gets the nearest node with name entityName
* @param entityName the name of the node to look for
* @return the nearest node with a matching name or null
* @author Ceikry
*/
fun getNearestNode(entityName: String): Node? {
return processEvaluationList(RegionManager.forId(bot.location.regionId).planes[bot.location.z].entities, acceptedName = listOf(entityName))
}
/**
@@ -184,31 +137,39 @@ class ScriptAPI(private val bot: Player) {
* @return the nearest matching node or null.
* @author Ceikry
*/
fun getNearestNode(name: String, `object`: Boolean): Node? {
if (`object`) {
fun getNearestNode(name: String, isObject: Boolean): Node? {
if (isObject)
return processEvaluationList(RegionManager.forId(bot.location.regionId).planes[bot.location.z].objectList, acceptedName = listOf(name))
else
return processEvaluationList(RegionManager.forId(bot.location.regionId).planes[bot.location.z].entities, acceptedName = listOf(name))
}
fun evaluateViability (e: Node?, minDistance: Double, maxDistance: Double, acceptedNames: List<String>? = null, acceptedId: Int = -1): Boolean {
if (e == null || !e.isActive)
return false
if (acceptedId != -1 && e.id != acceptedId)
return false
val dist = distance(bot, e)
if (dist > maxDistance || dist > minDistance)
return false
val name = e?.name
return (acceptedNames?.contains(name) ?: true && !Pathfinder.find(bot, e).isMoveNear)
}
fun processEvaluationList (list: List<Node>, acceptedName: List<String>? = null, acceptedId: Int = -1): Node? {
var entity: Node? = null
var minDistance = Double.MAX_VALUE
for (objects in RegionManager.forId(bot.location.regionId).planes[bot.location.z].objects) {
for(e in objects) {
if (e != null && e.name.toLowerCase() == name.toLowerCase() && distance(bot, e) < minDistance && !Pathfinder.find(bot, e).isMoveNear && e.isActive) {
entity = e
minDistance = distance(bot, e)
}
}
}
return if(entity == null) null else entity as Scenery
} else {
var entity: Node? = null
var minDistance = Double.MAX_VALUE
for (e in RegionManager.forId(bot.location.regionId).planes[bot.location.z].entities) {
if (e != null && e.name.toLowerCase() == name.toLowerCase() && distance(bot, e) < minDistance && !Pathfinder.find(bot, e).isMoveNear) {
val maxDistance = ServerConstants.MAX_PATHFIND_DISTANCE.toDouble()
for (e in list) {
if (evaluateViability(e, minDistance, maxDistance, acceptedName, acceptedId)) {
entity = e
minDistance = distance(bot, e)
}
}
return entity
}
}
/**
* Gets the nearest ground item with matching ID from the list in AIRepository.
@@ -350,11 +311,9 @@ class ScriptAPI(private val bot: Player) {
*/
fun walkTo(loc: Location){
if(!bot.walkingQueue.isMoving) {
GlobalScope.launch {
walkToIterator(loc)
}
}
}
/**
* Function to iteratively walk an array of Locations to get over complex obstacles.
@@ -391,13 +350,10 @@ class ScriptAPI(private val bot: Player) {
*/
fun randomWalkTo(loc: Location, radius: Int) {
if(!bot.walkingQueue.isMoving) {
GlobalScope.launch {
var newloc = loc.transform(RandomFunction.random(radius,-radius),
RandomFunction.random(radius,-radius), 0)
var newloc = loc.transform(RandomFunction.random(radius,-radius),RandomFunction.random(radius,-radius), 0)
walkToIterator(newloc)
}
}
}
/**
@@ -408,15 +364,12 @@ class ScriptAPI(private val bot: Player) {
private fun walkToIterator(loc: Location){
var diffX = loc.x - bot.location.x
var diffY = loc.y - bot.location.y
while(!bot.location.transform(diffX, diffY, 0).withinDistance(bot.location)) {
diffX /= 2
diffY /= 2
}
GameWorld.Pulser.submit(object : MovementPulse(bot, bot.location.transform(diffX, diffY, 0), Pathfinder.SMART) {
override fun pulse(): Boolean {
return true
}
})
val vec = Vector.betweenLocs(bot.location, loc)
val norm = vec.normalized()
val tiles = kotlin.math.min(kotlin.math.floor(vec.magnitude()).toInt(), ServerConstants.MAX_PATHFIND_DISTANCE - 1)
val loc = bot.location.transform(norm * tiles)
bot.pulseManager.run(object : MovementPulse(bot, loc) { override fun pulse() : Boolean { return true } })
}
/**
@@ -469,6 +422,7 @@ class ScriptAPI(private val bot: Player) {
override fun pulse(): Boolean {
bot.unlock()
bot.properties.teleportLocation = location
bot.pulseManager.clear()
bot.animator.reset()
return true
}
@@ -599,6 +553,7 @@ class ScriptAPI(private val bot: Player) {
override fun pulse(): Boolean {
bot.unlock()
bot.properties.teleportLocation = location
bot.pulseManager.clear()
bot.animator.reset()
return true
}
@@ -78,6 +78,8 @@ public abstract class MovementPulse extends Pulse {
private Function2<Entity,Node,Location> overrideMethod;
private Location previousLoc;
/**
* Constructs a new {@code MovementPulse} {@code Object}.
*
@@ -263,7 +265,7 @@ public abstract class MovementPulse extends Pulse {
else if (inside) {
loc = findBorderLocation();
}
}
} else if (loc == previousLoc) return;
if (destination == null) {
return;
@@ -293,6 +295,7 @@ public abstract class MovementPulse extends Pulse {
}
Path path = Pathfinder.find(mover, loc != null ? loc : destination, true, pathfinder);
loc = destination.getLocation();
near = !path.isSuccessful() || path.isMoveNear();
interactLocation = mover.getLocation();
boolean canMove = true;
@@ -330,6 +333,7 @@ public abstract class MovementPulse extends Pulse {
}
}
}
previousLoc = loc;
}
last = destination.getLocation();
}
@@ -45,11 +45,11 @@ class ScriptProcessor(val entity: Entity) {
if (entity !is Player) return
if (!entity.delayed() && canProcess && interactTarget != null) {
if (opScript != null && inOperableDistance()) {
face(entity, interactTarget?.centerLocation ?: return)
face(entity, interactTarget?.getFaceLocation(entity.location) ?: return)
processInteractScript(opScript ?: return)
}
else if (apScript != null && inApproachDistance(apScript ?: return)) {
face(entity, interactTarget?.centerLocation ?: return)
face(entity, interactTarget?.getFaceLocation(entity.location) ?: return)
processInteractScript(apScript ?: return)
}
else if (apScript == null && opScript == null && inOperableDistance()) {
+16
View File
@@ -9,6 +9,7 @@ import core.game.node.scenery.Scenery;
import core.game.world.map.Direction;
import core.game.world.map.Location;
import core.tools.StringUtils;
import core.api.utils.Vector;
/**
* Represents a node which is anything that is interactable in Keldagrim.
@@ -120,6 +121,21 @@ public abstract class Node {
return location.transform(offset, offset, 0);
}
public Vector getMathematicalCenter() {
Location topRight = location.transform(size - 1, size - 1, 0);
double x = ((double) location.getX() + (double) topRight.getX()) / 2.0;
double y = ((double) location.getY() + (double) topRight.getY()) / 2.0;
return new Vector(x, y);
}
public Location getFaceLocation (Location fromLoc) {
Vector center = getMathematicalCenter();
Vector fromVec = new Vector((double) fromLoc.getX(), (double) fromLoc.getY());
Vector difference = fromVec.minus(center);
Vector end = center.plus(difference.invert());
return Location.create((int)end.getX(), (int)end.getY(), fromLoc.getZ());
}
/**
* Gets the name of this node.
* @return The name.
@@ -150,6 +150,7 @@ object ServerConfigParser {
ServerConstants.DRAGON_AXE_USE_OSRS_SPEC = data.getBoolean("world.dragon_axe_use_osrs_spec", false)
ServerConstants.DISCORD_OPENRSC_HOOK = data.getString("server.openrsc_integration_webhook", "")
ServerConstants.ENABLE_GLOBALCHAT = data.getBoolean("world.enable_globalchat", true)
ServerConstants.MAX_PATHFIND_DISTANCE = data.getLong("server.max_pathfind_dist", 25L).toInt()
val logLevel = data.getString("server.log_level", "VERBOSE").uppercase()
ServerConstants.LOG_LEVEL = parseEnumEntry<LogLevel>(logLevel) ?: LogLevel.VERBOSE
@@ -202,6 +202,17 @@ public final class RegionPlane {
return objects;
}
public List<Scenery> getObjectList() {
ArrayList<Scenery> list = new ArrayList();
for (int x = 0; x < REGION_SIZE; x++) {
for (int y = 0; y < REGION_SIZE; y++) {
if (objects[x][y] != null)
list.add(objects[x][y]);
}
}
return list;
}
/**
* Clears this region plane.
*/
@@ -151,6 +151,10 @@ public abstract class Pathfinder {
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);
}
/**
* Finds a path from the start location to the end location.
* @param destination The destination node.
@@ -4,6 +4,8 @@ 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.api.utils.Vector
import core.ServerConstants
import java.util.Comparator
import java.util.PriorityQueue
@@ -95,9 +97,22 @@ internal constructor() : Pathfinder() {
}
}
override fun find(start: Location?, moverSize: Int, end: Location?, sizeX: Int, sizeY: Int, rotation: Int, type: Int, walkingFlag: Int, near: Boolean, clipMaskSupplier: ClipMaskSupplier?): Path {
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 && end != null)
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) {
e.printStackTrace()
}
}
val path = Path()
foundPath = false
for (x in 0..103) {
@@ -226,6 +241,8 @@ internal constructor() : Pathfinder() {
path.points.add(Point(absX, absY))
}
path.setSuccesful(true)
if (end != dest)
path.isMoveNear = true
return path
}
@@ -55,6 +55,7 @@ import core.net.packet.`in`.Packet
import core.net.packet.`in`.RunScript
import core.tools.Log
import core.worker.ManagementEvents
import core.api.utils.Vector
import java.io.PrintWriter
import java.io.StringWriter
import java.lang.Math.min
@@ -452,15 +453,22 @@ object PacketProcessor {
//there's more data in this packet, we're just not using it
}
var loc = Location.create(x,y,player.location.z)
var canWalk = !player.locks.isMovementLocked
val vec = Vector.betweenLocs(player.location, loc)
if (vec.magnitude() > ServerConstants.MAX_PATHFIND_DISTANCE) {
val newVec = vec.normalized() * (ServerConstants.MAX_PATHFIND_DISTANCE - 1)
loc = player.location.transform(newVec)
}
if (canWalk && player.interfaceManager.isOpened && !player.interfaceManager.opened.definition.isWalkable)
canWalk = canWalk && player.interfaceManager.close()
if (canWalk && player.interfaceManager.hasChatbox() && !player.interfaceManager.chatbox.definition.isWalkable)
player.interfaceManager.closeChatbox()
if (!canWalk || !player.dialogueInterpreter.close()) {
player.debug("[WALK ACTION]-- NO HANDLE: PLAYER LOCKED OR INTERFACES SAY NO")
player.debug("[WALK ACTION]-- Action canceled. Either player is locked, interfaces can't close, or distance is beyond server pathfinding limit.")
return sendClearMinimap(player)
}
@@ -474,7 +482,7 @@ object PacketProcessor {
player.faceLocation(null)
player.scripts.reset()
player.pulseManager.run(object : MovementPulse(player, Location.create(x,y,player.location.z), isRunning) {
player.pulseManager.run(object : MovementPulse(player, loc, isRunning) {
override fun pulse(): Boolean {
if (isRunning)
player.walkingQueue.isRunning = false