From 191bf31eafefb5dcabbc65ecf1bf949f9ecc0df9 Mon Sep 17 00:00:00 2001 From: chsami Date: Tue, 13 Oct 2026 13:36:07 +0200 Subject: [PATCH 1/7] fix(hub): act on hopToWorld result instead of assuming the hop happened (#598) Eleven scripts ignored Microbot.hopToWorld's boolean and carried on as if the hop happened: counters and occupancy flags were reset, states advanced to the next world's logic, a refused hop blacklisted the target world or stopped the script, and two helpers re-called hopToWorld in a 15s sleepUntil loop. Each now acts only on success and retries the hop on a later loop iteration, leaving pacing to the client's HopBudget. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../AmmoniteCrabs/AmmoniteCrabPlugin.java | 2 +- .../AmmoniteCrabs/AmmoniteCrabScript.java | 5 +- .../banksshopper/BanksShopperPlugin.java | 2 +- .../banksshopper/BanksShopperScript.java | 13 +- .../bluedragons/BlueDragonsPlugin.java | 2 +- .../bluedragons/BlueDragonsScript.java | 23 ++- .../microbot/giantmole/GiantMolePlugin.java | 2 +- .../microbot/giantmole/GiantMoleScript.java | 6 +- .../hsblackjack/HSBlackJackPlugin.java | 2 +- .../hsblackjack/HSBlackJackScript.java | 26 +++- .../microhunter/AutoHunterPlugin.java | 2 +- .../microhunter/scripts/AutoChinScript.java | 24 ++- .../mke_wintertodt/MKE_WintertodtPlugin.java | 2 +- .../mke_wintertodt/MKE_WintertodtScript.java | 143 ++---------------- .../microbot/revkiller/revKillerPlugin.java | 2 +- .../microbot/revkiller/revKillerScript.java | 15 +- .../microbot/sandcrabs/SandCrabPlugin.java | 2 +- .../microbot/sandcrabs/SandCrabScript.java | 17 ++- .../plugins/microbot/slayer/SlayerPlugin.java | 2 +- .../plugins/microbot/slayer/SlayerScript.java | 5 +- .../microbot/thieving/ThievingPlugin.java | 2 +- .../microbot/thieving/ThievingScript.java | 47 +----- .../BanksShopperHopResultTest.java | 51 +++++++ .../giantmole/GiantMoleHopResultTest.java | 62 ++++++++ .../hsblackjack/HSBlackJackHopResultTest.java | 51 +++++++ .../scripts/AutoChinHopResultTest.java | 95 ++++++++++++ .../WintertodtHopResultTest.java | 72 +++++++++ 27 files changed, 459 insertions(+), 218 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperHopResultTest.java create mode 100644 src/test/java/net/runelite/client/plugins/microbot/giantmole/GiantMoleHopResultTest.java create mode 100644 src/test/java/net/runelite/client/plugins/microbot/hsblackjack/HSBlackJackHopResultTest.java create mode 100644 src/test/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinHopResultTest.java create mode 100644 src/test/java/net/runelite/client/plugins/microbot/mke_wintertodt/WintertodtHopResultTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabPlugin.java index b75908576f..30fde56e16 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabPlugin.java @@ -28,7 +28,7 @@ @Slf4j public class AmmoniteCrabPlugin extends Plugin { - public final static String version = "1.1.4"; + public final static String version = "1.1.5"; @Inject public AmmoniteCrabScript ammoniteCrabScript; @Inject diff --git a/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabScript.java b/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabScript.java index fe02d9dcf8..4d03afefbb 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabScript.java @@ -101,7 +101,10 @@ public boolean run(AmmoniteCrabConfig config) { break; case HOP_WORLD: int world = Login.getRandomWorld(true, null); - Microbot.hopToWorld(world); + if (!Microbot.hopToWorld(world)) { + Microbot.log("World hop to " + world + " refused, retrying next loop"); + break; + } timesHopped++; boolean result = sleepUntil(() -> Rs2Widget.findWidget("Switch World") != null); Rs2Keyboard.keyPress(KeyEvent.VK_SPACE); diff --git a/src/main/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperPlugin.java index c4c679e13b..c2a4100c4c 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperPlugin.java @@ -36,7 +36,7 @@ ) @Slf4j public class BanksShopperPlugin extends Plugin { - public final static String version = "1.4.2"; + public final static String version = "1.4.3"; @Inject private BanksShopperConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperScript.java b/src/main/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperScript.java index 2670596c54..1ee85224b7 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperScript.java @@ -158,8 +158,9 @@ public boolean run(BanksShopperConfig config) { state = ShopperState.SHOPPING; break; case HOPPING: - hopWorld(); - state = ShopperState.SHOPPING; + if (hopWorld()) { + state = ShopperState.SHOPPING; + } break; } } catch (Exception ex) { @@ -298,13 +299,17 @@ private boolean ensureShopOpen() { /** * Hops to a new world */ - private void hopWorld() { + private boolean hopWorld() { System.out.println("Hopping worlds..."); Rs2Random.waitEx(3200, 800); // this sleep is required to avoid the message: please finish what you're doing before using the world switcher. int world = plugin.isUseNextWorld() ? Login.getNextWorld(Rs2Player.isMember()) : Login.getRandomWorld(Rs2Player.isMember()); - sleepUntil(() -> Microbot.hopToWorld(world), 15000); + if (!Microbot.hopToWorld(world)) { + System.out.println("World hop to " + world + " refused, retrying next loop"); + return false; + } System.out.println("Successfully hopped to world: " + world); + return true; } diff --git a/src/main/java/net/runelite/client/plugins/microbot/bluedragons/BlueDragonsPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/bluedragons/BlueDragonsPlugin.java index 52c38073ac..dafed31b83 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/bluedragons/BlueDragonsPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/bluedragons/BlueDragonsPlugin.java @@ -27,7 +27,7 @@ ) public class BlueDragonsPlugin extends Plugin { - public static final String version = "1.2.0"; + public static final String version = "1.2.1"; static final String CONFIG = "bluedragons"; @Inject diff --git a/src/main/java/net/runelite/client/plugins/microbot/bluedragons/BlueDragonsScript.java b/src/main/java/net/runelite/client/plugins/microbot/bluedragons/BlueDragonsScript.java index d8d09082fb..e06d59f909 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/bluedragons/BlueDragonsScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/bluedragons/BlueDragonsScript.java @@ -187,7 +187,9 @@ private void handleBanking() { private void handleTravelToDragons() { if (isPlayerAtSafeSpot()) { - currentState = BlueDragonState.COMBAT; + if (hopIfPlayerAtSafeSpot()) { + currentState = BlueDragonState.COMBAT; + } return; } @@ -199,8 +201,7 @@ private void handleTravelToDragons() { sleepUntil(this::isPlayerAtSafeSpot, 10000); } - if (isPlayerAtSafeSpot()) { - hopIfPlayerAtSafeSpot(); + if (isPlayerAtSafeSpot() && hopIfPlayerAtSafeSpot()) { currentState = BlueDragonState.COMBAT; } } @@ -343,17 +344,23 @@ private boolean isPlayerAtSafeSpot() { Microbot.getClient().getLocalPlayer().getWorldLocation())); } - private void hopIfPlayerAtSafeSpot() { + private boolean hopIfPlayerAtSafeSpot() { List players = Rs2Player.getPlayers(it -> it != null).collect(Collectors.toList()); boolean occupied = players.stream().anyMatch(p -> p != null && !p.equals(Microbot.getClient().getLocalPlayer()) && p.getWorldLocation().distanceTo(SAFE_SPOT) <= 1); - if (occupied) { - Microbot.log("Player at safe spot. Hopping worlds."); - Microbot.hopToWorld(findRandomWorld()); - sleep(5000); + if (!occupied) { + return true; } + Microbot.log("Player at safe spot. Hopping worlds."); + int world = findRandomWorld(); + if (!Microbot.hopToWorld(world)) { + Microbot.log("World hop to " + world + " refused, retrying next loop"); + return false; + } + sleep(5000); + return true; } private int findRandomWorld() { diff --git a/src/main/java/net/runelite/client/plugins/microbot/giantmole/GiantMolePlugin.java b/src/main/java/net/runelite/client/plugins/microbot/giantmole/GiantMolePlugin.java index 1e9f60ba60..e64d24583a 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/giantmole/GiantMolePlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/giantmole/GiantMolePlugin.java @@ -32,7 +32,7 @@ ) @Slf4j public class GiantMolePlugin extends Plugin { - public static final String version = "0.0.5"; + public static final String version = "0.0.6"; @Inject private GiantMoleConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/giantmole/GiantMoleScript.java b/src/main/java/net/runelite/client/plugins/microbot/giantmole/GiantMoleScript.java index c10bb2ffeb..01308061e4 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/giantmole/GiantMoleScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/giantmole/GiantMoleScript.java @@ -230,7 +230,11 @@ public static boolean isInMoleTunnel() public void hopWorlds() { int randomWorld = Login.getRandomWorld(isMember()); - Microbot.hopToWorld(randomWorld); + if (!Microbot.hopToWorld(randomWorld)) + { + Microbot.log("World hop to " + randomWorld + " refused, retrying next loop"); + return; + } checkedIfWorldOccupied = false; state = GiantMoleState.IDLE; } diff --git a/src/main/java/net/runelite/client/plugins/microbot/hsblackjack/HSBlackJackPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/hsblackjack/HSBlackJackPlugin.java index 056776637a..75e78196a5 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/hsblackjack/HSBlackJackPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/hsblackjack/HSBlackJackPlugin.java @@ -27,7 +27,7 @@ @Slf4j public class HSBlackJackPlugin extends Plugin { - static final String version = "1.0.1"; + static final String version = "1.0.2"; @Inject private HSBlackJackConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/hsblackjack/HSBlackJackScript.java b/src/main/java/net/runelite/client/plugins/microbot/hsblackjack/HSBlackJackScript.java index b42f62b8e2..0208df8e60 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/hsblackjack/HSBlackJackScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/hsblackjack/HSBlackJackScript.java @@ -685,10 +685,14 @@ private Rs2NpcModel findReachableTarget(String targetName) { * Hops to a random world - used when the tent stays blocked too long, the * bar's stock is too low, or a lure's NPC never makes it inside in time. */ - private void hopWorld() { + private boolean hopWorld() { sleep(600, 1000); int world = Login.getRandomWorld(Rs2Player.isMember()); - sleepUntil(() -> Microbot.hopToWorld(world), 15000); + if (Microbot.hopToWorld(world)) { + return true; + } + setState("World hop to " + world + " refused - retrying next loop"); + return false; } /** @@ -768,8 +772,9 @@ private void handleCheckTentOccupancy() { if (allInTent.size() != 1 || targetsInTent.size() != 1) { setState("Tent occupied by " + allInTent.size() + " NPC(s), not a clean single target - hopping world"); - hopWorld(); - phase = Phase.TRAVEL_TO_START; + if (hopWorld()) { + phase = Phase.TRAVEL_TO_START; + } return; } @@ -1013,7 +1018,9 @@ private void handleCloseCurtain() { if (waitedFor >= NPC_ENTER_TENT_TIMEOUT_MS) { setState("NPC never entered tent (" + (waitedFor / 1000) + "s) - hopping world, restarting"); - hopWorld(); + if (!hopWorld()) { + return; + } npcEnterTentWaitSince = -1; luredNpc = null; needsLureInsideTent = false; @@ -1062,7 +1069,9 @@ private void handleKnockOut() { if (blockedFor >= ROOM_BLOCKED_HOP_THRESHOLD_MS) { setState("Tent occupied too long (" + (blockedFor / 1000) + "s) - hopping world"); - hopWorld(); + if (!hopWorld()) { + return; + } roomBlockedSince = -1; luredNpc = null; phase = Phase.TRAVEL_TO_START; @@ -1219,8 +1228,9 @@ private void handleRestockFlow() { long stuckFor = System.currentTimeMillis() - restockCurtainStuckSince; if (stuckFor >= CURTAIN_STUCK_TIMEOUT_MS) { setState("Restock: stuck opening curtain for " + (stuckFor / 1000) + "s - hopping world"); - hopWorld(); - restockCurtainStuckSince = -1; + if (hopWorld()) { + restockCurtainStuckSince = -1; + } return; } diff --git a/src/main/java/net/runelite/client/plugins/microbot/microhunter/AutoHunterPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/microhunter/AutoHunterPlugin.java index 5b39219b00..4bb8b85152 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/microhunter/AutoHunterPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/microhunter/AutoHunterPlugin.java @@ -31,7 +31,7 @@ @Slf4j @PluginDependency(HunterPlugin.class) public class AutoHunterPlugin extends Plugin { - public static final String version = "1.5.0"; + public static final String version = "1.5.1"; @Inject private AutoHunterConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinScript.java b/src/main/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinScript.java index 5f88d2a016..e32b513039 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinScript.java @@ -84,6 +84,7 @@ private enum Action { private static final long OCCUPANCY_SCAN_MS = 7_000; private static final long PLAYER_PERSISTENCE_MS = 3_500; private static final long WORLD_HOP_TIMEOUT_MS = 15_000; + private static final long REFUSED_HOP_RETRY_MS = 3_000; private static final long REJECTED_WORLD_COOLDOWN_MS = 600_000; private static final long BREAK_RECOVERY_CLEAR_MS = 1_500; private static final int MAX_WORLD_HOP_ATTEMPTS = 5; @@ -140,6 +141,7 @@ private enum Action { private int hopTargetWorld = -1; private int hopSourceWorld = -1; private long hopStartedAt; + private boolean hopRefused; private boolean breakRecoveryRequested; private boolean breakRecoveryComplete; private boolean breakRecoveryLockHeld; @@ -196,6 +198,7 @@ private void resetSession() { hopTargetWorld = -1; hopSourceWorld = -1; hopStartedAt = 0; + hopRefused = false; breakRecoveryRequested = false; breakRecoveryComplete = false; breakRecoveryLockHeld = false; @@ -307,6 +310,7 @@ private void beginWorldArrival(AutoHunterConfig config, int world) { hopTargetWorld = -1; hopSourceWorld = -1; hopStartedAt = 0; + hopRefused = false; nearbyPlayerSeenSince = 0; if (!config.avoidOccupiedWorlds()) { worldHopAttempts = 0; @@ -426,14 +430,22 @@ private void requestAustralianWorldHop(String reason) { stopSafely("No eligible Australian world is currently available"); return; } - worldHopAttempts++; hopSourceWorld = Microbot.getClient().getWorld(); hopTargetWorld = target; - hopStartedAt = System.currentTimeMillis(); transition(State.HOPPING_WORLD, "Hopping to Australian world " + target + " (" + reason + ")"); Microbot.log("AutoHunter occupancy: hopping " + hopSourceWorld + " -> " + target - + " attempt=" + worldHopAttempts); - Microbot.hopToWorld(target); + + " attempt=" + (worldHopAttempts + 1)); + issueWorldHop(); + } + + private void issueWorldHop() { + hopStartedAt = System.currentTimeMillis(); + hopRefused = !Microbot.hopToWorld(hopTargetWorld); + if (hopRefused) { + Microbot.log("AutoHunter occupancy: hop to world " + hopTargetWorld + " refused; retrying"); + return; + } + worldHopAttempts++; } private Integer selectAustralianWorld() { @@ -460,6 +472,10 @@ private Integer selectAustralianWorld() { private void monitorWorldHop() { int currentWorld = Microbot.getClient().getWorld(); if (hopSourceWorld > 0 && currentWorld != hopSourceWorld) return; + if (hopRefused) { + if (System.currentTimeMillis() - hopStartedAt >= REFUSED_HOP_RETRY_MS) issueWorldHop(); + return; + } if (System.currentTimeMillis() - hopStartedAt < WORLD_HOP_TIMEOUT_MS) return; if (hopTargetWorld > 0) { rejectedWorlds.put(hopTargetWorld, System.currentTimeMillis() + REJECTED_WORLD_COOLDOWN_MS); diff --git a/src/main/java/net/runelite/client/plugins/microbot/mke_wintertodt/MKE_WintertodtPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/mke_wintertodt/MKE_WintertodtPlugin.java index e54d0fca9b..8b21d996ef 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/mke_wintertodt/MKE_WintertodtPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/mke_wintertodt/MKE_WintertodtPlugin.java @@ -42,7 +42,7 @@ ) @Slf4j public class MKE_WintertodtPlugin extends Plugin { - static final String version = "2.2.3"; + static final String version = "2.2.4"; // Core plugin components @Inject diff --git a/src/main/java/net/runelite/client/plugins/microbot/mke_wintertodt/MKE_WintertodtScript.java b/src/main/java/net/runelite/client/plugins/microbot/mke_wintertodt/MKE_WintertodtScript.java index 11abd36f88..e132b42850 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/mke_wintertodt/MKE_WintertodtScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/mke_wintertodt/MKE_WintertodtScript.java @@ -126,6 +126,8 @@ public class MKE_WintertodtScript extends Script { private boolean wasOnBreak = false; private boolean wasOnBreakHandlerBreak = false; private boolean worldChecked = false; // Track if we've already checked/hopped to Wintertodt world + private static final long REFUSED_HOP_RETRY_MS = 5_000; + private long lastRefusedHopAt = 0; // Wintertodt round timer tracking private long roundEndTime = -1; @@ -3902,9 +3904,9 @@ private void logCurrentGearSetup(WintertodtGearManager gearManager) { /** * Ensures the player is on a Wintertodt world, hopping if necessary. * Only hops when in a safe location (not in combat, not interacting, etc.). - * Stops script and logs out on failure. + * A refused hop is retried on a later call. * - * @return true if on Wintertodt world or successfully hopped, false if failed (script should stop) + * @return false only if the world check itself failed (script should stop) */ private boolean ensureWintertodtWorldSafely() { try { @@ -3932,16 +3934,17 @@ private boolean ensureWintertodtWorldSafely() { return true; // Don't fail, just retry later } + if (System.currentTimeMillis() - lastRefusedHopAt < REFUSED_HOP_RETRY_MS) { + return true; + } + // Not on a Wintertodt world, need to hop Microbot.log("Not on a Wintertodt world (current: " + currentWorld + ") - hopping to a Wintertodt world..."); - boolean hopSuccess = hopToWintertodtWorldSafely(wintertodtWorlds); - - if (!hopSuccess) { - // Hop failed - stop script and logout - Microbot.log("CRITICAL: Failed to hop to Wintertodt world - stopping script and logging out"); - stopScriptAndLogout(); - return false; + if (!hopToWintertodtWorldSafely(wintertodtWorlds)) { + lastRefusedHopAt = System.currentTimeMillis(); + Microbot.log("World hop to a Wintertodt world refused - will try again later"); + return true; } worldChecked = true; // Mark as checked after successful hop @@ -3999,30 +4002,6 @@ private boolean isSafeToHopWorlds() { } } - /** - * Stops the script and logs out the player. - */ - private void stopScriptAndLogout() { - try { - Microbot.log("Stopping script due to critical error..."); - - // Stop the script - this.shutdown(); - - // Wait a moment for shutdown to process - sleepGaussian(3000, 500); - - // Logout the player - if (Microbot.isLoggedIn()) { - Microbot.log("Logging out player..."); - Rs2Player.logout(); - } - - } catch (Exception e) { - Microbot.log("Error during script stop and logout: " + e.getMessage()); - } - } - /** * Hops to a random Wintertodt world from the available options. * @@ -4031,105 +4010,13 @@ private void stopScriptAndLogout() { */ private boolean hopToWintertodtWorldSafely(int[] wintertodtWorlds) { try { - // Select a random Wintertodt world int targetWorld = wintertodtWorlds[Rs2Random.between(0, wintertodtWorlds.length - 1)]; - int originalWorld = Rs2Player.getWorld(); - Microbot.log("Attempting to hop to Wintertodt world " + targetWorld + "..."); - - // Perform the world hop (NOTE: Microbot.hopToWorld() has a bug - returns false even on success) - Microbot.hopToWorld(targetWorld); - - sleep(1000); - - // If the world hop failed, try again (This happens basically every time) - if (Rs2Player.getWorld() != targetWorld) { - Microbot.hopToWorld(targetWorld); - } - - // Don't rely on return value - wait and check if world actually changed - Microbot.log("World hop initiated, waiting for completion..."); - - // Wait for world hop to complete (up to 15 seconds) - long hopStartTime = System.currentTimeMillis(); - boolean hopCompleted = sleepUntilTrue(() -> { - int currentWorld = Rs2Player.getWorld(); - // Success if we reach target world or any other Wintertodt world - if (currentWorld == targetWorld && Microbot.isLoggedIn()) { - return true; - } - // Also accept if we ended up on any Wintertodt world - for (int wintertodtWorld : wintertodtWorlds) { - if (currentWorld == wintertodtWorld && Microbot.isLoggedIn()) { - return true; - } - } + if (!Microbot.hopToWorld(targetWorld)) { return false; - }, 100, 15000); - - int finalWorld = Rs2Player.getWorld(); - - if (hopCompleted) { - Microbot.log("Successfully hopped to Wintertodt world " + finalWorld); - return true; - } else if (finalWorld != originalWorld) { - // We hopped somewhere, but not to a Wintertodt world - this is partial success - Microbot.log("Hopped to world " + finalWorld + " but not a Wintertodt world - trying to hop again"); - - // Try once more to a Wintertodt world - int retryWorld = wintertodtWorlds[Rs2Random.between(0, wintertodtWorlds.length - 1)]; - Microbot.log("Attempting second hop to Wintertodt world " + retryWorld + "..."); - Microbot.hopToWorld(retryWorld); - - boolean retryCompleted = sleepUntilTrue(() -> { - int currentWorld = Rs2Player.getWorld(); - for (int wintertodtWorld : wintertodtWorlds) { - if (currentWorld == wintertodtWorld && Microbot.isLoggedIn()) { - return true; - } - } - return false; - }, 100, 15000); - - if (retryCompleted) { - Microbot.log("Successfully hopped to Wintertodt world " + Rs2Player.getWorld() + " on second attempt"); - return true; - } else { - Microbot.log("Second hop attempt failed - still on world " + Rs2Player.getWorld()); - } - } else { - Microbot.log("World hop failed - still on original world " + originalWorld + " after 15 seconds"); } - - // Final attempt with a different world if we have multiple options - if (wintertodtWorlds.length > 1) { - int finalAttemptWorld; - do { - finalAttemptWorld = wintertodtWorlds[Rs2Random.between(0, wintertodtWorlds.length - 1)]; - } while (finalAttemptWorld == targetWorld && wintertodtWorlds.length > 1); - - Microbot.log("Final attempt - trying Wintertodt world " + finalAttemptWorld + "..."); - Microbot.hopToWorld(finalAttemptWorld); - - boolean finalCompleted = sleepUntilTrue(() -> { - int currentWorld = Rs2Player.getWorld(); - for (int wintertodtWorld : wintertodtWorlds) { - if (currentWorld == wintertodtWorld && Microbot.isLoggedIn()) { - return true; - } - } - return false; - }, 100, 15000); - - if (finalCompleted) { - Microbot.log("Successfully hopped to Wintertodt world " + Rs2Player.getWorld() + " on final attempt"); - return true; - } - } - - Microbot.log("CRITICAL: Failed to hop to any Wintertodt world after multiple attempts"); - return false; - + Microbot.log("Successfully hopped to Wintertodt world " + Rs2Player.getWorld()); + return true; } catch (Exception e) { Microbot.log("Error during world hop: " + e.getMessage()); e.printStackTrace(); diff --git a/src/main/java/net/runelite/client/plugins/microbot/revkiller/revKillerPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/revkiller/revKillerPlugin.java index 9da25bdf09..cd8075e676 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/revkiller/revKillerPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/revkiller/revKillerPlugin.java @@ -32,7 +32,7 @@ ) @Slf4j public class revKillerPlugin extends Plugin { - public static final String version = "2.0.9"; + public static final String version = "2.0.10"; @Inject private net.runelite.client.plugins.microbot.revkiller.revKillerConfig config; @Provides diff --git a/src/main/java/net/runelite/client/plugins/microbot/revkiller/revKillerScript.java b/src/main/java/net/runelite/client/plugins/microbot/revkiller/revKillerScript.java index f758823166..1c2957857d 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/revkiller/revKillerScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/revkiller/revKillerScript.java @@ -548,7 +548,9 @@ public void hopWorldsBasedOnTimer(){ return; } } - hopToNewWorld(); + if(!hopToNewWorld()){ + return; + } startTime = currentTime; howLongUntilHop = config.hopInMinutes(); int mixUp = Rs2Random.between(5,10); @@ -892,21 +894,21 @@ public void reJfount(){ } } - public void hopToNewWorld(){ + public boolean hopToNewWorld(){ // Thank you george! World currentWorld = Microbot.getWorldService().getWorlds().findWorld(Microbot.getClient().getWorld()); if(currentWorld == null){ Microbot.log("Couldn't find our world"); - return; + return false; } int newRandomWorld = Login.getRandomWorld(true, currentWorld.getRegion()); if(newRandomWorld == 0){ Microbot.log("Couldn't find a new random world"); - return; + return false; } Microbot.log("We're going to world " + newRandomWorld); @@ -928,7 +930,12 @@ public void hopToNewWorld(){ sleep(500,700); attempts++; } + if(Rs2Player.getWorld() != newRandomWorld){ + Microbot.log("World hop to " + newRandomWorld + " refused, retrying later"); + return false; + } hoppedWorld = true; + return true; } public void equipArrows(){ diff --git a/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabPlugin.java index e1e40ce765..92a215a5db 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabPlugin.java @@ -26,7 +26,7 @@ ) @Slf4j public class SandCrabPlugin extends Plugin { - public final static String version = "1.5.4"; + public final static String version = "1.5.5"; @Inject private SandCrabConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabScript.java b/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabScript.java index 5920491f2e..b2c6d72658 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabScript.java @@ -162,14 +162,15 @@ public boolean run(SandCrabConfig config, SandCrabPlugin plugin) { break; case HOP_WORLD: int world = Login.getRandomWorld(true, null); - boolean isHopped = Microbot.hopToWorld(world); - if (isHopped) { - boolean result = sleepUntil(() -> Rs2Widget.findWidget("Switch World") != null); - if (result) { - Rs2Keyboard.keyPress(KeyEvent.VK_SPACE); - sleepUntil(() -> Microbot.getClient().getGameState() == GameState.HOPPING); - sleepUntil(() -> Microbot.getClient().getGameState() == GameState.LOGGED_IN); - } + if (!Microbot.hopToWorld(world)) { + Microbot.log("World hop to " + world + " refused, retrying next loop"); + break; + } + boolean result = sleepUntil(() -> Rs2Widget.findWidget("Switch World") != null); + if (result) { + Rs2Keyboard.keyPress(KeyEvent.VK_SPACE); + sleepUntil(() -> Microbot.getClient().getGameState() == GameState.HOPPING); + sleepUntil(() -> Microbot.getClient().getGameState() == GameState.LOGGED_IN); } if (timesHopped > 10) { diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerPlugin.java index 45196fa27b..8d1e6ee6d0 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerPlugin.java @@ -42,7 +42,7 @@ @Slf4j public class SlayerPlugin extends Plugin { - static final String version = "1.1.1"; + static final String version = "1.1.2"; @Inject private SlayerConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerScript.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerScript.java index 51fb7654a6..94e4b8a44f 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerScript.java @@ -2947,7 +2947,10 @@ private void hopToNewWorld() { } log.info("Hopping to world {}", world); - Microbot.hopToWorld(world); + if (!Microbot.hopToWorld(world)) { + log.warn("World hop to {} refused, retrying on a later loop", world); + return; + } // Wait for switch world confirmation dialog sleepUntil(() -> Rs2Widget.findWidget("Switch World") != null, 5000); diff --git a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java index 229f40dd04..53dabb06af 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java @@ -34,7 +34,7 @@ ) @Slf4j public class ThievingPlugin extends Plugin { - public static final String version = "2.1.2"; + public static final String version = "2.1.3"; @Inject @Getter diff --git a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingScript.java b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingScript.java index fe5fc5e4d8..2c59b1ba2a 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingScript.java @@ -9,7 +9,6 @@ import net.runelite.api.Tile; import net.runelite.api.TileObject; import net.runelite.api.coords.WorldPoint; -import net.runelite.api.GameState; import net.runelite.api.gameval.ItemID; import net.runelite.api.Skill; import net.runelite.api.events.ChatMessage; @@ -249,15 +248,6 @@ private boolean isBeingAttackByNpc() { return getAttackingNpcs(npcs -> npcs.findAny().isPresent(), false); } - private Rs2NpcModel getAttackingNpc() { - final WorldPoint myLoc = Rs2Player.getWorldLocation(); - if (myLoc == null) return null; - return getAttackingNpcs(npcs -> - npcs.min(Comparator.comparingInt(npc -> myLoc.distanceTo(npc.getWorldLocation()))) - .orElse(null), null - ); - } - private int getMostExpensiveGroundItemId() { final int minPrice = config.keepItemsAboveValue(); // takes long of client thread if there are a lot of dropped items @@ -1051,39 +1041,16 @@ private void hopWorld() { return; } lastHopAt = now; - DOOR_TIMER.unset(); - final int maxAttempts = 5; log.debug("Hopping world, please wait"); - for (int attempt = 0; attempt < maxAttempts; attempt++) { - final int currentWorld = Microbot.getClient().getWorld(); - Microbot.hopToWorld(Login.getRandomWorld(true, null)); - - BooleanSupplier attackedInterrupt = () -> { - final Rs2NpcModel attacking = getAttackingNpc(); - if (attacking == null) return false; - final WorldPoint me = Rs2Player.getWorldLocation(); - final WorldPoint npc = attacking.getWorldLocation(); - if (me != null && npc != null && me.distanceTo(npc) <= 2) { - log.warn("Getting attacked while hopping"); - return true; - } - return false; - }; - - sleepUntilWithInterrupt(() -> Microbot.getClient().getGameState() == GameState.HOPPING, attackedInterrupt, 15_000); - sleepUntilWithInterrupt(() -> Microbot.getClient().getGameState() == GameState.LOGGED_IN, attackedInterrupt, 15_000); - boolean changed = sleepUntilWithInterrupt(() -> Microbot.getClient().getWorld() != currentWorld, attackedInterrupt, 15_000); - - if (changed) { - log.debug("Successful hop world"); - thievingNpc = null; // force fresh lookup after hop - return; - } - sleep(250, 350); - log.warn("Hop attempt {}/{} failed, retrying...", attempt + 1, maxAttempts); + final int world = Login.getRandomWorld(true, null); + if (!Microbot.hopToWorld(world)) { + log.warn("World hop to {} refused, retrying later", world); + return; } - log.error("Failed to hop world"); + DOOR_TIMER.unset(); + log.debug("Successful hop world"); + thievingNpc = null; // force fresh lookup after hop } private void drinkAncientBrew() { diff --git a/src/test/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperHopResultTest.java b/src/test/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperHopResultTest.java new file mode 100644 index 0000000000..d5a50d91cf --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperHopResultTest.java @@ -0,0 +1,51 @@ +package net.runelite.client.plugins.microbot.banksshopper; + +import net.runelite.api.Client; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.math.Rs2Random; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.security.Login; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; + +class BanksShopperHopResultTest { + + @Test + void refusedHopIsAttemptedOnceAndReportedAsNotHopped() throws Exception { + assertEquals(Boolean.FALSE, hopWorld(false)); + } + + @Test + void successfulHopIsReportedAsHopped() throws Exception { + assertEquals(Boolean.TRUE, hopWorld(true)); + } + + private static Object hopWorld(boolean hopResult) throws Exception { + BanksShopperScript script = new BanksShopperScript(mock(BanksShopperPlugin.class)); + Client client = mock(Client.class); + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic login = mockStatic(Login.class); + MockedStatic player = mockStatic(Rs2Player.class); + MockedStatic random = mockStatic(Rs2Random.class)) { + microbot.when(Microbot::getClient).thenReturn(client); + player.when(Rs2Player::isMember).thenReturn(false); + login.when(() -> Login.getRandomWorld(false)).thenReturn(301); + microbot.when(() -> Microbot.hopToWorld(anyInt())).thenReturn(hopResult); + + Method hopWorld = BanksShopperScript.class.getDeclaredMethod("hopWorld"); + hopWorld.setAccessible(true); + Object result = hopWorld.invoke(script); + + microbot.verify(() -> Microbot.hopToWorld(anyInt()), times(1)); + return result; + } + } +} diff --git a/src/test/java/net/runelite/client/plugins/microbot/giantmole/GiantMoleHopResultTest.java b/src/test/java/net/runelite/client/plugins/microbot/giantmole/GiantMoleHopResultTest.java new file mode 100644 index 0000000000..c1e1119220 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/giantmole/GiantMoleHopResultTest.java @@ -0,0 +1,62 @@ +package net.runelite.client.plugins.microbot.giantmole; + +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.giantmole.enums.GiantMoleState; +import net.runelite.client.plugins.microbot.util.security.Login; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mockStatic; + +class GiantMoleHopResultTest { + + @AfterEach + void resetStatics() { + GiantMoleScript.checkedIfWorldOccupied = false; + GiantMoleScript.isWorldOccupied = true; + GiantMoleScript.state = GiantMoleState.IDLE; + } + + @Test + void refusedHopKeepsOccupiedWorldVerdictSoTheNextLoopRetries() { + GiantMoleScript.checkedIfWorldOccupied = true; + GiantMoleScript.isWorldOccupied = true; + GiantMoleScript.state = GiantMoleState.HOPPING; + + hop(false); + + assertTrue(GiantMoleScript.checkedIfWorldOccupied); + assertEquals(GiantMoleState.HOPPING, GiantMoleScript.state); + } + + @Test + void successfulHopClearsVerdictForTheNewWorld() { + GiantMoleScript.checkedIfWorldOccupied = true; + GiantMoleScript.isWorldOccupied = true; + GiantMoleScript.state = GiantMoleState.HOPPING; + + hop(true); + + assertFalse(GiantMoleScript.checkedIfWorldOccupied); + assertEquals(GiantMoleState.IDLE, GiantMoleScript.state); + } + + private static void hop(boolean hopResult) { + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic login = mockStatic(Login.class); + MockedStatic player = mockStatic(Rs2Player.class)) { + player.when(Rs2Player::isMember).thenReturn(true); + login.when(() -> Login.getRandomWorld(true)).thenReturn(420); + microbot.when(() -> Microbot.hopToWorld(420)).thenReturn(hopResult); + + new GiantMoleScript().hopWorlds(); + + microbot.verify(() -> Microbot.hopToWorld(420)); + } + } +} diff --git a/src/test/java/net/runelite/client/plugins/microbot/hsblackjack/HSBlackJackHopResultTest.java b/src/test/java/net/runelite/client/plugins/microbot/hsblackjack/HSBlackJackHopResultTest.java new file mode 100644 index 0000000000..2892403331 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/hsblackjack/HSBlackJackHopResultTest.java @@ -0,0 +1,51 @@ +package net.runelite.client.plugins.microbot.hsblackjack; + +import net.runelite.api.Client; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.math.Rs2Random; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.security.Login; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; + +class HSBlackJackHopResultTest { + + @Test + void refusedHopIsAttemptedOnceAndReportedAsNotHopped() throws Exception { + assertEquals(Boolean.FALSE, hopWorld(false)); + } + + @Test + void successfulHopIsReportedAsHopped() throws Exception { + assertEquals(Boolean.TRUE, hopWorld(true)); + } + + private static Object hopWorld(boolean hopResult) throws Exception { + HSBlackJackScript script = new HSBlackJackScript(mock(HSBlackJackPlugin.class), mock(HSBlackJackConfig.class)); + Client client = mock(Client.class); + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic login = mockStatic(Login.class); + MockedStatic player = mockStatic(Rs2Player.class); + MockedStatic random = mockStatic(Rs2Random.class)) { + microbot.when(Microbot::getClient).thenReturn(client); + player.when(Rs2Player::isMember).thenReturn(true); + login.when(() -> Login.getRandomWorld(true)).thenReturn(302); + microbot.when(() -> Microbot.hopToWorld(anyInt())).thenReturn(hopResult); + + Method hopWorld = HSBlackJackScript.class.getDeclaredMethod("hopWorld"); + hopWorld.setAccessible(true); + Object result = hopWorld.invoke(script); + + microbot.verify(() -> Microbot.hopToWorld(anyInt()), times(1)); + return result; + } + } +} diff --git a/src/test/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinHopResultTest.java b/src/test/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinHopResultTest.java new file mode 100644 index 0000000000..4755686fdc --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinHopResultTest.java @@ -0,0 +1,95 @@ +package net.runelite.client.plugins.microbot.microhunter.scripts; + +import net.runelite.api.Client; +import net.runelite.client.game.WorldService; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.http.api.worlds.World; +import net.runelite.http.api.worlds.WorldRegion; +import net.runelite.http.api.worlds.WorldResult; +import net.runelite.http.api.worlds.WorldType; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +class AutoChinHopResultTest { + + private static final int SOURCE_WORLD = 330; + private static final int TARGET_WORLD = 480; + + @Test + void refusedHopIsNotCountedOrBlacklistedAndIsRetriedForTheSameWorld() throws Exception { + AutoChinScript script = new AutoChinScript(); + Client client = mock(Client.class); + when(client.getWorld()).thenReturn(SOURCE_WORLD); + WorldService worldService = mock(WorldService.class); + WorldResult worldResult = mock(WorldResult.class); + World target = mock(World.class); + when(target.getId()).thenReturn(TARGET_WORLD); + when(target.getRegion()).thenReturn(WorldRegion.AUSTRALIA); + when(target.getTypes()).thenReturn(EnumSet.of(WorldType.MEMBERS)); + when(target.getPlayers()).thenReturn(200); + when(worldResult.getWorlds()).thenReturn(List.of(target)); + when(worldService.getWorlds()).thenReturn(worldResult); + + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic player = mockStatic(Rs2Player.class)) { + microbot.when(Microbot::getClient).thenReturn(client); + microbot.when(Microbot::getWorldService).thenReturn(worldService); + player.when(Rs2Player::isMember).thenReturn(true); + microbot.when(() -> Microbot.hopToWorld(TARGET_WORLD)).thenReturn(false); + + invoke(script, "requestAustralianWorldHop", "occupied"); + + assertEquals(0, field(script, "worldHopAttempts")); + assertTrue(((Map) field(script, "rejectedWorlds")).isEmpty()); + assertEquals(TARGET_WORLD, field(script, "hopTargetWorld")); + assertEquals("HOPPING_WORLD", field(script, "currentState").toString()); + + invoke(script, "monitorWorldHop"); + microbot.verify(() -> Microbot.hopToWorld(TARGET_WORLD), times(1)); + + setField(script, "hopStartedAt", System.currentTimeMillis() - 3_000); + microbot.when(() -> Microbot.hopToWorld(TARGET_WORLD)).thenReturn(true); + invoke(script, "monitorWorldHop"); + + microbot.verify(() -> Microbot.hopToWorld(TARGET_WORLD), times(2)); + assertEquals(1, field(script, "worldHopAttempts")); + assertTrue(((Map) field(script, "rejectedWorlds")).isEmpty()); + } + } + + private static void invoke(Object target, String name, Object... args) throws Exception { + Class[] types = new Class[args.length]; + for (int i = 0; i < args.length; i++) { + types[i] = args[i].getClass(); + } + Method method = target.getClass().getDeclaredMethod(name, types); + method.setAccessible(true); + method.invoke(target, args); + } + + private static Object field(Object target, String name) throws Exception { + Field field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + return field.get(target); + } + + private static void setField(Object target, String name, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } +} diff --git a/src/test/java/net/runelite/client/plugins/microbot/mke_wintertodt/WintertodtHopResultTest.java b/src/test/java/net/runelite/client/plugins/microbot/mke_wintertodt/WintertodtHopResultTest.java new file mode 100644 index 0000000000..554de2000d --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/mke_wintertodt/WintertodtHopResultTest.java @@ -0,0 +1,72 @@ +package net.runelite.client.plugins.microbot.mke_wintertodt; + +import net.runelite.api.Client; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.breakhandler.BreakHandlerScript; +import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; +import net.runelite.client.plugins.microbot.util.math.Rs2Random; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; + +class WintertodtHopResultTest { + + @Test + void refusedHopKeepsTheScriptRunningAndRetriesAfterACooldown() throws Exception { + MKE_WintertodtScript script = new MKE_WintertodtScript(); + Client client = mock(Client.class); + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic player = mockStatic(Rs2Player.class); + MockedStatic bank = mockStatic(Rs2Bank.class); + MockedStatic breakHandler = mockStatic(BreakHandlerScript.class); + MockedStatic random = mockStatic(Rs2Random.class)) { + microbot.when(Microbot::getClient).thenReturn(client); + microbot.when(Microbot::isLoggedIn).thenReturn(true); + player.when(Rs2Player::getWorld).thenReturn(301); + random.when(() -> Rs2Random.between(anyInt(), anyInt())).thenReturn(0, 1, 2, 3); + microbot.when(() -> Microbot.hopToWorld(anyInt())).thenReturn(false); + + assertTrue(ensureWintertodtWorld(script)); + microbot.verify(() -> Microbot.hopToWorld(anyInt()), times(1)); + assertFalse((Boolean) field(script, "worldChecked")); + + assertTrue(ensureWintertodtWorld(script)); + microbot.verify(() -> Microbot.hopToWorld(anyInt()), times(1)); + + setField(script, "lastRefusedHopAt", System.currentTimeMillis() - 5_000); + microbot.when(() -> Microbot.hopToWorld(anyInt())).thenReturn(true); + assertTrue(ensureWintertodtWorld(script)); + microbot.verify(() -> Microbot.hopToWorld(anyInt()), times(2)); + assertEquals(Boolean.TRUE, field(script, "worldChecked")); + } + } + + private static boolean ensureWintertodtWorld(MKE_WintertodtScript script) throws Exception { + Method method = MKE_WintertodtScript.class.getDeclaredMethod("ensureWintertodtWorldSafely"); + method.setAccessible(true); + return (Boolean) method.invoke(script); + } + + private static Object field(Object target, String name) throws Exception { + Field field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + return field.get(target); + } + + private static void setField(Object target, String name, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } +} From d442676aaa7d649494b3ac08d77b3a34f02fb2ca Mon Sep 17 00:00:00 2001 From: chsami Date: Tue, 13 Oct 2026 13:49:38 +0200 Subject: [PATCH 2/7] fix(shootingstar): walk to an F2P star before hopping off a members world (#599) The WALKING state hopped to the star's world before moving. From a members-only area the server refuses a hop to a non-members world, and newer clients block repeating that hop from the same tile, so the script stood still forever. For a non-members star the script now walks to the star first when it is on a members world or a previous hop failed, then hops on arrival (clearing a stale members-area refusal flag there). Members-world stars still hop before walking. Bump to 1.4.8. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../shootingstar/ShootingStarPlugin.java | 2 +- .../shootingstar/ShootingStarScript.java | 91 +++++++---- .../ShootingStarWalkBeforeHopTest.java | 152 ++++++++++++++++++ 3 files changed, 216 insertions(+), 29 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarWalkBeforeHopTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarPlugin.java index 3853812c87..e857489276 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarPlugin.java @@ -73,7 +73,7 @@ public class ShootingStarPlugin extends Plugin { - static final String version = "1.4.7"; + static final String version = "1.4.8"; @Getter private final List starList = new ArrayList<>(); diff --git a/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarScript.java b/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarScript.java index 9694832728..cf202d3528 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarScript.java @@ -7,7 +7,6 @@ import java.util.stream.Collectors; import javax.inject.Inject; import lombok.extern.slf4j.Slf4j; -import net.runelite.api.GameState; import net.runelite.api.coords.WorldPoint; import net.runelite.api.gameval.ObjectID; import net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel; @@ -48,6 +47,7 @@ public class ShootingStarScript extends Script private Star currentStar; private boolean hasEquipment = false; private boolean hasInventory = false; + private boolean walkBeforeHop = false; public Instant startTime; @@ -75,6 +75,7 @@ public boolean run() initialPlayerLocation = null; hasEquipment = false; hasInventory = false; + walkBeforeHop = false; Rs2Antiban.resetAntibanSettings(); applyAntiBanSettings(); Rs2Antiban.setActivity(Activity.GENERAL_MINING); @@ -167,33 +168,7 @@ public boolean run() state = ShootingStarState.WALKING; break; case WALKING: - toggleLockState(true); - - if (Rs2Player.getWorld() != currentStar.getWorld()) - { - Microbot.hopToWorld(currentStar.getWorld()); - sleepUntil(() -> Microbot.getClient().getGameState() == GameState.LOGGED_IN); - return; - } - - boolean isNearShootingStar = Rs2Player.getWorldLocation().distanceTo(currentStar.getShootingStarLocation().getWorldPoint()) < 6; - - if (!isNearShootingStar) - { - WalkerState walkerState = Rs2Walker.walkWithState(currentStar.getShootingStarLocation().getWorldPoint(), 6); - if (walkerState == WalkerState.UNREACHABLE) - { - log.debug("Walker State is {}, removing star from list", walkerState); - plugin.removeStar(plugin.getSelectedStar()); - plugin.updatePanelList(true); - state = ShootingStarState.WAITING_FOR_STAR; - } - return; - } - - initialPlayerLocation = Rs2Player.getWorldLocation(); - - state = ShootingStarState.MINING; + handleWalking(currentStar); break; case MINING: if (!currentStar.hasRequirements()) @@ -332,6 +307,66 @@ public void shutdown() Rs2Antiban.resetAntibanSettings(); } + void handleWalking(Star star) + { + toggleLockState(true); + + boolean isOnStarWorld = Rs2Player.getWorld() == star.getWorld(); + boolean isNearShootingStar = Rs2Player.getWorldLocation().distanceTo(star.getShootingStarLocation().getWorldPoint()) < 6; + + if (!isOnStarWorld && (isNearShootingStar || !shouldWalkBeforeHop(star))) + { + hopToStarWorld(star, isNearShootingStar); + return; + } + + if (!isNearShootingStar) + { + WalkerState walkerState = Rs2Walker.walkWithState(star.getShootingStarLocation().getWorldPoint(), 6); + if (walkerState == WalkerState.UNREACHABLE) + { + log.debug("Walker State is {}, removing star from list", walkerState); + plugin.removeStar(plugin.getSelectedStar()); + plugin.updatePanelList(true); + state = ShootingStarState.WAITING_FOR_STAR; + } + return; + } + + initialPlayerLocation = Rs2Player.getWorldLocation(); + + state = ShootingStarState.MINING; + } + + private boolean shouldWalkBeforeHop(Star star) + { + if (star.isMemberWorld()) + { + return false; + } + return walkBeforeHop || Rs2Player.isInMemberWorld(); + } + + private void hopToStarWorld(Star star, boolean isNearShootingStar) + { + if (isNearShootingStar && !star.isMemberWorld() && Microbot.cantHopWorld) + { + Microbot.cantHopWorld = false; + } + + if (Microbot.hopToWorld(star.getWorld())) + { + walkBeforeHop = false; + return; + } + + if (!star.isMemberWorld() && !isNearShootingStar) + { + log.info("Hop to non-members world {} failed, walking to the star before hopping again", star.getWorld()); + walkBeforeHop = true; + } + } + private boolean isUsingInventorySetup() { boolean isInventorySetupPluginEnabled = Microbot.isPluginEnabled(MInventorySetupsPlugin.class); diff --git a/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarWalkBeforeHopTest.java b/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarWalkBeforeHopTest.java new file mode 100644 index 0000000000..ea010a61c3 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarWalkBeforeHopTest.java @@ -0,0 +1,152 @@ +package net.runelite.client.plugins.microbot.shootingstar; + +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.shootingstar.enums.ShootingStarLocation; +import net.runelite.client.plugins.microbot.shootingstar.enums.ShootingStarState; +import net.runelite.client.plugins.microbot.shootingstar.model.Star; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; +import net.runelite.client.plugins.microbot.util.walker.WalkerState; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +class ShootingStarWalkBeforeHopTest +{ + private static final int MEMBERS_WORLD = 302; + private static final int STAR_WORLD = 301; + private static final ShootingStarLocation LOCATION = ShootingStarLocation.DRAYNOR_VILLAGE; + private static final WorldPoint FAR_AWAY = new WorldPoint(2605, 3092, 0); + private static final WorldPoint AT_STAR = LOCATION.getWorldPoint().dx(2); + + private MockedStatic microbot; + private MockedStatic player; + private MockedStatic walker; + private ShootingStarScript script; + + @BeforeEach + void setUp() + { + microbot = mockStatic(Microbot.class); + player = mockStatic(Rs2Player.class); + walker = mockStatic(Rs2Walker.class); + walker.when(() -> Rs2Walker.walkWithState(any(WorldPoint.class), anyInt())).thenReturn(WalkerState.MOVING); + player.when(Rs2Player::getWorld).thenReturn(MEMBERS_WORLD); + ShootingStarPlugin plugin = mock(ShootingStarPlugin.class); + script = new ShootingStarScript(plugin, mock(ShootingStarConfig.class)); + ShootingStarScript.state = ShootingStarState.WALKING; + Microbot.cantHopWorld = false; + } + + @AfterEach + void tearDown() + { + walker.close(); + player.close(); + microbot.close(); + ShootingStarScript.state = null; + Microbot.cantHopWorld = false; + } + + @Test + void f2pStarFromMembersWorldWalksToTheStarInsteadOfHopping() + { + standAt(FAR_AWAY, true); + + script.handleWalking(star(false)); + + microbot.verify(() -> Microbot.hopToWorld(anyInt()), never()); + walker.verify(() -> Rs2Walker.walkWithState(LOCATION.getWorldPoint(), 6)); + } + + @Test + void f2pStarHopsOnceThePlayerReachedTheStar() + { + standAt(AT_STAR, true); + microbot.when(() -> Microbot.hopToWorld(STAR_WORLD)).thenReturn(true); + + script.handleWalking(star(false)); + + microbot.verify(() -> Microbot.hopToWorld(STAR_WORLD)); + walker.verify(() -> Rs2Walker.walkWithState(any(WorldPoint.class), anyInt()), never()); + assertEquals(ShootingStarState.WALKING, ShootingStarScript.state); + } + + @Test + void membersStarStillHopsBeforeWalking() + { + standAt(FAR_AWAY, true); + microbot.when(() -> Microbot.hopToWorld(STAR_WORLD)).thenReturn(true); + + script.handleWalking(star(true)); + + microbot.verify(() -> Microbot.hopToWorld(STAR_WORLD)); + walker.verify(() -> Rs2Walker.walkWithState(any(WorldPoint.class), anyInt()), never()); + } + + @Test + void failedHopToF2pWorldMakesTheNextLoopWalkInsteadOfRetryingTheHop() + { + standAt(FAR_AWAY, false); + microbot.when(() -> Microbot.hopToWorld(STAR_WORLD)).thenReturn(false); + Star star = star(false); + + script.handleWalking(star); + script.handleWalking(star); + + microbot.verify(() -> Microbot.hopToWorld(STAR_WORLD), times(1)); + walker.verify(() -> Rs2Walker.walkWithState(LOCATION.getWorldPoint(), 6), times(1)); + } + + @Test + void staleMembersAreaRefusalIsClearedOnceAtTheF2pStar() + { + standAt(AT_STAR, true); + Microbot.cantHopWorld = true; + microbot.when(() -> Microbot.hopToWorld(STAR_WORLD)).thenAnswer(invocation -> !Microbot.cantHopWorld); + + script.handleWalking(star(false)); + + assertFalse(Microbot.cantHopWorld); + microbot.verify(() -> Microbot.hopToWorld(STAR_WORLD)); + } + + @Test + void onTheStarWorldNextToTheStarStartsMining() + { + standAt(AT_STAR, false); + player.when(Rs2Player::getWorld).thenReturn(STAR_WORLD); + + script.handleWalking(star(false)); + + microbot.verify(() -> Microbot.hopToWorld(anyInt()), never()); + assertEquals(ShootingStarState.MINING, ShootingStarScript.state); + } + + private void standAt(WorldPoint location, boolean onMembersWorld) + { + player.when(Rs2Player::getWorldLocation).thenReturn(location); + player.when(Rs2Player::isInMemberWorld).thenReturn(onMembersWorld); + } + + private static Star star(boolean membersWorld) + { + Star star = mock(Star.class); + when(star.getWorld()).thenReturn(STAR_WORLD); + when(star.isMemberWorld()).thenReturn(membersWorld); + when(star.getShootingStarLocation()).thenReturn(LOCATION); + return star; + } +} From e66d88403032ae5c66e60d1b226be2fac91bbe40 Mon Sep 17 00:00:00 2001 From: chsami Date: Tue, 13 Oct 2026 14:08:24 +0200 Subject: [PATCH 3/7] fix(hub): bound refused hop retries left by the hopToWorld result change (#600) AutoChin retried a refused target every 3s forever without counting an attempt or blacklisting it; it now gives up on a target after 3 refusals, blacklists it, counts the attempt and picks another world, so the 5-attempt cap still applies. Slayer retried a refused crash hop every loop with prayers toggled off each time; it now waits 10s after a refusal and skips the hop (and the prayer toggle) while in combat. Thieving dropped its escape hop when the game refused it after combat; the hop now stays pending and is retried at the escape spot, paced by the existing 15s hop throttle, for up to 60s. Slayer, Ammonite Crab and Sand Crab no longer wait 5s for a Switch World dialog and press space after hopToWorld has already landed. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../AmmoniteCrabs/AmmoniteCrabPlugin.java | 2 +- .../AmmoniteCrabs/AmmoniteCrabScript.java | 6 - .../microhunter/AutoHunterPlugin.java | 2 +- .../microhunter/scripts/AutoChinScript.java | 20 ++- .../microbot/sandcrabs/SandCrabPlugin.java | 2 +- .../microbot/sandcrabs/SandCrabScript.java | 10 +- .../plugins/microbot/slayer/SlayerPlugin.java | 2 +- .../plugins/microbot/slayer/SlayerScript.java | 30 +++-- .../microbot/thieving/ThievingPlugin.java | 2 +- .../microbot/thieving/ThievingScript.java | 58 ++++++-- .../scripts/AutoChinHopResultTest.java | 78 +++++++++++ .../microbot/slayer/SlayerHopResultTest.java | 125 ++++++++++++++++++ .../thieving/ThievingEscapeHopTest.java | 104 +++++++++++++++ 13 files changed, 398 insertions(+), 43 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/slayer/SlayerHopResultTest.java create mode 100644 src/test/java/net/runelite/client/plugins/microbot/thieving/ThievingEscapeHopTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabPlugin.java index 30fde56e16..6233dd29c1 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabPlugin.java @@ -28,7 +28,7 @@ @Slf4j public class AmmoniteCrabPlugin extends Plugin { - public final static String version = "1.1.5"; + public final static String version = "1.1.6"; @Inject public AmmoniteCrabScript ammoniteCrabScript; @Inject diff --git a/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabScript.java b/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabScript.java index 4d03afefbb..4706db437b 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabScript.java @@ -13,15 +13,12 @@ import net.runelite.client.plugins.microbot.util.grounditem.LootingParameters; import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItem; import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.keyboard.Rs2Keyboard; import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.microbot.util.player.Rs2PlayerModel; import net.runelite.client.plugins.microbot.util.security.Login; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; -import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; -import java.awt.event.KeyEvent; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -106,9 +103,6 @@ public boolean run(AmmoniteCrabConfig config) { break; } timesHopped++; - boolean result = sleepUntil(() -> Rs2Widget.findWidget("Switch World") != null); - Rs2Keyboard.keyPress(KeyEvent.VK_SPACE); - sleepUntil(() -> Microbot.getClient().getGameState() == GameState.HOPPING); sleepUntil(() -> Microbot.getClient().getGameState() == GameState.LOGGED_IN); sleep(1200, 2000); // After successful hop, reset counters and scan the area diff --git a/src/main/java/net/runelite/client/plugins/microbot/microhunter/AutoHunterPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/microhunter/AutoHunterPlugin.java index 4bb8b85152..970d5b3b2e 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/microhunter/AutoHunterPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/microhunter/AutoHunterPlugin.java @@ -31,7 +31,7 @@ @Slf4j @PluginDependency(HunterPlugin.class) public class AutoHunterPlugin extends Plugin { - public static final String version = "1.5.1"; + public static final String version = "1.5.2"; @Inject private AutoHunterConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinScript.java b/src/main/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinScript.java index e32b513039..4b030d9b38 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinScript.java @@ -85,6 +85,7 @@ private enum Action { private static final long PLAYER_PERSISTENCE_MS = 3_500; private static final long WORLD_HOP_TIMEOUT_MS = 15_000; private static final long REFUSED_HOP_RETRY_MS = 3_000; + private static final int MAX_REFUSED_HOPS_PER_WORLD = 3; private static final long REJECTED_WORLD_COOLDOWN_MS = 600_000; private static final long BREAK_RECOVERY_CLEAR_MS = 1_500; private static final int MAX_WORLD_HOP_ATTEMPTS = 5; @@ -142,6 +143,7 @@ private enum Action { private int hopSourceWorld = -1; private long hopStartedAt; private boolean hopRefused; + private int hopRefusals; private boolean breakRecoveryRequested; private boolean breakRecoveryComplete; private boolean breakRecoveryLockHeld; @@ -199,6 +201,7 @@ private void resetSession() { hopSourceWorld = -1; hopStartedAt = 0; hopRefused = false; + hopRefusals = 0; breakRecoveryRequested = false; breakRecoveryComplete = false; breakRecoveryLockHeld = false; @@ -311,6 +314,7 @@ private void beginWorldArrival(AutoHunterConfig config, int world) { hopSourceWorld = -1; hopStartedAt = 0; hopRefused = false; + hopRefusals = 0; nearbyPlayerSeenSince = 0; if (!config.avoidOccupiedWorlds()) { worldHopAttempts = 0; @@ -432,6 +436,7 @@ private void requestAustralianWorldHop(String reason) { } hopSourceWorld = Microbot.getClient().getWorld(); hopTargetWorld = target; + hopRefusals = 0; transition(State.HOPPING_WORLD, "Hopping to Australian world " + target + " (" + reason + ")"); Microbot.log("AutoHunter occupancy: hopping " + hopSourceWorld + " -> " + target + " attempt=" + (worldHopAttempts + 1)); @@ -442,7 +447,9 @@ private void issueWorldHop() { hopStartedAt = System.currentTimeMillis(); hopRefused = !Microbot.hopToWorld(hopTargetWorld); if (hopRefused) { - Microbot.log("AutoHunter occupancy: hop to world " + hopTargetWorld + " refused; retrying"); + hopRefusals++; + Microbot.log("AutoHunter occupancy: hop to world " + hopTargetWorld + " refused (" + + hopRefusals + "/" + MAX_REFUSED_HOPS_PER_WORLD + ")"); return; } worldHopAttempts++; @@ -473,7 +480,16 @@ private void monitorWorldHop() { int currentWorld = Microbot.getClient().getWorld(); if (hopSourceWorld > 0 && currentWorld != hopSourceWorld) return; if (hopRefused) { - if (System.currentTimeMillis() - hopStartedAt >= REFUSED_HOP_RETRY_MS) issueWorldHop(); + if (System.currentTimeMillis() - hopStartedAt < REFUSED_HOP_RETRY_MS) return; + if (hopRefusals < MAX_REFUSED_HOPS_PER_WORLD) { + issueWorldHop(); + return; + } + worldHopAttempts++; + rejectedWorlds.put(hopTargetWorld, System.currentTimeMillis() + REJECTED_WORLD_COOLDOWN_MS); + Microbot.log("AutoHunter occupancy: giving up on world " + hopTargetWorld + " after " + + hopRefusals + " refused hops"); + requestAustralianWorldHop("previous hop refused"); return; } if (System.currentTimeMillis() - hopStartedAt < WORLD_HOP_TIMEOUT_MS) return; diff --git a/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabPlugin.java index 92a215a5db..22ac529283 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabPlugin.java @@ -26,7 +26,7 @@ ) @Slf4j public class SandCrabPlugin extends Plugin { - public final static String version = "1.5.5"; + public final static String version = "1.5.6"; @Inject private SandCrabConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabScript.java b/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabScript.java index b2c6d72658..6639fb7089 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabScript.java @@ -10,7 +10,6 @@ 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.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.keyboard.Rs2Keyboard; import net.runelite.client.plugins.microbot.util.math.Rs2Random; import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; import net.runelite.client.plugins.microbot.util.player.Rs2Player; @@ -18,9 +17,7 @@ import net.runelite.client.plugins.microbot.util.security.Login; import net.runelite.client.plugins.microbot.util.tabs.Rs2Tab; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; -import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; -import java.awt.event.KeyEvent; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -166,12 +163,7 @@ public boolean run(SandCrabConfig config, SandCrabPlugin plugin) { Microbot.log("World hop to " + world + " refused, retrying next loop"); break; } - boolean result = sleepUntil(() -> Rs2Widget.findWidget("Switch World") != null); - if (result) { - Rs2Keyboard.keyPress(KeyEvent.VK_SPACE); - sleepUntil(() -> Microbot.getClient().getGameState() == GameState.HOPPING); - sleepUntil(() -> Microbot.getClient().getGameState() == GameState.LOGGED_IN); - } + sleepUntil(() -> Microbot.getClient().getGameState() == GameState.LOGGED_IN); if (timesHopped > 10) { timesHopped = 0; diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerPlugin.java index 8d1e6ee6d0..6fd51890e1 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerPlugin.java @@ -42,7 +42,7 @@ @Slf4j public class SlayerPlugin extends Plugin { - static final String version = "1.1.2"; + static final String version = "1.1.3"; @Inject private SlayerConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerScript.java b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerScript.java index 94e4b8a44f..2ef14097a7 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/slayer/SlayerScript.java @@ -110,9 +110,11 @@ public class SlayerScript extends Script { // Crash detection / world hop tracking private long crashDetectionStartTime = 0; private long lastWorldHopTime = 0; + private long lastHopRefusedAt = 0; private boolean isSearchingForTargets = false; private static final int CRASH_HOP_DELAY_MS = 20000; // 20 seconds before hopping private static final int WORLD_HOP_COOLDOWN_MS = 60000; // 1 minute cooldown between hops + private static final int REFUSED_HOP_RETRY_MS = 10000; // Task completion loot delay private long taskCompletedTime = 0; @@ -2898,7 +2900,11 @@ private void handleCombat() { if (timeSinceStart >= CRASH_HOP_DELAY_MS) { // Check cooldown long timeSinceLastHop = System.currentTimeMillis() - lastWorldHopTime; - if (timeSinceLastHop >= WORLD_HOP_COOLDOWN_MS) { + long timeSinceRefusedHop = System.currentTimeMillis() - lastHopRefusedAt; + if (timeSinceRefusedHop < REFUSED_HOP_RETRY_MS) { + log.debug("Last world hop was refused, retrying in {} seconds", + (REFUSED_HOP_RETRY_MS - timeSinceRefusedHop) / 1000); + } else if (timeSinceLastHop >= WORLD_HOP_COOLDOWN_MS) { log.info("No targets found for {} seconds - hopping worlds (likely crashed)", CRASH_HOP_DELAY_MS / 1000); hopToNewWorld(); @@ -2935,29 +2941,31 @@ private void handleCombat() { */ private void hopToNewWorld() { try { - // Disable prayers before hopping - deactivateProfilePrayer(); - deactivateOffensivePrayers(); + if (Rs2Combat.inCombat()) { + lastHopRefusedAt = System.currentTimeMillis(); + log.info("In combat, delaying world hop"); + return; + } // Get world from config list or random int world = getHopWorld(); if (world <= 0) { + lastHopRefusedAt = System.currentTimeMillis(); log.warn("Could not determine a valid world to hop to"); return; } + + // Disable prayers before hopping + deactivateProfilePrayer(); + deactivateOffensivePrayers(); log.info("Hopping to world {}", world); if (!Microbot.hopToWorld(world)) { - log.warn("World hop to {} refused, retrying on a later loop", world); + lastHopRefusedAt = System.currentTimeMillis(); + log.warn("World hop to {} refused, retrying in {} seconds", world, REFUSED_HOP_RETRY_MS / 1000); return; } - // Wait for switch world confirmation dialog - sleepUntil(() -> Rs2Widget.findWidget("Switch World") != null, 5000); - Rs2Keyboard.keyPress(KeyEvent.VK_SPACE); - - // Wait for hop to complete - sleepUntil(() -> Microbot.getClient().getGameState() == GameState.HOPPING, 5000); sleepUntil(() -> Microbot.getClient().getGameState() == GameState.LOGGED_IN, 10000); // Brief pause after hopping diff --git a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java index 53dabb06af..87fdb039c3 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java @@ -34,7 +34,7 @@ ) @Slf4j public class ThievingPlugin extends Plugin { - public static final String version = "2.1.3"; + public static final String version = "2.1.4"; @Inject @Getter diff --git a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingScript.java b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingScript.java index 2c59b1ba2a..4343cb9e85 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingScript.java @@ -88,6 +88,8 @@ ThievingNpc.WEALTHY_CITIZEN, new WealthyCitizenStrategy() private int doorCloseIndex = 0; private long lastAction = Long.MAX_VALUE; private long lastHopAt = 0; + private long escapeHopUntil = 0; + private static final long ESCAPE_HOP_WINDOW_MS = 60_000; protected static int getCloseDoorTime() { return DOOR_TIMER.getRemainingTime(); @@ -277,6 +279,8 @@ private State getCurrentState() { return applyOverride(State.ESCAPE); } + if (isEscapeHopPending()) return applyOverride(State.ESCAPE); + if (!hasReqs()) return applyOverride(State.BANK); if (config.useFood()) { @@ -433,6 +437,10 @@ private void loop() { item.click("Take"); return; case ESCAPE: + if (!underAttack && isEscapeHopPending()) { + if (attemptEscapeHop()) returnToStartAfterEscape(); + return; + } WorldPoint escape = null; final String escapeLocationString = config.customEscapeLocation(); if (!escapeLocationString.isBlank()) { @@ -458,16 +466,14 @@ private void loop() { underAttack = false; if (!isRunning()) return; if (shouldHop()) { - hopWorld(); + requestEscapeHop(); } else { log.debug("Skipping world hop (Wealthy Citizen)"); } } - if (walkTo("Walk to start", startingLocation, 3)) { - sleepUntilWithInterrupt(() -> !Rs2Player.isMoving(), 1_200); - } - DOOR_TIMER.set(); + if (isEscapeHopPending() && !attemptEscapeHop()) return; + returnToStartAfterEscape(); return; } else { log.error("Failed to use escape route defaulting to bank escape"); @@ -603,6 +609,7 @@ public boolean run() { lastAction = System.currentTimeMillis(); nextShadowVeil = System.currentTimeMillis()+60_000; underAttack = false; + escapeHopUntil = 0; startupGraceUntil = System.currentTimeMillis() + 4_000; startTime = Instant.now(); mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { @@ -1007,18 +1014,48 @@ private void bankAndEquip() { if (underAttack) { underAttack = false; if (shouldHop()) { - hopWorld(); + requestEscapeHop(); } else { log.debug("Skipping world hop (Wealthy Citizen)"); } } - if (walkTo("Return to npc", startingLocation, 3)) { + if (isEscapeHopPending() && !attemptEscapeHop()) return; + returnToStartAfterEscape(); + } + + private void returnToStartAfterEscape() { + if (walkTo("Walk to start", startingLocation, 3)) { sleepUntilWithInterrupt(() -> !Rs2Player.isMoving(), 1_200); } DOOR_TIMER.set(); } + private boolean isEscapeHopPending() { + return escapeHopUntil != 0; + } + + private void requestEscapeHop() { + if (escapeHopUntil == 0) escapeHopUntil = System.currentTimeMillis() + ESCAPE_HOP_WINDOW_MS; + } + + private boolean attemptEscapeHop() { + if (!config.escapeAttacking() || !shouldHop()) { + escapeHopUntil = 0; + return true; + } + if (hopWorld()) { + escapeHopUntil = 0; + return true; + } + if (System.currentTimeMillis() >= escapeHopUntil) { + log.warn("Escape hop did not succeed within {}s, returning without hopping", ESCAPE_HOP_WINDOW_MS / 1000); + escapeHopUntil = 0; + return true; + } + return false; + } + private void dropAllExceptImportant() { final Set keep = getExclusions(); if (config.DoNotDropItemList() != null && !config.DoNotDropItemList().isEmpty()) @@ -1034,11 +1071,11 @@ private void dropAllExceptImportant() { } - private void hopWorld() { + private boolean hopWorld() { final long now = System.currentTimeMillis(); if (now - lastHopAt < 15000) { log.debug("Skipping hop; last hop was {}ms ago", now - lastHopAt); - return; + return false; } lastHopAt = now; log.debug("Hopping world, please wait"); @@ -1046,11 +1083,12 @@ private void hopWorld() { final int world = Login.getRandomWorld(true, null); if (!Microbot.hopToWorld(world)) { log.warn("World hop to {} refused, retrying later", world); - return; + return false; } DOOR_TIMER.unset(); log.debug("Successful hop world"); thievingNpc = null; // force fresh lookup after hop + return true; } private void drinkAncientBrew() { diff --git a/src/test/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinHopResultTest.java b/src/test/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinHopResultTest.java index 4755686fdc..b862483c6b 100644 --- a/src/test/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinHopResultTest.java +++ b/src/test/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinHopResultTest.java @@ -13,12 +13,15 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.EnumSet; import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.times; @@ -71,6 +74,81 @@ void refusedHopIsNotCountedOrBlacklistedAndIsRetriedForTheSameWorld() throws Exc } } + @Test + void persistentRefusalBlacklistsTheTargetAndMovesToAnotherWorld() throws Exception { + AutoChinScript script = new AutoChinScript(); + Client client = mock(Client.class); + when(client.getWorld()).thenReturn(SOURCE_WORLD); + WorldService worldService = australianWorlds(TARGET_WORLD, TARGET_WORLD + 1); + + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic player = mockStatic(Rs2Player.class)) { + microbot.when(Microbot::getClient).thenReturn(client); + microbot.when(Microbot::getWorldService).thenReturn(worldService); + player.when(Rs2Player::isMember).thenReturn(true); + microbot.when(() -> Microbot.hopToWorld(anyInt())).thenReturn(false); + + invoke(script, "requestAustralianWorldHop", "occupied"); + int firstTarget = (int) field(script, "hopTargetWorld"); + + for (int i = 0; i < 3; i++) { + setField(script, "hopStartedAt", System.currentTimeMillis() - 3_000); + invoke(script, "monitorWorldHop"); + } + + microbot.verify(() -> Microbot.hopToWorld(firstTarget), times(3)); + assertTrue(((Map) field(script, "rejectedWorlds")).containsKey(firstTarget)); + assertEquals(1, field(script, "worldHopAttempts")); + int secondTarget = (int) field(script, "hopTargetWorld"); + assertNotEquals(firstTarget, secondTarget); + microbot.verify(() -> Microbot.hopToWorld(secondTarget), times(1)); + assertEquals("HOPPING_WORLD", field(script, "currentState").toString()); + } + } + + @Test + void persistentRefusalOnEveryWorldStopsAtTheAttemptCap() throws Exception { + AutoChinScript script = new AutoChinScript(); + Client client = mock(Client.class); + when(client.getWorld()).thenReturn(SOURCE_WORLD); + WorldService worldService = australianWorlds(480, 481, 482, 483, 484, 485, 486, 487); + + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic player = mockStatic(Rs2Player.class)) { + microbot.when(Microbot::getClient).thenReturn(client); + microbot.when(Microbot::getWorldService).thenReturn(worldService); + player.when(Rs2Player::isMember).thenReturn(true); + microbot.when(() -> Microbot.hopToWorld(anyInt())).thenReturn(false); + + invoke(script, "requestAustralianWorldHop", "occupied"); + for (int i = 0; i < 100 && !"STOPPED".equals(field(script, "currentState").toString()); i++) { + setField(script, "hopStartedAt", System.currentTimeMillis() - 3_000); + invoke(script, "monitorWorldHop"); + } + + assertEquals("STOPPED", field(script, "currentState").toString()); + microbot.verify(() -> Microbot.hopToWorld(anyInt()), times(15)); + assertEquals(5, ((Map) field(script, "rejectedWorlds")).size()); + } + } + + private static WorldService australianWorlds(int... ids) { + List worlds = new ArrayList<>(); + for (int id : ids) { + World world = mock(World.class); + when(world.getId()).thenReturn(id); + when(world.getRegion()).thenReturn(WorldRegion.AUSTRALIA); + when(world.getTypes()).thenReturn(EnumSet.of(WorldType.MEMBERS)); + when(world.getPlayers()).thenReturn(200); + worlds.add(world); + } + WorldResult worldResult = mock(WorldResult.class); + when(worldResult.getWorlds()).thenReturn(worlds); + WorldService worldService = mock(WorldService.class); + when(worldService.getWorlds()).thenReturn(worldResult); + return worldService; + } + private static void invoke(Object target, String name, Object... args) throws Exception { Class[] types = new Class[args.length]; for (int i = 0; i < args.length; i++) { diff --git a/src/test/java/net/runelite/client/plugins/microbot/slayer/SlayerHopResultTest.java b/src/test/java/net/runelite/client/plugins/microbot/slayer/SlayerHopResultTest.java new file mode 100644 index 0000000000..9a1cc352cc --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/slayer/SlayerHopResultTest.java @@ -0,0 +1,125 @@ +package net.runelite.client.plugins.microbot.slayer; + +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.combat.Rs2Combat; +import net.runelite.client.plugins.microbot.util.keyboard.Rs2Keyboard; +import net.runelite.client.plugins.microbot.util.prayer.Rs2Prayer; +import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +class SlayerHopResultTest { + + private static final int CURRENT_WORLD = 330; + private static final int TARGET_WORLD = 331; + + @Test + void refusedHopStartsTheRefusalCooldownWithoutCountingAsAHop() throws Exception { + SlayerScript script = scriptWithHopList(); + Client client = client(); + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic combat = mockStatic(Rs2Combat.class); + MockedStatic prayer = mockStatic(Rs2Prayer.class)) { + microbot.when(Microbot::getClient).thenReturn(client); + combat.when(Rs2Combat::inCombat).thenReturn(false); + microbot.when(() -> Microbot.hopToWorld(TARGET_WORLD)).thenReturn(false); + + long before = System.currentTimeMillis(); + invoke(script, "hopToNewWorld"); + + microbot.verify(() -> Microbot.hopToWorld(TARGET_WORLD), times(1)); + assertTrue((long) field(script, "lastHopRefusedAt") >= before); + assertEquals(0L, field(script, "lastWorldHopTime")); + } + } + + @Test + void inCombatSkipsTheHopAndLeavesPrayersAlone() throws Exception { + SlayerScript script = scriptWithHopList(); + Client client = client(); + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic combat = mockStatic(Rs2Combat.class); + MockedStatic prayer = mockStatic(Rs2Prayer.class)) { + microbot.when(Microbot::getClient).thenReturn(client); + combat.when(Rs2Combat::inCombat).thenReturn(true); + + long before = System.currentTimeMillis(); + invoke(script, "hopToNewWorld"); + + microbot.verify(() -> Microbot.hopToWorld(anyInt()), never()); + prayer.verifyNoInteractions(); + assertTrue((long) field(script, "lastHopRefusedAt") >= before); + } + } + + @Test + void landedHopDoesNotWaitForSwitchWorldDialogOrPressSpace() throws Exception { + SlayerScript script = scriptWithHopList(); + Client client = client(); + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic combat = mockStatic(Rs2Combat.class); + MockedStatic prayer = mockStatic(Rs2Prayer.class); + MockedStatic widget = mockStatic(Rs2Widget.class); + MockedStatic keyboard = mockStatic(Rs2Keyboard.class)) { + microbot.when(Microbot::getClient).thenReturn(client); + combat.when(Rs2Combat::inCombat).thenReturn(false); + microbot.when(() -> Microbot.hopToWorld(TARGET_WORLD)).thenReturn(true); + + long before = System.currentTimeMillis(); + invoke(script, "hopToNewWorld"); + + keyboard.verify(() -> Rs2Keyboard.keyPress(anyInt()), never()); + widget.verifyNoInteractions(); + assertTrue((long) field(script, "lastWorldHopTime") >= before); + assertEquals(0L, field(script, "lastHopRefusedAt")); + } + } + + private static SlayerScript scriptWithHopList() throws Exception { + SlayerScript script = new SlayerScript(); + SlayerConfig config = mock(SlayerConfig.class); + when(config.hopWorldList()).thenReturn(String.valueOf(TARGET_WORLD)); + when(config.prayerFlickStyle()).thenReturn(PrayerFlickStyle.OFF); + setField(script, "config", config); + return script; + } + + private static Client client() { + Client client = mock(Client.class); + when(client.getWorld()).thenReturn(CURRENT_WORLD); + when(client.getGameState()).thenReturn(GameState.LOGGED_IN); + return client; + } + + private static void invoke(Object target, String name) throws Exception { + Method method = target.getClass().getDeclaredMethod(name); + method.setAccessible(true); + method.invoke(target); + } + + private static Object field(Object target, String name) throws Exception { + Field field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + return field.get(target); + } + + private static void setField(Object target, String name, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } +} diff --git a/src/test/java/net/runelite/client/plugins/microbot/thieving/ThievingEscapeHopTest.java b/src/test/java/net/runelite/client/plugins/microbot/thieving/ThievingEscapeHopTest.java new file mode 100644 index 0000000000..513a0c57a5 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/thieving/ThievingEscapeHopTest.java @@ -0,0 +1,104 @@ +package net.runelite.client.plugins.microbot.thieving; + +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.thieving.enums.ThievingNpc; +import net.runelite.client.plugins.microbot.util.security.Login; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +class ThievingEscapeHopTest { + + private static final int TARGET_WORLD = 420; + + @Test + void refusedEscapeHopStaysPendingAndIsRetriedUntilItLands() throws Exception { + ThievingScript script = script(ThievingNpc.VYRES); + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic login = mockStatic(Login.class)) { + login.when(() -> Login.getRandomWorld(anyBoolean(), any())).thenReturn(TARGET_WORLD); + microbot.when(() -> Microbot.hopToWorld(TARGET_WORLD)).thenReturn(false); + + invoke(script, "requestEscapeHop"); + assertFalse(call(script, "attemptEscapeHop")); + assertTrue(call(script, "isEscapeHopPending")); + + assertFalse(call(script, "attemptEscapeHop")); + microbot.verify(() -> Microbot.hopToWorld(anyInt()), times(1)); + assertTrue(call(script, "isEscapeHopPending")); + + setField(script, "lastHopAt", System.currentTimeMillis() - 15_000); + microbot.when(() -> Microbot.hopToWorld(TARGET_WORLD)).thenReturn(true); + assertTrue(call(script, "attemptEscapeHop")); + + microbot.verify(() -> Microbot.hopToWorld(TARGET_WORLD), times(2)); + assertFalse(call(script, "isEscapeHopPending")); + } + } + + @Test + void escapeHopIsDroppedOnceTheRetryWindowRunsOut() throws Exception { + ThievingScript script = script(ThievingNpc.VYRES); + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic login = mockStatic(Login.class)) { + login.when(() -> Login.getRandomWorld(anyBoolean(), any())).thenReturn(TARGET_WORLD); + microbot.when(() -> Microbot.hopToWorld(TARGET_WORLD)).thenReturn(false); + + invoke(script, "requestEscapeHop"); + setField(script, "escapeHopUntil", System.currentTimeMillis() - 1); + + assertTrue(call(script, "attemptEscapeHop")); + assertFalse(call(script, "isEscapeHopPending")); + } + } + + @Test + void escapeHopIsDroppedWhenEscapingIsTurnedOff() throws Exception { + ThievingConfig config = mock(ThievingConfig.class); + when(config.THIEVING_NPC()).thenReturn(ThievingNpc.VYRES); + when(config.escapeAttacking()).thenReturn(false); + ThievingScript script = new ThievingScript(config, mock(ThievingPlugin.class)); + try (MockedStatic microbot = mockStatic(Microbot.class)) { + invoke(script, "requestEscapeHop"); + assertTrue(call(script, "attemptEscapeHop")); + assertFalse(call(script, "isEscapeHopPending")); + microbot.verify(() -> Microbot.hopToWorld(anyInt()), never()); + } + } + + private static ThievingScript script(ThievingNpc npc) { + ThievingConfig config = mock(ThievingConfig.class); + when(config.THIEVING_NPC()).thenReturn(npc); + when(config.escapeAttacking()).thenReturn(true); + return new ThievingScript(config, mock(ThievingPlugin.class)); + } + + private static Object invoke(Object target, String name) throws Exception { + Method method = target.getClass().getDeclaredMethod(name); + method.setAccessible(true); + return method.invoke(target); + } + + private static boolean call(Object target, String name) throws Exception { + return (boolean) invoke(target, name); + } + + private static void setField(Object target, String name, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } +} From 101cb92517fbb0e49d53b8f48017f56045dff7fa Mon Sep 17 00:00:00 2001 From: chsami Date: Tue, 13 Oct 2026 14:23:49 +0200 Subject: [PATCH 4/7] fix(shootingstar): consistent star arrival radius, per-star walk-first flag, login-time world filter (#601) - Treat the star as reached at <= 6 tiles, the same radius passed to the walker, and act on a walker ARRIVED result instead of calling the walker again; a player stopped exactly 6 tiles away no longer stays in WALKING without hopping. - Key the walk-before-hop flag to the star whose hop failed, so a new star does not inherit it. - Recompute the members/F2P star filter from the client world type on the first LOGGED_IN after startup or after the login screen, instead of only once at startup when the world list may not be known. - Bump ShootingStar to 1.4.9. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../shootingstar/ShootingStarPlugin.java | 35 ++++- .../shootingstar/ShootingStarScript.java | 26 ++-- .../ShootingStarWalkBeforeHopTest.java | 79 ++++++++++- .../ShootingStarWorldFilterTest.java | 127 ++++++++++++++++++ 4 files changed, 254 insertions(+), 13 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarWorldFilterTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarPlugin.java index e857489276..c94c3049c4 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarPlugin.java @@ -34,6 +34,7 @@ import lombok.extern.slf4j.Slf4j; import net.runelite.api.ChatMessageType; import net.runelite.api.GameState; +import net.runelite.api.WorldType; import net.runelite.api.coords.WorldArea; import net.runelite.api.coords.WorldPoint; import net.runelite.api.events.ChatMessage; @@ -49,7 +50,6 @@ import net.runelite.client.plugins.microbot.shootingstar.model.Star; import net.runelite.client.plugins.microbot.shortestpath.ShortestPathPlugin; import net.runelite.client.plugins.microbot.shortestpath.pathfinder.Pathfinder; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; import net.runelite.client.task.Schedule; import net.runelite.client.ui.ClientToolbar; @@ -73,7 +73,7 @@ public class ShootingStarPlugin extends Plugin { - static final String version = "1.4.8"; + static final String version = "1.4.9"; @Getter private final List starList = new ArrayList<>(); @@ -117,6 +117,7 @@ ShootingStarConfig provideConfig(ConfigManager configManager) private boolean hideMembersWorlds; @Getter private boolean hideF2PWorlds; + private boolean worldFilterResolved; private Set blacklistedLocations = new HashSet<>(); @@ -124,8 +125,8 @@ ShootingStarConfig provideConfig(ConfigManager configManager) protected void startUp() throws AWTException { displayAsMinutes = config.isDisplayAsMinutes(); - hideMembersWorlds = !Rs2Player.isInMemberWorld(); - hideF2PWorlds = Rs2Player.isInMemberWorld(); + worldFilterResolved = Microbot.isLoggedIn(); + applyWorldFilter(worldFilterResolved && isOnMembersWorld()); loadBlacklistedLocations(); fetchStars(); @@ -142,6 +143,7 @@ protected void shutDown() removePanel(); starList.clear(); lastWorld = -1; + worldFilterResolved = false; apiTickCounter.set(0); overlayManager.remove(shootingStarOverlay); } @@ -213,8 +215,22 @@ public void tick() { @Subscribe public void onGameStateChanged(GameStateChanged event) { + if (event.getGameState() == GameState.LOGIN_SCREEN) + { + worldFilterResolved = false; + return; + } + if (event.getGameState() == GameState.LOGGED_IN) { + if (!worldFilterResolved) + { + worldFilterResolved = true; + applyWorldFilter(isOnMembersWorld()); + updateHiddenStars(); + updatePanelList(true); + } + if (lastWorld == -1) { lastWorld = Microbot.getClient().getWorld(); @@ -243,6 +259,17 @@ public void onChatMessage(ChatMessage event) { } } + private void applyWorldFilter(boolean onMembersWorld) + { + hideMembersWorlds = !onMembersWorld; + hideF2PWorlds = onMembersWorld; + } + + private boolean isOnMembersWorld() + { + return Microbot.getClient().getWorldType().contains(WorldType.MEMBERS); + } + public void fetchStars() { List latestStars = shootingStarApiClient.getStarData(config.getProvider()); diff --git a/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarScript.java b/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarScript.java index cf202d3528..065564bd00 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarScript.java @@ -39,6 +39,7 @@ @Slf4j public class ShootingStarScript extends Script { + static final int STAR_ARRIVAL_DISTANCE = 6; public static ShootingStarState state; private final ShootingStarPlugin plugin; private final ShootingStarConfig config; @@ -47,7 +48,7 @@ public class ShootingStarScript extends Script private Star currentStar; private boolean hasEquipment = false; private boolean hasInventory = false; - private boolean walkBeforeHop = false; + private Star walkBeforeHopStar; public Instant startTime; @@ -75,7 +76,7 @@ public boolean run() initialPlayerLocation = null; hasEquipment = false; hasInventory = false; - walkBeforeHop = false; + walkBeforeHopStar = null; Rs2Antiban.resetAntibanSettings(); applyAntiBanSettings(); Rs2Antiban.setActivity(Activity.GENERAL_MINING); @@ -312,7 +313,7 @@ void handleWalking(Star star) toggleLockState(true); boolean isOnStarWorld = Rs2Player.getWorld() == star.getWorld(); - boolean isNearShootingStar = Rs2Player.getWorldLocation().distanceTo(star.getShootingStarLocation().getWorldPoint()) < 6; + boolean isNearShootingStar = Rs2Player.getWorldLocation().distanceTo(star.getShootingStarLocation().getWorldPoint()) <= STAR_ARRIVAL_DISTANCE; if (!isOnStarWorld && (isNearShootingStar || !shouldWalkBeforeHop(star))) { @@ -322,15 +323,24 @@ void handleWalking(Star star) if (!isNearShootingStar) { - WalkerState walkerState = Rs2Walker.walkWithState(star.getShootingStarLocation().getWorldPoint(), 6); + WalkerState walkerState = Rs2Walker.walkWithState(star.getShootingStarLocation().getWorldPoint(), STAR_ARRIVAL_DISTANCE); if (walkerState == WalkerState.UNREACHABLE) { log.debug("Walker State is {}, removing star from list", walkerState); plugin.removeStar(plugin.getSelectedStar()); plugin.updatePanelList(true); state = ShootingStarState.WAITING_FOR_STAR; + return; + } + if (walkerState != WalkerState.ARRIVED) + { + return; + } + if (!isOnStarWorld) + { + hopToStarWorld(star, true); + return; } - return; } initialPlayerLocation = Rs2Player.getWorldLocation(); @@ -344,7 +354,7 @@ private boolean shouldWalkBeforeHop(Star star) { return false; } - return walkBeforeHop || Rs2Player.isInMemberWorld(); + return star.equals(walkBeforeHopStar) || Rs2Player.isInMemberWorld(); } private void hopToStarWorld(Star star, boolean isNearShootingStar) @@ -356,14 +366,14 @@ private void hopToStarWorld(Star star, boolean isNearShootingStar) if (Microbot.hopToWorld(star.getWorld())) { - walkBeforeHop = false; + walkBeforeHopStar = null; return; } if (!star.isMemberWorld() && !isNearShootingStar) { log.info("Hop to non-members world {} failed, walking to the star before hopping again", star.getWorld()); - walkBeforeHop = true; + walkBeforeHopStar = star; } } diff --git a/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarWalkBeforeHopTest.java b/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarWalkBeforeHopTest.java index ea010a61c3..a01dc63e3d 100644 --- a/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarWalkBeforeHopTest.java +++ b/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarWalkBeforeHopTest.java @@ -27,6 +27,7 @@ class ShootingStarWalkBeforeHopTest { private static final int MEMBERS_WORLD = 302; private static final int STAR_WORLD = 301; + private static final int OTHER_STAR_WORLD = 308; private static final ShootingStarLocation LOCATION = ShootingStarLocation.DRAYNOR_VILLAGE; private static final WorldPoint FAR_AWAY = new WorldPoint(2605, 3092, 0); private static final WorldPoint AT_STAR = LOCATION.getWorldPoint().dx(2); @@ -135,6 +136,77 @@ void onTheStarWorldNextToTheStarStartsMining() assertEquals(ShootingStarState.MINING, ShootingStarScript.state); } + @Test + void standingExactlyOnTheWalkerArrivalDistanceHopsInsteadOfWalkingAgain() + { + standAt(LOCATION.getWorldPoint().dx(ShootingStarScript.STAR_ARRIVAL_DISTANCE), true); + walker.when(() -> Rs2Walker.walkWithState(any(WorldPoint.class), anyInt())).thenReturn(WalkerState.ARRIVED); + microbot.when(() -> Microbot.hopToWorld(STAR_WORLD)).thenReturn(true); + + script.handleWalking(star(false)); + + microbot.verify(() -> Microbot.hopToWorld(STAR_WORLD)); + walker.verify(() -> Rs2Walker.walkWithState(any(WorldPoint.class), anyInt()), never()); + } + + @Test + void standingExactlyOnTheWalkerArrivalDistanceOnTheStarWorldStartsMining() + { + standAt(LOCATION.getWorldPoint().dx(ShootingStarScript.STAR_ARRIVAL_DISTANCE), false); + player.when(Rs2Player::getWorld).thenReturn(STAR_WORLD); + walker.when(() -> Rs2Walker.walkWithState(any(WorldPoint.class), anyInt())).thenReturn(WalkerState.ARRIVED); + + script.handleWalking(star(false)); + + assertEquals(ShootingStarState.MINING, ShootingStarScript.state); + } + + @Test + void walkerReportingArrivedOutsideTheRadiusHopsInsteadOfLoopingOnTheWalker() + { + standAt(LOCATION.getWorldPoint().dx(ShootingStarScript.STAR_ARRIVAL_DISTANCE + 1), true); + walker.when(() -> Rs2Walker.walkWithState(any(WorldPoint.class), anyInt())).thenReturn(WalkerState.ARRIVED); + microbot.when(() -> Microbot.hopToWorld(STAR_WORLD)).thenReturn(true); + + script.handleWalking(star(false)); + + walker.verify(() -> Rs2Walker.walkWithState(LOCATION.getWorldPoint(), ShootingStarScript.STAR_ARRIVAL_DISTANCE), times(1)); + microbot.verify(() -> Microbot.hopToWorld(STAR_WORLD)); + } + + @Test + void walkBeforeHopResetsAfterASuccessfulHop() + { + standAt(FAR_AWAY, false); + microbot.when(() -> Microbot.hopToWorld(STAR_WORLD)).thenReturn(false); + Star star = star(false); + script.handleWalking(star); + + standAt(AT_STAR, false); + microbot.when(() -> Microbot.hopToWorld(STAR_WORLD)).thenReturn(true); + script.handleWalking(star); + + standAt(FAR_AWAY, false); + script.handleWalking(star); + + microbot.verify(() -> Microbot.hopToWorld(STAR_WORLD), times(3)); + walker.verify(() -> Rs2Walker.walkWithState(any(WorldPoint.class), anyInt()), never()); + } + + @Test + void walkBeforeHopFromAFailedHopDoesNotCarryOverToTheNextStar() + { + standAt(FAR_AWAY, false); + microbot.when(() -> Microbot.hopToWorld(anyInt())).thenReturn(false); + script.handleWalking(star(false)); + + Star nextStar = star(false, OTHER_STAR_WORLD); + script.handleWalking(nextStar); + + microbot.verify(() -> Microbot.hopToWorld(OTHER_STAR_WORLD)); + walker.verify(() -> Rs2Walker.walkWithState(any(WorldPoint.class), anyInt()), never()); + } + private void standAt(WorldPoint location, boolean onMembersWorld) { player.when(Rs2Player::getWorldLocation).thenReturn(location); @@ -142,9 +214,14 @@ private void standAt(WorldPoint location, boolean onMembersWorld) } private static Star star(boolean membersWorld) + { + return star(membersWorld, STAR_WORLD); + } + + private static Star star(boolean membersWorld, int world) { Star star = mock(Star.class); - when(star.getWorld()).thenReturn(STAR_WORLD); + when(star.getWorld()).thenReturn(world); when(star.isMemberWorld()).thenReturn(membersWorld); when(star.getShootingStarLocation()).thenReturn(LOCATION); return star; diff --git a/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarWorldFilterTest.java b/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarWorldFilterTest.java new file mode 100644 index 0000000000..561cd86038 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarWorldFilterTest.java @@ -0,0 +1,127 @@ +package net.runelite.client.plugins.microbot.shootingstar; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.EnumSet; +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.api.WorldType; +import net.runelite.api.events.GameStateChanged; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.shootingstar.enums.ShootingStarLocation; +import net.runelite.client.plugins.microbot.shootingstar.model.Star; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ShootingStarWorldFilterTest +{ + private static final int MEMBERS_WORLD = 302; + private static final int F2P_WORLD = 301; + + private MockedStatic microbot; + private Client client; + private ShootingStarPlugin plugin; + + @BeforeEach + void setUp() throws Exception + { + client = mock(Client.class); + microbot = mockStatic(Microbot.class); + microbot.when(Microbot::getClient).thenReturn(client); + + ShootingStarApiClient apiClient = mock(ShootingStarApiClient.class); + when(apiClient.getStarData(any())).thenReturn(Collections.emptyList()); + + plugin = new ShootingStarPlugin(); + inject("config", mock(ShootingStarConfig.class)); + inject("shootingStarApiClient", apiClient); + inject("panel", mock(ShootingStarPanel.class)); + inject("hideMembersWorlds", true); + inject("hideF2PWorlds", false); + } + + @AfterEach + void tearDown() + { + microbot.close(); + } + + @Test + void firstLoginOnAMembersWorldShowsMembersStarsAndHidesF2pStars() + { + Star membersStar = star(true); + Star f2pStar = star(false); + plugin.getStarList().add(membersStar); + plugin.getStarList().add(f2pStar); + + logInTo(MEMBERS_WORLD, EnumSet.of(WorldType.MEMBERS)); + + assertFalse(plugin.isHideMembersWorlds()); + assertTrue(plugin.isHideF2PWorlds()); + verify(membersStar).setHidden(false); + verify(f2pStar).setHidden(true); + } + + @Test + void hoppingToAnF2pWorldAfterLoginKeepsTheMembersFilter() + { + logInTo(MEMBERS_WORLD, EnumSet.of(WorldType.MEMBERS)); + + fire(GameState.HOPPING); + logInTo(F2P_WORLD, EnumSet.noneOf(WorldType.class)); + + assertFalse(plugin.isHideMembersWorlds()); + assertTrue(plugin.isHideF2PWorlds()); + } + + @Test + void returningToTheLoginScreenRecomputesTheFilterOnTheNextLogin() + { + logInTo(MEMBERS_WORLD, EnumSet.of(WorldType.MEMBERS)); + + fire(GameState.LOGIN_SCREEN); + logInTo(F2P_WORLD, EnumSet.noneOf(WorldType.class)); + + assertTrue(plugin.isHideMembersWorlds()); + assertFalse(plugin.isHideF2PWorlds()); + } + + private void logInTo(int world, EnumSet worldTypes) + { + when(client.getWorld()).thenReturn(world); + when(client.getWorldType()).thenReturn(worldTypes); + fire(GameState.LOGGED_IN); + } + + private void fire(GameState gameState) + { + GameStateChanged event = new GameStateChanged(); + event.setGameState(gameState); + plugin.onGameStateChanged(event); + } + + private void inject(String fieldName, Object value) throws Exception + { + Field field = ShootingStarPlugin.class.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(plugin, value); + } + + private static Star star(boolean membersWorld) + { + Star star = mock(Star.class); + when(star.isMemberWorld()).thenReturn(membersWorld); + when(star.getShootingStarLocation()).thenReturn(ShootingStarLocation.DRAYNOR_VILLAGE); + return star; + } +} From e371f11f3f62486c6906050851802186a24f9ca4 Mon Sep 17 00:00:00 2001 From: chsami Date: Tue, 13 Oct 2026 18:13:02 +0200 Subject: [PATCH 5/7] fix(hub): eat during a pending thieving escape hop; stop penalising worlds for local autochin hop blocks (#602) ThievingScript returned ESCAPE for the whole 60s escape-hop window, ahead of EAT, so poison or residual damage went unhealed. Low HP with food in the inventory now returns EAT first and leaves the hop pending. AutoChinScript counted every hopToWorld false as a refusal by the target world. A refusal is now only counted and blacklisted when the local hop preconditions held before and after the call (logged in, not in combat or the 10s lockout, no bank/shop/dialogue, not animating). Otherwise the hop waits and retries without penalising the world, and a click-to- continue dialogue is dismissed. Thieving 2.1.5, AutoHunter 1.5.3. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../microhunter/AutoHunterPlugin.java | 2 +- .../microhunter/scripts/AutoChinScript.java | 45 +++++++-- .../microbot/thieving/ThievingPlugin.java | 2 +- .../microbot/thieving/ThievingScript.java | 21 +++- .../scripts/AutoChinHopResultTest.java | 99 +++++++++++++++++++ .../thieving/ThievingEscapeHopTest.java | 64 ++++++++++++ 6 files changed, 220 insertions(+), 13 deletions(-) diff --git a/src/main/java/net/runelite/client/plugins/microbot/microhunter/AutoHunterPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/microhunter/AutoHunterPlugin.java index 970d5b3b2e..b408efb164 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/microhunter/AutoHunterPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/microhunter/AutoHunterPlugin.java @@ -31,7 +31,7 @@ @Slf4j @PluginDependency(HunterPlugin.class) public class AutoHunterPlugin extends Plugin { - public static final String version = "1.5.2"; + public static final String version = "1.5.3"; @Inject private AutoHunterConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinScript.java b/src/main/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinScript.java index 4b030d9b38..f7c4cd0cf9 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinScript.java @@ -17,11 +17,15 @@ import net.runelite.client.plugins.microbot.breakhandler.BreakHandlerScript; import net.runelite.client.plugins.microbot.util.antiban.Rs2Antiban; import net.runelite.client.plugins.microbot.util.antiban.enums.ActivityIntensity; +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.microhunter.AutoHunterConfig; import net.runelite.client.plugins.microbot.util.menu.NewMenuEntry; import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.microbot.util.player.Rs2PlayerModel; +import net.runelite.client.plugins.microbot.util.shop.Rs2Shop; import net.runelite.client.plugins.microbot.util.tile.Rs2Tile; import net.runelite.http.api.worlds.World; import net.runelite.http.api.worlds.WorldRegion; @@ -89,6 +93,7 @@ private enum Action { private static final long REJECTED_WORLD_COOLDOWN_MS = 600_000; private static final long BREAK_RECOVERY_CLEAR_MS = 1_500; private static final int MAX_WORLD_HOP_ATTEMPTS = 5; + private static final String HOP_BLOCKER_DIALOGUE = "dialogue open"; private static final EnumSet UNSAFE_WORLD_TYPES = EnumSet.of( WorldType.PVP, WorldType.BOUNTY, WorldType.PVP_ARENA, WorldType.SKILL_TOTAL, WorldType.QUEST_SPEEDRUNNING, WorldType.HIGH_RISK, WorldType.LAST_MAN_STANDING, @@ -144,6 +149,7 @@ private enum Action { private long hopStartedAt; private boolean hopRefused; private int hopRefusals; + private String hopBlocker; private boolean breakRecoveryRequested; private boolean breakRecoveryComplete; private boolean breakRecoveryLockHeld; @@ -202,6 +208,7 @@ private void resetSession() { hopStartedAt = 0; hopRefused = false; hopRefusals = 0; + hopBlocker = null; breakRecoveryRequested = false; breakRecoveryComplete = false; breakRecoveryLockHeld = false; @@ -315,6 +322,7 @@ private void beginWorldArrival(AutoHunterConfig config, int world) { hopStartedAt = 0; hopRefused = false; hopRefusals = 0; + hopBlocker = null; nearbyPlayerSeenSince = 0; if (!config.avoidOccupiedWorlds()) { worldHopAttempts = 0; @@ -437,6 +445,7 @@ private void requestAustralianWorldHop(String reason) { hopSourceWorld = Microbot.getClient().getWorld(); hopTargetWorld = target; hopRefusals = 0; + hopBlocker = null; transition(State.HOPPING_WORLD, "Hopping to Australian world " + target + " (" + reason + ")"); Microbot.log("AutoHunter occupancy: hopping " + hopSourceWorld + " -> " + target + " attempt=" + (worldHopAttempts + 1)); @@ -445,14 +454,38 @@ private void requestAustralianWorldHop(String reason) { private void issueWorldHop() { hopStartedAt = System.currentTimeMillis(); - hopRefused = !Microbot.hopToWorld(hopTargetWorld); - if (hopRefused) { - hopRefusals++; - Microbot.log("AutoHunter occupancy: hop to world " + hopTargetWorld + " refused (" - + hopRefusals + "/" + MAX_REFUSED_HOPS_PER_WORLD + ")"); + String blocker = localHopBlocker(); + if (blocker == null) { + if (Microbot.hopToWorld(hopTargetWorld)) { + hopRefused = false; + hopBlocker = null; + worldHopAttempts++; + return; + } + blocker = localHopBlocker(); + } + hopRefused = true; + if (blocker != null) { + if (!blocker.equals(hopBlocker)) { + Microbot.log("AutoHunter occupancy: hop to world " + hopTargetWorld + " waiting (" + blocker + ")"); + } + hopBlocker = blocker; + if (HOP_BLOCKER_DIALOGUE.equals(blocker)) Rs2Dialogue.clickContinue(); return; } - worldHopAttempts++; + hopBlocker = null; + hopRefusals++; + Microbot.log("AutoHunter occupancy: hop to world " + hopTargetWorld + " refused (" + + hopRefusals + "/" + MAX_REFUSED_HOPS_PER_WORLD + ")"); + } + + private String localHopBlocker() { + if (!Microbot.isLoggedIn()) return "not logged in"; + if (Rs2Player.isInCombat() || Rs2Combat.inCombat()) return "in combat"; + if (Rs2Bank.isOpen() || Rs2Shop.isOpen()) return "interface open"; + if (Rs2Dialogue.isInDialogue()) return HOP_BLOCKER_DIALOGUE; + if (Rs2Player.isAnimating()) return "animating"; + return null; } private Integer selectAustralianWorld() { diff --git a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java index 87fdb039c3..0e8848eb7c 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java @@ -34,7 +34,7 @@ ) @Slf4j public class ThievingPlugin extends Plugin { - public static final String version = "2.1.4"; + public static final String version = "2.1.5"; @Inject @Getter diff --git a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingScript.java b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingScript.java index 4343cb9e85..c44c8a54ee 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingScript.java @@ -279,15 +279,15 @@ private State getCurrentState() { return applyOverride(State.ESCAPE); } - if (isEscapeHopPending()) return applyOverride(State.ESCAPE); + if (isEscapeHopPending()) { + if (needsToEat() && hasFoodToEat()) return applyOverride(State.EAT); + return applyOverride(State.ESCAPE); + } if (!hasReqs()) return applyOverride(State.BANK); if (config.useFood()) { - boolean needToEat = Rs2Player.getHealthPercentage() <= config.hitpoints(); - boolean needToDrink = config.food() == ThievingFood.ANCIENT_BREW && !Rs2Player.hasPrayerPoints(); - - if (needToEat || needToDrink) { + if (needsToEat()) { return applyOverride(State.EAT); } @@ -626,6 +626,17 @@ public boolean run() { return true; } + private boolean needsToEat() { + if (!config.useFood()) return false; + if (Rs2Player.getHealthPercentage() <= config.hitpoints()) return true; + return config.food() == ThievingFood.ANCIENT_BREW && !Rs2Player.hasPrayerPoints(); + } + + private boolean hasFoodToEat() { + if (config.food() == ThievingFood.ANCIENT_BREW) return hasAncientBrew(); + return !Rs2Inventory.getInventoryFood().isEmpty(); + } + private boolean hasReqs() { if (System.currentTimeMillis() < startupGraceUntil) return true; boolean hasReqs = true; diff --git a/src/test/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinHopResultTest.java b/src/test/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinHopResultTest.java index b862483c6b..67c0206a47 100644 --- a/src/test/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinHopResultTest.java +++ b/src/test/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinHopResultTest.java @@ -3,11 +3,17 @@ import net.runelite.api.Client; import net.runelite.client.game.WorldService; import net.runelite.client.plugins.microbot.Microbot; +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.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.shop.Rs2Shop; import net.runelite.http.api.worlds.World; import net.runelite.http.api.worlds.WorldRegion; import net.runelite.http.api.worlds.WorldResult; import net.runelite.http.api.worlds.WorldType; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; @@ -24,6 +30,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.when; @@ -32,6 +39,27 @@ class AutoChinHopResultTest { private static final int SOURCE_WORLD = 330; private static final int TARGET_WORLD = 480; + private MockedStatic combat; + private MockedStatic bank; + private MockedStatic shop; + private MockedStatic dialogue; + + @BeforeEach + void mockLocalHopPreconditions() { + combat = mockStatic(Rs2Combat.class); + bank = mockStatic(Rs2Bank.class); + shop = mockStatic(Rs2Shop.class); + dialogue = mockStatic(Rs2Dialogue.class); + } + + @AfterEach + void closeLocalHopPreconditions() { + combat.close(); + bank.close(); + shop.close(); + dialogue.close(); + } + @Test void refusedHopIsNotCountedOrBlacklistedAndIsRetriedForTheSameWorld() throws Exception { AutoChinScript script = new AutoChinScript(); @@ -51,6 +79,7 @@ void refusedHopIsNotCountedOrBlacklistedAndIsRetriedForTheSameWorld() throws Exc MockedStatic player = mockStatic(Rs2Player.class)) { microbot.when(Microbot::getClient).thenReturn(client); microbot.when(Microbot::getWorldService).thenReturn(worldService); + microbot.when(Microbot::isLoggedIn).thenReturn(true); player.when(Rs2Player::isMember).thenReturn(true); microbot.when(() -> Microbot.hopToWorld(TARGET_WORLD)).thenReturn(false); @@ -85,6 +114,7 @@ void persistentRefusalBlacklistsTheTargetAndMovesToAnotherWorld() throws Excepti MockedStatic player = mockStatic(Rs2Player.class)) { microbot.when(Microbot::getClient).thenReturn(client); microbot.when(Microbot::getWorldService).thenReturn(worldService); + microbot.when(Microbot::isLoggedIn).thenReturn(true); player.when(Rs2Player::isMember).thenReturn(true); microbot.when(() -> Microbot.hopToWorld(anyInt())).thenReturn(false); @@ -117,6 +147,7 @@ void persistentRefusalOnEveryWorldStopsAtTheAttemptCap() throws Exception { MockedStatic player = mockStatic(Rs2Player.class)) { microbot.when(Microbot::getClient).thenReturn(client); microbot.when(Microbot::getWorldService).thenReturn(worldService); + microbot.when(Microbot::isLoggedIn).thenReturn(true); player.when(Rs2Player::isMember).thenReturn(true); microbot.when(() -> Microbot.hopToWorld(anyInt())).thenReturn(false); @@ -132,6 +163,74 @@ void persistentRefusalOnEveryWorldStopsAtTheAttemptCap() throws Exception { } } + @Test + void locallyBlockedHopWaitsWithoutBlacklistingOrCountingTheTarget() throws Exception { + AutoChinScript script = new AutoChinScript(); + Client client = mock(Client.class); + when(client.getWorld()).thenReturn(SOURCE_WORLD); + WorldService worldService = australianWorlds(480, 481, 482, 483, 484, 485, 486, 487); + + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic player = mockStatic(Rs2Player.class)) { + microbot.when(Microbot::getClient).thenReturn(client); + microbot.when(Microbot::getWorldService).thenReturn(worldService); + microbot.when(Microbot::isLoggedIn).thenReturn(true); + player.when(Rs2Player::isMember).thenReturn(true); + player.when(Rs2Player::isInCombat).thenReturn(true); + microbot.when(() -> Microbot.hopToWorld(anyInt())).thenReturn(false); + + invoke(script, "requestAustralianWorldHop", "occupied"); + int target = (int) field(script, "hopTargetWorld"); + for (int i = 0; i < 40; i++) { + setField(script, "hopStartedAt", System.currentTimeMillis() - 3_000); + invoke(script, "monitorWorldHop"); + } + + microbot.verify(() -> Microbot.hopToWorld(anyInt()), never()); + assertEquals("HOPPING_WORLD", field(script, "currentState").toString()); + assertEquals(target, field(script, "hopTargetWorld")); + assertEquals(0, field(script, "worldHopAttempts")); + assertEquals(0, field(script, "hopRefusals")); + assertTrue(((Map) field(script, "rejectedWorlds")).isEmpty()); + + player.when(Rs2Player::isInCombat).thenReturn(false); + microbot.when(() -> Microbot.hopToWorld(target)).thenReturn(true); + setField(script, "hopStartedAt", System.currentTimeMillis() - 3_000); + invoke(script, "monitorWorldHop"); + + microbot.verify(() -> Microbot.hopToWorld(target), times(1)); + assertEquals(1, field(script, "worldHopAttempts")); + assertTrue(((Map) field(script, "rejectedWorlds")).isEmpty()); + } + } + + @Test + void refusalCausedByALocalBlockThatAppearedDuringTheHopIsNotCounted() throws Exception { + AutoChinScript script = new AutoChinScript(); + Client client = mock(Client.class); + when(client.getWorld()).thenReturn(SOURCE_WORLD); + WorldService worldService = australianWorlds(TARGET_WORLD); + + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic player = mockStatic(Rs2Player.class)) { + microbot.when(Microbot::getClient).thenReturn(client); + microbot.when(Microbot::getWorldService).thenReturn(worldService); + microbot.when(Microbot::isLoggedIn).thenReturn(true); + player.when(Rs2Player::isMember).thenReturn(true); + dialogue.when(Rs2Dialogue::isInDialogue).thenReturn(false, true); + microbot.when(() -> Microbot.hopToWorld(TARGET_WORLD)).thenReturn(false); + + invoke(script, "requestAustralianWorldHop", "occupied"); + + microbot.verify(() -> Microbot.hopToWorld(TARGET_WORLD), times(1)); + dialogue.verify(Rs2Dialogue::clickContinue, times(1)); + assertEquals(0, field(script, "hopRefusals")); + assertEquals(0, field(script, "worldHopAttempts")); + assertTrue(((Map) field(script, "rejectedWorlds")).isEmpty()); + assertEquals("HOPPING_WORLD", field(script, "currentState").toString()); + } + } + private static WorldService australianWorlds(int... ids) { List worlds = new ArrayList<>(); for (int id : ids) { diff --git a/src/test/java/net/runelite/client/plugins/microbot/thieving/ThievingEscapeHopTest.java b/src/test/java/net/runelite/client/plugins/microbot/thieving/ThievingEscapeHopTest.java index 513a0c57a5..a40a8c88e0 100644 --- a/src/test/java/net/runelite/client/plugins/microbot/thieving/ThievingEscapeHopTest.java +++ b/src/test/java/net/runelite/client/plugins/microbot/thieving/ThievingEscapeHopTest.java @@ -1,14 +1,23 @@ package net.runelite.client.plugins.microbot.thieving; +import net.runelite.api.Client; +import net.runelite.client.callback.ClientThread; import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.thieving.enums.ThievingFood; import net.runelite.client.plugins.microbot.thieving.enums.ThievingNpc; +import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; +import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.microbot.util.security.Login; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.List; +import java.util.Optional; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -79,6 +88,61 @@ void escapeHopIsDroppedWhenEscapingIsTurnedOff() throws Exception { } } + @Test + void lowHitpointsEatBeforeAPendingEscapeHopAndTheHopStaysPending() throws Exception { + ThievingScript script = foodScript(); + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic player = mockStatic(Rs2Player.class); + MockedStatic inventory = mockStatic(Rs2Inventory.class)) { + stubIdleWorld(microbot); + inventory.when(Rs2Inventory::getInventoryFood).thenReturn(List.of(mock(Rs2ItemModel.class))); + player.when(Rs2Player::getHealthPercentage).thenReturn(30.0); + + invoke(script, "requestEscapeHop"); + + assertEquals(State.EAT, invoke(script, "getCurrentState")); + assertTrue(call(script, "isEscapeHopPending")); + + player.when(Rs2Player::getHealthPercentage).thenReturn(80.0); + assertEquals(State.ESCAPE, invoke(script, "getCurrentState")); + assertTrue(call(script, "isEscapeHopPending")); + } + } + + @Test + void pendingEscapeHopKeepsEscapingWhenThereIsNoFoodToEat() throws Exception { + ThievingScript script = foodScript(); + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic player = mockStatic(Rs2Player.class); + MockedStatic inventory = mockStatic(Rs2Inventory.class)) { + stubIdleWorld(microbot); + inventory.when(Rs2Inventory::getInventoryFood).thenReturn(List.of()); + player.when(Rs2Player::getHealthPercentage).thenReturn(30.0); + + invoke(script, "requestEscapeHop"); + + assertEquals(State.ESCAPE, invoke(script, "getCurrentState")); + assertTrue(call(script, "isEscapeHopPending")); + } + } + + private static ThievingScript foodScript() { + ThievingConfig config = mock(ThievingConfig.class); + when(config.THIEVING_NPC()).thenReturn(ThievingNpc.VYRES); + when(config.escapeAttacking()).thenReturn(true); + when(config.useFood()).thenReturn(true); + when(config.food()).thenReturn(ThievingFood.SHARK); + when(config.hitpoints()).thenReturn(50); + return new ThievingScript(config, mock(ThievingPlugin.class)); + } + + private static void stubIdleWorld(MockedStatic microbot) { + ClientThread clientThread = mock(ClientThread.class); + when(clientThread.runOnClientThreadOptional(any())).thenReturn(Optional.empty()); + microbot.when(Microbot::getClientThread).thenReturn(clientThread); + microbot.when(Microbot::getClient).thenReturn(mock(Client.class)); + } + private static ThievingScript script(ThievingNpc npc) { ThievingConfig config = mock(ThievingConfig.class); when(config.THIEVING_NPC()).thenReturn(npc); From 713ca5d83ec49b49d55cc3961e58622ae5735e01 Mon Sep 17 00:00:00 2001 From: chsami Date: Tue, 13 Oct 2026 18:27:24 +0200 Subject: [PATCH 6/7] fix(shootingstar): key the world filter on membership, client-thread world reads, thread-safe star list (#603) - shouldWalkBeforeHop reads the client world type on the client thread instead of the HTTP world list - default star filter follows account membership (ACCOUNT_CREDIT varp), falling back to the world type, and upgrades to the members filter when the varp arrives after login, so a relog on an F2P world keeps members stars - starList is a CopyOnWriteArrayList; new stars are added with addIfAbsent - on the star world a walker ARRIVED only enters MINING when the player is within the arrival distance - startUp reads login state, membership and world type on the client thread - bump ShootingStar to 1.4.10 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../shootingstar/ShootingStarPlugin.java | 58 +++++-- .../shootingstar/ShootingStarScript.java | 19 ++- .../ShootingStarWalkBeforeHopTest.java | 86 ++++++++++- .../ShootingStarWorldFilterTest.java | 145 +++++++++++++++++- 4 files changed, 290 insertions(+), 18 deletions(-) diff --git a/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarPlugin.java index c94c3049c4..45d9ed1082 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarPlugin.java @@ -22,6 +22,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import javax.inject.Inject; @@ -39,6 +40,8 @@ import net.runelite.api.coords.WorldPoint; import net.runelite.api.events.ChatMessage; import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.VarbitChanged; +import net.runelite.api.gameval.VarPlayerID; import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.Subscribe; import net.runelite.client.events.ConfigChanged; @@ -73,10 +76,10 @@ public class ShootingStarPlugin extends Plugin { - static final String version = "1.4.9"; + static final String version = "1.4.10"; @Getter - private final List starList = new ArrayList<>(); + private final CopyOnWriteArrayList starList = new CopyOnWriteArrayList<>(); @Inject private ShootingStarScript shootingStarScript; @@ -114,10 +117,10 @@ ShootingStarConfig provideConfig(ConfigManager configManager) @Getter private boolean displayAsMinutes; @Getter - private boolean hideMembersWorlds; + private volatile boolean hideMembersWorlds; @Getter - private boolean hideF2PWorlds; - private boolean worldFilterResolved; + private volatile boolean hideF2PWorlds; + private volatile boolean worldFilterResolved; private Set blacklistedLocations = new HashSet<>(); @@ -125,8 +128,10 @@ ShootingStarConfig provideConfig(ConfigManager configManager) protected void startUp() throws AWTException { displayAsMinutes = config.isDisplayAsMinutes(); - worldFilterResolved = Microbot.isLoggedIn(); - applyWorldFilter(worldFilterResolved && isOnMembersWorld()); + Optional membersAccount = Microbot.getClientThread().runOnClientThreadOptional( + () -> Microbot.isLoggedIn() ? isMembersAccount() : null); + worldFilterResolved = membersAccount.isPresent(); + applyWorldFilter(membersAccount.orElse(false)); loadBlacklistedLocations(); fetchStars(); @@ -226,7 +231,7 @@ public void onGameStateChanged(GameStateChanged event) if (!worldFilterResolved) { worldFilterResolved = true; - applyWorldFilter(isOnMembersWorld()); + applyWorldFilter(isMembersAccount()); updateHiddenStars(); updatePanelList(true); } @@ -249,6 +254,24 @@ public void onGameStateChanged(GameStateChanged event) } } + @Subscribe + public void onVarbitChanged(VarbitChanged event) + { + if (event.getVarpId() != VarPlayerID.ACCOUNT_CREDIT || event.getValue() <= 0) + { + return; + } + + if (!worldFilterResolved || !hideMembersWorlds) + { + return; + } + + applyWorldFilter(true); + updateHiddenStars(); + updatePanelList(true); + } + @Subscribe public void onChatMessage(ChatMessage event) { if (event.getType() != ChatMessageType.GAMEMESSAGE) return; @@ -259,13 +282,18 @@ public void onChatMessage(ChatMessage event) { } } - private void applyWorldFilter(boolean onMembersWorld) + private void applyWorldFilter(boolean membersAccount) { - hideMembersWorlds = !onMembersWorld; - hideF2PWorlds = onMembersWorld; + hideMembersWorlds = !membersAccount; + hideF2PWorlds = membersAccount; } - private boolean isOnMembersWorld() + private static boolean isMembersAccount() + { + return Microbot.getClient().getVarpValue(VarPlayerID.ACCOUNT_CREDIT) > 0 || isOnMembersWorld(); + } + + static boolean isOnMembersWorld() { return Microbot.getClient().getWorldType().contains(WorldType.MEMBERS); } @@ -291,8 +319,10 @@ public void fetchStars() } // If oldStar not found, add new star into the list - starList.add(star); - fullUpdate = true; + if (starList.addIfAbsent(star)) + { + fullUpdate = true; + } } if (fullUpdate) { diff --git a/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarScript.java b/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarScript.java index 065564bd00..5b8f02c5a2 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarScript.java @@ -313,7 +313,7 @@ void handleWalking(Star star) toggleLockState(true); boolean isOnStarWorld = Rs2Player.getWorld() == star.getWorld(); - boolean isNearShootingStar = Rs2Player.getWorldLocation().distanceTo(star.getShootingStarLocation().getWorldPoint()) <= STAR_ARRIVAL_DISTANCE; + boolean isNearShootingStar = isNearStar(star); if (!isOnStarWorld && (isNearShootingStar || !shouldWalkBeforeHop(star))) { @@ -341,6 +341,11 @@ void handleWalking(Star star) hopToStarWorld(star, true); return; } + if (!isNearStar(star)) + { + log.debug("Walker reported {} but the player is still more than {} tiles from the star, retrying", walkerState, STAR_ARRIVAL_DISTANCE); + return; + } } initialPlayerLocation = Rs2Player.getWorldLocation(); @@ -348,13 +353,23 @@ void handleWalking(Star star) state = ShootingStarState.MINING; } + private static boolean isNearStar(Star star) + { + return Rs2Player.getWorldLocation().distanceTo(star.getShootingStarLocation().getWorldPoint()) <= STAR_ARRIVAL_DISTANCE; + } + private boolean shouldWalkBeforeHop(Star star) { if (star.isMemberWorld()) { return false; } - return star.equals(walkBeforeHopStar) || Rs2Player.isInMemberWorld(); + return star.equals(walkBeforeHopStar) || isOnMembersWorld(); + } + + private static boolean isOnMembersWorld() + { + return Microbot.getClientThread().runOnClientThreadOptional(ShootingStarPlugin::isOnMembersWorld).orElse(false); } private void hopToStarWorld(Star star, boolean isNearShootingStar) diff --git a/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarWalkBeforeHopTest.java b/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarWalkBeforeHopTest.java index a01dc63e3d..0fab6a8876 100644 --- a/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarWalkBeforeHopTest.java +++ b/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarWalkBeforeHopTest.java @@ -1,6 +1,12 @@ package net.runelite.client.plugins.microbot.shootingstar; +import java.util.EnumSet; +import java.util.Optional; +import java.util.concurrent.Callable; +import net.runelite.api.Client; +import net.runelite.api.WorldType; import net.runelite.api.coords.WorldPoint; +import net.runelite.client.callback.ClientThread; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.shootingstar.enums.ShootingStarLocation; import net.runelite.client.plugins.microbot.shootingstar.enums.ShootingStarState; @@ -17,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; @@ -36,6 +43,8 @@ class ShootingStarWalkBeforeHopTest private MockedStatic player; private MockedStatic walker; private ShootingStarScript script; + private Client client; + private boolean onClientThread; @BeforeEach void setUp() @@ -43,6 +52,21 @@ void setUp() microbot = mockStatic(Microbot.class); player = mockStatic(Rs2Player.class); walker = mockStatic(Rs2Walker.class); + client = mock(Client.class); + ClientThread clientThread = mock(ClientThread.class); + when(clientThread.runOnClientThreadOptional(any())).thenAnswer(invocation -> { + onClientThread = true; + try + { + return Optional.ofNullable(((Callable) invocation.getArgument(0)).call()); + } + finally + { + onClientThread = false; + } + }); + microbot.when(Microbot::getClient).thenReturn(client); + microbot.when(Microbot::getClientThread).thenReturn(clientThread); walker.when(() -> Rs2Walker.walkWithState(any(WorldPoint.class), anyInt())).thenReturn(WalkerState.MOVING); player.when(Rs2Player::getWorld).thenReturn(MEMBERS_WORLD); ShootingStarPlugin plugin = mock(ShootingStarPlugin.class); @@ -174,6 +198,59 @@ void walkerReportingArrivedOutsideTheRadiusHopsInsteadOfLoopingOnTheWalker() microbot.verify(() -> Microbot.hopToWorld(STAR_WORLD)); } + @Test + void membersWorldIsReadFromTheClientWorldTypeNotTheHttpWorldList() + { + standAt(FAR_AWAY, true); + player.when(Rs2Player::isInMemberWorld).thenReturn(false); + + script.handleWalking(star(false)); + + microbot.verify(() -> Microbot.hopToWorld(anyInt()), never()); + walker.verify(() -> Rs2Walker.walkWithState(LOCATION.getWorldPoint(), ShootingStarScript.STAR_ARRIVAL_DISTANCE)); + } + + @Test + void f2pWorldTypeHopsEvenWhenTheHttpWorldListSaysMembers() + { + standAt(FAR_AWAY, false); + player.when(Rs2Player::isInMemberWorld).thenReturn(true); + microbot.when(() -> Microbot.hopToWorld(STAR_WORLD)).thenReturn(true); + + script.handleWalking(star(false)); + + microbot.verify(() -> Microbot.hopToWorld(STAR_WORLD)); + walker.verify(() -> Rs2Walker.walkWithState(any(WorldPoint.class), anyInt()), never()); + } + + @Test + void walkerArrivedOnTheStarWorldFarFromTheStarKeepsWalkingInsteadOfMining() + { + standAt(FAR_AWAY, false); + player.when(Rs2Player::getWorld).thenReturn(STAR_WORLD); + walker.when(() -> Rs2Walker.walkWithState(any(WorldPoint.class), anyInt())).thenReturn(WalkerState.ARRIVED); + + script.handleWalking(star(false)); + + assertEquals(ShootingStarState.WALKING, ShootingStarScript.state); + microbot.verify(() -> Microbot.hopToWorld(anyInt()), never()); + } + + @Test + void walkerArrivedOnTheStarWorldNextToTheStarStartsMining() + { + standAt(FAR_AWAY, false); + player.when(Rs2Player::getWorld).thenReturn(STAR_WORLD); + walker.when(() -> Rs2Walker.walkWithState(any(WorldPoint.class), anyInt())).thenAnswer(invocation -> { + player.when(Rs2Player::getWorldLocation).thenReturn(AT_STAR); + return WalkerState.ARRIVED; + }); + + script.handleWalking(star(false)); + + assertEquals(ShootingStarState.MINING, ShootingStarScript.state); + } + @Test void walkBeforeHopResetsAfterASuccessfulHop() { @@ -210,7 +287,14 @@ void walkBeforeHopFromAFailedHopDoesNotCarryOverToTheNextStar() private void standAt(WorldPoint location, boolean onMembersWorld) { player.when(Rs2Player::getWorldLocation).thenReturn(location); - player.when(Rs2Player::isInMemberWorld).thenReturn(onMembersWorld); + EnumSet worldTypes = onMembersWorld ? EnumSet.of(WorldType.MEMBERS) : EnumSet.noneOf(WorldType.class); + doAnswer(invocation -> { + if (!onClientThread) + { + throw new AssertionError("world type read off the client thread"); + } + return worldTypes; + }).when(client).getWorldType(); } private static Star star(boolean membersWorld) diff --git a/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarWorldFilterTest.java b/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarWorldFilterTest.java index 561cd86038..cb162af8e8 100644 --- a/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarWorldFilterTest.java +++ b/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarWorldFilterTest.java @@ -3,10 +3,16 @@ import java.lang.reflect.Field; import java.util.Collections; import java.util.EnumSet; +import java.util.Optional; +import java.util.concurrent.Callable; import net.runelite.api.Client; import net.runelite.api.GameState; import net.runelite.api.WorldType; import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.VarbitChanged; +import net.runelite.api.gameval.VarPlayerID; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.config.ConfigManager; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.shootingstar.enums.ShootingStarLocation; import net.runelite.client.plugins.microbot.shootingstar.model.Star; @@ -15,9 +21,13 @@ import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.verify; @@ -27,10 +37,12 @@ class ShootingStarWorldFilterTest { private static final int MEMBERS_WORLD = 302; private static final int F2P_WORLD = 301; + private static final int MEMBERSHIP_DAYS = 30; private MockedStatic microbot; private Client client; private ShootingStarPlugin plugin; + private boolean onClientThread; @BeforeEach void setUp() throws Exception @@ -42,9 +54,25 @@ void setUp() throws Exception ShootingStarApiClient apiClient = mock(ShootingStarApiClient.class); when(apiClient.getStarData(any())).thenReturn(Collections.emptyList()); + ClientThread clientThread = mock(ClientThread.class); + when(clientThread.runOnClientThreadOptional(any())).thenAnswer(invocation -> { + onClientThread = true; + try + { + return Optional.ofNullable(((Callable) invocation.getArgument(0)).call()); + } + finally + { + onClientThread = false; + } + }); + microbot.when(Microbot::getClientThread).thenReturn(clientThread); + microbot.when(Microbot::getConfigManager).thenReturn(mock(ConfigManager.class)); + plugin = new ShootingStarPlugin(); inject("config", mock(ShootingStarConfig.class)); inject("shootingStarApiClient", apiClient); + inject("shootingStarScript", mock(ShootingStarScript.class)); inject("panel", mock(ShootingStarPanel.class)); inject("hideMembersWorlds", true); inject("hideF2PWorlds", false); @@ -85,7 +113,7 @@ void hoppingToAnF2pWorldAfterLoginKeepsTheMembersFilter() } @Test - void returningToTheLoginScreenRecomputesTheFilterOnTheNextLogin() + void returningToTheLoginScreenWithUnknownMembershipFallsBackToTheWorldType() { logInTo(MEMBERS_WORLD, EnumSet.of(WorldType.MEMBERS)); @@ -96,6 +124,121 @@ void returningToTheLoginScreenRecomputesTheFilterOnTheNextLogin() assertFalse(plugin.isHideF2PWorlds()); } + @Test + void membersAccountRelogOnAnF2pWorldKeepsTheMembersFilter() + { + when(client.getVarpValue(VarPlayerID.ACCOUNT_CREDIT)).thenReturn(MEMBERSHIP_DAYS); + logInTo(MEMBERS_WORLD, EnumSet.of(WorldType.MEMBERS)); + + fire(GameState.HOPPING); + logInTo(F2P_WORLD, EnumSet.noneOf(WorldType.class)); + fire(GameState.LOGIN_SCREEN); + logInTo(F2P_WORLD, EnumSet.noneOf(WorldType.class)); + + assertFalse(plugin.isHideMembersWorlds()); + assertTrue(plugin.isHideF2PWorlds()); + } + + @Test + void membershipArrivingAfterLoginOnAnF2pWorldSwitchesToTheMembersFilter() + { + Star membersStar = star(true); + Star f2pStar = star(false); + plugin.getStarList().add(membersStar); + plugin.getStarList().add(f2pStar); + logInTo(F2P_WORLD, EnumSet.noneOf(WorldType.class)); + + accountCredit(MEMBERSHIP_DAYS); + + assertFalse(plugin.isHideMembersWorlds()); + assertTrue(plugin.isHideF2PWorlds()); + verify(membersStar).setHidden(false); + verify(f2pStar).setHidden(true); + } + + @Test + void accountCreditResetDuringAHopDoesNotDropTheMembersFilter() + { + when(client.getVarpValue(VarPlayerID.ACCOUNT_CREDIT)).thenReturn(MEMBERSHIP_DAYS); + logInTo(MEMBERS_WORLD, EnumSet.of(WorldType.MEMBERS)); + + fire(GameState.HOPPING); + when(client.getWorld()).thenReturn(F2P_WORLD); + when(client.getWorldType()).thenReturn(EnumSet.noneOf(WorldType.class)); + accountCredit(0); + + assertFalse(plugin.isHideMembersWorlds()); + assertTrue(plugin.isHideF2PWorlds()); + } + + @Test + void accountCreditBeforeTheFirstLoginWaitsForTheLogin() + { + accountCredit(MEMBERSHIP_DAYS); + + assertTrue(plugin.isHideMembersWorlds()); + assertFalse(plugin.isHideF2PWorlds()); + } + + @Test + void startUpWhileLoggedInReadsMembershipOnTheClientThread() throws Exception + { + inject("hideMembersWorlds", false); + microbot.when(Microbot::isLoggedIn).thenReturn(true); + when(client.getVarpValue(anyInt())).thenAnswer(invocation -> { + assertTrue(onClientThread, "varp read off the client thread"); + return MEMBERSHIP_DAYS; + }); + when(client.getWorldType()).thenAnswer(invocation -> { + assertTrue(onClientThread, "world type read off the client thread"); + return EnumSet.noneOf(WorldType.class); + }); + + plugin.startUp(); + + assertFalse(plugin.isHideMembersWorlds()); + assertTrue(plugin.isHideF2PWorlds()); + } + + @Test + void startUpWhileLoggedOutWaitsForTheLogin() throws Exception + { + plugin.startUp(); + + assertTrue(plugin.isHideMembersWorlds()); + assertFalse(plugin.isHideF2PWorlds()); + + when(client.getVarpValue(VarPlayerID.ACCOUNT_CREDIT)).thenReturn(MEMBERSHIP_DAYS); + logInTo(F2P_WORLD, EnumSet.noneOf(WorldType.class)); + + assertFalse(plugin.isHideMembersWorlds()); + } + + @Test + void removingAStarWhileTheFilterIsAppliedDoesNotBreakTheIteration() + { + Star first = star(true); + Star second = star(false); + plugin.getStarList().add(first); + plugin.getStarList().add(second); + doAnswer(invocation -> { + plugin.removeStar(second); + return null; + }).when(first).setHidden(anyBoolean()); + + assertDoesNotThrow(plugin::updateHiddenStars); + assertFalse(plugin.getStarList().contains(second)); + } + + private void accountCredit(int days) + { + when(client.getVarpValue(VarPlayerID.ACCOUNT_CREDIT)).thenReturn(days); + VarbitChanged event = new VarbitChanged(); + event.setVarpId(VarPlayerID.ACCOUNT_CREDIT); + event.setValue(days); + plugin.onVarbitChanged(event); + } + private void logInTo(int world, EnumSet worldTypes) { when(client.getWorld()).thenReturn(world); From 362786ba9114db7c006cfef5f9c0d041e413ed2d Mon Sep 17 00:00:00 2001 From: chsami Date: Tue, 13 Oct 2026 20:50:23 +0200 Subject: [PATCH 7/7] fix(autohunter): stop visibly when a world hop stays locally blocked for 3 minutes (#604) Since #602 a local hop blocker (combat, open interface, dialogue, animation) keeps AutoChin in HOPPING_WORLD and retries every 3s without penalising the target. A block that never clears therefore idled the script forever. Track how long the hop has been continuously blocked and stop with the actual blocker reason once it reaches 3 minutes. The timer restarts when the block clears, a hop is issued, or the world changes; local blocks still never blacklist the target or count as attempts. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../microhunter/AutoHunterPlugin.java | 2 +- .../microhunter/scripts/AutoChinScript.java | 12 +++ .../scripts/AutoChinHopResultTest.java | 87 +++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/runelite/client/plugins/microbot/microhunter/AutoHunterPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/microhunter/AutoHunterPlugin.java index b408efb164..7e420ab3a2 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/microhunter/AutoHunterPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/microhunter/AutoHunterPlugin.java @@ -31,7 +31,7 @@ @Slf4j @PluginDependency(HunterPlugin.class) public class AutoHunterPlugin extends Plugin { - public static final String version = "1.5.3"; + public static final String version = "1.5.4"; @Inject private AutoHunterConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinScript.java b/src/main/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinScript.java index f7c4cd0cf9..9cf1ad54c6 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinScript.java @@ -90,6 +90,7 @@ private enum Action { private static final long WORLD_HOP_TIMEOUT_MS = 15_000; private static final long REFUSED_HOP_RETRY_MS = 3_000; private static final int MAX_REFUSED_HOPS_PER_WORLD = 3; + private static final long LOCAL_HOP_BLOCK_LIMIT_MS = 180_000; private static final long REJECTED_WORLD_COOLDOWN_MS = 600_000; private static final long BREAK_RECOVERY_CLEAR_MS = 1_500; private static final int MAX_WORLD_HOP_ATTEMPTS = 5; @@ -150,6 +151,7 @@ private enum Action { private boolean hopRefused; private int hopRefusals; private String hopBlocker; + private long hopBlockedSince; private boolean breakRecoveryRequested; private boolean breakRecoveryComplete; private boolean breakRecoveryLockHeld; @@ -209,6 +211,7 @@ private void resetSession() { hopRefused = false; hopRefusals = 0; hopBlocker = null; + hopBlockedSince = 0; breakRecoveryRequested = false; breakRecoveryComplete = false; breakRecoveryLockHeld = false; @@ -323,6 +326,7 @@ private void beginWorldArrival(AutoHunterConfig config, int world) { hopRefused = false; hopRefusals = 0; hopBlocker = null; + hopBlockedSince = 0; nearbyPlayerSeenSince = 0; if (!config.avoidOccupiedWorlds()) { worldHopAttempts = 0; @@ -459,6 +463,7 @@ private void issueWorldHop() { if (Microbot.hopToWorld(hopTargetWorld)) { hopRefused = false; hopBlocker = null; + hopBlockedSince = 0; worldHopAttempts++; return; } @@ -466,6 +471,12 @@ private void issueWorldHop() { } hopRefused = true; if (blocker != null) { + if (hopBlockedSince == 0) hopBlockedSince = hopStartedAt; + long blockedFor = hopStartedAt - hopBlockedSince; + if (blockedFor >= LOCAL_HOP_BLOCK_LIMIT_MS) { + stopSafely("World hop blocked locally for " + (blockedFor / 1_000) + "s: " + blocker); + return; + } if (!blocker.equals(hopBlocker)) { Microbot.log("AutoHunter occupancy: hop to world " + hopTargetWorld + " waiting (" + blocker + ")"); } @@ -474,6 +485,7 @@ private void issueWorldHop() { return; } hopBlocker = null; + hopBlockedSince = 0; hopRefusals++; Microbot.log("AutoHunter occupancy: hop to world " + hopTargetWorld + " refused (" + hopRefusals + "/" + MAX_REFUSED_HOPS_PER_WORLD + ")"); diff --git a/src/test/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinHopResultTest.java b/src/test/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinHopResultTest.java index 67c0206a47..d6e16581b5 100644 --- a/src/test/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinHopResultTest.java +++ b/src/test/java/net/runelite/client/plugins/microbot/microhunter/scripts/AutoChinHopResultTest.java @@ -231,6 +231,93 @@ void refusalCausedByALocalBlockThatAppearedDuringTheHopIsNotCounted() throws Exc } } + @Test + void persistentLocalBlockStopsVisiblyAfterTheCapWithoutPenalisingTheTarget() throws Exception { + AutoChinScript script = new AutoChinScript(); + Client client = mock(Client.class); + when(client.getWorld()).thenReturn(SOURCE_WORLD); + WorldService worldService = australianWorlds(480, 481, 482, 483, 484, 485, 486, 487); + + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic player = mockStatic(Rs2Player.class)) { + microbot.when(Microbot::getClient).thenReturn(client); + microbot.when(Microbot::getWorldService).thenReturn(worldService); + microbot.when(Microbot::isLoggedIn).thenReturn(true); + player.when(Rs2Player::isMember).thenReturn(true); + player.when(Rs2Player::isInCombat).thenReturn(true); + microbot.when(() -> Microbot.hopToWorld(anyInt())).thenReturn(false); + + invoke(script, "requestAustralianWorldHop", "occupied"); + int target = (int) field(script, "hopTargetWorld"); + + setField(script, "hopBlockedSince", System.currentTimeMillis() - 170_000); + setField(script, "hopStartedAt", System.currentTimeMillis() - 3_000); + invoke(script, "monitorWorldHop"); + assertEquals("HOPPING_WORLD", field(script, "currentState").toString()); + + setField(script, "hopBlockedSince", System.currentTimeMillis() - 181_000); + setField(script, "hopStartedAt", System.currentTimeMillis() - 3_000); + invoke(script, "monitorWorldHop"); + + assertEquals("STOPPED", field(script, "currentState").toString()); + String reason = script.getStopReason(); + assertTrue(reason.startsWith("World hop blocked locally for 18"), reason); + assertTrue(reason.endsWith("s: in combat"), reason); + microbot.verify(() -> Microbot.hopToWorld(anyInt()), never()); + assertEquals(target, field(script, "hopTargetWorld")); + assertEquals(0, field(script, "worldHopAttempts")); + assertEquals(0, field(script, "hopRefusals")); + assertTrue(((Map) field(script, "rejectedWorlds")).isEmpty()); + } + } + + @Test + void localBlockTimerRestartsWhenTheBlockClears() throws Exception { + AutoChinScript script = new AutoChinScript(); + Client client = mock(Client.class); + when(client.getWorld()).thenReturn(SOURCE_WORLD); + WorldService worldService = australianWorlds(TARGET_WORLD); + + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic player = mockStatic(Rs2Player.class)) { + microbot.when(Microbot::getClient).thenReturn(client); + microbot.when(Microbot::getWorldService).thenReturn(worldService); + microbot.when(Microbot::isLoggedIn).thenReturn(true); + player.when(Rs2Player::isMember).thenReturn(true); + dialogue.when(Rs2Dialogue::isInDialogue).thenReturn(true); + microbot.when(() -> Microbot.hopToWorld(TARGET_WORLD)).thenReturn(false); + + invoke(script, "requestAustralianWorldHop", "occupied"); + assertTrue((long) field(script, "hopBlockedSince") > 0); + + setField(script, "hopBlockedSince", System.currentTimeMillis() - 170_000); + dialogue.when(Rs2Dialogue::isInDialogue).thenReturn(false); + setField(script, "hopStartedAt", System.currentTimeMillis() - 3_000); + invoke(script, "monitorWorldHop"); + assertEquals(0L, field(script, "hopBlockedSince")); + assertEquals(1, field(script, "hopRefusals")); + + dialogue.when(Rs2Dialogue::isInDialogue).thenReturn(true); + for (int i = 0; i < 5; i++) { + setField(script, "hopStartedAt", System.currentTimeMillis() - 3_000); + invoke(script, "monitorWorldHop"); + } + assertEquals("HOPPING_WORLD", field(script, "currentState").toString()); + assertTrue(System.currentTimeMillis() - (long) field(script, "hopBlockedSince") < 10_000); + + setField(script, "hopBlockedSince", System.currentTimeMillis() - 170_000); + dialogue.when(Rs2Dialogue::isInDialogue).thenReturn(false); + microbot.when(() -> Microbot.hopToWorld(TARGET_WORLD)).thenReturn(true); + setField(script, "hopStartedAt", System.currentTimeMillis() - 3_000); + invoke(script, "monitorWorldHop"); + + assertEquals(0L, field(script, "hopBlockedSince")); + assertEquals(1, field(script, "worldHopAttempts")); + assertEquals("HOPPING_WORLD", field(script, "currentState").toString()); + assertTrue(((Map) field(script, "rejectedWorlds")).isEmpty()); + } + } + private static WorldService australianWorlds(int... ids) { List worlds = new ArrayList<>(); for (int id : ids) {