diff --git a/Server/detekt.yml b/Server/detekt.yml index 8895d520d..6a18854c9 100644 --- a/Server/detekt.yml +++ b/Server/detekt.yml @@ -437,7 +437,7 @@ potential-bugs: - '*.CanIgnoreReturnValue' ignoreFunctionCall: [] ImplicitDefaultLocale: - active: true + active: false ImplicitUnitReturnType: active: false allowExplicitReturnType: true diff --git a/Server/src/main/core/game/node/entity/impl/WalkingQueue.java b/Server/src/main/core/game/node/entity/impl/WalkingQueue.java index 4bfa60ba8..fdbd827da 100644 --- a/Server/src/main/core/game/node/entity/impl/WalkingQueue.java +++ b/Server/src/main/core/game/node/entity/impl/WalkingQueue.java @@ -12,7 +12,7 @@ import core.tools.Log; import core.tools.SystemLogger; import java.util.Deque; -import java.util.LinkedList; +import java.util.ArrayDeque; import static core.api.ContentAPIKt.log; @@ -25,7 +25,7 @@ public final class WalkingQueue { /** * The walking queue. */ - private final Deque walkingQueue = new LinkedList(); + private final Deque walkingQueue = new ArrayDeque(); /** * The entity. diff --git a/Server/src/main/core/game/system/config/ServerConfigParser.kt b/Server/src/main/core/game/system/config/ServerConfigParser.kt index d6244b6ad..62f519fcc 100644 --- a/Server/src/main/core/game/system/config/ServerConfigParser.kt +++ b/Server/src/main/core/game/system/config/ServerConfigParser.kt @@ -90,6 +90,7 @@ object ServerConfigParser { enable_doubling_money_scammers = data.getBoolean("world.enable_doubling_money_scammers", false), wild_pvp_enabled = data.getBoolean("world.wild_pvp_enabled"), jad_practice_enabled = data.getBoolean("world.jad_practice_enabled"), + smartpathfinder_bfs = data.getBoolean("world.smartpathfinder_bfs", false), message_model = data.getString("world.motw_identifier").toInt(), message_string = data.getString("world.motw_text").replace("@name", ServerConstants.SERVER_NAME) ) diff --git a/Server/src/main/core/game/world/GameSettings.kt b/Server/src/main/core/game/world/GameSettings.kt index 71352c6c2..36aefd890 100644 --- a/Server/src/main/core/game/world/GameSettings.kt +++ b/Server/src/main/core/game/world/GameSettings.kt @@ -82,6 +82,7 @@ class GameSettings var enable_doubling_money_scammers: Boolean, var wild_pvp_enabled: Boolean, var jad_practice_enabled: Boolean, + var smartpathfinder_bfs: Boolean, /**"Lobby" interface * The message of the week models to display @@ -136,6 +137,7 @@ class GameSettings val enable_doubling_money_scammers = if(data.containsKey("enable_doubling_money_scammers")) data["enable_doubling_money_scammers"] as Boolean else false 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 smartpathfinder_bfs = if(data.containsKey("smartpathfinder_bfs")) data["smartpathfinder_bfs"] 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() val message_of_the_week_text = data["message_of_the_week_text"].toString() @@ -165,6 +167,7 @@ class GameSettings enable_doubling_money_scammers, wild_pvp_enabled, jad_practice_enabled, + smartpathfinder_bfs, message_of_the_week_identifier, message_of_the_week_text ) @@ -188,4 +191,4 @@ class GameSettings return properties } } -} \ No newline at end of file +} diff --git a/Server/src/main/core/game/world/ImmerseWorld.kt b/Server/src/main/core/game/world/ImmerseWorld.kt index 22b20c1ef..287f657ca 100644 --- a/Server/src/main/core/game/world/ImmerseWorld.kt +++ b/Server/src/main/core/game/world/ImmerseWorld.kt @@ -15,7 +15,9 @@ import kotlin.concurrent.schedule class ImmerseWorld : StartupListener { override fun startup() { - spawnBots() + if(GameWorld.settings?.max_adv_bots!! > 0) { + spawnBots() + } } companion object { diff --git a/Server/src/main/core/game/world/map/Direction.java b/Server/src/main/core/game/world/map/Direction.java index 5b34c2f50..4555f931b 100644 --- a/Server/src/main/core/game/world/map/Direction.java +++ b/Server/src/main/core/game/world/map/Direction.java @@ -1,5 +1,7 @@ package core.game.world.map; +import core.game.world.map.path.ClipMaskSupplier; + /** * Represents a direction. * @author Emperor @@ -254,6 +256,29 @@ public enum Direction { return true; } + public boolean canMoveFrom(int z, int x, int y, ClipMaskSupplier clipMaskSupplier) { + int dx, dy; + boolean ret = true; + for (int f : traversal) { + switch(f) { + case 0x12c0120: dx = 0; dy = 1; break; // north + case 0x12c0180: dx = 1; dy = 0; break; // east + case 0x12c01e0: dx = 1; dy = 1; break; // northeast + case 0x12c0102: dx = 0; dy = -1; break; // south + case 0x12c0183: dx = 1; dy = -1; break; // southeast + case 0x12c0108: dx = -1; dy = 0; break; // west + case 0x12c010e: dx = -1; dy = -1; break; // southwest + case 0x12c0138: dx = -1; dy = 1; break; // northwest + default: return false; + } + int flag = clipMaskSupplier.getClippingFlag(z, x+dx, y+dy); + if ((flag & f) != 0) { + ret = false; + } + } + return ret; + } + /** * Sets the traversal. * @param traversal The traversal to set. @@ -261,4 +286,4 @@ public enum Direction { private void setTraversal(int[] traversal) { this.traversal = traversal; } -} \ No newline at end of file +} diff --git a/Server/src/main/core/game/world/map/path/Pathfinder.java b/Server/src/main/core/game/world/map/path/Pathfinder.java index 4202fa9bb..1b0dd6d2c 100644 --- a/Server/src/main/core/game/world/map/path/Pathfinder.java +++ b/Server/src/main/core/game/world/map/path/Pathfinder.java @@ -4,6 +4,7 @@ import core.game.node.Node; import core.game.node.entity.Entity; 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; @@ -73,6 +74,20 @@ public abstract class Pathfinder { */ 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; + } + } + /** * Finds a path from the location to the end location. * @param location The start location. @@ -689,4 +704,4 @@ public abstract class Pathfinder { } return false; } -} \ No newline at end of file +} diff --git a/Server/src/main/core/game/world/map/path/SmartPathfinder.kt b/Server/src/main/core/game/world/map/path/SmartPathfinder.kt index ece18eef2..a4e95789d 100644 --- a/Server/src/main/core/game/world/map/path/SmartPathfinder.kt +++ b/Server/src/main/core/game/world/map/path/SmartPathfinder.kt @@ -1,8 +1,17 @@ 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 java.util.Comparator +import java.util.PriorityQueue + +import java.io.File +import javax.imageio.ImageIO +import java.awt.image.BufferedImage + class SmartPathfinder /** * Constructs a new `SmartPathfinder` `Object`. @@ -76,12 +85,14 @@ internal constructor() : Pathfinder() { * @param dir The direction. * @param currentCost The current cost. */ - fun check(x: Int, y: Int, dir: Int, currentCost: Int) { - queueX[writePathPosition] = x - queueY[writePathPosition] = y - via[x][y] = dir - cost[x][y] = currentCost - writePathPosition = writePathPosition + 1 and 0xfff + 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, end: Location?, sizeX: Int, sizeY: Int, rotation: Int, type: Int, walkingFlag: Int, near: Boolean, clipMaskSupplier: ClipMaskSupplier?): Path { @@ -106,13 +117,29 @@ internal constructor() : Pathfinder() { check(curX, curY, 99, 0) try { if (moverSize < 2) { - checkSingleTraversal(end, sizeX, sizeY, type, rotation, walkingFlag, location, clipMaskSupplier!!) + 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 @@ -174,8 +201,20 @@ internal constructor() : Pathfinder() { } 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] @@ -190,6 +229,89 @@ internal constructor() : Pathfinder() { return path } + class UIntAsPointComparator(val end: Location) : Comparator { + 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. @@ -443,4 +565,4 @@ internal constructor() : Pathfinder() { } } } -} \ No newline at end of file +}