From ac8625f94294c6f0242ae934447036db5f92b8d5 Mon Sep 17 00:00:00 2001 From: Alex <45095641+runsonmypc@users.noreply.github.com> Date: Fri, 12 Jun 2026 19:34:45 -0400 Subject: [PATCH 1/2] fix(GotrPlugin): stop idle stalls in Guardians of the Rift The plugin would stand idle mid-game. Three root causes, all addressed: - Query-API migration dropped auto-walk: `cache.query().interact(id, action)` clicks at the player's tile and does not walk into range, so any out-of-range object interaction silently no-ops every tick. Route object interactions through a walk-first helper that web-walks when >51 tiles away and hands off to click-to-walk once close (restores legacy behaviour). - repairCells() locked the loop: it looked up the shield pylons by a name filter (`contains("cell_tile")`) that never matches a real object name, so the query was always empty, yet the method returned true unconditionally. That short-circuited the main loop at `if (repairCells()) return;` on every tick whenever a powered cell was held, leaving the bot frozen until the next game start. Match pylons by object id via CellType.GetShieldTier, and only return true when a cell is actually placed/used. - Static state leaked across plugin restarts; reset it in run() so a restart behaves like a first start. Also removed a per-tick blanket Rs2Walker.setTarget(null) that churned the walker and spammed the log. Verified live: full mine -> craft -> portal -> craft cycle with no stalls, including while holding a powered cell. Bump version to 1.5.6. --- .../plugins/microbot/gotr/GotrPlugin.java | 2 +- .../plugins/microbot/gotr/GotrScript.java | 148 ++++++++++++------ 2 files changed, 104 insertions(+), 46 deletions(-) diff --git a/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrPlugin.java index 7f552ea253..3b6d59d0eb 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrPlugin.java @@ -41,7 +41,7 @@ ) @Slf4j public class GotrPlugin extends Plugin { - public static final String version = "1.5.4"; + public static final String version = "1.5.6"; @Inject private GotrConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrScript.java b/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrScript.java index c36c592c6c..f45f5e6a53 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrScript.java @@ -107,6 +107,23 @@ private void initializeGuardianPortalInfo() { public boolean run(GotrConfig config) { this.config = config; + // Static (and singleton-instance) state persists for the whole JVM session and leaks + // across plugin disable/re-enable (see docs/PLUGIN_DEBUGGING_NOTES.md §5). Reset it here + // so a restart behaves like a first start instead of inheriting a stale state machine. + shouldMineGuardianRemains = true; + isInMiniGame = false; + isFirstPortal = true; + state = null; + nextGameStart = Optional.empty(); + timeSincePortal = Optional.empty(); + elementalRewardPoints = 0; + catalyticRewardPoints = 0; + useNpcContact = true; + initCheck = false; + optimizedEssenceLoop = false; + guardians.clear(); + activeGuardianPortals.clear(); + greatGuardian = null; mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { try { if (!Microbot.isLoggedIn()) return; @@ -122,8 +139,6 @@ public boolean run(GotrConfig config) { initCheck = true; } - Rs2Walker.setTarget(null); - if (!Rs2Inventory.hasItem("pickaxe") && !Rs2Equipment.isWearing("pickaxe")) { log("You need to have a pickaxe before you can participate in this minigame."); return; @@ -243,7 +258,7 @@ private boolean waitingForGameToStart(int timeToStart) { // Return to large mine if we were there before if (!isInLargeMine() && shouldMineGuardianRemains) { if (Rs2Walker.walkTo(new WorldPoint(3632, 9503, 0), 20)) { - Microbot.getRs2TileObjectCache().query().interact(ObjectID.RUBBLE_43724); + interactObject(ObjectID.RUBBLE_43724); return true; } } @@ -261,33 +276,42 @@ private boolean waitingForGameToStart(int timeToStart) { private boolean repairCells() { Rs2ItemModel cell = Rs2Inventory.get(CellType.PoweredCellList().stream().mapToInt(i -> i).toArray()); - if (cell != null && isInMainRegion() && isInMiniGame() && !shouldMineGuardianRemains && !isInLargeMine() && !isInHugeMine()) { - int cellTier = CellType.GetCellTier(cell.getId()); - List shieldCells = Microbot.getRs2TileObjectCache().query() - .where(o -> o.getName() != null && o.getName().toLowerCase().contains("cell_tile")) - .toListOnClientThread(); - - if (Rs2Inventory.hasItemAmount(GUARDIAN_ESSENCE, 10)) { - for (Rs2TileObjectModel shieldCell : shieldCells) { - if (CellType.GetShieldTier(shieldCell.getId()) < cellTier) { - Microbot.log("Upgrading power cell at " + shieldCell.getWorldLocation()); - shieldCell.click("Place-cell"); - sleepUntil(() -> !Rs2Player.isMoving()); - return true; - } + if (cell == null || !isInMainRegion() || !isInMiniGame() || shouldMineGuardianRemains || isInLargeMine() || isInHugeMine()) { + return false; + } + int cellTier = CellType.GetCellTier(cell.getId()); + // Identify the shield pylons by object id (CellType.GetShieldTier knows them all and + // returns -1 for anything else). The previous filter matched on a name containing + // "cell_tile", but the real pylon objects aren't named that, so the query always came + // back empty — yet the method still returned true unconditionally below. That made the + // main loop short-circuit at `if (repairCells()) return;` on every tick whenever a + // powered cell was held, leaving the bot standing idle until the next game start. Match + // by id, and only claim the tick when we actually place/use a cell. + List shieldCells = Microbot.getRs2TileObjectCache().query() + .where(o -> CellType.GetShieldTier(o.getId()) >= 0) + .toListOnClientThread(); + + if (Rs2Inventory.hasItemAmount(GUARDIAN_ESSENCE, 10)) { + for (Rs2TileObjectModel shieldCell : shieldCells) { + if (CellType.GetShieldTier(shieldCell.getId()) < cellTier) { + Microbot.log("Upgrading power cell at " + shieldCell.getWorldLocation()); + shieldCell.click("Place-cell"); + sleepUntil(() -> !Rs2Player.isMoving()); + return true; } } - Rs2TileObjectModel cellToUse = shieldCells.stream() - .filter(o -> o.getId() != ObjectID.CELL_TILE_BROKEN) - .findFirst().orElse(null); - if (cellToUse != null) { - cellToUse.click(); - log("Using cell with id " + cellToUse.getId()); - sleep(Rs2Random.randomGaussian(1000, 300)); - sleepUntil(() -> !Rs2Player.isMoving()); - } + } + Rs2TileObjectModel cellToUse = shieldCells.stream() + .filter(o -> CellType.GetShieldTier(o.getId()) > 0) + .findFirst().orElse(null); + if (cellToUse != null) { + cellToUse.click(); + log("Using cell with id " + cellToUse.getId()); + sleep(Rs2Random.randomGaussian(1000, 300)); + sleepUntil(() -> !Rs2Player.isMoving()); return true; } + // Nothing to place — don't pretend we handled the tick, or the loop will never craft. return false; } @@ -314,7 +338,7 @@ private void takeUnchargedCells() { } } - Microbot.getRs2TileObjectCache().query().interact(ObjectID.UNCHARGED_CELLS_43732, "Take-10"); + interactObject(ObjectID.UNCHARGED_CELLS_43732, "Take-10"); log("Taking uncharged cells..."); Rs2Player.waitForAnimation(); } @@ -338,7 +362,7 @@ private boolean usePortal() { private boolean depositRunesIntoPool() { if (config.shouldDepositRunes() && Rs2Inventory.hasItem(runeIds.stream().mapToInt(i -> i).toArray()) && !isInLargeMine() && !isInHugeMine() && !Rs2Inventory.isFull() && !optimizedEssenceLoop) { if (Rs2Player.isMoving()) return true; - if (Microbot.getRs2TileObjectCache().query().interact(ObjectID.DEPOSIT_POOL)) { + if (interactObject(ObjectID.DEPOSIT_POOL)) { log("Deposit runes into pool..."); sleep(600, 2400); } @@ -362,7 +386,7 @@ private boolean enterAltar() { } private boolean craftGuardianEssences() { - if (Microbot.getRs2TileObjectCache().query().interact(ObjectID.WORKBENCH_43754)) { + if (interactObject(ObjectID.WORKBENCH_43754)) { state = GotrState.CRAFT_GUARDIAN_ESSENCE; sleep(Rs2Random.randomGaussian(Rs2Random.between(600, 900), Rs2Random.between(150, 300))); log("Crafting guardian essences..."); @@ -373,7 +397,7 @@ private boolean craftGuardianEssences() { private boolean leaveLargeMine() { if (isInLargeMine()) { - Microbot.getRs2TileObjectCache().query().interact(ObjectID.RUBBLE_43726); + interactObject(ObjectID.RUBBLE_43726); Rs2Player.waitForAnimation(); log("Leaving large mine..."); state = GotrState.LEAVING_LARGE_MINE; @@ -416,13 +440,13 @@ private boolean craftRunes() { if (Rs2Inventory.hasItem(GUARDIAN_ESSENCE)) { state = GotrState.CRAFTING_RUNES; optimizedEssenceLoop = false; - Microbot.getRs2TileObjectCache().query().interact(rcAltar.getId()); + interactObject(rcAltar, null); log("Crafting runes on altar " + rcAltar.getId()); sleep(Rs2Random.randomGaussian(Rs2Random.between(1000, 1500), 300)); } else if (!Rs2Player.isMoving()) { state = GotrState.LEAVING_ALTAR; Rs2TileObjectModel rcPortal = findPortalToLeaveAltar(); - if (Microbot.getRs2TileObjectCache().query().interact(rcPortal.getId())) { + if (interactObject(rcPortal, null)) { log("Leaving the altar..."); sleepUntilTrue(GotrScript::isInMainRegion,100,10000); sleep(Rs2Random.randomGaussian(750, 150)); @@ -437,7 +461,7 @@ private boolean craftRunes() { private static boolean waitForMinigameToStart() { if (!isInMainRegion()) { Rs2TileObjectModel rcPortal = findPortalToLeaveAltar(); - if (rcPortal != null && Microbot.getRs2TileObjectCache().query().interact(rcPortal.getId())) { + if (rcPortal != null && interactObject(rcPortal, null)) { state = GotrState.LEAVING_ALTAR; return true; } @@ -446,13 +470,13 @@ private static boolean waitForMinigameToStart() { if (state != GotrState.WAITING) { state = GotrState.WAITING; log("Make sure to start the script near the minigame barrier."); - Microbot.getRs2TileObjectCache().query().interact(ObjectID.BARRIER_43849, "Peek"); + interactObject(ObjectID.BARRIER_43849, "Peek"); } return state == GotrState.WAITING; } private static boolean enterMinigame() { - if (Microbot.getRs2TileObjectCache().query().interact(ObjectID.BARRIER_43700, "quick-pass")) { + if (interactObject(ObjectID.BARRIER_43700, "quick-pass")) { Rs2Player.waitForWalking(); state = GotrState.ENTER_GAME; GotrScript.shouldMineGuardianRemains = true; @@ -479,10 +503,10 @@ private boolean mineHugeGuardianRemain() { } if (!Rs2Inventory.isFull()) { if (!Rs2Player.isAnimating()) { - Microbot.getRs2TileObjectCache().query().interact(ObjectID.HUGE_GUARDIAN_REMAINS); + interactObject(ObjectID.HUGE_GUARDIAN_REMAINS); Rs2Player.waitForAnimation(); if (!Rs2Player.isAnimating()) - Microbot.getRs2TileObjectCache().query().interact(ObjectID.HUGE_GUARDIAN_REMAINS); + interactObject(ObjectID.HUGE_GUARDIAN_REMAINS); } } else { if (Rs2Inventory.allPouchesFull()) { @@ -493,7 +517,7 @@ private boolean mineHugeGuardianRemain() { Rs2Inventory.fillPouches(); sleep(Rs2Random.randomGaussian(Rs2Random.between(600, 1200), Rs2Random.between(100, 300))); if (!Rs2Inventory.isFull()) { - Microbot.getRs2TileObjectCache().query().interact(ObjectID.HUGE_GUARDIAN_REMAINS); + interactObject(ObjectID.HUGE_GUARDIAN_REMAINS); } } } @@ -518,13 +542,13 @@ private void mineGuardianRemains() { if (!isInLargeMine() && !isInHugeMine() && (!Rs2Inventory.hasItem(GUARDIAN_FRAGMENTS) || getStartTimer() == -1)) { if (Rs2Walker.walkTo(new WorldPoint(3632, 9503, 0), 20)) { log("Traveling to large mine..."); - Microbot.getRs2TileObjectCache().query().interact(ObjectID.RUBBLE_43724); + interactObject(ObjectID.RUBBLE_43724); if (sleepUntil(Rs2Player::isAnimating)) { sleepUntil(GotrScript::isInLargeMine); if (isInLargeMine()) { sleep(Rs2Random.randomGaussian(Rs2Random.between(2000, 2400), Rs2Random.between(100, 300))); log("Interacting with large guardian remains..."); - Microbot.getRs2TileObjectCache().query().interact(ObjectID.LARGE_GUARDIAN_REMAINS); + interactObject(ObjectID.LARGE_GUARDIAN_REMAINS); sleepGaussian(1200, 150); } } @@ -538,7 +562,7 @@ private void mineGuardianRemains() { checkPouches(Rs2Random.between(1, 20) == 2, Rs2Random.between(100, 600), Rs2Random.between(100, 300)); repairPouches(); - Microbot.getRs2TileObjectCache().query().interact(ObjectID.LARGE_GUARDIAN_REMAINS); + interactObject(ObjectID.LARGE_GUARDIAN_REMAINS); sleepGaussian(1200, 150); } } @@ -552,7 +576,7 @@ private void mineGuardianRemains() { Rs2Combat.setSpecState(true, 1000); } repairPouches(); - Microbot.getRs2TileObjectCache().query().interact(ObjectID.GUARDIAN_PARTS_43716); + interactObject(ObjectID.GUARDIAN_PARTS_43716); sleepGaussian(1200, 150); // we can assume that if the player is mining within the startTimer range, he will get enough guardian remains for the game shouldMineGuardianRemains = false; @@ -561,7 +585,7 @@ private void mineGuardianRemains() { } private void leaveHugeMine() { - Microbot.getRs2TileObjectCache().query().interact(38044); + interactObject(38044); log("Leave huge mine..."); Global.sleepUntil(() -> !isInHugeMine(), 5000); @@ -765,6 +789,40 @@ public static void resetPlugin() { Microbot.getClient().clearHintArrow(); } + /** + * Walk-first object interaction. + * + *

