From 05f96a4379bb5f975d2eebcb841d63227080b6f0 Mon Sep 17 00:00:00 2001 From: Copilot Date: Sat, 29 Aug 2026 22:46:07 -0400 Subject: [PATCH 1/7] commit 1 --- .../lumbridge/lumbridge.npc-spawns.toml | 2 +- .../lumbridge/lumbridge.patrols.toml | 17 +++++++ data/entity/npc/wander_ranges.tables.toml | 1 + .../area/misthalin/lumbridge/castle/Hans.kt | 44 ++++++++++++++++++- 4 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 data/area/misthalin/lumbridge/lumbridge.patrols.toml diff --git a/data/area/misthalin/lumbridge/lumbridge.npc-spawns.toml b/data/area/misthalin/lumbridge/lumbridge.npc-spawns.toml index 7079fe9b0a..b253704f63 100644 --- a/data/area/misthalin/lumbridge/lumbridge.npc-spawns.toml +++ b/data/area/misthalin/lumbridge/lumbridge.npc-spawns.toml @@ -145,7 +145,7 @@ spawns = [ { id = "shop_assistant_lumbridge", x = 3214, y = 3243 }, { id = "bob", x = 3228, y = 3203 }, { id = "banker_3", x = 3208, y = 3222, level = 2, direction = "SOUTH" }, - { id = "hans", x = 3223, y = 3225 }, + { id = "hans", x = 3219, y = 3222 }, { id = "giant_spider", x = 3249, y = 3249 }, { id = "giant_spider", x = 3241, y = 3241 }, { id = "giant_spider", x = 3250, y = 3239 }, diff --git a/data/area/misthalin/lumbridge/lumbridge.patrols.toml b/data/area/misthalin/lumbridge/lumbridge.patrols.toml new file mode 100644 index 0000000000..3de63a455e --- /dev/null +++ b/data/area/misthalin/lumbridge/lumbridge.patrols.toml @@ -0,0 +1,17 @@ +[hans] +points = [ + { x = 3219, y = 3222 }, + { x = 3221, y = 3222 }, + { x = 3221, y = 3212 }, + { x = 3220, y = 3212 }, + { x = 3218, y = 3210 }, + { x = 3218, y = 3208 }, + { x = 3217, y = 3208 }, + { x = 3214, y = 3205 }, + { x = 3202, y = 3205 }, + { x = 3202, y = 3232 }, + { x = 3203, y = 3233 }, + { x = 3207, y = 3233 }, + { x = 3210, y = 3230 }, + { x = 3219, y = 3230 }, +] diff --git a/data/entity/npc/wander_ranges.tables.toml b/data/entity/npc/wander_ranges.tables.toml index 1bd7e3c9e1..c9eb6cf171 100644 --- a/data/entity/npc/wander_ranges.tables.toml +++ b/data/entity/npc/wander_ranges.tables.toml @@ -4,6 +4,7 @@ max_range = "int" wander_range = "int" [.hans] +wander_range = 0 max_range = 40 [.farmer] diff --git a/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Hans.kt b/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Hans.kt index 638c58f8b0..eefdc19c2d 100644 --- a/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Hans.kt +++ b/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Hans.kt @@ -5,26 +5,68 @@ import content.entity.player.dialogue.EvilLaugh import content.entity.player.dialogue.Neutral import content.entity.player.dialogue.type.choice import content.entity.player.dialogue.type.npc +import org.rsmod.game.pathfinder.PathFinder import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.data.definition.PatrolDefinitions +import world.gregs.voidps.engine.entity.character.mode.EmptyMode +import world.gregs.voidps.engine.entity.character.mode.Patrol import world.gregs.voidps.engine.entity.character.mode.Retreat +import world.gregs.voidps.engine.map.collision.Collisions +import world.gregs.voidps.engine.queue.queue -class Hans : Script { +class Hans( + private val patrols: PatrolDefinitions, +) : Script { + + private val pathFinder = PathFinder(flags = Collisions.map, useRouteBlockerFlags = true) init { + npcSpawn("hans") { + val patrol = patrols.get("hans") + if (patrol.waypoints.isEmpty()) { + return@npcSpawn + } + if (tile != patrol.waypoints.first().first) { + return@npcSpawn + } + mode = Patrol(this, patrol.waypoints) + } + npcOperate("Talk-to", "hans") { (target) -> npc("Hello. What are you doing here?") choice { option("I'm looking for whoever is in charge of this place.") { npc("Who, the Duke? He's in his study, on the first floor.") + resumePatrol(target) } option("I have come to kill everyone in this castle!") { target.say("Help! Help!") target.mode = Retreat(target, this) + resumePatrol(target, delay = 10) } option("I don't know. I'm lost. Where am I?") { npc("You are in Lumbridge Castle.") + resumePatrol(target) } } } } + + private fun resumePatrol(target: world.gregs.voidps.engine.entity.character.npc.NPC, delay: Int = 0) { + target.queue("hans_resume_patrol", delay) { + val patrol = patrols.get("hans") + if (patrol.waypoints.isEmpty()) { + return@queue + } + val resumeIndex = patrol.waypoints.withIndex() + .minByOrNull { target.tile.distanceTo(it.value.first) } + ?.index ?: 0 + val waypoint = patrol.waypoints[resumeIndex].first + val route = pathFinder.findPath(target.tile.level, target.tile.x, target.tile.y, waypoint.x, waypoint.y) + target.steps.queueRoute(route, waypoint) + target.set("patrol_index", resumeIndex) + target.mode = EmptyMode + target.mode = Patrol(target, patrol.waypoints) + } + } } From 575dfee2e381f386d6c4b7c9411213e3b85063fc Mon Sep 17 00:00:00 2001 From: Copilot Date: Sun, 30 Aug 2026 21:05:11 -0400 Subject: [PATCH 2/7] commit 2 --- .../lumbridge/lumbridge.patrols.toml | 2 +- .../area/misthalin/lumbridge/castle/Hans.kt | 72 ++++++++++++------- 2 files changed, 46 insertions(+), 28 deletions(-) diff --git a/data/area/misthalin/lumbridge/lumbridge.patrols.toml b/data/area/misthalin/lumbridge/lumbridge.patrols.toml index 3de63a455e..0079d9baae 100644 --- a/data/area/misthalin/lumbridge/lumbridge.patrols.toml +++ b/data/area/misthalin/lumbridge/lumbridge.patrols.toml @@ -1,6 +1,6 @@ [hans] points = [ - { x = 3219, y = 3222 }, + { x = 3219, y = 3222, delay = 10 }, { x = 3221, y = 3222 }, { x = 3221, y = 3212 }, { x = 3220, y = 3212 }, diff --git a/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Hans.kt b/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Hans.kt index eefdc19c2d..a42dc67568 100644 --- a/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Hans.kt +++ b/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Hans.kt @@ -5,68 +5,86 @@ import content.entity.player.dialogue.EvilLaugh import content.entity.player.dialogue.Neutral import content.entity.player.dialogue.type.choice import content.entity.player.dialogue.type.npc -import org.rsmod.game.pathfinder.PathFinder import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.ui.dialogue import world.gregs.voidps.engine.data.definition.PatrolDefinitions -import world.gregs.voidps.engine.entity.character.mode.EmptyMode +import world.gregs.voidps.engine.entity.character.npc.NPC +import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.mode.Patrol import world.gregs.voidps.engine.entity.character.mode.Retreat -import world.gregs.voidps.engine.map.collision.Collisions import world.gregs.voidps.engine.queue.queue class Hans( private val patrols: PatrolDefinitions, ) : Script { - private val pathFinder = PathFinder(flags = Collisions.map, useRouteBlockerFlags = true) - init { npcSpawn("hans") { val patrol = patrols.get("hans") - if (patrol.waypoints.isEmpty()) { - return@npcSpawn + if (patrol.waypoints.isNotEmpty()) { + set("hans_patrol_index", 0) + mode = Patrol(this, patrol.waypoints) } - if (tile != patrol.waypoints.first().first) { - return@npcSpawn + } + + npcMoved("hans") { + if (mode is Patrol) { + set("hans_patrol_index", get("patrol_index", 0)) } - mode = Patrol(this, patrol.waypoints) } npcOperate("Talk-to", "hans") { (target) -> + scheduleResume(this, target, 10) npc("Hello. What are you doing here?") - choice { - option("I'm looking for whoever is in charge of this place.") { + when (choice(listOf( + "I'm looking for whoever is in charge of this place.", + "I have come to kill everyone in this castle!", + "I don't know. I'm lost. Where am I?", + ))) { + 1 -> { npc("Who, the Duke? He's in his study, on the first floor.") - resumePatrol(target) } - option("I have come to kill everyone in this castle!") { + 2 -> { + target["hans_resume_nearest"] = true target.say("Help! Help!") target.mode = Retreat(target, this) - resumePatrol(target, delay = 10) } - option("I don't know. I'm lost. Where am I?") { + 3 -> { npc("You are in Lumbridge Castle.") - resumePatrol(target) } + else -> Unit } } } - private fun resumePatrol(target: world.gregs.voidps.engine.entity.character.npc.NPC, delay: Int = 0) { + private fun scheduleResume(player: Player, target: NPC, delay: Int) { + target.queue.clear("hans_resume_patrol") target.queue("hans_resume_patrol", delay) { - val patrol = patrols.get("hans") - if (patrol.waypoints.isEmpty()) { + if (player.dialogue != null) { + scheduleResume(player, target, 1) return@queue } - val resumeIndex = patrol.waypoints.withIndex() + resumePatrol(target, nearest = target["hans_resume_nearest", false]) + target.clear("hans_resume_nearest") + } + } + + private fun resumePatrol(target: NPC, nearest: Boolean = false) { + val patrol = patrols.get("hans") + if (patrol.waypoints.isEmpty()) { + return + } + val size = patrol.waypoints.size + val baseIndex = if (nearest) { + patrol.waypoints.withIndex() .minByOrNull { target.tile.distanceTo(it.value.first) } ?.index ?: 0 - val waypoint = patrol.waypoints[resumeIndex].first - val route = pathFinder.findPath(target.tile.level, target.tile.x, target.tile.y, waypoint.x, waypoint.y) - target.steps.queueRoute(route, waypoint) - target.set("patrol_index", resumeIndex) - target.mode = EmptyMode - target.mode = Patrol(target, patrol.waypoints) + } else { + target["hans_patrol_index", 0].mod(size) } + val resumeIndex = if (patrol.waypoints[baseIndex].first == target.tile) (baseIndex + 1).mod(size) else baseIndex + target.mode = Patrol(target, patrol.waypoints) + target.set("patrol_index", resumeIndex) + target.steps.clear() } } From d3d5a027bec8d9ebc03e3ca392a1b892fcdf5148 Mon Sep 17 00:00:00 2001 From: Copilot Date: Sun, 30 Aug 2026 21:23:46 -0400 Subject: [PATCH 3/7] commit 3 --- .../area/misthalin/lumbridge/castle/Hans.kt | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Hans.kt b/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Hans.kt index a42dc67568..16c069a856 100644 --- a/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Hans.kt +++ b/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Hans.kt @@ -8,10 +8,10 @@ import content.entity.player.dialogue.type.npc import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.ui.dialogue import world.gregs.voidps.engine.data.definition.PatrolDefinitions -import world.gregs.voidps.engine.entity.character.npc.NPC -import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.mode.Patrol import world.gregs.voidps.engine.entity.character.mode.Retreat +import world.gregs.voidps.engine.entity.character.npc.NPC +import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.queue.queue class Hans( @@ -36,23 +36,18 @@ class Hans( npcOperate("Talk-to", "hans") { (target) -> scheduleResume(this, target, 10) npc("Hello. What are you doing here?") - when (choice(listOf( - "I'm looking for whoever is in charge of this place.", - "I have come to kill everyone in this castle!", - "I don't know. I'm lost. Where am I?", - ))) { - 1 -> { + choice { + option("I'm looking for whoever is in charge of this place.") { npc("Who, the Duke? He's in his study, on the first floor.") } - 2 -> { + option("I have come to kill everyone in this castle!") { target["hans_resume_nearest"] = true target.say("Help! Help!") target.mode = Retreat(target, this) } - 3 -> { + option("I don't know. I'm lost. Where am I?") { npc("You are in Lumbridge Castle.") } - else -> Unit } } } From 3801d0abd662111618502c69cbd205fddae80011 Mon Sep 17 00:00:00 2001 From: V0lcanic <171797752+V0lcanic@users.noreply.github.com> Date: Sun, 30 Aug 2026 21:40:04 -0400 Subject: [PATCH 4/7] revert wander_range change copilot added that change. no clue why --- data/entity/npc/wander_ranges.tables.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/data/entity/npc/wander_ranges.tables.toml b/data/entity/npc/wander_ranges.tables.toml index c9eb6cf171..1bd7e3c9e1 100644 --- a/data/entity/npc/wander_ranges.tables.toml +++ b/data/entity/npc/wander_ranges.tables.toml @@ -4,7 +4,6 @@ max_range = "int" wander_range = "int" [.hans] -wander_range = 0 max_range = 40 [.farmer] From 24d759016df41ccaf31321058acc10aaa708e0ef Mon Sep 17 00:00:00 2001 From: Copilot Date: Sun, 30 Aug 2026 22:05:03 -0400 Subject: [PATCH 5/7] commit 5 --- .../area/misthalin/lumbridge/castle/Hans.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Hans.kt b/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Hans.kt index 16c069a856..550781b249 100644 --- a/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Hans.kt +++ b/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Hans.kt @@ -34,14 +34,18 @@ class Hans( } npcOperate("Talk-to", "hans") { (target) -> - scheduleResume(this, target, 10) + var resumeFromNearest = false + var resumeDelay = 10 + scheduleResume(this, target, resumeDelay) { resumeFromNearest } npc("Hello. What are you doing here?") choice { option("I'm looking for whoever is in charge of this place.") { npc("Who, the Duke? He's in his study, on the first floor.") } option("I have come to kill everyone in this castle!") { - target["hans_resume_nearest"] = true + resumeFromNearest = true + resumeDelay = 15 + scheduleResume(this, target, resumeDelay) { resumeFromNearest } target.say("Help! Help!") target.mode = Retreat(target, this) } @@ -52,15 +56,14 @@ class Hans( } } - private fun scheduleResume(player: Player, target: NPC, delay: Int) { + private fun scheduleResume(player: Player, target: NPC, delay: Int, nearest: () -> Boolean) { target.queue.clear("hans_resume_patrol") target.queue("hans_resume_patrol", delay) { if (player.dialogue != null) { - scheduleResume(player, target, 1) + scheduleResume(player, target, 1, nearest) return@queue } - resumePatrol(target, nearest = target["hans_resume_nearest", false]) - target.clear("hans_resume_nearest") + resumePatrol(target, nearest = nearest()) } } From 45b67b6343b5a46448c768855e8a6c764b3928eb Mon Sep 17 00:00:00 2001 From: V0lcanic <171797752+V0lcanic@users.noreply.github.com> Date: Sun, 30 Aug 2026 23:05:04 -0400 Subject: [PATCH 6/7] commit 5 --- .../area/misthalin/lumbridge/castle/Hans.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Hans.kt b/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Hans.kt index 16c069a856..550781b249 100644 --- a/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Hans.kt +++ b/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Hans.kt @@ -34,14 +34,18 @@ class Hans( } npcOperate("Talk-to", "hans") { (target) -> - scheduleResume(this, target, 10) + var resumeFromNearest = false + var resumeDelay = 10 + scheduleResume(this, target, resumeDelay) { resumeFromNearest } npc("Hello. What are you doing here?") choice { option("I'm looking for whoever is in charge of this place.") { npc("Who, the Duke? He's in his study, on the first floor.") } option("I have come to kill everyone in this castle!") { - target["hans_resume_nearest"] = true + resumeFromNearest = true + resumeDelay = 15 + scheduleResume(this, target, resumeDelay) { resumeFromNearest } target.say("Help! Help!") target.mode = Retreat(target, this) } @@ -52,15 +56,14 @@ class Hans( } } - private fun scheduleResume(player: Player, target: NPC, delay: Int) { + private fun scheduleResume(player: Player, target: NPC, delay: Int, nearest: () -> Boolean) { target.queue.clear("hans_resume_patrol") target.queue("hans_resume_patrol", delay) { if (player.dialogue != null) { - scheduleResume(player, target, 1) + scheduleResume(player, target, 1, nearest) return@queue } - resumePatrol(target, nearest = target["hans_resume_nearest", false]) - target.clear("hans_resume_nearest") + resumePatrol(target, nearest = nearest()) } } From 1366ad4c9d6ad6d9353e8d1957e9b77db1116d21 Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 1 Sep 2026 16:28:16 -0400 Subject: [PATCH 7/7] Does this look alright? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed: lumbridge.patrols.toml Converted from the new inline points array back to the older per-point patrol format. Changes: • Added defaultmode = "patrol" • Replaced the points = [...] block with patrol0 through patrol13 • Each entry now uses the legacy encoded tile string like 0_50_50_19_22,10 Effect: same Hans patrol path, just stored in the legacy format that PatrolDefinitions now accepts. PatrolDefinitions.kt updated to support older patrol config formats. What changed: •It now accepts defaultmode entries and ignores them. •It now accepts legacy patrol* keys and converts those strings with parseLegacyPatrolPoint(...). •A new helper parses legacy values like level_regionX_regionY_localX_localY[,delay] into Tile to delay. Net effect: the loader can read both the newer points format and older patrol definitions without throwing on legacy keys. --- .../lumbridge/lumbridge.patrols.toml | 31 +++++++++---------- .../data/definition/PatrolDefinitions.kt | 20 +++++++++++- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/data/area/misthalin/lumbridge/lumbridge.patrols.toml b/data/area/misthalin/lumbridge/lumbridge.patrols.toml index 0079d9baae..eb4284c82c 100644 --- a/data/area/misthalin/lumbridge/lumbridge.patrols.toml +++ b/data/area/misthalin/lumbridge/lumbridge.patrols.toml @@ -1,17 +1,16 @@ [hans] -points = [ - { x = 3219, y = 3222, delay = 10 }, - { x = 3221, y = 3222 }, - { x = 3221, y = 3212 }, - { x = 3220, y = 3212 }, - { x = 3218, y = 3210 }, - { x = 3218, y = 3208 }, - { x = 3217, y = 3208 }, - { x = 3214, y = 3205 }, - { x = 3202, y = 3205 }, - { x = 3202, y = 3232 }, - { x = 3203, y = 3233 }, - { x = 3207, y = 3233 }, - { x = 3210, y = 3230 }, - { x = 3219, y = 3230 }, -] +defaultmode = "patrol" +patrol0 = "0_50_50_19_22,10" +patrol1 = "0_50_50_21_22,0" +patrol2 = "0_50_50_21_12,0" +patrol3 = "0_50_50_20_12,0" +patrol4 = "0_50_50_18_10,0" +patrol5 = "0_50_50_18_8,0" +patrol6 = "0_50_50_17_8,0" +patrol7 = "0_50_50_14_5,0" +patrol8 = "0_50_50_2_5,0" +patrol9 = "0_50_50_2_32,0" +patrol10 = "0_50_50_3_33,0" +patrol11 = "0_50_50_7_33,0" +patrol12 = "0_50_50_10_30,0" +patrol13 = "0_50_50_19_30,0" diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/data/definition/PatrolDefinitions.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/data/definition/PatrolDefinitions.kt index f2d151edfa..d60580bce7 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/data/definition/PatrolDefinitions.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/data/definition/PatrolDefinitions.kt @@ -42,7 +42,12 @@ class PatrolDefinitions { } points.add(Tile(x, y, level) to delay) } - else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") + "defaultmode" -> string() + else -> if (key.startsWith("patrol")) { + points.add(parseLegacyPatrolPoint(string())) + } else { + throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") + } } } definitions[stringId] = PatrolDefinition(stringId = stringId, waypoints = points) @@ -54,4 +59,17 @@ class PatrolDefinitions { } return this } + + private fun parseLegacyPatrolPoint(value: String): Pair { + val parts = value.split(",", limit = 2) + val coords = parts[0].split("_") + require(coords.size == 5) { "Unexpected patrol point value: '$value'" } + val level = coords[0].toInt() + val regionX = coords[1].toInt() + val regionY = coords[2].toInt() + val localX = coords[3].toInt() + val localY = coords[4].toInt() + val delay = parts.getOrNull(1)?.toIntOrNull() ?: 0 + return Tile(regionX * 64 + localX, regionY * 64 + localY, level) to delay + } }