From e8456644ff1ba413af3a784392632a49fd9a8c26 Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Tue, 12 May 2026 10:38:53 -0400 Subject: [PATCH 1/2] fix(microbot): hopToWorld guard rejects valid hops after interactions Player.isInteracting() stays true for many seconds after benign actions (closing a shop, talking to an NPC, getting aggro'd) because the engine keeps the interaction pointer set until the player moves or combat resets. OSRS does not block hops in that state, but this guard did, causing plugins to spin on "Local player is interacting, cannot hop worlds" forever after Rs2Shop.closeShop() and similar flows. Replace the isInteracting() check with the conditions that actually block a hop: active combat (Rs2Combat.inCombat) and blocking widgets (Rs2Bank/Rs2Shop/Rs2Dialogue). Wilderness PvP timer remains server-enforced via client.hopToWorld(). Closes chsami/Microbot#1772 --- .../runelite/client/plugins/microbot/Microbot.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java index 4404f8a677..b4effb0d9f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java @@ -36,7 +36,11 @@ import net.runelite.client.plugins.loottracker.LootTrackerRecord; import net.runelite.client.plugins.microbot.configs.SpecialAttackConfigs; import net.runelite.client.plugins.microbot.pouch.PouchScript; +import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; +import net.runelite.client.plugins.microbot.util.combat.Rs2Combat; +import net.runelite.client.plugins.microbot.util.dialogues.Rs2Dialogue; import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; +import net.runelite.client.plugins.microbot.util.shop.Rs2Shop; import net.runelite.client.plugins.microbot.util.item.Rs2ItemManager; import net.runelite.client.plugins.microbot.util.math.Rs2Random; import net.runelite.client.plugins.microbot.util.menu.NewMenuEntry; @@ -363,8 +367,12 @@ public static boolean hopToWorld(int worldNumber) { return false; } boolean isHopping = Microbot.getClientThread().runOnClientThreadOptional(() -> { - if (Microbot.getClient().getLocalPlayer() != null && Microbot.getClient().getLocalPlayer().isInteracting()) { - log.error("Local player is interacting, cannot hop worlds"); + if (Rs2Combat.inCombat()) { + log.error("Player is in combat, cannot hop worlds"); + return false; + } + if (Rs2Bank.isOpen() || Rs2Shop.isOpen() || Rs2Dialogue.isInDialogue()) { + log.error("Blocking widget open (bank/shop/dialogue), cannot hop worlds"); return false; } if (quickHopTargetWorld != null || Microbot.getClient().getGameState() != GameState.LOGGED_IN) { From a37dcefac82d7c9a3147182abfe04827bbd3ee8c Mon Sep 17 00:00:00 2001 From: runsonmypc Date: Tue, 12 May 2026 14:23:23 -0400 Subject: [PATCH 2/2] fix(microbot): wait for hop off client thread to stop spam Global.sleep / sleepUntil early-return when invoked on the client thread, so the post-hop wait inside the runOnClientThreadOptional lambda was a no-op: the success check fired microseconds after issuing the request, before the server had processed it. The function returned false on every call, callers retried via sleepUntil polling, and Microbot.java logged "Failed to hop to world N" 3-4 times per actually-successful hop. Move the wait off the client thread. The lambda now only validates and issues the hop request. The script thread then sleeps and sleepUntils for HOPPING / world-change / confirm widget, handles the confirm-dialog fallback, and waits for the hop to fully land (GameState.LOGGED_IN on the target world) before returning. Success is reported as getWorld() == worldNumber, the definitive signal, so callers can trust the return value. Also: the "already on target world" guard now returns true (idempotent success) instead of false, killing the retry loop that triggered the spam on the second and subsequent calls. Closes chsami/Microbot#1550 --- .../client/plugins/microbot/Microbot.java | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java index b4effb0d9f..1270bfa3c3 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java @@ -366,7 +366,7 @@ public static boolean hopToWorld(int worldNumber) { log.error("Can't hop world, already trying to hop"); return false; } - boolean isHopping = Microbot.getClientThread().runOnClientThreadOptional(() -> { + boolean hopIssued = Microbot.getClientThread().runOnClientThreadOptional(() -> { if (Rs2Combat.inCombat()) { log.error("Player is in combat, cannot hop worlds"); return false; @@ -380,7 +380,7 @@ public static boolean hopToWorld(int worldNumber) { return false; } if (Microbot.getClient().getWorld() == worldNumber) { - return false; + return true; } World newWorld = Microbot.getWorldService().getWorlds().findWorld(worldNumber); if (newWorld == null) { @@ -402,29 +402,46 @@ public static boolean hopToWorld(int worldNumber) { Microbot.getClient().openWorldHopper(); Microbot.getClient().hopToWorld(rsWorld); quickHopTargetWorld = null; - sleep(600); - sleepUntil(() -> Microbot.isHopping() || Rs2Widget.getWidget(193, 0) != null, 2000); - return Microbot.isHopping(); + return true; }).orElse(false); - if (!isHopping) { + if (!hopIssued) { + log.error("Failed to hop to world {}", worldNumber); + return false; + } + // Wait off the client thread so sleeps actually block. The lambda above runs on + // the client thread, where Global.sleep / sleepUntil early-return — so any post-hop + // wait inside it is a no-op and the success check fires before the server has + // even processed the request. That's the source of "Failed to hop" spam. + if (Microbot.getClient().getWorld() != worldNumber) { + sleep(600); + sleepUntil(() -> Microbot.isHopping() + || Microbot.getClient().getWorld() == worldNumber + || Rs2Widget.getWidget(193, 0) != null, 5000); + } + boolean hopping = Microbot.isHopping() || Microbot.getClient().getWorld() == worldNumber; + if (!hopping) { Widget confirmRoot = Rs2Widget.getWidget(193, 0); if (confirmRoot != null) { List children = Arrays.stream(confirmRoot.getDynamicChildren()).collect(Collectors.toList()); Widget switchWorldWidget = sleepUntilNotNull(() -> Rs2Widget.findWidget("Switch world", children, true), 2000); - if (switchWorldWidget != null) { - boolean clicked = Rs2Widget.clickWidget(switchWorldWidget); - if (clicked) { - sleepUntil(Microbot::isHopping, 4000); - return Microbot.isHopping(); - } + if (switchWorldWidget != null && Rs2Widget.clickWidget(switchWorldWidget)) { + sleepUntil(() -> Microbot.isHopping() + || Microbot.getClient().getWorld() == worldNumber, 4000); + hopping = Microbot.isHopping() || Microbot.getClient().getWorld() == worldNumber; } } } - if (!isHopping) { + if (hopping) { + // Block until the hop fully lands so callers don't race against HOPPING/LOGIN_SCREEN. + sleepUntil(() -> Microbot.getClient().getWorld() == worldNumber + && Microbot.getClient().getGameState() == GameState.LOGGED_IN, 15000); + } + boolean success = Microbot.getClient().getWorld() == worldNumber; + if (!success) { log.error("Failed to hop to world {}", worldNumber); } - return false; + return success; } public static void showMessage(String message) {