From 444f06e852f58a00f714b0f28a4bf2acd50bcfac Mon Sep 17 00:00:00 2001 From: infuse21 Date: Sat, 18 Jul 2026 12:39:16 +0100 Subject: [PATCH 1/4] ## Summary - Defers off-path recalculation while the player is still moving, animating, interacting, settling after doors/transports, or making recent route/interim progress. - Reworks route minimap clicks and unreachable-tile recovery to stay anchored to the planned raw path, use Euclidean minimap reach, and avoid backtracking to earlier nearby path branches. - Adds sticky interim checkpoint tracking so recovery/continuation clicks are not spammed while valid movement is already in flight. - Tightens door handling by deferring door probes while moving and only blacklisting wrong-traversal doors when the player started near the attempted door edge. - Improves charter ship destination widget lookup/clicking and optional confirmation handling. --- docs/entity-guides/movement.md | 32 +- .../shortestpath/ShortestPathPlugin.java | 2 +- .../pathfinder/PathfinderConfig.java | 2 +- .../microbot/util/walker/Rs2Walker.java | 1337 ++++++++++++++--- .../microbot/util/walker/WebWalkLog.java | 21 +- .../util/walker/Rs2WalkerUnitTest.java | 284 +++- 6 files changed, 1457 insertions(+), 221 deletions(-) diff --git a/docs/entity-guides/movement.md b/docs/entity-guides/movement.md index 4a658235f4..2ed25f0c5d 100644 --- a/docs/entity-guides/movement.md +++ b/docs/entity-guides/movement.md @@ -77,6 +77,8 @@ waitForDoorInteractionProgress(fromWp, toWp); **Defensive check:** In live testing, a door should produce one interaction followed by movement/path progress, not repeated `Raw path door handler resolved obstacle` messages every tick while the player is moving. +If a previous minimap click is still moving the player, do not attribute that movement to a new gate interaction. A valid gate can be blacklisted when the walker clicks an interim waypoint, notices the gate while movement is still in flight, then compares the pre/post positions from the old minimap movement against the gate edge. Wrong-traversal blacklisting should only run when the player started near the attempted door edge. + ## 5. Suppress the inverse adjacent transport after crossing a same-plane door Some doors are represented in `transports.tsv` as two adjacent same-plane transports, one for each direction. After the walker clicks one side and arrives on the other, immediately accepting the inverse transport can bounce the player back through the same door instead of letting the next minimap step continue away from it. Mark both tiles of a successful adjacent same-plane transport as recently handled for a short window. @@ -286,12 +288,38 @@ if (isStuckTooLong()) { After a handled transport, avoid expensive path-adjacent or raw transport scans on ordinary open-ground segments unless a nearby planned transport or recent door attempt exists. Those scans are recovery tools, and on long outdoor routes a no-op scan can add several seconds before the next minimap click. -For long-route minimap walking, let the next checkpoint selection happen before the current minimap target is fully consumed. Waiting until the player is only a few tiles from the interim makes the walker visibly stop before issuing the next click; handing off at a moderate remaining distance keeps movement continuous without rapid re-clicking. If an interim clears as close and no nearby route door/transport is pending, issue the next route-aligned continuation click immediately instead of waiting for idle-nudge recovery. +For long-route minimap walking, let the next checkpoint selection happen before the current minimap target is fully consumed. Waiting until the player is only a few tiles from the interim makes the walker visibly stop before issuing the next click; handing off at a moderate remaining distance keeps movement continuous without rapid re-clicking. If an interim clears as close and no nearby route door/transport is pending, issue the next route-aligned continuation click immediately instead of waiting for idle-nudge recovery. Stale-progress checks must also count distance progress toward the actual interim checkpoint, not only smoothed path index progress; sparse smoothed paths can otherwise clear valid raw-path checkpoints as stale while the player is still walking toward them. + +Before the first movement click, only scan a small number of immediate route edges for doors or blockers. A door several segments ahead can be handled after the first minimap click moves the player toward it; spending startup time scanning every nearby raw segment creates a visible cold start. Continuation clicks that keep an active route moving should be tail-exempt like `interim-in-flight`; otherwise very long routes can exhaust `MAX_PROCESS_WALK_TAIL_ITERATIONS` while still making progress and trigger an unnecessary auto-retry. Sticky interim targets should also clear when route-index progress goes stale. If the player keeps moving but the closest path index does not advance for `INTERIM_PROGRESS_TIMEOUT_MS`, treat the checkpoint as stale and select a fresh route-aligned target instead of waiting for max-age expiry. -When a route-following minimap click is outside the minimap clip, fallback clicks must stay on the raw path. A generic "reachable tile closer to target" fallback can select a tile far away from the route in open areas, especially near the final destination. +When a route-following minimap click is outside the minimap clip, fallback clicks must stay on the raw path. This includes the "clicked but no movement yet" retry after a route click and route-backed direct short-walks near the final destination. A generic "reachable tile closer to target" fallback can select a tile far away from the route in open areas, especially near fences and the final destination. Raw fallback must be anchored from the stabilized route index, not a fresh nearest-raw-tile lookup, or the walker can snap backward to an already-travelled branch. For adjacent same-plane shortcuts, do not treat any movement away from the origin as success. Some shortcuts, such as stepping stones, can fail and place the player on a fallback tile; once the player is settled away from the expected destination, stop the landing wait and replan from the actual tile. + +## 14. Keep optimistic recovery clicks route-backed and paced + +Unreachable-tile recovery is a fallback for bounded reachability false negatives, not a license to pick an arbitrary minimap point. Recovery clicks should use the same route-backed raw-path fallback as normal route movement, then store the actual clicked target as the sticky interim checkpoint. After issuing the click, wait only for movement to start or the checkpoint/goal to be reached; do not wait for a full idle cycle before returning to the walk loop. + +**Why this matters:** Bounded local reachability can report a valid route tile as unreachable even though the server pathfinder can still walk toward it. Arbitrary or repeated recovery clicks can pull the player away from the planned route and create visible stop-start movement. + +**Pattern to follow:** + +```java +int reach = STALL_RECOVERY_MINIMAP_REACH_EUCLIDEAN; +WorldPoint clickTarget = clampToEuclideanRadius(playerLoc, recoverTarget, reach - 1); +WorldPoint clickedTarget = clickMiniMapOrFallback(rawPath, clickTarget, playerLoc, reach - 1, false, rawAnchorIndex); +if (clickedTarget != null) { + interimTargetWp = clickedTarget; + waitForMovementStartAfterRecovery(target, playerLoc, clickedTarget, target, finishThreshold); +} +``` + +**Where this applies:** `Rs2Walker.processWalk` unreachable-smoothed-tile recovery, route-backed direct short-walks, and any fallback that runs after local reachability says a route tile is not currently reachable. + +**Defensive check:** Exercise a long outdoor route and a route with gates or transports. Recovery logs should select route-backed points, and there should be no long idle pause between `unreachable optimistic recovery` and the next route-aligned movement. + +After recovery sets a sticky interim target, do not issue another optimistic recovery while that interim is still active and movement/progress is fresh. Yield as `interim-in-flight` or another tail-exempt movement state until the checkpoint is close, stale, or expired. Otherwise the walker can loop through several recovery clicks while the player is already moving, especially when local reachability reports the next smoothed tile as unreachable. diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPlugin.java index 6a62b53d07..3f4c1437d1 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPlugin.java @@ -308,7 +308,7 @@ protected void shutDown() { //Method from microbot public static void exit() { if (pathfindingExecutor != null) { - Rs2Walker.setTarget(null); + Rs2Walker.clearWalkingRoute("shortest-path-plugin:exit"); pathfindingExecutor.shutdownNow(); pathfindingExecutor = null; } 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 211e1fdeef..92855da1cd 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 @@ -879,7 +879,7 @@ private boolean useTransport(Transport transport) { if (transport.getCurrencyAmount() > 0) { if (refreshCurrencyCache != null) { int[] cached = refreshCurrencyCache.computeIfAbsent(transport.getCurrencyName(), name -> { - int invCount = Rs2Inventory.count(name); + int invCount = Rs2Inventory.itemQuantity(name); int bankCount = useBankItems ? Rs2Bank.count(name) : 0; return new int[]{invCount, bankCount}; }); 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 2c760efe15..68df3dceea 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 @@ -126,29 +126,38 @@ public static WorldPoint getCurrentTarget() { private static volatile long interimSetAtMs = 0L; private static volatile long interimLastProgressAtMs = 0L; private static volatile int interimLastBestPathIdx = -1; + private static volatile int interimLastDistanceToTarget = Integer.MAX_VALUE; private static volatile long interimLastRetargetAtMs = 0L; /** Cooldown so partial-segment in-transit {@link #recalculatePath()} does not spam. */ private static volatile long lastPartialTransRecalcMs = 0L; private static final long PARTIAL_TRANS_RECAL_COOLDOWN_MS = 3500L; - private static final int INTERIM_CLOSE_TILES = 4; + private static final int INTERIM_CLOSE_TILES = 5; private static final int INTERIM_PRECLICK_TILES = 6; - private static final int INTERIM_RUN_PRECLICK_TILES = 11; + private static final int INTERIM_RUN_PRECLICK_TILES = 8; private static final int INTERIM_MOVING_POLL_MS = 450; private static final long INTERIM_PROGRESS_TIMEOUT_MS = 2500L; private static final long INTERIM_MAX_AGE_MS = 10_000L; private static final long INTERIM_RETARGET_COOLDOWN_MS = 900L; + private static final long ROUTE_PROGRESS_STALL_GRACE_MS = 4_000L; + private static final long OFF_PATH_RECALC_RECENT_MOVEMENT_MS = 2_000L; + private static final long OFF_PATH_RECALC_ROUTE_PROGRESS_GRACE_MS = 3_500L; + private static final long OFF_PATH_RECALC_MINIMAP_CLICK_GRACE_MS = 2_500L; + private static final int OFF_PATH_RECALC_DEFER_WAIT_MIN_MS = 250; + private static final int OFF_PATH_RECALC_DEFER_WAIT_MAX_MS = 1_200; private static final long RAW_SCAN_DOOR_FOCUS_MAX_MS = 2200L; private static final int RAW_SCAN_DOOR_FOCUS_MAX_ATTEMPTS = 3; private static final long DOOR_POST_INTERACT_SETTLE_MS = 900L; private static final long DOOR_EDGE_SKIP_COOLDOWN_MS = 700L; - private static final long RECOVERY_MOVEMENT_IN_FLIGHT_MS = 1400L; + private static final long RECOVERY_MOVEMENT_IN_FLIGHT_MS = 3_500L; private static final long DOOR_TRAVERSAL_RECOVERY_BLOCK_MS = 2_200L; + private static final long POST_DOOR_NUDGE_RECENT_ATTEMPT_MS = 6_000L; private static final int PATHFINDER_DONE_POLL_WAIT_MS = 1200; private static final int PATHFINDER_DONE_RETRY_SLEEP_MIN_MS = 120; private static final int PATHFINDER_DONE_RETRY_SLEEP_MAX_MS = 220; private static final long STARTUP_FIRST_CLICK_BUDGET_MS = 2200L; + private static final int STARTUP_PRECLICK_DOOR_SCAN_LOOKAHEAD_EDGES = 2; private static final int POST_DOOR_FAST_CLICK_MAX_EUCLIDEAN = 13; private static final int POST_DOOR_EDGE_NUDGE_MAX_FROM_PLAYER = 3; private static final int POST_DOOR_EDGE_NUDGE_WAIT_MS = 1200; @@ -162,7 +171,7 @@ public static WorldPoint getCurrentTarget() { private static final int UNREACHABLE_DOOR_RECOVERY_BACKTRACK_EDGES = 2; private static final int UNREACHABLE_DOOR_RECOVERY_LOOKAHEAD_EDGES = 10; private static final int STALL_RECOVERY_MINIMAP_REACH_EUCLIDEAN = 10; - private static final int NORMAL_MINIMAP_REACH_EUCLIDEAN = 12; + private static final int NORMAL_MINIMAP_REACH_EUCLIDEAN = 11; private static final int UNREACHABLE_RECOVERY_FORWARD_SCAN_TILES = 24; private static final long ACTIVE_ROUTE_IDLE_NUDGE_MS = 2_500L; private static final long ACTIVE_ROUTE_IDLE_NUDGE_COOLDOWN_MS = 2_000L; @@ -196,6 +205,7 @@ public static WorldPoint getCurrentTarget() { private static volatile WorldPoint routeProgressPathStart = null; private static volatile WorldPoint routeProgressPathEnd = null; private static volatile int routeProgressPathSize = -1; + private static volatile long routeProgressAdvancedAtMs = 0L; private static final java.util.Deque expectedTransportDestinations = new ArrayDeque<>(); private static final Set startupPhasesLogged = ConcurrentHashMap.newKeySet(); @@ -314,6 +324,24 @@ private enum WalkerPhase { STEADY } + private static final class WalkLoopSnapshot { + private final WorldPoint playerLoc; + private final HashMap closestReachableTiles; + + private WalkLoopSnapshot(WorldPoint playerLoc) { + this.playerLoc = playerLoc; + this.closestReachableTiles = getClosestIndexReachableTiles(playerLoc); + } + + private static WalkLoopSnapshot capture() { + return new WalkLoopSnapshot(Rs2Player.getWorldLocation()); + } + + private int closestTileIndex(List path) { + return getClosestTileIndex(path, playerLoc, closestReachableTiles); + } + } + private interface ObstaclePolicy { long segmentDoorTimeoutMs(); long unreachableDoorTimeoutMs(); @@ -420,6 +448,21 @@ private static ObstaclePolicy obstaclePolicyForCurrentPhase() { : STEADY_OBSTACLE_POLICY; } + static boolean shouldSkipStartupPreclickSegmentHandlers(boolean startupBeforeFirstClick, + int segmentIdx, + int routeStartIdx, + boolean recentDoorAttemptNearSegment, + boolean doorSettling, + boolean recoveryInFlight) { + if (!startupBeforeFirstClick || routeStartIdx < 0 || segmentIdx < routeStartIdx) { + return false; + } + if (recentDoorAttemptNearSegment || doorSettling || recoveryInFlight) { + return false; + } + return segmentIdx > routeStartIdx + STARTUP_PRECLICK_DOOR_SCAN_LOOKAHEAD_EDGES; + } + /** * Same-plane Chebyshev distance from player to {@code dest} strictly less than {@code maxChebyshevExclusive}. * Requires matching {@link WorldPoint#getPlane()} before using {@link WorldPoint#distanceTo2D} — that method only @@ -491,6 +534,7 @@ private static int tightFinishThreshold(WorldPoint goal, WorldPoint pathLastWalk /** If phase 1 exits on arrival distance while still moving, wait briefly for idle-only (reduces tail churn). */ private static final int POST_SCENE_WALK_IDLE_SECOND_PHASE_MS_MAX = 4_000; + private static final int POST_RECOVERY_MOVEMENT_START_WAIT_MS = 1_200; private static void waitUntilIdleAfterSceneWalk(WorldPoint cancelGoal, int timeoutMs) { waitUntilIdleAfterSceneWalk(cancelGoal, timeoutMs, null, 0); @@ -749,6 +793,7 @@ public static List getSeasonalTransportHandlers() */ public static final class Telemetry { public static final AtomicInteger offPathRecalcCount = new AtomicInteger(); + public static final AtomicInteger offPathRecalcDeferredCount = new AtomicInteger(); public static final AtomicInteger stallRecalcCount = new AtomicInteger(); public static final AtomicInteger partialRetryCount = new AtomicInteger(); public static final AtomicInteger unreachableCount = new AtomicInteger(); @@ -766,6 +811,9 @@ public static final class Telemetry { private static final ConcurrentHashMap doorRejectByCause = new ConcurrentHashMap<>(); private static final AtomicInteger doorRejectSummaryLogSeq = new AtomicInteger(0); private static final int DOOR_REJECT_SUMMARY_LOG_INTERVAL = 40; + private static final ConcurrentHashMap offPathDeferredByReason = new ConcurrentHashMap<>(); + private static final AtomicInteger offPathDeferredSummaryLogSeq = new AtomicInteger(0); + private static final int OFF_PATH_DEFERRED_SUMMARY_LOG_INTERVAL = 20; /** * Rate-limited debug summary of {@link #doorRejectByCause} tallies (noise control on tight door clusters). @@ -805,6 +853,22 @@ public static void recordOffPathRecalc(WorldPoint playerPos, int pathSize) { playerPos, pathSize, offPathRecalcCount.get(), stallRecalcCount.get()); } + public static void recordOffPathRecalcDeferred(String reason, WorldPoint playerPos, + WorldPoint target, int pathSize) { + if (reason == null || reason.isEmpty()) { + reason = "unknown"; + } + offPathRecalcDeferredCount.incrementAndGet(); + offPathDeferredByReason.computeIfAbsent(reason, k -> new AtomicInteger()).incrementAndGet(); + lastReason = "off-path-deferred:" + reason; + lastEventAtMs.set(System.currentTimeMillis()); + if (Rs2LogRateLimit.everyN(offPathDeferredSummaryLogSeq, OFF_PATH_DEFERRED_SUMMARY_LOG_INTERVAL) + && log.isDebugEnabled()) { + log.debug("[WalkerTelemetry] OFFPATH_RECALC_DEFERRED player={} target={} pathSize={} summary={}", + playerPos, target, pathSize, offPathDeferredByReason); + } + } + public static void recordStallRecalc(long sinceMovedMs, WorldPoint playerPos) { stallRecalcCount.incrementAndGet(); lastReason = "stall"; @@ -836,6 +900,7 @@ public static void recordUnreachable(String cause, WorldPoint player, WorldPoint public static void reset() { offPathRecalcCount.set(0); + offPathRecalcDeferredCount.set(0); stallRecalcCount.set(0); partialRetryCount.set(0); unreachableCount.set(0); @@ -845,6 +910,8 @@ public static void reset() { seasonalHandlerMissCount.set(0); doorRejectByCause.clear(); doorRejectSummaryLogSeq.set(0); + offPathDeferredByReason.clear(); + offPathDeferredSummaryLogSeq.set(0); lastEventAtMs.set(0); lastReason = ""; log.info("[WalkerTelemetry] counters reset"); @@ -1054,6 +1121,7 @@ private static WalkerState walkWithStateInternal(WorldPoint target, int distance interimSetAtMs = 0L; interimLastProgressAtMs = 0L; interimLastBestPathIdx = -1; + interimLastDistanceToTarget = Integer.MAX_VALUE; interimLastRetargetAtMs = 0L; lastPartialTransRecalcMs = 0L; idleNudgeLastObservedLocation = playerLocWalk; @@ -1191,9 +1259,10 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part int rawSize = rawPath == null ? -1 : rawPath.size(); int walkSize = path == null ? -1 : path.size(); markStartupPhase("path_snapshot", target, "raw=" + rawSize + " walk=" + walkSize); + final WalkLoopSnapshot walkLoop = WalkLoopSnapshot.capture(); final WorldPoint dst; if (path == null || path.isEmpty()) { - dst = Rs2Player.getWorldLocation(); + dst = walkLoop.playerLoc; } else { dst = path.get(path.size()-1); } @@ -1204,7 +1273,7 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part WebWalkLog.partialSegment(dst, dst.distanceTo(target), target, path.size()); partialPath = true; } else { - Telemetry.recordUnreachable("no-walkable-path", Rs2Player.getWorldLocation(), + Telemetry.recordUnreachable("no-walkable-path", walkLoop.playerLoc, target, dst, path == null ? 0 : path.size(), distance, pathfinder); setTarget(null, "rs2walker:processWalk:no-walkable-path"); return WalkerState.UNREACHABLE; @@ -1218,11 +1287,11 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part // Partial segment: before standing on the segment endpoint, refresh routing from current // position so the continuation is ready (smooth handoff vs dead stop at segment end). if (partialPath) { - WorldPoint playerPt = Rs2Player.getWorldLocation(); + WorldPoint playerPt = walkLoop.playerLoc; if (playerPt != null && dst != null) { int distToDstSeg = playerPt.distanceTo2D(dst); int distToGoal = playerPt.distanceTo2D(target); - int closestEarly = getClosestTileIndex(path); + int closestEarly = walkLoop.closestTileIndex(path); int remainingSteps = closestEarly >= 0 ? (path.size() - 1 - closestEarly) : Integer.MAX_VALUE; final int nearSegmentEndTiles = 12; final int nearSegmentEndSteps = 10; @@ -1316,13 +1385,13 @@ && tryIssueRouteRecoveryClick(rawPath, path, target, "active route idle nudge")) } } - WorldPoint playerLocForIndex = Rs2Player.getWorldLocation(); - int indexOfStartPoint = stabilizeRouteProgressIndex(path, getClosestTileIndex(path), target, playerLocForIndex); + WorldPoint playerLocForIndex = walkLoop.playerLoc; + int indexOfStartPoint = stabilizeRouteProgressIndex(path, walkLoop.closestTileIndex(path), target, playerLocForIndex); indexOfStartPoint = advanceIndexPastRecentTransportEdge(path, indexOfStartPoint, playerLocForIndex); if (indexOfStartPoint == -1) { walkerDiag("getClosestTileIndex=-1 pathSize=%d player=%s pathFirst=%s pathLast=%s", path.size(), - Rs2Player.getWorldLocation(), + playerLocForIndex, path.isEmpty() ? null : path.get(0), path.isEmpty() ? null : path.get(path.size() - 1)); traceProcessWalkExit("closest-index-none", target, processWalkTail); @@ -1331,7 +1400,7 @@ && tryIssueRouteRecoveryClick(rawPath, path, target, "active route idle nudge")) } primeExpectedTransportDestinations(path, indexOfStartPoint); - lastPosition = Rs2Player.getWorldLocation(); + lastPosition = playerLocForIndex; boolean clearedInterimTarget = clearInterimTargetIfReachedOrExpired(lastPosition, path, System.currentTimeMillis()); WorldPoint plImmediate = lastPosition; @@ -1489,21 +1558,21 @@ && tryIssueRouteContinuationClick(rawPath, path, target)) { "nearPath=false variance=" + nearPathByVariance); } if (!nearPath && !recentTransportWindow && !nearPathByVariance) { - // Avoid mid-walk recalculation while the player is still moving. recalculatePath() - // cancels the pathfinder and waits up to 10s for a new one — a visible stall. - // isStuckTooLong() will trigger a real recalculation if progress actually halts. - boolean movingOrRecentlyMoved = Rs2Player.isMoving() - || (lastMovedTimeMs > 0 && System.currentTimeMillis() - lastMovedTimeMs < 2000); - if (movingOrRecentlyMoved) { + // Avoid mid-walk recalculation while recent clicks, route progress, or busy state + // indicate the player may still be advancing. isStuckTooLong() will trigger a + // real recovery path once progress actually halts. + String deferReason = currentOffPathRecalcDeferralReason(lastAttemptedMinimapClickAtMs); + if (deferReason != null) { + Telemetry.recordOffPathRecalcDeferred(deferReason, playerForPathCheck, target, path.size()); if (lastTransportHandledAtMs > 0 && System.currentTimeMillis() - lastTransportHandledAtMs <= POST_TRANSPORT_PATH_TMARK_WINDOW_MS) { WebWalkLog.tmark("post_transport_offpath_moving_yield", System.currentTimeMillis() - lastTransportHandledAtMs, target, playerForPathCheck, - "movingRecent=true"); + "defer=" + deferReason); } - exitReason = "off-path-but-moving"; + exitReason = "off-path-deferred:" + deferReason; break; } Telemetry.recordOffPathRecalc(Rs2Player.getWorldLocation(), path.size()); @@ -1543,15 +1612,28 @@ && tryIssueRouteContinuationClick(rawPath, path, target)) { && !isDoorInteractionSettling() && !isRecoveryMovementInFlight() && reachableTilesCache.containsKey(currentWorldPoint); - if (skipPostTransportSegmentHandlers) { + boolean skipStartupPreclickSegmentHandlers = shouldSkipStartupPreclickSegmentHandlers( + currentWalkerPhase() == WalkerPhase.STARTUP, + i, + indexOfStartPoint, + recentDoorAttemptNearSegment, + isDoorInteractionSettling(), + isRecoveryMovementInFlight()); + if (skipPostTransportSegmentHandlers || skipStartupPreclickSegmentHandlers) { + if (skipStartupPreclickSegmentHandlers) { + markStartupPhase("preclick_segment_handler_skip", target, + "i=" + i + " reason=startup_preclick_budget"); + } tmarkPostTransport("post_transport_segment_handler_skip", target, - "i=" + i + " reason=no_nearby_planned_transport"); + "i=" + i + " reason=" + (skipPostTransportSegmentHandlers + ? "no_nearby_planned_transport" + : "startup_preclick_budget")); } else { long segmentHandlerStartAt = System.currentTimeMillis(); int rawI = (i < smoothedToRaw.length) ? smoothedToRaw[i] : 0; int rawEnd = rawEndForSmoothedIndex(i, smoothedToRaw, rawPath, path); - if (!isDoorInteractionSettling() && !isRecoveryMovementInFlight()) { + if (!Rs2Player.isMoving() && !isDoorInteractionSettling() && !isRecoveryMovementInFlight()) { doorOrTransportResult = handleDoorsInRawSegment(rawPath, rawI, rawEnd, obstaclePolicy.segmentDoorTimeoutMs(), doorEdgesAttemptedThisTail, reachableTilesCache); @@ -1677,6 +1759,14 @@ && tryIssueRouteContinuationClick(rawPath, path, target)) { exitReason = "door-traversal-pending-yield"; break; } + if (shouldYieldForActiveRecoveryInterim(playerLoc, path, System.currentTimeMillis())) { + exitReason = "interim-in-flight"; + break; + } + if (tryRecentDoorAttemptEdgeNudge(playerLoc, target)) { + exitReason = "recent-door-edge-nudge"; + break; + } if (handlePendingDoorNearRawPath(rawPath, obstaclePolicy.unreachableDoorTimeoutMs(), doorEdgesAttemptedThisTail, playerLoc, 2, 14)) { exitReason = "door-handled-unreachable-raw-scan"; @@ -1697,6 +1787,17 @@ && tryIssueRouteContinuationClick(rawPath, path, target)) { UNREACHABLE_DOOR_RECOVERY_BACKTRACK_EDGES, UNREACHABLE_DOOR_RECOVERY_LOOKAHEAD_EDGES, HANDLER_RANGE); + if (!gateDoorInteraction + && unresolvedDoorNearRawPath + && handleUnresolvedDoorNearRawPath(rawPath, rawEdgeStart, + obstaclePolicy.unreachableDoorTimeoutMs(), doorEdgesAttemptedThisTail, + playerLoc, + UNREACHABLE_DOOR_RECOVERY_BACKTRACK_EDGES, + UNREACHABLE_DOOR_RECOVERY_LOOKAHEAD_EDGES, + HANDLER_RANGE)) { + exitReason = "door-handled-nearby-route-door"; + break; + } // Fallback: only interact with objects on/adjacent to blocked path edges // within ~15 tiles. Prevents clicking already-open / unrelated doors. final long nowMs = System.currentTimeMillis(); @@ -1726,27 +1827,27 @@ && tryIssueRouteContinuationClick(rawPath, path, target)) { // like a human clicking the furthest visible tile. We trust the server path // (no reachability gate on the click); isKnownWalkableOrUnloaded only keeps // us from clicking into a known wall, it is NOT the bounded BFS check. - final int RECOVERY_MINIMAP_REACH_EUCLIDEAN = 13; + final int recoveryMinimapReach = STALL_RECOVERY_MINIMAP_REACH_EUCLIDEAN; int recoverIdx = findForwardReachableRecoveryIndex(path, i, playerLoc, - RECOVERY_MINIMAP_REACH_EUCLIDEAN); + recoveryMinimapReach); if (recoverIdx < 0) { recoverIdx = findFurthestClickableIndex(path, i, playerLoc, wp -> { Set ts = ShortestPathPlugin.getTransports().get(wp); return ts != null && !ts.isEmpty(); }, - RECOVERY_MINIMAP_REACH_EUCLIDEAN); + recoveryMinimapReach); } int minRecoveryIdx = Math.max(indexOfStartPoint, i); recoverIdx = Math.min(Math.max(recoverIdx, minRecoveryIdx), path.size() - 1); WorldPoint recoverTarget = path.get(recoverIdx); if (euclideanSq(recoverTarget, playerLoc) - > RECOVERY_MINIMAP_REACH_EUCLIDEAN * RECOVERY_MINIMAP_REACH_EUCLIDEAN) { + > recoveryMinimapReach * recoveryMinimapReach) { // Furthest in-range path tile still beyond the minimap clip (e.g. a // diagonal segment). Interpolate a point near the minimap edge toward // path[i]; the server routes through whatever blocks line-of-sight. recoverTarget = interpolateClickableTarget(path, i, playerLoc, - path.get(i), RECOVERY_MINIMAP_REACH_EUCLIDEAN - 1, + path.get(i), recoveryMinimapReach - 1, wp -> inInstance || isKnownWalkableOrUnloaded(wp)); } // Don't let recovery park the player on a tile next to an aggressive NPC @@ -1764,9 +1865,24 @@ && tryIssueRouteContinuationClick(rawPath, path, target)) { recoverIdx = safeIdx; recoverTarget = path.get(safeIdx); } - boolean clicked = recoverTarget != null - && !recoverTarget.equals(playerLoc) - && Rs2Walker.walkMiniMap(recoverTarget); + int rawAnchorIndex = rawIndexForSmoothedIndex(recoverIdx, smoothedToRaw, rawPath); + WorldPoint rawRecoveryTarget = findFurthestReachableRawPathPoint(rawPath, playerLoc, + recoveryMinimapReach - 1, rawAnchorIndex); + if (rawRecoveryTarget != null + && !rawRecoveryTarget.equals(playerLoc) + && (dangerCfg == null + || !dangerCfg.isAvoidDangerousNpcs() + || !dangerCfg.isDangerousAdjacentTile(WorldPointUtil.packWorldPoint(rawRecoveryTarget)))) { + recoverTarget = rawRecoveryTarget; + } + WorldPoint clickedRecoveryTarget = null; + if (recoverTarget != null && !recoverTarget.equals(playerLoc)) { + recoverTarget = clampToEuclideanRadius(playerLoc, recoverTarget, + recoveryMinimapReach - 1); + clickedRecoveryTarget = clickMiniMapOrFallback(rawPath, recoverTarget, playerLoc, + recoveryMinimapReach - 1, rawPath == null || rawPath.isEmpty(), rawAnchorIndex); + } + boolean clicked = clickedRecoveryTarget != null; // Scene-click fallback only on final-adjacent approach (minimap click may // miss the clip when very close); kept gated on reachability since it is a // last resort, not the primary recovery path. @@ -1777,23 +1893,28 @@ && tryIssueRouteContinuationClick(rawPath, path, target)) { && Rs2Tile.isTileReachable(recoverTarget) && walkFastCanvas(recoverTarget)) { clicked = true; + clickedRecoveryTarget = recoverTarget; log.debug("[Walker] unreachable recovery: scene click -> {}", recoverTarget); } log.info("[Walker] unreachable optimistic recovery: clicked={} to={} pathTile={} idx={}", - clicked, recoverTarget, currentWorldPoint, recoverIdx); + clicked, clicked ? clickedRecoveryTarget : recoverTarget, currentWorldPoint, recoverIdx); if (clicked) { markFirstMovementClick("first_recovery_click", target, playerLoc, - "to=" + compactWorldPoint(recoverTarget)); + "to=" + compactWorldPoint(clickedRecoveryTarget)); lastUnreachableRecoveryClickAtMs = System.currentTimeMillis(); // Sticky interim: subsequent iterations travel toward this point via the // interim-in-flight path instead of re-running the (false-negative) // reachability check and re-clicking every tick. - interimTargetWp = recoverTarget; + interimTargetWp = clickedRecoveryTarget; interimTargetIdx = recoverIdx; interimSetAtMs = System.currentTimeMillis(); + interimLastProgressAtMs = interimSetAtMs; + interimLastBestPathIdx = getClosestTileIndex(path, playerLoc); + interimLastDistanceToTarget = distanceToInterimOrMax(clickedRecoveryTarget, playerLoc); + interimLastRetargetAtMs = interimSetAtMs; WorldPoint pathLastRecovery = path.get(path.size() - 1); int finishThRecovery = tightFinishThreshold(target, pathLastRecovery, distance); - waitUntilIdleAfterSceneWalk(target, POST_SCENE_WALK_IDLE_WAIT_MS_MAX, target, + waitForMovementStartAfterRecovery(target, playerLoc, clickedRecoveryTarget, target, finishThRecovery); // Next outer iteration runs checkIfStuck/isStuckTooLong before tile delta — avoid // spurious stall-recalc right after issuing recovery movement. @@ -1826,8 +1947,8 @@ && walkFastCanvas(recoverTarget)) { // Minimap clickable area is a circle, so reach is a Euclidean radius — // cardinal tiles reach ~13, diagonals ~9. Empirically 14 was too // optimistic (clicks at 13.5–13.9 Euclidean missed the clip). - final int MINIMAP_REACH_EUCLIDEAN = NORMAL_MINIMAP_REACH_EUCLIDEAN; WorldPoint playerLoc = Rs2Player.getWorldLocation(); + final int MINIMAP_REACH_EUCLIDEAN = NORMAL_MINIMAP_REACH_EUCLIDEAN; // Checkpoint-style walking: once we set a minimap flag, let the player actually // travel toward it. Do not keep recalculating/clicking new targets mid-run. @@ -1848,6 +1969,7 @@ && walkFastCanvas(recoverTarget)) { interimSetAtMs = 0L; interimLastProgressAtMs = 0L; interimLastBestPathIdx = -1; + interimLastDistanceToTarget = Integer.MAX_VALUE; interimLastRetargetAtMs = 0L; doorOrTransportResult = true; exitReason = "door-handled-during-interim"; @@ -1859,17 +1981,20 @@ && walkFastCanvas(recoverTarget)) { || !Rs2Player.isMoving(), INTERIM_MOVING_POLL_MS); WorldPoint posAfterWait = Rs2Player.getWorldLocation(); + recordInterimDistanceProgress(interimFinal, posAfterWait, System.currentTimeMillis()); if ((posAfterWait != null && posBeforeWait.distanceTo2D(posAfterWait) > 0) || Rs2Player.isMoving()) { lastMovedTimeMs = System.currentTimeMillis(); stuckCount = 0; } - boolean readyForNextClick = posAfterWait != null - && interimFinal.distanceTo2D(posAfterWait) <= interimPreclickTiles(); - if (!readyForNextClick && Rs2Player.isMoving()) { + boolean closeEnoughForNextClick = posAfterWait != null + && interimFinal.distanceTo2D(posAfterWait) <= INTERIM_CLOSE_TILES; + if (!closeEnoughForNextClick && Rs2Player.isMoving()) { exitReason = "interim-in-flight"; walkerDiag("interim-in-flight interim=%s interimDist=%d player=%s moving=true", - interimFinal, interimDist, playerLoc); + interimFinal, + posAfterWait == null ? interimDist : interimFinal.distanceTo2D(posAfterWait), + posAfterWait == null ? playerLoc : posAfterWait); break; } } else { @@ -1881,6 +2006,7 @@ && walkFastCanvas(recoverTarget)) { interimSetAtMs = 0L; interimLastProgressAtMs = 0L; interimLastBestPathIdx = -1; + interimLastDistanceToTarget = Integer.MAX_VALUE; interimLastRetargetAtMs = 0L; } } @@ -1890,34 +2016,21 @@ && walkFastCanvas(recoverTarget)) { interimSetAtMs = 0L; interimLastProgressAtMs = 0L; interimLastBestPathIdx = -1; + interimLastDistanceToTarget = Integer.MAX_VALUE; interimLastRetargetAtMs = 0L; } - int targetIdx = findFurthestClickableIndex(path, i, playerLoc, + int targetIdx = findFurthestForwardClickableIndex(path, i, playerLoc, wp -> { Set ts = ShortestPathPlugin.getTransports().get(wp); return ts != null && !ts.isEmpty(); }, MINIMAP_REACH_EUCLIDEAN); WorldPoint targetWp = path.get(targetIdx); - // Forward waypoint out of minimap reach (e.g., diagonal PathSmoother - // segment at Chebyshev 10 = Euclidean 14.1). Backward-scan returns - // the previous in-reach waypoint, but clicking it walks backward or - // barely advances. Instead, interpolate a point close to the minimap - // edge toward the forward waypoint — the server's walk-here - // pathfinder routes through whatever is blocking line-of-sight - // (door, gate, diagonal offset). Each interpolated click covers - // ~12 tiles, matching a human clicking the furthest visible tile. - if (targetIdx < i) { - targetWp = interpolateClickableTarget( - path, - i, - playerLoc, - targetWp, - MINIMAP_REACH_EUCLIDEAN - 1, - wp -> inInstance || isKnownWalkableOrUnloaded(wp)); - targetIdx = Math.max(indexOfStartPoint, i - 1); - } else if (euclideanSq(targetWp, playerLoc) > MINIMAP_REACH_EUCLIDEAN * MINIMAP_REACH_EUCLIDEAN) { + // If the forward waypoint is outside minimap reach, interpolate a + // visible point in that direction instead of falling back to an + // earlier path tile. + if (euclideanSq(targetWp, playerLoc) > MINIMAP_REACH_EUCLIDEAN * MINIMAP_REACH_EUCLIDEAN) { targetWp = interpolateClickableTarget( path, i, @@ -1934,17 +2047,19 @@ && walkFastCanvas(recoverTarget)) { WorldPoint sticky = interimTargetWp; if (sticky != null && sticky.getPlane() == playerLoc.getPlane()) { int stickyDist = sticky.distanceTo2D(playerLoc); + recordInterimDistanceProgress(sticky, playerLoc, nowMs); if (stickyDist <= INTERIM_CLOSE_TILES || nowMs - interimSetAtMs > INTERIM_MAX_AGE_MS) { interimTargetWp = null; interimTargetIdx = -1; interimSetAtMs = 0L; interimLastProgressAtMs = 0L; interimLastBestPathIdx = -1; + interimLastDistanceToTarget = Integer.MAX_VALUE; interimLastRetargetAtMs = 0L; } else { // U-turn safe progress: track progress along the path index, not Euclidean // distance-to-target (which can increase on U-shaped routes). - int bestIdxNow = getClosestTileIndex(path); + int bestIdxNow = getClosestTileIndex(path, playerLoc); if (bestIdxNow > interimLastBestPathIdx) { interimLastBestPathIdx = bestIdxNow; interimLastProgressAtMs = nowMs; @@ -1969,10 +2084,11 @@ && walkFastCanvas(recoverTarget)) { WorldPoint posBefore = playerLoc; WorldPoint clickTarget = inInstance ? targetWp : getPointWithWallDistance(targetWp); - if (!inInstance && !Rs2Tile.isTileReachable(clickTarget)) { + if (!inInstance) { + int rawAnchorIndex = rawIndexForSmoothedIndex(i, smoothedToRaw, rawPath); WorldPoint rawReachableTarget = findFurthestReachableRawPathPoint(rawPath, playerLoc, - MINIMAP_REACH_EUCLIDEAN - 1); - if (rawReachableTarget != null) { + MINIMAP_REACH_EUCLIDEAN - 1, rawAnchorIndex); + if (!Rs2Tile.isTileReachable(clickTarget) && rawReachableTarget != null) { targetWp = rawReachableTarget; clickTarget = rawReachableTarget; } @@ -1984,6 +2100,7 @@ && walkFastCanvas(recoverTarget)) { exitReason = "door-handled-before-minimap-click"; break; } + clickTarget = clampToEuclideanRadius(playerLoc, clickTarget, MINIMAP_REACH_EUCLIDEAN - 1); long nowBeforeClick = System.currentTimeMillis(); if (lastTransportHandledAtMs > 0 && nowBeforeClick - lastTransportHandledAtMs <= POST_TRANSPORT_PATH_TMARK_WINDOW_MS) { @@ -1994,27 +2111,25 @@ && walkFastCanvas(recoverTarget)) { "to=" + compactWorldPoint(clickTarget)); } markStartupPhase("click_candidate_found", target, "to=" + compactWorldPoint(clickTarget)); - boolean clicked = Rs2Walker.walkMiniMap(clickTarget); - if (!clicked) { - clicked = walkRawPathMiniMapToward(rawPath, clickTarget, playerLoc, MINIMAP_REACH_EUCLIDEAN - 1); - if (!clicked && (rawPath == null || rawPath.isEmpty())) { - clicked = walkMiniMapToward(clickTarget, playerLoc, MINIMAP_REACH_EUCLIDEAN - 1); - } - } + int rawAnchorIndex = rawIndexForSmoothedIndex(i, smoothedToRaw, rawPath); + WorldPoint clickedTarget = clickMiniMapOrFallback(rawPath, clickTarget, playerLoc, + MINIMAP_REACH_EUCLIDEAN - 1, rawPath == null || rawPath.isEmpty(), rawAnchorIndex); + boolean clicked = clickedTarget != null; if (walkCancelledDiag(target, "processWalk:after-minimap-click", processWalkTail)) { return WalkerState.EXIT; } - lastAttemptedMinimapClick = targetWp; + lastAttemptedMinimapClick = clicked ? clickedTarget : clickTarget; lastAttemptedMinimapClickOk = clicked; lastAttemptedMinimapClickAtMs = nowMs; if (clicked) { markFirstMovementClick("first_minimap_click", target, posBefore, - "to=" + compactWorldPoint(clickTarget)); - interimTargetWp = targetWp; + "to=" + compactWorldPoint(clickedTarget)); + interimTargetWp = clickedTarget; interimTargetIdx = targetIdx; interimSetAtMs = nowMs; interimLastProgressAtMs = nowMs; - interimLastBestPathIdx = getClosestTileIndex(path); + interimLastBestPathIdx = getClosestTileIndex(path, posBefore); + interimLastDistanceToTarget = distanceToInterimOrMax(clickedTarget, posBefore); interimLastRetargetAtMs = nowMs; final WorldPoint b = targetWp; @@ -2037,8 +2152,23 @@ && walkFastCanvas(recoverTarget)) { return before.distanceTo2D(now) >= progressCap; }, 1200); WorldPoint afterClickWait = Rs2Player.getWorldLocation(); + boolean routeRetryClicked = false; + if (afterClickWait != null && afterClickWait.equals(before) && !Rs2Player.isMoving()) { + WorldPoint routeRetryTarget = clickMiniMapOrFallback(rawPath, b, before, + MINIMAP_REACH_EUCLIDEAN - 1, false, rawAnchorIndex); + if (routeRetryTarget != null) { + routeRetryClicked = true; + lastAttemptedMinimapClick = routeRetryTarget; + lastAttemptedMinimapClickOk = true; + lastAttemptedMinimapClickAtMs = System.currentTimeMillis(); + interimTargetWp = routeRetryTarget; + interimLastProgressAtMs = lastAttemptedMinimapClickAtMs; + interimLastDistanceToTarget = distanceToInterimOrMax(routeRetryTarget, before); + interimLastRetargetAtMs = lastAttemptedMinimapClickAtMs; + } + } if (afterClickWait != null && afterClickWait.equals(before) && !Rs2Player.isMoving() - && walkReachableMiniMapToward(b, before, MINIMAP_REACH_EUCLIDEAN - 1)) { + && routeRetryClicked) { sleepUntil(() -> { if (isWalkCancelled(target)) return true; WorldPoint now = Rs2Player.getWorldLocation(); @@ -2079,6 +2209,7 @@ && walkReachableMiniMapToward(b, before, MINIMAP_REACH_EUCLIDEAN - 1)) { interimSetAtMs = 0L; interimLastProgressAtMs = 0L; interimLastBestPathIdx = -1; + interimLastDistanceToTarget = Integer.MAX_VALUE; interimLastRetargetAtMs = 0L; sleepUntil(() -> isWalkCancelled(target) || !Rs2Player.isMoving(), 1200); if (walkCancelledDiag(target, "processWalk:after-click-failed-wait", processWalkTail)) { @@ -2124,8 +2255,8 @@ && walkReachableMiniMapToward(b, before, MINIMAP_REACH_EUCLIDEAN - 1)) { } // Only do the final-tile canvas click if we iterated the whole path cleanly. - // Exiting because the player left the path ("off-path-but-moving"/"not-near-path") - // means the player is still walking somewhere else — don't clobber that destination. + // Exiting because the player left the path may still mean movement is active. + // so don't clobber that destination. if (!doorOrTransportResult && "end-of-path".equals(exitReason)) { if (walkCancelledDiag(target, "processWalk:before-final-canvas", processWalkTail)) { return WalkerState.EXIT; @@ -2148,7 +2279,16 @@ && walkReachableMiniMapToward(b, before, MINIMAP_REACH_EUCLIDEAN - 1)) { if (Rs2Tile.isTileReachable(finalTile) && Rs2Player.getWorldLocation().distanceTo(finalTile) >= finishTh) { final WorldPoint canvasClickWp = finalTile; - if (Rs2Walker.walkFastCanvas(canvasClickWp)) { + WorldPoint finalPlayerLoc = Rs2Player.getWorldLocation(); + boolean finalClick; + if (rawPath != null && !rawPath.isEmpty() && finalPlayerLoc != null) { + int rawAnchorIndex = rawAnchorIndexForPathPosition(rawPath, path, finalPlayerLoc); + finalClick = clickRouteBackedShortWalk(rawPath, canvasClickWp, finalPlayerLoc, + NORMAL_MINIMAP_REACH_EUCLIDEAN - 1, rawAnchorIndex); + } else { + finalClick = Rs2Walker.walkFastCanvas(canvasClickWp); + } + if (finalClick) { waitUntilIdleAfterSceneWalk(target, POST_SCENE_WALK_IDLE_WAIT_MS_MAX, target, finishTh); if (walkCancelledDiag(target, "processWalk:after-final-canvas-wait", processWalkTail)) { return WalkerState.EXIT; @@ -2180,10 +2320,16 @@ && walkReachableMiniMapToward(b, before, MINIMAP_REACH_EUCLIDEAN - 1)) { setTarget(null, "rs2walker:processWalk:partial-retries-exhausted"); return WalkerState.UNREACHABLE; } else { - if ("off-path-but-moving".equals(exitReason)) { - // Wait for the player to re-enter the path or to stop moving. Prevents a tight - // recursion loop that would spin on isNearPath() while the player is walking. - long offPathWaitMs = 2000L; + if (isOffPathRecalcDeferredExit(exitReason)) { + // Wait briefly for the player to re-enter the path or for the progress signal + // that deferred the recalc to expire. Prevents a tight loop around isNearPath(). + String deferReason = offPathDeferredReasonFromExit(exitReason); + long offPathWaitMs = offPathRecalcDeferredWaitMs(deferReason, + System.currentTimeMillis(), + lastMovedTimeMs, + routeProgressAdvancedAtMs, + lastAttemptedMinimapClickAtMs, + interimLastProgressAtMs); long now = System.currentTimeMillis(); if (lastTransportHandledAtMs > 0 && now - lastTransportHandledAtMs <= POST_TRANSPORT_PATH_TMARK_WINDOW_MS) { @@ -2196,6 +2342,7 @@ && walkReachableMiniMapToward(b, before, MINIMAP_REACH_EUCLIDEAN - 1)) { } } if (offPathWaitMs > 0) { + final long deferredLastClickAtMs = lastAttemptedMinimapClickAtMs; if (lastTransportHandledAtMs > 0 && System.currentTimeMillis() - lastTransportHandledAtMs <= POST_TRANSPORT_PATH_TMARK_WINDOW_MS) { WebWalkLog.tmark("post_transport_offpath_sleep", @@ -2204,7 +2351,9 @@ && walkReachableMiniMapToward(b, before, MINIMAP_REACH_EUCLIDEAN - 1)) { Rs2Player.getWorldLocation(), "ms=" + offPathWaitMs); } - sleepUntil(() -> isWalkCancelled(target) || isNearPath() || !Rs2Player.isMoving(), + sleepUntil(() -> isWalkCancelled(target) + || isNearPath() + || currentOffPathRecalcDeferralReason(deferredLastClickAtMs) == null, (int) offPathWaitMs); } if (walkCancelledDiag(target, "processWalk:after-off-path-wait", processWalkTail)) { @@ -2213,7 +2362,9 @@ && walkReachableMiniMapToward(b, before, MINIMAP_REACH_EUCLIDEAN - 1)) { } // Benign yields: outer for-loop increments processWalkTail each iteration; exempt so // long minimap interim waits cannot exhaust MAX_PROCESS_WALK_TAIL_ITERATIONS and EXIT. - if ("interim-in-flight".equals(exitReason) || "off-path-but-moving".equals(exitReason)) { + if ("interim-in-flight".equals(exitReason) + || "recovery-move-in-flight".equals(exitReason) + || isOffPathRecalcDeferredExit(exitReason)) { walkerDiag("tail exempt exitReason=%s tailBefore=%d", exitReason, processWalkTail); processWalkTail--; } @@ -2488,15 +2639,55 @@ private static boolean walkRawPathMiniMapToward(List rawPath, WorldPoint target, WorldPoint playerLoc, int maxEuclidean) { - WorldPoint fallback = findFurthestVisibleReachableRawPathPoint(rawPath, playerLoc, maxEuclidean); + return walkRawPathMiniMapTargetToward(rawPath, target, playerLoc, maxEuclidean, -1) != null; + } + + private static WorldPoint clickMiniMapOrFallback(List rawPath, + WorldPoint target, + WorldPoint playerLoc, + int maxEuclidean, + boolean allowDirectionalFallback) { + return clickMiniMapOrFallback(rawPath, target, playerLoc, maxEuclidean, allowDirectionalFallback, -1); + } + + private static WorldPoint clickMiniMapOrFallback(List rawPath, + WorldPoint target, + WorldPoint playerLoc, + int maxEuclidean, + boolean allowDirectionalFallback, + int rawAnchorIndex) { + if (target == null || playerLoc == null || target.equals(playerLoc)) { + return null; + } + if (walkMiniMap(target)) { + return target; + } + WorldPoint rawFallback = walkRawPathMiniMapTargetToward(rawPath, target, playerLoc, + maxEuclidean, rawAnchorIndex); + if (rawFallback != null) { + return rawFallback; + } + if (allowDirectionalFallback && walkMiniMapToward(target, playerLoc, maxEuclidean)) { + return target; + } + return null; + } + + private static WorldPoint walkRawPathMiniMapTargetToward(List rawPath, + WorldPoint target, + WorldPoint playerLoc, + int maxEuclidean, + int rawAnchorIndex) { + WorldPoint fallback = findFurthestVisibleReachableRawPathPoint(rawPath, playerLoc, + maxEuclidean, rawAnchorIndex); if (fallback == null || fallback.equals(playerLoc) || fallback.equals(target)) { - return false; + return null; } if (walkMiniMap(fallback)) { log.info("[Walker] Minimap click target {} was outside clip; used route fallback {}", target, fallback); - return true; + return fallback; } - return false; + return null; } static boolean walkMiniMapToward(WorldPoint target, WorldPoint playerLoc, int maxEuclidean) { @@ -2559,18 +2750,27 @@ && euclideanSq(tile, target) < currentDistance) .orElse(false); } - private static WorldPoint findFurthestReachableRawPathPoint(List rawPath, - WorldPoint playerLoc, - int maxEuclidean) { + static WorldPoint findFurthestReachableRawPathPoint(List rawPath, + WorldPoint playerLoc, + int maxEuclidean) { + return findFurthestReachableRawPathPoint(rawPath, playerLoc, maxEuclidean, -1); + } + + static WorldPoint findFurthestReachableRawPathPoint(List rawPath, + WorldPoint playerLoc, + int maxEuclidean, + int rawAnchorIndex) { if (rawPath == null || rawPath.isEmpty() || playerLoc == null) { return null; } - int closestRawIndex = getClosestTileIndex(rawPath); + int closestRawIndex = rawPathForwardAnchorIndex(rawPath, playerLoc, rawAnchorIndex); if (closestRawIndex < 0) { return null; } int maxSq = maxEuclidean * maxEuclidean; + int maxRouteSteps = maxEuclidean + 2; + int routeSteps = 0; Set reachable = Rs2Tile.getReachableTilesFromTile(playerLoc, Math.max(2, maxEuclidean)).keySet(); WorldPoint best = null; for (int rawIndex = closestRawIndex; rawIndex < rawPath.size(); rawIndex++) { @@ -2578,6 +2778,13 @@ private static WorldPoint findFurthestReachableRawPathPoint(List raw if (candidate == null || candidate.getPlane() != playerLoc.getPlane()) { break; } + if (rawIndex > closestRawIndex) { + WorldPoint previous = rawPath.get(rawIndex - 1); + routeSteps += rawPathStepDistance(previous, candidate); + if (routeSteps > maxRouteSteps) { + break; + } + } if (euclideanSq(candidate, playerLoc) > maxSq) { break; } @@ -2588,18 +2795,27 @@ private static WorldPoint findFurthestReachableRawPathPoint(List raw return best; } - private static WorldPoint findFurthestVisibleReachableRawPathPoint(List rawPath, - WorldPoint playerLoc, - int maxEuclidean) { + static WorldPoint findFurthestVisibleReachableRawPathPoint(List rawPath, + WorldPoint playerLoc, + int maxEuclidean) { + return findFurthestVisibleReachableRawPathPoint(rawPath, playerLoc, maxEuclidean, -1); + } + + static WorldPoint findFurthestVisibleReachableRawPathPoint(List rawPath, + WorldPoint playerLoc, + int maxEuclidean, + int rawAnchorIndex) { if (rawPath == null || rawPath.isEmpty() || playerLoc == null) { return null; } - int closestRawIndex = getClosestTileIndex(rawPath); + int closestRawIndex = rawPathForwardAnchorIndex(rawPath, playerLoc, rawAnchorIndex); if (closestRawIndex < 0) { return null; } int maxSq = maxEuclidean * maxEuclidean; + int maxRouteSteps = maxEuclidean + 2; + int routeSteps = 0; Set reachable = Rs2Tile.getReachableTilesFromTile(playerLoc, Math.max(2, maxEuclidean)).keySet(); WorldPoint best = null; for (int rawIndex = closestRawIndex; rawIndex < rawPath.size(); rawIndex++) { @@ -2607,6 +2823,13 @@ private static WorldPoint findFurthestVisibleReachableRawPathPoint(List closestRawIndex) { + WorldPoint previous = rawPath.get(rawIndex - 1); + routeSteps += rawPathStepDistance(previous, candidate); + if (routeSteps > maxRouteSteps) { + break; + } + } if (euclideanSq(candidate, playerLoc) > maxSq) { break; } @@ -2619,6 +2842,45 @@ && isMiniMapClickable(candidate, 5)) { return best; } + private static int rawPathStepDistance(WorldPoint previous, WorldPoint current) { + if (previous == null || current == null || previous.getPlane() != current.getPlane()) { + return Integer.MAX_VALUE / 4; + } + return Math.max(1, previous.distanceTo2D(current)); + } + + static int rawPathForwardAnchorIndex(List rawPath, WorldPoint playerLoc, int rawAnchorIndex) { + if (rawPath == null || rawPath.isEmpty() || playerLoc == null) { + return -1; + } + if (rawAnchorIndex < 0) { + return getClosestTileIndex(rawPath, playerLoc); + } + + int start = Math.max(0, Math.min(rawAnchorIndex, rawPath.size() - 1)); + int endExclusive = Math.min(rawPath.size(), start + ROUTE_PROGRESS_FORWARD_SEARCH_TILES + 1); + int bestIdx = -1; + int bestDist = Integer.MAX_VALUE; + for (int i = start; i < endExclusive; i++) { + WorldPoint point = rawPath.get(i); + if (point == null) { + continue; + } + if (point.getPlane() != playerLoc.getPlane()) { + break; + } + int dist = point.distanceTo2D(playerLoc); + if (dist < bestDist) { + bestIdx = i; + bestDist = dist; + if (dist == 0) { + break; + } + } + } + return bestIdx >= 0 ? bestIdx : start; + } + private static boolean shouldIssueActiveRouteIdleNudge() { WorldPoint playerLoc = Rs2Player.getWorldLocation(); long now = System.currentTimeMillis(); @@ -2658,7 +2920,7 @@ private static boolean tryIssueRouteContinuationClick(List rawPath, return false; } if (rawPath != null && !rawPath.isEmpty()) { - int rawIdx = getClosestTileIndex(rawPath); + int rawIdx = getClosestTileIndex(rawPath, playerLoc); if (rawIdx >= 0 && hasUnresolvedDoorLikeObjectNearRawPath(rawPath, rawIdx, playerLoc, @@ -2668,7 +2930,7 @@ private static boolean tryIssueRouteContinuationClick(List rawPath, return false; } } - int pathIdx = Math.max(0, getClosestTileIndex(path)); + int pathIdx = Math.max(0, getClosestTileIndex(path, playerLoc)); if (hasUpcomingNearbyTransportStep(path, pathIdx, playerLoc, POST_TRANSPORT_RAW_SCAN_TRANSPORT_LOOKAHEAD_EDGES, POST_TRANSPORT_RAW_SCAN_TRANSPORT_MAX_DIST)) { @@ -2688,15 +2950,18 @@ private static boolean tryIssueRouteMovementClick(List rawPath, if (playerLoc == null || path == null || path.isEmpty()) { return false; } + int targetIdx = stabilizeRouteProgressIndex(path, getClosestTileIndex(path, playerLoc), target, playerLoc); + int[] routeSmoothedToRaw = mapSmoothedToRaw(path, rawPath); + int rawAnchorIndex = rawIndexForSmoothedIndex(targetIdx, routeSmoothedToRaw, rawPath); WorldPoint clickTarget = findFurthestReachableRawPathPoint( rawPath, playerLoc, - maxEuclidean); - int targetIdx = getClosestTileIndex(path); + maxEuclidean, + rawAnchorIndex); if (clickTarget == null) { int startIdx = Math.max(0, targetIdx); - int clickableIdx = findFurthestClickableIndex(path, startIdx, playerLoc, + int clickableIdx = findFurthestForwardClickableIndex(path, startIdx, playerLoc, wp -> { Set ts = ShortestPathPlugin.getTransports().get(wp); return ts != null && !ts.isEmpty(); @@ -2718,28 +2983,34 @@ private static boolean tryIssueRouteMovementClick(List rawPath, } boolean clicked = false; + WorldPoint clickedTarget = null; if (clickTarget != null && !clickTarget.equals(playerLoc)) { - clicked = walkMiniMap(clickTarget) - || walkRawPathMiniMapToward(rawPath, clickTarget, playerLoc, maxEuclidean - 1); - if (!clicked && (rawPath == null || rawPath.isEmpty())) { - clicked = walkMiniMapToward(clickTarget, playerLoc, maxEuclidean - 1); - } + clickTarget = clampToEuclideanRadius(playerLoc, clickTarget, maxEuclidean - 1); + clickedTarget = clickMiniMapOrFallback(rawPath, clickTarget, playerLoc, + maxEuclidean - 1, rawPath == null || rawPath.isEmpty(), rawAnchorIndex); + clicked = clickedTarget != null; + } + if (clicked && "interim close route click".equals(logLabel)) { + log.debug("[Walker] {}: clicked={} to={} player={} idx={}", + logLabel, true, clickedTarget, playerLoc, targetIdx); + } else { + log.info("[Walker] {}: clicked={} to={} player={} idx={}", + logLabel, clicked, clicked ? clickedTarget : clickTarget, playerLoc, targetIdx); } - log.info("[Walker] {}: clicked={} to={} player={} idx={}", - logLabel, clicked, clickTarget, playerLoc, targetIdx); if (!clicked) { return false; } - markFirstMovementClick("stall recovery click".equals(logLabel) ? "stall_recovery_click" : "active_route_idle_nudge", + markFirstMovementClick(routeMovementClickPhase(logLabel), target, playerLoc, - "to=" + compactWorldPoint(clickTarget)); - interimTargetWp = clickTarget; + "to=" + compactWorldPoint(clickedTarget)); + interimTargetWp = clickedTarget; interimTargetIdx = targetIdx; interimSetAtMs = System.currentTimeMillis(); interimLastProgressAtMs = interimSetAtMs; - interimLastBestPathIdx = getClosestTileIndex(path); + interimLastBestPathIdx = getClosestTileIndex(path, playerLoc); + interimLastDistanceToTarget = distanceToInterimOrMax(clickedTarget, playerLoc); interimLastRetargetAtMs = interimSetAtMs; if (markRecoveryCooldown) { lastUnreachableRecoveryClickAtMs = interimSetAtMs; @@ -2755,6 +3026,19 @@ private static boolean tryIssueRouteMovementClick(List rawPath, return true; } + static String routeMovementClickPhase(String logLabel) { + if ("stall recovery click".equals(logLabel)) { + return "stall_recovery_click"; + } + if ("active route idle nudge".equals(logLabel)) { + return "active_route_idle_nudge"; + } + if ("interim close route click".equals(logLabel)) { + return "interim_close_route_click"; + } + return "route_movement_click"; + } + /** * Used in instances like vorkath, jad, nmz * @@ -2782,6 +3066,46 @@ public static boolean walkFastCanvas(WorldPoint worldPoint) { return walkFastCanvas(worldPoint, true); } + private static boolean walkFastCanvasOnScreenOnly(WorldPoint worldPoint, boolean toggleRun) { + LocalPoint localPoint = localPointForWorld(worldPoint); + if (localPoint == null || !Rs2Camera.isTileOnScreen(localPoint)) { + return false; + } + Point canvasPoint = Perspective.localToCanvas( + Microbot.getClient(), + localPoint, + Microbot.getClient().getTopLevelWorldView().getPlane()); + int canvasX = canvasPoint != null ? canvasPoint.getX() : -1; + int canvasY = canvasPoint != null ? canvasPoint.getY() : -1; + if (canvasX < 0 || canvasY < 0) { + return false; + } + + Rs2Player.toggleRunEnergy(toggleRun); + NewMenuEntry entry = new NewMenuEntry() + .param0(canvasX) + .param1(canvasY) + .type(MenuAction.WALK) + .identifier(0) + .itemId(0) + .option("Walk here"); + + Microbot.doInvoke(entry, + new Rectangle(canvasX, canvasY, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight())); + return true; + } + + private static LocalPoint localPointForWorld(WorldPoint worldPoint) { + if (worldPoint == null) { + return null; + } + LocalPoint localPoint = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), worldPoint); + if (Microbot.getClient().getTopLevelWorldView().isInstance() && localPoint == null) { + localPoint = Rs2LocalPoint.fromWorldInstance(worldPoint); + } + return localPoint; + } + public static boolean walkFastCanvas(WorldPoint worldPoint, boolean toggleRun) { if (worldPoint == null) { log.debug("[Walker] walkFastCanvas rejected: null worldPoint"); @@ -3086,32 +3410,38 @@ public static List getTransportsForPath(List path, int in */ public static List getTransportsForPath(List path, int indexOfStartPoint, TransportType prefTransportType, boolean applyFiltering) { List transportList = new ArrayList<>(); + if (path == null || path.isEmpty() || indexOfStartPoint < 0 || indexOfStartPoint >= path.size()) { + return transportList; + } + Map pathFirstIndex = buildPathFirstIndex(path); + WorldPoint playerLoc = Rs2Player.getWorldLocation(); int currentIndex = indexOfStartPoint; // Loop through the path until the end while (currentIndex < path.size()) { WorldPoint currentPoint = path.get(currentIndex); // Get any transports that start at this point (or keyed by this point) - Set transportsAtPoint = ShortestPathPlugin.getTransports() - .getOrDefault(currentPoint, new HashSet<>()); + Set transportsAtPoint = ShortestPathPlugin.getTransports().get(currentPoint); + if (transportsAtPoint == null || transportsAtPoint.isEmpty()) { + currentIndex++; + continue; + } boolean foundTransport = false; // sort by type to prioritize teleportation items first, then other types - transportsAtPoint = transportsAtPoint.stream() - .sorted(Comparator.comparing(Transport::getType, (type1, type2) -> { - // sort teleportation items by preference transport type for the current path point. - - if (type1 == prefTransportType && type2 != prefTransportType) { - return -1; - } - if (type2 == prefTransportType && type1 != prefTransportType) { - return 1; - } - // For all other types, use natural enum ordering - return type1.compareTo(type2); - })) - .collect(Collectors.toCollection(LinkedHashSet::new)); + List orderedTransports = new ArrayList<>(transportsAtPoint); + orderedTransports.sort(Comparator.comparing(Transport::getType, (type1, type2) -> { + // sort teleportation items by preference transport type for the current path point. + if (type1 == prefTransportType && type2 != prefTransportType) { + return -1; + } + if (type2 == prefTransportType && type1 != prefTransportType) { + return 1; + } + // For all other types, use natural enum ordering + return type1.compareTo(type2); + })); // Iterate over each available transport - for (Transport transport : transportsAtPoint) { + for (Transport transport : orderedTransports) { // Special handling for teleportation-like transports (originless) // NOTE: Leagues "Area" teleports are injected as SEASONAL_TRANSPORT with null origin. @@ -3125,8 +3455,8 @@ public static List getTransportsForPath(List path, int in || isLeaguesAreaTeleport) { // For teleportation, we assume origin is null and simply check if the destination exists in the path. - int destIndex = path.indexOf(transport.getDestination()); - if (destIndex != -1) { + Integer destIndex = pathFirstIndex.get(transport.getDestination()); + if (destIndex != null) { transportList.add(transport); // Advance the current index to the destination tile (or at least one forward) currentIndex = destIndex > currentIndex ? destIndex : currentIndex + 1; @@ -3146,19 +3476,20 @@ public static List getTransportsForPath(List path, int in for (WorldPoint origin : originPoints) { // If an origin is defined but the player's plane doesn't match, skip it. - WorldPoint plTransportFilter = Rs2Player.getWorldLocation(); - if (transport.getOrigin() != null && plTransportFilter != null - && plTransportFilter.getPlane() != transport.getOrigin().getPlane()) { + if (transport.getOrigin() != null && playerLoc != null + && playerLoc.getPlane() != transport.getOrigin().getPlane()) { continue; } // For non-teleportation transports, ensure both origin and destination exist in the path // and that the destination comes after the origin. - int indexOfDestination = path.indexOf(transport.getDestination()); + Integer indexOfDestinationValue = pathFirstIndex.get(transport.getDestination()); + int indexOfDestination = indexOfDestinationValue != null ? indexOfDestinationValue : -1; if (transport.getType() != TransportType.TELEPORTATION_ITEM && transport.getType() != TransportType.TELEPORTATION_SPELL && !isLeaguesAreaTeleport) { - int indexOfOrigin = path.indexOf(transport.getOrigin()); + Integer indexOfOriginValue = pathFirstIndex.get(transport.getOrigin()); + int indexOfOrigin = indexOfOriginValue != null ? indexOfOriginValue : -1; if (indexOfOrigin == -1 || indexOfDestination == -1 || indexOfDestination < indexOfOrigin) { continue; } @@ -3192,6 +3523,14 @@ public static List getTransportsForPath(List path, int in return transportList; } + private static Map buildPathFirstIndex(List path) { + Map pathFirstIndex = new HashMap<>(path.size()); + for (int i = 0; i < path.size(); i++) { + pathFirstIndex.putIfAbsent(path.get(i), i); + } + return pathFirstIndex; + } + /** * Applies transport filtering and requirement setup for transport items. * This method filters transports to only include those that require items and @@ -3347,7 +3686,6 @@ private static WalkerState tryDirectShortWalk(WorldPoint target, if (!inInstance && localRouteDetoursFromComputedRoute(rawPath, end, directClickMaxDistance)) { return WalkerState.MOVING; } - long suppressUntil = suppressTryDirectShortWalkUntilMs; if (suppressUntil != 0L && System.currentTimeMillis() < suppressUntil) { log.debug("[Walker] defer tryDirectShortWalk minimap (post door canvas nudge, {}ms window)", @@ -3355,12 +3693,20 @@ private static WalkerState tryDirectShortWalk(WorldPoint target, return WalkerState.MOVING; } - boolean clicked = walkMiniMap(end); - if (!clicked) { - clicked = walkMiniMapToward(end, playerLoc, directClickMaxDistance - 1); - } - if (!clicked) { - clicked = walkFastCanvas(end); + boolean routeBacked = rawPath != null && !rawPath.isEmpty(); + int rawAnchorIndex = routeBacked ? rawAnchorIndexForPathPosition(rawPath, path, playerLoc) : -1; + boolean clicked; + if (routeBacked) { + clicked = clickRouteBackedShortWalk(rawPath, end, playerLoc, + directClickMaxDistance - 1, rawAnchorIndex); + } else { + clicked = walkMiniMap(end); + if (!clicked) { + clicked = walkMiniMapToward(end, playerLoc, directClickMaxDistance - 1); + } + if (!clicked) { + clicked = walkFastCanvas(end); + } } if (!clicked) { return WalkerState.MOVING; @@ -3373,7 +3719,14 @@ private static WalkerState tryDirectShortWalk(WorldPoint target, }, 800); if (!moved) { - clicked = walkFastCanvas(end); + WorldPoint retryPlayerLoc = Rs2Player.getWorldLocation(); + if (routeBacked && retryPlayerLoc != null) { + int retryRawAnchorIndex = rawPathForwardAnchorIndex(rawPath, retryPlayerLoc, rawAnchorIndex); + clicked = clickRouteBackedShortWalk(rawPath, end, retryPlayerLoc, + directClickMaxDistance - 1, retryRawAnchorIndex); + } else { + clicked = walkFastCanvas(end); + } if (!clicked) { return WalkerState.MOVING; } @@ -3403,6 +3756,28 @@ private static WalkerState tryDirectShortWalk(WorldPoint target, return WalkerState.MOVING; } + private static int rawAnchorIndexForPathPosition(List rawPath, + List path, + WorldPoint playerLoc) { + int closestPathIdx = getClosestTileIndex(path, playerLoc); + int[] smoothedToRaw = mapSmoothedToRaw(path, rawPath); + int rawAnchorIndex = rawIndexForSmoothedIndex(closestPathIdx, smoothedToRaw, rawPath); + return rawPathForwardAnchorIndex(rawPath, playerLoc, rawAnchorIndex); + } + + private static boolean clickRouteBackedShortWalk(List rawPath, + WorldPoint end, + WorldPoint playerLoc, + int maxEuclidean, + int rawAnchorIndex) { + WorldPoint clickedTarget = clickMiniMapOrFallback(rawPath, end, playerLoc, + maxEuclidean, false, rawAnchorIndex); + if (clickedTarget != null) { + return true; + } + return walkFastCanvasOnScreenOnly(end, true); + } + private static boolean hasPendingExplicitTransportStepBeforeArrival(List path, WorldPoint target, int distance) { @@ -3441,7 +3816,7 @@ private static boolean localRouteDetoursFromComputedRoute(List rawPa return false; } - int rawStart = getClosestTileIndex(rawPath); + int rawStart = getClosestTileIndex(rawPath, playerLoc); if (rawStart < 0 || rawStart >= rawPath.size() - 1) { return false; } @@ -3481,7 +3856,7 @@ private static boolean hasPendingDoorLikeSceneObjectBeforeDirectClick(List= route.size()) { return false; } @@ -3524,6 +3899,9 @@ private static boolean handlePendingDoorBeforeRouteClick(List rawPat || playerLoc == null || targetPathIdx < fromPathIdx) { return false; } + if (Rs2Player.isMoving()) { + return false; + } int rawStart = rawIndexForSmoothedIndex(fromPathIdx, smoothedToRaw, rawPath); int rawTarget = rawIndexForSmoothedIndex(targetPathIdx, smoothedToRaw, rawPath); @@ -3564,7 +3942,7 @@ private static boolean handlePendingDoorDuringInterim(List rawPath, WorldPoint playerLoc) { if (rawPath == null || rawPath.size() < 2 || playerLoc == null || isDoorInteractionSettling() || isDoorEdgePassSkipCoolingDown() - || isRecoveryMovementInFlight()) { + || isRecoveryMovementInFlight() || Rs2Player.isMoving()) { return false; } @@ -3580,8 +3958,11 @@ private static boolean handlePendingDoorNearRawPath(List rawPath, if (rawPath == null || rawPath.size() < 2 || playerLoc == null) { return false; } + if (Rs2Player.isMoving()) { + return false; + } - int rawStart = getClosestTileIndex(rawPath); + int rawStart = getClosestTileIndex(rawPath, playerLoc); if (rawStart < 0) { return false; } @@ -3613,6 +3994,45 @@ private static boolean handlePendingDoorNearRawPath(List rawPath, return false; } + private static boolean handleUnresolvedDoorNearRawPath(List rawPath, + int rawEdgeStart, + long timeoutMs, + Map attempted, + WorldPoint playerLoc, + int backtrackEdges, + int lookaheadEdges, + int radiusTiles) { + if (rawPath == null || rawPath.size() < 2 || rawEdgeStart < 0 || playerLoc == null || Rs2Player.isMoving()) { + return false; + } + + int start = Math.max(0, rawEdgeStart - Math.max(0, backtrackEdges)); + int endExclusive = Math.min(rawPath.size() - 1, rawEdgeStart + Math.max(1, lookaheadEdges)); + for (int ri = start; ri < endExclusive && ri < rawPath.size() - 1; ri++) { + WorldPoint from = rawPath.get(ri); + WorldPoint to = rawPath.get(ri + 1); + if (from == null || to == null) { + continue; + } + if (from.getPlane() != playerLoc.getPlane() || to.getPlane() != playerLoc.getPlane()) { + break; + } + if (from.distanceTo2D(playerLoc) > radiusTiles && to.distanceTo2D(playerLoc) > radiusTiles) { + continue; + } + if (isCatalogBackedTransportSegment(rawPath, ri) && !isDoorLikeCatalogTransportSegment(rawPath, ri)) { + continue; + } + if (!hasUnresolvedDoorLikeSceneObjectOnSegment(from, to, playerLoc, radiusTiles)) { + continue; + } + if (handleDoorsWithTimeout(rawPath, ri, timeoutMs, attempted, true)) { + return true; + } + } + return false; + } + private static int rawIndexForSmoothedIndex(int smoothedIdx, int[] smoothedToRaw, List rawPath) { if (rawPath == null || rawPath.isEmpty()) { return -1; @@ -3654,7 +4074,7 @@ private static boolean handleNearbyRawPathSceneObjects(List rawPath, return false; } - int rawStart = getClosestTileIndex(rawPath); + int rawStart = getClosestTileIndex(rawPath, playerLoc); if (rawStart < 0) { clearRawScanDoorFocus("raw-start-missing"); return false; @@ -3878,10 +4298,19 @@ private static void addForwardPathIndices(Map forwardIndex, return; } - int closestIndex = IntStream.range(0, path.size()) - .boxed() - .min(Comparator.comparingInt(i -> playerLoc.distanceTo(path.get(i)))) - .orElse(0); + int closestIndex = 0; + int closestDistance = Integer.MAX_VALUE; + for (int i = 0; i < path.size(); i++) { + WorldPoint point = path.get(i); + if (point == null) { + continue; + } + int distance = playerLoc.distanceTo(point); + if (distance < closestDistance) { + closestDistance = distance; + closestIndex = i; + } + } for (int i = closestIndex; i < path.size(); i++) { forwardIndex.putIfAbsent(path.get(i), i); } @@ -3895,6 +4324,9 @@ private static void addForwardPathIndices(Map forwardIndex, private static final long STATIONARY_DOOR_SUPPRESS_MS = 10_000; private static final Map recentDoorAttemptByEdge = new ConcurrentHashMap<>(); private static final long DOOR_ATTEMPT_EDGE_COOLDOWN_MS = 2_500; + private static volatile WorldPoint lastDoorAttemptFrom = null; + private static volatile WorldPoint lastDoorAttemptTo = null; + private static volatile long lastDoorAttemptAtMs = 0L; private static final Map recentCurrentTileTransportByEdge = new ConcurrentHashMap<>(); private static final long CURRENT_TILE_TRANSPORT_EDGE_COOLDOWN_MS = 2_200; private static final long DOOR_INTERACTION_GLOBAL_COOLDOWN_MS = 1_800; @@ -3971,6 +4403,25 @@ static int findFurthestClickableIndex(List path, int startIdx, World return bestIdx; } + static int findFurthestForwardClickableIndex(List path, int startIdx, WorldPoint playerLoc, + java.util.function.Predicate isTransportOrigin, + int maxEuclidean) { + if (path == null || startIdx < 0 || startIdx >= path.size()) return startIdx; + WorldPoint startWp = path.get(startIdx); + if (startWp == null) return startIdx; + final int maxSq = maxEuclidean * maxEuclidean; + + int bestIdx = startIdx; + for (int j = startIdx; j < path.size(); j++) { + WorldPoint candidate = path.get(j); + if (candidate == null || candidate.getPlane() != startWp.getPlane()) break; + if (j > startIdx && isTransportOrigin != null && isTransportOrigin.test(candidate)) break; + if (playerLoc != null && euclideanSq(candidate, playerLoc) > maxSq) break; + bestIdx = j; + } + return bestIdx; + } + private static int findForwardReachableRecoveryIndex(List path, int startIdx, WorldPoint playerLoc, @@ -4064,6 +4515,47 @@ static WorldPoint interpolateClickableTarget(List path, return fallbackWp; } + static WorldPoint clampToEuclideanRadius(WorldPoint playerLoc, WorldPoint target, int maxEuclidean) { + if (playerLoc == null || target == null || target.getPlane() != playerLoc.getPlane() || maxEuclidean <= 0) { + return target; + } + int maxSq = maxEuclidean * maxEuclidean; + if (euclideanSq(playerLoc, target) <= maxSq) { + return target; + } + + int dx = target.getX() - playerLoc.getX(); + int dy = target.getY() - playerLoc.getY(); + double distance = Math.sqrt(dx * dx + dy * dy); + if (distance <= 1) { + return target; + } + + double scale = maxEuclidean / distance; + int stepX = (int) Math.round(dx * scale); + int stepY = (int) Math.round(dy * scale); + if (stepX == 0 && stepY == 0) { + if (Math.abs(dx) >= Math.abs(dy)) { + stepX = Integer.signum(dx); + } else { + stepY = Integer.signum(dy); + } + } + + WorldPoint clamped = new WorldPoint(playerLoc.getX() + stepX, playerLoc.getY() + stepY, playerLoc.getPlane()); + while (!clamped.equals(playerLoc) && euclideanSq(playerLoc, clamped) > maxSq) { + if (Math.abs(stepX) >= Math.abs(stepY) && stepX != 0) { + stepX -= Integer.signum(stepX); + } else if (stepY != 0) { + stepY -= Integer.signum(stepY); + } else { + break; + } + clamped = new WorldPoint(playerLoc.getX() + stepX, playerLoc.getY() + stepY, playerLoc.getPlane()); + } + return clamped.equals(playerLoc) ? target : clamped; + } + private static int euclideanSq(WorldPoint a, WorldPoint b) { int dx = a.getX() - b.getX(); int dy = a.getY() - b.getY(); @@ -4333,6 +4825,11 @@ && isNullOrPlaceholderObjectName(comp.getName())) { compactWorldPoint(probe), compactWorldPoint(fromWp), compactWorldPoint(toWp)); return false; } + if (Rs2Player.isMoving()) { + WebWalkLog.spInfo("door_interact_deferred | reason=moving mode=segment-door probe={} from={} to={}", + compactWorldPoint(probe), compactWorldPoint(fromWp), compactWorldPoint(toWp)); + return false; + } markDoorAttempt(probe, fromWp, toWp); markGlobalDoorInteractionCooldown(); WorldPoint posBefore = Rs2Player.getWorldLocation(); @@ -4477,6 +4974,11 @@ private static boolean tryHandleDoorObject(TileObject object, WorldPoint probe, compactWorldPoint(probe), compactWorldPoint(fromWp), compactWorldPoint(toWp)); return false; } + if (Rs2Player.isMoving()) { + WebWalkLog.spInfo("door_interact_deferred | reason=moving mode=segment-probe probe={} from={} to={}", + compactWorldPoint(probe), compactWorldPoint(fromWp), compactWorldPoint(toWp)); + return false; + } markDoorAttempt(probe, fromWp, toWp); markGlobalDoorInteractionCooldown(); WorldPoint posBefore = Rs2Player.getWorldLocation(); @@ -4760,6 +5262,31 @@ private static boolean tryDoorEdgeCrossNudge(WorldPoint fromWp, WorldPoint toWp, return progressed; } + private static boolean tryRecentDoorAttemptEdgeNudge(WorldPoint playerLoc, WorldPoint target) { + WorldPoint from = lastDoorAttemptFrom; + WorldPoint to = lastDoorAttemptTo; + long attemptedAt = lastDoorAttemptAtMs; + if (playerLoc == null || from == null || to == null || attemptedAt <= 0L) { + return false; + } + long ageMs = System.currentTimeMillis() - attemptedAt; + if (ageMs < 0L || ageMs > POST_DOOR_NUDGE_RECENT_ATTEMPT_MS) { + return false; + } + if (playerLoc.getPlane() != to.getPlane() || playerLoc.distanceTo2D(to) > POST_DOOR_EDGE_NUDGE_MAX_FROM_PLAYER) { + return false; + } + if (Rs2Player.isMoving() || Rs2Player.isAnimating()) { + return false; + } + boolean nudged = tryDoorEdgeCrossNudge(from, to, target); + if (nudged) { + WebWalkLog.tmark("recent_door_edge_nudge", System.currentTimeMillis() - walkSessionStartedAtMs, + target, playerLoc, "from=" + compactWorldPoint(from) + " to=" + compactWorldPoint(to)); + } + return nudged; + } + static boolean isDoorEdgeNudgeResolved(WorldPoint before, WorldPoint after, WorldPoint fromWp, WorldPoint toWp) { if (before == null || after == null || fromWp == null || toWp == null) { return false; @@ -4803,8 +5330,7 @@ static boolean shouldClearInterimTarget(WorldPoint interim, if (playerLoc == null || playerLoc.getPlane() != interim.getPlane()) { return true; } - int preclickTiles = interimPreclickTiles(); - if (playerLoc.distanceTo2D(interim) <= preclickTiles) { + if (playerLoc.distanceTo2D(interim) <= INTERIM_CLOSE_TILES) { return true; } if (lastProgressAtMs > 0L && nowMs - lastProgressAtMs > INTERIM_PROGRESS_TIMEOUT_MS) { @@ -4813,12 +5339,59 @@ static boolean shouldClearInterimTarget(WorldPoint interim, return setAtMs > 0L && nowMs - setAtMs > INTERIM_MAX_AGE_MS; } + static int distanceToInterimOrMax(WorldPoint interim, WorldPoint playerLoc) { + if (interim == null || playerLoc == null || interim.getPlane() != playerLoc.getPlane()) { + return Integer.MAX_VALUE; + } + return playerLoc.distanceTo2D(interim); + } + + private static void recordInterimDistanceProgress(WorldPoint interim, WorldPoint playerLoc, long nowMs) { + int distance = distanceToInterimOrMax(interim, playerLoc); + if (distance < interimLastDistanceToTarget) { + interimLastDistanceToTarget = distance; + interimLastProgressAtMs = nowMs; + } + } + + private static void waitForMovementStartAfterRecovery(WorldPoint cancelGoal, + WorldPoint playerBefore, + WorldPoint interimGoal, + WorldPoint arrivalGoal, + int arrivalMaxChebyshev) { + if (cancelGoal == null || playerBefore == null) { + return; + } + sleepUntil(() -> { + if (isWalkCancelled(cancelGoal)) { + return true; + } + WorldPoint playerNow = Rs2Player.getWorldLocation(); + if (playerNow == null) { + return false; + } + if (!playerNow.equals(playerBefore) || Rs2Player.isMoving()) { + return true; + } + if (interimGoal != null + && interimGoal.getPlane() == playerNow.getPlane() + && playerNow.distanceTo2D(interimGoal) <= INTERIM_CLOSE_TILES) { + return true; + } + return arrivalGoal != null + && arrivalMaxChebyshev >= 0 + && arrivalGoal.getPlane() == playerNow.getPlane() + && playerNow.distanceTo2D(arrivalGoal) <= arrivalMaxChebyshev; + }, POST_RECOVERY_MOVEMENT_START_WAIT_MS); + } + private static boolean clearInterimTargetIfReachedOrExpired(WorldPoint playerLoc, List path, long nowMs) { WorldPoint interim = interimTargetWp; + recordInterimDistanceProgress(interim, playerLoc, nowMs); if (interim != null && path != null && !path.isEmpty()) { - int bestIdxNow = getClosestTileIndex(path); + int bestIdxNow = getClosestTileIndex(path, playerLoc); if (bestIdxNow > interimLastBestPathIdx) { interimLastBestPathIdx = bestIdxNow; interimLastProgressAtMs = nowMs; @@ -4830,7 +5403,7 @@ private static boolean clearInterimTargetIfReachedOrExpired(WorldPoint playerLoc String reason; if (playerLoc == null || interim == null || playerLoc.getPlane() != interim.getPlane()) { reason = "invalid"; - } else if (playerLoc.distanceTo2D(interim) <= interimPreclickTiles()) { + } else if (playerLoc.distanceTo2D(interim) <= INTERIM_CLOSE_TILES) { reason = "close"; } else if (interimLastProgressAtMs > 0L && nowMs - interimLastProgressAtMs > INTERIM_PROGRESS_TIMEOUT_MS) { reason = "stale-progress"; @@ -4841,16 +5414,72 @@ private static boolean clearInterimTargetIfReachedOrExpired(WorldPoint playerLoc return true; } + private static boolean shouldYieldForActiveRecoveryInterim(WorldPoint playerLoc, + List path, + long nowMs) { + WorldPoint interim = interimTargetWp; + if (interim == null) { + return false; + } + recordInterimDistanceProgress(interim, playerLoc, nowMs); + if (playerLoc != null && path != null && !path.isEmpty()) { + int bestIdxNow = getClosestTileIndex(path, playerLoc); + if (bestIdxNow > interimLastBestPathIdx) { + interimLastBestPathIdx = bestIdxNow; + interimLastProgressAtMs = nowMs; + } + } + return shouldYieldForActiveRecoveryInterim(interim, + playerLoc, + interimSetAtMs, + interimLastProgressAtMs, + nowMs, + lastMovedTimeMs, + lastUnreachableRecoveryClickAtMs, + Rs2Player.isMoving()); + } + + static boolean shouldYieldForActiveRecoveryInterim(WorldPoint interim, + WorldPoint playerLoc, + long setAtMs, + long lastProgressAtMs, + long nowMs, + long lastMovedAtMs, + long lastRecoveryClickAtMs, + boolean playerMoving) { + if (interim == null) { + return false; + } + if (shouldClearInterimTarget(interim, playerLoc, setAtMs, lastProgressAtMs, nowMs)) { + return false; + } + if (playerMoving) { + return true; + } + if (isRecentEvent(nowMs, lastProgressAtMs, INTERIM_PROGRESS_TIMEOUT_MS)) { + return true; + } + if (isRecentEvent(nowMs, lastMovedAtMs, RECOVERY_MOVEMENT_IN_FLIGHT_MS)) { + return true; + } + return isRecentEvent(nowMs, lastRecoveryClickAtMs, RECOVERY_MOVEMENT_IN_FLIGHT_MS); + } + private static void clearInterimTarget(String reason) { WorldPoint old = interimTargetWp; if (old != null) { - WebWalkLog.spInfo("interim_clear | reason={} interim={}", reason, compactWorldPoint(old)); + if ("close".equals(reason)) { + WebWalkLog.spDebug("interim_clear | reason={} interim={}", reason, compactWorldPoint(old)); + } else { + WebWalkLog.spInfo("interim_clear | reason={} interim={}", reason, compactWorldPoint(old)); + } } interimTargetWp = null; interimTargetIdx = -1; interimSetAtMs = 0L; interimLastProgressAtMs = 0L; interimLastBestPathIdx = -1; + interimLastDistanceToTarget = Integer.MAX_VALUE; interimLastRetargetAtMs = 0L; } @@ -4899,6 +5528,11 @@ private static void markGlobalDoorInteractionCooldown() { private static void markDoorAttempt(WorldPoint doorTile, WorldPoint fromWp, WorldPoint toWp) { Rs2DoorHandler.markDoorAttempt(recentDoorAttemptByEdge, doorTile, fromWp, toWp); + if (fromWp != null && toWp != null) { + lastDoorAttemptFrom = fromWp; + lastDoorAttemptTo = toWp; + lastDoorAttemptAtMs = System.currentTimeMillis(); + } } private static boolean shouldThrottleCurrentTileTransportAttempt(WorldPoint fromWp, WorldPoint toWp) { @@ -5190,6 +5824,9 @@ static boolean shouldBlacklistDoorAfterWrongTraversal(WorldPoint start, WorldPoi if (start.getPlane() != end.getPlane()) { return true; } + if (!startedNearDoorEdge(start, fromWp, toWp)) { + return false; + } int moved = start.distanceTo2D(end); if (moved < 3) { return false; @@ -5207,6 +5844,20 @@ static boolean shouldBlacklistDoorAfterWrongTraversal(WorldPoint start, WorldPoi return endFrom >= startFrom + 2; } + private static boolean startedNearDoorEdge(WorldPoint start, WorldPoint fromWp, WorldPoint toWp) { + if (start == null) { + return false; + } + final int maxDoorStartDistance = 3; + boolean nearFrom = fromWp != null + && fromWp.getPlane() == start.getPlane() + && start.distanceTo2D(fromWp) <= maxDoorStartDistance; + boolean nearTo = toWp != null + && toWp.getPlane() == start.getPlane() + && start.distanceTo2D(toWp) <= maxDoorStartDistance; + return nearFrom || nearTo; + } + private static boolean movedAcrossInteractedObject(WorldPoint start, WorldPoint end, WorldPoint objectLoc) { int startRelX = Integer.compare(start.getX(), objectLoc.getX()); int endRelX = Integer.compare(end.getX(), objectLoc.getX()); @@ -6474,40 +7125,72 @@ private static boolean searchNeighborPoint(int orientation, WorldPoint point, Wo * @return closest tile index */ public static int getClosestTileIndex(List path) { + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + return getClosestTileIndex(path, playerLoc, getClosestIndexReachableTiles(playerLoc)); + } - var tiles = Rs2Tile.getReachableTilesFromTile(Rs2Player.getWorldLocation(), 20); + static int getClosestTileIndex(List path, WorldPoint playerLoc) { + return getClosestTileIndex(path, playerLoc, getClosestIndexReachableTiles(playerLoc)); + } - /** - * Exception to handle objects that handle long animations or walk - * ignore colission if we did not find a valid tile to walk on - * this is to ensure we stay on the path even if we are on a agility obstacle - */ - if (tiles.keySet().isEmpty()) { - tiles = Rs2Tile.getReachableTilesFromTileIgnoreCollision(Rs2Player.getWorldLocation(), 20); + static int getClosestTileIndex(List path, + WorldPoint playerLoc, + Map reachableTiles) { + if (path == null || path.isEmpty() || playerLoc == null) { + return -1; } - final HashMap _tiles = tiles; - WorldPoint startPoint = path.stream() - .min(Comparator.comparingInt(a -> _tiles.getOrDefault(a, Integer.MAX_VALUE))) - .orElse(null); + int bestReachableIndex = -1; + int bestReachableDistance = Integer.MAX_VALUE; + if (reachableTiles != null && !reachableTiles.isEmpty()) { + for (int i = 0; i < path.size(); i++) { + WorldPoint point = path.get(i); + if (point == null) { + continue; + } + int reachableDistance = reachableTiles.getOrDefault(point, Integer.MAX_VALUE); + if (reachableDistance < bestReachableDistance) { + bestReachableDistance = reachableDistance; + bestReachableIndex = i; + if (reachableDistance == 0) { + break; + } + } + } + } - /** - * Check if the startPoint is null or no matching tile is found - * If either condition is true, proceed to find the closest index in the path list. - */ - if (startPoint == null || _tiles.getOrDefault(startPoint, Integer.MAX_VALUE) == Integer.MAX_VALUE) { - Optional closestIndexOptional = IntStream.range(0, path.size()) - .boxed() - .min(Comparator.comparingInt(i -> Rs2Player.getWorldLocation().distanceTo(path.get(i)))); - if (closestIndexOptional.isPresent()) { - return closestIndexOptional.get(); + if (bestReachableIndex >= 0 && bestReachableDistance != Integer.MAX_VALUE) { + return bestReachableIndex; + } + + int closestIndex = -1; + int closestDistance = Integer.MAX_VALUE; + for (int i = 0; i < path.size(); i++) { + WorldPoint point = path.get(i); + if (point == null) { + continue; + } + int distance = playerLoc.distanceTo(point); + if (distance < closestDistance) { + closestDistance = distance; + closestIndex = i; } } + return closestIndex; + } - return IntStream.range(0, path.size()) - .filter(i -> path.get(i).equals(startPoint)) - .findFirst() - .orElse(-1); + private static HashMap getClosestIndexReachableTiles(WorldPoint playerLoc) { + if (playerLoc == null) { + return new HashMap<>(); + } + HashMap tiles = Rs2Tile.getReachableTilesFromTile(playerLoc, 20); + + // If an animation/shortcut puts the player on a collision-odd tile, keep route progress + // anchored by distance instead of repeatedly recalculating an empty reachable set. + if (tiles.isEmpty()) { + tiles = Rs2Tile.getReachableTilesFromTileIgnoreCollision(playerLoc, 20); + } + return tiles; } static int stabilizeRouteProgressIndex(List path, int closestIdx, WorldPoint target, WorldPoint playerLoc) { @@ -6529,10 +7212,14 @@ static int stabilizeRouteProgressIndex(List path, int closestIdx, Wo routeProgressPathEnd = pathEnd; routeProgressPathSize = path.size(); routeProgressIdx = closestIdx; + routeProgressAdvancedAtMs = System.currentTimeMillis(); return closestIdx; } if (routeProgressIdx < 0 || closestIdx >= routeProgressIdx) { + if (closestIdx > routeProgressIdx) { + recordRouteProgressAdvanced(); + } routeProgressIdx = closestIdx; return closestIdx; } @@ -6541,6 +7228,7 @@ static int stabilizeRouteProgressIndex(List path, int closestIdx, Wo if (forwardIdx >= routeProgressIdx) { if (forwardIdx > routeProgressIdx) { routeProgressIdx = forwardIdx; + recordRouteProgressAdvanced(); } return routeProgressIdx; } @@ -6601,6 +7289,14 @@ private static void resetRouteProgress() { routeProgressPathStart = null; routeProgressPathEnd = null; routeProgressPathSize = -1; + routeProgressAdvancedAtMs = 0L; + } + + private static void recordRouteProgressAdvanced() { + long now = System.currentTimeMillis(); + routeProgressAdvancedAtMs = now; + lastMovedTimeMs = now; + stuckCount = 0; } private static boolean isRecentTransportEdgeWindow() { @@ -7618,7 +8314,8 @@ private static boolean hasPrecomputedContinuationFromTransport(Transport transpo if (walkPath == null || walkPath.size() < 2) { return false; } - int closest = getClosestTileIndex(walkPath); + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + int closest = getClosestTileIndex(walkPath, playerLoc); if (closest < 0) { return false; } @@ -8150,7 +8847,7 @@ private static boolean isNearPathByVariance(List path, WorldPoint pl if (path == null || path.isEmpty() || playerLoc == null) { return false; } - int closestIdx = getClosestTileIndex(path); + int closestIdx = getClosestTileIndex(path, playerLoc); if (closestIdx < 0 || closestIdx >= path.size()) { return false; } @@ -8160,6 +8857,104 @@ private static boolean isNearPathByVariance(List path, WorldPoint pl && closest.distanceTo2D(playerLoc) <= PATH_VARIANCE_TOLERANCE_CHEBYSHEV; } + static String offPathRecalcDeferralReason(boolean playerMoving, + boolean playerAnimating, + boolean playerInteracting, + boolean doorSettling, + boolean transportSettling, + boolean interimActive, + long nowMs, + long lastMovedAtMs, + long routeProgressAtMs, + long minimapClickAtMs, + long interimProgressAtMs) { + if (doorSettling) { + return "door-settling"; + } + if (transportSettling) { + return "transport-settling"; + } + if (playerMoving) { + return "moving"; + } + if (playerAnimating) { + return "animating"; + } + if (playerInteracting) { + return "interacting"; + } + if (isRecentEvent(nowMs, routeProgressAtMs, OFF_PATH_RECALC_ROUTE_PROGRESS_GRACE_MS)) { + return "route-progress"; + } + if (isRecentEvent(nowMs, minimapClickAtMs, OFF_PATH_RECALC_MINIMAP_CLICK_GRACE_MS)) { + return "recent-click"; + } + if (interimActive && isRecentEvent(nowMs, interimProgressAtMs, INTERIM_PROGRESS_TIMEOUT_MS)) { + return "interim-progress"; + } + if (isRecentEvent(nowMs, lastMovedAtMs, OFF_PATH_RECALC_RECENT_MOVEMENT_MS)) { + return "recent-movement"; + } + return null; + } + + private static String currentOffPathRecalcDeferralReason(long minimapClickAtMs) { + return offPathRecalcDeferralReason( + Rs2Player.isMoving(), + Rs2Player.isAnimating(), + Rs2Player.isInteracting(), + isDoorInteractionSettling(), + isTransportInteractionSettling(), + interimTargetWp != null, + System.currentTimeMillis(), + lastMovedTimeMs, + routeProgressAdvancedAtMs, + minimapClickAtMs, + interimLastProgressAtMs); + } + + static int offPathRecalcDeferredWaitMs(String reason, + long nowMs, + long lastMovedAtMs, + long routeProgressAtMs, + long minimapClickAtMs, + long interimProgressAtMs) { + long remainingMs = OFF_PATH_RECALC_DEFER_WAIT_MAX_MS; + if ("route-progress".equals(reason)) { + remainingMs = remainingRecentEventMs(nowMs, routeProgressAtMs, OFF_PATH_RECALC_ROUTE_PROGRESS_GRACE_MS); + } else if ("recent-click".equals(reason)) { + remainingMs = remainingRecentEventMs(nowMs, minimapClickAtMs, OFF_PATH_RECALC_MINIMAP_CLICK_GRACE_MS); + } else if ("interim-progress".equals(reason)) { + remainingMs = remainingRecentEventMs(nowMs, interimProgressAtMs, INTERIM_PROGRESS_TIMEOUT_MS); + } else if ("recent-movement".equals(reason)) { + remainingMs = remainingRecentEventMs(nowMs, lastMovedAtMs, OFF_PATH_RECALC_RECENT_MOVEMENT_MS); + } + return (int) Math.max(OFF_PATH_RECALC_DEFER_WAIT_MIN_MS, + Math.min(OFF_PATH_RECALC_DEFER_WAIT_MAX_MS, remainingMs)); + } + + private static boolean isOffPathRecalcDeferredExit(String exitReason) { + return exitReason != null && exitReason.startsWith("off-path-deferred:"); + } + + private static String offPathDeferredReasonFromExit(String exitReason) { + if (!isOffPathRecalcDeferredExit(exitReason)) { + return ""; + } + return exitReason.substring("off-path-deferred:".length()); + } + + private static boolean isRecentEvent(long nowMs, long eventAtMs, long graceMs) { + return eventAtMs > 0L && nowMs >= eventAtMs && nowMs - eventAtMs < graceMs; + } + + private static long remainingRecentEventMs(long nowMs, long eventAtMs, long graceMs) { + if (!isRecentEvent(nowMs, eventAtMs, graceMs)) { + return OFF_PATH_RECALC_DEFER_WAIT_MIN_MS; + } + return graceMs - (nowMs - eventAtMs); + } + private static boolean hasUpcomingNearbyTransportStep(List path, int startIdx, WorldPoint playerLoc, @@ -8279,6 +9074,11 @@ private static boolean isStuckTooLong() { return false; } + long routeProgressAt = routeProgressAdvancedAtMs; + if (routeProgressAt > 0L && System.currentTimeMillis() - routeProgressAt < ROUTE_PROGRESS_STALL_GRACE_MS) { + return false; + } + return lastMovedTimeMs > 0 && System.currentTimeMillis() - lastMovedTimeMs > stallThresholdMs(); } @@ -8959,33 +9759,160 @@ private static boolean handleCharterShip(Transport transport) { log.info("Charter Ship NPC: " + npcName + " - " + (npc != null ? npc.getId() : "not found")); if (Rs2Npc.canWalkTo(npc, 20) && Rs2Npc.interact(npc, transport.getAction())) { Rs2Player.waitForWalking(); - sleepUntil(() -> Rs2Widget.isWidgetVisible(885, 4)); - List destinationWidgets = Arrays.stream(Rs2Widget.getWidget(885, 4).getDynamicChildren()) - .filter(w -> w.getActions() != null) - .collect(Collectors.toList()); + if (!sleepUntil(() -> Rs2Widget.isWidgetVisible(885, 4), 5000)) { + return false; + } - if (destinationWidgets.isEmpty()) return false; + Widget destinationWidget = findCharterDestinationWidget(transport.getDisplayInfo()); + if (!invokeCharterDestinationWidget(destinationWidget, transport.getDisplayInfo())) { + return false; + } + confirmCharterTravelIfPrompted(); + return true; + } + return false; + } - String destinationText = transport.getDisplayInfo(); + private static Widget findCharterDestinationWidget(String destinationText) { + return Microbot.getClientThread().runOnClientThreadOptional(() -> { + Widget root = Microbot.getClient().getWidget(885, 4); + if (root == null || root.isHidden()) { + return null; + } - Widget destinationWidget = Rs2Widget.findWidget(destinationText, destinationWidgets); - if (destinationWidget == null) return false; + Widget textMatch = findCharterDestinationTextWidget(root, destinationText); + if (textMatch == null) { + return null; + } - boolean isWidgetVisible = Microbot.getClientThread().runOnClientThreadOptional(() -> !destinationWidget.isHidden()).orElse(false); + Widget clickable = findClickableCharterWidget(textMatch, root); + return clickable != null ? clickable : textMatch; + }).orElse(null); + } - NewMenuEntry destinationMenuEntry = new NewMenuEntry() - .option(destinationText) - .target("") - .identifier(1) - .type(MenuAction.CC_OP) - .param0(destinationWidget.getIndex()) - .param1(destinationWidget.getId()) - .forceLeftClick(false); + private static Widget findCharterDestinationTextWidget(Widget widget, String destinationText) { + if (widget == null || widget.isHidden()) { + return null; + } + if (charterWidgetMatchesDestination(widget, destinationText)) { + return widget; + } - Microbot.doInvoke(destinationMenuEntry, new Rectangle(1, 1)); + Widget[] staticChildren = widget.getStaticChildren(); + Widget found = findCharterDestinationTextWidget(staticChildren, destinationText); + if (found != null) { + return found; + } + + Widget[] dynamicChildren = widget.getDynamicChildren(); + found = findCharterDestinationTextWidget(dynamicChildren, destinationText); + if (found != null) { + return found; + } + + return findCharterDestinationTextWidget(widget.getNestedChildren(), destinationText); + } + + private static Widget findCharterDestinationTextWidget(Widget[] widgets, String destinationText) { + if (widgets == null) { + return null; + } + for (Widget widget : widgets) { + Widget found = findCharterDestinationTextWidget(widget, destinationText); + if (found != null) { + return found; + } + } + return null; + } + + private static boolean charterWidgetMatchesDestination(Widget widget, String destinationText) { + String needle = normalizeCharterWidgetText(destinationText); + if (needle.isEmpty()) { + return false; + } + if (normalizeCharterWidgetText(widget.getText()).contains(needle) + || normalizeCharterWidgetText(widget.getName()).contains(needle)) { return true; } - return false; + String[] actions = widget.getActions(); + if (actions == null) { + return false; + } + return Arrays.stream(actions) + .filter(Objects::nonNull) + .map(Rs2Walker::normalizeCharterWidgetText) + .anyMatch(action -> action.contains(needle)); + } + + private static String normalizeCharterWidgetText(String text) { + if (text == null || text.isEmpty()) { + return ""; + } + return Rs2UiHelper.stripTagsToSpace(text) + .trim() + .toLowerCase(Locale.ROOT) + .replaceAll("\\s+", " "); + } + + private static Widget findClickableCharterWidget(Widget widget, Widget root) { + Widget current = widget; + while (current != null) { + if (hasWidgetActions(current)) { + return current; + } + if (current == root) { + return null; + } + current = current.getParent(); + } + return null; + } + + private static boolean hasWidgetActions(Widget widget) { + String[] actions = widget.getActions(); + return actions != null && Arrays.stream(actions).anyMatch(action -> action != null && !action.isEmpty()); + } + + private static boolean invokeCharterDestinationWidget(Widget widget, String destinationText) { + if (widget == null) { + return false; + } + + String option = getFirstWidgetAction(widget); + if (option == null || option.isBlank()) { + option = destinationText; + } + + NewMenuEntry destinationMenuEntry = new NewMenuEntry() + .option(option) + .target("") + .identifier(1) + .type(MenuAction.CC_OP) + .param0(widget.getIndex()) + .param1(widget.getId()) + .forceLeftClick(false); + + Rectangle bounds = widget.getBounds(); + Microbot.doInvoke(destinationMenuEntry, bounds != null ? bounds : Rs2UiHelper.getDefaultRectangle()); + return true; + } + + private static String getFirstWidgetAction(Widget widget) { + String[] actions = widget.getActions(); + if (actions == null) { + return null; + } + return Arrays.stream(actions) + .filter(action -> action != null && !action.isEmpty()) + .findFirst() + .orElse(null); + } + + private static void confirmCharterTravelIfPrompted() { + if (sleepUntil(Rs2Dialogue::hasSelectAnOption, 2000)) { + Rs2Dialogue.clickOption("Yes", true); + } } /** * interact with interfaces like spirit tree etc... diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/WebWalkLog.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/WebWalkLog.java index cec154d812..63d7d7c4d1 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/WebWalkLog.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/WebWalkLog.java @@ -22,7 +22,7 @@ public static void routeClearMissingReason(String threadName) { LOG.warn("[WebWalk] clear | reason= thread={}", threadName); } - /** Cancel / EXIT / traceProcessWalkExit — compact WARN for production diagnosis. */ + /** Cancel / EXIT / traceProcessWalkExit - compact WARN for production diagnosis. */ public static void exitWarn(String reason, boolean nullCurrent, boolean targetMismatch, boolean interrupted, WorldPoint goal, WorldPoint current, int tailIdx, int tailMax, WorldPoint player) { LOG.warn("[WebWalk] exit | r={} nullCur={} mismatch={} intr={} goal={} cur={} tail={}/{} at={}", @@ -33,18 +33,23 @@ public static void exitDetailDebug(String fmt, Object... args) { LOG.debug("[WebWalk] exit_dbg | " + fmt, args); } - /** interim-in-flight and similar yields — DEBUG to avoid tick spam. */ + /** interim-in-flight and similar yields - DEBUG to avoid tick spam. */ public static void yieldDebug(String reason, WorldPoint player, WorldPoint goal, WorldPoint pathEnd, int idxStart, int pathLen) { LOG.debug("[WebWalk] yield | r={} player={} goal={} end={} idx={}/{}", reason, player, goal, pathEnd, idxStart, pathLen); } public static void earlyExit(String reason, WorldPoint player, WorldPoint goal, WorldPoint pathEnd, int idxStart, int pathLen) { + if ("interim-in-flight".equals(reason)) { + LOG.debug("[WebWalk] early_exit | r={} at={} goal={} end={} idx={}/{}", + reason, player, goal, pathEnd, idxStart, pathLen); + return; + } LOG.info("[WebWalk] early_exit | r={} at={} goal={} end={} idx={}/{}", reason, player, goal, pathEnd, idxStart, pathLen); } - /** Path ends far from goal — walking multi-hop segment. */ + /** Path ends far from goal - walking multi-hop segment. */ public static void partialSegment(WorldPoint pathEnd, int distToGoal, WorldPoint goal, int waypointCount) { LOG.info("[WebWalk] partial_seg | end={} dGoal={} goal={} nWp={}", pathEnd, distToGoal, goal, waypointCount); } @@ -65,7 +70,7 @@ public static void stallContextDebug(WorldPoint lastClick, boolean clickOk, long LOG.debug("[WebWalk] stall_ctx | lastClick={} ok={} ageMs={} interim={}", lastClick, clickOk, clickAgeMs, interim); } - /** Off-path / unreachable recovery / generic replan — single INFO line. */ + /** Off-path / unreachable recovery / generic replan - single INFO line. */ public static void recalc(String reason) { LOG.info("[WebWalk] recalc | {}", reason); } @@ -86,12 +91,12 @@ public static void tailExceeded(int maxTail, WorldPoint target, WorldPoint curre maxTail, target, current, interim, stuck, player); } - /** Pathfinder {@link net.runelite.client.plugins.microbot.shortestpath.pathfinder.Pathfinder} — DEBUG volume. */ + /** Pathfinder {@link net.runelite.client.plugins.microbot.shortestpath.pathfinder.Pathfinder} - DEBUG volume. */ public static void pf(String fmt, Object... args) { LOG.debug("[WebWalk] pf | " + fmt, args); } - /** {@link net.runelite.client.plugins.microbot.shortestpath.pathfinder.PathfinderConfig} refresh — DEBUG volume. */ + /** {@link net.runelite.client.plugins.microbot.shortestpath.pathfinder.PathfinderConfig} refresh - DEBUG volume. */ public static void cfg(String fmt, Object... args) { LOG.debug("[WebWalk] cfg | " + fmt, args); } @@ -100,7 +105,7 @@ public static void leagues(String fmt, Object... args) { LOG.debug("[WebWalk] leagues | " + fmt, args); } - /** Leagues calibration, region unlock, explicit no-op — INFO (not tick-spam paths). */ + /** Leagues calibration, region unlock, explicit no-op - INFO (not tick-spam paths). */ public static void leaguesInfo(String fmt, Object... args) { LOG.info("[WebWalk] leagues | " + fmt, args); } @@ -131,7 +136,7 @@ public static void compareError(double totalMs, WorldPoint target, String err) { LOG.warn("[WebWalk] compare_err | {}ms target={} err={}", String.format("%.1f", totalMs), target, err); } - /** Banked-route helper: per-path transport scan — DEBUG volume. */ + /** Banked-route helper: per-path transport scan - DEBUG volume. */ public static void bankPathTransportsDebug(int count, WorldPoint from, WorldPoint to) { LOG.debug("[WebWalk] bank_path | transports={} {} -> {}", count, from, to); } diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerUnitTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerUnitTest.java index 31c84b2018..b0f01f0ac5 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerUnitTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerUnitTest.java @@ -11,8 +11,10 @@ import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Predicate; @@ -238,6 +240,30 @@ public void rankSidestep_preservesAllTilesIncludingEquidistant() { assertTrue(ranked.contains(north)); } + @Test + public void getClosestTileIndex_usesReachableDistanceWhenAvailable() { + WorldPoint player = new WorldPoint(3200, 3200, 0); + WorldPoint farByWorldDistance = new WorldPoint(3210, 3200, 0); + WorldPoint nearReachable = new WorldPoint(3220, 3200, 0); + List path = Arrays.asList(farByWorldDistance, nearReachable); + Map reachable = new HashMap<>(); + reachable.put(farByWorldDistance, 8); + reachable.put(nearReachable, 3); + + assertEquals(1, Rs2Walker.getClosestTileIndex(path, player, reachable)); + } + + @Test + public void getClosestTileIndex_fallsBackToWorldDistanceWhenNoReachablePathTile() { + WorldPoint player = new WorldPoint(3200, 3200, 0); + List path = Arrays.asList( + new WorldPoint(3210, 3200, 0), + new WorldPoint(3202, 3200, 0), + new WorldPoint(3220, 3200, 0)); + + assertEquals(1, Rs2Walker.getClosestTileIndex(path, player, Collections.emptyMap())); + } + // --------------------------------------------------------------------------- // #21 — Minimap forward-scan // --------------------------------------------------------------------------- @@ -356,7 +382,7 @@ public void findFurthest_nullPredicate_treatsAsNoTransport() { } @Test - public void stabilizeRouteProgressIndex_doesNotJumpBackToEarlierSwitchbackBranch() { + public void stabilizeRouteProgressIndex_doesNotJumpBackToEarlierNearbyBranch() { WorldPoint target = new WorldPoint(3200, 3201, 0); List path = Arrays.asList( new WorldPoint(3200, 3200, 0), @@ -376,7 +402,7 @@ public void stabilizeRouteProgressIndex_doesNotJumpBackToEarlierSwitchbackBranch } @Test - public void findForwardRecoveryIndex_prefersLaterReachableSwitchbackBranch() { + public void findForwardRecoveryIndex_prefersLaterReachableBranch() { WorldPoint player = new WorldPoint(1000, 1000, 0); List path = Arrays.asList( new WorldPoint(998, 1000, 0), @@ -396,7 +422,7 @@ public void findForwardRecoveryIndex_prefersLaterReachableSwitchbackBranch() { } @Test - public void findFurthestClickableIndex_canReturnEarlierSwitchbackBranch() { + public void findFurthestClickableIndex_canReturnEarlierNearbyBranch() { WorldPoint player = new WorldPoint(1000, 1000, 0); List path = Arrays.asList( new WorldPoint(998, 1000, 0), @@ -410,6 +436,83 @@ public void findFurthestClickableIndex_canReturnEarlierSwitchbackBranch() { assertEquals("generic fallback is allowed to backtrack; recovery clamps this at the call site", 2, idx); } + @Test + public void findFurthestForwardClickableIndex_doesNotBacktrackToEarlierNearbyBranch() { + WorldPoint player = new WorldPoint(1000, 1000, 0); + List path = Arrays.asList( + new WorldPoint(998, 1000, 0), + new WorldPoint(999, 1000, 0), + new WorldPoint(1000, 1001, 0), + new WorldPoint(1015, 1000, 0), + new WorldPoint(1016, 1001, 0)); + + int idx = Rs2Walker.findFurthestForwardClickableIndex(path, 3, player, wp -> false, 13); + + assertEquals("normal route following should interpolate toward the forward tile, not backtrack", 3, idx); + } + + @Test + public void findFurthestForwardClickableIndex_stopsBeforeTransportOrigin() { + WorldPoint player = new WorldPoint(3200, 3200, 0); + WorldPoint transportOrigin = new WorldPoint(3203, 3200, 0); + List path = Arrays.asList( + new WorldPoint(3201, 3200, 0), + new WorldPoint(3202, 3200, 0), + transportOrigin, + new WorldPoint(3204, 3200, 0)); + + int idx = Rs2Walker.findFurthestForwardClickableIndex(path, 0, player, transportOrigin::equals, 13); + + assertEquals("route clicks must not skip a planned transport origin", 1, idx); + } + + @Test + public void offPathRecalcDeferralReason_prefersSettlingAndBusyState() { + long now = 10_000L; + + assertEquals("door-settling", Rs2Walker.offPathRecalcDeferralReason( + true, false, false, true, false, false, + now, 0L, 0L, 0L, 0L)); + assertEquals("transport-settling", Rs2Walker.offPathRecalcDeferralReason( + true, false, false, false, true, false, + now, 0L, 0L, 0L, 0L)); + assertEquals("moving", Rs2Walker.offPathRecalcDeferralReason( + true, false, false, false, false, false, + now, 0L, 0L, 0L, 0L)); + } + + @Test + public void offPathRecalcDeferralReason_recentProgressDefersAfterMovementStops() { + long now = 10_000L; + + assertEquals("route-progress", Rs2Walker.offPathRecalcDeferralReason( + false, false, false, false, false, false, + now, 0L, 8_000L, 0L, 0L)); + assertEquals("recent-click", Rs2Walker.offPathRecalcDeferralReason( + false, false, false, false, false, false, + now, 0L, 0L, 8_000L, 0L)); + assertEquals("interim-progress", Rs2Walker.offPathRecalcDeferralReason( + false, false, false, false, false, true, + now, 0L, 0L, 0L, 8_000L)); + } + + @Test + public void offPathRecalcDeferralReason_allowsRecalcWhenSignalsExpired() { + long now = 10_000L; + + assertEquals(null, Rs2Walker.offPathRecalcDeferralReason( + false, false, false, false, false, false, + now, 7_000L, 6_000L, 7_000L, 7_000L)); + } + + @Test + public void offPathRecalcDeferredWaitMs_isBounded() { + assertEquals(1200, Rs2Walker.offPathRecalcDeferredWaitMs( + "route-progress", 10_000L, 0L, 9_700L, 0L, 0L)); + assertEquals(250, Rs2Walker.offPathRecalcDeferredWaitMs( + "route-progress", 10_000L, 0L, 6_600L, 0L, 0L)); + } + @Test public void interpolateClickableTarget_usesInterpolatedPointWhenUsable() { WorldPoint player = new WorldPoint(3200, 3200, 0); @@ -453,6 +556,26 @@ public void interpolateClickableTarget_shortensOutOfReachForwardWaypoint() { new WorldPoint(3212, 3200, 0), target); } + @Test + public void clampToEuclideanRadius_shortensDiagonalTargetInsideCircle() { + WorldPoint player = new WorldPoint(2875, 3418, 0); + WorldPoint target = new WorldPoint(2886, 3428, 0); + + WorldPoint clamped = Rs2Walker.clampToEuclideanRadius(player, target, 10); + + assertTrue(clamped.distanceTo2D(player) <= 10); + assertTrue(clamped.getX() > player.getX()); + assertTrue(clamped.getY() > player.getY()); + } + + @Test + public void clampToEuclideanRadius_keepsInRangeTarget() { + WorldPoint player = new WorldPoint(3200, 3200, 0); + WorldPoint target = new WorldPoint(3206, 3203, 0); + + assertEquals(target, Rs2Walker.clampToEuclideanRadius(player, target, 10)); + } + // --------------------------------------------------------------------------- // Raw-path wall-door segment probing // --------------------------------------------------------------------------- @@ -525,6 +648,23 @@ public void shouldClearInterimTarget_closeToCheckpoint_returnsTrue() { 2_000L)); } + @Test + public void shouldClearInterimTarget_preclickDistanceStillKeepsCheckpoint() { + assertFalse(Rs2Walker.shouldClearInterimTarget( + new WorldPoint(2890, 3396, 0), + new WorldPoint(2884, 3396, 0), + 1_000L, + 1_500L, + 2_000L)); + } + + @Test + public void distanceToInterimOrMax_samePlaneReturnsDistance() { + assertEquals(8, Rs2Walker.distanceToInterimOrMax( + new WorldPoint(2850, 3506, 0), + new WorldPoint(2849, 3498, 0))); + } + @Test public void shouldClearInterimTarget_expiredCheckpoint_returnsTrue() { assertTrue(Rs2Walker.shouldClearInterimTarget( @@ -555,10 +695,124 @@ public void shouldClearInterimTarget_activeFarCheckpoint_returnsFalse() { 5_000L)); } + @Test + public void shouldYieldForActiveRecoveryInterim_recentProgress_returnsTrue() { + assertTrue(Rs2Walker.shouldYieldForActiveRecoveryInterim( + new WorldPoint(2890, 3396, 0), + new WorldPoint(2884, 3396, 0), + 1_000L, + 2_500L, + 3_000L, + 0L, + 0L, + false)); + } + + @Test + public void shouldYieldForActiveRecoveryInterim_staleProgress_returnsFalse() { + assertFalse(Rs2Walker.shouldYieldForActiveRecoveryInterim( + new WorldPoint(2890, 3396, 0), + new WorldPoint(2880, 3396, 0), + 1_000L, + 1_500L, + 5_000L, + 0L, + 0L, + false)); + } + + @Test + public void shouldYieldForActiveRecoveryInterim_recentRecoveryClick_returnsTrue() { + assertTrue(Rs2Walker.shouldYieldForActiveRecoveryInterim( + new WorldPoint(2890, 3396, 0), + new WorldPoint(2880, 3396, 0), + 1_000L, + 0L, + 3_000L, + 0L, + 2_000L, + false)); + } + @Test public void interimPreclickTiles_runHandsOffEarlierThanWalk() { assertEquals(6, Rs2Walker.interimPreclickTiles(false)); - assertEquals(11, Rs2Walker.interimPreclickTiles(true)); + assertEquals(8, Rs2Walker.interimPreclickTiles(true)); + } + + @Test + public void routeMovementClickPhase_labelsContinuationSeparatelyFromRecovery() { + assertEquals("stall_recovery_click", Rs2Walker.routeMovementClickPhase("stall recovery click")); + assertEquals("active_route_idle_nudge", Rs2Walker.routeMovementClickPhase("active route idle nudge")); + assertEquals("interim_close_route_click", Rs2Walker.routeMovementClickPhase("interim close route click")); + assertEquals("route_movement_click", Rs2Walker.routeMovementClickPhase("other")); + } + + @Test + public void shouldSkipStartupPreclickSegmentHandlers_skipsBeyondStartupLookahead() { + assertTrue(Rs2Walker.shouldSkipStartupPreclickSegmentHandlers( + true, + 8, + 5, + false, + false, + false)); + } + + @Test + public void shouldSkipStartupPreclickSegmentHandlers_keepsImmediateAndDoorRecoveryEdges() { + assertFalse(Rs2Walker.shouldSkipStartupPreclickSegmentHandlers( + true, + 7, + 5, + false, + false, + false)); + assertFalse(Rs2Walker.shouldSkipStartupPreclickSegmentHandlers( + true, + 8, + 5, + true, + false, + false)); + assertFalse(Rs2Walker.shouldSkipStartupPreclickSegmentHandlers( + false, + 8, + 5, + false, + false, + false)); + } + + @Test + public void rawPathForwardAnchorIndex_keepsFallbackAheadOfAnchor() { + List rawPath = Arrays.asList( + new WorldPoint(3200, 3200, 0), + new WorldPoint(3201, 3200, 0), + new WorldPoint(3202, 3200, 0), + new WorldPoint(3203, 3200, 0), + new WorldPoint(3204, 3200, 0), + new WorldPoint(3205, 3200, 0), + new WorldPoint(3206, 3200, 0), + new WorldPoint(3207, 3200, 0), + new WorldPoint(3208, 3200, 0), + new WorldPoint(3209, 3200, 0), + new WorldPoint(3210, 3200, 0), + new WorldPoint(3210, 3201, 0), + new WorldPoint(3210, 3202, 0), + new WorldPoint(3210, 3203, 0), + new WorldPoint(3210, 3204, 0), + new WorldPoint(3209, 3204, 0), + new WorldPoint(3208, 3204, 0), + new WorldPoint(3207, 3204, 0), + new WorldPoint(3206, 3204, 0), + new WorldPoint(3205, 3204, 0), + new WorldPoint(3204, 3204, 0)); + WorldPoint playerOnReturnBranch = rawPath.get(20); + + assertEquals("forward anchor must keep raw fallback on the current return branch", + 20, + Rs2Walker.rawPathForwardAnchorIndex(rawPath, playerOnReturnBranch, 14)); } @Test @@ -600,6 +854,16 @@ public void shouldBlacklistDoorAfterWrongTraversal_teleportAway_returnsTrue() { new WorldPoint(1988, 5569, 0))); } + @Test + public void shouldBlacklistDoorAfterWrongTraversal_startedFarFromDoor_returnsFalse() { + assertFalse("movement from an earlier minimap click must not blacklist a valid gate", + Rs2Walker.shouldBlacklistDoorAfterWrongTraversal( + new WorldPoint(3270, 3320, 0), + new WorldPoint(3275, 3325, 0), + new WorldPoint(3262, 3322, 0), + new WorldPoint(3261, 3321, 0))); + } + @Test public void shouldBlacklistDoorAfterWrongTraversal_progressTowardEdge_returnsFalse() { assertFalse(Rs2Walker.shouldBlacklistDoorAfterWrongTraversal( @@ -768,6 +1032,18 @@ public void telemetry_counterIsIndependentOfOtherReasons() { assertEquals(1, Rs2Walker.Telemetry.partialRetryCount.get()); } + @Test + public void telemetry_recordOffPathRecalcDeferred_setsReasonButDoesNotCountAsRecalc() { + Rs2Walker.Telemetry.recordOffPathRecalcDeferred("route-progress", + new WorldPoint(3200, 3200, 0), + new WorldPoint(3210, 3210, 0), + 20); + + assertEquals(1, Rs2Walker.Telemetry.offPathRecalcDeferredCount.get()); + assertEquals("off-path-deferred:route-progress", Rs2Walker.Telemetry.lastReason); + assertEquals(0, Rs2Walker.Telemetry.totalRecalcs()); + } + @Test public void telemetry_reset_clearsUnreachable() { Rs2Walker.Telemetry.recordUnreachable("no-walkable-path", From 51af3342a22275218604fbfd035eede7e3583d77 Mon Sep 17 00:00:00 2001 From: infuse21 Date: Sat, 18 Jul 2026 14:14:48 +0100 Subject: [PATCH 2/4] changed client-thread-guardrail-baseline.txt cold start - hopefuly fixed Mid-route Stalling rs2 walker now accepts adjacent same-plane transport/objct landing --- docs/entity-guides/movement.md | 4 +- .../microbot/util/walker/Rs2Walker.java | 45 ++++++++---- .../util/walker/Rs2WalkerUnitTest.java | 70 ++++++++++++++++--- .../client-thread-guardrail-baseline.txt | 69 ++++++++++-------- 4 files changed, 136 insertions(+), 52 deletions(-) diff --git a/docs/entity-guides/movement.md b/docs/entity-guides/movement.md index 2ed25f0c5d..123d6eadef 100644 --- a/docs/entity-guides/movement.md +++ b/docs/entity-guides/movement.md @@ -290,7 +290,7 @@ After a handled transport, avoid expensive path-adjacent or raw transport scans For long-route minimap walking, let the next checkpoint selection happen before the current minimap target is fully consumed. Waiting until the player is only a few tiles from the interim makes the walker visibly stop before issuing the next click; handing off at a moderate remaining distance keeps movement continuous without rapid re-clicking. If an interim clears as close and no nearby route door/transport is pending, issue the next route-aligned continuation click immediately instead of waiting for idle-nudge recovery. Stale-progress checks must also count distance progress toward the actual interim checkpoint, not only smoothed path index progress; sparse smoothed paths can otherwise clear valid raw-path checkpoints as stale while the player is still walking toward them. -Before the first movement click, only scan a small number of immediate route edges for doors or blockers. A door several segments ahead can be handled after the first minimap click moves the player toward it; spending startup time scanning every nearby raw segment creates a visible cold start. +Before the first movement click, avoid timeout-backed speculative segment scans for doors, blockers, rockfalls, or transports. Let the first minimap click move the player toward the route; actual visible doors can still be handled by the dedicated pre-click route-door guard, and steady-state handling can resolve later obstacles once movement has started. Spending startup time scanning every nearby raw segment creates a visible cold start. Continuation clicks that keep an active route moving should be tail-exempt like `interim-in-flight`; otherwise very long routes can exhaust `MAX_PROCESS_WALK_TAIL_ITERATIONS` while still making progress and trigger an unnecessary auto-retry. @@ -298,7 +298,7 @@ Sticky interim targets should also clear when route-index progress goes stale. I When a route-following minimap click is outside the minimap clip, fallback clicks must stay on the raw path. This includes the "clicked but no movement yet" retry after a route click and route-backed direct short-walks near the final destination. A generic "reachable tile closer to target" fallback can select a tile far away from the route in open areas, especially near fences and the final destination. Raw fallback must be anchored from the stabilized route index, not a fresh nearest-raw-tile lookup, or the walker can snap backward to an already-travelled branch. -For adjacent same-plane shortcuts, do not treat any movement away from the origin as success. Some shortcuts, such as stepping stones, can fail and place the player on a fallback tile; once the player is settled away from the expected destination, stop the landing wait and replan from the actual tile. +For adjacent same-plane shortcuts, do not treat any movement away from the origin as success. Some shortcuts, such as stepping stones, can fail and place the player on a fallback tile; once the player is settled away from the expected destination, stop the landing wait and replan from the actual tile. If the player is settled within one tile of the expected destination and is no longer on the origin, accept the landing instead of waiting for an exact tile that server pathing may not choose. ## 14. Keep optimistic recovery clicks route-backed and paced 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 68df3dceea..9de5153b3a 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 @@ -156,8 +156,6 @@ public static WorldPoint getCurrentTarget() { private static final int PATHFINDER_DONE_POLL_WAIT_MS = 1200; private static final int PATHFINDER_DONE_RETRY_SLEEP_MIN_MS = 120; private static final int PATHFINDER_DONE_RETRY_SLEEP_MAX_MS = 220; - private static final long STARTUP_FIRST_CLICK_BUDGET_MS = 2200L; - private static final int STARTUP_PRECLICK_DOOR_SCAN_LOOKAHEAD_EDGES = 2; private static final int POST_DOOR_FAST_CLICK_MAX_EUCLIDEAN = 13; private static final int POST_DOOR_EDGE_NUDGE_MAX_FROM_PLAYER = 3; private static final int POST_DOOR_EDGE_NUDGE_WAIT_MS = 1200; @@ -433,13 +431,7 @@ private static WalkerPhase currentWalkerPhase() { if (firstMovementClickMarked) { return WalkerPhase.STEADY; } - long startedAt = walkSessionStartedAtMs; - if (startedAt <= 0) { - return WalkerPhase.STEADY; - } - return (System.currentTimeMillis() - startedAt) <= STARTUP_FIRST_CLICK_BUDGET_MS - ? WalkerPhase.STARTUP - : WalkerPhase.STEADY; + return WalkerPhase.STARTUP; } private static ObstaclePolicy obstaclePolicyForCurrentPhase() { @@ -460,7 +452,7 @@ static boolean shouldSkipStartupPreclickSegmentHandlers(boolean startupBeforeFir if (recentDoorAttemptNearSegment || doorSettling || recoveryInFlight) { return false; } - return segmentIdx > routeStartIdx + STARTUP_PRECLICK_DOOR_SCAN_LOOKAHEAD_EDGES; + return true; } /** @@ -1622,13 +1614,13 @@ && tryIssueRouteContinuationClick(rawPath, path, target)) { if (skipPostTransportSegmentHandlers || skipStartupPreclickSegmentHandlers) { if (skipStartupPreclickSegmentHandlers) { markStartupPhase("preclick_segment_handler_skip", target, - "i=" + i + " reason=startup_preclick_budget"); + "i=" + i + " reason=startup_before_first_click"); } tmarkPostTransport("post_transport_segment_handler_skip", target, "i=" + i + " reason=" + (skipPostTransportSegmentHandlers ? "no_nearby_planned_transport" - : "startup_preclick_budget")); + : "startup_before_first_click")); } else { long segmentHandlerStartAt = System.currentTimeMillis(); int rawI = (i < smoothedToRaw.length) ? smoothedToRaw[i] : 0; @@ -7932,6 +7924,7 @@ private static boolean waitForPostHandleObjectLanding(Transport transport, int maxInclusive) { long waitStartedAt = System.currentTimeMillis(); AtomicBoolean settledAwayFromAdjacentDestination = new AtomicBoolean(false); + AtomicBoolean settledNearAdjacentDestination = new AtomicBoolean(false); boolean completed = sleepUntil(() -> { if (isPlayerWithinChebyshevInclusive(destWait, maxInclusive)) { return true; @@ -7945,6 +7938,10 @@ private static boolean waitForPostHandleObjectLanding(Transport transport, || Rs2Player.isMoving() || Rs2Player.isAnimating()) { return false; } + if (isSettledNearAdjacentSamePlaneLanding(transport, playerLoc, destWait, maxInclusive)) { + settledNearAdjacentDestination.set(true); + return true; + } WorldPoint origin = transport == null ? null : transport.getOrigin(); boolean settledAwayFromOrigin = origin != null && playerLoc.distanceTo2D(origin) > 1; if (playerLoc.distanceTo2D(destWait) > Math.max(1, maxInclusive) @@ -7955,6 +7952,11 @@ private static boolean waitForPostHandleObjectLanding(Transport transport, return false; }, POST_HANDLE_OBJECT_LANDING_WAIT_MS); + if (settledNearAdjacentDestination.get()) { + WebWalkLog.spInfo("post-handleObject adjacent landing accepted | dest={} at={}", + compactWorldPoint(destWait), compactWorldPoint(Rs2Player.getWorldLocation())); + return true; + } if (settledAwayFromAdjacentDestination.get()) { WebWalkLog.spInfo("post-handleObject adjacent landing failed | dest={} at={}", compactWorldPoint(destWait), compactWorldPoint(Rs2Player.getWorldLocation())); @@ -7963,6 +7965,25 @@ private static boolean waitForPostHandleObjectLanding(Transport transport, return completed; } + static boolean isSettledNearAdjacentSamePlaneLanding(Transport transport, + WorldPoint playerLoc, + WorldPoint destWait, + int maxInclusive) { + if (!isAdjacentSamePlaneTransport(transport) + || playerLoc == null + || destWait == null + || playerLoc.getPlane() != destWait.getPlane()) { + return false; + } + WorldPoint origin = transport.getOrigin(); + if (origin == null || playerLoc.equals(origin)) { + return false; + } + int destinationDistance = playerLoc.distanceTo2D(destWait); + return destinationDistance <= Math.max(1, maxInclusive) + && playerLoc.distanceTo2D(origin) > 0; + } + /** * Handles the transportation process specifically for instances of PohTransport. * Any Transport param that reaches this is assumed to be a PohTransport. diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerUnitTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerUnitTest.java index b0f01f0ac5..5de440bd1e 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerUnitTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerUnitTest.java @@ -116,6 +116,63 @@ public void shouldRecalculatePathAfterTransport_skipsAdjacentSamePlaneTransport( assertFalse(Rs2Walker.shouldRecalculatePathAfterTransport(door)); } + @Test + public void isSettledNearAdjacentSamePlaneLanding_acceptsNearDestinationOffOrigin() { + Transport door = new Transport( + new WorldPoint(3152, 3363, 0), + new WorldPoint(3153, 3363, 0), + "Door", + TransportType.TRANSPORT, + false, + "Open", + "Door", + 136); + + assertTrue(Rs2Walker.isSettledNearAdjacentSamePlaneLanding( + door, + new WorldPoint(3154, 3363, 0), + new WorldPoint(3153, 3363, 0), + 0)); + } + + @Test + public void isSettledNearAdjacentSamePlaneLanding_rejectsOriginTile() { + Transport door = new Transport( + new WorldPoint(3152, 3363, 0), + new WorldPoint(3153, 3363, 0), + "Door", + TransportType.TRANSPORT, + false, + "Open", + "Door", + 136); + + assertFalse(Rs2Walker.isSettledNearAdjacentSamePlaneLanding( + door, + new WorldPoint(3152, 3363, 0), + new WorldPoint(3153, 3363, 0), + 0)); + } + + @Test + public void isSettledNearAdjacentSamePlaneLanding_rejectsTilesTooFarFromDestination() { + Transport door = new Transport( + new WorldPoint(3152, 3363, 0), + new WorldPoint(3153, 3363, 0), + "Door", + TransportType.TRANSPORT, + false, + "Open", + "Door", + 136); + + assertFalse(Rs2Walker.isSettledNearAdjacentSamePlaneLanding( + door, + new WorldPoint(3155, 3363, 0), + new WorldPoint(3153, 3363, 0), + 0)); + } + @Test public void shouldRecalculatePathAfterTransport_includesLongDistanceTransport() { Transport ship = new Transport( @@ -749,10 +806,10 @@ public void routeMovementClickPhase_labelsContinuationSeparatelyFromRecovery() { } @Test - public void shouldSkipStartupPreclickSegmentHandlers_skipsBeyondStartupLookahead() { + public void shouldSkipStartupPreclickSegmentHandlers_skipsBeforeFirstMovementClick() { assertTrue(Rs2Walker.shouldSkipStartupPreclickSegmentHandlers( true, - 8, + 5, 5, false, false, @@ -760,14 +817,7 @@ public void shouldSkipStartupPreclickSegmentHandlers_skipsBeyondStartupLookahead } @Test - public void shouldSkipStartupPreclickSegmentHandlers_keepsImmediateAndDoorRecoveryEdges() { - assertFalse(Rs2Walker.shouldSkipStartupPreclickSegmentHandlers( - true, - 7, - 5, - false, - false, - false)); + public void shouldSkipStartupPreclickSegmentHandlers_keepsDoorRecoveryAndSteadyEdges() { assertFalse(Rs2Walker.shouldSkipStartupPreclickSegmentHandlers( true, 8, 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 3395d9f6e9..a2605ab88d 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 @@ -720,10 +720,18 @@ net.runelite.client.plugins.microbot.util.walker.Rs2MiniMap#worldToMinimap(World net.runelite.client.plugins.microbot.util.walker.Rs2MiniMap#worldToMinimap(WorldPoint): Point -> net.runelite.api.WorldView#isInstance(): boolean net.runelite.client.plugins.microbot.util.walker.Rs2MiniMap#worldToMinimap(WorldPoint): Point -> net.runelite.api.coords.LocalPoint#fromWorld(WorldView, WorldPoint): LocalPoint net.runelite.client.plugins.microbot.util.walker.Rs2Walker#adjacentSamePlaneTransportSuppressionPoints(Transport, TileObject): Set -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#charterWidgetMatchesDestination(Widget, String): boolean -> net.runelite.api.widgets.Widget#getActions(): String[] +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#charterWidgetMatchesDestination(Widget, String): boolean -> net.runelite.api.widgets.Widget#getName(): String +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#charterWidgetMatchesDestination(Widget, String): boolean -> net.runelite.api.widgets.Widget#getText(): String net.runelite.client.plugins.microbot.util.walker.Rs2Walker#closeWorldMap(): boolean -> net.runelite.api.widgets.Widget#getBounds(): Rectangle 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#doorObjectStillHasAction(TileObject, WorldPoint, WorldPoint, WorldPoint, List, String): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#findCharterDestinationTextWidget(Widget, String): Widget -> net.runelite.api.widgets.Widget#getDynamicChildren(): Widget[] +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#findCharterDestinationTextWidget(Widget, String): Widget -> net.runelite.api.widgets.Widget#getStaticChildren(): Widget[] +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#findCharterDestinationTextWidget(Widget, String): Widget -> net.runelite.api.widgets.Widget#isHidden(): boolean +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#findClickableCharterWidget(Widget, Widget): Widget -> net.runelite.api.widgets.Widget#getParent(): Widget +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#getFirstWidgetAction(Widget): String -> net.runelite.api.widgets.Widget#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[] @@ -735,9 +743,6 @@ 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#isInstance(): boolean 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#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): boolean -> net.runelite.api.Client#getTopLevelWorldView(): WorldView net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleDoors(List, int, boolean): boolean -> net.runelite.api.ObjectComposition#getImpostorIds(): int[] net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleDoors(List, int, boolean): boolean -> net.runelite.api.ObjectComposition#getName(): String @@ -771,7 +776,11 @@ net.runelite.client.plugins.microbot.util.walker.Rs2Walker#hasDoorCandidateOnRaw net.runelite.client.plugins.microbot.util.walker.Rs2Walker#hasDoorCandidateOnRawSegment(List, int): boolean -> net.runelite.api.WorldView#getScene(): Scene net.runelite.client.plugins.microbot.util.walker.Rs2Walker#hasLineOfSightBetween(WorldPoint, WorldPoint): boolean -> net.runelite.api.Client#getTopLevelWorldView(): WorldView net.runelite.client.plugins.microbot.util.walker.Rs2Walker#hasLineOfSightBetween(WorldPoint, WorldPoint): boolean -> net.runelite.api.coords.WorldArea#hasLineOfSightTo(WorldView, WorldArea): boolean +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#hasWidgetActions(Widget): boolean -> net.runelite.api.widgets.Widget#getActions(): String[] net.runelite.client.plugins.microbot.util.walker.Rs2Walker#interactingActorNearWalkablePath(): boolean -> net.runelite.api.Actor#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#invokeCharterDestinationWidget(Widget, String): boolean -> net.runelite.api.widgets.Widget#getBounds(): Rectangle +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#invokeCharterDestinationWidget(Widget, String): boolean -> net.runelite.api.widgets.Widget#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#invokeCharterDestinationWidget(Widget, String): boolean -> net.runelite.api.widgets.Widget#getIndex(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isCatalogTransportObject(TileObject): boolean -> net.runelite.api.TileObject#getId(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isCatalogTransportObject(TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isCloseToRegion(int, int, int): boolean -> net.runelite.api.Client#getTopLevelWorldView(): WorldView @@ -788,33 +797,35 @@ net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isPendingRouteDoorObj net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isPendingRouteDoorObject(TileObject, WorldPoint, WorldPoint, WorldPoint, int): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isUnresolvedRouteDoorObject(TileObject, WorldPoint, WorldPoint, WorldPoint, int): boolean -> net.runelite.api.ObjectComposition#getName(): String net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isUnresolvedRouteDoorObject(TileObject, WorldPoint, WorldPoint, WorldPoint, int): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$findDoorNearSegment$42(WorldPoint, WorldPoint, WorldPoint, List, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$findDoorNearSegment$43(WorldPoint, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleCharterShip$181(Widget): boolean -> net.runelite.api.widgets.Widget#getActions(): String[] -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleDoors$40(WorldPoint, GameObject): boolean -> net.runelite.api.GameObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleDoors$41(WorldPoint, GameObject): boolean -> net.runelite.api.GameObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleFairyRing$187(Transport, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleMinigameTeleport$158(Widget, Object[]): boolean -> net.runelite.api.widgets.Widget#getOnOpListener(): Object[] -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleMinigameTeleport$160(String): boolean -> net.runelite.api.widgets.Widget#getText(): String -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$136(int, TileObject): boolean -> net.runelite.api.TileObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$142(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$142(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$143(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$143(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleRockfall$22(WorldPoint, Tile): boolean -> net.runelite.api.Tile#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$116(int, boolean, TileObject): boolean -> net.runelite.api.ObjectComposition#getName(): String -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$116(int, boolean, TileObject): boolean -> net.runelite.api.TileObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$118(Transport, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$119(TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$120(Transport, Object): Integer -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$121(Transport, Object): Integer -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$123(int, List, TileObject): boolean -> net.runelite.api.TileObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$124(Transport, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleWildernessObelisk$150(Transport, GameObject): boolean -> net.runelite.api.GameObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleWildernessObelisk$151(Transport, GameObject): boolean -> net.runelite.api.GameObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$findDoorNearSegment$40(WorldPoint, WorldPoint, WorldPoint, List, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$findDoorNearSegment$41(WorldPoint, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleDoors$38(WorldPoint, GameObject): boolean -> net.runelite.api.GameObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleDoors$39(WorldPoint, GameObject): boolean -> net.runelite.api.GameObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleFairyRing$185(Transport, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleMinigameTeleport$154(Widget, Object[]): boolean -> net.runelite.api.widgets.Widget#getOnOpListener(): Object[] +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleMinigameTeleport$156(String): boolean -> net.runelite.api.widgets.Widget#getText(): String +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$132(int, TileObject): boolean -> net.runelite.api.TileObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$138(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$138(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$139(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$139(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleRockfall$21(WorldPoint, Tile): boolean -> net.runelite.api.Tile#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$112(int, boolean, TileObject): boolean -> net.runelite.api.ObjectComposition#getName(): String +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$112(int, boolean, TileObject): boolean -> net.runelite.api.TileObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$114(Transport, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$115(TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$116(Transport, Object): Integer -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$117(Transport, Object): Integer -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$119(int, List, TileObject): boolean -> net.runelite.api.TileObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$120(Transport, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleWildernessObelisk$146(Transport, GameObject): boolean -> net.runelite.api.GameObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleWildernessObelisk$147(Transport, GameObject): boolean -> net.runelite.api.GameObject#getId(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$processWalk$2(): boolean -> net.runelite.api.widgets.Widget#getSpriteId(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$processWalk$3(): boolean -> net.runelite.api.widgets.Widget#getSpriteId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$tryHandleBlockingPathObjectsWithTimeout$69(WorldPoint, GameObject): boolean -> net.runelite.api.GameObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$tryHandleBlockingPathObjectsWithTimeout$68(WorldPoint, GameObject): boolean -> net.runelite.api.GameObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#localPointForWorld(WorldPoint): LocalPoint -> net.runelite.api.Client#getTopLevelWorldView(): WorldView +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#localPointForWorld(WorldPoint): LocalPoint -> net.runelite.api.WorldView#isInstance(): boolean +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#localPointForWorld(WorldPoint): LocalPoint -> net.runelite.api.coords.LocalPoint#fromWorld(WorldView, WorldPoint): LocalPoint net.runelite.client.plugins.microbot.util.walker.Rs2Walker#markNearbyDoorFamilyOpened(TileObject, WorldPoint, String, int): void -> net.runelite.api.GameObject#getWorldLocation(): WorldPoint net.runelite.client.plugins.microbot.util.walker.Rs2Walker#maybeCanvasNudgeAfterDoor(WorldPoint, int, List): void -> net.runelite.api.Client#getTopLevelWorldView(): WorldView net.runelite.client.plugins.microbot.util.walker.Rs2Walker#maybeCanvasNudgeAfterDoor(WorldPoint, int, List): void -> net.runelite.api.coords.LocalPoint#fromWorld(WorldView, WorldPoint): LocalPoint @@ -848,6 +859,8 @@ net.runelite.client.plugins.microbot.util.walker.Rs2Walker#walkFastCanvas(WorldP net.runelite.client.plugins.microbot.util.walker.Rs2Walker#walkFastCanvas(WorldPoint, boolean): boolean -> net.runelite.api.WorldView#getPlane(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#walkFastCanvas(WorldPoint, boolean): boolean -> net.runelite.api.WorldView#isInstance(): boolean net.runelite.client.plugins.microbot.util.walker.Rs2Walker#walkFastCanvas(WorldPoint, boolean): boolean -> net.runelite.api.coords.LocalPoint#fromWorld(WorldView, WorldPoint): LocalPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#walkFastCanvasOnScreenOnly(WorldPoint, boolean): boolean -> net.runelite.api.Client#getTopLevelWorldView(): WorldView +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#walkFastCanvasOnScreenOnly(WorldPoint, boolean): boolean -> net.runelite.api.WorldView#getPlane(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#walkFastLocal(LocalPoint): void -> net.runelite.api.Client#getTopLevelWorldView(): WorldView net.runelite.client.plugins.microbot.util.walker.Rs2Walker#walkFastLocal(LocalPoint): void -> net.runelite.api.WorldView#getPlane(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#walkNextToInstance(GameObject): void -> net.runelite.api.Client#getTopLevelWorldView(): WorldView From 82b537720efb5e5e7ed1314df8e977e617c02d76 Mon Sep 17 00:00:00 2001 From: infuse21 Date: Sun, 19 Jul 2026 15:28:45 +0100 Subject: [PATCH 3/4] summary for all current new commits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds a per-loop `WalkLoopSnapshot` so route progress decisions reuse a stable player location and reachable-tile snapshot during each walker loop. - Reworks closest-path-index and route-progress tracking to prefer reachable distance when available, fall back safely when reachability is empty, and preserve forward progress on routes that double back near earlier branches. - Keeps primary minimap clicks, direct short-walk fallback, and unreachable recovery anchored to the raw shortest-path route before using smoothed waypoint nudges or generic fallbacks. - Adds raw-route target selection helpers with candidate predicates so recovery and continuation clicks stay on known walkable/unloaded route tiles. - Defers off-path recalculation while movement, interaction, animation, door/transport settling, recent clicks, route progress, or interim checkpoint progress indicate the player is still advancing. - Adds sticky interim checkpoint tracking and early `interim-in-flight` yields before broad scans, current-tile transport checks, direct short-walk attempts, and path segment scans. - Hints route progress after clicked-ahead checkpoints so later route anchoring does not snap back to nearby already-travelled branches. - Improves startup behavior by skipping speculative timeout-backed obstacle scans before the first movement click, while still allowing immediate planned transport steps to dispatch. - Suppresses active route idle nudges and stall recovery clicks when the current route segment is an explicit transport edge. - Tightens door/gate handling so wrong-traversal blacklisting only happens when the player started near the attempted door edge. - Accepts adjacent same-plane transport/object landings when the player settles near the expected destination instead of requiring the exact destination tile. - Improves charter ship handling by recursively resolving destination widgets, invoking the clickable widget/action, and confirming travel prompts when needed. - Updates `ShortestPathPlugin` route cleanup, pathfinder inventory quantity checks, WebWalk logging volume, movement docs, unit coverage, and the client-thread guardrail baseline. • Rs2Walker.java: immediate planned transports now suppress idle/stall recovery, so first-step teleports like Home Teleport should fire before pointless movement clicks. • Rs2Walker.java: active interim route clicks now yield before broad scans, reducing stop-start spam. • Rs2Walker.java: normal route clicks prefer raw shortest-path tiles instead of side/fallback tiles. • Rs2Walker.java: public walker entrypoints now reject client-thread calls before lock waits or player/tile reads. • Rs2WalkerUnitTest.java: added focused tests for route anchoring, raw-route click selection, interim yielding, and immediate-transport idle nudge suppression. • movement.md: documented the new walker gotchas Implemented the Al Kharid toll gate fix. What changed: • Added Pay-toll(10gp) transports for all four gate tiles in transports.tsv. • Added quest-free Open gate transports for Prince Ali Rescue completion. • Replaced the old quest-only tile restriction with blocked edge rules, so non-quest accounts can route through if they have 10 coins. • Walker now treats paid TRANSPORT rows as currency transports for item/banking checks. • Added special handling for Al Kharid toll gate confirmation: it uses Pay-toll(10gp) and clicks Yes, okay / Yes if prompted. I did not add the “talk to guard” fallback yet; this handles the cleaner object interaction path first. --- docs/entity-guides/movement.md | 91 +++- .../microbot/util/walker/Rs2Walker.java | 446 ++++++++++++++---- .../banking/Rs2WalkerBankingPlanner.java | 6 +- .../microbot/shortestpath/blocked_edges.tsv | 2 + .../microbot/shortestpath/restrictions.tsv | 8 +- .../microbot/shortestpath/transports.tsv | 8 + .../shortestpath/ShortestPathCoreTest.java | 72 +++ .../util/walker/Rs2WalkerUnitTest.java | 161 +++++++ 8 files changed, 684 insertions(+), 110 deletions(-) diff --git a/docs/entity-guides/movement.md b/docs/entity-guides/movement.md index 123d6eadef..235384a743 100644 --- a/docs/entity-guides/movement.md +++ b/docs/entity-guides/movement.md @@ -237,9 +237,9 @@ if (isStuckTooLong()) { For long open routes, retarget before the player fully stops when they are already close to the interim checkpoint, and keep normal minimap clicks slightly inside the observed minimap edge. This reduces visible stop/start pauses and outside-clip fallback clicks without reintroducing rapid click thrash. -## 12. Do not let optimistic recovery override unresolved door blockers +## 12. Do not let local recovery override unresolved door blockers -Unreachable-tile recovery is useful for outdoor false negatives, but in tight rooms it can fight the door resolver. If a route edge still has a door-like scene object on or adjacent to the raw path, suppress broad minimap recovery and let the door scanners retry after their normal cooldowns. Do not permanently blacklist a path-adjacent fallback door just because one attempt traversed the wrong way; in small door clusters the same object may be the correct blocker again once the player has moved to the other side. +Local-reachability recovery is useful for outdoor false negatives, but in tight rooms it can fight the door resolver. If a route edge still has a door-like scene object on or adjacent to the raw path, suppress broad minimap recovery and let the door scanners retry after their normal cooldowns. Do not permanently blacklist a path-adjacent fallback door just because one attempt traversed the wrong way; in small door clusters the same object may be the correct blocker again once the player has moved to the other side. **Why this matters:** In POH-style tight rooms with several doors close together, a fallback door click can move the player away from the intended route. If that door tile is session-blacklisted and optimistic recovery keeps clicking route tiles beyond the blocker, the walker loops around the room until a user manually opens the final door. @@ -257,7 +257,7 @@ clickOptimisticRecoveryTarget(); **Where this applies:** `Rs2Walker.processWalk` unreachable-tile handling, `tryResolvePathAdjacentBlocker`, and any fallback that issues minimap recovery clicks after door/path-adjacent scans fail. -**Defensive check:** Reproduce a route through a small room with three nearby doors and a POH portal. The walker should retry the route-door blocker and avoid repeated `unreachable optimistic recovery` loops around the room; it should not need the user to manually open the final door. +**Defensive check:** Reproduce a route through a small room with three nearby doors and a POH portal. The walker should retry the route-door blocker and avoid repeated `route-backed local recovery click` loops around the room; it should not need the user to manually open the final door. ## 13. Stall recalculation must also issue fresh movement @@ -300,9 +300,9 @@ When a route-following minimap click is outside the minimap clip, fallback click For adjacent same-plane shortcuts, do not treat any movement away from the origin as success. Some shortcuts, such as stepping stones, can fail and place the player on a fallback tile; once the player is settled away from the expected destination, stop the landing wait and replan from the actual tile. If the player is settled within one tile of the expected destination and is no longer on the origin, accept the landing instead of waiting for an exact tile that server pathing may not choose. -## 14. Keep optimistic recovery clicks route-backed and paced +## 14. Keep local recovery clicks route-backed and paced -Unreachable-tile recovery is a fallback for bounded reachability false negatives, not a license to pick an arbitrary minimap point. Recovery clicks should use the same route-backed raw-path fallback as normal route movement, then store the actual clicked target as the sticky interim checkpoint. After issuing the click, wait only for movement to start or the checkpoint/goal to be reached; do not wait for a full idle cycle before returning to the walk loop. +Local-reachability recovery is a fallback for bounded reachability false negatives, not a license to pick an arbitrary minimap point. Recovery clicks should use the same route-backed raw-path fallback as normal route movement, then store the actual clicked target as the sticky interim checkpoint. After issuing the click, wait only for movement to start or the checkpoint/goal to be reached; do not wait for a full idle cycle before returning to the walk loop. **Why this matters:** Bounded local reachability can report a valid route tile as unreachable even though the server pathfinder can still walk toward it. Arbitrary or repeated recovery clicks can pull the player away from the planned route and create visible stop-start movement. @@ -318,8 +318,85 @@ if (clickedTarget != null) { } ``` -**Where this applies:** `Rs2Walker.processWalk` unreachable-smoothed-tile recovery, route-backed direct short-walks, and any fallback that runs after local reachability says a route tile is not currently reachable. +**Where this applies:** `Rs2Walker.processWalk` local-reachability recovery, route-backed direct short-walks, and any fallback that runs after local reachability says a route tile is not currently reachable. -**Defensive check:** Exercise a long outdoor route and a route with gates or transports. Recovery logs should select route-backed points, and there should be no long idle pause between `unreachable optimistic recovery` and the next route-aligned movement. +**Defensive check:** Exercise a long outdoor route and a route with gates or transports. Recovery logs should select route-backed points, and there should be no long idle pause between `route-backed local recovery click` and the next route-aligned movement. After recovery sets a sticky interim target, do not issue another optimistic recovery while that interim is still active and movement/progress is fresh. Yield as `interim-in-flight` or another tail-exempt movement state until the checkpoint is close, stale, or expired. Otherwise the walker can loop through several recovery clicks while the player is already moving, especially when local reachability reports the next smoothed tile as unreachable. + +## 15. Keep primary route clicks on the raw route + +When a shortest-path raw route exists, choose the next minimap target from that raw route before applying smoothed-waypoint wall-distance nudges, bounded reachability shortcuts, or generic directional fallbacks. Sideways reachable tiles can be valid server walk targets while still being the wrong side of a fence, gate, or corridor corner. Generic fallback is only appropriate when no route-backed raw point is available. + +**Why this matters:** Tight routes near gates and fences can fail when the walker clicks a tile left or right of the drawn route line. The game then chooses its own path around the obstacle, which fights the planned door/transport handlers and produces stop-start recovery. + +**Pattern to follow:** + +```java +WorldPoint rawTarget = findFurthestRawPathPointMatching(rawPath, playerLoc, reach, rawAnchor, + Rs2Walker::isKnownWalkableOrUnloaded); +WorldPoint clickTarget = rawTarget != null ? rawTarget : getPointWithWallDistance(smoothedTarget); +``` + +**Where this applies:** `Rs2Walker.processWalk`, recovery clicks after false unreachable reports, route continuation clicks, direct short-walk fallbacks, and any minimap click helper used while a shortest-path raw route exists. + +**Defensive check:** Unit-test raw-route target selection with a route that doubles back near an earlier branch; the selected tile must stay at or ahead of the anchored raw route index. + +## 16. Yield before scans while an interim route click is in flight + +Sticky interim targets only reduce click thrash if they are checked before broad route scans. When a fresh interim checkpoint is still far away and the player is moving or has very recent checkpoint progress, yield before raw-scene scans, current-tile transport scans, direct short-walk attempts, and path segment scans. Resume normal route work once the checkpoint is close, stale, expired, on another plane, or movement has genuinely stopped. + +**Why this matters:** Long post-transport routes can otherwise run several no-op scans and select several small follow-up clicks while the previous minimap flag is still carrying the player. That looks like stop-start walking and burns tail iterations even though the route is making progress. + +**Pattern to follow:** + +```java +if (shouldYieldForActiveRouteInterim(playerLoc, path, nowMs)) { + exitReason = "interim-in-flight"; + continue; +} +``` + +**Where this applies:** `Rs2Walker.processWalk` before broad raw-scene handling, current-tile transport handling, direct short-walk handling, and the main path loop. + +**Defensive check:** Start a long route immediately after a transport. While the player is still moving toward the active minimap checkpoint, the walker should log a tail-exempt `interim-in-flight` yield instead of repeated `post_transport_path_selected` clicks for nearby path indices. + +## 17. Dispatch immediate planned transports before idle nudges + +When the current route index is an explicit shortest-path transport edge, let the segment transport handler run even during startup, before route idle nudges or stall recovery clicks. Keep startup door/rockfall probes skipped, but do not postpone teleports, ships, ladders, or other catalog-backed route transports that start at the player's current segment. + +**Why this matters:** Originless spell/item teleports can be the first edge in a route. If startup logic suppresses the segment handler and idle recovery runs first, the walker can sit on the origin, spam no-op route nudges, trigger stall recovery, and only then cast the teleport. + +**Pattern to follow:** + +```java +boolean immediateTransport = hasImmediatePlannedTransportStep(path, routeStartIdx, playerLoc); +if (shouldRunActiveRouteIdleNudge(idleNudgeDue, immediateTransport)) { + tryIssueRouteRecoveryClick(rawPath, path, target, "active route idle nudge"); +} +``` + +**Where this applies:** `Rs2Walker.processWalk` startup handling, active route idle nudges, stall recovery clicks, and segment transport dispatch. + +**Defensive check:** Start a route whose first planned edge is Home Teleport. Logs should show `transport_handoff_enter` for `TELEPORTATION_SPELL` without several `active route idle nudge: clicked=false` entries first. + +## 18. Model conditional paid gates as transports plus blocked edges + +When a gate can be crossed by either a quest unlock or a currency payment, do not model the gate tiles as quest-only restrictions. A tile restriction cannot express "quest complete OR pay coins", and it can prevent the walker from reaching the paid transport origin. Model each crossing as explicit paid/free transports and add a blocked edge override so normal walking cannot bypass the unavailable transport. + +**Why this matters:** The Lumbridge-to-Al Kharid toll gate can be opened freely after Prince Ali Rescue or paid through with 10 coins. A quest-only restriction blocks the paid route for accounts without the quest, while no edge block can let the pathfinder walk through the gate without paying. + +**Pattern to follow:** + +```java +// Wrong: restrict both sides of the gate to one quest state. +3267 3227 0 Prince Ali Rescue + +// Right: transport data decides availability; blocked_edges prevents free walking. +3267 3227 0 3268 3227 0 Pay-toll(10gp);Gate;2786 ... 10 Coins +3267 3227 0 3268 3227 0 Open;Gate;2786 ... Prince Ali Rescue +``` + +**Where this applies:** `transports.tsv`, `blocked_edges.tsv`, `restrictions.tsv`, `PathfinderConfig`, and `Rs2Walker.handleTransports`. + +**Defensive check:** Add resource tests that assert both paid and quest-free transports load, the gate tiles are not quest-only restricted, and the gate edge is blocked without a usable transport. 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 9de5153b3a..9afa31c64c 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 @@ -48,6 +48,7 @@ import net.runelite.client.plugins.microbot.util.leaguetransport.SeasonalTransportHandlers; import net.runelite.client.plugins.microbot.util.logging.Rs2LogRateLimit; import java.util.function.BooleanSupplier; +import java.util.function.Predicate; import org.slf4j.event.Level; import net.runelite.client.plugins.microbot.util.poh.PohTeleports; import net.runelite.client.plugins.microbot.util.poh.PohTransport; @@ -206,6 +207,16 @@ public static WorldPoint getCurrentTarget() { private static volatile long routeProgressAdvancedAtMs = 0L; private static final java.util.Deque expectedTransportDestinations = new ArrayDeque<>(); private static final Set startupPhasesLogged = ConcurrentHashMap.newKeySet(); + private static final Set AL_KHARID_TOLL_GATE_OBJECT_IDS = Set.of( + net.runelite.api.ObjectID.CITY_GATE_2786, + net.runelite.api.ObjectID.CITY_GATE_2787, + net.runelite.api.ObjectID.CITY_GATE_2788, + net.runelite.api.ObjectID.CITY_GATE_2789); + private static final Set AL_KHARID_TOLL_GATE_POINTS = Set.of( + new WorldPoint(3267, 3227, 0), + new WorldPoint(3267, 3228, 0), + new WorldPoint(3268, 3227, 0), + new WorldPoint(3268, 3228, 0)); /** * Max Chebyshev "radius" for Quetzal / near-destination checks — guards use {@code distanceTo2D < OFFSET}. @@ -434,6 +445,15 @@ private static WalkerPhase currentWalkerPhase() { return WalkerPhase.STARTUP; } + private static boolean isClientThread() { + Client client = Microbot.getClient(); + return client != null && client.isClientThread(); + } + + private static int reachedDistanceOrDefault() { + return config != null ? config.reachedDistance() : 10; + } + private static ObstaclePolicy obstaclePolicyForCurrentPhase() { return currentWalkerPhase() == WalkerPhase.STARTUP ? STARTUP_OBSTACLE_POLICY @@ -455,6 +475,11 @@ static boolean shouldSkipStartupPreclickSegmentHandlers(boolean startupBeforeFir return true; } + static boolean shouldRunActiveRouteIdleNudge(boolean idleNudgeDue, + boolean immediateRouteTransportPending) { + return idleNudgeDue && !immediateRouteTransportPending; + } + /** * Same-plane Chebyshev distance from player to {@code dest} strictly less than {@code maxChebyshevExclusive}. * Requires matching {@link WorldPoint#getPlane()} before using {@link WorldPoint#distanceTo2D} — that method only @@ -921,7 +946,7 @@ public static int totalRecalcs() { ); public static boolean walkTo(int x, int y, int plane) { - return walkTo(x, y, plane, config.reachedDistance()); + return walkTo(x, y, plane, reachedDistanceOrDefault()); } /** @@ -937,7 +962,7 @@ public static boolean walkTo(int x, int y, int plane, int distance) { * result is {@code false}, same as any non-arrival outcome. */ public static boolean walkTo(WorldPoint target) { - return walkWithState(target, config.reachedDistance()) == WalkerState.ARRIVED; + return walkWithState(target, reachedDistanceOrDefault()) == WalkerState.ARRIVED; } /** @@ -991,6 +1016,10 @@ public static WalkerState walkWithState(WorldPoint target, int distance) { log.warn("[Walker] walk rejected: null target"); return WalkerState.EXIT; } + if (isClientThread()) { + log.warn("Please do not call the walker from the main thread"); + return WalkerState.EXIT; + } if (!walkerLock.tryLock()) { log.warn("[Walker] concurrent walk request detected, waiting for in-flight walk (held by {}); new target={}", Thread.currentThread().getName(), target); @@ -1031,6 +1060,11 @@ public static WalkerState walkWithStateTry(WorldPoint target, int distance, long log.warn("[Walker] walk rejected: null target"); return WalkerState.EXIT; } + if (isClientThread()) + { + log.warn("Please do not call the walker from the main thread"); + return WalkerState.EXIT; + } if (lockWaitMs < 0) { throw new IllegalArgumentException("lockWaitMs must be >= 0"); @@ -1080,6 +1114,10 @@ public static WalkerState walkWithStateTry(WorldPoint target, int distance, long */ private static WalkerState walkWithStateInternal(WorldPoint target, int distance) { Objects.requireNonNull(target, "walk target"); + if (isClientThread()) { + log.warn("Please do not call the walker from the main thread"); + return WalkerState.EXIT; + } WorldPoint playerLocWalk = Rs2Player.getWorldLocation(); if (playerLocWalk == null) { return WalkerState.MOVING; @@ -1120,11 +1158,6 @@ private static WalkerState walkWithStateInternal(WorldPoint target, int distance idleNudgeStationarySinceMs = System.currentTimeMillis(); lastActiveRouteIdleNudgeAtMs = 0L; - if (Microbot.getClient().isClientThread()) { - log.warn("Please do not call the walker from the main thread"); - return WalkerState.EXIT; - } - closeWorldMap(); if (Rs2Bank.isOpen()) { Rs2Bank.closeBank(); @@ -1138,7 +1171,7 @@ private static WalkerState walkWithStateInternal(WorldPoint target, int distance * @return */ public static WalkerState walkWithState(WorldPoint target) { - return walkWithState(target, config.reachedDistance()); + return walkWithState(target, reachedDistanceOrDefault()); } /** @@ -1306,6 +1339,9 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part } } + int earlyRouteStartIdx = stabilizeRouteProgressIndex(path, walkLoop.closestTileIndex(path), target, walkLoop.playerLoc); + boolean immediateRouteTransportPending = hasImmediatePlannedTransportStep(path, earlyRouteStartIdx, walkLoop.playerLoc); + // Do not clear walk target while a sticky minimap interim is active — breaks // isWalkCancelled and forces EXIT while the flag is still carrying the player. // Partial paths end at an intermediate waypoint (dst still far from {@code target}); @@ -1349,18 +1385,25 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part lastMovedTimeMs = System.currentTimeMillis(); stuckCount = 0; clearInterimTarget("stall-recalc"); - setTarget(target); - if (!Rs2Player.isMoving() && !Rs2Player.isAnimating() && !Rs2Player.isInteracting()) { + if (immediateRouteTransportPending) { + WebWalkLog.spDebug("stall_recovery_suppressed | reason=immediate-route-transport idx={}", earlyRouteStartIdx); + } else if (!Rs2Player.isMoving() && !Rs2Player.isAnimating() && !Rs2Player.isInteracting()) { + setTarget(target); tryIssueRouteRecoveryClick(rawPath, path, target, "stall recovery click"); + continue; + } else { + setTarget(target); + continue; } - continue; } - if (shouldIssueActiveRouteIdleNudge - && tryIssueRouteRecoveryClick(rawPath, path, target, "active route idle nudge")) { - lastAttemptedMinimapClick = null; - lastAttemptedMinimapClickOk = false; - lastAttemptedMinimapClickAtMs = 0L; - continue; + if (shouldRunActiveRouteIdleNudge(shouldIssueActiveRouteIdleNudge, immediateRouteTransportPending)) { + if (tryIssueRouteRecoveryClick(rawPath, path, target, "active route idle nudge")) { + lastAttemptedMinimapClick = null; + lastAttemptedMinimapClickOk = false; + lastAttemptedMinimapClickAtMs = 0L; + continue; + } + lastActiveRouteIdleNudgeAtMs = System.currentTimeMillis(); } if (stuckCount > 10) { var reachable = Rs2Tile.getReachableTilesFromTile(Rs2Player.getWorldLocation(), 5).keySet(); @@ -1462,6 +1505,31 @@ && tryIssueRouteContinuationClick(rawPath, path, target)) { Map doorEdgesAttemptedThisTail = new HashMap<>(); ObstaclePolicy startupPolicy = obstaclePolicyForCurrentPhase(); + WorldPoint activeInterimPlayer = Rs2Player.getWorldLocation(); + long activeInterimNowMs = System.currentTimeMillis(); + if (!Rs2Player.isInteracting() + && !Rs2Player.isAnimating() + && !isDoorInteractionSettling() + && !isTransportInteractionSettling() + && (target == null + || activeInterimPlayer == null + || activeInterimPlayer.distanceTo(target) > immediateFinishTh) + && shouldYieldForActiveRouteInterim(activeInterimPlayer, path, activeInterimNowMs)) { + exitReason = "interim-in-flight"; + WebWalkLog.earlyExit(exitReason, + activeInterimPlayer, + target, + path.get(path.size() - 1), + indexOfStartPoint, + path.size()); + walkerDiag("tail exempt exitReason=%s tailBefore=%d early=true interim=%s", + exitReason, + processWalkTail, + interimTargetWp); + processWalkTail--; + continue; + } + boolean postTransportWindow = lastTransportHandledAtMs > 0 && System.currentTimeMillis() - lastTransportHandledAtMs <= POST_TRANSPORT_PATH_TMARK_WINDOW_MS; boolean allowRawSceneScan = startupPolicy.allowBroadRawHandlers() && rawPath != null && path != null; @@ -1597,6 +1665,8 @@ && tryIssueRouteContinuationClick(rawPath, path, target)) { boolean upcomingNearbyTransport = hasUpcomingNearbyTransportStep(path, i, playerNearSeg, POST_TRANSPORT_RAW_SCAN_TRANSPORT_LOOKAHEAD_EDGES, POST_TRANSPORT_RAW_SCAN_TRANSPORT_MAX_DIST); + boolean startupBeforeFirstClick = currentWalkerPhase() == WalkerPhase.STARTUP; + boolean immediateSegmentTransportStep = hasImmediatePlannedTransportStep(path, i, playerNearSeg); boolean recentDoorAttemptNearSegment = hasRecentDoorAttemptNearIndex(path, i); boolean skipPostTransportSegmentHandlers = recentTransportWindow && !upcomingNearbyTransport @@ -1604,8 +1674,9 @@ && tryIssueRouteContinuationClick(rawPath, path, target)) { && !isDoorInteractionSettling() && !isRecoveryMovementInFlight() && reachableTilesCache.containsKey(currentWorldPoint); - boolean skipStartupPreclickSegmentHandlers = shouldSkipStartupPreclickSegmentHandlers( - currentWalkerPhase() == WalkerPhase.STARTUP, + boolean skipStartupPreclickSegmentHandlers = !immediateSegmentTransportStep + && shouldSkipStartupPreclickSegmentHandlers( + startupBeforeFirstClick, i, indexOfStartPoint, recentDoorAttemptNearSegment, @@ -1625,7 +1696,9 @@ && tryIssueRouteContinuationClick(rawPath, path, target)) { long segmentHandlerStartAt = System.currentTimeMillis(); int rawI = (i < smoothedToRaw.length) ? smoothedToRaw[i] : 0; int rawEnd = rawEndForSmoothedIndex(i, smoothedToRaw, rawPath, path); - if (!Rs2Player.isMoving() && !isDoorInteractionSettling() && !isRecoveryMovementInFlight()) { + boolean startupImmediateTransportOnly = startupBeforeFirstClick && immediateSegmentTransportStep; + if (!startupImmediateTransportOnly + && !Rs2Player.isMoving() && !isDoorInteractionSettling() && !isRecoveryMovementInFlight()) { doorOrTransportResult = handleDoorsInRawSegment(rawPath, rawI, rawEnd, obstaclePolicy.segmentDoorTimeoutMs(), doorEdgesAttemptedThisTail, reachableTilesCache); @@ -1641,7 +1714,8 @@ && tryIssueRouteContinuationClick(rawPath, path, target)) { boolean allowPathAdjacentProbe = !recentTransportWindow || upcomingNearbyTransport || recentDoorAttemptNearSegment; - if (!Rs2Player.isMoving() && obstaclePolicy.allowPathAdjacentProbe() + if (!startupImmediateTransportOnly + && !Rs2Player.isMoving() && obstaclePolicy.allowPathAdjacentProbe() && allowPathAdjacentProbe) { if (tryHandleBlockingPathObjectsWithTimeout(rawPath, rawI, 5, 10, obstaclePolicy.pathAdjacentProbeTimeoutMs(), doorEdgesAttemptedThisTail)) { @@ -1652,8 +1726,10 @@ && tryIssueRouteContinuationClick(rawPath, path, target)) { } } - doorOrTransportResult = handleRockfallInRawSegment(rawPath, rawI, rawEnd, - reachableTilesCache); + if (!startupImmediateTransportOnly) { + doorOrTransportResult = handleRockfallInRawSegment(rawPath, rawI, rawEnd, + reachableTilesCache); + } if (doorOrTransportResult) { tmarkPostTransport("post_transport_segment_handler", target, "stage=rockfall handled=true i=" + i + " ms=" + (System.currentTimeMillis() - segmentHandlerStartAt)); @@ -1661,7 +1737,9 @@ && tryIssueRouteContinuationClick(rawPath, path, target)) { break; } - boolean allowSegmentTransportScan = !recentTransportWindow || upcomingNearbyTransport; + boolean allowSegmentTransportScan = !recentTransportWindow + || upcomingNearbyTransport + || immediateSegmentTransportStep; if ((PohTeleports.isInHouse() || !inInstance) && allowSegmentTransportScan) { doorOrTransportResult = handleTransportsInRawSegment(rawPath, rawI, rawEnd); } @@ -1697,7 +1775,7 @@ && tryIssueRouteContinuationClick(rawPath, path, target)) { if (playerLoc != null) { int unreachableDist = currentWorldPoint.distanceTo2D(playerLoc); if (unreachableDist <= HANDLER_RANGE + 2) { - log.info("[Walker] unreachable path tile near player: tile={} idx={}/{} player={} target={}", + log.info("[Walker] local reachability miss near player; checking blockers/recovery: tile={} idx={}/{} player={} target={}", currentWorldPoint, i, path.size(), playerLoc, target); int edgeIdx = Math.max(indexOfStartPoint, i - 1); @@ -1761,13 +1839,13 @@ && tryIssueRouteContinuationClick(rawPath, path, target)) { } if (handlePendingDoorNearRawPath(rawPath, obstaclePolicy.unreachableDoorTimeoutMs(), doorEdgesAttemptedThisTail, playerLoc, 2, 14)) { - exitReason = "door-handled-unreachable-raw-scan"; + exitReason = "door-handled-local-reachability-raw-scan"; break; } if (handleDoorsInRawSegment(rawPath, rawEdgeStart, rawEdgeEnd, obstaclePolicy.unreachableDoorTimeoutMs(), doorEdgesAttemptedThisTail, null)) { - exitReason = "door-handled-unreachable"; + exitReason = "door-handled-local-reachability"; break; } if (isRecoveryMovementInFlight()) { @@ -1858,8 +1936,12 @@ && handleUnresolvedDoorNearRawPath(rawPath, rawEdgeStart, recoverTarget = path.get(safeIdx); } int rawAnchorIndex = rawIndexForSmoothedIndex(recoverIdx, smoothedToRaw, rawPath); - WorldPoint rawRecoveryTarget = findFurthestReachableRawPathPoint(rawPath, playerLoc, - recoveryMinimapReach - 1, rawAnchorIndex); + WorldPoint rawRecoveryTarget = inInstance ? null : findFurthestRawPathPointMatching( + rawPath, + playerLoc, + recoveryMinimapReach - 1, + rawAnchorIndex, + Rs2Walker::isKnownWalkableOrUnloaded); if (rawRecoveryTarget != null && !rawRecoveryTarget.equals(playerLoc) && (dangerCfg == null @@ -1888,11 +1970,14 @@ && walkFastCanvas(recoverTarget)) { clickedRecoveryTarget = recoverTarget; log.debug("[Walker] unreachable recovery: scene click -> {}", recoverTarget); } - log.info("[Walker] unreachable optimistic recovery: clicked={} to={} pathTile={} idx={}", + log.info("[Walker] route-backed local recovery click: clicked={} to={} pathTile={} idx={}", clicked, clicked ? clickedRecoveryTarget : recoverTarget, currentWorldPoint, recoverIdx); if (clicked) { - markFirstMovementClick("first_recovery_click", target, playerLoc, + markFirstMovementClick("first_local_recovery_click", target, playerLoc, "to=" + compactWorldPoint(clickedRecoveryTarget)); + hintRouteProgressIndex(path, + Math.min(recoverIdx, i + INTERIM_CLOSE_TILES), + target); lastUnreachableRecoveryClickAtMs = System.currentTimeMillis(); // Sticky interim: subsequent iterations travel toward this point via the // interim-in-flight path instead of re-running the (false-negative) @@ -1912,10 +1997,10 @@ && walkFastCanvas(recoverTarget)) { // spurious stall-recalc right after issuing recovery movement. lastMovedTimeMs = System.currentTimeMillis(); stuckCount = 0; - exitReason = "unreachable-recovery-click"; + exitReason = "local-recovery-click"; break; } - exitReason = "tile-unreachable-near-player"; + exitReason = "local-reachability-miss-no-click"; break; } } @@ -2075,9 +2160,21 @@ && walkFastCanvas(recoverTarget)) { } WorldPoint posBefore = playerLoc; - WorldPoint clickTarget = inInstance ? targetWp : getPointWithWallDistance(targetWp); - if (!inInstance) { - int rawAnchorIndex = rawIndexForSmoothedIndex(i, smoothedToRaw, rawPath); + int rawAnchorIndex = rawIndexForSmoothedIndex(i, smoothedToRaw, rawPath); + WorldPoint rawRouteTarget = inInstance ? null : findFurthestRawPathPointMatching( + rawPath, + playerLoc, + MINIMAP_REACH_EUCLIDEAN - 1, + rawAnchorIndex, + Rs2Walker::isKnownWalkableOrUnloaded); + WorldPoint clickTarget; + if (rawRouteTarget != null && !rawRouteTarget.equals(playerLoc)) { + targetWp = rawRouteTarget; + clickTarget = rawRouteTarget; + } else { + clickTarget = inInstance ? targetWp : getPointWithWallDistance(targetWp); + } + if (!inInstance && rawRouteTarget == null) { WorldPoint rawReachableTarget = findFurthestReachableRawPathPoint(rawPath, playerLoc, MINIMAP_REACH_EUCLIDEAN - 1, rawAnchorIndex); if (!Rs2Tile.isTileReachable(clickTarget) && rawReachableTarget != null) { @@ -2103,7 +2200,6 @@ && walkFastCanvas(recoverTarget)) { "to=" + compactWorldPoint(clickTarget)); } markStartupPhase("click_candidate_found", target, "to=" + compactWorldPoint(clickTarget)); - int rawAnchorIndex = rawIndexForSmoothedIndex(i, smoothedToRaw, rawPath); WorldPoint clickedTarget = clickMiniMapOrFallback(rawPath, clickTarget, playerLoc, MINIMAP_REACH_EUCLIDEAN - 1, rawPath == null || rawPath.isEmpty(), rawAnchorIndex); boolean clicked = clickedTarget != null; @@ -2116,6 +2212,9 @@ && walkFastCanvas(recoverTarget)) { if (clicked) { markFirstMovementClick("first_minimap_click", target, posBefore, "to=" + compactWorldPoint(clickedTarget)); + hintRouteProgressIndex(path, + Math.min(targetIdx, i + INTERIM_CLOSE_TILES), + target); interimTargetWp = clickedTarget; interimTargetIdx = targetIdx; interimSetAtMs = nowMs; @@ -2150,6 +2249,9 @@ && walkFastCanvas(recoverTarget)) { MINIMAP_REACH_EUCLIDEAN - 1, false, rawAnchorIndex); if (routeRetryTarget != null) { routeRetryClicked = true; + hintRouteProgressIndex(path, + Math.min(targetIdx, i + INTERIM_CLOSE_TILES), + target); lastAttemptedMinimapClick = routeRetryTarget; lastAttemptedMinimapClickOk = true; lastAttemptedMinimapClickAtMs = System.currentTimeMillis(); @@ -2670,7 +2772,7 @@ private static WorldPoint walkRawPathMiniMapTargetToward(List rawPat WorldPoint playerLoc, int maxEuclidean, int rawAnchorIndex) { - WorldPoint fallback = findFurthestVisibleReachableRawPathPoint(rawPath, playerLoc, + WorldPoint fallback = findFurthestVisibleKnownRawPathPoint(rawPath, playerLoc, maxEuclidean, rawAnchorIndex); if (fallback == null || fallback.equals(playerLoc) || fallback.equals(target)) { return null; @@ -2752,6 +2854,18 @@ static WorldPoint findFurthestReachableRawPathPoint(List rawPath, WorldPoint playerLoc, int maxEuclidean, int rawAnchorIndex) { + Set reachable = playerLoc == null + ? Collections.emptySet() + : Rs2Tile.getReachableTilesFromTile(playerLoc, Math.max(2, maxEuclidean)).keySet(); + return findFurthestRawPathPointMatching(rawPath, playerLoc, maxEuclidean, rawAnchorIndex, + reachable::contains); + } + + static WorldPoint findFurthestRawPathPointMatching(List rawPath, + WorldPoint playerLoc, + int maxEuclidean, + int rawAnchorIndex, + Predicate isCandidate) { if (rawPath == null || rawPath.isEmpty() || playerLoc == null) { return null; } @@ -2763,7 +2877,6 @@ static WorldPoint findFurthestReachableRawPathPoint(List rawPath, int maxSq = maxEuclidean * maxEuclidean; int maxRouteSteps = maxEuclidean + 2; int routeSteps = 0; - Set reachable = Rs2Tile.getReachableTilesFromTile(playerLoc, Math.max(2, maxEuclidean)).keySet(); WorldPoint best = null; for (int rawIndex = closestRawIndex; rawIndex < rawPath.size(); rawIndex++) { WorldPoint candidate = rawPath.get(rawIndex); @@ -2780,58 +2893,31 @@ static WorldPoint findFurthestReachableRawPathPoint(List rawPath, if (euclideanSq(candidate, playerLoc) > maxSq) { break; } - if (reachable.contains(candidate)) { + if (!candidate.equals(playerLoc) && (isCandidate == null || isCandidate.test(candidate))) { best = candidate; } } return best; } - static WorldPoint findFurthestVisibleReachableRawPathPoint(List rawPath, - WorldPoint playerLoc, - int maxEuclidean) { - return findFurthestVisibleReachableRawPathPoint(rawPath, playerLoc, maxEuclidean, -1); + static WorldPoint findFurthestVisibleKnownRawPathPoint(List rawPath, + WorldPoint playerLoc, + int maxEuclidean) { + return findFurthestVisibleKnownRawPathPoint(rawPath, playerLoc, maxEuclidean, -1); } - static WorldPoint findFurthestVisibleReachableRawPathPoint(List rawPath, - WorldPoint playerLoc, - int maxEuclidean, - int rawAnchorIndex) { + static WorldPoint findFurthestVisibleKnownRawPathPoint(List rawPath, + WorldPoint playerLoc, + int maxEuclidean, + int rawAnchorIndex) { if (rawPath == null || rawPath.isEmpty() || playerLoc == null) { return null; } - int closestRawIndex = rawPathForwardAnchorIndex(rawPath, playerLoc, rawAnchorIndex); - if (closestRawIndex < 0) { - return null; - } - int maxSq = maxEuclidean * maxEuclidean; - int maxRouteSteps = maxEuclidean + 2; - int routeSteps = 0; - Set reachable = Rs2Tile.getReachableTilesFromTile(playerLoc, Math.max(2, maxEuclidean)).keySet(); - WorldPoint best = null; - for (int rawIndex = closestRawIndex; rawIndex < rawPath.size(); rawIndex++) { - WorldPoint candidate = rawPath.get(rawIndex); - if (candidate == null || candidate.getPlane() != playerLoc.getPlane()) { - break; - } - if (rawIndex > closestRawIndex) { - WorldPoint previous = rawPath.get(rawIndex - 1); - routeSteps += rawPathStepDistance(previous, candidate); - if (routeSteps > maxRouteSteps) { - break; - } - } - if (euclideanSq(candidate, playerLoc) > maxSq) { - break; - } - if (!candidate.equals(playerLoc) - && reachable.contains(candidate) - && isMiniMapClickable(candidate, 5)) { - best = candidate; - } - } - return best; + return findFurthestRawPathPointMatching(rawPath, playerLoc, maxEuclidean, rawAnchorIndex, + candidate -> !candidate.equals(playerLoc) + && isKnownWalkableOrUnloaded(candidate) + && isMiniMapClickable(candidate, 5)); } private static int rawPathStepDistance(WorldPoint previous, WorldPoint current) { @@ -2943,16 +3029,17 @@ private static boolean tryIssueRouteMovementClick(List rawPath, return false; } int targetIdx = stabilizeRouteProgressIndex(path, getClosestTileIndex(path, playerLoc), target, playerLoc); + int startIdx = Math.max(0, targetIdx); int[] routeSmoothedToRaw = mapSmoothedToRaw(path, rawPath); int rawAnchorIndex = rawIndexForSmoothedIndex(targetIdx, routeSmoothedToRaw, rawPath); - WorldPoint clickTarget = findFurthestReachableRawPathPoint( + WorldPoint clickTarget = findFurthestRawPathPointMatching( rawPath, playerLoc, maxEuclidean, - rawAnchorIndex); + rawAnchorIndex, + Rs2Walker::isKnownWalkableOrUnloaded); if (clickTarget == null) { - int startIdx = Math.max(0, targetIdx); int clickableIdx = findFurthestForwardClickableIndex(path, startIdx, playerLoc, wp -> { Set ts = ShortestPathPlugin.getTransports().get(wp); @@ -2997,6 +3084,9 @@ private static boolean tryIssueRouteMovementClick(List rawPath, target, playerLoc, "to=" + compactWorldPoint(clickedTarget)); + hintRouteProgressIndex(path, + Math.min(targetIdx, startIdx + INTERIM_CLOSE_TILES), + target); interimTargetWp = clickedTarget; interimTargetIdx = targetIdx; interimSetAtMs = System.currentTimeMillis(); @@ -3538,6 +3628,7 @@ private static List applyTransportFiltering(List transport t.getType() == TransportType.BOAT || t.getType() == TransportType.CHARTER_SHIP || t.getType() == TransportType.SHIP || t.getType() == TransportType.MINECART || t.getType() == TransportType.MAGIC_CARPET || t.getType() == TransportType.SPIRIT_TREE || + (t.getType() == TransportType.TRANSPORT && t.getCurrencyAmount() > 0) || (t.getType() == TransportType.SEASONAL_TRANSPORT && Rs2LeaguesTransport.isLeaguesActive() && t.getDisplayInfo() != null @@ -5431,6 +5522,31 @@ private static boolean shouldYieldForActiveRecoveryInterim(WorldPoint playerLoc, Rs2Player.isMoving()); } + private static boolean shouldYieldForActiveRouteInterim(WorldPoint playerLoc, + List path, + long nowMs) { + WorldPoint interim = interimTargetWp; + if (interim == null) { + return false; + } + recordInterimDistanceProgress(interim, playerLoc, nowMs); + if (playerLoc != null && path != null && !path.isEmpty()) { + int bestIdxNow = getClosestTileIndex(path, playerLoc); + if (bestIdxNow > interimLastBestPathIdx) { + interimLastBestPathIdx = bestIdxNow; + interimLastProgressAtMs = nowMs; + } + } + return shouldDeferRouteWorkForActiveInterim(interim, + playerLoc, + interimSetAtMs, + interimLastProgressAtMs, + nowMs, + lastMovedTimeMs, + Rs2Player.isMoving(), + INTERIM_CLOSE_TILES); + } + static boolean shouldYieldForActiveRecoveryInterim(WorldPoint interim, WorldPoint playerLoc, long setAtMs, @@ -5445,16 +5561,46 @@ static boolean shouldYieldForActiveRecoveryInterim(WorldPoint interim, if (shouldClearInterimTarget(interim, playerLoc, setAtMs, lastProgressAtMs, nowMs)) { return false; } - if (playerMoving) { + if (shouldDeferRouteWorkForActiveInterim(interim, + playerLoc, + setAtMs, + lastProgressAtMs, + nowMs, + lastMovedAtMs, + playerMoving, + INTERIM_CLOSE_TILES)) { return true; } - if (isRecentEvent(nowMs, lastProgressAtMs, INTERIM_PROGRESS_TIMEOUT_MS)) { + return isRecentEvent(nowMs, lastRecoveryClickAtMs, RECOVERY_MOVEMENT_IN_FLIGHT_MS); + } + + static boolean shouldDeferRouteWorkForActiveInterim(WorldPoint interim, + WorldPoint playerLoc, + long setAtMs, + long lastProgressAtMs, + long nowMs, + long lastMovedAtMs, + boolean playerMoving, + int handoffTiles) { + if (interim == null) { + return false; + } + if (shouldClearInterimTarget(interim, playerLoc, setAtMs, lastProgressAtMs, nowMs)) { + return false; + } + if (playerLoc == null || playerLoc.getPlane() != interim.getPlane()) { + return false; + } + if (playerLoc.distanceTo2D(interim) <= Math.max(0, handoffTiles)) { + return false; + } + if (playerMoving) { return true; } - if (isRecentEvent(nowMs, lastMovedAtMs, RECOVERY_MOVEMENT_IN_FLIGHT_MS)) { + if (isRecentEvent(nowMs, lastProgressAtMs, INTERIM_PROGRESS_TIMEOUT_MS)) { return true; } - return isRecentEvent(nowMs, lastRecoveryClickAtMs, RECOVERY_MOVEMENT_IN_FLIGHT_MS); + return isRecentEvent(nowMs, lastMovedAtMs, RECOVERY_MOVEMENT_IN_FLIGHT_MS); } private static void clearInterimTarget(String reason) { @@ -5584,6 +5730,20 @@ private static boolean hasExplicitTransportStep(List path, int index return matchesDirectedTransportCatalogEdge(path.get(index), path.get(index + 1)); } + private static boolean hasImmediatePlannedTransportStep(List path, + int routeStartIdx, + WorldPoint playerLoc) { + if (path == null || routeStartIdx < 0 || routeStartIdx >= path.size() - 1 || playerLoc == null) { + return false; + } + WorldPoint origin = path.get(routeStartIdx); + if (origin == null || origin.getPlane() != playerLoc.getPlane() + || origin.distanceTo2D(playerLoc) > HANDLER_RANGE) { + return false; + } + return hasExplicitTransportStep(path, routeStartIdx); + } + /** * Whether this path edge is covered by a transport catalog row (same coordinates loaded from TSV into * {@link ShortestPathPlugin#getTransports()}). Includes strict origin-destination steps (including @@ -7227,6 +7387,35 @@ static int stabilizeRouteProgressIndex(List path, int closestIdx, Wo return routeProgressIdx; } + static void hintRouteProgressIndex(List path, int hintedIdx, WorldPoint target) { + if (path == null || path.isEmpty() || hintedIdx < 0 || hintedIdx >= path.size()) { + return; + } + + WorldPoint pathStart = path.get(0); + WorldPoint pathEnd = path.get(path.size() - 1); + boolean routeChanged = routeProgressTarget == null + || !routeProgressTarget.equals(target) + || routeProgressPathSize != path.size() + || !Objects.equals(routeProgressPathStart, pathStart) + || !Objects.equals(routeProgressPathEnd, pathEnd) + || routeProgressIdx >= path.size(); + if (routeChanged) { + routeProgressTarget = target; + routeProgressPathStart = pathStart; + routeProgressPathEnd = pathEnd; + routeProgressPathSize = path.size(); + routeProgressIdx = hintedIdx; + recordRouteProgressAdvanced(); + return; + } + + if (hintedIdx > routeProgressIdx) { + routeProgressIdx = hintedIdx; + recordRouteProgressAdvanced(); + } + } + static int advanceIndexPastRecentTransportEdge(List path, int index, WorldPoint playerLoc) { if (path == null || path.isEmpty() || index < 0 || index >= path.size() || !isRecentTransportEdgeWindow()) { @@ -7497,7 +7686,10 @@ && recentlyOpenedStationaryDoorOnSegment(path.get(indexOfStartPoint), path.get(i pathFirstIndex.putIfAbsent(path.get(idx), idx); } - for (Transport transport : transports) { + List orderedTransports = new ArrayList<>(transports); + orderedTransports.sort(Comparator.comparingInt(Rs2Walker::transportHandlingPreference)); + + for (Transport transport : orderedTransports) { Collection worldPointCollections; //in some cases the getOrigin is null, for teleports that start the player location if (transport.getOrigin() == null) { @@ -7821,8 +8013,10 @@ && recentlyOpenedStationaryDoorOnSegment(path.get(indexOfStartPoint), path.get(i final boolean allowClosedVariant = "Climb-down".equalsIgnoreCase(transportAction) || "Climb down".equalsIgnoreCase(transportAction); + final boolean allowAlKharidTollGateVariant = isAlKharidTollGateObjectId(transportObjectId); List objects = Rs2GameObject.getAll(o -> { if (o.getId() == transportObjectId) return true; + if (allowAlKharidTollGateVariant && isAlKharidTollGateObjectId(o.getId())) return true; Integer legacyClosed = OPEN_TO_CLOSED_MAPPINGS.get(transportObjectId); if (legacyClosed != null && o.getId() == legacyClosed) return true; if (!allowClosedVariant) return false; @@ -8384,6 +8578,61 @@ static Set adjacentSamePlaneTransportSuppressionPoints(Transport tra return points; } + private static int transportHandlingPreference(Transport transport) { + if (isAlKharidTollGateTransport(transport) && transport.getCurrencyAmount() > 0) { + return 1; + } + return 0; + } + + private static boolean isAlKharidTollGateTransport(Transport transport, TileObject tileObject) { + return isAlKharidTollGateTransport(transport) + && tileObject != null + && isAlKharidTollGateObjectId(tileObject.getId()); + } + + private static boolean isAlKharidTollGateTransport(Transport transport) { + return transport != null + && isAlKharidTollGateObjectId(transport.getObjectId()) + && AL_KHARID_TOLL_GATE_POINTS.contains(transport.getOrigin()) + && AL_KHARID_TOLL_GATE_POINTS.contains(transport.getDestination()); + } + + private static boolean isAlKharidTollGateObjectId(int objectId) { + return AL_KHARID_TOLL_GATE_OBJECT_IDS.contains(objectId); + } + + private static boolean isPayTollAction(String action) { + return action != null && action.toLowerCase(Locale.ROOT).startsWith("pay-toll"); + } + + private static boolean handleAlKharidTollGate(Transport transport) { + if (Rs2Player.isMoving()) { + Rs2Player.waitForWalking(); + } + + boolean confirmed = false; + if (sleepUntil(Rs2Dialogue::hasSelectAnOption, 2500)) { + confirmed = Rs2Dialogue.clickOption("Yes, okay", "Yes"); + } + + boolean reachedDestination = sleepUntil(() -> { + WorldPoint now = Rs2Player.getWorldLocation(); + WorldPoint destination = transport.getDestination(); + return now != null + && destination != null + && now.getPlane() == destination.getPlane() + && now.distanceTo2D(destination) <= 1; + }, POST_HANDLE_OBJECT_LANDING_WAIT_MS); + if (!confirmed && !reachedDestination) { + WebWalkLog.spWarn( + "Al Kharid toll gate confirmation unresolved dest={} at={}", + compactWorldPoint(transport.getDestination()), + compactWorldPoint(Rs2Player.getWorldLocation())); + } + return true; + } + private static boolean handleObjectExceptions(Transport transport, TileObject tileObject) { for (Map.Entry entry : OPEN_TO_CLOSED_MAPPINGS.entrySet()) { final int closedTrapdoorId = entry.getKey(); @@ -8415,6 +8664,10 @@ private static boolean handleObjectExceptions(Transport transport, TileObject ti } } + if (isAlKharidTollGateTransport(transport, tileObject) && isPayTollAction(transport.getAction())) { + return handleAlKharidTollGate(transport); + } + //Al kharid broken wall will animate once and then stop and then animate again if (tileObject.getId() == ObjectID.KHARID_POSHWALL_TOPLESS || tileObject.getId() == ObjectID.KHARID_BIGWINDOW) { Rs2Player.waitForAnimation(); @@ -8623,7 +8876,7 @@ private static boolean handleTeleportItem(Transport transport) { for (Integer itemId : itemIds) { if (Rs2Walker.currentTarget == null) break; // reachedDistance <= 0: do not treat as "already at destination" (legacy: raw distance < 0 never true). - int reachRd = config.reachedDistance(); + int reachRd = reachedDistanceOrDefault(); if (reachRd > 0 && isPlayerWithinChebyshevOf(transport.getDestination(), reachRd)) { break; } @@ -9107,14 +9360,16 @@ private static boolean isStuckTooLong() { * @param start */ public void setStart(WorldPoint start) { - if (ShortestPathPlugin.getPathfinder() == null) { + Pathfinder pathfinder = ShortestPathPlugin.getPathfinder(); + if (pathfinder == null) { return; } + Set targets = pathfinder.getTargets(); ShortestPathPlugin.setStartPointSet(true); - if (Microbot.getClient().isClientThread()) { - Microbot.getClientThread().runOnSeperateThread(() -> restartPathfinding(start, ShortestPathPlugin.getPathfinder().getTargets())); + if (isClientThread()) { + Microbot.getClientThread().runOnSeperateThread(() -> restartPathfinding(start, targets)); } else { - restartPathfinding(start, ShortestPathPlugin.getPathfinder().getTargets()); + restartPathfinding(start, targets); } } @@ -10404,7 +10659,8 @@ private static boolean isCurrencyBasedTransport(TransportType transportType) { || transportType == TransportType.CHARTER_SHIP || transportType == TransportType.SHIP || transportType == TransportType.MINECART - || transportType == TransportType.MAGIC_CARPET; + || transportType == TransportType.MAGIC_CARPET + || transportType == TransportType.TRANSPORT; } private static int getCurrencyItemId(String currencyName) { @@ -10454,7 +10710,7 @@ public static boolean walkWithBankedTransports(WorldPoint target) { return walkWithBankedTransports(target, false); } public static boolean walkWithBankedTransports(WorldPoint target, boolean forceBanking) { - int d = config != null ? config.reachedDistance() : 10; + int d = reachedDistanceOrDefault(); return walkWithBankedTransportsAndState(target, d, forceBanking) == WalkerState.ARRIVED; } public static boolean walkWithBankedTransports(WorldPoint target, int distance, boolean forceBanking){ @@ -10475,7 +10731,7 @@ public static WalkerState walkWithBankedTransportsAndState(WorldPoint target, in log.warn("Cannot travel to null target location"); return WalkerState.EXIT; } - if (Microbot.getClient().isClientThread()) { + if (isClientThread()) { log.error("Please do not call the walker from the main thread"); return WalkerState.EXIT; } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/banking/Rs2WalkerBankingPlanner.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/banking/Rs2WalkerBankingPlanner.java index 64f3c61a2e..287a41bba8 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/banking/Rs2WalkerBankingPlanner.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/banking/Rs2WalkerBankingPlanner.java @@ -80,7 +80,8 @@ public static boolean hasRequiredTransportItems(Transport transport) { || transport.getType() == TransportType.CHARTER_SHIP || transport.getType() == TransportType.SHIP || transport.getType() == TransportType.MINECART - || transport.getType() == TransportType.MAGIC_CARPET) { + || transport.getType() == TransportType.MAGIC_CARPET + || (transport.getType() == TransportType.TRANSPORT && transport.getCurrencyAmount() > 0)) { if (transport.getType() == TransportType.TELEPORTATION_SPELL && transport.getDisplayInfo() != null) { String spellName = transport.getDisplayInfo().contains(":") ? transport.getDisplayInfo().split(":")[0].trim() @@ -405,7 +406,8 @@ private static boolean isCurrencyBasedTransport(TransportType transportType) { || transportType == TransportType.CHARTER_SHIP || transportType == TransportType.SHIP || transportType == TransportType.MINECART - || transportType == TransportType.MAGIC_CARPET; + || transportType == TransportType.MAGIC_CARPET + || transportType == TransportType.TRANSPORT; } private static int getCurrencyItemId(String currencyName) { diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/blocked_edges.tsv b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/blocked_edges.tsv index 49bc9a2141..4b6822e52d 100644 --- a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/blocked_edges.tsv +++ b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/blocked_edges.tsv @@ -1,5 +1,7 @@ # Origin Destination Bidirectional Display info # Static collision edge overrides for map data gaps. These block movement across the edge, not the whole tile. +3267 3227 0 3268 3227 0 true Al Kharid toll gate +3267 3228 0 3268 3228 0 true Al Kharid toll gate 3229 3472 0 3229 3471 0 true Varrock Palace garden south fence 3230 3472 0 3230 3471 0 true Varrock Palace garden south fence 3231 3472 0 3231 3471 0 true Varrock Palace garden south fence diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/restrictions.tsv b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/restrictions.tsv index 539c6d881f..c66bd5a99f 100644 --- a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/restrictions.tsv +++ b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/restrictions.tsv @@ -20,11 +20,7 @@ 3207 3217 0 Recipe for Disaster 3213 3222 0 Recipe for Disaster 3213 3221 0 Recipe for Disaster -# Al Kharid Gate -3267 3228 0 Prince Ali Rescue -3267 3227 0 Prince Ali Rescue -3268 3227 0 Prince Ali Rescue -3268 3228 0 Prince Ali Rescue +# Al Kharid toll gate is modeled as transports plus blocked edge overrides. 2936 9810 0 63 Agility # Tree near Draynor Manor 3138 3352 0 @@ -236,4 +232,4 @@ 2432 3118 0 2431 3118 0 2385 3068 0 -2385 3069 0 \ No newline at end of file +2385 3069 0 diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv index 28fc583725..da52a59975 100644 --- a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv +++ b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv @@ -1018,6 +1018,14 @@ 3097 3432 2 3097 3432 1 Climb-down;Ladder;16685 # Al Kharid +3267 3227 0 3268 3227 0 Pay-toll(10gp);Gate;2786 10 Coins 2 +3267 3228 0 3268 3228 0 Pay-toll(10gp);Gate;2787 10 Coins 2 +3268 3227 0 3267 3227 0 Pay-toll(10gp);Gate;2788 10 Coins 2 +3268 3228 0 3267 3228 0 Pay-toll(10gp);Gate;2789 10 Coins 2 +3267 3227 0 3268 3227 0 Open;Gate;2786 Prince Ali Rescue 1 +3267 3228 0 3268 3228 0 Open;Gate;2787 Prince Ali Rescue 1 +3268 3227 0 3267 3227 0 Open;Gate;2788 Prince Ali Rescue 1 +3268 3228 0 3267 3228 0 Open;Gate;2789 Prince Ali Rescue 1 3298 3124 0 3297 3124 0 Open;Prison door;2692 3297 3124 0 3298 3124 0 Open;Prison door;2692 3303 3117 0 3304 3115 0 Go-through;Shantay pass;4031 1854 2 diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathCoreTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathCoreTest.java index 4adaf188ef..2bbc7972bc 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathCoreTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathCoreTest.java @@ -1,5 +1,6 @@ package net.runelite.client.plugins.microbot.shortestpath; +import net.runelite.api.Quest; import net.runelite.api.VarPlayer; import net.runelite.api.coords.WorldArea; import net.runelite.api.coords.WorldPoint; @@ -14,6 +15,10 @@ public class ShortestPathCoreTest { private static SplitFlagMap collisionMap; + private static final WorldPoint AL_KHARID_GATE_WEST_SOUTH = new WorldPoint(3267, 3227, 0); + private static final WorldPoint AL_KHARID_GATE_WEST_NORTH = new WorldPoint(3267, 3228, 0); + private static final WorldPoint AL_KHARID_GATE_EAST_SOUTH = new WorldPoint(3268, 3227, 0); + private static final WorldPoint AL_KHARID_GATE_EAST_NORTH = new WorldPoint(3268, 3228, 0); @BeforeClass public static void loadCollisionMap() { @@ -193,6 +198,52 @@ public void testTransportLoadingDoesNotThrow() { assertTrue("Should load at least 100 transport origins", transports.size() > 100); } + @Test + public void testAlKharidTollGateTransportsLoaded() { + HashMap> transports = Transport.loadAllFromResources(); + + assertTollGateTransport(transports, AL_KHARID_GATE_WEST_SOUTH, AL_KHARID_GATE_EAST_SOUTH, + "Pay-toll(10gp)", 10, false); + assertTollGateTransport(transports, AL_KHARID_GATE_WEST_NORTH, AL_KHARID_GATE_EAST_NORTH, + "Pay-toll(10gp)", 10, false); + assertTollGateTransport(transports, AL_KHARID_GATE_EAST_SOUTH, AL_KHARID_GATE_WEST_SOUTH, + "Pay-toll(10gp)", 10, false); + assertTollGateTransport(transports, AL_KHARID_GATE_EAST_NORTH, AL_KHARID_GATE_WEST_NORTH, + "Pay-toll(10gp)", 10, false); + + assertTollGateTransport(transports, AL_KHARID_GATE_WEST_SOUTH, AL_KHARID_GATE_EAST_SOUTH, + "Open", 0, true); + assertTollGateTransport(transports, AL_KHARID_GATE_WEST_NORTH, AL_KHARID_GATE_EAST_NORTH, + "Open", 0, true); + assertTollGateTransport(transports, AL_KHARID_GATE_EAST_SOUTH, AL_KHARID_GATE_WEST_SOUTH, + "Open", 0, true); + assertTollGateTransport(transports, AL_KHARID_GATE_EAST_NORTH, AL_KHARID_GATE_WEST_NORTH, + "Open", 0, true); + } + + @Test + public void testAlKharidTollGateIsEdgeBlockedNotTileRestricted() { + Set gateTiles = new HashSet<>(Arrays.asList( + WorldPointUtil.packWorldPoint(AL_KHARID_GATE_WEST_SOUTH), + WorldPointUtil.packWorldPoint(AL_KHARID_GATE_WEST_NORTH), + WorldPointUtil.packWorldPoint(AL_KHARID_GATE_EAST_SOUTH), + WorldPointUtil.packWorldPoint(AL_KHARID_GATE_EAST_NORTH))); + + List restrictions = Restriction.loadAllFromResources(); + assertFalse("Al Kharid gate tiles must not be quest-only restrictions", + restrictions.stream().anyMatch(r -> gateTiles.contains(r.getPackedWorldPoint()))); + + PathfinderConfig config = createMinimalConfig(); + assertTrue("South Al Kharid gate edge should be blocked without transport", + config.isBlockedTransportStep( + WorldPointUtil.packWorldPoint(AL_KHARID_GATE_WEST_SOUTH), + WorldPointUtil.packWorldPoint(AL_KHARID_GATE_EAST_SOUTH))); + assertTrue("North Al Kharid gate edge should be blocked without transport", + config.isBlockedTransportStep( + WorldPointUtil.packWorldPoint(AL_KHARID_GATE_WEST_NORTH), + WorldPointUtil.packWorldPoint(AL_KHARID_GATE_EAST_NORTH))); + } + @Test public void testNewTransportTypesLoaded() { HashMap> transports = Transport.loadAllFromResources(); @@ -259,6 +310,27 @@ && new WorldPoint(3221, 3218, 0).equals(t.getDestination())) return lumbridgeHomeTeleport.get(); } + private static void assertTollGateTransport(HashMap> transports, + WorldPoint origin, WorldPoint destination, String action, int currencyAmount, boolean princeAliRequired) { + Optional match = transports.getOrDefault(origin, Collections.emptySet()).stream() + .filter(t -> destination.equals(t.getDestination())) + .filter(t -> action.equals(t.getAction())) + .filter(t -> "Gate".equals(t.getName())) + .findFirst(); + + assertTrue("Missing Al Kharid toll gate transport " + action + " from " + origin + " to " + destination, + match.isPresent()); + Transport transport = match.get(); + assertEquals("Gate transport should use normal TRANSPORT type", + TransportType.TRANSPORT, transport.getType()); + assertEquals("Unexpected gate currency amount", currencyAmount, transport.getCurrencyAmount()); + if (currencyAmount > 0) { + assertEquals("Unexpected gate currency name", "Coins", transport.getCurrencyName()); + } + assertEquals("Unexpected Prince Ali Rescue requirement on gate transport", + princeAliRequired, transport.getQuests().containsKey(Quest.PRINCE_ALI_RESCUE)); + } + @Test public void testFairyRingTransportsExist() { HashMap> transports = Transport.loadAllFromResources(); diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerUnitTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerUnitTest.java index 5de440bd1e..b117cae7f6 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerUnitTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerUnitTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyInt; @@ -458,6 +459,26 @@ public void stabilizeRouteProgressIndex_doesNotJumpBackToEarlierNearbyBranch() { assertEquals(6, Rs2Walker.stabilizeRouteProgressIndex(path, 6, target, new WorldPoint(3201, 3201, 0))); } + @Test + public void hintRouteProgressIndex_keepsProgressAnchoredAheadOfEarlierBranch() { + WorldPoint target = new WorldPoint(3200, 3201, 0); + List path = Arrays.asList( + new WorldPoint(3200, 3200, 0), + new WorldPoint(3201, 3200, 0), + new WorldPoint(3202, 3200, 0), + new WorldPoint(3203, 3200, 0), + new WorldPoint(3203, 3201, 0), + new WorldPoint(3202, 3201, 0), + new WorldPoint(3201, 3201, 0), + target); + + Rs2Walker.hintRouteProgressIndex(path, 5, target); + + assertEquals("a clicked-ahead route checkpoint should not snap back to a nearby earlier branch", + 5, + Rs2Walker.stabilizeRouteProgressIndex(path, 2, target, new WorldPoint(3202, 3201, 0))); + } + @Test public void findForwardRecoveryIndex_prefersLaterReachableBranch() { WorldPoint player = new WorldPoint(1000, 1000, 0); @@ -508,6 +529,87 @@ public void findFurthestForwardClickableIndex_doesNotBacktrackToEarlierNearbyBra assertEquals("normal route following should interpolate toward the forward tile, not backtrack", 3, idx); } + @Test + public void findFurthestRawPathPointMatching_keepsPrimaryClickOnForwardRawRoute() { + WorldPoint player = new WorldPoint(1000, 1000, 0); + List rawPath = Arrays.asList( + new WorldPoint(998, 1000, 0), + new WorldPoint(999, 1000, 0), + new WorldPoint(1000, 1001, 0), + new WorldPoint(1010, 1000, 0), + new WorldPoint(1009, 1000, 0), + new WorldPoint(1008, 1000, 0)); + + WorldPoint target = Rs2Walker.findFurthestRawPathPointMatching( + rawPath, + player, + 13, + 3, + wp -> true); + + assertEquals("raw-route selection must not snap back to an earlier nearby branch", + new WorldPoint(1008, 1000, 0), target); + } + + @Test + public void findFurthestRawPathPointMatching_honorsCandidatePredicate() { + WorldPoint player = new WorldPoint(3200, 3200, 0); + WorldPoint blocked = new WorldPoint(3203, 3200, 0); + WorldPoint allowed = new WorldPoint(3202, 3200, 0); + List rawPath = Arrays.asList( + new WorldPoint(3200, 3200, 0), + new WorldPoint(3201, 3200, 0), + allowed, + blocked); + + WorldPoint target = Rs2Walker.findFurthestRawPathPointMatching( + rawPath, + player, + 13, + 0, + wp -> !wp.equals(blocked)); + + assertEquals("primary raw-route target must be the furthest acceptable raw tile", + allowed, target); + } + + @Test + public void findFurthestRawPathPointMatching_doesNotReturnCurrentTile() { + WorldPoint player = new WorldPoint(3200, 3200, 0); + List rawPath = Arrays.asList( + player, + new WorldPoint(3215, 3200, 0)); + + WorldPoint target = Rs2Walker.findFurthestRawPathPointMatching( + rawPath, + player, + 10, + 0, + wp -> true); + + assertNull("same-tile route targets are no-op clicks, not recovery candidates", target); + } + + @Test + public void findFurthestRawPathPointMatching_stopsBeforeRouteStepBudgetIsExceeded() { + WorldPoint player = new WorldPoint(3200, 3200, 0); + List rawPath = Arrays.asList( + new WorldPoint(3200, 3200, 0), + new WorldPoint(3205, 3200, 0), + new WorldPoint(3210, 3200, 0), + new WorldPoint(3216, 3200, 0)); + + WorldPoint target = Rs2Walker.findFurthestRawPathPointMatching( + rawPath, + player, + 13, + 0, + wp -> true); + + assertEquals("route step budget keeps one minimap click from spanning too far ahead", + new WorldPoint(3210, 3200, 0), target); + } + @Test public void findFurthestForwardClickableIndex_stopsBeforeTransportOrigin() { WorldPoint player = new WorldPoint(3200, 3200, 0); @@ -791,6 +893,58 @@ public void shouldYieldForActiveRecoveryInterim_recentRecoveryClick_returnsTrue( false)); } + @Test + public void shouldDeferRouteWorkForActiveInterim_movingFarCheckpoint_returnsTrue() { + assertTrue(Rs2Walker.shouldDeferRouteWorkForActiveInterim( + new WorldPoint(2890, 3396, 0), + new WorldPoint(2880, 3396, 0), + 1_000L, + 4_500L, + 5_000L, + 0L, + true, + 5)); + } + + @Test + public void shouldDeferRouteWorkForActiveInterim_recentProgressStoppedFar_returnsTrue() { + assertTrue(Rs2Walker.shouldDeferRouteWorkForActiveInterim( + new WorldPoint(2890, 3396, 0), + new WorldPoint(2880, 3396, 0), + 1_000L, + 4_900L, + 5_000L, + 0L, + false, + 5)); + } + + @Test + public void shouldDeferRouteWorkForActiveInterim_closeCheckpoint_returnsFalse() { + assertFalse(Rs2Walker.shouldDeferRouteWorkForActiveInterim( + new WorldPoint(2890, 3396, 0), + new WorldPoint(2886, 3396, 0), + 1_000L, + 4_500L, + 5_000L, + 0L, + true, + 5)); + } + + @Test + public void shouldDeferRouteWorkForActiveInterim_staleStoppedCheckpoint_returnsFalse() { + assertFalse(Rs2Walker.shouldDeferRouteWorkForActiveInterim( + new WorldPoint(2890, 3396, 0), + new WorldPoint(2880, 3396, 0), + 1_000L, + 1_500L, + 5_000L, + 0L, + false, + 5)); + } + @Test public void interimPreclickTiles_runHandsOffEarlierThanWalk() { assertEquals(6, Rs2Walker.interimPreclickTiles(false)); @@ -805,6 +959,13 @@ public void routeMovementClickPhase_labelsContinuationSeparatelyFromRecovery() { assertEquals("route_movement_click", Rs2Walker.routeMovementClickPhase("other")); } + @Test + public void shouldRunActiveRouteIdleNudge_waitsForImmediateTransport() { + assertFalse(Rs2Walker.shouldRunActiveRouteIdleNudge(true, true)); + assertTrue(Rs2Walker.shouldRunActiveRouteIdleNudge(true, false)); + assertFalse(Rs2Walker.shouldRunActiveRouteIdleNudge(false, false)); + } + @Test public void shouldSkipStartupPreclickSegmentHandlers_skipsBeforeFirstMovementClick() { assertTrue(Rs2Walker.shouldSkipStartupPreclickSegmentHandlers( From fe771870a1255bbee0e008d4f4635d837e312f0b Mon Sep 17 00:00:00 2001 From: infuse21 Date: Sun, 19 Jul 2026 17:26:34 +0100 Subject: [PATCH 4/4] regen client-thread-guardrail-baseline.txt removed toll-gate helper pattern to avoid guardrail violation --- .../microbot/util/walker/Rs2Walker.java | 8 +--- .../client-thread-guardrail-baseline.txt | 48 +++++++++---------- 2 files changed, 24 insertions(+), 32 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 9afa31c64c..6bf2a33f90 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 @@ -8585,12 +8585,6 @@ private static int transportHandlingPreference(Transport transport) { return 0; } - private static boolean isAlKharidTollGateTransport(Transport transport, TileObject tileObject) { - return isAlKharidTollGateTransport(transport) - && tileObject != null - && isAlKharidTollGateObjectId(tileObject.getId()); - } - private static boolean isAlKharidTollGateTransport(Transport transport) { return transport != null && isAlKharidTollGateObjectId(transport.getObjectId()) @@ -8664,7 +8658,7 @@ private static boolean handleObjectExceptions(Transport transport, TileObject ti } } - if (isAlKharidTollGateTransport(transport, tileObject) && isPayTollAction(transport.getAction())) { + if (isAlKharidTollGateTransport(transport) && isPayTollAction(transport.getAction())) { return handleAlKharidTollGate(transport); } 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 a2605ab88d..dab8036f3b 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 @@ -783,6 +783,7 @@ net.runelite.client.plugins.microbot.util.walker.Rs2Walker#invokeCharterDestinat net.runelite.client.plugins.microbot.util.walker.Rs2Walker#invokeCharterDestinationWidget(Widget, String): boolean -> net.runelite.api.widgets.Widget#getIndex(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isCatalogTransportObject(TileObject): boolean -> net.runelite.api.TileObject#getId(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isCatalogTransportObject(TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isClientThread(): boolean -> net.runelite.api.Client#isClientThread(): boolean 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#getImpostorIds(): int[] @@ -797,32 +798,32 @@ net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isPendingRouteDoorObj net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isPendingRouteDoorObject(TileObject, WorldPoint, WorldPoint, WorldPoint, int): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isUnresolvedRouteDoorObject(TileObject, WorldPoint, WorldPoint, WorldPoint, int): boolean -> net.runelite.api.ObjectComposition#getName(): String net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isUnresolvedRouteDoorObject(TileObject, WorldPoint, WorldPoint, WorldPoint, int): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$findDoorNearSegment$40(WorldPoint, WorldPoint, WorldPoint, List, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$findDoorNearSegment$41(WorldPoint, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleDoors$38(WorldPoint, GameObject): boolean -> net.runelite.api.GameObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$findDoorNearSegment$41(WorldPoint, WorldPoint, WorldPoint, List, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$findDoorNearSegment$42(WorldPoint, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleDoors$39(WorldPoint, GameObject): boolean -> net.runelite.api.GameObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleFairyRing$185(Transport, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleMinigameTeleport$154(Widget, Object[]): boolean -> net.runelite.api.widgets.Widget#getOnOpListener(): Object[] -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleMinigameTeleport$156(String): boolean -> net.runelite.api.widgets.Widget#getText(): String -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$132(int, TileObject): boolean -> net.runelite.api.TileObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$138(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$138(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$139(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$139(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleRockfall$21(WorldPoint, Tile): boolean -> net.runelite.api.Tile#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$112(int, boolean, TileObject): boolean -> net.runelite.api.ObjectComposition#getName(): String -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$112(int, boolean, TileObject): boolean -> net.runelite.api.TileObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$114(Transport, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$115(TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$116(Transport, Object): Integer -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleDoors$40(WorldPoint, GameObject): boolean -> net.runelite.api.GameObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleFairyRing$187(Transport, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleMinigameTeleport$156(Widget, Object[]): boolean -> net.runelite.api.widgets.Widget#getOnOpListener(): Object[] +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleMinigameTeleport$158(String): boolean -> net.runelite.api.widgets.Widget#getText(): String +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$134(int, TileObject): boolean -> net.runelite.api.TileObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$140(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$140(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$141(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$141(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleRockfall$22(WorldPoint, Tile): boolean -> net.runelite.api.Tile#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$113(int, boolean, boolean, TileObject): boolean -> net.runelite.api.ObjectComposition#getName(): String +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$113(int, boolean, boolean, TileObject): boolean -> net.runelite.api.TileObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$115(Transport, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$116(TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$117(Transport, Object): Integer -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$119(int, List, TileObject): boolean -> net.runelite.api.TileObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$120(Transport, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleWildernessObelisk$146(Transport, GameObject): boolean -> net.runelite.api.GameObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleWildernessObelisk$147(Transport, GameObject): boolean -> net.runelite.api.GameObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$118(Transport, Object): Integer -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$120(int, List, TileObject): boolean -> net.runelite.api.TileObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$121(Transport, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleWildernessObelisk$148(Transport, GameObject): boolean -> net.runelite.api.GameObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleWildernessObelisk$149(Transport, GameObject): boolean -> net.runelite.api.GameObject#getId(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$processWalk$2(): boolean -> net.runelite.api.widgets.Widget#getSpriteId(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$processWalk$3(): boolean -> net.runelite.api.widgets.Widget#getSpriteId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$tryHandleBlockingPathObjectsWithTimeout$68(WorldPoint, GameObject): boolean -> net.runelite.api.GameObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$tryHandleBlockingPathObjectsWithTimeout$69(WorldPoint, GameObject): boolean -> net.runelite.api.GameObject#getWorldLocation(): WorldPoint net.runelite.client.plugins.microbot.util.walker.Rs2Walker#localPointForWorld(WorldPoint): LocalPoint -> net.runelite.api.Client#getTopLevelWorldView(): WorldView net.runelite.client.plugins.microbot.util.walker.Rs2Walker#localPointForWorld(WorldPoint): LocalPoint -> net.runelite.api.WorldView#isInstance(): boolean net.runelite.client.plugins.microbot.util.walker.Rs2Walker#localPointForWorld(WorldPoint): LocalPoint -> net.runelite.api.coords.LocalPoint#fromWorld(WorldView, WorldPoint): LocalPoint @@ -838,7 +839,6 @@ net.runelite.client.plugins.microbot.util.walker.Rs2Walker#processWalk(WorldPoin net.runelite.client.plugins.microbot.util.walker.Rs2Walker#resolveDoorSegment(List, int): WorldPoint[] -> net.runelite.api.Client#getTopLevelWorldView(): WorldView net.runelite.client.plugins.microbot.util.walker.Rs2Walker#resolveDoorSegment(List, int): WorldPoint[] -> net.runelite.api.Scene#isInstance(): boolean net.runelite.client.plugins.microbot.util.walker.Rs2Walker#resolveDoorSegment(List, int): WorldPoint[] -> net.runelite.api.WorldView#getScene(): Scene -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, String): void -> net.runelite.api.Client#getLocalPlayer(): Player 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 @@ -865,11 +865,9 @@ net.runelite.client.plugins.microbot.util.walker.Rs2Walker#walkFastLocal(LocalPo net.runelite.client.plugins.microbot.util.walker.Rs2Walker#walkFastLocal(LocalPoint): void -> net.runelite.api.WorldView#getPlane(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#walkNextToInstance(GameObject): void -> net.runelite.api.Client#getTopLevelWorldView(): WorldView net.runelite.client.plugins.microbot.util.walker.Rs2Walker#walkNextToInstance(GameObject): void -> net.runelite.api.coords.LocalPoint#fromWorld(WorldView, WorldPoint): LocalPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#walkWithBankedTransportsAndState(WorldPoint, int, boolean): WalkerState -> net.runelite.api.Client#isClientThread(): boolean net.runelite.client.plugins.microbot.util.walker.Rs2Walker#walkWithBankedTransportsAndStateLocked(WorldPoint, int, boolean): WalkerState -> net.runelite.api.Client#getTopLevelWorldView(): WorldView net.runelite.client.plugins.microbot.util.walker.Rs2Walker#walkWithBankedTransportsAndStateLocked(WorldPoint, int, boolean): WalkerState -> net.runelite.api.coords.LocalPoint#fromWorld(WorldView, WorldPoint): LocalPoint 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.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