diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/moaaudit/MoaAuditPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/moaaudit/MoaAuditPlugin.java deleted file mode 100644 index 03345e71e8..0000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/moaaudit/MoaAuditPlugin.java +++ /dev/null @@ -1,32 +0,0 @@ -package net.runelite.client.plugins.microbot.moaaudit; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.Plugin; -import net.runelite.client.plugins.PluginDescriptor; -import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; - -// TEMP debug plugin: on enable, iterates every Map of Alacrity seasonal transport, -// attempts to teleport, and logs actual landing vs expected coord for each. Used to -// catch bad destination coords in seasonal_transports.tsv. Delete when done. -@PluginDescriptor( - name = PluginDescriptor.Default + "MoA Audit", - description = "[TEMP] Record Map of Alacrity teleport landing tiles", - tags = {"temp", "debug", "league", "microbot"}, - enabledByDefault = false -) -@Slf4j -public class MoaAuditPlugin extends Plugin { - private Thread worker; - - @Override - protected void startUp() { - worker = new Thread(Rs2Walker::runMoaAudit, "moa-audit"); - worker.setDaemon(true); - worker.start(); - } - - @Override - protected void shutDown() { - if (worker != null) worker.interrupt(); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/CollisionMap.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/CollisionMap.java index af2da68622..74a88396e6 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 @@ -157,6 +157,7 @@ public List getNeighbors(Node node, VisitedTiles visited, PathfinderConfig int moaAddedHere = 0; int moaVisited = 0; int moaIgnored = 0; + List moaCosts = null; // Transports are pre-filtered by PathfinderConfig.refreshTransports // Thus any transports in the list are guaranteed to be valid per the user's settings @@ -177,8 +178,13 @@ public List getNeighbors(Node node, VisitedTiles visited, PathfinderConfig if (isMoa) moaIgnored++; continue; } - neighbors.add(new TransportNode(transport.getDestination(), node, config.getDistanceBeforeUsingTeleport() + transport.getDuration())); - if (isMoa) moaAddedHere++; + int cost = config.getDistanceBeforeUsingTeleport() + transport.getDuration(); + neighbors.add(new TransportNode(transport.getDestination(), node, cost)); + if (isMoa) { + moaAddedHere++; + if (moaCosts == null) moaCosts = new ArrayList<>(); + moaCosts.add(cost); + } } else { neighbors.add(new TransportNode(transport.getDestination(), node, transport.getDuration())); } @@ -186,10 +192,10 @@ public List getNeighbors(Node node, VisitedTiles visited, PathfinderConfig } if (moaSeenHere > 0) { - log.debug("[MoA] getNeighbors @ ({},{},{}): seen={} added={} visited={} ignored={} (distanceBeforeUsingTeleport={}, cost={})", + log.debug("[MoA] getNeighbors @ ({},{},{}): seen={} added={} visited={} ignored={} (distanceBeforeUsingTeleport={}, costs={})", x, y, z, moaSeenHere, moaAddedHere, moaVisited, moaIgnored, config.getDistanceBeforeUsingTeleport(), - config.getDistanceBeforeUsingTeleport() + 4); + moaCosts == null ? "[]" : moaCosts); } if (isBlocked(x, y, z)) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java index 60ec296983..ef68324dfb 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 @@ -577,6 +577,23 @@ private boolean useTransport(Transport transport) { return false; } + // Region-level lock: once handleSeasonalTransport sees a region render with + // (locked), reject every destination in that region. Without this, + // the pathfinder keeps picking a different Asgarnia/Desert/etc. destination on + // each re-path — walker fails, blacklists one, re-path picks the next, infinite + // "running around" loop. Display info format: "Map of Alacrity: - ". + if (traceMoa && !Rs2Walker.lockedMoaRegions.isEmpty()) { + String disp = transport.getDisplayInfo(); + int colon = disp.indexOf(':'); + int dash = colon >= 0 ? disp.indexOf(" - ", colon) : -1; + if (colon >= 0 && dash > colon) { + String region = disp.substring(colon + 1, dash).trim().toLowerCase(); + if (Rs2Walker.lockedMoaRegions.contains(region)) { + return false; + } + } + } + // Check if the feature flag is disabled if (!isFeatureEnabled(transport)) { log.debug("Transport Type {} is disabled by feature flag", transport.getType()); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/TransportNode.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/TransportNode.java index b54b549375..28b048e6c0 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/TransportNode.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/TransportNode.java @@ -4,11 +4,14 @@ public class TransportNode extends Node implements Comparable { public TransportNode(WorldPoint point, Node previous, int travelTime) { - super(point, previous, cost(previous, travelTime)); - } - - private static int cost(Node previous, int travelTime) { - return (previous != null ? previous.cost : 0) + travelTime; + // Use Node(int, Node, int cost) which assigns cost directly. The WorldPoint + // Node constructor re-adds previous.cost via its cost(previous, wait) method, + // which caused (a) double-counting when we passed prev.cost + travelTime as + // wait and (b) integer overflow for plane-crossing transports with travelTime=0 + // because its distance fallback returns Integer.MAX_VALUE across planes. + super(net.runelite.client.plugins.microbot.shortestpath.WorldPointUtil.packWorldPoint(point), + previous, + (previous != null ? previous.cost : 0) + travelTime); } @Override diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2Walker.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2Walker.java index b7315900e9..60b39b7b97 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 @@ -2709,7 +2709,7 @@ public static int getDistanceBetween(WorldPoint startpoint, WorldPoint endpoint) private static boolean handleSeasonalTransport(Transport transport) { String displayInfo = transport.getDisplayInfo(); - log.info("[MoA] entry: displayInfo='{}'", displayInfo); + log.debug("[MoA] entry: displayInfo='{}'", displayInfo); if (displayInfo == null) return false; if (!displayInfo.toLowerCase().contains("map of alacrity")) { @@ -2826,7 +2826,7 @@ private static boolean handleSeasonalTransport(Transport transport) { // Select via the row's in-game hotkey (1-9 then A-Z). Keybinds work even when the row // is scrolled off-screen, which clickWidget cannot handle. - log.info("[MoA] selecting destination '{}' (text='{}')", shortName, destText); + log.debug("[MoA] selecting destination '{}' (text='{}')", shortName, destText); Character hotkey = extractMoaHotkey(destText); if (hotkey == null) { Widget destRoot = Rs2Widget.getWidget(MAP_OF_ALACRITY_WIDGET_GROUP, MAP_OF_ALACRITY_LIST_CHILD); @@ -2858,30 +2858,27 @@ private static Widget findMoaWidget(Widget root, String shortName) { if (normalised.isEmpty()) return null; String[] tokens = normalised.split(" "); return Microbot.getClientThread().runOnClientThreadOptional(() -> { - for (Widget w : collectMoaChildren(root)) { - String hay = normaliseMoaText(w.getText()); - if (hay.isEmpty()) continue; - boolean all = true; - for (String t : tokens) { - if (t.isEmpty()) continue; - if (!hay.contains(t)) { all = false; break; } + Widget[][] groups = { root.getDynamicChildren(), root.getNestedChildren(), root.getStaticChildren() }; + for (Widget[] g : groups) { + if (g == null) continue; + for (Widget w : g) { + if (w == null) continue; + String hay = normaliseMoaText(w.getText()); + if (hay.isEmpty()) continue; + // Token-set membership avoids substring false positives (e.g. "log" matching "logstrum"). + java.util.Set haySet = new java.util.HashSet<>(java.util.Arrays.asList(hay.split(" "))); + boolean all = true; + for (String t : tokens) { + if (t.isEmpty()) continue; + if (!haySet.contains(t)) { all = false; break; } + } + if (all) return w; } - if (all) return w; } return null; }).orElse(null); } - private static java.util.List collectMoaChildren(Widget root) { - java.util.List out = new java.util.ArrayList<>(); - Widget[][] groups = { root.getDynamicChildren(), root.getNestedChildren(), root.getStaticChildren() }; - for (Widget[] g : groups) { - if (g == null) continue; - for (Widget w : g) if (w != null) out.add(w); - } - return out; - } - private static String normaliseMoaText(String s) { if (s == null) return ""; s = MOA_MARKUP_PATTERN.matcher(s).replaceAll(" "); @@ -2906,12 +2903,17 @@ private static Character computeMoaHotkeyByIndex(Widget root, Widget destMatch) if (root == null) return null; return Microbot.getClientThread().runOnClientThreadOptional(() -> { int idx = 0; - for (Widget sibling : collectMoaChildren(root)) { - String t = sibling.getText(); - if (t == null || t.isEmpty()) continue; - if (t.contains(MOA_LOCKED_MARKUP)) continue; - if (sibling == destMatch) return indexToHotkey(idx); - idx++; + Widget[][] groups = { root.getDynamicChildren(), root.getNestedChildren(), root.getStaticChildren() }; + for (Widget[] g : groups) { + if (g == null) continue; + for (Widget sibling : g) { + if (sibling == null) continue; + String t = sibling.getText(); + if (t == null || t.isEmpty()) continue; + if (t.contains(MOA_LOCKED_MARKUP)) continue; + if (sibling == destMatch) return indexToHotkey(idx); + idx++; + } } return null; }).orElse(null); @@ -2956,96 +2958,6 @@ private static void dumpMapOfAlacrityWidget(Widget listRoot) { }); } - // TEMP: iterate every MoA seasonal transport, attempt it, log landing vs expected. - // Run from a dedicated worker thread (blocks). Requires Map of Alacrity in inventory; - // locked regions/destinations are reported and skipped via the existing handler's guards. - public static void runMoaAudit() { - try { - while (!Microbot.isLoggedIn()) { - if (Thread.currentThread().isInterrupted()) return; - sleep(1000); - } - if (Rs2Inventory.get(MAP_OF_ALACRITY_ITEM_ID) == null) { - log.warn("[MoA-AUDIT] Map of Alacrity not in inventory — aborting"); - return; - } - - HashMap> all = Transport.loadAllFromResources(); - List moa = new ArrayList<>(); - for (Set set : all.values()) { - for (Transport t : set) { - if (t.getType() == TransportType.SEASONAL_TRANSPORT - && t.getDisplayInfo() != null - && t.getDisplayInfo().toLowerCase().contains("map of alacrity")) { - moa.add(t); - } - } - } - moa.sort(Comparator.comparing(Transport::getDisplayInfo)); - log.info("[MoA-AUDIT] {} MoA transports queued", moa.size()); - blacklistedMoaDestinations.clear(); - lockedMoaRegions.clear(); - - int landed = 0, skipped = 0; - for (int i = 0; i < moa.size(); i++) { - if (Thread.currentThread().isInterrupted()) break; - if (!Microbot.isLoggedIn()) { log.warn("[MoA-AUDIT] logged out — stopping"); break; } - - Transport t = moa.get(i); - String disp = t.getDisplayInfo(); - WorldPoint expected = t.getDestination(); - WorldPoint before = Rs2Player.getWorldLocation(); - if (before == null) { sleep(500); continue; } - - log.info("[MoA-AUDIT] {}/{}: {} (expected {},{},{})", - i + 1, moa.size(), disp, - expected.getX(), expected.getY(), expected.getPlane()); - - if (!handleSeasonalTransport(t)) { - log.info("[MoA-AUDIT] handler returned false"); - closeMoaWidgetIfOpen(); - skipped++; - sleep(600); - continue; - } - - boolean moved = sleepUntil(() -> { - WorldPoint now = Rs2Player.getWorldLocation(); - return now != null && (now.distanceTo(before) > 5 || now.getPlane() != before.getPlane()); - }, 8000); - - if (!moved) { - log.info("[MoA-AUDIT] no teleport detected"); - closeMoaWidgetIfOpen(); - skipped++; - continue; - } - - sleep(1500); // settle - WorldPoint after = Rs2Player.getWorldLocation(); - int dist = after.getPlane() == expected.getPlane() ? after.distanceTo(expected) : -1; - String marker = dist == 0 ? "EXACT" : (dist > 0 && dist <= 2 ? "close" : (dist > 0 && dist <= 10 ? "NEAR" : "FAR")); - log.info("[MoA-AUDIT] LAND {} | actual={},{},{} expected={},{},{} dist={} | {}", - marker, - after.getX(), after.getY(), after.getPlane(), - expected.getX(), expected.getY(), expected.getPlane(), - dist, disp); - landed++; - sleep(1500); - } - log.info("[MoA-AUDIT] complete: landed={}/{} skipped={}", landed, moa.size(), skipped); - } catch (Exception e) { - log.error("[MoA-AUDIT] crashed", e); - } - } - - private static void closeMoaWidgetIfOpen() { - if (Rs2Widget.isWidgetVisible(MAP_OF_ALACRITY_WIDGET_GROUP, MAP_OF_ALACRITY_LIST_CHILD)) { - Rs2Keyboard.keyPress(27); // ESC - sleep(400); - } - } - private static boolean handleSpiritTree(Transport transport) { // Get Transport Information String displayInfo = transport.getDisplayInfo(); 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 f4fe7820d0..3ca47a0032 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 @@ -689,9 +689,6 @@ net.runelite.client.plugins.microbot.util.walker.Rs2MiniMap#getMinimapDrawWidget 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#closeWorldMap(): boolean -> net.runelite.api.widgets.Widget#getBounds(): Rectangle -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#collectMoaChildren(Widget): List -> net.runelite.api.widgets.Widget#getDynamicChildren(): Widget[] -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#collectMoaChildren(Widget): List -> net.runelite.api.widgets.Widget#getNestedChildren(): Widget[] -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#collectMoaChildren(Widget): List -> net.runelite.api.widgets.Widget#getStaticChildren(): Widget[] net.runelite.client.plugins.microbot.util.walker.Rs2Walker#distanceToRegion(int, int): int -> net.runelite.api.Client#getTopLevelWorldView(): WorldView net.runelite.client.plugins.microbot.util.walker.Rs2Walker#distanceToRegion(int, int): int -> net.runelite.api.WorldView#getPlane(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#getPointWithWallDistance(WorldPoint): WorldPoint -> net.runelite.api.Client#getTopLevelWorldView(): WorldView @@ -734,11 +731,11 @@ 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.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#lambda$handleCharterShip$115(Widget): boolean -> net.runelite.api.widgets.Widget#getActions(): String[] +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleCharterShip$114(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$121(Transport, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleMinigameTeleport$94(Widget, Object[]): boolean -> net.runelite.api.widgets.Widget#getOnOpListener(): Object[] -net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleMinigameTeleport$96(String): boolean -> net.runelite.api.widgets.Widget#getText(): String +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleFairyRing$120(Transport, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleMinigameTeleport$93(Widget, Object[]): boolean -> net.runelite.api.widgets.Widget#getOnOpListener(): Object[] +net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleMinigameTeleport$95(String): boolean -> net.runelite.api.widgets.Widget#getText(): String net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$66(int, TileObject): boolean -> net.runelite.api.TileObject#getId(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$72(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getId(): int net.runelite.client.plugins.microbot.util.walker.Rs2Walker#lambda$handleObjectExceptions$72(WorldPoint, TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint