From f77b03c286e0bc0b5e13081dd54ea42cc991e2e6 Mon Sep 17 00:00:00 2001 From: chsami <23292806+chsami@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:30:44 +0200 Subject: [PATCH] fix(walker): report ARRIVED when a pass starts on the final path endpoint processWalk cleared the target on reaching the path endpoint and let the same pass fall through to the after-stuck-check, which read the null currentTarget as an external cancel and returned EXIT even when the player stood on the goal. The endpoint branch now checks for a genuine cancel first, then returns ARRIVED when the player is within the tight finish threshold of the target (EXIT otherwise, with an honest trace). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV --- .../microbot/util/walker/Rs2Walker.java | 19 ++- .../Rs2WalkerPathEndpointArrivalTest.java | 139 ++++++++++++++++++ 2 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerPathEndpointArrivalTest.java diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2Walker.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2Walker.java index dae623eafd1..1b43f2c4679 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2Walker.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2Walker.java @@ -457,6 +457,10 @@ static WalkerRouteState routeStateForTesting() { return routeState; } + static WalkerState processWalkForTesting(WorldPoint target, int distance) { + return processWalk(target, distance, 0); + } + private static void clearRecentTransportContext() { routeState.clearRecentTransportContext(); @@ -1757,6 +1761,19 @@ private static WalkerState processWalk(WorldPoint target, int distance) { return processWalk(target, distance, 0); } + private static WalkerState settleReachedPathEndpoint(WorldPoint target, WorldPoint endpoint, WorldPoint playerLoc, + int distance, int processWalkTail) { + if (walkCancelledDiag(target, "processWalk:at-path-endpoint", processWalkTail)) { + return WalkerState.EXIT; + } + boolean arrived = playerLoc.distanceTo(target) <= tightFinishThreshold(target, endpoint, distance); + if (!arrived) { + traceProcessWalkExit("path-endpoint-short-of-target", target, processWalkTail); + } + setTarget(null, "rs2walker:processWalk:reached-path-endpoint"); + return arrived ? WalkerState.ARRIVED : WalkerState.EXIT; + } + private static WalkerState processWalk(WorldPoint target, int distance, int partialRetries) { if (debug) { return WalkerState.EXIT; @@ -1948,7 +1965,7 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part // Partial paths end at an intermediate waypoint (dst still far from {@code target}); // clearing here would drop currentTarget before the partial-path retry/recalc branch. if (!partialPath && isNear(dst, walkLoop.playerLoc) && routeState.interimTargetWp == null) { - setTarget(null, "rs2walker:processWalk:reached-path-endpoint"); + return settleReachedPathEndpoint(target, dst, walkLoop.playerLoc, distance, processWalkTail); } boolean shouldIssueActiveRouteIdleNudge = shouldIssueActiveRouteIdleNudge(); diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerPathEndpointArrivalTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerPathEndpointArrivalTest.java new file mode 100644 index 00000000000..8a868fb3f7c --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerPathEndpointArrivalTest.java @@ -0,0 +1,139 @@ +package net.runelite.client.plugins.microbot.util.walker; + +import net.runelite.api.Client; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.shortestpath.ShortestPathConfig; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.tile.Rs2Tile; +import net.runelite.client.plugins.microbot.util.walker.state.WalkerRouteState; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.util.HashMap; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Callable; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class Rs2WalkerPathEndpointArrivalTest +{ + private static final WorldPoint TARGET = new WorldPoint(3227, 3214, 0); + private static final List PATH = List.of( + new WorldPoint(3224, 3214, 0), + new WorldPoint(3225, 3214, 0), + new WorldPoint(3226, 3214, 0), + TARGET); + private static final String REACHED_ENDPOINT = "rs2walker:processWalk:reached-path-endpoint"; + + private MockedStatic microbot; + private MockedStatic player; + private MockedStatic tile; + private MockedStatic pathApi; + private ShortestPathConfig originalConfig; + private WorldPoint originalTarget; + private Rs2ActiveRouteStatus status; + + @Before + public void setUp() + { + originalConfig = Rs2Walker.config; + originalTarget = Rs2Walker.currentTarget; + Rs2Walker.config = mock(ShortestPathConfig.class); + Rs2Walker.clearWalkerDedupeForTesting(); + WalkerRouteState routeState = Rs2Walker.routeStateForTesting(); + routeState.lastRouteClearReason = ""; + routeState.interimTargetWp = null; + routeState.interimTargetIdx = -1; + routeState.stuckCount = 0; + routeState.lastMovedTimeMs = System.currentTimeMillis(); + + microbot = Mockito.mockStatic(Microbot.class); + microbot.when(Microbot::getClient).thenReturn(mock(Client.class)); + microbot.when(Microbot::isLoggedIn).thenReturn(true); + ClientThread clientThread = mock(ClientThread.class); + when(clientThread.runOnClientThreadOptional(any())).thenAnswer(invocation -> { + Callable callable = invocation.getArgument(0); + return Optional.ofNullable(callable.call()); + }); + microbot.when(Microbot::getClientThread).thenReturn(clientThread); + + player = Mockito.mockStatic(Rs2Player.class); + player.when(Rs2Player::getWorldLocation).thenReturn(TARGET); + + tile = Mockito.mockStatic(Rs2Tile.class); + tile.when(() -> Rs2Tile.getReachableTilesFromTile(any(WorldPoint.class), anyInt())).thenReturn(new HashMap<>()); + + status = mock(Rs2ActiveRouteStatus.class); + when(status.isPresent()).thenReturn(true); + when(status.isReady()).thenReturn(true); + when(status.getRawPath()).thenReturn(PATH); + when(status.getWalkablePath()).thenReturn(PATH); + when(status.getMetrics()).thenReturn(Optional.empty()); + pathApi = Mockito.mockStatic(Rs2PathApi.class); + pathApi.when(Rs2PathApi::getActiveRouteStatus).thenReturn(status); + pathApi.when(() -> Rs2PathApi.hasWalkableTileWithin(any(WorldPoint.class), anyInt())).thenReturn(true); + + Rs2Walker.currentTarget = TARGET; + } + + @After + public void tearDown() + { + pathApi.close(); + tile.close(); + player.close(); + microbot.close(); + Rs2Walker.currentTarget = originalTarget; + Rs2Walker.config = originalConfig; + Rs2Walker.clearWalkerDedupeForTesting(); + } + + @Test + public void standingOnTheEndpointThatIsTheTargetArrives() + { + WalkerState state = Rs2Walker.processWalkForTesting(TARGET, 0); + + assertEquals(WalkerState.ARRIVED, state); + assertEquals(REACHED_ENDPOINT, Rs2Walker.getLastRouteClearReason()); + assertNull(Rs2Walker.currentTarget); + } + + @Test + public void externalCancelBeforeTheEndpointCheckStillExits() + { + player.when(Rs2Player::isInteracting).thenAnswer(invocation -> { + Rs2Walker.currentTarget = null; + return false; + }); + + WalkerState state = Rs2Walker.processWalkForTesting(TARGET, 0); + + assertEquals(WalkerState.EXIT, state); + assertNotEquals(REACHED_ENDPOINT, Rs2Walker.getLastRouteClearReason()); + } + + @Test + public void endpointShortOfTheTightFinishThresholdDoesNotArrive() + { + WorldPoint farTarget = new WorldPoint(3230, 3214, 0); + Rs2Walker.currentTarget = farTarget; + + WalkerState state = Rs2Walker.processWalkForTesting(farTarget, 5); + + assertEquals(WalkerState.EXIT, state); + assertEquals(REACHED_ENDPOINT, Rs2Walker.getLastRouteClearReason()); + assertNull(Rs2Walker.currentTarget); + } +}