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/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/Pathfinder.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java index 789f67ac02..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 @@ -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: @@ -236,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(); @@ -285,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; } @@ -295,6 +310,14 @@ public void run() { addNeighbors(node); } + // 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)); 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..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 @@ -122,6 +122,26 @@ 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; + + // 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; @@ -207,8 +227,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(); @@ -288,6 +311,7 @@ private void refreshTransports(WorldPoint target) { transports.clear(); transportsPacked.clear(); usableTeleports.clear(); + blockedWalkingEdges.clear(); long mergeStart = System.currentTimeMillis(); Map> mergedList = createMergedList(); @@ -358,7 +382,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); @@ -381,6 +423,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; @@ -565,6 +620,169 @@ 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)); + } + + /** + * 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); + // 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<>()); + 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()); + } + + 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 +795,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..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 @@ -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; } @@ -322,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) { @@ -358,7 +385,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(); @@ -545,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); @@ -554,6 +618,8 @@ && handleNearbyRawPathSceneObjects(rawPath, HANDLER_RANGE)) { if (isWalkCancelled(target)) { return WalkerState.EXIT; } + log.debug("[Walker click] target={} pathIdx={} playerBefore={} clicked={}", + targetWp, targetIdx, posBefore, clicked); if (clicked) { final WorldPoint b = targetWp; final WorldPoint before = posBefore; @@ -576,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.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. // Without this, isStuckTooLong() fires after long successful walks because @@ -637,7 +709,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 +725,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) { @@ -1583,7 +1664,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; @@ -1748,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; @@ -1760,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; @@ -2278,6 +2358,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) { @@ -2568,7 +2659,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; } } } 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[]