diff --git a/Server/src/main/core/game/node/entity/npc/NPC.java b/Server/src/main/core/game/node/entity/npc/NPC.java index 71071cf64..8d8ccf59b 100644 --- a/Server/src/main/core/game/node/entity/npc/NPC.java +++ b/Server/src/main/core/game/node/entity/npc/NPC.java @@ -462,7 +462,8 @@ public class NPC extends Entity { getLocks().lockMovement(100); getImpactHandler().setDisabledTicks(100); setAttribute("return-to-spawn", true); - GameWorld.getPulser().submit(new MovementPulse(this, getProperties().getSpawnLocation(), Pathfinder.SMART) { + + MovementPulse returnPulse = new MovementPulse(this, getProperties().getSpawnLocation(), Pathfinder.SMART) { @Override public boolean pulse() { getProperties().getCombatPulse().stop(); @@ -470,9 +471,13 @@ public class NPC extends Entity { fullRestore(); getImpactHandler().setDisabledTicks(0); removeAttribute("return-to-spawn"); + removeAttribute("return-to-spawn-pulse"); return true; } - }); + }; + + setAttribute("return-to-spawn-pulse", returnPulse); + GameWorld.getPulser().submit(returnPulse); return; } if (dialoguePlayer == null || !dialoguePlayer.isActive() || !dialoguePlayer.getInterfaceManager().hasChatbox()) { @@ -528,6 +533,14 @@ public class NPC extends Entity { getWalkingQueue().reset(); getPulseManager().clear(); getUpdateMasks().reset(); + if (getAttribute("return-to-spawn", false)) { + this.location = getProperties().getSpawnLocation(); + MovementPulse returnPulse = getAttribute("return-to-spawn-pulse"); + if (returnPulse != null) { + returnPulse.pulse(); + returnPulse.stop(); + } + } Repository.removeRenderableNPC(this); if (getViewport().getRegion() instanceof DynamicRegion) { clear(); diff --git a/Server/src/test/kotlin/core/PathfinderTests.kt b/Server/src/test/kotlin/core/PathfinderTests.kt index aa5b1b0f1..1b701d133 100644 --- a/Server/src/test/kotlin/core/PathfinderTests.kt +++ b/Server/src/test/kotlin/core/PathfinderTests.kt @@ -16,6 +16,7 @@ import core.game.node.entity.impl.PulseType import core.game.node.entity.npc.NPC import core.game.node.entity.player.Player import core.game.world.GameWorld +import core.game.world.map.Region import core.net.packet.PacketProcessor import core.plugin.ClassScanner import core.plugin.Plugin @@ -221,4 +222,39 @@ class PathfinderTests { Assertions.assertEquals(1.0, p.location.getDistance(npc.location)) } } + + @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)) + npc.isWalks = true + npc.isNeverWalks = false + npc.walkRadius = 5 + npc.init() + npc.properties.spawnLocation = ServerConstants.HOME_LOCATION + TestUtils.advanceTicks(5, false) + Assertions.assertEquals(true, npc.getAttribute("return-to-spawn", false)) + TestUtils.advanceTicks(50, false) + Assertions.assertEquals(true, npc.location.getDistance(ServerConstants.HOME_LOCATION) <= 9) + } + } + + @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)) + npc.isWalks = true + npc.isNeverWalks = false + npc.walkRadius = 5 + npc.init() + npc.properties.spawnLocation = ServerConstants.HOME_LOCATION + TestUtils.advanceTicks(3, false) + Assertions.assertEquals(true, npc.getAttribute("return-to-spawn", false)) + p.clear() + RegionManager.forId(npc.location.regionId).flagInactive(true) + TestUtils.advanceTicks(50, false) + Assertions.assertEquals(false, npc.getAttribute("return-to-spawn", false)) + Assertions.assertEquals(true, npc.location.getDistance(ServerConstants.HOME_LOCATION) <= 5) + } + } } \ No newline at end of file