diff --git a/Server/src/main/core/game/interaction/InteractionListeners.kt b/Server/src/main/core/game/interaction/InteractionListeners.kt index e3cab3e34..df9142e40 100644 --- a/Server/src/main/core/game/interaction/InteractionListeners.kt +++ b/Server/src/main/core/game/interaction/InteractionListeners.kt @@ -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(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 diff --git a/Server/src/test/kotlin/core/PathfinderTests.kt b/Server/src/test/kotlin/core/PathfinderTests.kt index f3c2732c7..9bfcfbcd2 100644 --- a/Server/src/test/kotlin/core/PathfinderTests.kt +++ b/Server/src/test/kotlin/core/PathfinderTests.kt @@ -1,12 +1,18 @@ package core import TestUtils +import content.global.handlers.scenery.BankBoothListener +import content.global.handlers.npc.NPCTalkListener 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.Direction import core.game.world.map.Location import core.game.world.map.RegionManager import org.junit.jupiter.api.Assertions @@ -24,9 +30,18 @@ import core.plugin.ClassScanner import core.plugin.Plugin import core.tools.Log import org.rs09.consts.NPCs +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() + } + val NPC_TEST_LOC = ServerConstants.HOME_LOCATION!!.transform(2, 10, 0) + } @Test fun getOccupiedTilesShouldReturnCorrectSetOfTilesThatAnObjectOccupiesAtAllRotations() { //clay fireplace - 13609 - sizex: 1, sizey: 2 @@ -65,6 +80,222 @@ class PathfinderTests { ) } + @Test fun dumbPathfinderShouldUseRsmodRouting() { + val start = Location.create(3200, 3200, 0) + val dest = Location.create(3202, 3200, 0) + val blockedMiddle = ClipMaskSupplier { _, x, y -> + if (x == 3201 && y == 3200) 0x100 else 0 + } + + val path = Pathfinder.DUMB.find(start, 1, dest, 0, 0, 0, -1, 0, false, blockedMiddle) + + Assertions.assertTrue(path.isSuccessful) + Assertions.assertTrue(path.points.isNotEmpty()) + } + + @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) + } finally { + RegionManager.setRsmodFlag(0, 3201, 3200, true, 0) + } + } + + @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 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 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 { + 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 { + 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)