From 2028d84c60622d359816f8a2a75cd8d81b09168c Mon Sep 17 00:00:00 2001 From: seppulcro Date: Tue, 5 May 2026 11:57:01 +0100 Subject: [PATCH 1/3] fix(walker): convert unbounded recursion to iterative loop processWalk() calls itself recursively at 3 sites with no depth limit. When the walker gets stuck (e.g. near obstacles on long paths), this floods the client thread task queue causing cascading TimeoutExceptions that freeze the entire client. Replace recursive processWalk() calls with a while(true) loop capped at MAX_RECALC_ATTEMPTS=15. Returns UNREACHABLE cleanly instead of crashing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../microbot/util/walker/Rs2Walker.java | 48 ++++++++++++++++--- 1 file changed, 41 insertions(+), 7 deletions(-) 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 b7315900e9..e30cc08404 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 @@ -265,10 +265,39 @@ private static WalkerState processWalk(WorldPoint target, int distance) { return processWalk(target, distance, 0); } + private static final int MAX_RECALC_ATTEMPTS = 15; + + /** + * Checks whether the walker has exceeded its maximum allowed path recalculation attempts. + * If the limit is reached, clears the target and returns true to signal the caller to give up. + * + * @param attempts the current number of recalculation attempts + * @return true if the limit has been exceeded, false otherwise + */ + private static boolean hasExceededRecalcLimit(int attempts) { + if (attempts >= MAX_RECALC_ATTEMPTS) { + log.warn("[Walker] Exceeded max recalc attempts ({}), giving up", MAX_RECALC_ATTEMPTS); + setTarget(null); + return true; + } + return false; + } + + /** + * Core walk loop that iteratively follows the calculated path to the target. + * Handles stall detection, partial path retries, and off-path recovery with a + * bounded recalculation limit to prevent infinite looping. + * + * @param target the destination world point + * @param distance acceptable distance threshold to consider arrival + * @param partialRetries number of partial-path retries already consumed + */ private static WalkerState processWalk(WorldPoint target, int distance, int partialRetries) { if (debug) { return WalkerState.EXIT; } + int recalcAttempts = 0; + while (true) { try { if (!Microbot.isLoggedIn()) { setTarget(null); @@ -340,7 +369,10 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part lastMovedTimeMs = System.currentTimeMillis(); stuckCount = 0; setTarget(target); - return processWalk(target, distance, partialRetries); + if (hasExceededRecalcLimit(++recalcAttempts)) { + return WalkerState.UNREACHABLE; + } + continue; } if (stuckCount > 10) { var reachable = Rs2Tile.getReachableTilesFromTile(Rs2Player.getWorldLocation(), 5).keySet(); @@ -588,7 +620,8 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part log.info("[Walker] Walked partial path ({} tiles remaining), retrying from current position (attempt {}/3)", finalDist, partialRetries + 1); recalculatePath(); - return processWalk(target, distance, partialRetries + 1); + partialRetries++; + continue; } log.info("[Walker] Walked partial path, exhausted retries. final distance to target: {}", finalDist); Telemetry.recordUnreachable("partial-retries-exhausted", Rs2Player.getWorldLocation(), @@ -597,11 +630,12 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part return WalkerState.UNREACHABLE; } else { if ("off-path-but-moving".equals(exitReason)) { - // Wait for the player to re-enter the path or to stop moving. Prevents a tight - // recursion loop that would spin on isNearPath() while the player is walking. sleepUntil(() -> isNearPath() || !Rs2Player.isMoving(), 2000); } - return processWalk(target, distance, partialRetries); + if (hasExceededRecalcLimit(++recalcAttempts)) { + return WalkerState.UNREACHABLE; + } + continue; } } catch (Exception ex) { if (ex instanceof InterruptedException) { @@ -610,9 +644,9 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part return WalkerState.EXIT; } log.error("Exception in Rs2Walker:", ex); + return WalkerState.EXIT; + } } - log.info("Exiting walker: 403"); - return WalkerState.EXIT; } public static boolean walkNextTo(GameObject target) { From 95cb23585d977cf0b0fb66a135b07c18efab6e72 Mon Sep 17 00:00:00 2001 From: seppulcro Date: Wed, 6 May 2026 11:54:31 +0100 Subject: [PATCH 2/3] fix: only increment recalcAttempts on actual recalculations off-path-but-moving is normal forward progress and should not count toward the MAX_RECALC_ATTEMPTS limit. This prevents premature UNREACHABLE returns on valid long walks. Addresses CodeRabbit review feedback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../runelite/client/plugins/microbot/util/walker/Rs2Walker.java | 2 ++ 1 file changed, 2 insertions(+) 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 e30cc08404..c449779124 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 @@ -631,7 +631,9 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part } else { if ("off-path-but-moving".equals(exitReason)) { sleepUntil(() -> isNearPath() || !Rs2Player.isMoving(), 2000); + continue; // normal progress, don't count as recalc } + // Actual recalc scenario (not-near-path, etc.) if (hasExceededRecalcLimit(++recalcAttempts)) { return WalkerState.UNREACHABLE; } From 3e22ad95fd3a251fa42d80808447f268a0f5a6b0 Mon Sep 17 00:00:00 2001 From: seppulcro Date: Sat, 9 May 2026 00:49:13 +0100 Subject: [PATCH 3/3] fix(walker): early-arrive at unwalkable tiles + preserve minimap zoom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the player is adjacent (dist≤1) to an unwalkable target tile (NPC, object, wall), return ARRIVED immediately instead of burning all 15 recalc attempts on a tile that can never be reached. When recalc attempts are exhausted but the player is within max(distance, 5) tiles of the target, treat as ARRIVED instead of UNREACHABLE. The ShortestPath pathfinder only considers exact tile matches as 'reached' — when the target tile has collision flags, the best path ends 1 tile short. The walker should accept that as success. Also stop resetting minimap zoom to 5 on every walkMiniMap() call — preserve the player's current zoom level. The forced zoom reset is detectable bot behaviour. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../microbot/util/walker/Rs2Walker.java | 52 ++++++++++++++----- 1 file changed, 40 insertions(+), 12 deletions(-) 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 c449779124..f8ca6c7e60 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 @@ -269,18 +269,27 @@ private static WalkerState processWalk(WorldPoint target, int distance) { /** * Checks whether the walker has exceeded its maximum allowed path recalculation attempts. - * If the limit is reached, clears the target and returns true to signal the caller to give up. + * When the limit is reached, checks if the player is close enough to the target to treat + * as arrived (common with world-map clicks and quest targets on unwalkable tiles). * * @param attempts the current number of recalculation attempts - * @return true if the limit has been exceeded, false otherwise + * @param target the destination world point + * @param distance the acceptable arrival distance + * @return ARRIVED if close enough, UNREACHABLE if too far, or null if under the limit */ - private static boolean hasExceededRecalcLimit(int attempts) { - if (attempts >= MAX_RECALC_ATTEMPTS) { - log.warn("[Walker] Exceeded max recalc attempts ({}), giving up", MAX_RECALC_ATTEMPTS); + private static WalkerState checkRecalcLimit(int attempts, WorldPoint target, int distance) { + if (attempts < MAX_RECALC_ATTEMPTS) { + return null; + } + int closeDist = Rs2Player.getWorldLocation().distanceTo(target); + if (closeDist <= Math.max(distance, 5)) { + log.info("[Walker] Exceeded max recalc but close enough (dist={}), treating as arrived", closeDist); setTarget(null); - return true; + return WalkerState.ARRIVED; } - return false; + log.warn("[Walker] Exceeded max recalc attempts ({}), giving up (dist={})", MAX_RECALC_ATTEMPTS, closeDist); + setTarget(null); + return WalkerState.UNREACHABLE; } /** @@ -360,6 +369,17 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part checkIfStuck(); if (isStuckTooLong()) { + // If we're adjacent to an unwalkable target tile (NPC, object, wall), + // the player is as close as they can get — treat as arrived instead of + // burning recalc attempts on a tile we can never stand on. + int distNow = Rs2Player.getWorldLocation().distanceTo(target); + LocalPoint lp = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), target); + if (distNow <= 1 && !Rs2Tile.isWalkable(lp)) { + log.info("[Walker] Adjacent to unwalkable target {} (dist={}), treating as arrived", target, distNow); + setTarget(null); + return WalkerState.ARRIVED; + } + long sinceMoved = System.currentTimeMillis() - lastMovedTimeMs; long threshold = stallThresholdMs(); Telemetry.recordStallRecalc(sinceMoved, Rs2Player.getWorldLocation()); @@ -369,8 +389,9 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part lastMovedTimeMs = System.currentTimeMillis(); stuckCount = 0; setTarget(target); - if (hasExceededRecalcLimit(++recalcAttempts)) { - return WalkerState.UNREACHABLE; + WalkerState recalcResult = checkRecalcLimit(++recalcAttempts, target, distance); + if (recalcResult != null) { + return recalcResult; } continue; } @@ -634,8 +655,9 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part continue; // normal progress, don't count as recalc } // Actual recalc scenario (not-near-path, etc.) - if (hasExceededRecalcLimit(++recalcAttempts)) { - return WalkerState.UNREACHABLE; + WalkerState recalcResult2 = checkRecalcLimit(++recalcAttempts, target, distance); + if (recalcResult2 != null) { + return recalcResult2; } continue; } @@ -851,7 +873,13 @@ public static boolean walkMiniMap(WorldPoint worldPoint, double zoomDistance) { public static boolean walkMiniMap(WorldPoint worldPoint) { - return walkMiniMap(worldPoint, 5); + // Preserve the player's current minimap zoom instead of resetting it. + // Forcibly setting zoom to 5 every click is detectable bot behaviour. + Point point = Rs2MiniMap.worldToMinimap(worldPoint); + if (point == null) return false; + if (!disableWalkerUpdate && !Rs2MiniMap.isPointInsideMinimap(point)) return false; + Microbot.getMouse().click(point); + return true; } /**