From 9754926150ad9bf792572cff97bc6b96105fd67b Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Sun, 26 Apr 2026 12:59:11 -0400 Subject: [PATCH 01/10] fix(rs2walker): blacklist failed transports, bound retries, guard Rs2Tile bounds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three independent bugs caused a single account to oscillate at the Varrock palace north wall for ~3h (240 STALL_RECALC events, 15+ identical wall-door interactions in 8 min, 29 identical pathfinder calls in 13 min) — exactly the shape of macro-detection signals. PathfinderConfig: add a TTL-keyed runtime blacklist of transport origins. Static eligibility (member flag, levels, quests, varbits) can pass plan-time while a hidden server-side gate (region unlock, sub-quest progression) makes the click a no-op at runtime; without this the next pathfinder run keeps re-emitting the same broken edge. Rs2Walker.handleTransports: when handleObject returns and the player did not reach the destination within the wait window, record the origin as failed and clear the current target so the next walk computes a fresh path that routes around it. Rs2Walker.processWalk: thread a no-progress retry counter through the recursive call chain. After MAX_NO_PROGRESS_RETRIES (5) re-entries where finalDist did not decrease, return WalkerState.UNREACHABLE instead of looping. Worst case is now ~50s of stuck behaviour, not unbounded. Rs2Tile.isTileReachable: bounds-check startX/startY before writing to the visited[][] grid. Player coordinates outside the loaded scene grid (instance transitions, stale worldview, base mismatch) previously threw ArrayIndexOutOfBoundsException ("Index 166 out of bounds for length 104") and crashed the walker thread mid-loop. --- .../pathfinder/PathfinderConfig.java | 39 +++++++++++++++++ .../plugins/microbot/util/tile/Rs2Tile.java | 7 +++ .../microbot/util/walker/Rs2Walker.java | 43 ++++++++++++++++--- 3 files changed, 83 insertions(+), 6 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java index 60ec296983..afcf4b7e95 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java @@ -122,6 +122,16 @@ public class PathfinderConfig { private final Set restrictedPointsPacked; private final Set internalRestrictedPointsPacked; private volatile boolean useNpcs; + + // Runtime blacklist of transport origin tiles whose interaction failed to deliver + // the player to the destination. The static eligibility checks (member flag, levels, + // quests, varbits) can't capture every server-side gate (region unlocks, sub-quest + // progression, etc.), so a transport can pass plan-time and still no-op at click + // time. Without this, the pathfinder keeps emitting the same broken edge and the + // walker re-attempts it forever. TTL clears stale entries so the player can retry + // after world hops or unlock progression. + private final Map failedTransportOriginsPacked = new ConcurrentHashMap<>(); + private static final long FAILED_TRANSPORT_TTL_MS = 5L * 60L * 1000L; //END microbot variables private volatile TeleportationItem useTeleportationItems; @@ -565,6 +575,27 @@ private boolean varplayerChecks(Transport transport) { .allMatch(varplayerCheck -> varplayerCheck.matches(Microbot.getVarbitPlayerValue(varplayerCheck.getVarplayerId()))); } + public void addFailedTransportRestriction(WorldPoint origin) { + if (origin == null) return; + failedTransportOriginsPacked.put(WorldPointUtil.packWorldPoint(origin), System.currentTimeMillis()); + } + + public void clearFailedTransportRestrictions() { + failedTransportOriginsPacked.clear(); + } + + private boolean isRecentlyFailedOrigin(WorldPoint origin) { + if (origin == null) return false; + int packed = WorldPointUtil.packWorldPoint(origin); + Long failedAt = failedTransportOriginsPacked.get(packed); + if (failedAt == null) return false; + if (System.currentTimeMillis() - failedAt > FAILED_TRANSPORT_TTL_MS) { + failedTransportOriginsPacked.remove(packed); + return false; + } + return true; + } + private boolean useTransport(Transport transport) { boolean traceMoa = transport.getType() == TransportType.SEASONAL_TRANSPORT && transport.getDisplayInfo() != null @@ -577,6 +608,14 @@ private boolean useTransport(Transport transport) { return false; } + // Runtime blacklist for transports whose origin tile recently failed to deliver + // the player. Skip before any other check so a broken edge stops re-emerging + // from the next pathfinder run (see addFailedTransportRestriction comment). + if (isRecentlyFailedOrigin(transport.getOrigin())) { + log.debug("Transport ( O: {} D: {} ) skipped: origin recently failed", transport.getOrigin(), transport.getDestination()); + return false; + } + // Check if the feature flag is disabled if (!isFeatureEnabled(transport)) { log.debug("Transport Type {} is disabled by feature flag", transport.getType()); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tile/Rs2Tile.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tile/Rs2Tile.java index 3193ce542c..0843a4957f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tile/Rs2Tile.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tile/Rs2Tile.java @@ -415,6 +415,13 @@ public static boolean isTileReachable(WorldPoint targetPoint) { startX = playerLoc.getX() - Microbot.getClient().getBaseX(); startY = playerLoc.getY() - Microbot.getClient().getBaseY(); } + // Player can be outside the loaded scene grid (instance transitions, stale + // worldview, base mismatch in non-top-level views). Without this guard the + // visited[startX][startY] write throws ArrayIndexOutOfBoundsException and + // crashes the walker thread mid-loop. + if (!isWithinBounds(startX, startY)) { + return false; + } final int startPoint = (startX << 16) | startY; ArrayDeque queue = new ArrayDeque<>(); 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 1a87e96ab7..a6e5c71ed7 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 @@ -270,10 +270,18 @@ public static WalkerState walkWithState(WorldPoint target) { * @param distance */ private static WalkerState processWalk(WorldPoint target, int distance) { - return processWalk(target, distance, 0); + return processWalk(target, distance, 0, 0, Integer.MAX_VALUE); } - private static WalkerState processWalk(WorldPoint target, int distance, int partialRetries) { + // Bound for the no-progress recursion path (line where finalDist >= distance and we + // re-enter). Each retry can burn ~10s on a failed transport interaction or a stuck + // mini-map click, so 5 retries caps the worst-case stuck loop at ~50s before the + // walker bails to UNREACHABLE. Without this, a transport that passes plan-time but + // fails at click-time put the walker into an unbounded loop (3h+ in the wild). + private static final int MAX_NO_PROGRESS_RETRIES = 5; + + private static WalkerState processWalk(WorldPoint target, int distance, int partialRetries, + int noProgressRetries, int prevFinalDist) { if (debug) { return WalkerState.EXIT; } @@ -358,7 +366,7 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part lastMovedTimeMs = System.currentTimeMillis(); stuckCount = 0; setTarget(target); - return processWalk(target, distance, partialRetries); + return processWalk(target, distance, partialRetries, noProgressRetries, prevFinalDist); } if (stuckCount > 10) { var reachable = Rs2Tile.getReachableTilesFromTile(Rs2Player.getWorldLocation(), 5).keySet(); @@ -637,7 +645,7 @@ && handleNearbyRawPathSceneObjects(rawPath, HANDLER_RANGE)) { log.info("[Walker] Walked partial path ({} tiles remaining), retrying from current position (attempt {}/3)", finalDist, partialRetries + 1); recalculatePath(); - return processWalk(target, distance, partialRetries + 1); + return processWalk(target, distance, partialRetries + 1, noProgressRetries, prevFinalDist); } log.info("[Walker] Walked partial path, exhausted retries. final distance to target: {}", finalDist); Telemetry.recordUnreachable("partial-retries-exhausted", Rs2Player.getWorldLocation(), @@ -653,7 +661,16 @@ && handleNearbyRawPathSceneObjects(rawPath, HANDLER_RANGE)) { return WalkerState.EXIT; } } - return processWalk(target, distance, partialRetries); + int nextNoProgress = (finalDist >= prevFinalDist) ? noProgressRetries + 1 : 0; + if (nextNoProgress > MAX_NO_PROGRESS_RETRIES) { + log.warn("[Walker] No progress after {} retries (finalDist={}, prevFinalDist={}, exitReason={}) — bailing UNREACHABLE", + nextNoProgress, finalDist, prevFinalDist, exitReason); + Telemetry.recordUnreachable("no-progress-retries-exhausted", Rs2Player.getWorldLocation(), + target, Rs2Player.getWorldLocation(), 0, distance, ShortestPathPlugin.getPathfinder()); + setTarget(null); + return WalkerState.UNREACHABLE; + } + return processWalk(target, distance, partialRetries, nextNoProgress, finalDist); } } catch (Exception ex) { if (ex instanceof InterruptedException || ex.getCause() instanceof InterruptedException) { @@ -2568,7 +2585,21 @@ private static boolean handleTransports(List path, int indexOfStartP return false; } sleepUntil(() -> !Rs2Player.isAnimating()); - return sleepUntilTrue(() -> Rs2Player.getWorldLocation().distanceTo(transport.getDestination()) < OFFSET); + boolean reached = sleepUntilTrue(() -> Rs2Player.getWorldLocation().distanceTo(transport.getDestination()) < OFFSET); + if (!reached) { + // Static eligibility (member flag, levels, quest, varbits) said this + // transport was usable, but the click produced no movement (10s timeout). + // Hidden server-side gate (region unlock, sub-quest progression, etc.). + // Blacklist the origin so the next pathfinder run routes around it + // instead of re-emitting the same broken edge — the unbounded retry + // loop this prevented produced ~3 hours of position oscillation in the + // wild (240 STALL_RECALC events on one Varrock palace shortcut). + log.warn("[Walker] Transport {} (type={} origin={} dest={}) failed to deliver player after click — blacklisting origin", + transport.getDisplayInfo(), transport.getType(), transport.getOrigin(), transport.getDestination()); + ShortestPathPlugin.getPathfinderConfig().addFailedTransportRestriction(transport.getOrigin()); + setTarget(null); + } + return reached; } } } From 6985aaae3d54de87a6ad9cd746aaa9e76bc620c8 Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Mon, 27 Apr 2026 14:44:31 -0400 Subject: [PATCH 02/10] fix(pathfinder): block walking edges whose only crossing is a filtered transport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a transport is filtered out (member-only, quest gate, F2P, level requirement), the upstream collision map can still permit walking across the underlying edge — the TSV transport entry is the only signal that a fence/jump-gap exists there. BFS would happily route across the obstacle as if it weren't there, producing paths that look valid but require an obstacle the player can't use (canonical case: trellis at (3228,3470) on F2P). For each filtered transport with adjacent (Chebyshev 1) origin and destination on the same plane, register both directions of the edge in a blocked-edges set rebuilt every refreshTransports(). CollisionMap consults it during neighbor expansion and skips blocked edges. --- .../shortestpath/pathfinder/CollisionMap.java | 4 ++ .../pathfinder/PathfinderConfig.java | 46 ++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/CollisionMap.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/CollisionMap.java index af2da68622..130ceb1c90 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/CollisionMap.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/CollisionMap.java @@ -226,6 +226,10 @@ public List getNeighbors(Node node, VisitedTiles visited, PathfinderConfig if (visited.get(neighborPacked)) continue; if (config.getRestrictedPointsPacked().contains(neighborPacked)) continue; if (config.getCustomRestrictions().contains(neighborPacked)) continue; + // Block walking across edges whose only crossing is a filtered transport + // (e.g. trellis fence on F2P). The TSV transport entry is the only signal + // the obstacle exists; without this guard BFS strolls across. + if (config.isWalkingEdgeBlocked(node.packedPosition, neighborPacked)) continue; if (ignoreCollisionPacked.contains(node.packedPosition)) { neighbors.add(new Node(neighborPacked, node)); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java index afcf4b7e95..6175a8f138 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java @@ -132,6 +132,16 @@ public class PathfinderConfig { // after world hops or unlock progression. private final Map failedTransportOriginsPacked = new ConcurrentHashMap<>(); private static final long FAILED_TRANSPORT_TTL_MS = 5L * 60L * 1000L; + + // Walking edges that the BFS must treat as walls because the only known way to + // cross them is a transport that's currently filtered out (member flag, quest, F2P + // gate, etc.). Trellis at (3228,3470)<->(3228,3471) is the canonical case: the + // upstream collision map omits the fence, relying on the agility-shortcut transport + // to imply the obstacle. Without this set, BFS happily walks across the fence as if + // it were open ground when the transport is excluded — producing paths that look + // valid but require climbing an obstacle the player can't use. Encoded as + // (fromPacked << 32) | toPacked. Rebuilt every refreshTransports(). + private final Set blockedWalkingEdges = ConcurrentHashMap.newKeySet(); //END microbot variables private volatile TeleportationItem useTeleportationItems; @@ -298,6 +308,7 @@ private void refreshTransports(WorldPoint target) { transports.clear(); transportsPacked.clear(); usableTeleports.clear(); + blockedWalkingEdges.clear(); long mergeStart = System.currentTimeMillis(); Map> mergedList = createMergedList(); @@ -368,7 +379,25 @@ private void refreshTransports(WorldPoint target) { stats[2] += (int)(elapsed / 1_000); if (usable) stats[1]++; - if (!usable) continue; + if (!usable) { + // The transport is filtered out, but the TSV entry implies an + // obstacle exists between origin and destination — the upstream + // collision map sometimes relies on the transport edge to encode + // the fence/jump-gap. If origin and destination are adjacent + // (Chebyshev 1) on the same plane, treat the walking edge as + // blocked in both directions so BFS doesn't stroll across. + WorldPoint o = transport.getOrigin(); + WorldPoint d = transport.getDestination(); + if (o != null && d != null + && o.getPlane() == d.getPlane() + && Math.max(Math.abs(o.getX() - d.getX()), Math.abs(o.getY() - d.getY())) == 1) { + int op = WorldPointUtil.packWorldPoint(o); + int dp = WorldPointUtil.packWorldPoint(d); + blockedWalkingEdges.add(packEdge(op, dp)); + blockedWalkingEdges.add(packEdge(dp, op)); + } + continue; + } checkedTransports++; if (point == null) { usableTeleports.add(transport); @@ -575,6 +604,21 @@ private boolean varplayerChecks(Transport transport) { .allMatch(varplayerCheck -> varplayerCheck.matches(Microbot.getVarbitPlayerValue(varplayerCheck.getVarplayerId()))); } + private static long packEdge(int fromPacked, int toPacked) { + return ((long) fromPacked << 32) | (toPacked & 0xFFFFFFFFL); + } + + /** + * True when the walking edge from {@code fromPacked} to {@code toPacked} is + * blocked by an excluded transport — i.e. the only known way to traverse this + * edge is a transport the player can't currently use (quest/skill/F2P gate). + * Used by {@link CollisionMap#getNeighbors} to prevent BFS from walking across + * fences whose only crossing is a filtered agility shortcut etc. + */ + public boolean isWalkingEdgeBlocked(int fromPacked, int toPacked) { + return !blockedWalkingEdges.isEmpty() && blockedWalkingEdges.contains(packEdge(fromPacked, toPacked)); + } + public void addFailedTransportRestriction(WorldPoint origin) { if (origin == null) return; failedTransportOriginsPacked.put(WorldPointUtil.packWorldPoint(origin), System.currentTimeMillis()); From d94f1b75bc9772a93fe4e85550bf58c02ba8d6c0 Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Mon, 27 Apr 2026 14:45:43 -0400 Subject: [PATCH 03/10] fix(walker): bail UNREACHABLE on BFS exhaustion outside proximity threshold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pathfinder now records a searchExhausted flag when both boundary and pending queues drain without reaching a target — distinguished from cutoff (queues still hold candidates) and target-found (early break). The walker checks it on entry and bails UNREACHABLE only when the best endpoint is farther than the caller's `distance` threshold. When the endpoint is within threshold (NPC reach, throne/altar approach, etc.) walking the partial path is the correct outcome, so don't bail there — this avoids regressing valid one-tile-short approaches. Also dumps the raw path tiles + every transport hop on each completed run so the actual edge sequence BFS picked is visible in the log. --- .../shortestpath/pathfinder/Pathfinder.java | 53 +++++++++++++++++++ .../microbot/util/walker/Rs2Walker.java | 19 +++++++ 2 files changed, 72 insertions(+) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java index 789f67ac02..25c8e6f238 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java @@ -58,6 +58,15 @@ public class Pathfinder implements Runnable { private volatile boolean pathNeedsUpdate = false; private volatile boolean smoothed = false; private volatile Node bestLastNode; + // True when the BFS exhausted both boundary and pending without reaching a + // target — the destination is genuinely unreachable from the source given + // the current transport graph. Distinguished from cutoff timeout (queues + // still hold candidates) and target-found (loop broke early). Walker uses + // this to bail UNREACHABLE instead of walking a partial path toward the + // closest-by-heuristic node, which on a quest-locked area produced a + // 60-second tour around the locked region in the wild. + @Getter + private volatile boolean searchExhausted = false; /** * Teleportation transports are updated when this changes. * Can be either: @@ -295,6 +304,11 @@ public void run() { addNeighbors(node); } + // Capture exhaustion before finally clears the queues. Both empty + // after the while loop means BFS terminated by the loop condition + // (no more nodes to expand) — i.e. dest is unreachable. Cutoff and + // target-reached exits leave queues non-empty. + searchExhausted = !cancelled && boundary.isEmpty() && pending.isEmpty(); log.info("[Pathfinder] Loop exited. cancelled={}, boundaryEmpty={}, pendingEmpty={}, bestLastNode={}", cancelled, boundary.isEmpty(), pending.isEmpty(), bestLastNode == null ? "null" : WorldPointUtil.toString(bestLastNode.packedPosition)); @@ -311,6 +325,45 @@ public void run() { log.info("[Pathfinder] run() completed. done={}, cancelled={}, stats={}", done, cancelled, getStats() != null ? getStats().toString() : "null"); + + // Diagnostic: walk the bestLastNode chain and log every transport hop in + // the final path. Lets us prove which transports BFS actually selected + // (e.g., distinguishes a palace door from an agility-shortcut trellis at + // adjacent tiles). Cheap — only walks the linked list once per run. + if (bestLastNode != null) { + int hops = 0; + Node cur = bestLastNode; + while (cur != null && cur.previous != null) { + if (cur instanceof TransportNode) { + log.info("[Pathfinder] transport hop in path: {} -> {} cost={}", + WorldPointUtil.toString(cur.previous.packedPosition), + WorldPointUtil.toString(cur.packedPosition), + cur.cost); + hops++; + } + cur = cur.previous; + } + if (hops == 0) { + log.info("[Pathfinder] transport hops in path: 0 (walking-only)"); + } + + // Dump the full raw path (pre-smoother) for diagnosis. Tells us the + // actual tile sequence BFS picked, so we can spot illegal wall steps + // (e.g., (3228,3470) -> (3228,3471) without a transport — which would + // be the trellis fence walked through as if it weren't there). + java.util.List rawPath = bestLastNode.getPath(); + int sz = rawPath.size(); + log.info("[Pathfinder] raw path: {} tiles", sz); + if (sz > 0) { + StringBuilder sb = new StringBuilder("[Pathfinder] raw path tiles: "); + for (int i = 0; i < sz; i++) { + WorldPoint p = rawPath.get(i); + sb.append("(").append(p.getX()).append(",").append(p.getY()).append(",").append(p.getPlane()).append(")"); + if (i < sz - 1) sb.append(" "); + } + log.info(sb.toString()); + } + } } } 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 a6e5c71ed7..24cbf0bae7 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 @@ -330,6 +330,25 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part dst = path.get(path.size()-1); } + // BFS exhausted both queues without reaching the target — destination is + // genuinely unreachable (quest-locked gate, walled-off area, etc.). Bail + // ONLY if endpoint is outside the caller's proximity threshold: when + // dst is within `distance` of target, the partial path lands the player + // close enough to satisfy the request (e.g., NPCs accept 1-tile reach, + // BFS commonly exhausts at adjacent-to-target when the final tile is a + // throne/altar/object the player can't stand on). Walking the path is + // correct in that case. Bailing only matters when the closest-found tile + // is genuinely far — that's what produced the 25+ tile palace tour. + if (pathfinder.isSearchExhausted() + && (dst == null || dst.distanceTo(target) > distance)) { + log.warn("[Walker] Pathfinder exhausted search without reaching target {} (best endpoint {}, dist={}, threshold={}) — bailing UNREACHABLE", + target, dst, dst == null ? -1 : dst.distanceTo(target), distance); + Telemetry.recordUnreachable("search-exhausted", Rs2Player.getWorldLocation(), + target, dst, path == null ? 0 : path.size(), distance, pathfinder); + setTarget(null); + return WalkerState.UNREACHABLE; + } + boolean partialPath = false; if (dst == null || dst.distanceTo(target) > distance) { if (path != null && path.size() > 1) { From 9723c58183992cd9d739839e271fd7d37571fd6b Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Mon, 27 Apr 2026 14:46:49 -0400 Subject: [PATCH 04/10] feat(pathfinder): inject scene-discovered doors as virtual transports for BFS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit transports.tsv is incomplete — palace internal doors, throne rooms, and generic interior doors aren't listed, so BFS can't route through them and bails UNREACHABLE for destinations a human player could trivially reach. On every refresh, scan the loaded scene for door WallObjects whose ObjectComposition exposes an "Open" action and inject them as bidirectional virtual Transport edges (TSV entries take precedence — existing edges aren't overridden). The whole scan runs in a single client-thread hop. Each WallObject's ObjectComposition lookup needs the client thread; doing those individually from a background thread costs ~26s for ~200 walls. One hop drops it to ~20ms. Bounded to a 52-tile radius around the player to match the rest of the walker's scene-handler range. Existing handleDoors() in Rs2Walker opens these doors at click time — this commit only makes BFS aware of them. Rs2Walker.sessionBlacklistedDoors is promoted to public so the scan can skip blacklisted tiles. --- .../pathfinder/PathfinderConfig.java | 137 ++++++++++++++++++ .../microbot/util/walker/Rs2Walker.java | 2 +- 2 files changed, 138 insertions(+), 1 deletion(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java index 6175a8f138..4a4bc36ac4 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java @@ -420,6 +420,19 @@ private void refreshTransports(WorldPoint target) { } long similarTime = System.currentTimeMillis() - similarStart; + // Patch H: scene-discovered doors. The TSV is incomplete (palace internal + // doors, throne rooms, etc.) — without these, BFS can't route through them + // and bails UNREACHABLE for destinations a human player could trivially + // reach. Scan loaded scene for door WallObjects and inject as virtual + // transports so BFS plans through them. handleDoors() in the walker opens + // them at click time. + long doorStart = System.currentTimeMillis(); + int doorsAdded = discoverSceneDoors(); + long doorTime = System.currentTimeMillis() - doorStart; + if (doorsAdded > 0) { + log.info("[refreshTransports] scene doors: added={}, time={}ms", doorsAdded, doorTime); + } + refreshAvailableItemIds = null; refreshBoostedLevels = null; refreshCurrencyCache = null; @@ -619,6 +632,130 @@ public boolean isWalkingEdgeBlocked(int fromPacked, int toPacked) { return !blockedWalkingEdges.isEmpty() && blockedWalkingEdges.contains(packEdge(fromPacked, toPacked)); } + /** + * Patch H: enumerate door WallObjects in the loaded scene and inject them as + * virtual Transport edges so BFS can route through doors not present in + * transports.tsv (palace internal chambers, generic interior doors, etc.). + * + * Only WallObjects whose ObjectComposition exposes an "Open" action are + * added — gated actions (Pay-toll, Pick-lock) imply quest/skill requirements + * that the TSV captures more accurately, so we defer to the TSV for those. + * Existing transports at the same edge are not overridden. + * + * @return number of door edges added (one direction == 1, bidirectional adds 2) + */ + private int discoverSceneDoors() { + if (!GameState.LOGGED_IN.equals(client.getGameState())) return 0; + + // Whole scan on client thread in one hop. Each WallObject requires an + // ObjectComposition lookup which itself needs the client thread; doing + // them individually from a background thread costs ~26s for ~200 walls + // because every lookup blocks waiting for a thread switch. + Integer result = Microbot.getClientThread().runOnClientThreadOptional(this::discoverSceneDoorsOnClientThread) + .orElse(0); + return result != null ? result : 0; + } + + private int discoverSceneDoorsOnClientThread() { + try { + if (client.getTopLevelWorldView().getScene().isInstance()) return 0; + } catch (Exception e) { + return 0; + } + + Player player = client.getLocalPlayer(); + if (player == null) return 0; + WorldPoint playerLoc = player.getWorldLocation(); + if (playerLoc == null) return 0; + int playerX = playerLoc.getX(); + int playerY = playerLoc.getY(); + int playerPlane = playerLoc.getPlane(); + + Scene scene = player.getWorldView().getScene(); + Tile[][][] tiles = scene.getTiles(); + if (tiles == null) return 0; + + int added = 0; + for (int z = 0; z < tiles.length; z++) { + Tile[][] plane = tiles[z]; + if (plane == null) continue; + for (int x = 0; x < plane.length; x++) { + Tile[] col = plane[x]; + if (col == null) continue; + for (int y = 0; y < col.length; y++) { + Tile tile = col[y]; + if (tile == null) continue; + WallObject wall = tile.getWallObject(); + if (wall == null) continue; + + WorldPoint probe = wall.getWorldLocation(); + if (probe == null || probe.getPlane() != playerPlane) continue; + // Same scene-radius cap as the rest of the walker (52 tiles). + if (Math.max(Math.abs(probe.getX() - playerX), Math.abs(probe.getY() - playerY)) > 52) continue; + if (Rs2Walker.sessionBlacklistedDoors.contains(probe)) continue; + + ObjectComposition comp = client.getObjectDefinition(wall.getId()); + if (comp == null || comp.getImpostorIds() != null) continue; + String name = comp.getName(); + if (name == null || "null".equals(name)) continue; + + String[] actions = comp.getActions(); + if (actions == null) continue; + String action = null; + for (String a : actions) { + if (a == null) continue; + if (a.toLowerCase().startsWith("open")) { + action = a; + break; + } + } + if (action == null) continue; + + int dx, dy; + switch (wall.getOrientationA()) { + case 1: dx = -1; dy = 0; break; + case 2: dx = 0; dy = 1; break; + case 4: dx = 1; dy = 0; break; + case 8: dx = 0; dy = -1; break; + default: continue; + } + + WorldPoint neighbor = new WorldPoint(probe.getX() + dx, probe.getY() + dy, probe.getPlane()); + if (Rs2Walker.sessionBlacklistedDoors.contains(neighbor)) continue; + + int objectId = comp.getId(); + String displayInfo = "Door (" + name + ") @ " + probe; + added += addRuntimeDoorEdge(probe, neighbor, displayInfo, action, name, objectId); + added += addRuntimeDoorEdge(neighbor, probe, displayInfo, action, name, objectId); + } + } + } + return added; + } + + private int addRuntimeDoorEdge(WorldPoint origin, WorldPoint destination, + String displayInfo, String action, String name, int objectId) { + Set existing = transports.get(origin); + if (existing != null) { + for (Transport t : existing) { + if (destination.equals(t.getDestination())) { + return 0; // TSV already covers this edge + } + } + } + Transport door = new Transport(origin, destination, displayInfo, + TransportType.TRANSPORT, false, action, name, objectId); + // Per refreshTransports invariant, transports and transportsPacked share + // the same Set reference per origin — add once via the shared reference. + Set set = transports.computeIfAbsent(origin, k -> new HashSet<>()); + int packedOrigin = WorldPointUtil.packWorldPoint(origin); + if (transportsPacked.get(packedOrigin) == null) { + transportsPacked.put(packedOrigin, set); + } + set.add(door); + return 1; + } + public void addFailedTransportRestriction(WorldPoint origin) { if (origin == null) return; failedTransportOriginsPacked.put(WorldPointUtil.packWorldPoint(origin), System.currentTimeMillis()); 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 24cbf0bae7..151242755e 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 @@ -1619,7 +1619,7 @@ private static boolean handleNearbyRawPathSceneObjects(List rawPath, // Session-local set of door tiles the walker detected as quest/stat-locked after a // failed interact. Cleared when the client restarts. Prevents infinite retry loops // through the same restricted door when the restriction isn't in restrictions.tsv. - static final Set sessionBlacklistedDoors = ConcurrentHashMap.newKeySet(); + public static final Set sessionBlacklistedDoors = ConcurrentHashMap.newKeySet(); static boolean hasQuestLockKeywords(String text) { if (text == null || text.isEmpty()) return false; From 8fe12ca94a989a33b7a6b316f2b9e799fe2f0c5b Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Mon, 27 Apr 2026 14:47:41 -0400 Subject: [PATCH 05/10] fix(walker): skip scene-discovered transports in handleTransports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit handleTransports() was silently opening Patch H virtual door transports on tiles whose two endpoints both landed somewhere on the path — common in Varrock palace where the path winds past doors connecting rooms it visits. The destIdx >= originIdx ordering check was the only filter, so non-consecutive door pairs would fire and click off-edge. No log line on the open, so it was invisible to debugging. Tag every Patch H virtual door with sceneDiscovered=true at injection time. handleTransports skips tagged transports — handleDoors owns the click, and its WallObject orientation check against path[i]→path[i+1] guarantees only the wall blocking the current step is opened. Add an INFO log right before handleObject() so any future stray opens from this code path are visible in the log. --- .../plugins/microbot/shortestpath/Transport.java | 13 +++++++++++++ .../shortestpath/pathfinder/PathfinderConfig.java | 3 +++ .../plugins/microbot/util/walker/Rs2Walker.java | 11 +++++++++++ 3 files changed, 27 insertions(+) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/Transport.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/Transport.java index 6c454db897..fc42523702 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/Transport.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/Transport.java @@ -119,6 +119,19 @@ public class Transport { @Getter private boolean isMembers = false; + /** + * True for runtime virtual edges injected by PathfinderConfig.discoverSceneDoors + * (Patch H). These represent scene WallObjects so BFS can route through doors + * not in transports.tsv. handleTransports must skip them — handleDoors opens + * the actual wall via orientation-checked WallObject scan, which is the only + * path-edge-aware way to interact with these doors. Letting handleTransports + * fire on a scene-discovered transport opens any door whose two tiles both + * appear on the path even when the path doesn't actually traverse it. + */ + @Getter + @Setter + private boolean sceneDiscovered = false; + /** diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java index 4a4bc36ac4..56c8a53b4d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java @@ -745,6 +745,9 @@ private int addRuntimeDoorEdge(WorldPoint origin, WorldPoint destination, } Transport door = new Transport(origin, destination, displayInfo, TransportType.TRANSPORT, false, action, name, objectId); + // Tag for handleTransports to skip — handleDoors opens these via the + // WallObject orientation check, which is the only edge-aware mechanism. + door.setSceneDiscovered(true); // Per refreshTransports invariant, transports and transportsPacked share // the same Set reference per origin — add once via the shared reference. Set set = transports.computeIfAbsent(origin, k -> new HashSet<>()); 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 151242755e..6b65fe722d 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 @@ -2314,6 +2314,17 @@ private static boolean handleTransports(List path, int indexOfStartP } for (Transport transport : transports) { + // Patch H virtual door edges exist purely so BFS can route through scene + // doors not in transports.tsv. The actual click is owned by handleDoors, + // which probes wall orientation against the consecutive fromWp↔toWp edge + // and only opens doors that block that specific step. handleTransports + // here would open any door whose origin matches path[i] and whose + // destination is anywhere later on the path — so a scene wall whose two + // tiles both happen to land on the path (common in Varrock palace where + // the path winds past multiple doors) gets opened off-edge. + if (transport.isSceneDiscovered()) { + continue; + } Collection worldPointCollections; //in some cases the getOrigin is null, for teleports that start the player location if (transport.getOrigin() == null) { From 2bcca19ec7fb55674806637aaa85ea136c1e6d94 Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Mon, 27 Apr 2026 14:48:54 -0400 Subject: [PATCH 06/10] chore(pathfinder): null-guard refresh tab switch + AGILITY_SHORTCUT F2P diagnostic Two small changes folded together since they live in the same area of PathfinderConfig: 1. refresh() can fire on a tick before LocalPlayer is hydrated post-login. getWorldLocation() returned null and NPE'd the client during startup. Skip the tab switch entirely when player location is null. 2. One-shot diagnostic logs the world-type set + agility-shortcut filter decision the first time isFeatureEnabled() sees an AGILITY_SHORTCUT after each refresh(). Used to investigate whether the F2P gate is actually filtering trellises etc. Reset on every refresh() so each cache rebuild logs once. --- .../pathfinder/PathfinderConfig.java | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java index 56c8a53b4d..ea9a63abc0 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java @@ -184,6 +184,7 @@ public CollisionMap getMap() { } public void refresh(WorldPoint target) { + diagAgilityShortcutLogged.set(false); calculationCutoffMillis = (long) config.calculationCutoff() * Constants.GAME_TICK_LENGTH; avoidWilderness = ShortestPathPlugin.override("avoidWilderness", config.avoidWilderness()); useAgilityShortcuts = ShortestPathPlugin.override("useAgilityShortcuts", config.useAgilityShortcuts()); @@ -227,8 +228,11 @@ public void refresh(WorldPoint target) { refreshRestrictionData(); long t2 = System.currentTimeMillis(); - // Do not switch back to inventory tab if we are inside of the telekinetic room in Mage Training Arena - if (Rs2Player.getWorldLocation().getRegionID() != 13463) { + // Do not switch back to inventory tab if we are inside of the telekinetic room in Mage Training Arena. + // Skip the tab switch entirely when LocalPlayer isn't hydrated yet (refresh can fire on a tick before + // the player object is available post-login, NPE'd the client during startup). + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + if (playerLoc != null && playerLoc.getRegionID() != 13463) { Rs2Tab.switchTo(InterfaceTab.INVENTORY); } long t3 = System.currentTimeMillis(); @@ -957,10 +961,23 @@ private boolean isSpiritTreeDestinationEnabled(Transport transport) { return true; } + // Diagnostic one-shot — logs the world type set + agility shortcut decision the + // first time isFeatureEnabled sees an AGILITY_SHORTCUT after each refresh(). + // Used to investigate whether the F2P gate is actually filtering trellises etc. + // Reset to false at the top of refresh() so each cache rebuild logs once. + private final java.util.concurrent.atomic.AtomicBoolean diagAgilityShortcutLogged = + new java.util.concurrent.atomic.AtomicBoolean(false); + private boolean isFeatureEnabled(Transport transport) { TransportType type = transport.getType(); - if (!client.getWorldType().contains(WorldType.MEMBERS)) { + boolean membersWorld = client.getWorldType().contains(WorldType.MEMBERS); + if (type == TransportType.AGILITY_SHORTCUT && diagAgilityShortcutLogged.compareAndSet(false, true)) { + log.warn("[F2P diag] AGILITY_SHORTCUT useTransport probe: worldTypes={} membersFlag={} useAgilityShortcuts={} transport.isMembers={} origin={} dest={}", + client.getWorldType(), membersWorld, useAgilityShortcuts, transport.isMembers(), transport.getOrigin(), transport.getDestination()); + } + + if (!membersWorld) { // Transport types that require membership switch (type) { case AGILITY_SHORTCUT: From 3940198fce4b080e6a5c70e97b44c004acf0c866 Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Mon, 27 Apr 2026 14:50:04 -0400 Subject: [PATCH 07/10] fix(walker): pre-scan path for closed doors before skip-clicking, drop wrong diagonal probes Two related fixes to door handling at click time. (1) Pre-scan: the walker speeds up by clicking far-away minimap targets and advancing the path index past tiles it visually walked over. If a closed door sat between the current tile and the click target, the OSRS server routed the player into the door, and handleDoors() never got called for the skipped indices. Before each skip-click, scan ascending from i for the nearest path tile within scene range that has a closed door; if found, open it and re-enter processWalk so the player's new position is past the door. Bounded by HANDLER_RANGE (not targetIdx) so it still works for interpolated click targets where the indexed range collapses to a single tile. (2) Diagonal corner probes removed: handleDoors() was probing the two shoulder tiles when the path step was diagonal. A wall at the corner facing one of the path tiles passes the orientation check but blocks a different edge entirely, so the OSRS server picks a shoulder for diagonals and opening the wrong shoulder's door is wasted action. Restrict probes to the path tiles themselves (fromWp and toWp via the existing offset loop). Also adds [Walker click] target/result INFO logs so the click sequence is traceable in the log. --- .../microbot/util/walker/Rs2Walker.java | 58 ++++++++++++++++--- 1 file changed, 51 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 6b65fe722d..cc1ac1675b 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 @@ -572,6 +572,43 @@ && handleNearbyRawPathSceneObjects(rawPath, HANDLER_RANGE)) { wp -> inInstance || isKnownWalkableOrUnloaded(wp)); } + // Pre-scan the path segment we're about to skip past for any closed + // door. handleDoors() per-iteration only inspects the current tile + // boundary, but `i = targetIdx` jumps ahead after the click, so any + // door between i and targetIdx never gets opened — the server then + // strands the player against the closed door that BFS treated as a + // normal walking edge. Open it here before the click goes through. + // Walk j ascending — path tiles are in walking order, so the first + // door we find is the nearest closed door between the player and + // wherever the OSRS server will route them on this click. handleDoors() + // opens it and returns; we break and let processWalk recurse so the + // next iteration starts from the player's new position past the door + // (and catches any further-out doors on its own next pre-scan). + // + // Bounded by HANDLER_RANGE distance, NOT by targetIdx — when the + // walker uses an interpolated click target (the targetIdx < i path + // above sets targetIdx = i - 1), the indexed range is empty but the + // actual click destination is far, so the relevant doors live along + // path[i..end] within Euclidean reach. Iterate until a door is found + // or every tile in scene-range has been checked. + boolean preDoorHandled = false; + int preDoorAt = -1; + for (int j = i; j < path.size(); j++) { + int distFromPlayer = path.get(j).distanceTo2D(Rs2Player.getWorldLocation()); + if (distFromPlayer > HANDLER_RANGE) continue; // out of scene; handle when closer + if (handleDoors(path, j)) { + preDoorHandled = true; + preDoorAt = j; + break; + } + } + if (preDoorHandled) { + log.info("[Walker pre-scan] opened door at path idx {} ({}) before clicking past — re-evaluating", + preDoorAt, path.get(preDoorAt)); + exitReason = "door-prescan"; + break; + } + WorldPoint posBefore = playerLoc; WorldPoint clickTarget = inInstance ? targetWp : getPointWithWallDistance(targetWp); boolean clicked = Rs2Walker.walkMiniMap(clickTarget); @@ -581,6 +618,8 @@ && handleNearbyRawPathSceneObjects(rawPath, HANDLER_RANGE)) { if (isWalkCancelled(target)) { return WalkerState.EXIT; } + log.info("[Walker click] target={} pathIdx={} playerBefore={} clicked={}", + targetWp, targetIdx, posBefore, clicked); if (clicked) { final WorldPoint b = targetWp; final WorldPoint before = posBefore; @@ -603,6 +642,12 @@ && handleNearbyRawPathSceneObjects(rawPath, HANDLER_RANGE)) { if (isWalkCancelled(target)) { return WalkerState.EXIT; } + WorldPoint posAfter = Rs2Player.getWorldLocation(); + int moved = posAfter == null ? -1 : posBefore.distanceTo2D(posAfter); + int distLeft = posAfter == null ? -1 : b.distanceTo2D(posAfter); + long elapsedMs = System.currentTimeMillis() - clickedAt; + log.info("[Walker click] result moved={} tiles posAfter={} distFromTarget={} elapsedMs={} reachedProximity={}", + moved, posAfter, distLeft, elapsedMs, distLeft >= 0 && distLeft <= proximityWake); } // Keep stuck-detection honest: observed movement resets the movement timer. // Without this, isStuckTooLong() fires after long successful walks because @@ -1784,9 +1829,6 @@ private static boolean handleDoors(List path, int index, boolean all return false; } - boolean diagonal = Math.abs(fromWp.getX() - toWp.getX()) > 0 - && Math.abs(fromWp.getY() - toWp.getY()) > 0; - for (int offset = 0; offset <= 1; offset++) { int doorIdx = index + offset; if (doorIdx >= path.size()) continue; @@ -1796,12 +1838,14 @@ private static boolean handleDoors(List path, int index, boolean all ? Rs2WorldPoint.convertInstancedWorldPoint(rawDoorWp) : rawDoorWp; + // Only probe the path tiles themselves (fromWp and toWp via the offset + // loop). Diagonal corner probes were opening doors on shoulder tiles + // that don't actually block the fromWp↔toWp edge — a wall at the + // corner facing one of the path tiles passes the orientation check but + // is on a different edge entirely. The OSRS server picks a shoulder + // for diagonals; opening the wrong shoulder's door is wasted action. List probes = new ArrayList<>(); probes.add(doorWp); - if (diagonal) { - probes.add(new WorldPoint(toWp.getX(), fromWp.getY(), doorWp.getPlane())); - probes.add(new WorldPoint(fromWp.getX(), toWp.getY(), doorWp.getPlane())); - } for (WorldPoint probe : probes) { boolean adjacentToPath = probe.distanceTo(fromWp) <= 1 || probe.distanceTo(toWp) <= 1; From ad0b47e69b3ebd73e604f1d361eeef2189da428d Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Mon, 27 Apr 2026 18:36:44 -0400 Subject: [PATCH 08/10] chore(walker): drop investigation diagnostics, demote per-click logs to DEBUG - Remove Pathfinder.java post-run diagnostic dump (per-transport hop log + raw path tile dump). Used during the off-path-door investigation; no longer load-bearing. - Remove [F2P diag] AGILITY_SHORTCUT one-shot in PathfinderConfig and the diagAgilityShortcutLogged AtomicBoolean that backed it. - Demote [Walker click] target/result logs and [Walker] handleTransports interacting log to DEBUG. Useful when actively debugging walker decisions, but noisy at INFO during normal walks. Kept at INFO: [refreshTransports] scene doors added (one line per refresh) and [Walker pre-scan] door-opened (rare event signaling an active behavior worth tracing). --- .../shortestpath/pathfinder/Pathfinder.java | 39 ------------------- .../pathfinder/PathfinderConfig.java | 16 +------- .../microbot/util/walker/Rs2Walker.java | 4 +- 3 files changed, 3 insertions(+), 56 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java index 25c8e6f238..8933b70ece 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java @@ -325,45 +325,6 @@ public void run() { log.info("[Pathfinder] run() completed. done={}, cancelled={}, stats={}", done, cancelled, getStats() != null ? getStats().toString() : "null"); - - // Diagnostic: walk the bestLastNode chain and log every transport hop in - // the final path. Lets us prove which transports BFS actually selected - // (e.g., distinguishes a palace door from an agility-shortcut trellis at - // adjacent tiles). Cheap — only walks the linked list once per run. - if (bestLastNode != null) { - int hops = 0; - Node cur = bestLastNode; - while (cur != null && cur.previous != null) { - if (cur instanceof TransportNode) { - log.info("[Pathfinder] transport hop in path: {} -> {} cost={}", - WorldPointUtil.toString(cur.previous.packedPosition), - WorldPointUtil.toString(cur.packedPosition), - cur.cost); - hops++; - } - cur = cur.previous; - } - if (hops == 0) { - log.info("[Pathfinder] transport hops in path: 0 (walking-only)"); - } - - // Dump the full raw path (pre-smoother) for diagnosis. Tells us the - // actual tile sequence BFS picked, so we can spot illegal wall steps - // (e.g., (3228,3470) -> (3228,3471) without a transport — which would - // be the trellis fence walked through as if it weren't there). - java.util.List rawPath = bestLastNode.getPath(); - int sz = rawPath.size(); - log.info("[Pathfinder] raw path: {} tiles", sz); - if (sz > 0) { - StringBuilder sb = new StringBuilder("[Pathfinder] raw path tiles: "); - for (int i = 0; i < sz; i++) { - WorldPoint p = rawPath.get(i); - sb.append("(").append(p.getX()).append(",").append(p.getY()).append(",").append(p.getPlane()).append(")"); - if (i < sz - 1) sb.append(" "); - } - log.info(sb.toString()); - } - } } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java index ea9a63abc0..cf2246a001 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java @@ -184,7 +184,6 @@ public CollisionMap getMap() { } public void refresh(WorldPoint target) { - diagAgilityShortcutLogged.set(false); calculationCutoffMillis = (long) config.calculationCutoff() * Constants.GAME_TICK_LENGTH; avoidWilderness = ShortestPathPlugin.override("avoidWilderness", config.avoidWilderness()); useAgilityShortcuts = ShortestPathPlugin.override("useAgilityShortcuts", config.useAgilityShortcuts()); @@ -961,23 +960,10 @@ private boolean isSpiritTreeDestinationEnabled(Transport transport) { return true; } - // Diagnostic one-shot — logs the world type set + agility shortcut decision the - // first time isFeatureEnabled sees an AGILITY_SHORTCUT after each refresh(). - // Used to investigate whether the F2P gate is actually filtering trellises etc. - // Reset to false at the top of refresh() so each cache rebuild logs once. - private final java.util.concurrent.atomic.AtomicBoolean diagAgilityShortcutLogged = - new java.util.concurrent.atomic.AtomicBoolean(false); - private boolean isFeatureEnabled(Transport transport) { TransportType type = transport.getType(); - boolean membersWorld = client.getWorldType().contains(WorldType.MEMBERS); - if (type == TransportType.AGILITY_SHORTCUT && diagAgilityShortcutLogged.compareAndSet(false, true)) { - log.warn("[F2P diag] AGILITY_SHORTCUT useTransport probe: worldTypes={} membersFlag={} useAgilityShortcuts={} transport.isMembers={} origin={} dest={}", - client.getWorldType(), membersWorld, useAgilityShortcuts, transport.isMembers(), transport.getOrigin(), transport.getDestination()); - } - - if (!membersWorld) { + if (!client.getWorldType().contains(WorldType.MEMBERS)) { // Transport types that require membership switch (type) { case AGILITY_SHORTCUT: 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 cc1ac1675b..ab13fedbd1 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 @@ -618,7 +618,7 @@ && handleNearbyRawPathSceneObjects(rawPath, HANDLER_RANGE)) { if (isWalkCancelled(target)) { return WalkerState.EXIT; } - log.info("[Walker click] target={} pathIdx={} playerBefore={} clicked={}", + log.debug("[Walker click] target={} pathIdx={} playerBefore={} clicked={}", targetWp, targetIdx, posBefore, clicked); if (clicked) { final WorldPoint b = targetWp; @@ -646,7 +646,7 @@ && handleNearbyRawPathSceneObjects(rawPath, HANDLER_RANGE)) { int moved = posAfter == null ? -1 : posBefore.distanceTo2D(posAfter); int distLeft = posAfter == null ? -1 : b.distanceTo2D(posAfter); long elapsedMs = System.currentTimeMillis() - clickedAt; - log.info("[Walker click] result moved={} tiles posAfter={} distFromTarget={} elapsedMs={} reachedProximity={}", + log.debug("[Walker click] result moved={} tiles posAfter={} distFromTarget={} elapsedMs={} reachedProximity={}", moved, posAfter, distLeft, elapsedMs, distLeft >= 0 && distLeft <= proximityWake); } // Keep stuck-detection honest: observed movement resets the movement timer. From de8ec9cc2b99ff261483c6bfa85fb801be1fa119 Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Mon, 27 Apr 2026 19:44:49 -0400 Subject: [PATCH 09/10] fix(walker): tighten searchExhausted gate and arrival threshold off-by-ones Pathfinder: gate searchExhausted on !reachedTarget && !cutoffHit so that polling the last queued node and exiting via target-found or cutoff isn't misread as queue exhaustion; the cutoff variant could otherwise trip a spurious UNREACHABLE downstream. Rs2Walker: arrival check at the end of processWalk used finalDist < distance while the pre-walk early-arrival check at line 225 uses <=. Endpoints exactly at the proximity threshold dropped into the no-progress retry loop instead of returning ARRIVED. Switch to <= for consistency. --- .../shortestpath/pathfinder/Pathfinder.java | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java index 8933b70ece..a68a237975 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java @@ -245,6 +245,8 @@ public void run() { long cutoffDurationMillis = config.getCalculationCutoffMillis(); long cutoffTimeMillis = System.currentTimeMillis() + cutoffDurationMillis; config.refreshTeleports(start, 31); + boolean reachedTarget = false; + boolean cutoffHit = false; while (!cancelled && (!boundary.isEmpty() || !pending.isEmpty())) { Node b = boundary.peek(); Node p = pending.peek(); @@ -294,9 +296,13 @@ public void run() { cutoffTimeMillis = System.currentTimeMillis() + cutoffDurationMillis; } } - if (reached) break; + if (reached) { + reachedTarget = true; + break; + } if (System.currentTimeMillis() > cutoffTimeMillis) { + cutoffHit = true; log.info("[Pathfinder] Cutoff reached. bestDistance={}, nodesChecked={}", bestDistance, stats.getNodesChecked()); break; } @@ -304,11 +310,14 @@ public void run() { addNeighbors(node); } - // Capture exhaustion before finally clears the queues. Both empty - // after the while loop means BFS terminated by the loop condition - // (no more nodes to expand) — i.e. dest is unreachable. Cutoff and - // target-reached exits leave queues non-empty. - searchExhausted = !cancelled && boundary.isEmpty() && pending.isEmpty(); + // Capture exhaustion before finally clears the queues. Gate on + // !reachedTarget && !cutoffHit so polling the last queued node and + // then exiting via target-found or cutoff isn't misread as the BFS + // running out of frontier — those leave the queues empty for a + // benign reason and would otherwise trip a spurious UNREACHABLE + // downstream. + searchExhausted = !cancelled && !reachedTarget && !cutoffHit + && boundary.isEmpty() && pending.isEmpty(); log.info("[Pathfinder] Loop exited. cancelled={}, boundaryEmpty={}, pendingEmpty={}, bestLastNode={}", cancelled, boundary.isEmpty(), pending.isEmpty(), bestLastNode == null ? "null" : WorldPointUtil.toString(bestLastNode.packedPosition)); From 6748852f82e8f4dc1ea94dcaeacaa024a315131e Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Sat, 9 May 2026 13:05:13 -0400 Subject: [PATCH 10/10] test(threadsafety): refresh client-thread guardrail baseline post-rebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regenerated after rebasing onto upstream/development. The earlier baseline refresh (c81e241495 on the pre-rebase branch) was skipped during the rebase because both upstream's 98e1cfe16f and our refresh produced different snapshots — lambda indices and inferred-list violations depend on the combined bytecode, which only exists post-rebase. 57 new entries + 29 obsolete entries removed; reflects upstream's walker refactor (f9e1b9de04) layered with our walker fixes. Equivalent to: ./gradlew :client:runUnitTests --tests "*ClientThreadGuardrailTest*" \ -Dmicrobot.guardrail.regenerate-baseline=true --- .../client-thread-guardrail-baseline.txt | 86 ++++++++++++------- 1 file changed, 57 insertions(+), 29 deletions(-) diff --git a/runelite-client/src/test/resources/threadsafety/client-thread-guardrail-baseline.txt b/runelite-client/src/test/resources/threadsafety/client-thread-guardrail-baseline.txt index 6a932817bc..73c003b300 100644 --- a/runelite-client/src/test/resources/threadsafety/client-thread-guardrail-baseline.txt +++ b/runelite-client/src/test/resources/threadsafety/client-thread-guardrail-baseline.txt @@ -67,12 +67,14 @@ net.runelite.client.plugins.microbot.api.tileobject.Rs2TileObjectCache#getStream net.runelite.client.plugins.microbot.api.tileobject.Rs2TileObjectCache#getStream(): Stream -> net.runelite.api.Client#getWorldView(int): WorldView net.runelite.client.plugins.microbot.api.tileobject.Rs2TileObjectCache#getStream(): Stream -> net.runelite.api.Scene#getTiles(): Tile[][][] net.runelite.client.plugins.microbot.api.tileobject.Rs2TileObjectCache#getStream(): Stream -> net.runelite.api.Tile#getGameObjects(): GameObject[] +net.runelite.client.plugins.microbot.api.tileobject.Rs2TileObjectCache#getStream(): Stream -> net.runelite.api.Tile#getWallObject(): WallObject net.runelite.client.plugins.microbot.api.tileobject.Rs2TileObjectCache#getStream(): Stream -> net.runelite.api.WorldView#getId(): int net.runelite.client.plugins.microbot.api.tileobject.Rs2TileObjectCache#getStream(): Stream -> net.runelite.api.WorldView#getPlane(): int net.runelite.client.plugins.microbot.api.tileobject.Rs2TileObjectCache#getStream(): Stream -> net.runelite.api.WorldView#getScene(): Scene net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel#click(String): boolean -> net.runelite.api.Client#isWidgetSelected(): boolean net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel#click(String): boolean -> net.runelite.api.GameObject#sizeX(): int net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel#click(String): boolean -> net.runelite.api.GameObject#sizeY(): int +net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel#click(String): boolean -> net.runelite.api.ObjectComposition#getActions(): String[] net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel#click(String): boolean -> net.runelite.api.ObjectComposition#getImpostor(): ObjectComposition net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel#click(String): boolean -> net.runelite.api.ObjectComposition#getImpostorIds(): int[] net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel#click(String): boolean -> net.runelite.api.ObjectComposition#getName(): String @@ -181,6 +183,7 @@ net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#clickObject(T net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#clickObject(TileObject, String): boolean -> net.runelite.api.Client#isWidgetSelected(): boolean net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#clickObject(TileObject, String): boolean -> net.runelite.api.GameObject#sizeX(): int net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#clickObject(TileObject, String): boolean -> net.runelite.api.GameObject#sizeY(): int +net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#clickObject(TileObject, String): boolean -> net.runelite.api.ObjectComposition#getActions(): String[] net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#clickObject(TileObject, String): boolean -> net.runelite.api.ObjectComposition#getImpostor(): ObjectComposition net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#clickObject(TileObject, String): boolean -> net.runelite.api.ObjectComposition#getImpostorIds(): int[] net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#clickObject(TileObject, String): boolean -> net.runelite.api.ObjectComposition#getName(): String @@ -255,6 +258,7 @@ net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#getWallObject net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#getWorldArea(GameObject): WorldArea -> net.runelite.api.GameObject#sizeX(): int net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#getWorldArea(GameObject): WorldArea -> net.runelite.api.GameObject#sizeY(): int net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#getWorldArea(GameObject): WorldArea -> net.runelite.api.coords.WorldPoint#fromLocal(Client, LocalPoint): WorldPoint +net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#hasAction(ObjectComposition, String, boolean): boolean -> net.runelite.api.ObjectComposition#getActions(): String[] net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#hasLineOfSight(WorldPoint, TileObject): boolean -> net.runelite.api.Client#getTopLevelWorldView(): WorldView net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#hasLineOfSight(WorldPoint, TileObject): boolean -> net.runelite.api.GameObject#sizeX(): int net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#hasLineOfSight(WorldPoint, TileObject): boolean -> net.runelite.api.GameObject#sizeY(): int @@ -266,6 +270,7 @@ net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#lambda$findBa net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#lambda$findDepositBox$20(GameObject): boolean -> net.runelite.api.GameObject#getId(): int net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#lambda$findGameObjectByLocation$7(WorldPoint, GameObject): boolean -> net.runelite.api.GameObject#getWorldLocation(): WorldPoint net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#lambda$findGrandExchangeBooth$21(WallObject): boolean -> net.runelite.api.WallObject#getId(): int +net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#lambda$findGrandExchangeBooth$21(WallObject): boolean -> net.runelite.api.WallObject#getWorldLocation(): WorldPoint net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#lambda$findObject$10(int, WorldPoint, GameObject): boolean -> net.runelite.api.GameObject#getId(): int net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#lambda$findObject$10(int, WorldPoint, GameObject): boolean -> net.runelite.api.GameObject#getWorldLocation(): WorldPoint net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#lambda$findObject$37(Set, GameObject): boolean -> net.runelite.api.GameObject#getId(): int @@ -294,11 +299,15 @@ net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#lambda$getWal net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#lambda$getWallObject$60(Set, WallObject): boolean -> net.runelite.api.WallObject#getId(): int net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#lambda$nameMatches$85(Set, boolean, String, String, TileObject): boolean -> net.runelite.api.TileObject#getId(): int net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#lambda$static$0(Tile): Collection -> net.runelite.api.Tile#getGameObjects(): GameObject[] +net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#lambda$static$3(Tile): Collection -> net.runelite.api.Tile#getWallObject(): WallObject +net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#lambda$static$4(Tile): Collection -> net.runelite.api.Tile#getWallObject(): WallObject net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#localPointFromWorldSafe(WorldPoint): LocalPoint -> net.runelite.api.coords.LocalPoint#fromWorld(WorldView, WorldPoint): LocalPoint net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel#(TileObject, Tile): void -> net.runelite.api.Client#getTickCount(): int net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel#(TileObject, Tile): void -> net.runelite.api.GameObject#sizeX(): int net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel#(TileObject, Tile): void -> net.runelite.api.GameObject#sizeY(): int +net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel#blocksLineOfSight(): boolean -> net.runelite.api.ObjectComposition#getActions(): String[] net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel#blocksLineOfSight(): boolean -> net.runelite.api.ObjectComposition#getName(): String +net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel#getActions(): String[] -> net.runelite.api.ObjectComposition#getActions(): String[] net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel#getCanonicalLocation(): WorldPoint -> net.runelite.api.Tile#getWorldLocation(): WorldPoint net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel#getCanonicalLocation(): WorldPoint -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel#getId(): int -> net.runelite.api.TileObject#getId(): int @@ -681,6 +690,7 @@ net.runelite.client.plugins.microbot.util.tile.Rs2Tile#isVisited(WorldPoint, boo net.runelite.client.plugins.microbot.util.tile.Rs2Tile#isWalkable(WorldPoint): boolean -> net.runelite.api.Client#getTopLevelWorldView(): WorldView net.runelite.client.plugins.microbot.util.tile.Rs2Tile#isWalkable(WorldPoint): boolean -> net.runelite.api.coords.LocalPoint#fromWorld(WorldView, WorldPoint): LocalPoint net.runelite.client.plugins.microbot.util.tile.Rs2Tile#lambda$isBankBooth$20(WorldPoint, GameObject): boolean -> net.runelite.api.GameObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.tile.Rs2Tile#lambda$tileHasWalls$19(WorldPoint, WallObject): boolean -> net.runelite.api.WallObject#getWorldLocation(): WorldPoint net.runelite.client.plugins.microbot.util.tile.Rs2Tile#pathTo(Tile, Tile): List -> net.runelite.api.Client#getScene(): Scene net.runelite.client.plugins.microbot.util.tile.Rs2Tile#pathTo(Tile, Tile): List -> net.runelite.api.Client#getTopLevelWorldView(): WorldView net.runelite.client.plugins.microbot.util.tile.Rs2Tile#pathTo(Tile, Tile): List -> net.runelite.api.CollisionData#getFlags(): int[][] @@ -690,6 +700,7 @@ net.runelite.client.plugins.microbot.util.tileobject.Rs2TileObjectModel#click(St net.runelite.client.plugins.microbot.util.tileobject.Rs2TileObjectModel#click(String): boolean -> net.runelite.api.Client#isWidgetSelected(): boolean net.runelite.client.plugins.microbot.util.tileobject.Rs2TileObjectModel#click(String): boolean -> net.runelite.api.GameObject#sizeX(): int net.runelite.client.plugins.microbot.util.tileobject.Rs2TileObjectModel#click(String): boolean -> net.runelite.api.GameObject#sizeY(): int +net.runelite.client.plugins.microbot.util.tileobject.Rs2TileObjectModel#click(String): boolean -> net.runelite.api.ObjectComposition#getActions(): String[] net.runelite.client.plugins.microbot.util.tileobject.Rs2TileObjectModel#click(String): boolean -> net.runelite.api.ObjectComposition#getImpostor(): ObjectComposition net.runelite.client.plugins.microbot.util.tileobject.Rs2TileObjectModel#click(String): boolean -> net.runelite.api.ObjectComposition#getImpostorIds(): int[] net.runelite.client.plugins.microbot.util.tileobject.Rs2TileObjectModel#click(String): boolean -> net.runelite.api.ObjectComposition#getName(): String @@ -710,6 +721,7 @@ net.runelite.client.plugins.microbot.util.walker.Rs2Walker#collectMoaChildren(Wi net.runelite.client.plugins.microbot.util.walker.Rs2Walker#collectMoaChildren(Widget): List -> net.runelite.api.widgets.Widget#getStaticChildren(): Widget[] net.runelite.client.plugins.microbot.util.walker.Rs2Walker#distanceToRegion(int, int): int -> net.runelite.api.Client#getTopLevelWorldView(): WorldView net.runelite.client.plugins.microbot.util.walker.Rs2Walker#distanceToRegion(int, int): int -> net.runelite.api.WorldView#getPlane(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#getDoorAction(ObjectComposition, List): String -> net.runelite.api.ObjectComposition#getActions(): String[] net.runelite.client.plugins.microbot.util.walker.Rs2Walker#getPointWithWallDistance(WorldPoint): WorldPoint -> net.runelite.api.Client#getTopLevelWorldView(): WorldView net.runelite.client.plugins.microbot.util.walker.Rs2Walker#getPointWithWallDistance(WorldPoint): WorldPoint -> net.runelite.api.CollisionData#getFlags(): int[][] net.runelite.client.plugins.microbot.util.walker.Rs2Walker#getPointWithWallDistance(WorldPoint): WorldPoint -> net.runelite.api.WorldView#getCollisionMaps(): CollisionData[] @@ -720,13 +732,14 @@ net.runelite.client.plugins.microbot.util.walker.Rs2Walker#getTile(WorldPoint): net.runelite.client.plugins.microbot.util.walker.Rs2Walker#getTile(WorldPoint): Tile -> net.runelite.api.WorldView#getScene(): Scene net.runelite.client.plugins.microbot.util.walker.Rs2Walker#getTile(WorldPoint): Tile -> net.runelite.api.coords.LocalPoint#fromWorld(WorldView, WorldPoint): LocalPoint net.runelite.client.plugins.microbot.util.walker.Rs2Walker#getTransportsForPath(List, int, TransportType, boolean): List -> net.runelite.api.Client#getTopLevelWorldView(): WorldView +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleCanoe(Transport): boolean -> net.runelite.api.ObjectComposition#getActions(): String[] net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleCharterShip(Transport): boolean -> net.runelite.api.widgets.Widget#getDynamicChildren(): Widget[] net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleCharterShip(Transport): boolean -> net.runelite.api.widgets.Widget#getId(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleCharterShip(Transport): boolean -> net.runelite.api.widgets.Widget#getIndex(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleDoors(List, int): boolean -> net.runelite.api.Client#getTopLevelWorldView(): WorldView -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleDoors(List, int): boolean -> net.runelite.api.Scene#isInstance(): boolean -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleDoors(List, int): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleDoors(List, int): boolean -> net.runelite.api.WorldView#getScene(): Scene +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleDoors(List, int, boolean): boolean -> net.runelite.api.Client#getTopLevelWorldView(): WorldView +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleDoors(List, int, boolean): boolean -> net.runelite.api.Scene#isInstance(): boolean +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleDoors(List, int, boolean): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleDoors(List, int, boolean): boolean -> net.runelite.api.WorldView#getScene(): Scene net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleFairyRing(Transport): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleMasterScrollBook(String): boolean -> net.runelite.api.widgets.Widget#getStaticChildren(): Widget[] net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleMinigameTeleport(Transport): boolean -> net.runelite.api.widgets.Widget#getDynamicChildren(): Widget[] @@ -744,42 +757,54 @@ net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleRockfall(List, net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleRockfall(List, int): boolean -> net.runelite.api.TileObject#getId(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleSpiritTree(Transport): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleTransports(List, int): boolean -> net.runelite.api.Client#getTopLevelWorldView(): WorldView +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleTransports(List, int): boolean -> net.runelite.api.ObjectComposition#getActions(): String[] net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleTransports(List, int): boolean -> net.runelite.api.ObjectComposition#getName(): String net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleTransports(List, int): boolean -> net.runelite.api.Scene#isInstance(): boolean net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleTransports(List, int): boolean -> net.runelite.api.TileObject#getId(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleTransports(List, int): boolean -> net.runelite.api.WorldView#getScene(): Scene net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isCloseToRegion(int, int, int): boolean -> net.runelite.api.Client#getTopLevelWorldView(): WorldView net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isCloseToRegion(int, int, int): boolean -> net.runelite.api.WorldView#getPlane(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isDoorComposition(ObjectComposition, List): boolean -> net.runelite.api.ObjectComposition#getActions(): String[] net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isDoorComposition(ObjectComposition, List): boolean -> net.runelite.api.ObjectComposition#getImpostorIds(): int[] net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isDoorComposition(ObjectComposition, List): boolean -> net.runelite.api.ObjectComposition#getName(): String net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isDoorOnSegment(TileObject, WorldPoint, WorldPoint): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$findDoorNearSegment$15(WorldPoint, List, WorldPoint, WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$findDoorNearSegment$16(WorldPoint, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleCharterShip$121(Widget): boolean -> net.runelite.api.widgets.Widget#getActions(): String[] -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleDoors$14(WorldPoint, GameObject): boolean -> net.runelite.api.GameObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleFairyRing$127(Transport, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleMinigameTeleport$100(Widget, Object[]): boolean -> net.runelite.api.widgets.Widget#getOnOpListener(): Object[] -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleMinigameTeleport$102(String): boolean -> net.runelite.api.widgets.Widget#getText(): String -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$72(int, TileObject): boolean -> net.runelite.api.TileObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$78(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$78(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$79(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$79(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleRockfall$10(WorldPoint, Tile): boolean -> net.runelite.api.Tile#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$50(int, TileObject): boolean -> net.runelite.api.TileObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$51(Transport, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$53(TileObject): boolean -> net.runelite.api.ObjectComposition#getName(): String -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$54(Transport, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$55(TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$56(Transport, Object): Integer -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$57(Transport, Object): Integer -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$61(int, String, TileObject): boolean -> net.runelite.api.TileObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$62(Transport, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleWildernessObelisk$86(Transport, GameObject): boolean -> net.runelite.api.GameObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleWildernessObelisk$87(Transport, GameObject): boolean -> net.runelite.api.GameObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isKnownWalkableOrUnloaded(WorldPoint): boolean -> net.runelite.api.Client#getTopLevelWorldView(): WorldView +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isKnownWalkableOrUnloaded(WorldPoint): boolean -> net.runelite.api.coords.LocalPoint#fromWorld(WorldView, WorldPoint): LocalPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#kickStartShortLocalWalk(WorldPoint, int): void -> net.runelite.api.Client#getTopLevelWorldView(): WorldView +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#kickStartShortLocalWalk(WorldPoint, int): void -> net.runelite.api.coords.LocalPoint#fromWorld(WorldView, WorldPoint): LocalPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$findDoorNearSegment$20(WorldPoint, WorldPoint, WorldPoint, List, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$findDoorNearSegment$21(WorldPoint, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleCanoe$111(Transport, String): boolean -> net.runelite.api.ObjectComposition#getActions(): String[] +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleCanoe$114(Transport, String): boolean -> net.runelite.api.ObjectComposition#getActions(): String[] +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleCanoe$116(Transport, String): boolean -> net.runelite.api.ObjectComposition#getActions(): String[] +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleCharterShip$126(Widget): boolean -> net.runelite.api.widgets.Widget#getActions(): String[] +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleDoors$18(WorldPoint, WallObject): boolean -> net.runelite.api.WallObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleDoors$19(WorldPoint, GameObject): boolean -> net.runelite.api.GameObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleFairyRing$132(Transport, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleMinigameTeleport$105(Widget, Object[]): boolean -> net.runelite.api.widgets.Widget#getOnOpListener(): Object[] +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleMinigameTeleport$107(String): boolean -> net.runelite.api.widgets.Widget#getText(): String +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$77(int, TileObject): boolean -> net.runelite.api.TileObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$83(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$83(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$84(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$84(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleRockfall$12(WorldPoint, Tile): boolean -> net.runelite.api.Tile#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$55(int, TileObject): boolean -> net.runelite.api.TileObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$56(Transport, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$58(TileObject): boolean -> net.runelite.api.ObjectComposition#getActions(): String[] +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$58(TileObject): boolean -> net.runelite.api.ObjectComposition#getName(): String +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$59(Transport, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$60(TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$61(Transport, Object): Integer -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$62(Transport, Object): Integer -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$66(int, String, TileObject): boolean -> net.runelite.api.ObjectComposition#getActions(): String[] +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$66(int, String, TileObject): boolean -> net.runelite.api.TileObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$67(Transport, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleWildernessObelisk$91(Transport, GameObject): boolean -> net.runelite.api.GameObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleWildernessObelisk$92(Transport, GameObject): boolean -> net.runelite.api.GameObject#getId(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$processWalk$0(): boolean -> net.runelite.api.widgets.Widget#getSpriteId(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$processWalk$1(): boolean -> net.runelite.api.widgets.Widget#getSpriteId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#processWalk(WorldPoint, int, int): WalkerState -> net.runelite.api.Client#getTopLevelWorldView(): WorldView +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#processWalk(WorldPoint, int, int, int, int): WalkerState -> net.runelite.api.Client#getTopLevelWorldView(): WorldView net.runelite.client.plugins.microbot.util.walker.Rs2Walker#restartPathfinding(WorldPoint, Set): boolean -> net.runelite.api.Client#isClientThread(): boolean net.runelite.client.plugins.microbot.util.walker.Rs2Walker#setStart(WorldPoint): void -> net.runelite.api.Client#isClientThread(): boolean net.runelite.client.plugins.microbot.util.walker.Rs2Walker#setTarget(WorldPoint): void -> net.runelite.api.Client#getLocalPlayer(): Player @@ -789,6 +814,7 @@ net.runelite.client.plugins.microbot.util.walker.Rs2Walker#setTarget(WorldPoint) net.runelite.client.plugins.microbot.util.walker.Rs2Walker#staminaThreshold(): int -> net.runelite.api.Client#getLocalPlayer(): Player net.runelite.client.plugins.microbot.util.walker.Rs2Walker#staminaThreshold(): int -> net.runelite.api.Player#getName(): String net.runelite.client.plugins.microbot.util.walker.Rs2Walker#tryHandleDoorObject(TileObject, WorldPoint, WorldPoint, WorldPoint, List, boolean): boolean -> net.runelite.api.ObjectComposition#getName(): String +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#tryHandleDoorObject(TileObject, WorldPoint, WorldPoint, WorldPoint, List, boolean): boolean -> net.runelite.api.WallObject#getOrientationA(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#walkCanvas(WorldPoint): WorldPoint -> net.runelite.api.Client#getTopLevelWorldView(): WorldView net.runelite.client.plugins.microbot.util.walker.Rs2Walker#walkCanvas(WorldPoint): WorldPoint -> net.runelite.api.WorldView#getPlane(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#walkCanvas(WorldPoint): WorldPoint -> net.runelite.api.coords.LocalPoint#fromWorld(WorldView, WorldPoint): LocalPoint @@ -805,6 +831,8 @@ net.runelite.client.plugins.microbot.util.walker.Rs2Walker#walkWithBankedTranspo net.runelite.client.plugins.microbot.util.walker.Rs2Walker#walkWithStateInternal(WorldPoint, int): WalkerState -> net.runelite.api.Client#getTopLevelWorldView(): WorldView net.runelite.client.plugins.microbot.util.walker.Rs2Walker#walkWithStateInternal(WorldPoint, int): WalkerState -> net.runelite.api.Client#isClientThread(): boolean net.runelite.client.plugins.microbot.util.walker.Rs2Walker#walkWithStateInternal(WorldPoint, int): WalkerState -> net.runelite.api.coords.LocalPoint#fromWorld(WorldView, WorldPoint): LocalPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#wallDoorTouchesSegment(WallObject, WorldPoint, WorldPoint): boolean -> net.runelite.api.WallObject#getOrientationA(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#wallDoorTouchesSegment(WallObject, WorldPoint, WorldPoint): boolean -> net.runelite.api.WallObject#getWorldLocation(): WorldPoint net.runelite.client.plugins.microbot.util.widget.Rs2Widget#checkBoundsOverlapWidgetInMainModal(Rectangle, int, int): boolean -> net.runelite.api.widgets.Widget#isHidden(): boolean net.runelite.client.plugins.microbot.util.widget.Rs2Widget#checkWidgetAndDescendantsForOverlapCanvas(Widget, Rectangle, int, int): boolean -> net.runelite.api.widgets.Widget#getBounds(): Rectangle net.runelite.client.plugins.microbot.util.widget.Rs2Widget#checkWidgetAndDescendantsForOverlapCanvas(Widget, Rectangle, int, int): boolean -> net.runelite.api.widgets.Widget#getDynamicChildren(): Widget[]