The migrated Queryable API ({@code cache.query().interact(id, action)}) resolves + * {@code nearestReachable()} and clicks at the player's current tile — it does NOT walk into + * range. Legacy {@code Rs2GameObject.interact(id, action)} auto-walked when the target was + * more than 51 tiles away. After the query-API migration GOTR lost that auto-walk, so any + * interaction issued while out of range silently no-ops every tick and the bot just stands + * there (see docs/PLUGIN_DEBUGGING_NOTES.md §3). This restores the legacy behaviour: web-walk + * when far, hand off to the game's click-to-walk once close. + */ + private static boolean interactObject(int id) { + return interactObject(id, null); + } + + private static boolean interactObject(int id, String action) { + return interactObject(Microbot.getRs2TileObjectCache().query().withId(id).nearest(), action); + } + + private static boolean interactObject(Rs2TileObjectModel obj, String action) { + if (obj == null) return false; + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + WorldPoint objLoc = obj.getWorldLocation(); + if (playerLoc != null && objLoc != null && playerLoc.distanceTo(objLoc) > 51) { + log("Object " + obj.getId() + " is " + playerLoc.distanceTo(objLoc) + " tiles away, walking into range..."); + Rs2Walker.walkTo(objLoc); + return false; + } + // In click range: drop any lingering web-walk target so the game's click-to-walk drives + // the final approach, then interact. + Rs2Walker.setTarget(null); + return (action == null || action.isEmpty()) ? obj.click() : obj.click(action); + } + public static Rs2TileObjectModel findRcAltar() { return Microbot.getRs2TileObjectCache().query().withIds( ObjectID.ALTAR_34760, ObjectID.ALTAR_34761, ObjectID.ALTAR_34762, ObjectID.ALTAR_34763, ObjectID.ALTAR_34764, @@ -784,7 +842,7 @@ public static boolean leaveMinigame() { return true; // Already outside the minigame, successfully left } if(isInLargeMine()) { - Microbot.getRs2TileObjectCache().query().interact(ObjectID.RUBBLE_43726); + interactObject(ObjectID.RUBBLE_43726); Rs2Player.waitForAnimation(); sleepUntil(()-> !isInLargeMine()); if (isInLargeMine()){ @@ -793,7 +851,7 @@ public static boolean leaveMinigame() { } } - Microbot.getRs2TileObjectCache().query().interact(ObjectID.BARRIER_43700, "quick-pass"); + interactObject(ObjectID.BARRIER_43700, "quick-pass"); Rs2Player.waitForWalking(); sleepUntil( ()-> {return !(!isOutsideBarrier() && isInMainRegion());}, 200); GotrScript.isInMiniGame = !isOutsideBarrier() && isInMainRegion(); From 59fa46b7d67c0d00a214d7fc8e174513ed3d0a41 Mon Sep 17 00:00:00 2001 From: Alex <45095641+runsonmypc@users.noreply.github.com> Date: Sat, 13 Jun 2026 12:05:43 -0400 Subject: [PATCH 2/2] fix(GotrPlugin): always deposit crafted runes at the end of a round Runes were only deposited from the !shouldMineGuardianRemains (crafting) branch. When a round ends the chat handler sets shouldMineGuardianRemains = true, so the loop switches to the mining branch and waitingForGameToStart() intercepts the between-rounds lobby tick entirely -- the deposit call was never reached and runes carried into the next round. The !isFull guard also skipped depositing a full inventory of crafted runes (the typical end-of-round state). - depositRunesIntoPool(): drop the !isFull and !optimizedEssenceLoop guards that suppressed depositing, and make it freeze-safe -- return true only when the deposit pool actually exists (walk-first toward it / click it), else return false so the loop never locks up holding runes. - waitingForGameToStart(): deposit any held runes before prepping/mining for the next game, so end-of-round runes are banked reliably. Bump version to 1.5.7. --- .../plugins/microbot/gotr/GotrPlugin.java | 2 +- .../plugins/microbot/gotr/GotrScript.java | 30 ++++++++++++++----- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrPlugin.java index 3b6d59d0eb..3dbd787f09 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrPlugin.java @@ -41,7 +41,7 @@ ) @Slf4j public class GotrPlugin extends Plugin { - public static final String version = "1.5.6"; + public static final String version = "1.5.7"; @Inject private GotrConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrScript.java b/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrScript.java index f45f5e6a53..c5f856f767 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrScript.java @@ -248,6 +248,11 @@ private boolean waitingForGameToStart(int timeToStart) { if (getStartTimer() > Rs2Random.randomGaussian(35, Rs2Random.between(1, 5)) || getStartTimer() == -1 || timeToStart > 10) { + // A round just ended (or hasn't started yet) and this path runs instead of the + // craft branch — bank any crafted runes into the pool before prepping for the next + // game, so we never carry runes over. + if (depositRunesIntoPool()) return true; + // Only take cells if we don't already have them if (!Rs2Inventory.hasItem("Uncharged cell")) { // If in large mine and need cells, leave first @@ -360,15 +365,24 @@ private boolean usePortal() { } private boolean depositRunesIntoPool() { - if (config.shouldDepositRunes() && Rs2Inventory.hasItem(runeIds.stream().mapToInt(i -> i).toArray()) && !isInLargeMine() && !isInHugeMine() && !Rs2Inventory.isFull() && !optimizedEssenceLoop) { - if (Rs2Player.isMoving()) return true; - if (interactObject(ObjectID.DEPOSIT_POOL)) { - log("Deposit runes into pool..."); - sleep(600, 2400); - } - return true; + if (!config.shouldDepositRunes() + || !Rs2Inventory.hasItem(runeIds.stream().mapToInt(i -> i).toArray()) + || isInLargeMine() || isInHugeMine()) { + return false; } - return false; + if (Rs2Player.isMoving()) return true; + // Walk-first interaction, but only claim the tick when the pool actually exists — otherwise + // return false so we never lock the loop standing around holding runes. Dropped the old + // !isFull / !optimizedEssenceLoop guards: they skipped exactly the end-of-round case, where + // a full inventory of crafted runes would otherwise never be deposited and carried into the + // next round. + Rs2TileObjectModel pool = Microbot.getRs2TileObjectCache().query().withId(ObjectID.DEPOSIT_POOL).nearest(); + if (pool == null) return false; + if (interactObject(pool, null)) { + log("Deposit runes into pool..."); + sleep(600, 2400); + } + return true; } private boolean enterAltar() {