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..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 @@ -265,10 +265,48 @@ 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. + * 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 + * @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 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 WalkerState.ARRIVED; + } + log.warn("[Walker] Exceeded max recalc attempts ({}), giving up (dist={})", MAX_RECALC_ATTEMPTS, closeDist); + setTarget(null); + return WalkerState.UNREACHABLE; + } + + /** + * 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); @@ -331,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()); @@ -340,7 +389,11 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part lastMovedTimeMs = System.currentTimeMillis(); stuckCount = 0; setTarget(target); - return processWalk(target, distance, partialRetries); + WalkerState recalcResult = checkRecalcLimit(++recalcAttempts, target, distance); + if (recalcResult != null) { + return recalcResult; + } + continue; } if (stuckCount > 10) { var reachable = Rs2Tile.getReachableTilesFromTile(Rs2Player.getWorldLocation(), 5).keySet(); @@ -588,7 +641,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 +651,15 @@ 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); + continue; // normal progress, don't count as recalc + } + // Actual recalc scenario (not-near-path, etc.) + WalkerState recalcResult2 = checkRecalcLimit(++recalcAttempts, target, distance); + if (recalcResult2 != null) { + return recalcResult2; } - return processWalk(target, distance, partialRetries); + continue; } } catch (Exception ex) { if (ex instanceof InterruptedException) { @@ -610,9 +668,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) { @@ -815,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; } /**