From 9ae1a76cc647370e46cd5d653c9b05e908e97517 Mon Sep 17 00:00:00 2001 From: Sami Date: Sun, 10 May 2026 16:07:07 +0200 Subject: [PATCH 1/3] refactor(pathfinder): enhance transport edge handling and pathfinding logic --- docs/F2P_WEBWALKER_HARNESS.md | 1 + docs/entity-guides/movement.md | 102 +++++ .../agentserver/handler/WalkHandler.java | 4 + .../shortestpath/ShortestPathPlugin.java | 1 + .../shortestpath/pathfinder/CollisionMap.java | 1 + .../shortestpath/pathfinder/PathSmoother.java | 18 +- .../shortestpath/pathfinder/Pathfinder.java | 2 +- .../pathfinder/PathfinderConfig.java | 105 ++++- .../webwalker/F2PWebWalkerHarnessPlugin.java | 202 +++++++-- .../testing/webwalker/F2PWebWalkerRoute.java | 38 +- .../GeLumbridgeTeleportHarnessPlugin.java | 384 ++++++++++++++++++ .../client/plugins/microbot/util/Global.java | 31 +- .../microbot/util/walker/Rs2Walker.java | 187 +++++++-- .../shortestpath/teleportation_spells.tsv | 4 +- .../shortestpath/ShortestPathCoreTest.java | 199 ++++++++- .../util/walker/Rs2WalkerUnitTest.java | 123 ++++++ scripts/run-ge-lumbridge-teleport-harness.sh | 63 +++ 17 files changed, 1357 insertions(+), 108 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/GeLumbridgeTeleportHarnessPlugin.java create mode 100755 scripts/run-ge-lumbridge-teleport-harness.sh diff --git a/docs/F2P_WEBWALKER_HARNESS.md b/docs/F2P_WEBWALKER_HARNESS.md index 694da873f2..62a9774565 100644 --- a/docs/F2P_WEBWALKER_HARNESS.md +++ b/docs/F2P_WEBWALKER_HARNESS.md @@ -64,3 +64,4 @@ The runner forwards route settings through `microbot.test.webwalker.*` system pr | F2P-14 | `3092,3245,0` | `3109,3341,0` | Draynor Manor approach | | F2P-15 | `3109,3341,0` | `3106,3363,0` | Draynor Manor door/object handling | | F2P-16 | `3106,3363,0` | `3092,3245,0` | Reverse manor exit behavior | +| F2P-17 | current live player tile | `3237,9858,0` | Captures current origin, then walks to Varrock Sewers 5 times on a F2P world with agility shortcuts and teleports disabled | diff --git a/docs/entity-guides/movement.md b/docs/entity-guides/movement.md index 23252886d8..470dba4efd 100644 --- a/docs/entity-guides/movement.md +++ b/docs/entity-guides/movement.md @@ -76,3 +76,105 @@ waitForDoorInteractionProgress(fromWp, toWp); **Where this applies:** `Rs2Walker.handleNearbyRawPathSceneObjects`, door handlers that call `Rs2GameObject.interact`, and any recovery logic that recurses into `processWalk`. **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. + +## 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. + +**Why this matters:** Leaving Draynor Manor through the east/back door can alternate between `3123,3360,0` and `3123,3361,0`, repeatedly logging raw-path/current-tile transport handling and burning the route timeout before walking back to Draynor. + +**Pattern to follow:** + +```java +boolean reachedDestination = sleepUntil(() -> atTransportDestination(transport), 5000); +if (reachedDestination && isAdjacentSamePlaneTransport(transport)) { + markStationaryDoorOpened(transport.getOrigin()); + markStationaryDoorOpened(transport.getDestination()); +} +``` + +**Where this applies:** `Rs2Walker.handleTransports`, current-tile transport recovery, raw-path transport probing, and bidirectional same-plane door/gate transports. + +**Defensive check:** A successful adjacent same-plane transport should be followed by a minimap/path step away from the doorway, not by alternating `Raw path transport handler` and `Current-tile transport handler` logs for the same two tiles. + +## 6. Recalculate after long-distance object transports + +Not every large map transition changes plane or uses a teleport type. Some object transports, such as the Varrock Sewers ladder, remain on plane 0 while jumping between coordinate bands. After a successful object interaction reaches one of these destinations, run the normal transport finalizer so the shortest path is rebuilt from the new location. + +**Why this matters:** A route from Varrock Sewers back to a surface origin can climb the ladder successfully, then continue using a path that was calculated from the underground coordinate band. The walker may drift off path or exit during setup even though the transport itself worked. + +**Pattern to follow:** + +```java +if (reachedDestination) { + markAdjacentSamePlaneTransportHandled(transport, object); + return finishHandledTransport(transport); +} +``` + +**Where this applies:** `Rs2Walker.handleTransports` object interactions and any object-transport handler that waits for the destination tile directly. + +**Defensive check:** Same-plane object transports with a large `distanceTo2D` delta should produce a fresh pathfinder start near the post-transport player location before the next minimap step. + +## 7. Model missing collision edges before tuning walker retries + +Some static collision gaps are specific edges, not whole tiles. If the pathfinder repeatedly routes through a visible fence/wall and the live client keeps clicking fallback tiles near that boundary, add an explicit blocked edge to pathfinding and smoothing instead of trying to solve it with longer timeouts or broader minimap fallback. + +**Why this matters:** The Varrock Palace garden south fence can be missing from the bundled collision map near `3229..3241,3472 -> 3471`. A no-agility F2P route to the Varrock Sewers manhole can walk around the trellis correctly, then stall against that garden boundary because the path says the south edge is traversable. + +**Pattern to follow:** + +```java +if (config.isBlockedTransportEdge(node.packedPosition, neighborPacked)) { + continue; +} +``` + +**Where this applies:** `CollisionMap.getNeighbors`, `PathSmoother.lineOfSight`, and any path data correction where only one edge between adjacent tiles is invalid. + +**Defensive check:** Add a core pathfinder regression from the observed stuck tile; assert neither the raw path nor smoothed path crosses the blocked edge, and that the route still reaches the original destination. + +## 8. Do not click a visible endpoint before honoring pending route interactions + +An endpoint being visible on the minimap does not mean it is the next correct click. If the computed shortest path reaches that endpoint through an intermediate door, gate, transport, shortcut, ladder, or other route object, the walker must process the first route interaction before issuing a direct endpoint click. + +**Why this matters:** From Varrock Palace, a destination such as `3229,3473,0` can be visible on the minimap while the shorter route requires opening the palace doors first. Clicking the endpoint lets the game choose a longer collision-valid detour and bypasses the webwalker's route. + +**Pattern to follow:** + +```java +if (handleNearbyRawPathSceneObjects(rawPath, HANDLER_RANGE)) { + return true; +} +if (!hasPendingExplicitTransportStepBeforeArrival(rawPath, target, distance) + && !localRouteDetoursFromComputedRoute(rawPath, end, DIRECT_CLICK_MAX_DISTANCE)) { + walkMiniMap(end); +} +``` + +**Where this applies:** `Rs2Walker.walkWithStateInternal`, short local walk kick-starts, final/minimap endpoint clicks, and any future fast-path that bypasses normal path iteration. + +**Defensive check:** Reproduce with closed Varrock Palace doors toward `3229,3473,0`; the first action should target the door or route waypoint, not the final endpoint tile. + +## 9. Preserve interrupts so walker cancellation stops waits immediately + +Ctrl+X and script shutdown cancel the active walk task with `Future.cancel(true)` and clear the walker target. Shared sleep/poll helpers must preserve the interrupted flag and stop polling when interruption is observed; otherwise the walker can continue through several timeout cycles before noticing the cleared target. + +**Why this matters:** A user pressing Ctrl+X expects the webwalker to stop issuing route actions immediately. If `InterruptedException` is swallowed, long waits in object, transport, dialogue, or animation handling can keep cycling until their normal timeout elapses. + +**Pattern to follow:** + +```java +try { + Thread.sleep(delayMs); +} catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); +} +while (!Thread.currentThread().isInterrupted() && !condition.getAsBoolean()) { + sleep(pollMs); +} +``` + +**Where this applies:** `Global.sleep*`, `Global.sleepUntil*`, `Rs2Walker.setTarget(null)`, and any walker helper that waits after clicking a door, shortcut, transport, or minimap tile. + +**Defensive check:** Start a long webwalk, press Ctrl+X during movement or a route-object wait, and verify no additional path recalculations or route-object interactions occur after the cancel log. diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/WalkHandler.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/WalkHandler.java index 2f71c081bb..063ab2d043 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/WalkHandler.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/WalkHandler.java @@ -185,6 +185,10 @@ private static synchronized Future submitWalk(WorldPoint destinatio && reachedDistance == activeWalkReachedDistance) { return activeWalk; } + if (activeWalk != null && !activeWalk.isDone()) { + activeWalk.cancel(true); + Rs2Walker.setTarget(null); + } activeWalkTarget = destination; activeWalkReachedDistance = reachedDistance; activeWalk = WALK_EXECUTOR.submit(() -> Rs2Walker.walkWithState(destination, reachedDistance)); 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 fa6f78fd45..9aaddaa302 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 @@ -1027,6 +1027,7 @@ public void keyPressed(KeyEvent e) { */ if (e.getKeyCode() == KeyEvent.VK_X && e.isControlDown()) { shortestPathScript.setTriggerWalker(null); + e.consume(); } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/CollisionMap.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/CollisionMap.java index 74a88396e6..17b0e1b689 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/CollisionMap.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/CollisionMap.java @@ -232,6 +232,7 @@ public List getNeighbors(Node node, VisitedTiles visited, PathfinderConfig if (visited.get(neighborPacked)) continue; if (config.getRestrictedPointsPacked().contains(neighborPacked)) continue; if (config.getCustomRestrictions().contains(neighborPacked)) continue; + if (config.isBlockedTransportStep(node.packedPosition, neighborPacked)) continue; if (ignoreCollisionPacked.contains(node.packedPosition)) { neighbors.add(new Node(neighborPacked, node)); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathSmoother.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathSmoother.java index ea9dca7f49..7062d1844c 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathSmoother.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathSmoother.java @@ -1,6 +1,7 @@ package net.runelite.client.plugins.microbot.shortestpath.pathfinder; import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.shortestpath.WorldPointUtil; import java.util.ArrayList; import java.util.Collections; @@ -46,12 +47,20 @@ public static List smooth(List path, CollisionMap map) { } public static List smooth(List path, CollisionMap map, Set transportAnchors) { + return smooth(path, map, transportAnchors, Collections.emptySet()); + } + + public static List smooth(List path, CollisionMap map, Set transportAnchors, + Set blockedTransportEdges) { if (path == null || path.size() < 3 || map == null) { return path; } if (transportAnchors == null) { transportAnchors = Collections.emptySet(); } + if (blockedTransportEdges == null) { + blockedTransportEdges = Collections.emptySet(); + } final int n = path.size(); List result = new ArrayList<>(n); @@ -64,7 +73,7 @@ public static List smooth(List path, CollisionMap map, S && !transportAnchors.contains(path.get(j)) && isChebyshevAdjacentSamePlane(path.get(j), path.get(j + 1)) && chebyshev(path.get(i), path.get(j + 1)) <= MAX_SEGMENT_CHEBYSHEV - && lineOfSight(path.get(i), path.get(j + 1), map)) { + && lineOfSight(path.get(i), path.get(j + 1), map, blockedTransportEdges)) { j++; } result.add(path.get(j)); @@ -83,7 +92,7 @@ private static int chebyshev(WorldPoint a, WorldPoint b) { return Math.max(Math.abs(a.getX() - b.getX()), Math.abs(a.getY() - b.getY())); } - private static boolean lineOfSight(WorldPoint from, WorldPoint to, CollisionMap map) { + private static boolean lineOfSight(WorldPoint from, WorldPoint to, CollisionMap map, Set blockedTransportEdges) { if (from.getPlane() != to.getPlane()) return false; final int z = from.getPlane(); int x = from.getX(); @@ -93,6 +102,11 @@ private static boolean lineOfSight(WorldPoint from, WorldPoint to, CollisionMap while (x != tx || y != ty) { int dx = Integer.signum(tx - x); int dy = Integer.signum(ty - y); + int fromPacked = WorldPointUtil.packWorldPoint(x, y, z); + int toPacked = WorldPointUtil.packWorldPoint(x + dx, y + dy, z); + if (PathfinderConfig.isBlockedTransportStep(fromPacked, toPacked, blockedTransportEdges)) { + return false; + } if (!map.canStep(x, y, z, dx, dy)) { return false; } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java index 789f67ac02..b21426978c 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java @@ -146,7 +146,7 @@ public List getWalkablePath() { return raw; } if (!smoothed) { - smoothedPath = PathSmoother.smooth(raw, map, buildTransportAnchors(raw)); + smoothedPath = PathSmoother.smooth(raw, map, buildTransportAnchors(raw), config.getBlockedTransportEdgesPacked()); smoothed = true; } return smoothedPath; 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 ef68324dfb..567c10dcca 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 @@ -51,9 +51,10 @@ public class PathfinderConfig { private static final WorldArea NOT_WILDERNESS_4 = new WorldArea(3031, 3525, 2, 2, 0); private static final WorldPoint SPIRIT_TREE_ETCETERIA = new WorldPoint(2613, 3855, 0); private static final WorldPoint SPIRIT_TREE_BRIMHAVEN = new WorldPoint(2800, 3203, 0); - private static final WorldPoint SPIRIT_TREE_PORT_SARIM = new WorldPoint(3058, 3257, 0); + private static final WorldPoint SPIRIT_TREE_PORT_SARIM = new WorldPoint(3058, 3257, 0); private static final WorldPoint SPIRIT_TREE_HOSIDIUS = new WorldPoint(1693, 3540, 0); private static final WorldPoint SPIRIT_TREE_FARMING_GUILD = new WorldPoint(1251, 3750, 0); + private static final Set STATIC_BLOCKED_EDGES_PACKED = createStaticBlockedEdges(); private final SplitFlagMap mapData; private final ThreadLocal map; @@ -71,6 +72,8 @@ public class PathfinderConfig { // Copy of transports with packed positions for the hotpath; lists are not copied and are the same reference in both maps @Getter private final PrimitiveIntHashMap> transportsPacked; + @Getter + private final Set blockedTransportEdgesPacked; private final Client client; private final ShortestPathConfig config; @@ -149,6 +152,8 @@ public PathfinderConfig(SplitFlagMap mapData, Map> tr this.usableTeleports = ConcurrentHashMap.newKeySet(allTransports.size() / 20); this.transports = new ConcurrentHashMap<>(allTransports.size() / 2); this.transportsPacked = new PrimitiveIntHashMap<>(allTransports.size() / 2); + this.blockedTransportEdgesPacked = ConcurrentHashMap.newKeySet(); + addStaticBlockedEdges(); this.client = client; this.config = config; //START microbot variables @@ -287,6 +292,8 @@ private void refreshTransports(WorldPoint target) { transports.clear(); transportsPacked.clear(); + blockedTransportEdgesPacked.clear(); + addStaticBlockedEdges(); usableTeleports.clear(); long mergeStart = System.currentTimeMillis(); @@ -358,7 +365,10 @@ private void refreshTransports(WorldPoint target) { stats[2] += (int)(elapsed / 1_000); if (usable) stats[1]++; - if (!usable) continue; + if (!usable) { + addBlockedTransportEdgeIfNeeded(transport); + continue; + } checkedTransports++; if (point == null) { usableTeleports.add(transport); @@ -400,6 +410,87 @@ private void refreshTransports(WorldPoint target) { Microbot.getVarbitValue(10032)); } + public boolean isBlockedTransportEdge(int originPacked, int destinationPacked) { + return blockedTransportEdgesPacked.contains(transportEdgeKey(originPacked, destinationPacked)); + } + + public boolean isBlockedTransportStep(int originPacked, int destinationPacked) { + return isBlockedTransportStep(originPacked, destinationPacked, blockedTransportEdgesPacked); + } + + static boolean isBlockedTransportStep(int originPacked, int destinationPacked, Set blockedEdges) { + if (blockedEdges == null || blockedEdges.isEmpty()) { + return false; + } + if (blockedEdges.contains(transportEdgeKey(originPacked, destinationPacked))) { + return true; + } + + int ox = WorldPointUtil.unpackWorldX(originPacked); + int oy = WorldPointUtil.unpackWorldY(originPacked); + int oz = WorldPointUtil.unpackWorldPlane(originPacked); + int dx = Integer.signum(WorldPointUtil.unpackWorldX(destinationPacked) - ox); + int dy = Integer.signum(WorldPointUtil.unpackWorldY(destinationPacked) - oy); + int dz = WorldPointUtil.unpackWorldPlane(destinationPacked) - oz; + if (dz != 0 || dx == 0 || dy == 0) { + return false; + } + + int xThenY = WorldPointUtil.packWorldPoint(ox + dx, oy, oz); + int yThenX = WorldPointUtil.packWorldPoint(ox, oy + dy, oz); + return blockedEdges.contains(transportEdgeKey(originPacked, xThenY)) + || blockedEdges.contains(transportEdgeKey(xThenY, destinationPacked)) + || blockedEdges.contains(transportEdgeKey(originPacked, yThenX)) + || blockedEdges.contains(transportEdgeKey(yThenX, destinationPacked)); + } + + public void addBlockedTransportEdgeIfNeeded(Transport transport) { + if (!blocksWalkingEdgeWhenUnavailable(transport)) { + return; + } + addBlockedEdge(transport.getOrigin(), transport.getDestination()); + } + + static long transportEdgeKey(int originPacked, int destinationPacked) { + return ((long) originPacked << 32) ^ (destinationPacked & 0xffffffffL); + } + + private void addStaticBlockedEdges() { + blockedTransportEdgesPacked.addAll(STATIC_BLOCKED_EDGES_PACKED); + } + + private void addBlockedEdge(WorldPoint origin, WorldPoint destination) { + blockedTransportEdgesPacked.add(transportEdgeKey( + WorldPointUtil.packWorldPoint(origin), + WorldPointUtil.packWorldPoint(destination))); + } + + private static Set createStaticBlockedEdges() { + Set edges = new HashSet<>(); + // The Varrock Palace garden south fence is underrepresented in the static + // collision data. Without these edges, no-agility F2P routes can try to walk + // straight through the garden boundary toward the Varrock Sewers manhole. + for (int x = 3229; x <= 3241; x++) { + addBidirectionalStaticEdge(edges, x, 3472, 0, x, 3471, 0); + } + return Collections.unmodifiableSet(edges); + } + + private static void addBidirectionalStaticEdge(Set edges, int ax, int ay, int az, int bx, int by, int bz) { + int a = WorldPointUtil.packWorldPoint(ax, ay, az); + int b = WorldPointUtil.packWorldPoint(bx, by, bz); + edges.add(transportEdgeKey(a, b)); + edges.add(transportEdgeKey(b, a)); + } + + private static boolean blocksWalkingEdgeWhenUnavailable(Transport transport) { + if (transport == null || transport.getOrigin() == null || transport.getDestination() == null) { + return false; + } + return transport.getType() == TransportType.AGILITY_SHORTCUT + || transport.getType() == TransportType.GRAPPLE_SHORTCUT; + } + private Map> createMergedList() { if (!usePoh) return allTransports; @@ -465,7 +556,7 @@ private void refreshRestrictionData() { return true; } // Varplayer check - if (entry.getVarplayers().stream().anyMatch(varplayerCheck -> !varplayerCheck.matches(Microbot.getVarbitPlayerValue(varplayerCheck.getVarplayerId())))) { + if (entry.getVarplayers().stream().anyMatch(varplayerCheck -> !varplayerCheck.matches(getLiveVarplayerValue(varplayerCheck.getVarplayerId())))) { return true; } // Skill level check @@ -562,7 +653,13 @@ private boolean varbitChecks(Transport transport) { private boolean varplayerChecks(Transport transport) { return transport.getVarplayers().isEmpty() || transport.getVarplayers().stream() - .allMatch(varplayerCheck -> varplayerCheck.matches(Microbot.getVarbitPlayerValue(varplayerCheck.getVarplayerId()))); + .allMatch(varplayerCheck -> varplayerCheck.matches(getLiveVarplayerValue(varplayerCheck.getVarplayerId()))); + } + + private int getLiveVarplayerValue(int varplayerId) { + return Microbot.getClientThread() + .runOnClientThreadOptional(() -> client.getVarpValue(varplayerId)) + .orElse(0); } private boolean useTransport(Transport transport) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/F2PWebWalkerHarnessPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/F2PWebWalkerHarnessPlugin.java index 3bc1a6d7c4..e2c48d229a 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/F2PWebWalkerHarnessPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/F2PWebWalkerHarnessPlugin.java @@ -4,6 +4,7 @@ import com.google.inject.Inject; import lombok.extern.slf4j.Slf4j; import net.runelite.api.Player; +import net.runelite.api.WorldType; import net.runelite.api.coords.WorldPoint; import net.runelite.api.events.GameTick; import net.runelite.client.eventbus.EventBus; @@ -106,8 +107,6 @@ private void runHarness() { return; } - applyShortestPathOverrides(); - log.info("[F2PWebWalkerHarness] Starting {} route(s): {}", routes.size(), result.selectedRoutes); for (F2PWebWalkerRoute route : routes) { if (Thread.currentThread().isInterrupted()) { @@ -116,6 +115,7 @@ private void runHarness() { break; } + applyShortestPathOverrides(route); RouteOutcome outcome = runRoute(route, result.walkTimeoutMs); result.routes.add(outcome); @@ -145,11 +145,16 @@ private RouteOutcome runRoute(F2PWebWalkerRoute route, int walkTimeoutMs) { RouteOutcome outcome = new RouteOutcome(); outcome.id = route.id; outcome.name = route.name; - outcome.start = format(route.start); outcome.destination = format(route.destination); outcome.startTolerance = route.startTolerance; outcome.destinationTolerance = route.destinationTolerance; + outcome.repetitions = route.repetitions; + outcome.currentLocationStart = route.currentLocationStart; + outcome.requireF2PWorld = route.requireF2PWorld; + outcome.forceNoAgilityShortcuts = route.forceNoAgilityShortcuts; + outcome.forceNoTeleports = route.forceNoTeleports; outcome.startedAt = Instant.now().toString(); + outcome.setupPassed = true; log.info("[F2PWebWalkerHarness] Running {}: {} -> {}", route.id, route.start, route.destination); @@ -161,54 +166,116 @@ private RouteOutcome runRoute(F2PWebWalkerRoute route, int walkTimeoutMs) { return outcome; } - int initialDistance = distance(current, route.start); - outcome.initialDistanceToStart = initialDistance; - if (initialDistance > route.startTolerance) { - long setupStart = System.currentTimeMillis(); - outcome.setupState = walk(route.start, route.startTolerance, walkTimeoutMs); - outcome.setupDurationMs = System.currentTimeMillis() - setupStart; - } else { - outcome.setupState = WalkerState.ARRIVED.name(); - outcome.setupDurationMs = 0; + boolean membersWorld = Microbot.getClient().getWorldType().contains(WorldType.MEMBERS); + outcome.membersWorld = membersWorld; + if (route.requireF2PWorld && membersWorld) { + outcome.setupPassed = false; + outcome.setupError = "Route " + route.id + " requires a F2P world, but the client is on a members world"; + outcome.error = outcome.setupError; + outcome.finishedAt = Instant.now().toString(); + return outcome; } - WorldPoint beforeRoute = safeLocation(); - outcome.routeStartLocation = format(beforeRoute); - outcome.distanceToStartAfterSetup = distance(beforeRoute, route.start); - outcome.setupPassed = WalkerState.ARRIVED.name().equals(outcome.setupState) - && outcome.distanceToStartAfterSetup <= route.startTolerance; - if (!outcome.setupPassed) { - outcome.setupError = "Setup webwalk failed for " + route.id - + ": state=" + outcome.setupState - + ", location=" + outcome.routeStartLocation - + ", distanceToStart=" + outcome.distanceToStartAfterSetup; + WorldPoint start = route.currentLocationStart ? current : route.start; + outcome.start = format(start); + if (start == null) { + outcome.setupPassed = false; + outcome.setupError = "Start location unavailable for " + route.id; outcome.error = outcome.setupError; outcome.finishedAt = Instant.now().toString(); return outcome; } - long walkStart = System.currentTimeMillis(); - outcome.walkerState = walk(route.destination, route.destinationTolerance, walkTimeoutMs); - outcome.walkDurationMs = System.currentTimeMillis() - walkStart; + for (int i = 1; i <= route.repetitions; i++) { + RouteAttemptOutcome attempt = runAttempt(route, start, i, walkTimeoutMs); + outcome.attempts.add(attempt); + outcome.initialDistanceToStart = i == 1 ? attempt.initialDistanceToStart : outcome.initialDistanceToStart; + outcome.setupState = attempt.setupState; + outcome.setupDurationMs += attempt.setupDurationMs; + outcome.routeStartLocation = attempt.routeStartLocation; + outcome.distanceToStartAfterSetup = attempt.distanceToStartAfterSetup; + outcome.walkerState = attempt.walkerState; + outcome.walkDurationMs += attempt.walkDurationMs; + outcome.endLocation = attempt.endLocation; + outcome.distanceToDestination = attempt.distanceToDestination; + + if (!attempt.setupPassed) { + outcome.setupPassed = false; + outcome.setupError = attempt.setupError; + outcome.error = attempt.setupError; + break; + } - WorldPoint end = safeLocation(); - outcome.endLocation = format(end); - outcome.distanceToDestination = distance(end, route.destination); - outcome.passed = WalkerState.ARRIVED.name().equals(outcome.walkerState) - && outcome.distanceToDestination <= route.destinationTolerance; - if (!outcome.passed) { - outcome.error = "Route webwalk failed for " + route.id - + ": state=" + outcome.walkerState - + ", location=" + outcome.endLocation - + ", distanceToDestination=" + outcome.distanceToDestination; + if (!attempt.passed) { + outcome.error = attempt.error; + break; + } + + outcome.successfulAttempts++; } + outcome.passed = outcome.setupPassed && outcome.successfulAttempts == route.repetitions; outcome.finishedAt = Instant.now().toString(); - log.info("[F2PWebWalkerHarness] {} finished: state={}, end={}, distance={}, duration={}ms", - route.id, outcome.walkerState, outcome.endLocation, outcome.distanceToDestination, outcome.walkDurationMs); + log.info("[F2PWebWalkerHarness] {} finished: passed={}, attempts={}/{}, state={}, end={}, distance={}, duration={}ms", + route.id, outcome.passed, outcome.successfulAttempts, route.repetitions, outcome.walkerState, + outcome.endLocation, outcome.distanceToDestination, outcome.walkDurationMs); return outcome; } + private RouteAttemptOutcome runAttempt(F2PWebWalkerRoute route, WorldPoint start, int attemptNumber, int walkTimeoutMs) { + RouteAttemptOutcome attempt = new RouteAttemptOutcome(); + attempt.attempt = attemptNumber; + attempt.start = format(start); + attempt.destination = format(route.destination); + attempt.startedAt = Instant.now().toString(); + + WorldPoint current = safeLocation(); + attempt.initialLocation = format(current); + attempt.initialDistanceToStart = distance(current, start); + if (attempt.initialDistanceToStart > route.startTolerance) { + long setupStart = System.currentTimeMillis(); + attempt.setupState = walk(start, route.startTolerance, walkTimeoutMs); + attempt.setupDurationMs = System.currentTimeMillis() - setupStart; + } else { + attempt.setupState = WalkerState.ARRIVED.name(); + attempt.setupDurationMs = 0; + } + + WorldPoint beforeRoute = safeLocation(); + attempt.routeStartLocation = format(beforeRoute); + attempt.distanceToStartAfterSetup = distance(beforeRoute, start); + attempt.setupPassed = WalkerState.ARRIVED.name().equals(attempt.setupState) + && attempt.distanceToStartAfterSetup <= route.startTolerance; + if (!attempt.setupPassed) { + attempt.setupError = "Setup webwalk failed for " + route.id + " attempt " + attemptNumber + + ": state=" + attempt.setupState + + ", location=" + attempt.routeStartLocation + + ", distanceToStart=" + attempt.distanceToStartAfterSetup; + attempt.error = attempt.setupError; + attempt.finishedAt = Instant.now().toString(); + return attempt; + } + + long walkStart = System.currentTimeMillis(); + attempt.walkerState = walk(route.destination, route.destinationTolerance, walkTimeoutMs); + attempt.walkDurationMs = System.currentTimeMillis() - walkStart; + + WorldPoint end = safeLocation(); + attempt.endLocation = format(end); + attempt.distanceToDestination = distance(end, route.destination); + attempt.passed = WalkerState.ARRIVED.name().equals(attempt.walkerState) + && attempt.distanceToDestination <= route.destinationTolerance; + if (!attempt.passed) { + attempt.error = "Route webwalk failed for " + route.id + " attempt " + attemptNumber + + ": state=" + attempt.walkerState + + ", location=" + attempt.endLocation + + ", distanceToDestination=" + attempt.distanceToDestination; + } + + attempt.finishedAt = Instant.now().toString(); + return attempt; + } + private String walk(WorldPoint destination, int tolerance, int timeoutMs) { ExecutorService walkExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder() .setNameFormat("F2PWebWalkerLeg-%d") @@ -219,6 +286,7 @@ private String walk(WorldPoint destination, int tolerance, int timeoutMs) { return future.get(timeoutMs, TimeUnit.MILLISECONDS).name(); } catch (TimeoutException e) { future.cancel(true); + Rs2Walker.setTarget(null); return "TIMEOUT"; } catch (Exception e) { log.warn("[F2PWebWalkerHarness] Webwalker leg failed for destination {}", destination, e); @@ -232,21 +300,37 @@ private WorldPoint safeLocation() { return lastLocation; } - private void applyShortestPathOverrides() { + private void applyShortestPathOverrides(F2PWebWalkerRoute route) { + Map config = new HashMap<>(); + String value = property(TEST_USE_TELEPORTATION_SPELLS_PROPERTY, USE_TELEPORTATION_SPELLS_PROPERTY, ""); - if (value.isBlank()) { - return; + if (!value.isBlank()) { + config.put("useTeleportationSpells", Boolean.parseBoolean(value)); } - boolean useTeleportationSpells = Boolean.parseBoolean(value); - Map config = new HashMap<>(); - config.put("useTeleportationSpells", useTeleportationSpells); + if (route.forceNoAgilityShortcuts) { + config.put("useAgilityShortcuts", false); + config.put("useGrappleShortcuts", false); + } + + if (route.forceNoTeleports) { + config.put("useTeleportationItems", "None"); + config.put("useTeleportationLevers", false); + config.put("useTeleportationMinigames", false); + config.put("useTeleportationPortals", false); + config.put("useTeleportationSpells", false); + config.put("useWildernessObelisks", false); + } + + if (config.isEmpty()) { + return; + } Map data = new HashMap<>(); data.put("config", config); eventBus.post(new PluginMessage("shortestpath", "path", data)); - log.info("[F2PWebWalkerHarness] Applied shortest path override: useTeleportationSpells={}", useTeleportationSpells); + log.info("[F2PWebWalkerHarness] Applied shortest path overrides for {}: {}", route.id, config); } private static boolean isHarnessTarget() { @@ -315,6 +399,36 @@ public static class RouteOutcome { public String destination; public int startTolerance; public int destinationTolerance; + public int repetitions; + public int successfulAttempts; + public boolean currentLocationStart; + public boolean requireF2PWorld; + public boolean membersWorld; + public boolean forceNoAgilityShortcuts; + public boolean forceNoTeleports; + public String startedAt; + public String finishedAt; + public String initialLocation; + public int initialDistanceToStart; + public String setupState; + public boolean setupPassed; + public String setupError; + public long setupDurationMs; + public String routeStartLocation; + public int distanceToStartAfterSetup; + public String walkerState; + public boolean passed; + public String error; + public long walkDurationMs; + public String endLocation; + public int distanceToDestination; + public List attempts = new ArrayList<>(); + } + + public static class RouteAttemptOutcome { + public int attempt; + public String start; + public String destination; public String startedAt; public String finishedAt; public String initialLocation; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/F2PWebWalkerRoute.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/F2PWebWalkerRoute.java index fde91fa2b3..82f00b72a1 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/F2PWebWalkerRoute.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/F2PWebWalkerRoute.java @@ -40,7 +40,9 @@ final class F2PWebWalkerRoute { route("F2P-15", "Draynor Manor outside to Draynor Manor interior", point(3109, 3341, 0), point(3106, 3363, 0), 1, 1), route("F2P-16", "Draynor Manor interior to Draynor bank", - point(3106, 3363, 0), point(3092, 3245, 0), 1, 2) + point(3106, 3363, 0), point(3092, 3245, 0), 1, 2), + currentRoute("F2P-17", "Current location to Varrock Sewers", + point(3237, 9858, 0), 1, 1, 5, true, true) ); final String id; @@ -49,6 +51,11 @@ final class F2PWebWalkerRoute { final WorldPoint destination; final int startTolerance; final int destinationTolerance; + final int repetitions; + final boolean currentLocationStart; + final boolean requireF2PWorld; + final boolean forceNoAgilityShortcuts; + final boolean forceNoTeleports; private F2PWebWalkerRoute( String id, @@ -56,7 +63,12 @@ private F2PWebWalkerRoute( WorldPoint start, WorldPoint destination, int startTolerance, - int destinationTolerance + int destinationTolerance, + int repetitions, + boolean currentLocationStart, + boolean requireF2PWorld, + boolean forceNoAgilityShortcuts, + boolean forceNoTeleports ) { this.id = id; this.name = name; @@ -64,6 +76,11 @@ private F2PWebWalkerRoute( this.destination = destination; this.startTolerance = startTolerance; this.destinationTolerance = destinationTolerance; + this.repetitions = repetitions; + this.currentLocationStart = currentLocationStart; + this.requireF2PWorld = requireF2PWorld; + this.forceNoAgilityShortcuts = forceNoAgilityShortcuts; + this.forceNoTeleports = forceNoTeleports; } static List selected(String routeFilter) { @@ -96,7 +113,22 @@ private static F2PWebWalkerRoute route( int startTolerance, int destinationTolerance ) { - return new F2PWebWalkerRoute(id, name, start, destination, startTolerance, destinationTolerance); + return new F2PWebWalkerRoute(id, name, start, destination, startTolerance, destinationTolerance, + 1, false, false, false, false); + } + + private static F2PWebWalkerRoute currentRoute( + String id, + String name, + WorldPoint destination, + int startTolerance, + int destinationTolerance, + int repetitions, + boolean forceNoAgilityShortcuts, + boolean forceNoTeleports + ) { + return new F2PWebWalkerRoute(id, name, null, destination, startTolerance, destinationTolerance, + repetitions, true, true, forceNoAgilityShortcuts, forceNoTeleports); } private static WorldPoint point(int x, int y, int plane) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/GeLumbridgeTeleportHarnessPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/GeLumbridgeTeleportHarnessPlugin.java new file mode 100644 index 0000000000..2bd9dc70f6 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/GeLumbridgeTeleportHarnessPlugin.java @@ -0,0 +1,384 @@ +package net.runelite.client.plugins.microbot.testing.webwalker; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.google.inject.Inject; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Player; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.GameTick; +import net.runelite.client.eventbus.EventBus; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.PluginMessage; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.testing.TestResult; +import net.runelite.client.plugins.microbot.testing.TestResultWriter; +import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; +import net.runelite.client.plugins.microbot.util.walker.WalkerState; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; + +@PluginDescriptor( + name = "GE Lumbridge Teleport Harness", + description = "Stress tests webwalking between Lumbridge Castle and the Grand Exchange", + tags = {"microbot", "test", "webwalker", "teleport"}, + hidden = true +) +@Slf4j +public class GeLumbridgeTeleportHarnessPlugin extends Plugin { + private static final String TEST_SCRIPT_PROPERTY = "microbot.test.script"; + private static final String SCRIPT_NAME = "GE Lumbridge Teleport Harness"; + private static final String ITERATIONS_PROPERTY = "microbot.test.geLumbridge.iterations"; + private static final String WALK_TIMEOUT_PROPERTY = "microbot.test.geLumbridge.walkTimeoutMs"; + private static final int DEFAULT_ITERATIONS = 10; + private static final int DEFAULT_WALK_TIMEOUT_MS = 300000; + private static final WorldPoint LUMBRIDGE_CASTLE = new WorldPoint(3222, 3218, 0); + private static final WorldPoint GRAND_EXCHANGE = new WorldPoint(3164, 3486, 0); + private static final WorldPoint VARROCK_TELEPORT = new WorldPoint(3213, 3424, 0); + private static final int DEFAULT_TOLERANCE = 2; + + @Inject + private EventBus eventBus; + + private ExecutorService executor; + private volatile WorldPoint lastLocation; + private volatile LegOutcome activeLeg; + + @Override + protected void startUp() { + if (!isHarnessTarget()) { + return; + } + + executor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder() + .setNameFormat("GeLumbridgeTeleportHarness-%d") + .build()); + executor.submit(this::runHarness); + } + + @Override + protected void shutDown() { + if (executor != null) { + executor.shutdownNow(); + } + } + + @Subscribe + public void onGameTick(GameTick event) { + if (!isHarnessTarget()) { + return; + } + + Player player = Microbot.getClient().getLocalPlayer(); + if (player == null) { + return; + } + + WorldPoint location = player.getWorldLocation(); + lastLocation = location; + + LegOutcome leg = activeLeg; + if (leg != null && location != null) { + observeLeg(leg, location); + } + } + + private void runHarness() { + GeLumbridgeTeleportResult result = new GeLumbridgeTeleportResult(SCRIPT_NAME); + result.iterations = intProperty(ITERATIONS_PROPERTY, DEFAULT_ITERATIONS); + result.walkTimeoutMs = intProperty(WALK_TIMEOUT_PROPERTY, DEFAULT_WALK_TIMEOUT_MS); + + int exitCode = 0; + try { + if (!sleepUntil(() -> safeLocation() != null, 60000)) { + result.addError("Timed out waiting for local player location before starting GE/Lumbridge harness"); + result.complete("login_failure"); + writeAndExit(result, result.exitCode); + return; + } + + applyTeleportSpellOverride(); + log.info("[GeLumbridgeTeleportHarness] Starting {} iteration(s)", result.iterations); + + LegOutcome setup = runLeg("setup", 0, "setup-to-lumbridge-castle", + safeLocation(), LUMBRIDGE_CASTLE, DEFAULT_TOLERANCE, result.walkTimeoutMs); + result.legs.add(setup); + result.addCheck(setup.name, setup.passed, setup.error); + if (!setup.passed) { + result.complete("completed"); + writeAndExit(result, result.exitCode); + return; + } + + for (int iteration = 1; iteration <= result.iterations; iteration++) { + LegOutcome toGe = runLeg("lumbridge-to-ge", iteration, "lumbridge-to-grand-exchange", + LUMBRIDGE_CASTLE, GRAND_EXCHANGE, DEFAULT_TOLERANCE, result.walkTimeoutMs); + result.legs.add(toGe); + result.addCheck(toGe.id, toGe.passed, toGe.error); + if (!toGe.passed) { + exitCode = 1; + break; + } + + LegOutcome toLumbridge = runLeg("ge-to-lumbridge", iteration, "grand-exchange-to-lumbridge-castle", + GRAND_EXCHANGE, LUMBRIDGE_CASTLE, DEFAULT_TOLERANCE, result.walkTimeoutMs); + result.legs.add(toLumbridge); + result.addCheck(toLumbridge.id, toLumbridge.passed, toLumbridge.error); + if (!toLumbridge.passed) { + exitCode = 1; + break; + } + } + + result.complete("completed"); + writeAndExit(result, exitCode == 0 ? result.exitCode : exitCode); + } catch (Throwable t) { + log.error("[GeLumbridgeTeleportHarness] Harness crashed", t); + result.addError(t.getClass().getSimpleName() + ": " + t.getMessage()); + result.complete("crash"); + writeAndExit(result, result.exitCode); + } + } + + private LegOutcome runLeg(String kind, int iteration, String name, WorldPoint expectedStart, + WorldPoint destination, int tolerance, int timeoutMs) { + LegOutcome outcome = new LegOutcome(); + outcome.kind = kind; + outcome.iteration = iteration; + outcome.id = iteration == 0 ? name : "iteration-" + iteration + "-" + name; + outcome.name = outcome.id; + outcome.expectedStart = format(expectedStart); + outcome.destination = format(destination); + outcome.startedAt = Instant.now().toString(); + outcome.initialLocation = format(safeLocation()); + outcome.initialDistanceToDestination = distance(safeLocation(), destination); + outcome.tolerance = tolerance; + + log.info("[GeLumbridgeTeleportHarness] Running {} from {} to {}", outcome.id, safeLocation(), destination); + activeLeg = outcome; + long startedAt = System.currentTimeMillis(); + outcome.walkerState = walk(destination, tolerance, timeoutMs, outcome); + outcome.durationMs = System.currentTimeMillis() - startedAt; + activeLeg = null; + + WorldPoint end = safeLocation(); + outcome.finishedAt = Instant.now().toString(); + outcome.endLocation = format(end); + outcome.distanceToDestination = distance(end, destination); + outcome.passed = WalkerState.ARRIVED.name().equals(outcome.walkerState) + && outcome.distanceToDestination <= tolerance + && !outcome.retreatAfterVarrockTeleport; + if (!outcome.passed) { + outcome.error = "Leg failed: state=" + outcome.walkerState + + ", end=" + outcome.endLocation + + ", distanceToDestination=" + outcome.distanceToDestination + + ", retreatAfterVarrockTeleport=" + outcome.retreatAfterVarrockTeleport; + } + + log.info("[GeLumbridgeTeleportHarness] {} finished: state={}, end={}, distance={}, duration={}ms, varrockTeleport={}, retreatAfterTeleport={}", + outcome.id, outcome.walkerState, outcome.endLocation, outcome.distanceToDestination, + outcome.durationMs, outcome.varrockTeleportDetected, outcome.retreatAfterVarrockTeleport); + return outcome; + } + + private void observeLeg(LegOutcome leg, WorldPoint location) { + leg.sampleCount++; + int distanceToDestination = distance(location, parsePoint(leg.destination)); + if (distanceToDestination > leg.previousDistanceToDestination) { + leg.destinationRegressionTicks++; + leg.maxConsecutiveDestinationRegressionTicks = Math.max( + leg.maxConsecutiveDestinationRegressionTicks, + leg.destinationRegressionTicks); + } else { + leg.destinationRegressionTicks = 0; + } + leg.previousDistanceToDestination = distanceToDestination; + + if (!"lumbridge-to-ge".equals(leg.kind)) { + return; + } + + if (!leg.varrockTeleportDetected && location.distanceTo2D(VARROCK_TELEPORT) <= 8) { + leg.varrockTeleportDetected = true; + leg.varrockTeleportLocation = format(location); + leg.distanceToExpectedStartAtVarrockTeleport = location.distanceTo2D(LUMBRIDGE_CASTLE); + leg.distanceToDestinationAtVarrockTeleport = location.distanceTo2D(GRAND_EXCHANGE); + leg.previousDistanceToExpectedStart = leg.distanceToExpectedStartAtVarrockTeleport; + leg.previousDistanceToGeAfterTeleport = leg.distanceToDestinationAtVarrockTeleport; + leg.observations.add("Varrock teleport detected at " + format(location)); + return; + } + + if (!leg.varrockTeleportDetected || leg.retreatAfterVarrockTeleport || leg.postTeleportTicks >= 12) { + return; + } + + leg.postTeleportTicks++; + int distanceToExpectedStart = location.distanceTo2D(LUMBRIDGE_CASTLE); + int distanceToGe = location.distanceTo2D(GRAND_EXCHANGE); + boolean movedTowardOriginalStart = distanceToExpectedStart <= leg.previousDistanceToExpectedStart - 2; + boolean movedAwayFromDestination = distanceToGe > leg.previousDistanceToGeAfterTeleport; + if (movedTowardOriginalStart && movedAwayFromDestination) { + leg.retreatAfterVarrockTeleport = true; + leg.observations.add("Retreat after Varrock teleport at " + format(location) + + ": distanceToLumbridge=" + distanceToExpectedStart + + ", distanceToGe=" + distanceToGe); + } + leg.previousDistanceToExpectedStart = distanceToExpectedStart; + leg.previousDistanceToGeAfterTeleport = distanceToGe; + } + + private String walk(WorldPoint destination, int tolerance, int timeoutMs, LegOutcome outcome) { + ExecutorService walkExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder() + .setNameFormat("GeLumbridgeTeleportLeg-%d") + .build()); + Future future = walkExecutor.submit(() -> Rs2Walker.walkWithState(destination, tolerance)); + + try { + return future.get(timeoutMs, TimeUnit.MILLISECONDS).name(); + } catch (TimeoutException e) { + outcome.walkerThreadDump = dumpWalkerThread(); + future.cancel(true); + return "TIMEOUT"; + } catch (Exception e) { + log.warn("[GeLumbridgeTeleportHarness] Webwalker leg failed for destination {}", destination, e); + return "ERROR:" + e.getClass().getSimpleName(); + } finally { + walkExecutor.shutdownNow(); + } + } + + private void applyTeleportSpellOverride() { + Map config = new HashMap<>(); + config.put("useTeleportationSpells", true); + + Map data = new HashMap<>(); + data.put("config", config); + + eventBus.post(new PluginMessage("shortestpath", "path", data)); + log.info("[GeLumbridgeTeleportHarness] Applied shortest path override: useTeleportationSpells=true"); + } + + private WorldPoint safeLocation() { + return lastLocation; + } + + private static boolean isHarnessTarget() { + return "true".equals(System.getProperty("microbot.test.mode")) + && System.getProperty(TEST_SCRIPT_PROPERTY, "").contains(SCRIPT_NAME); + } + + private static int intProperty(String property, int defaultValue) { + String value = System.getProperty(property); + if (value == null || value.isBlank()) { + return defaultValue; + } + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + private static int distance(WorldPoint from, WorldPoint to) { + if (from == null || to == null) { + return Integer.MAX_VALUE; + } + return from.distanceTo(to); + } + + private static WorldPoint parsePoint(String point) { + if (point == null || point.isBlank()) { + return null; + } + String[] parts = point.split(","); + if (parts.length != 3) { + return null; + } + return new WorldPoint(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]), Integer.parseInt(parts[2])); + } + + private static String format(WorldPoint point) { + if (point == null) { + return null; + } + return point.getX() + "," + point.getY() + "," + point.getPlane(); + } + + private static void writeAndExit(GeLumbridgeTeleportResult result, int exitCode) { + TestResultWriter.write(result); + System.exit(exitCode); + } + + public static class GeLumbridgeTeleportResult extends TestResult { + public int iterations; + public int walkTimeoutMs; + public List legs = new ArrayList<>(); + + public GeLumbridgeTeleportResult(String script) { + super(script); + } + } + + public static class LegOutcome { + public String id; + public String kind; + public int iteration; + public String name; + public String expectedStart; + public String destination; + public int tolerance; + public String startedAt; + public String finishedAt; + public String initialLocation; + public int initialDistanceToDestination; + public String walkerState; + public boolean passed; + public String error; + public long durationMs; + public String endLocation; + public int distanceToDestination; + public int sampleCount; + public int previousDistanceToDestination = Integer.MAX_VALUE; + public int destinationRegressionTicks; + public int maxConsecutiveDestinationRegressionTicks; + public boolean varrockTeleportDetected; + public String varrockTeleportLocation; + public int distanceToExpectedStartAtVarrockTeleport; + public int distanceToDestinationAtVarrockTeleport; + public int postTeleportTicks; + public int previousDistanceToExpectedStart = Integer.MAX_VALUE; + public int previousDistanceToGeAfterTeleport = Integer.MAX_VALUE; + public boolean retreatAfterVarrockTeleport; + public String walkerThreadDump; + public List observations = new ArrayList<>(); + } + + private static String dumpWalkerThread() { + return Thread.getAllStackTraces().entrySet().stream() + .filter(entry -> entry.getKey().getName().startsWith("GeLumbridgeTeleportLeg-")) + .findFirst() + .map(entry -> { + StringBuilder dump = new StringBuilder(entry.getKey().getName()) + .append(" state=") + .append(entry.getKey().getState()); + for (StackTraceElement frame : entry.getValue()) { + dump.append('\n').append(" at ").append(frame); + } + return dump.toString(); + }) + .orElse("No GeLumbridgeTeleportLeg thread found"); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/Global.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/Global.java index 3eccb7a91f..1baaa658d5 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/Global.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/Global.java @@ -41,7 +41,7 @@ public static void sleep(int start) { try { Thread.sleep(start); } catch (InterruptedException ignored) { - // ignore interrupted + Thread.currentThread().interrupt(); } } @@ -96,10 +96,13 @@ public static T sleepUntilNotNull(Callable method, int timeoutMillis, int T methodResponse; final long endTime = System.currentTimeMillis()+timeoutMillis; do { + if (Thread.currentThread().isInterrupted()) { + return null; + } methodResponse = method.call(); done = methodResponse != null; sleep(sleepMillis); - } while (!done && System.currentTimeMillis() < endTime); + } while (!done && !Thread.currentThread().isInterrupted() && System.currentTimeMillis() < endTime); return methodResponse; } @@ -123,7 +126,7 @@ public static boolean sleepUntil(BooleanSupplier awaitedCondition, int time) { if (Microbot.getClient().isClientThread()) return false; long startTime = System.currentTimeMillis(); try { - while (System.currentTimeMillis() - startTime < time) { + while (!Thread.currentThread().isInterrupted() && System.currentTimeMillis() - startTime < time) { if (awaitedCondition.getAsBoolean()) return true; sleep(nextPollIntervalMs()); } @@ -138,7 +141,7 @@ public static boolean sleepUntil(BooleanSupplier awaitedCondition, Runnable acti long startTime = System.nanoTime(); long timeoutNanos = TimeUnit.MILLISECONDS.toNanos(timeoutMillis); try { - while (System.nanoTime() - startTime < timeoutNanos) { + while (!Thread.currentThread().isInterrupted() && System.nanoTime() - startTime < timeoutNanos) { if (awaitedCondition.getAsBoolean()) { return true; } @@ -156,11 +159,14 @@ public static boolean sleepUntilTrue(BooleanSupplier awaitedCondition) { long startTime = System.currentTimeMillis(); try { do { + if (Thread.currentThread().isInterrupted()) { + return false; + } if (awaitedCondition.getAsBoolean()) { return true; } sleep(nextPollIntervalMs()); - } while (System.currentTimeMillis() - startTime < 5000); + } while (!Thread.currentThread().isInterrupted() && System.currentTimeMillis() - startTime < 5000); } catch (Exception e) { Microbot.logStackTrace("Global Sleep: ", e); } @@ -172,11 +178,14 @@ public static boolean sleepUntilTrue(BooleanSupplier awaitedCondition, int time, long startTime = System.currentTimeMillis(); try { do { + if (Thread.currentThread().isInterrupted()) { + return false; + } if (awaitedCondition.getAsBoolean()) { return true; } sleep(time); - } while (System.currentTimeMillis() - startTime < timeout); + } while (!Thread.currentThread().isInterrupted() && System.currentTimeMillis() - startTime < timeout); } catch (Exception e) { Microbot.logStackTrace("Global Sleep: ", e); } @@ -188,6 +197,9 @@ public static boolean sleepUntilTrue(BooleanSupplier awaitedCondition, BooleanSu long startTime = System.currentTimeMillis(); try { do { + if (Thread.currentThread().isInterrupted()) { + return false; + } if (resetCondition.getAsBoolean()) { startTime = System.currentTimeMillis(); } @@ -195,7 +207,7 @@ public static boolean sleepUntilTrue(BooleanSupplier awaitedCondition, BooleanSu return true; } sleep(time); - } while (System.currentTimeMillis() - startTime < timeout); + } while (!Thread.currentThread().isInterrupted() && System.currentTimeMillis() - startTime < timeout); } catch (Exception e) { Microbot.logStackTrace("Global Sleep: ", e); } @@ -215,8 +227,11 @@ public static void sleepUntilOnClientThread(BooleanSupplier awaitedCondition, in long startTime = System.currentTimeMillis(); try { do { + if (Thread.currentThread().isInterrupted()) { + return; + } done = Microbot.getClientThread().runOnClientThreadOptional(awaitedCondition::getAsBoolean).orElse(false); - } while (!done && System.currentTimeMillis() - startTime < time); + } while (!done && !Thread.currentThread().isInterrupted() && System.currentTimeMillis() - startTime < time); } catch (Exception e) { Microbot.logStackTrace("Global Sleep: ", e); } 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 84b62873e2..4478841dff 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 @@ -57,6 +57,7 @@ import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.ThreadFactory; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -250,7 +251,6 @@ private static WalkerState walkWithStateInternal(WorldPoint target, int distance } closeWorldMap(); - kickStartShortLocalWalk(target, distance); return processWalk(target, distance); } @@ -429,11 +429,7 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part boolean doorOrTransportResult = false; boolean inInstance = Microbot.getClient().getTopLevelWorldView().isInstance(); String exitReason = "end-of-path"; - final int HANDLER_RANGE = 8; - WalkerState directShortWalk = tryDirectShortWalk(target, distance, path, inInstance); - if (directShortWalk != WalkerState.MOVING) { - return directShortWalk; - } + final int HANDLER_RANGE = 13; if (rawPath != null && path != null && handleNearbyRawPathSceneObjects(rawPath, HANDLER_RANGE)) { @@ -447,6 +443,13 @@ && handleCurrentTileTransportTowardPath(rawPath, path, target)) { exitReason = "current-tile-transport-handled"; } + if (!doorOrTransportResult) { + WalkerState directShortWalk = tryDirectShortWalk(target, distance, rawPath, path, inInstance); + if (directShortWalk != WalkerState.MOVING) { + return directShortWalk; + } + } + for (int i = indexOfStartPoint; !doorOrTransportResult && i < path.size(); i++) { WorldPoint currentWorldPoint = path.get(i); if (isWalkCancelled(target)) { @@ -1529,6 +1532,7 @@ private static boolean handleRockfall(List path, int index) { private static WalkerState tryDirectShortWalk(WorldPoint target, int distance, + List rawPath, List path, boolean inInstance) { WorldPoint playerLoc = Rs2Player.getWorldLocation(); @@ -1552,12 +1556,20 @@ private static WalkerState tryDirectShortWalk(WorldPoint target, return WalkerState.MOVING; } + if (hasPendingExplicitTransportStepBeforeArrival(rawPath, target, distance) + || hasPendingExplicitTransportStepBeforeArrival(path, target, distance)) { + return WalkerState.MOVING; + } + if (!inInstance && !Rs2Tile.isWalkable(end)) { return WalkerState.MOVING; } if (!inInstance && !Rs2Tile.isTileReachable(end)) { return WalkerState.MOVING; } + if (!inInstance && localRouteDetoursFromComputedRoute(rawPath, end, directClickMaxDistance)) { + return WalkerState.MOVING; + } boolean clicked = walkMiniMap(end); if (!clicked) { @@ -1607,30 +1619,73 @@ private static WalkerState tryDirectShortWalk(WorldPoint target, return WalkerState.MOVING; } - private static void kickStartShortLocalWalk(WorldPoint target, int distance) { - try { - WorldPoint playerLoc = Rs2Player.getWorldLocation(); - if (target == null || playerLoc == null || playerLoc.getPlane() != target.getPlane()) { - return; - } + private static boolean hasPendingExplicitTransportStepBeforeArrival(List path, + WorldPoint target, + int distance) { + return hasPendingRouteStepBeforeArrival(path, target, distance, i -> hasExplicitTransportStep(path, i)); + } - int initialDist = playerLoc.distanceTo(target); - if (initialDist <= distance || initialDist > 13) { - return; - } + static boolean hasPendingRouteStepBeforeArrival(List path, + WorldPoint target, + int distance, + java.util.function.IntPredicate routeStepAtIndex) { + if (path == null || path.size() < 2 || routeStepAtIndex == null) { + return false; + } - LocalPoint localTarget = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), target); - if (localTarget == null || !Rs2Tile.isWalkable(localTarget)) { - return; + for (int i = 0; i < path.size() - 1; i++) { + WorldPoint point = path.get(i); + if (target != null && point != null && point.distanceTo(target) <= distance) { + return false; + } + if (routeStepAtIndex.test(i)) { + return true; } + } + return false; + } - boolean clicked = walkMiniMap(target); - if (!clicked) { - walkMiniMapToward(target, playerLoc, 12); + private static boolean localRouteDetoursFromComputedRoute(List rawPath, + WorldPoint end, + int directClickMaxDistance) { + if (rawPath == null || rawPath.size() < 2 || end == null) { + return false; + } + + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + if (playerLoc == null || playerLoc.getPlane() != end.getPlane()) { + return false; + } + + int rawStart = getClosestTileIndex(rawPath); + if (rawStart < 0 || rawStart >= rawPath.size() - 1) { + return false; + } + + int rawEnd = -1; + for (int i = rawStart; i < rawPath.size(); i++) { + WorldPoint point = rawPath.get(i); + if (point == null || point.getPlane() != end.getPlane()) { + break; } - } catch (Exception ex) { - log.debug("[Walker] short local kick-start failed: {}", ex.getMessage()); + if (point.equals(end)) { + rawEnd = i; + break; + } + } + if (rawEnd < 0) { + return false; + } + + int computedSteps = rawEnd - rawStart; + if (computedSteps <= 0) { + return false; } + + final int detourSlackTiles = 4; + int searchDistance = Math.max(directClickMaxDistance * 3, computedSteps + detourSlackTiles + 1); + Integer localSteps = Rs2Tile.getReachableTilesFromTile(playerLoc, searchDistance).get(end); + return localSteps == null || localSteps > computedSteps + detourSlackTiles; } private static boolean handleNearbyRawPathSceneObjects(List rawPath, int handlerRange) { @@ -2387,6 +2442,11 @@ public static void setTarget(WorldPoint target) { if (pathfinder != null) { pathfinder.cancel(); } + Future pathfinderFuture = ShortestPathPlugin.getPathfinderFuture(); + if (pathfinderFuture != null && !pathfinderFuture.isDone()) { + pathfinderFuture.cancel(true); + } + ShortestPathPlugin.setPathfinderFuture(null); ShortestPathPlugin.setPathfinder(null); } @@ -2649,8 +2709,11 @@ && recentlyOpenedStationaryDoorOnSegment(path.get(indexOfStartPoint), path.get(i Rs2Dialogue.clickOption(transport.getDisplayInfo()); } sleepUntil(() -> !Rs2Player.isAnimating()); - sleepUntil(() -> Rs2Player.getWorldLocation().distanceTo(transport.getDestination()) < 10); + boolean reachedDestination = sleepUntil(() -> Rs2Player.getWorldLocation().distanceTo(transport.getDestination()) < 10); sleepTickJitter(6); + if (reachedDestination) { + return finishHandledTransport(transport); + } } else { Rs2Walker.walkFastCanvas(path.get(i)); sleep(1200, 1600); @@ -2662,7 +2725,7 @@ && recentlyOpenedStationaryDoorOnSegment(path.get(indexOfStartPoint), path.get(i sleepUntil(() -> !Rs2Player.isAnimating()); sleepUntilTrue(() -> Rs2Player.getWorldLocation().distanceTo(transport.getDestination()) < 10); sleepTickJitter(4); // wait 4 extra ticks before walking - break; + return finishHandledTransport(transport); } } } @@ -2674,14 +2737,14 @@ && recentlyOpenedStationaryDoorOnSegment(path.get(indexOfStartPoint), path.get(i log.debug("[Walker] handlePohTransport({}) returned {}", transport.getDisplayInfo(), pohResult); if (pohResult) { sleepUntil(() -> Rs2Player.getWorldLocation().distanceTo(transport.getDestination()) < OFFSET, 10000); - break; + return finishHandledTransport(transport); } } if (transport.getType() == TransportType.CANOE) { if (handleCanoe(transport)) { sleepTickJitter(2); - break; + return finishHandledTransport(transport); } } @@ -2689,28 +2752,28 @@ && recentlyOpenedStationaryDoorOnSegment(path.get(indexOfStartPoint), path.get(i if (handleSpiritTree(transport)) { sleepUntil(() -> !Rs2Player.isAnimating()); sleepUntilTrue(() -> Rs2Player.getWorldLocation().distanceTo(transport.getDestination()) < 10); - break; + return finishHandledTransport(transport); } } if (transport.getType() == TransportType.QUETZAL) { if (handleQuetzal(transport)) { sleepTickJitter(2); - break; + return finishHandledTransport(transport); } } if (transport.getType() == TransportType.MAGIC_CARPET) { if (handleMagicCarpet(transport)) { sleepTickJitter(2); - break; + return finishHandledTransport(transport); } } if (transport.getType() == TransportType.WILDERNESS_OBELISK) { if (handleWildernessObelisk(transport)) { sleepTickJitter(2); - break; + return finishHandledTransport(transport); } } @@ -2719,21 +2782,21 @@ && recentlyOpenedStationaryDoorOnSegment(path.get(indexOfStartPoint), path.get(i sleepUntil(() -> !Rs2Player.isAnimating()); sleepUntilTrue(() -> Rs2Player.getWorldLocation().distanceTo(transport.getDestination()) < 10); sleepTickJitter(3); - break; + return finishHandledTransport(transport); } } if (transport.getType() == TransportType.FAIRY_RING && !Rs2Player.getWorldLocation().equals(transport.getDestination())) { if (handleFairyRing(transport)) { sleepUntilTrue(() -> Rs2Player.getWorldLocation().distanceTo(transport.getDestination()) < OFFSET); - break; + return finishHandledTransport(transport); } } if (transport.getType() == TransportType.TELEPORTATION_MINIGAME) { if (handleMinigameTeleport(transport)) { sleepUntilTrue(() -> Rs2Player.getWorldLocation().distanceTo(transport.getDestination()) < (OFFSET * 2)); - break; + return finishHandledTransport(transport); } } @@ -2741,7 +2804,7 @@ && recentlyOpenedStationaryDoorOnSegment(path.get(indexOfStartPoint), path.get(i if (handleTeleportItem(transport)) { sleepUntil(() -> !Rs2Player.isAnimating()); sleepUntilTrue(() -> Rs2Player.getWorldLocation().distanceTo(transport.getDestination()) < OFFSET); - break; + return finishHandledTransport(transport); } } @@ -2754,7 +2817,7 @@ && recentlyOpenedStationaryDoorOnSegment(path.get(indexOfStartPoint), path.get(i sleepUntilTrue(() -> Rs2Player.getWorldLocation().distanceTo(transport.getDestination()) < OFFSET); } Rs2Tab.switchTo(InterfaceTab.INVENTORY); - break; + return finishHandledTransport(transport); } } @@ -2762,7 +2825,7 @@ && recentlyOpenedStationaryDoorOnSegment(path.get(indexOfStartPoint), path.get(i if (handleSeasonalTransport(transport)) { sleepUntil(() -> !Rs2Player.isAnimating()); sleepUntilTrue(() -> Rs2Player.getWorldLocation().distanceTo(transport.getDestination()) < OFFSET); - break; + return finishHandledTransport(transport); } } @@ -2862,7 +2925,12 @@ && recentlyOpenedStationaryDoorOnSegment(path.get(indexOfStartPoint), path.get(i } sleepUntil(() -> !Rs2Player.isAnimating()); int destinationTolerance = isAdjacentSamePlaneTransport(transport) ? 0 : OFFSET; - return sleepUntil(() -> Rs2Player.getWorldLocation().distanceTo(transport.getDestination()) <= destinationTolerance, 5000); + boolean reachedDestination = sleepUntil(() -> Rs2Player.getWorldLocation().distanceTo(transport.getDestination()) <= destinationTolerance, 5000); + if (reachedDestination) { + markAdjacentSamePlaneTransportHandled(transport, object); + return finishHandledTransport(transport); + } + return false; } } } @@ -3005,6 +3073,47 @@ private static boolean isAdjacentSamePlaneTransport(Transport transport) { && transport.getOrigin().distanceTo(transport.getDestination()) <= 1; } + private static boolean finishHandledTransport(Transport transport) { + if (currentTarget != null && shouldRecalculatePathAfterTransport(transport)) { + recalculatePath(); + } + return true; + } + + static boolean shouldRecalculatePathAfterTransport(Transport transport) { + if (transport == null || transport.getDestination() == null) { + return false; + } + if (TransportType.isTeleport(transport.getType())) { + return true; + } + if (transport.getOrigin() == null) { + return false; + } + return transport.getOrigin().getPlane() != transport.getDestination().getPlane() + || transport.getOrigin().distanceTo2D(transport.getDestination()) > OFFSET; + } + + private static void markAdjacentSamePlaneTransportHandled(Transport transport, TileObject tileObject) { + for (WorldPoint point : adjacentSamePlaneTransportSuppressionPoints(transport, tileObject)) { + markStationaryDoorOpened(point); + } + } + + static Set adjacentSamePlaneTransportSuppressionPoints(Transport transport, TileObject tileObject) { + if (!isAdjacentSamePlaneTransport(transport)) { + return Collections.emptySet(); + } + + Set points = new LinkedHashSet<>(); + points.add(transport.getOrigin()); + points.add(transport.getDestination()); + if (tileObject != null && tileObject.getWorldLocation() != null) { + points.add(tileObject.getWorldLocation()); + } + return points; + } + private static boolean handleObjectExceptions(Transport transport, TileObject tileObject) { for (Map.Entry entry : OPEN_TO_CLOSED_MAPPINGS.entrySet()) { final int closedTrapdoorId = entry.getKey(); diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/teleportation_spells.tsv b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/teleportation_spells.tsv index c9c97952b1..e62cbf553e 100644 --- a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/teleportation_spells.tsv +++ b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/teleportation_spells.tsv @@ -1,7 +1,7 @@ # Destination Skills Quests Wilderness level Varbits Varplayers isMembers Duration Display info # Standard Spellbook - Varbit 4070=0 -# Lumbridge Home Teleport. Varbit 12353 is BUFF_HOME_TELEPORT_DISABLED. -3221 3218 0 19 4070=0;12353=0 20 Lumbridge Home Teleport +# Lumbridge Home Teleport. Varplayer 892 is LAST_HOME_TELEPORT in epoch minutes. +3221 3218 0 19 4070=0 892@30 20 Lumbridge Home Teleport # Varrock Teleport 3213 3424 0 25 Magic 19 4070=0;4585=0 4 Varrock Teleport 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 dcfe2e557c..4adaf188ef 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.VarPlayer; import net.runelite.api.coords.WorldArea; import net.runelite.api.coords.WorldPoint; import net.runelite.client.plugins.microbot.shortestpath.pathfinder.*; @@ -215,16 +216,47 @@ public void testNewTransportTypesLoaded() { @Test public void testLumbridgeHomeTeleportTransportLoaded() { + Transport transport = getLumbridgeHomeTeleportTransport(); + + assertTrue("Lumbridge Home Teleport should stay gated to the standard spellbook", + transport.getVarbits().stream().anyMatch(v -> v.getVarbitId() == 4070 && v.getValue() == 0)); + assertFalse("Lumbridge Home Teleport should not depend on the buff-display disabled varbit", + transport.getVarbits().stream().anyMatch(v -> v.getVarbitId() == 12353)); + assertTrue("Lumbridge Home Teleport should be gated by LAST_HOME_TELEPORT cooldown", + transport.getVarplayers().stream().anyMatch(v -> v.getVarplayerId() == VarPlayer.LAST_HOME_TELEPORT + && v.getOperator() == TransportVarPlayer.Operator.COOLDOWN_MINUTES + && v.getValue() == 30)); + } + + @Test + public void testLumbridgeHomeTeleportCooldownRejectsRecentUse() { + Transport transport = getLumbridgeHomeTeleportTransport(); + TransportVarPlayer cooldown = transport.getVarplayers().stream() + .filter(v -> v.getVarplayerId() == VarPlayer.LAST_HOME_TELEPORT) + .findFirst() + .orElseThrow(() -> new AssertionError("Lumbridge Home Teleport should have a LAST_HOME_TELEPORT varplayer")); + + int nowMinutes = (int) (System.currentTimeMillis() / 60000); + assertFalse("Home teleport should be unavailable shortly after use", + cooldown.matches(nowMinutes - 5)); + assertFalse("Home teleport should stay unavailable until more than 30 minutes have elapsed", + cooldown.matches(nowMinutes - 30)); + assertTrue("Home teleport should be available after the 30 minute cooldown has elapsed", + cooldown.matches(nowMinutes - 31)); + } + + private static Transport getLumbridgeHomeTeleportTransport() { HashMap> transports = Transport.loadAllFromResources(); - boolean hasLumbridgeHomeTeleport = transports.values().stream() + Optional lumbridgeHomeTeleport = transports.values().stream() .flatMap(Set::stream) - .anyMatch(t -> t.getType() == TransportType.TELEPORTATION_SPELL + .filter(t -> t.getType() == TransportType.TELEPORTATION_SPELL && "Lumbridge Home Teleport".equals(t.getDisplayInfo()) - && new WorldPoint(3221, 3218, 0).equals(t.getDestination()) - && t.getVarbits().stream().anyMatch(v -> v.getVarbitId() == 12353 && v.getValue() == 0)); + && new WorldPoint(3221, 3218, 0).equals(t.getDestination())) + .findFirst(); - assertTrue("Lumbridge Home Teleport should be loaded and gated by cooldown varbit", hasLumbridgeHomeTeleport); + assertTrue("Lumbridge Home Teleport should be loaded", lumbridgeHomeTeleport.isPresent()); + return lumbridgeHomeTeleport.get(); } @Test @@ -563,6 +595,58 @@ public void testDungeonPathToKnownReachableTile() { distToTarget <= 2); } + @Test + public void testVarrockSewerPathAvoidsDisabledPalaceTrellisShortcut() { + PathfinderConfig config = createConfigWithUnavailableShortcutEdges(TransportType.AGILITY_SHORTCUT); + WorldPoint src = new WorldPoint(3203, 3501, 0); + WorldPoint dst = new WorldPoint(3237, 9858, 0); + WorldPoint northTrellis = new WorldPoint(3228, 3471, 0); + WorldPoint southTrellis = new WorldPoint(3228, 3470, 0); + + Pathfinder pf = new Pathfinder(config, src, dst); + pf.run(); + + assertTrue("Pathfinder should complete", pf.isDone()); + List rawPath = pf.getPath(); + assertFalse("Path should not be empty", rawPath.isEmpty()); + assertFalse("Raw path must not cross the disabled Varrock Palace trellis shortcut", + hasConsecutiveStep(rawPath, northTrellis, southTrellis)); + + List smoothedPath = pf.getWalkablePath(); + assertFalse("Smoothed path must not cross the disabled Varrock Palace trellis shortcut", + hasLineSegmentStep(smoothedPath, northTrellis, southTrellis)); + + WorldPoint endpoint = rawPath.get(rawPath.size() - 1); + assertTrue("Path should still reach Varrock Sewers, ended at " + endpoint, + endpoint.distanceTo(dst) <= 1); + } + + @Test + public void testVarrockSewerPathAvoidsPalaceGardenSouthFenceCollisionGap() { + PathfinderConfig config = createConfigWithUnavailableShortcutEdges(TransportType.AGILITY_SHORTCUT); + WorldPoint src = new WorldPoint(3236, 3477, 0); + WorldPoint dst = new WorldPoint(3237, 9858, 0); + + Pathfinder pf = new Pathfinder(config, src, dst); + pf.run(); + + assertTrue("Pathfinder should complete", pf.isDone()); + List rawPath = pf.getPath(); + assertFalse("Path should not be empty", rawPath.isEmpty()); + String rawCrossing = findFenceConsecutiveCrossing(rawPath, 3229, 3241, 3472, 3471, 0); + assertNull("Raw path must not cross the Varrock Palace garden south fence: " + rawCrossing, + rawCrossing); + + List smoothedPath = pf.getWalkablePath(); + String smoothedCrossing = findFenceCrossing(smoothedPath, 3229, 3241, 3472, 3471, 0); + assertNull("Smoothed path must not cross the Varrock Palace garden south fence: " + smoothedCrossing, + smoothedCrossing); + + WorldPoint endpoint = rawPath.get(rawPath.size() - 1); + assertTrue("Path should still reach Varrock Sewers, ended at " + endpoint, + endpoint.distanceTo(dst) <= 1); + } + @Test public void testIgnoreCollisionPackedIsHashSetLookup() { int packed = WorldPointUtil.packWorldPoint(3142, 3457, 0); @@ -602,6 +686,111 @@ private PathfinderConfig createConfigWithTransports() { return config; } + private PathfinderConfig createConfigWithUnavailableShortcutEdges(TransportType... unavailableTypes) { + Set unavailable = new HashSet<>(Arrays.asList(unavailableTypes)); + HashMap> allTransports = Transport.loadAllFromResources(); + PathfinderConfig config = new PathfinderConfig( + collisionMap, + allTransports, + Collections.emptyList(), + null, + null + ); + try { + java.lang.reflect.Field f = PathfinderConfig.class.getDeclaredField("calculationCutoffMillis"); + f.setAccessible(true); + f.setLong(config, 10000); + + for (Map.Entry> entry : allTransports.entrySet()) { + if (entry.getKey() == null) { + continue; + } + Set usable = entry.getValue().stream() + .filter(t -> !unavailable.contains(t.getType())) + .collect(java.util.stream.Collectors.toSet()); + entry.getValue().stream() + .filter(t -> unavailable.contains(t.getType())) + .forEach(config::addBlockedTransportEdgeIfNeeded); + if (!usable.isEmpty()) { + config.getTransports().put(entry.getKey(), usable); + config.getTransportsPacked().put( + WorldPointUtil.packWorldPoint(entry.getKey()), usable); + } + } + } catch (Exception e) { + throw new RuntimeException("Failed to configure unavailable shortcut edges", e); + } + return config; + } + + private static boolean hasConsecutiveStep(List path, WorldPoint a, WorldPoint b) { + for (int i = 0; i + 1 < path.size(); i++) { + if (isEitherDirection(path.get(i), path.get(i + 1), a, b)) { + return true; + } + } + return false; + } + + private static boolean hasLineSegmentStep(List path, WorldPoint a, WorldPoint b) { + return findLineSegmentStep(path, a, b) != null; + } + + private static String findLineSegmentStep(List path, WorldPoint a, WorldPoint b) { + for (int i = 0; i + 1 < path.size(); i++) { + if (path.get(i).distanceTo2D(path.get(i + 1)) > 10) { + continue; + } + if (lineSegmentContainsStep(path.get(i), path.get(i + 1), a, b)) { + return path.get(i) + " -> " + path.get(i + 1); + } + } + return null; + } + + private static boolean lineSegmentContainsStep(WorldPoint from, WorldPoint to, WorldPoint a, WorldPoint b) { + if (from.getPlane() != to.getPlane()) return false; + int x = from.getX(); + int y = from.getY(); + while (x != to.getX() || y != to.getY()) { + WorldPoint stepFrom = new WorldPoint(x, y, from.getPlane()); + x += Integer.signum(to.getX() - x); + y += Integer.signum(to.getY() - y); + WorldPoint stepTo = new WorldPoint(x, y, from.getPlane()); + if (isEitherDirection(stepFrom, stepTo, a, b)) { + return true; + } + } + return false; + } + + private static boolean isEitherDirection(WorldPoint from, WorldPoint to, WorldPoint a, WorldPoint b) { + return (from.equals(a) && to.equals(b)) || (from.equals(b) && to.equals(a)); + } + + private static boolean hasFenceCrossing(List path, int minX, int maxX, int northY, int southY, int plane) { + return findFenceCrossing(path, minX, maxX, northY, southY, plane) != null; + } + + private static String findFenceConsecutiveCrossing(List path, int minX, int maxX, int northY, int southY, int plane) { + for (int x = minX; x <= maxX; x++) { + if (hasConsecutiveStep(path, new WorldPoint(x, northY, plane), new WorldPoint(x, southY, plane))) { + return x + "," + northY + "<->" + x + "," + southY; + } + } + return null; + } + + private static String findFenceCrossing(List path, int minX, int maxX, int northY, int southY, int plane) { + for (int x = minX; x <= maxX; x++) { + String segment = findLineSegmentStep(path, new WorldPoint(x, northY, plane), new WorldPoint(x, southY, plane)); + if (segment != null) { + return x + "," + northY + "<->" + x + "," + southY + " via " + segment; + } + } + return null; + } + // ======================== // Pathfinder Tiebreaker / Route Diversity Tests // ======================== 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 6399fd9aee..3cf267f366 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 @@ -2,6 +2,8 @@ import net.runelite.api.WallObject; import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.shortestpath.Transport; +import net.runelite.client.plugins.microbot.shortestpath.TransportType; import net.runelite.client.plugins.microbot.shortestpath.pathfinder.Pathfinder; import org.junit.After; import org.junit.Before; @@ -49,6 +51,127 @@ public void tearDown() { Rs2Walker.sessionBlacklistedDoors.clear(); } + @Test + public void adjacentTransportSuppression_onlyAdjacentSamePlaneTransports() { + Transport door = new Transport( + new WorldPoint(3123, 3360, 0), + new WorldPoint(3123, 3361, 0), + "Door", + TransportType.TRANSPORT, + false, + "Open", + "Door", + 136); + + assertEquals(new HashSet<>(Arrays.asList( + new WorldPoint(3123, 3360, 0), + new WorldPoint(3123, 3361, 0))), + Rs2Walker.adjacentSamePlaneTransportSuppressionPoints(door, null)); + } + + @Test + public void adjacentTransportSuppression_ignoresNonAdjacentTransports() { + Transport ladder = new Transport( + new WorldPoint(3092, 3361, 0), + new WorldPoint(3117, 9753, 0), + "Ladder", + TransportType.TRANSPORT, + false, + "Climb-down", + "Ladder", + 133); + + assertTrue(Rs2Walker.adjacentSamePlaneTransportSuppressionPoints(ladder, null).isEmpty()); + } + + @Test + public void shouldRecalculatePathAfterTransport_includesOriginlessTeleport() { + Transport varrockTeleport = new Transport( + new WorldPoint(3213, 3424, 0), + "Varrock Teleport", + TransportType.TELEPORTATION_SPELL, + false, + 20, + Collections.emptyMap()); + + assertTrue(Rs2Walker.shouldRecalculatePathAfterTransport(varrockTeleport)); + } + + @Test + public void shouldRecalculatePathAfterTransport_skipsAdjacentSamePlaneTransport() { + Transport door = new Transport( + new WorldPoint(3123, 3360, 0), + new WorldPoint(3123, 3361, 0), + "Door", + TransportType.TRANSPORT, + false, + "Open", + "Door", + 136); + + assertFalse(Rs2Walker.shouldRecalculatePathAfterTransport(door)); + } + + @Test + public void shouldRecalculatePathAfterTransport_includesLongDistanceTransport() { + Transport ship = new Transport( + new WorldPoint(3054, 3245, 0), + new WorldPoint(2956, 3146, 0), + "Port Sarim to Karamja", + TransportType.SHIP, + false, + "Cross", + "Gangplank", + 2082); + + assertTrue(Rs2Walker.shouldRecalculatePathAfterTransport(ship)); + } + + @Test + public void shouldRecalculatePathAfterTransport_includesSamePlaneCoordinateBandTransport() { + Transport varrockSewerLadder = new Transport( + new WorldPoint(3237, 9858, 0), + new WorldPoint(3236, 3458, 0), + "Varrock Sewers ladder", + TransportType.TRANSPORT, + false, + "Climb-up", + "Ladder", + 11806); + + assertTrue(Rs2Walker.shouldRecalculatePathAfterTransport(varrockSewerLadder)); + } + + @Test + public void hasPendingRouteStepBeforeArrival_detectsTransportBeforeDestination() { + List path = Arrays.asList( + new WorldPoint(3220, 3473, 0), + new WorldPoint(3221, 3473, 0), + new WorldPoint(3222, 3473, 0), + new WorldPoint(3229, 3473, 0)); + + assertTrue(Rs2Walker.hasPendingRouteStepBeforeArrival( + path, + new WorldPoint(3229, 3473, 0), + 0, + i -> i == 1)); + } + + @Test + public void hasPendingRouteStepBeforeArrival_ignoresStepsInsideArrivalTolerance() { + List path = Arrays.asList( + new WorldPoint(3220, 3473, 0), + new WorldPoint(3227, 3473, 0), + new WorldPoint(3228, 3473, 0), + new WorldPoint(3229, 3473, 0)); + + assertFalse(Rs2Walker.hasPendingRouteStepBeforeArrival( + path, + new WorldPoint(3229, 3473, 0), + 2, + i -> i == 2)); + } + // --------------------------------------------------------------------------- // #15 — Sidestep recovery ranking // --------------------------------------------------------------------------- diff --git a/scripts/run-ge-lumbridge-teleport-harness.sh b/scripts/run-ge-lumbridge-teleport-harness.sh new file mode 100755 index 0000000000..7c9ef9c6ef --- /dev/null +++ b/scripts/run-ge-lumbridge-teleport-harness.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +set -euo pipefail + +ITERATIONS="${MICROBOT_GE_LUMBRIDGE_ITERATIONS:-10}" +TIMEOUT_MS="${MICROBOT_GE_LUMBRIDGE_TIMEOUT_MS:-5400000}" +LEG_TIMEOUT_MS="${MICROBOT_GE_LUMBRIDGE_LEG_TIMEOUT_MS:-300000}" +OUTPUT_DIR="${MICROBOT_GE_LUMBRIDGE_OUTPUT_DIR:-$HOME/.runelite/test-results/ge-lumbridge-teleport}" +MONITOR_INTERVAL="${MICROBOT_GE_LUMBRIDGE_MONITOR_INTERVAL:-0.2}" + +cd "$(dirname "$0")/.." + +rm -rf "$OUTPUT_DIR" +mkdir -p "$OUTPUT_DIR" + +MONITOR_FILE="$OUTPUT_DIR/monitor.jsonl" +RUN_LOG="$OUTPUT_DIR/run.log" + +./gradlew :client:compileJava + +CMD=( + ./gradlew :client:runTest + -Dmicrobot.test.mode=true + "-Dmicrobot.test.script=GE Lumbridge Teleport Harness" + "-Dmicrobot.test.timeout=$TIMEOUT_MS" + "-Dmicrobot.test.output=$OUTPUT_DIR" + "-Dmicrobot.test.geLumbridge.iterations=$ITERATIONS" + "-Dmicrobot.test.geLumbridge.walkTimeoutMs=$LEG_TIMEOUT_MS" +) + +set +e +"${CMD[@]}" 2>&1 | tee "$RUN_LOG" & +RUN_PID=$! + +( + while kill -0 "$RUN_PID" 2>/dev/null; do + STATE="$(timeout 1 ./microbot-cli state 2>/dev/null | jq -c . 2>/dev/null)" + STATUS=$? + if [[ "$STATUS" -eq 0 && "$STATE" == \{* ]]; then + printf '{"sampledAt":"%s","state":%s}\n' "$(date --iso-8601=ns)" "$STATE" >> "$MONITOR_FILE" + fi + sleep "$MONITOR_INTERVAL" + done +) & +MONITOR_PID=$! + +wait "$RUN_PID" +STATUS=$? +kill "$MONITOR_PID" 2>/dev/null || true +wait "$MONITOR_PID" 2>/dev/null || true +set -e + +RESULT_FILE="$OUTPUT_DIR/result.json" +if [[ -f "$RESULT_FILE" ]]; then + echo "Result written to $RESULT_FILE" +else + echo "No result file was written at $RESULT_FILE" >&2 +fi + +if [[ -f "$MONITOR_FILE" ]]; then + echo "Monitor written to $MONITOR_FILE" +fi + +exit "$STATUS" From 0ef0a8753d88c9762ffc296da480367ad1ade180 Mon Sep 17 00:00:00 2001 From: Sami Date: Sun, 10 May 2026 18:23:31 +0200 Subject: [PATCH 2/3] refactor(pathfinder): load static blocked edges from resources and update .gitignore --- .gitignore | 3 + .../pathfinder/PathfinderConfig.java | 78 +++++++++++++++---- .../GeLumbridgeTeleportHarnessPlugin.java | 3 + .../microbot/shortestpath/blocked_edges.tsv | 15 ++++ .../shortestpath/teleportation_spells.tsv | 2 +- .../client-thread-guardrail-baseline.txt | 63 ++++++++------- 6 files changed, 121 insertions(+), 43 deletions(-) create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/blocked_edges.tsv diff --git a/.gitignore b/.gitignore index 5b50739056..9c78ab7a2c 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,9 @@ target/ ### Test loop artifacts ### logs/ REVIEW_FINDINGS.md.bak +.claude/scheduled_tasks.lock +__pycache__/ +*.py[cod] ### Stray Node lockfiles (this is a Java/Gradle project) ### node_modules/ 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 567c10dcca..3b9c60ed4d 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 @@ -22,6 +22,8 @@ import net.runelite.client.plugins.microbot.util.poh.PohTeleports; import net.runelite.client.plugins.microbot.util.tabs.Rs2Tab; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; @@ -51,10 +53,10 @@ public class PathfinderConfig { private static final WorldArea NOT_WILDERNESS_4 = new WorldArea(3031, 3525, 2, 2, 0); private static final WorldPoint SPIRIT_TREE_ETCETERIA = new WorldPoint(2613, 3855, 0); private static final WorldPoint SPIRIT_TREE_BRIMHAVEN = new WorldPoint(2800, 3203, 0); - private static final WorldPoint SPIRIT_TREE_PORT_SARIM = new WorldPoint(3058, 3257, 0); + private static final WorldPoint SPIRIT_TREE_PORT_SARIM = new WorldPoint(3058, 3257, 0); private static final WorldPoint SPIRIT_TREE_HOSIDIUS = new WorldPoint(1693, 3540, 0); private static final WorldPoint SPIRIT_TREE_FARMING_GUILD = new WorldPoint(1251, 3750, 0); - private static final Set STATIC_BLOCKED_EDGES_PACKED = createStaticBlockedEdges(); + private static final Set STATIC_BLOCKED_EDGES_PACKED = loadStaticBlockedEdgesFromResources(); private final SplitFlagMap mapData; private final ThreadLocal map; @@ -465,22 +467,72 @@ private void addBlockedEdge(WorldPoint origin, WorldPoint destination) { WorldPointUtil.packWorldPoint(destination))); } - private static Set createStaticBlockedEdges() { + private static Set loadStaticBlockedEdgesFromResources() { Set edges = new HashSet<>(); - // The Varrock Palace garden south fence is underrepresented in the static - // collision data. Without these edges, no-agility F2P routes can try to walk - // straight through the garden boundary toward the Varrock Sewers manhole. - for (int x = 3229; x <= 3241; x++) { - addBidirectionalStaticEdge(edges, x, 3472, 0, x, 3471, 0); + final String delimColumn = "\t"; + final String prefixComment = "#"; + + try { + String s = new String(Util.readAllBytes( + ShortestPathPlugin.class.getResourceAsStream("blocked_edges.tsv")), StandardCharsets.UTF_8); + Scanner scanner = new Scanner(s); + String headerLine = scanner.nextLine(); + headerLine = headerLine.startsWith(prefixComment + " ") + ? headerLine.replace(prefixComment + " ", prefixComment) + : headerLine; + headerLine = headerLine.startsWith(prefixComment) + ? headerLine.replace(prefixComment, "") + : headerLine; + String[] headers = headerLine.split(delimColumn); + + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + if (line.startsWith(prefixComment) || line.isBlank()) { + continue; + } + + String[] fields = line.split(delimColumn); + Map fieldMap = new HashMap<>(); + for (int i = 0; i < headers.length; i++) { + if (i < fields.length) { + fieldMap.put(headers[i], fields[i]); + } + } + + WorldPoint origin = parseBlockedEdgePoint(fieldMap.get("Origin")); + WorldPoint destination = parseBlockedEdgePoint(fieldMap.get("Destination")); + boolean bidirectional = Boolean.parseBoolean(fieldMap.getOrDefault("Bidirectional", "false")); + addStaticEdge(edges, origin, destination); + if (bidirectional) { + addStaticEdge(edges, destination, origin); + } + } + scanner.close(); + } catch (IOException e) { + throw new RuntimeException("Unable to load shortest-path blocked edges", e); } + return Collections.unmodifiableSet(edges); } - private static void addBidirectionalStaticEdge(Set edges, int ax, int ay, int az, int bx, int by, int bz) { - int a = WorldPointUtil.packWorldPoint(ax, ay, az); - int b = WorldPointUtil.packWorldPoint(bx, by, bz); - edges.add(transportEdgeKey(a, b)); - edges.add(transportEdgeKey(b, a)); + private static WorldPoint parseBlockedEdgePoint(String point) { + if (point == null || point.isBlank()) { + throw new IllegalArgumentException("Blocked edge point is blank"); + } + String[] parts = point.trim().split(" "); + if (parts.length != 3) { + throw new IllegalArgumentException("Blocked edge point must be 'x y plane': " + point); + } + return new WorldPoint( + Integer.parseInt(parts[0]), + Integer.parseInt(parts[1]), + Integer.parseInt(parts[2])); + } + + private static void addStaticEdge(Set edges, WorldPoint origin, WorldPoint destination) { + edges.add(transportEdgeKey( + WorldPointUtil.packWorldPoint(origin), + WorldPointUtil.packWorldPoint(destination))); } private static boolean blocksWalkingEdgeWhenUnavailable(Transport transport) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/GeLumbridgeTeleportHarnessPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/GeLumbridgeTeleportHarnessPlugin.java index 2bd9dc70f6..b366147632 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/GeLumbridgeTeleportHarnessPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/GeLumbridgeTeleportHarnessPlugin.java @@ -73,6 +73,8 @@ protected void shutDown() { if (executor != null) { executor.shutdownNow(); } + Rs2Walker.setTarget(null); + activeLeg = null; } @Subscribe @@ -251,6 +253,7 @@ private String walk(WorldPoint destination, int tolerance, int timeoutMs, LegOut } catch (TimeoutException e) { outcome.walkerThreadDump = dumpWalkerThread(); future.cancel(true); + Rs2Walker.setTarget(null); return "TIMEOUT"; } catch (Exception e) { log.warn("[GeLumbridgeTeleportHarness] Webwalker leg failed for destination {}", destination, e); 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 new file mode 100644 index 0000000000..86e50f9d00 --- /dev/null +++ b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/blocked_edges.tsv @@ -0,0 +1,15 @@ +# Origin Destination Bidirectional Display info +# Static collision edge overrides for map data gaps. These block movement across the edge, not the whole tile. +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 +3232 3472 0 3232 3471 0 true Varrock Palace garden south fence +3233 3472 0 3233 3471 0 true Varrock Palace garden south fence +3234 3472 0 3234 3471 0 true Varrock Palace garden south fence +3235 3472 0 3235 3471 0 true Varrock Palace garden south fence +3236 3472 0 3236 3471 0 true Varrock Palace garden south fence +3237 3472 0 3237 3471 0 true Varrock Palace garden south fence +3238 3472 0 3238 3471 0 true Varrock Palace garden south fence +3239 3472 0 3239 3471 0 true Varrock Palace garden south fence +3240 3472 0 3240 3471 0 true Varrock Palace garden south fence +3241 3472 0 3241 3471 0 true Varrock Palace garden south fence diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/teleportation_spells.tsv b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/teleportation_spells.tsv index e62cbf553e..816d88f483 100644 --- a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/teleportation_spells.tsv +++ b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/teleportation_spells.tsv @@ -1,6 +1,6 @@ # Destination Skills Quests Wilderness level Varbits Varplayers isMembers Duration Display info # Standard Spellbook - Varbit 4070=0 -# Lumbridge Home Teleport. Varplayer 892 is LAST_HOME_TELEPORT in epoch minutes. +# Lumbridge Home Teleport. Varplayer 892 is LAST_HOME_TELEPORT in epoch minutes. 3221 3218 0 19 4070=0 892@30 20 Lumbridge Home Teleport # Varrock Teleport 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 ef1c6441ab..cf4dd21500 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 @@ -704,6 +704,7 @@ net.runelite.client.plugins.microbot.util.walker.Rs2MiniMap#getMinimapClipAreaSi net.runelite.client.plugins.microbot.util.walker.Rs2MiniMap#getMinimapDrawWidget(): Widget -> net.runelite.api.Client#isResized(): boolean net.runelite.client.plugins.microbot.util.walker.Rs2MiniMap#worldToMinimap(WorldPoint): Point -> net.runelite.api.Client#getTopLevelWorldView(): WorldView 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#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 @@ -720,10 +721,10 @@ net.runelite.client.plugins.microbot.util.walker.Rs2Walker#getTransportsForPath( net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleCharterShip(Transport): boolean -> net.runelite.api.widgets.Widget#getDynamicChildren(): Widget[] net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleCharterShip(Transport): boolean -> net.runelite.api.widgets.Widget#getId(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleCharterShip(Transport): boolean -> net.runelite.api.widgets.Widget#getIndex(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleDoors(List, int): boolean -> net.runelite.api.Client#getTopLevelWorldView(): WorldView -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleDoors(List, int): boolean -> net.runelite.api.Scene#isInstance(): boolean -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleDoors(List, int): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleDoors(List, int): boolean -> net.runelite.api.WorldView#getScene(): Scene +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleDoors(List, int, boolean): boolean -> net.runelite.api.Client#getTopLevelWorldView(): WorldView +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleDoors(List, int, boolean): boolean -> net.runelite.api.Scene#isInstance(): boolean +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleDoors(List, int, boolean): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleDoors(List, int, boolean): boolean -> net.runelite.api.WorldView#getScene(): Scene net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleFairyRing(Transport): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleMasterScrollBook(String): boolean -> net.runelite.api.widgets.Widget#getStaticChildren(): Widget[] net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleMinigameTeleport(Transport): boolean -> net.runelite.api.widgets.Widget#getDynamicChildren(): Widget[] @@ -732,7 +733,7 @@ net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleMinigameTelepor net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleMinigameTeleport(Transport): boolean -> net.runelite.api.widgets.Widget#getOnOpListener(): Object[] net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleMinigameTeleport(Transport): boolean -> net.runelite.api.widgets.Widget#getSpriteId(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleMinigameTeleport(Transport): boolean -> net.runelite.api.widgets.Widget#getText(): String -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleObject(Transport, TileObject): boolean -> net.runelite.api.TileObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleObject(Transport, TileObject, String): boolean -> net.runelite.api.TileObject#getId(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleObjectExceptions(Transport, TileObject): boolean -> net.runelite.api.TileObject#getId(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleObjectExceptions(Transport, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleObjectExceptions(Transport, TileObject): boolean -> net.runelite.api.widgets.Widget#getItemId(): int @@ -744,36 +745,40 @@ net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleTransports(List net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleTransports(List, int): boolean -> net.runelite.api.ObjectComposition#getName(): String net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleTransports(List, int): boolean -> net.runelite.api.Scene#isInstance(): boolean net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleTransports(List, int): boolean -> net.runelite.api.TileObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleTransports(List, int): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint net.runelite.client.plugins.microbot.util.walker.Rs2Walker#handleTransports(List, int): boolean -> net.runelite.api.WorldView#getScene(): Scene net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isCloseToRegion(int, int, int): boolean -> net.runelite.api.Client#getTopLevelWorldView(): WorldView net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isCloseToRegion(int, int, int): boolean -> net.runelite.api.WorldView#getPlane(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isDoorComposition(ObjectComposition, List): boolean -> net.runelite.api.ObjectComposition#getImpostorIds(): int[] net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isDoorComposition(ObjectComposition, List): boolean -> net.runelite.api.ObjectComposition#getName(): String net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isDoorOnSegment(TileObject, WorldPoint, WorldPoint): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$findDoorNearSegment$15(WorldPoint, List, WorldPoint, WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$findDoorNearSegment$16(WorldPoint, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleCharterShip$121(Widget): boolean -> net.runelite.api.widgets.Widget#getActions(): String[] -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleDoors$14(WorldPoint, GameObject): boolean -> net.runelite.api.GameObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleFairyRing$127(Transport, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleMinigameTeleport$100(Widget, Object[]): boolean -> net.runelite.api.widgets.Widget#getOnOpListener(): Object[] -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleMinigameTeleport$102(String): boolean -> net.runelite.api.widgets.Widget#getText(): String -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$72(int, TileObject): boolean -> net.runelite.api.TileObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$78(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$78(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$79(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$79(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleRockfall$10(WorldPoint, Tile): boolean -> net.runelite.api.Tile#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$50(int, TileObject): boolean -> net.runelite.api.TileObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$51(Transport, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$53(TileObject): boolean -> net.runelite.api.ObjectComposition#getName(): String -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$54(Transport, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$55(TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$56(Transport, Object): Integer -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$57(Transport, Object): Integer -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$61(int, String, TileObject): boolean -> net.runelite.api.TileObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$62(Transport, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleWildernessObelisk$86(Transport, GameObject): boolean -> net.runelite.api.GameObject#getId(): int -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleWildernessObelisk$87(Transport, GameObject): boolean -> net.runelite.api.GameObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isKnownWalkableOrUnloaded(WorldPoint): boolean -> net.runelite.api.Client#getTopLevelWorldView(): WorldView +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#isKnownWalkableOrUnloaded(WorldPoint): boolean -> net.runelite.api.coords.LocalPoint#fromWorld(WorldView, WorldPoint): LocalPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$doorStillHasAction$35(WorldPoint, GameObject): boolean -> net.runelite.api.GameObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$findDoorNearSegment$32(WorldPoint, WorldPoint, WorldPoint, List, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$findDoorNearSegment$33(WorldPoint, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleCharterShip$146(Widget): boolean -> net.runelite.api.widgets.Widget#getActions(): String[] +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleDoors$31(WorldPoint, GameObject): boolean -> net.runelite.api.GameObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleFairyRing$152(Transport, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleMinigameTeleport$125(Widget, Object[]): boolean -> net.runelite.api.widgets.Widget#getOnOpListener(): Object[] +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleMinigameTeleport$127(String): boolean -> net.runelite.api.widgets.Widget#getText(): String +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$103(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$103(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$104(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$104(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$97(int, TileObject): boolean -> net.runelite.api.TileObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleRockfall$17(WorldPoint, Tile): boolean -> net.runelite.api.Tile#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$74(int, TileObject): boolean -> net.runelite.api.TileObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$76(Transport, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$78(TileObject): boolean -> net.runelite.api.ObjectComposition#getName(): String +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$79(Transport, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$80(TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$81(Transport, Object): Integer -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$82(Transport, Object): Integer -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$84(int, List, TileObject): boolean -> net.runelite.api.TileObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleTransports$85(Transport, TileObject): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleWildernessObelisk$111(Transport, GameObject): boolean -> net.runelite.api.GameObject#getId(): int +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleWildernessObelisk$112(Transport, GameObject): boolean -> net.runelite.api.GameObject#getId(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$processWalk$0(): boolean -> net.runelite.api.widgets.Widget#getSpriteId(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$processWalk$1(): boolean -> net.runelite.api.widgets.Widget#getSpriteId(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#processWalk(WorldPoint, int, int): WalkerState -> net.runelite.api.Client#getTopLevelWorldView(): WorldView From 828aecb66ea8ce96fb0426efc9106d14f1081900 Mon Sep 17 00:00:00 2001 From: Sami Date: Sun, 10 May 2026 18:23:46 +0200 Subject: [PATCH 3/3] chore(gradle): update microbot version to 2.5.8 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ccebfca0e0..08802b10fb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -31,7 +31,7 @@ project.build.group=net.runelite project.build.version=1.12.26.2 glslang.path= -microbot.version=2.5.7 +microbot.version=2.5.8 microbot.commit.sha=nogit microbot.repo.url=http://138.201.81.246:8081/repository/microbot-snapshot/ microbot.repo.username=