From d7d29d2f165bfd59cc2021cbb0ba74a6f573f2ab Mon Sep 17 00:00:00 2001 From: infuse21 Date: Mon, 29 Jun 2026 21:24:58 +0100 Subject: [PATCH 01/13] Agility plugin bug fixes --- .../microbot/agility/AgilityScript.java | 229 +++++++++++++----- .../microbot/agility/MicroAgilityOverlay.java | 5 + .../microbot/agility/MicroAgilityPlugin.java | 18 +- .../agility/courses/AgilityCourseHandler.java | 31 ++- .../agility/courses/BrimhavenSpikeCourse.java | 4 +- .../courses/ColossalWyrmAdvancedCourse.java | 4 +- .../courses/ColossalWyrmBasicCourse.java | 4 +- .../agility/courses/PrifddinasCourse.java | 24 +- .../agility/courses/PyramidCourse.java | 11 +- .../microbot/agility/courses/SeersCourse.java | 6 +- .../agility/courses/WerewolfCourse.java | 8 +- 11 files changed, 252 insertions(+), 92 deletions(-) diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java b/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java index 845a6e9fec..405bf72474 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java @@ -6,10 +6,13 @@ import net.runelite.api.gameval.ItemID; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.Script; +import net.runelite.client.plugins.microbot.agentserver.handler.ScriptHeartbeatRegistry; +import net.runelite.client.plugins.microbot.agility.courses.AgilityCourseHandler; import net.runelite.client.plugins.microbot.agility.courses.BrimhavenSpikeCourse; import net.runelite.client.plugins.microbot.agility.courses.GnomeStrongholdCourse; import net.runelite.client.plugins.microbot.agility.courses.PrifddinasCourse; import net.runelite.client.plugins.microbot.agility.courses.WerewolfCourse; +import net.runelite.client.plugins.microbot.agility.enums.AgilityCourse; import net.runelite.client.plugins.microbot.api.tileitem.models.Rs2TileItemModel; import net.runelite.client.plugins.microbot.util.antiban.Rs2Antiban; import net.runelite.client.plugins.microbot.util.antiban.Rs2AntibanSettings; @@ -22,7 +25,9 @@ import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; import javax.inject.Inject; +import java.awt.EventQueue; import java.util.Arrays; +import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -37,6 +42,10 @@ public class AgilityScript extends Script WorldPoint startPoint = null; int lastAgilityXp = 0; long lastTimeoutWarning = 0; // For throttled timeout warnings + private AgilityCourse activeCourse = null; + private AgilityCourseHandler activeHandler = null; + private static final int MARK_OF_GRACE_SEARCH_DISTANCE = 8; + private volatile boolean shuttingDown = false; @Inject public AgilityScript(MicroAgilityPlugin plugin, MicroAgilityConfig config) @@ -48,22 +57,57 @@ public AgilityScript(MicroAgilityPlugin plugin, MicroAgilityConfig config) @Override public void shutdown() { - // Reset BrimhavenSpike course flags if applicable - if (plugin.getCourseHandler() instanceof BrimhavenSpikeCourse) + shuttingDown = true; + ScriptHeartbeatRegistry.remove(this.getClass().getName()); + if (activeHandler != null) { - BrimhavenSpikeCourse course = (BrimhavenSpikeCourse) plugin.getCourseHandler(); - course.reset(); + activeHandler.reset(); } - - super.shutdown(); + activeCourse = null; + activeHandler = null; + startPoint = null; + initialPlayerLocation = null; + + if (mainScheduledFuture != null && !mainScheduledFuture.isDone()) + { + mainScheduledFuture.cancel(true); + } + if (scheduledFuture != null && !scheduledFuture.isDone()) + { + scheduledFuture.cancel(true); + } + + clearWalkingRouteForShutdown(); + if (Microbot.getClientThread().scheduledFuture != null) + { + Microbot.getClientThread().scheduledFuture.cancel(true); + } + Microbot.pauseAllScripts.set(false); + Rs2Walker.disableTeleports = false; + Microbot.getSpecialAttackConfigs().reset(); + } + + private void clearWalkingRouteForShutdown() + { + if (!EventQueue.isDispatchThread()) + { + Rs2Walker.clearWalkingRoute("agility:shutdown"); + return; + } + + Thread cleanupThread = new Thread(() -> Rs2Walker.clearWalkingRoute("agility:shutdown"), "AgilityScript-shutdown-cleanup"); + cleanupThread.setDaemon(true); + cleanupThread.start(); } public boolean run() { + shuttingDown = false; Microbot.enableAutoRunOn = true; Rs2Antiban.resetAntibanSettings(); Rs2Antiban.antibanSetupTemplates.applyAgilitySetup(); - startPoint = plugin.getCourseHandler().getStartPoint(); + AgilityCourseHandler initialHandler = getActiveHandler(); + startPoint = initialHandler.getStartPoint(); lastAgilityXp = Microbot.getClient().getSkillExperience(Skill.AGILITY); mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { try @@ -76,9 +120,22 @@ public boolean run() { return; } - - // Debug log to see if main loop is running - Microbot.log("AgilityScript main loop running - Course: " + config.agilityCourse().getTooltip()); + AgilityCourseHandler courseHandler = getActiveHandler(); + + if (startPoint == null) + { + Microbot.log("Early return: Start point is null"); + Microbot.showMessage("Agility course: " + config.agilityCourse().getTooltip() + " is not supported."); + sleep(10000); + return; + } + + if (handleSummerPies(courseHandler)) + { + Microbot.log("Early return: Handling summer pies"); + return; + } + if (!plugin.hasRequiredLevel()) { Microbot.log("Early return: Required level not met"); @@ -88,9 +145,9 @@ public boolean run() } // Check coin requirement for BrimhavenSpike course (only before payment) - if (plugin.getCourseHandler() instanceof BrimhavenSpikeCourse) + if (courseHandler instanceof BrimhavenSpikeCourse) { - BrimhavenSpikeCourse course = (BrimhavenSpikeCourse) plugin.getCourseHandler(); + BrimhavenSpikeCourse course = (BrimhavenSpikeCourse) courseHandler; if (!course.hasPaid() && !course.hasRequiredCoins()) { Microbot.log("Early return: Not enough coins for BrimhavenSpike course"); @@ -104,14 +161,6 @@ public boolean run() Microbot.log("Early return: Action cooldown active"); return; } - if (startPoint == null) - { - Microbot.log("Early return: Start point is null"); - Microbot.showMessage("Agility course: " + config.agilityCourse().getTooltip() + " is not supported."); - sleep(10000); - return; - } - final WorldPoint playerWorldLocation = Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation()); final int currentAgilityXp = Microbot.getClient().getSkillExperience(Skill.AGILITY); @@ -120,31 +169,19 @@ public boolean run() Microbot.log("Early return: Handling food"); return; } - if (handleSummerPies()) - { - Microbot.log("Early return: Handling summer pies"); - return; - } - - if (lootMarksOfGrace()) + if (lootMarksOfGrace(courseHandler)) { Microbot.log("Early return: Looting marks of grace"); return; } - if (handleCourseSpecificActions(playerWorldLocation)) + if (handleCourseSpecificActions(courseHandler, playerWorldLocation)) { return; } - - // Debug log to see if script is running - if (plugin.getCourseHandler() instanceof BrimhavenSpikeCourse) { - Microbot.log("BrimhavenSpike course detected, but handleCourseSpecificActions returned false"); - } - final int agilityExp = Microbot.getClient().getSkillExperience(Skill.AGILITY); - TileObject gameObject = plugin.getCourseHandler().getCurrentObstacle(); + TileObject gameObject = courseHandler.getCurrentObstacle(); if (gameObject == null) { @@ -158,7 +195,7 @@ public boolean run() } // Check if we should click (handles animation/XP logic) - if (!plugin.getCourseHandler().shouldClickObstacle(currentAgilityXp, lastAgilityXp)) + if (!courseHandler.shouldClickObstacle(currentAgilityXp, lastAgilityXp)) { return; // Not ready to click yet } @@ -215,7 +252,7 @@ public boolean run() // Normal obstacle interaction if (Rs2GameObject.interact(gameObject)) { // Wait for completion - this now returns quickly on XP drop - boolean completed = plugin.getCourseHandler().waitForCompletion(agilityExp, + boolean completed = courseHandler.waitForCompletion(agilityExp, Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation()).getPlane()); if (!completed) { @@ -241,12 +278,50 @@ public boolean run() } catch (Exception ex) { + if (isExpectedShutdownInterrupt(ex)) + { + return; + } Microbot.log("An error occurred: " + ex.getMessage(), ex); } }, 0, 100, TimeUnit.MILLISECONDS); return true; } + public boolean isShuttingDown() + { + return shuttingDown; + } + + private boolean isExpectedShutdownInterrupt(Exception ex) + { + if (!shuttingDown) + { + return false; + } + + if (Thread.currentThread().isInterrupted()) + { + return true; + } + + Throwable current = ex; + while (current != null) + { + if (current instanceof InterruptedException) + { + return true; + } + if (current.getMessage() != null && current.getMessage().contains("Interrupted waiting for client thread")) + { + return true; + } + current = current.getCause(); + } + + return false; + } + private Optional getAlchItem() { String itemsInput = config.itemsToAlch().trim(); @@ -279,20 +354,49 @@ private Optional getAlchItem() return Optional.empty(); } - private boolean lootMarksOfGrace() + private AgilityCourseHandler getActiveHandler() + { + AgilityCourse selectedCourse = config.agilityCourse(); + if (activeHandler == null || activeCourse != selectedCourse) + { + if (activeHandler != null) + { + activeHandler.reset(); + } + + activeCourse = selectedCourse; + activeHandler = selectedCourse.getHandler(); + activeHandler.reset(); + startPoint = activeHandler.getStartPoint(); + lastAgilityXp = Microbot.getClient().getSkillExperience(Skill.AGILITY); + } + return activeHandler; + } + + private boolean lootMarksOfGrace(AgilityCourseHandler courseHandler) { - final int lootDistance = plugin.getCourseHandler().getLootDistance(); if (Rs2Inventory.isFull() && !Rs2Inventory.contains(ItemID.GRACE)) { return false; } + WorldPoint playerLocation = courseHandler.getPlayerWorldLocation(); + if (playerLocation == null) + { + return false; + } + Rs2TileItemModel markOfGrace = Microbot.getRs2TileItemCache().query() .fromWorldView() .withId(ItemID.GRACE) .where(Rs2TileItemModel::isLootAble) - .where(item -> Rs2GameObject.canReach(item.getWorldLocation(), lootDistance, lootDistance, lootDistance, lootDistance)) - .nearest(lootDistance); + .where(item -> item.getWorldLocation() != null && item.getWorldLocation().getPlane() == playerLocation.getPlane()) + .where(item -> item.getWorldLocation().distanceTo(playerLocation) <= MARK_OF_GRACE_SEARCH_DISTANCE) + .where(item -> Rs2GameObject.canReach(item.getWorldLocation())) + .toList() + .stream() + .min(Comparator.comparingInt(item -> item.getWorldLocation().distanceTo(playerLocation))) + .orElse(null); if (markOfGrace == null) { @@ -310,6 +414,11 @@ private boolean lootMarksOfGrace() private boolean handleFood() { + if (config.hitpoints() <= 0) + { + return false; + } + if (Rs2Player.getHealthPercentage() > config.hitpoints()) { return false; @@ -318,7 +427,9 @@ private boolean handleFood() List foodItems = plugin.getInventoryFood(); if (foodItems.isEmpty()) { - return false; + Microbot.showMessage("Hitpoints are below the configured threshold and no food was found. Stopping agility."); + shutdown(); + return true; } Rs2ItemModel foodItem = foodItems.get(0); @@ -332,13 +443,17 @@ private boolean handleFood() return true; } - private boolean handleSummerPies() + private boolean handleSummerPies(AgilityCourseHandler courseHandler) { - if (plugin.getCourseHandler().getCurrentObstacleIndex() > 0) + if (!courseHandler.canBeBoosted()) { return false; } - if (Rs2Player.getBoostedSkillLevel(Skill.AGILITY) > plugin.getCourseHandler().getRequiredLevel()) + if (courseHandler.getCurrentObstacleIndex() > 0) + { + return false; + } + if (Rs2Player.getBoostedSkillLevel(Skill.AGILITY) >= courseHandler.getRequiredLevel()) { return false; } @@ -387,7 +502,7 @@ private boolean performEfficientAlch(TileObject gameObject, String alchItem, int sleep(100, 200); Rs2Magic.alch(alchItem, 50, 75); Rs2GameObject.interact(gameObject); - boolean completed = plugin.getCourseHandler().waitForCompletion(agilityExp, + boolean completed = getActiveHandler().waitForCompletion(agilityExp, Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation()).getPlane()); if (!completed) { @@ -415,34 +530,30 @@ private void performNormalAlch(String alchItem) Rs2Magic.alch(alchItem, 50, 75); } - private boolean handleCourseSpecificActions(WorldPoint playerWorldLocation) + private boolean handleCourseSpecificActions(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation) { - Microbot.log("handleCourseSpecificActions called for: " + plugin.getCourseHandler().getClass().getSimpleName()); - - if (plugin.getCourseHandler() instanceof PrifddinasCourse) + if (courseHandler instanceof PrifddinasCourse) { - PrifddinasCourse course = (PrifddinasCourse) plugin.getCourseHandler(); + PrifddinasCourse course = (PrifddinasCourse) courseHandler; return course.handlePortal() || course.handleWalkToStart(playerWorldLocation); } - else if (plugin.getCourseHandler() instanceof WerewolfCourse) + else if (courseHandler instanceof WerewolfCourse) { - WerewolfCourse course = (WerewolfCourse) plugin.getCourseHandler(); + WerewolfCourse course = (WerewolfCourse) courseHandler; return course.handleFirstSteppingStone(playerWorldLocation) || course.handleStickPickup(playerWorldLocation) || course.handleSlide() || course.handleStickReturn(playerWorldLocation); } - else if (plugin.getCourseHandler() instanceof BrimhavenSpikeCourse) + else if (courseHandler instanceof BrimhavenSpikeCourse) { - Microbot.log("BrimhavenSpikeCourse detected, calling handleWalkToStart"); - BrimhavenSpikeCourse course = (BrimhavenSpikeCourse) plugin.getCourseHandler(); + BrimhavenSpikeCourse course = (BrimhavenSpikeCourse) courseHandler; boolean result = course.handleWalkToStart(playerWorldLocation); - Microbot.log("BrimhavenSpikeCourse handleWalkToStart returned: " + result); return result; } - else if (!(plugin.getCourseHandler() instanceof GnomeStrongholdCourse)) + else if (!(courseHandler instanceof GnomeStrongholdCourse)) { - return plugin.getCourseHandler().handleWalkToStart(playerWorldLocation); + return courseHandler.handleWalkToStart(playerWorldLocation); } return false; } diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityOverlay.java b/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityOverlay.java index 36d27edb58..aa94510c62 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityOverlay.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityOverlay.java @@ -28,6 +28,11 @@ public class MicroAgilityOverlay extends OverlayPanel @Override public Dimension render(Graphics2D graphics) { + if (plugin.getAgilityScript().isShuttingDown() || !plugin.getAgilityScript().isRunning()) + { + return null; + } + try { panelComponent.setPreferredSize(new Dimension(200, 300)); diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityPlugin.java index 60ffdb09cb..66dca5fa31 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityPlugin.java @@ -34,7 +34,7 @@ @Slf4j public class MicroAgilityPlugin extends Plugin { - public static final String version = "1.2.7"; + public static final String version = "1.2.8"; @Inject private MicroAgilityConfig config; @Inject @@ -65,10 +65,14 @@ protected void startUp() throws AWTException agilityScript.run(); } + @Override protected void shutDown() { + if (overlayManager != null) + { + overlayManager.remove(agilityOverlay); + } agilityScript.shutdown(); - overlayManager.remove(agilityOverlay); } public AgilityCourseHandler getCourseHandler() @@ -88,12 +92,18 @@ public List getSummerPies() public boolean hasRequiredLevel() { + int requiredLevel = getCourseHandler().getRequiredLevel(); + if (Rs2Player.getRealSkillLevel(Skill.AGILITY) >= requiredLevel) + { + return true; + } + if (getSummerPies().isEmpty() || !getCourseHandler().canBeBoosted()) { - return Rs2Player.getRealSkillLevel(Skill.AGILITY) >= getCourseHandler().getRequiredLevel(); + return false; } - return Rs2Player.getBoostedSkillLevel(Skill.AGILITY) >= getCourseHandler().getRequiredLevel(); + return Rs2Player.getBoostedSkillLevel(Skill.AGILITY) >= requiredLevel; } public AgilityScript getAgilityScript() { diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/AgilityCourseHandler.java b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/AgilityCourseHandler.java index b5395fe169..67204f3838 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/AgilityCourseHandler.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/AgilityCourseHandler.java @@ -27,6 +27,25 @@ public interface AgilityCourseHandler Integer getRequiredLevel(); + default void reset() + { + } + + default WorldPoint getPlayerWorldLocation() + { + return Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation()); + } + + default int getClientPlane() + { + return Microbot.getClientThread().invoke(() -> Microbot.getClient().getTopLevelWorldView().getPlane()); + } + + default int getVarbitValue(int varbitId) + { + return Microbot.getClientThread().invoke(() -> Microbot.getClient().getVarbitValue(varbitId)); + } + default boolean canBeBoosted() { return true; @@ -34,7 +53,7 @@ default boolean canBeBoosted() default TileObject getCurrentObstacle() { - WorldPoint playerLocation = Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation()); + WorldPoint playerLocation = getPlayerWorldLocation(); List matchingObstacles = getObstacles().stream() .filter(o -> o.getOperationX().check(playerLocation.getX(), o.getRequiredX()) && o.getOperationY().check(playerLocation.getY(), o.getRequiredY())) @@ -118,8 +137,8 @@ default boolean waitForCompletion(final int agilityExp, final int plane) } // Check other completion conditions (health loss, plane change) - if (Rs2Player.getHealthPercentage() < initialHealth || - Microbot.getClient().getTopLevelWorldView().getPlane() != plane) + if (Rs2Player.getHealthPercentage() < initialHealth || + getClientPlane() != plane) { return true; } @@ -134,8 +153,8 @@ default boolean waitForCompletion(final int agilityExp, final int plane) default int getCurrentObstacleIndex() { - WorldPoint playerLoc = Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation()); - int playerPlane = Microbot.getClient().getTopLevelWorldView().getPlane(); + WorldPoint playerLoc = getPlayerWorldLocation(); + int playerPlane = getClientPlane(); if (playerPlane == 0 && playerLoc.distanceTo(getStartPoint()) < 5) { @@ -188,7 +207,7 @@ default int getCurrentObstacleIndex() default boolean handleWalkToStart(WorldPoint playerWorldLocation) { - if (Microbot.getClient().getTopLevelWorldView().getPlane() != 0) + if (getClientPlane() != 0) { return false; } diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/BrimhavenSpikeCourse.java b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/BrimhavenSpikeCourse.java index 854fe51c57..9696326812 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/BrimhavenSpikeCourse.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/BrimhavenSpikeCourse.java @@ -26,13 +26,13 @@ public class BrimhavenSpikeCourse implements AgilityCourseHandler { /** * Reset all flags when script shuts down */ + @Override public void reset() { hasPaid = false; hasClimbedLadder = false; lastAgilityXpForStep = -1; lastStepAtMs = 0; currentObstacleIndex = 0; - Microbot.log("BrimhavenSpike course flags reset"); } @Override @@ -214,6 +214,7 @@ public boolean handleSpikeTileWalking() { WorldPoint target = chooseFirst ? new WorldPoint(2800, 9568, 3) : new WorldPoint(2799, 9568, 3); + double initialHealth = Rs2Player.getHealthPercentage(); Microbot.log("Walking to spike tile: " + target); Rs2Walker.walkFastCanvas(target); @@ -227,7 +228,6 @@ public boolean handleSpikeTileWalking() { } // Check for damage (if player took damage, retry immediately) - double initialHealth = Rs2Player.getHealthPercentage(); if (Rs2Player.getHealthPercentage() < initialHealth) { Microbot.log("Player took damage - retrying obstacle immediately"); return false; // Let the script retry diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/ColossalWyrmAdvancedCourse.java b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/ColossalWyrmAdvancedCourse.java index d1bd153d1a..fb5d3deec0 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/ColossalWyrmAdvancedCourse.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/ColossalWyrmAdvancedCourse.java @@ -73,8 +73,8 @@ public boolean waitForCompletion(final int agilityExp, final int plane) } // Check other completion conditions (health loss, plane change) - if (Rs2Player.getHealthPercentage() < initialHealth || - Microbot.getClient().getTopLevelWorldView().getPlane() != plane) + if (Rs2Player.getHealthPercentage() < initialHealth || + getClientPlane() != plane) { return true; } diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/ColossalWyrmBasicCourse.java b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/ColossalWyrmBasicCourse.java index ad522f58f2..963ec0e50d 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/ColossalWyrmBasicCourse.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/ColossalWyrmBasicCourse.java @@ -73,8 +73,8 @@ public boolean waitForCompletion(final int agilityExp, final int plane) } // Check other completion conditions (health loss, plane change) - if (Rs2Player.getHealthPercentage() < initialHealth || - Microbot.getClient().getTopLevelWorldView().getPlane() != plane) + if (Rs2Player.getHealthPercentage() < initialHealth || + getClientPlane() != plane) { return true; } diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/PrifddinasCourse.java b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/PrifddinasCourse.java index 79c6e72c57..0c8b7a91fa 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/PrifddinasCourse.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/PrifddinasCourse.java @@ -63,6 +63,12 @@ public Integer getRequiredLevel() return 75; } + @Override + public void reset() + { + lastKnownHealth = -1; + } + public boolean handlePortal() { var portalModel = Microbot.getRs2TileObjectCache().query().withIds(PORTAL_OBSTACLE_IDS.stream().mapToInt(Integer::intValue).toArray()).within(10).nearest(); @@ -93,7 +99,7 @@ public boolean handleHealthMonitoring() if (currentHealth < lastKnownHealth) { log.info("Health dropped from {}% to {}% - fall detected", lastKnownHealth, currentHealth); - WorldPoint playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); + WorldPoint playerLocation = getPlayerWorldLocation(); log.info("Player location when fall detected: {}", playerLocation); // walk to exact fall recovery point @@ -114,7 +120,7 @@ public boolean handleHealthMonitoring() @Override public TileObject getCurrentObstacle() { - WorldPoint playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); + WorldPoint playerLocation = getPlayerWorldLocation(); // if we're near the start point, just find the first obstacle by ID within 5 tiles if (playerLocation.distanceTo(getStartPoint()) < 5) @@ -135,12 +141,12 @@ public TileObject getCurrentObstacle() @Override public boolean handleWalkToStart(WorldPoint playerWorldLocation) { - log.info("=== PrifddinasCourse.handleWalkToStart() called ==="); - log.info("Player location: {}", playerWorldLocation); - log.info("Start point: {}", getStartPoint()); - log.info("Distance to start: {}", playerWorldLocation.distanceTo(getStartPoint())); - log.info("Player plane: {}", Microbot.getClient().getTopLevelWorldView().getPlane()); - log.info("getCurrentObstacleIndex(): {}", getCurrentObstacleIndex()); + log.debug("Prifddinas handleWalkToStart: player={}, start={}, distance={}, plane={}, obstacleIndex={}", + playerWorldLocation, + getStartPoint(), + playerWorldLocation.distanceTo(getStartPoint()), + getClientPlane(), + getCurrentObstacleIndex()); // check for health-based fall detection if (handleHealthMonitoring()) @@ -148,7 +154,7 @@ public boolean handleWalkToStart(WorldPoint playerWorldLocation) return true; } - log.info("PrifddinasCourse.handleWalkToStart() returning false"); + log.debug("PrifddinasCourse.handleWalkToStart() returning false"); return false; } } diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/PyramidCourse.java b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/PyramidCourse.java index e33f484cba..a02b7c8569 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/PyramidCourse.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/PyramidCourse.java @@ -60,6 +60,11 @@ public List getObstacles() { new AgilityObstacleModel(10855) // Doorway ); } + + @Override + public void reset() { + state.reset(); + } @Override public TileObject getCurrentObstacle() { @@ -878,9 +883,7 @@ public boolean waitForCompletion(int agilityExp, int plane) { while (System.currentTimeMillis() - startTime < timeoutMs) { int currentXp = Microbot.getClient().getSkillExperience(Skill.AGILITY); - int currentPlane = Microbot.getClient().getTopLevelWorldView() != null - ? Microbot.getClient().getTopLevelWorldView().getPlane() - : Rs2Player.getWorldLocation().getPlane(); + int currentPlane = getClientPlane(); double currentHealth = Rs2Player.getHealthPercentage(); WorldPoint currentPos = Rs2Player.getWorldLocation(); @@ -1203,4 +1206,4 @@ private boolean handleEmptyWaterskins() { return false; } -} \ No newline at end of file +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/SeersCourse.java b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/SeersCourse.java index d67a45175c..2a6d25e026 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/SeersCourse.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/SeersCourse.java @@ -42,7 +42,7 @@ public Integer getRequiredLevel() @Override public boolean handleWalkToStart(WorldPoint playerWorldLocation) { - if (Microbot.getClient().getTopLevelWorldView().getPlane() != 0) + if (getClientPlane() != 0) { return false; } @@ -51,14 +51,14 @@ public boolean handleWalkToStart(WorldPoint playerWorldLocation) return false; } - if (Microbot.getVarbitValue(VarbitID.KANDARIN_DIARY_HARD_COMPLETE) == 1 + if (getVarbitValue(VarbitID.KANDARIN_DIARY_HARD_COMPLETE) == 1 && Rs2Magic.hasRequiredRunes(Rs2Spells.CAMELOT_TELEPORT) && playerWorldLocation.distanceTo(getStartPoint()) > 12) { Rs2Magic.cast(Rs2Spells.CAMELOT_TELEPORT, "Seers'", 2); return Global.sleepUntil(() -> { - WorldPoint currentLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); + WorldPoint currentLocation = getPlayerWorldLocation(); return currentLocation.distanceTo(getStartPoint()) <= 12; }, 5000); } diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/WerewolfCourse.java b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/WerewolfCourse.java index ddc02b8810..224e44b744 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/WerewolfCourse.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/WerewolfCourse.java @@ -56,12 +56,18 @@ public Integer getRequiredLevel() { return 60; } + @Override + public void reset() { + matchingObstacle = null; + matchingObject = null; + } + AgilityObstacleModel matchingObstacle; TileObject matchingObject; @Override public TileObject getCurrentObstacle() { - WorldPoint playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); + WorldPoint playerLocation = getPlayerWorldLocation(); List matchingObstacles = getObstacles().stream() .filter(o -> o.getOperationX().check(playerLocation.getX(), o.getRequiredX()) && o.getOperationY().check(playerLocation.getY(), o.getRequiredY())) From ac2c6afe58d5215a1a6964343a4bead1590a3b57 Mon Sep 17 00:00:00 2001 From: infuse21 Date: Tue, 30 Jun 2026 00:28:23 +0100 Subject: [PATCH 02/13] Agility plugin bug fixes --- .../microbot/agility/AgilityScript.java | 97 +++++++++++++++++-- 1 file changed, 90 insertions(+), 7 deletions(-) diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java b/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java index 405bf72474..47739fbae3 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java @@ -14,10 +14,10 @@ import net.runelite.client.plugins.microbot.agility.courses.WerewolfCourse; import net.runelite.client.plugins.microbot.agility.enums.AgilityCourse; import net.runelite.client.plugins.microbot.api.tileitem.models.Rs2TileItemModel; +import net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel; import net.runelite.client.plugins.microbot.util.antiban.Rs2Antiban; import net.runelite.client.plugins.microbot.util.antiban.Rs2AntibanSettings; import net.runelite.client.plugins.microbot.util.camera.Rs2Camera; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; 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.magic.Rs2Magic; @@ -44,7 +44,11 @@ public class AgilityScript extends Script long lastTimeoutWarning = 0; // For throttled timeout warnings private AgilityCourse activeCourse = null; private AgilityCourseHandler activeHandler = null; - private static final int MARK_OF_GRACE_SEARCH_DISTANCE = 8; + private static final int MARK_OF_GRACE_SEARCH_DISTANCE = 30; + private static final int MARK_OF_GRACE_PICKUP_TIMEOUT = 5000; + private WorldPoint pendingMarkOfGraceLocation = null; + private int pendingMarkOfGraceCount = 0; + private long pendingMarkOfGraceStartedAt = 0; private volatile boolean shuttingDown = false; @Inject @@ -67,6 +71,7 @@ public void shutdown() activeHandler = null; startPoint = null; initialPlayerLocation = null; + clearPendingMarkOfGrace(); if (mainScheduledFuture != null && !mainScheduledFuture.isDone()) { @@ -250,7 +255,7 @@ public boolean run() } // Normal obstacle interaction - if (Rs2GameObject.interact(gameObject)) { + if (interactWithObstacle(gameObject)) { // Wait for completion - this now returns quickly on XP drop boolean completed = courseHandler.waitForCompletion(agilityExp, Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation()).getPlane()); @@ -375,6 +380,28 @@ private AgilityCourseHandler getActiveHandler() private boolean lootMarksOfGrace(AgilityCourseHandler courseHandler) { + if (shuttingDown) + { + clearPendingMarkOfGrace(); + return false; + } + + if (pendingMarkOfGraceLocation != null) + { + if (markPickupResolved() || System.currentTimeMillis() - pendingMarkOfGraceStartedAt > MARK_OF_GRACE_PICKUP_TIMEOUT) + { + clearPendingMarkOfGrace(); + } + else if (Rs2Player.isMoving() || Rs2Player.isAnimating()) + { + return true; + } + else + { + clearPendingMarkOfGrace(); + } + } + if (Rs2Inventory.isFull() && !Rs2Inventory.contains(ItemID.GRACE)) { return false; @@ -392,7 +419,7 @@ private boolean lootMarksOfGrace(AgilityCourseHandler courseHandler) .where(Rs2TileItemModel::isLootAble) .where(item -> item.getWorldLocation() != null && item.getWorldLocation().getPlane() == playerLocation.getPlane()) .where(item -> item.getWorldLocation().distanceTo(playerLocation) <= MARK_OF_GRACE_SEARCH_DISTANCE) - .where(item -> Rs2GameObject.canReach(item.getWorldLocation())) + .where(item -> Rs2Walker.canReach(item.getWorldLocation())) .toList() .stream() .min(Comparator.comparingInt(item -> item.getWorldLocation().distanceTo(playerLocation))) @@ -403,15 +430,66 @@ private boolean lootMarksOfGrace(AgilityCourseHandler courseHandler) return false; } + if (Rs2Player.isMoving() || Rs2Player.isAnimating()) + { + return true; + } + + WorldPoint markLocation = markOfGrace.getWorldLocation(); + var markLocalLocation = markOfGrace.getLocalLocation(); + if (markLocation == null || markLocalLocation == null) + { + return false; + } + + if (!Rs2Camera.isTileOnScreen(markLocalLocation)) + { + Rs2Camera.turnTo(markLocalLocation); + sleep(300, 600); + return true; + } + + int markCount = Rs2Inventory.itemQuantity(ItemID.GRACE); if (!markOfGrace.pickup()) { return false; } + pendingMarkOfGraceLocation = markLocation; + pendingMarkOfGraceCount = markCount; + pendingMarkOfGraceStartedAt = System.currentTimeMillis(); - Rs2Player.waitForWalking(); + sleepUntil(() -> shuttingDown || markPickupResolved() || Rs2Player.isMoving(), 1800); + if (markPickupResolved() || shuttingDown) + { + clearPendingMarkOfGrace(); + } return true; } + private boolean markPickupResolved() + { + return pendingMarkOfGraceLocation != null + && (Rs2Inventory.itemQuantity(ItemID.GRACE) > pendingMarkOfGraceCount + || !hasLootableMarkAt(pendingMarkOfGraceLocation)); + } + + private void clearPendingMarkOfGrace() + { + pendingMarkOfGraceLocation = null; + pendingMarkOfGraceCount = 0; + pendingMarkOfGraceStartedAt = 0; + } + + private boolean hasLootableMarkAt(WorldPoint markLocation) + { + return Microbot.getRs2TileItemCache().query() + .fromWorldView() + .withId(ItemID.GRACE) + .where(Rs2TileItemModel::isLootAble) + .where(item -> markLocation.equals(item.getWorldLocation())) + .first() != null; + } + private boolean handleFood() { if (config.hitpoints() <= 0) @@ -497,11 +575,11 @@ private boolean performEfficientAlch(TileObject gameObject, String alchItem, int if (gameObject.getWorldLocation().distanceTo(playerLocation) >= 5) { // Efficient alching: click, alch, click - if (Rs2GameObject.interact(gameObject)) + if (interactWithObstacle(gameObject)) { sleep(100, 200); Rs2Magic.alch(alchItem, 50, 75); - Rs2GameObject.interact(gameObject); + interactWithObstacle(gameObject); boolean completed = getActiveHandler().waitForCompletion(agilityExp, Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation()).getPlane()); @@ -557,4 +635,9 @@ else if (!(courseHandler instanceof GnomeStrongholdCourse)) } return false; } + + private boolean interactWithObstacle(TileObject gameObject) + { + return gameObject != null && new Rs2TileObjectModel(gameObject).click(); + } } From 6c6e49867a842ecac15e7bc7ea16863607ebee4c Mon Sep 17 00:00:00 2001 From: infuse21 Date: Tue, 30 Jun 2026 00:36:54 +0100 Subject: [PATCH 03/13] Agility plugin bug fixes --- .../client/plugins/microbot/agility/AgilityScript.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java b/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java index 47739fbae3..c3ba5775fb 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java @@ -14,10 +14,10 @@ import net.runelite.client.plugins.microbot.agility.courses.WerewolfCourse; import net.runelite.client.plugins.microbot.agility.enums.AgilityCourse; import net.runelite.client.plugins.microbot.api.tileitem.models.Rs2TileItemModel; -import net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel; import net.runelite.client.plugins.microbot.util.antiban.Rs2Antiban; import net.runelite.client.plugins.microbot.util.antiban.Rs2AntibanSettings; import net.runelite.client.plugins.microbot.util.camera.Rs2Camera; +import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; 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.magic.Rs2Magic; @@ -638,6 +638,6 @@ else if (!(courseHandler instanceof GnomeStrongholdCourse)) private boolean interactWithObstacle(TileObject gameObject) { - return gameObject != null && new Rs2TileObjectModel(gameObject).click(); + return Rs2GameObject.interact(gameObject); } } From 30134066cdd90e3eafd0e329cacbd9c20f6847ed Mon Sep 17 00:00:00 2001 From: infuse21 Date: Tue, 30 Jun 2026 01:49:24 +0100 Subject: [PATCH 04/13] Agility plugin bug fixes - added config safety options, Bank for more food under ~ , and a optional wait till hp % if you run out of food to save script shutdown / optional banking for stamina pots and food & summer pies. banking only on plane 0. --- .../microbot/agility/AgilityScript.java | 341 +++++++++++++++++- .../microbot/agility/MicroAgilityConfig.java | 146 +++++++- .../microbot/agility/MicroAgilityPlugin.java | 2 +- .../agility/courses/PyramidCourse.java | 27 +- .../agility/enums/AgilityFoodOption.java | 55 +++ .../agility/enums/AgilityPotionOption.java | 34 ++ 6 files changed, 601 insertions(+), 4 deletions(-) create mode 100644 src/main/java/net/runelite/client/plugins/microbot/agility/enums/AgilityFoodOption.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/agility/enums/AgilityPotionOption.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java b/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java index c3ba5775fb..4f92e94bd9 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java @@ -13,14 +13,18 @@ import net.runelite.client.plugins.microbot.agility.courses.PrifddinasCourse; import net.runelite.client.plugins.microbot.agility.courses.WerewolfCourse; import net.runelite.client.plugins.microbot.agility.enums.AgilityCourse; +import net.runelite.client.plugins.microbot.agility.enums.AgilityFoodOption; +import net.runelite.client.plugins.microbot.agility.enums.AgilityPotionOption; import net.runelite.client.plugins.microbot.api.tileitem.models.Rs2TileItemModel; import net.runelite.client.plugins.microbot.util.antiban.Rs2Antiban; import net.runelite.client.plugins.microbot.util.antiban.Rs2AntibanSettings; +import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; import net.runelite.client.plugins.microbot.util.camera.Rs2Camera; import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; 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.magic.Rs2Magic; +import net.runelite.client.plugins.microbot.util.misc.Rs2Food; import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; @@ -46,9 +50,19 @@ public class AgilityScript extends Script private AgilityCourseHandler activeHandler = null; private static final int MARK_OF_GRACE_SEARCH_DISTANCE = 30; private static final int MARK_OF_GRACE_PICKUP_TIMEOUT = 5000; + private static final long BANK_RETRY_COOLDOWN_MS = 30_000; + private static final int SUMMER_PIE_ID = ItemID.SUMMER_PIE; + private static final int[] STAMINA_POTION_IDS = { + ItemID._4DOSESTAMINA, + ItemID._3DOSESTAMINA, + ItemID._2DOSESTAMINA, + ItemID._1DOSESTAMINA + }; private WorldPoint pendingMarkOfGraceLocation = null; private int pendingMarkOfGraceCount = 0; private long pendingMarkOfGraceStartedAt = 0; + private long lastBankFailureAt = 0; + private boolean lastFoodBankUnavailable = false; private volatile boolean shuttingDown = false; @Inject @@ -71,6 +85,8 @@ public void shutdown() activeHandler = null; startPoint = null; initialPlayerLocation = null; + lastBankFailureAt = 0; + lastFoodBankUnavailable = false; clearPendingMarkOfGrace(); if (mainScheduledFuture != null && !mainScheduledFuture.isDone()) @@ -126,6 +142,7 @@ public boolean run() return; } AgilityCourseHandler courseHandler = getActiveHandler(); + final WorldPoint playerWorldLocation = Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation()); if (startPoint == null) { @@ -141,6 +158,12 @@ public boolean run() return; } + if (!plugin.hasRequiredLevel() && handleBanking(courseHandler, playerWorldLocation)) + { + Microbot.log("Early return: Banking agility supplies"); + return; + } + if (!plugin.hasRequiredLevel()) { Microbot.log("Early return: Required level not met"); @@ -166,7 +189,6 @@ public boolean run() Microbot.log("Early return: Action cooldown active"); return; } - final WorldPoint playerWorldLocation = Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation()); final int currentAgilityXp = Microbot.getClient().getSkillExperience(Skill.AGILITY); if (handleFood()) @@ -174,6 +196,16 @@ public boolean run() Microbot.log("Early return: Handling food"); return; } + if (handleBanking(courseHandler, playerWorldLocation)) + { + Microbot.log("Early return: Banking agility supplies"); + return; + } + if (handleHealthSafety()) + { + Microbot.log("Early return: Waiting for safe hitpoints"); + return; + } if (lootMarksOfGrace(courseHandler)) { Microbot.log("Early return: Looting marks of grace"); @@ -374,6 +406,8 @@ private AgilityCourseHandler getActiveHandler() activeHandler.reset(); startPoint = activeHandler.getStartPoint(); lastAgilityXp = Microbot.getClient().getSkillExperience(Skill.AGILITY); + lastBankFailureAt = 0; + lastFoodBankUnavailable = false; } return activeHandler; } @@ -505,6 +539,10 @@ private boolean handleFood() List foodItems = plugin.getInventoryFood(); if (foodItems.isEmpty()) { + if (needsFoodBanking() || config.hpSafetyWait()) + { + return false; + } Microbot.showMessage("Hitpoints are below the configured threshold and no food was found. Stopping agility."); shutdown(); return true; @@ -521,6 +559,307 @@ private boolean handleFood() return true; } + private boolean handleHealthSafety() + { + if (!shouldWaitForHealthInsteadOfBanking()) + { + return false; + } + + int resumeAt = Math.max(config.hpSafetyResumeAt(), config.hpSafetyWaitBelow()); + Microbot.status = "Waiting for hitpoints"; + sleepUntil(() -> shuttingDown + || !plugin.getInventoryFood().isEmpty() + || Rs2Player.getHealthPercentage() >= resumeAt, 10_000); + return true; + } + + private boolean handleBanking(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation) + { + if (!shouldBankForSupplies(courseHandler)) + { + return false; + } + if (shouldWaitForHealthInsteadOfBanking()) + { + return false; + } + if (Rs2Player.isMoving() || Rs2Player.isAnimating()) + { + return true; + } + if (!canAttemptBanking(courseHandler, playerWorldLocation)) + { + Microbot.status = "Finishing course before banking"; + return false; + } + + long now = System.currentTimeMillis(); + if (lastBankFailureAt > 0 && now - lastBankFailureAt < BANK_RETRY_COOLDOWN_MS) + { + Microbot.status = "Waiting before retrying bank"; + return true; + } + + Microbot.status = "Banking agility supplies"; + lastFoodBankUnavailable = false; + if (!Rs2Bank.walkToBankAndUseBank()) + { + lastBankFailureAt = now; + Microbot.log("Unable to reach or open bank for agility supplies. Retrying after cooldown."); + return true; + } + + boolean success = withdrawConfiguredSupplies(courseHandler); + Rs2Bank.closeBank(); + lastBankFailureAt = success ? 0 : System.currentTimeMillis(); + return true; + } + + private boolean withdrawConfiguredSupplies(AgilityCourseHandler courseHandler) + { + boolean success = true; + if (needsFoodBanking()) + { + success &= withdrawConfiguredFood(); + } + if (needsPotionBanking()) + { + success &= withdrawConfiguredPotion(); + } + if (needsSummerPieBanking(courseHandler)) + { + success &= withdrawSummerPies(); + } + return success; + } + + private boolean withdrawConfiguredFood() + { + int targetAmount = config.foodWithdrawAmount(); + int currentFood = plugin.getInventoryFood().size(); + int missingFood = targetAmount - currentFood; + if (missingFood <= 0) + { + return true; + } + if (Rs2Inventory.emptySlotCount() <= 0) + { + Microbot.showMessage("Inventory is full. Unable to withdraw agility food."); + return false; + } + + int amountToWithdraw = Math.min(missingFood, Rs2Inventory.emptySlotCount()); + + AgilityFoodOption option = config.bankFood(); + if (option.isAuto()) + { + return withdrawAutoFood(amountToWithdraw, currentFood); + } + + Rs2Food food = option.getFood(); + if (food == null) + { + return false; + } + if (!Rs2Bank.hasBankItem(food.getId(), amountToWithdraw)) + { + lastFoodBankUnavailable = true; + Microbot.showMessage("No " + food.getName() + " found in the bank."); + return false; + } + + if (!Rs2Bank.withdrawX(food.getId(), amountToWithdraw)) + { + return false; + } + return sleepUntil(() -> plugin.getInventoryFood().size() >= currentFood + amountToWithdraw, 2400); + } + + private boolean withdrawAutoFood(int amountToWithdraw, int currentFood) + { + int remaining = amountToWithdraw; + int expectedFood = currentFood; + + List foodOptions = Arrays.stream(AgilityFoodOption.values()) + .filter(foodOption -> !foodOption.isNone() && !foodOption.isAuto()) + .filter(foodOption -> foodOption.getFood() != null) + .sorted(Comparator.comparingInt((AgilityFoodOption foodOption) -> foodOption.getFood().getHeal()).reversed()) + .collect(Collectors.toList()); + + for (AgilityFoodOption option : foodOptions) + { + if (remaining <= 0 || Rs2Inventory.emptySlotCount() <= 0) + { + break; + } + + Rs2Food food = option.getFood(); + int available = getBankItemQuantity(food.getId()); + if (available <= 0) + { + continue; + } + + int withdrawAmount = Math.min(Math.min(remaining, available), Rs2Inventory.emptySlotCount()); + if (!Rs2Bank.withdrawX(food.getId(), withdrawAmount)) + { + return false; + } + + int expectedAfterWithdraw = expectedFood + withdrawAmount; + if (!sleepUntil(() -> plugin.getInventoryFood().size() >= expectedAfterWithdraw, 2400)) + { + return false; + } + expectedFood = expectedAfterWithdraw; + remaining = currentFood + amountToWithdraw - plugin.getInventoryFood().size(); + } + + if (remaining > 0) + { + lastFoodBankUnavailable = true; + Microbot.showMessage("Not enough configured food was found in the bank."); + return false; + } + return true; + } + + private boolean withdrawConfiguredPotion() + { + AgilityPotionOption option = config.bankPotion(); + int targetAmount = config.potionWithdrawAmount(); + int currentPotions = getStaminaPotionCount(); + int missingPotions = targetAmount - currentPotions; + if (missingPotions <= 0) + { + return true; + } + if (Rs2Inventory.emptySlotCount() <= 0) + { + Microbot.showMessage("Inventory is full. Unable to withdraw agility potions."); + return false; + } + + int amountToWithdraw = Math.min(missingPotions, Rs2Inventory.emptySlotCount()); + if (option.isNone() || option.getItemId() <= 0) + { + return false; + } + if (!Rs2Bank.hasBankItem(option.getItemId(), amountToWithdraw)) + { + Microbot.showMessage("No " + option + " found in the bank."); + return false; + } + + if (!Rs2Bank.withdrawX(option.getItemId(), amountToWithdraw)) + { + return false; + } + return sleepUntil(() -> getStaminaPotionCount() >= currentPotions + amountToWithdraw, 2400); + } + + private boolean withdrawSummerPies() + { + int targetAmount = config.summerPieWithdrawAmount(); + int currentPies = plugin.getSummerPies().size(); + int missingPies = targetAmount - currentPies; + if (missingPies <= 0) + { + return true; + } + if (Rs2Inventory.emptySlotCount() <= 0) + { + Microbot.showMessage("Inventory is full. Unable to withdraw summer pies."); + return false; + } + + int amountToWithdraw = Math.min(missingPies, Rs2Inventory.emptySlotCount()); + if (!Rs2Bank.hasBankItem(SUMMER_PIE_ID, amountToWithdraw)) + { + Microbot.showMessage("No summer pies found in the bank."); + return false; + } + if (!Rs2Bank.withdrawX(SUMMER_PIE_ID, amountToWithdraw)) + { + return false; + } + return sleepUntil(() -> plugin.getSummerPies().size() >= currentPies + amountToWithdraw, 2400); + } + + private boolean shouldBankForSupplies(AgilityCourseHandler courseHandler) + { + return needsFoodBanking() || needsPotionBanking() || needsSummerPieBanking(courseHandler); + } + + private boolean needsFoodBanking() + { + AgilityFoodOption food = config.bankFood(); + return isFoodBankingConfigured() + && plugin.getInventoryFood().size() <= config.bankFoodAt() + && plugin.getInventoryFood().size() < config.foodWithdrawAmount(); + } + + private boolean isFoodBankingConfigured() + { + AgilityFoodOption food = config.bankFood(); + return !food.isNone() + && config.foodWithdrawAmount() > 0 + && config.bankFoodAt() > 0; + } + + private boolean needsPotionBanking() + { + AgilityPotionOption potion = config.bankPotion(); + return !potion.isNone() + && config.potionWithdrawAmount() > 0 + && getStaminaPotionCount() < config.potionWithdrawAmount(); + } + + private boolean needsSummerPieBanking(AgilityCourseHandler courseHandler) + { + return courseHandler != null + && courseHandler.canBeBoosted() + && config.summerPieWithdrawAmount() > 0 + && plugin.getSummerPies().size() < config.summerPieWithdrawAmount(); + } + + private int getStaminaPotionCount() + { + return Rs2Inventory.count(item -> item != null && Arrays.stream(STAMINA_POTION_IDS).anyMatch(id -> id == item.getId())); + } + + private boolean shouldWaitForHealthInsteadOfBanking() + { + return config.hpSafetyWait() + && plugin.getInventoryFood().isEmpty() + && Rs2Player.getHealthPercentage() < config.hpSafetyWaitBelow() + && (!isFoodBankingConfigured() || lastFoodBankUnavailable); + } + + private boolean canAttemptBanking(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation) + { + if (!config.bankOnlyAtCourseStart()) + { + return true; + } + if (courseHandler == null || playerWorldLocation == null || courseHandler.getStartPoint() == null) + { + return false; + } + return courseHandler.getClientPlane() == 0 + && courseHandler.getCurrentObstacleIndex() == 0 + && playerWorldLocation.distanceTo(courseHandler.getStartPoint()) <= 12; + } + + private int getBankItemQuantity(int itemId) + { + return Rs2Bank.bankItems().stream() + .filter(item -> item != null && item.getId() == itemId) + .mapToInt(Rs2ItemModel::getQuantity) + .sum(); + } + private boolean handleSummerPies(AgilityCourseHandler courseHandler) { if (!courseHandler.canBeBoosted()) diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityConfig.java b/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityConfig.java index 43ae6b1155..ebc99d3b02 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityConfig.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityConfig.java @@ -2,6 +2,8 @@ import net.runelite.client.config.*; import net.runelite.client.plugins.microbot.agility.enums.AgilityCourse; +import net.runelite.client.plugins.microbot.agility.enums.AgilityFoodOption; +import net.runelite.client.plugins.microbot.agility.enums.AgilityPotionOption; @ConfigGroup("MicroAgility") @ConfigInformation("Enable the plugin near the start of your selected agility course.
" + @@ -26,6 +28,22 @@ public interface MicroAgilityConfig extends Config ) String generalSection = "general"; + @ConfigSection( + name = "Banking", + description = "Banking", + position = 1, + closedByDefault = true + ) + String bankingSection = "banking"; + + @ConfigSection( + name = "Safety", + description = "Safety", + position = 2, + closedByDefault = true + ) + String safetySection = "safety"; + @ConfigItem( keyName = selectedCourse, name = "Course", @@ -50,6 +68,132 @@ default int hitpoints() return 20; } + @ConfigItem( + keyName = "bankFood", + name = "Food", + description = "Food to withdraw when banking. None disables food banking. Auto withdraws the best available configured food.", + position = 1, + section = bankingSection + ) + default AgilityFoodOption bankFood() + { + return AgilityFoodOption.NONE; + } + + @ConfigItem( + keyName = "foodWithdrawAmount", + name = "Food amount", + description = "Target amount of food to have after banking.", + position = 2, + section = bankingSection + ) + @Range(min = 0, max = 28) + default int foodWithdrawAmount() + { + return 0; + } + + @ConfigItem( + keyName = "bankFoodAt", + name = "Bank under food", + description = "Bank when inventory food count is at or below this amount. 0 disables food banking triggers.", + position = 3, + section = bankingSection + ) + @Range(min = 0, max = 28) + default int bankFoodAt() + { + return 0; + } + + @ConfigItem( + keyName = "bankPotion", + name = "Potion", + description = "Potion to withdraw when banking. None disables potion banking.", + position = 4, + section = bankingSection + ) + default AgilityPotionOption bankPotion() + { + return AgilityPotionOption.NONE; + } + + @ConfigItem( + keyName = "potionWithdrawAmount", + name = "Potion amount", + description = "Target amount of selected potion to have after banking.", + position = 5, + section = bankingSection + ) + @Range(min = 0, max = 28) + default int potionWithdrawAmount() + { + return 0; + } + + @ConfigItem( + keyName = "bankOnlyAtCourseStart", + name = "Bank at course start", + description = "Only bank from the ground near the course start. This avoids asking the walker to route from rooftops or awkward course positions.", + position = 6, + section = bankingSection + ) + default boolean bankOnlyAtCourseStart() + { + return true; + } + + @ConfigItem( + keyName = "summerPieWithdrawAmount", + name = "Summer pie amount", + description = "Target amount of summer pies to withdraw when banking. 0 disables summer pie banking.", + position = 7, + section = bankingSection + ) + @Range(min = 0, max = 28) + default int summerPieWithdrawAmount() + { + return 0; + } + + @ConfigItem( + keyName = "hpSafetyWait", + name = "Wait when unsafe", + description = "If out of food and HP is too low, wait instead of stopping or continuing.", + position = 1, + section = safetySection + ) + default boolean hpSafetyWait() + { + return false; + } + + @ConfigItem( + keyName = "hpSafetyWaitBelow", + name = "Wait below HP", + description = "When out of food, wait if HP percentage is below this value.", + position = 2, + section = safetySection + ) + @Range(min = 1, max = 99) + default int hpSafetyWaitBelow() + { + return 20; + } + + @ConfigItem( + keyName = "hpSafetyResumeAt", + name = "Resume at HP", + description = "Resume once HP percentage is at or above this value.", + position = 3, + section = safetySection + ) + @Range(min = 1, max = 100) + default int hpSafetyResumeAt() + { + return 50; + } + @ConfigItem( keyName = shouldAlch, name = "Alch", @@ -110,4 +254,4 @@ default int alchSkipChance() { return 5; } -} \ No newline at end of file +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityPlugin.java index 66dca5fa31..62abbd281b 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityPlugin.java @@ -34,7 +34,7 @@ @Slf4j public class MicroAgilityPlugin extends Plugin { - public static final String version = "1.2.8"; + public static final String version = "1.3.0"; @Inject private MicroAgilityConfig config; @Inject diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/PyramidCourse.java b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/PyramidCourse.java index a02b7c8569..b1bccab324 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/PyramidCourse.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/PyramidCourse.java @@ -28,9 +28,11 @@ public class PyramidCourse implements AgilityCourseHandler { private static final WorldPoint SIMON_LOCATION = new WorldPoint(3343, 2827, 0); private static final String SIMON_NAME = "Simon Templeton"; private static final int PYRAMID_TOP_REGION = 12105; + private static final long START_WALK_FAILURE_COOLDOWN_MS = 30_000; // Centralized state tracking private final PyramidState state = new PyramidState(); + private long lastStartWalkFailureAt = 0; // Obstacle areas are now defined in PyramidObstacleData for better maintainability @@ -64,6 +66,7 @@ public List getObstacles() { @Override public void reset() { state.reset(); + lastStartWalkFailureAt = 0; } @Override @@ -843,12 +846,34 @@ public boolean handleWalkToStart(WorldPoint playerLocation) { if (log.isDebugEnabled()) { log.debug("Walking to pyramid start point - stairs not reachable directly (distance: {})", distanceToStart); } - Rs2Walker.walkTo(START_POINT, 2); + if (shouldRetryStartWalk()) + { + boolean arrived = Rs2Walker.walkTo(START_POINT, 2); + if (!arrived) + { + lastStartWalkFailureAt = System.currentTimeMillis(); + log.warn("Pyramid start walk failed; backing off for {} seconds before retrying", START_WALK_FAILURE_COOLDOWN_MS / 1000); + } + } return true; } } return false; } + + private boolean shouldRetryStartWalk() + { + long elapsed = System.currentTimeMillis() - lastStartWalkFailureAt; + if (elapsed < START_WALK_FAILURE_COOLDOWN_MS) + { + if (log.isDebugEnabled()) + { + log.debug("Skipping pyramid start walk retry for {}ms after failed walker route", START_WALK_FAILURE_COOLDOWN_MS - elapsed); + } + return false; + } + return true; + } @Override public boolean waitForCompletion(int agilityExp, int plane) { diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/enums/AgilityFoodOption.java b/src/main/java/net/runelite/client/plugins/microbot/agility/enums/AgilityFoodOption.java new file mode 100644 index 0000000000..a9e4b97897 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/enums/AgilityFoodOption.java @@ -0,0 +1,55 @@ +package net.runelite.client.plugins.microbot.agility.enums; + +import lombok.Getter; +import net.runelite.client.plugins.microbot.util.misc.Rs2Food; + +@Getter +public enum AgilityFoodOption +{ + NONE("None", null, false), + AUTO("Auto (best available)", null, true), + DARK_CRAB(Rs2Food.Dark_Crab), + ROCKTAIL(Rs2Food.ROCKTAIL), + MANTA_RAY(Rs2Food.MANTA), + SEA_TURTLE(Rs2Food.SEA_TURTLE), + TUNA_POTATO(Rs2Food.TUNA_POTATO), + SHARK(Rs2Food.SHARK), + COOKED_MOONLIGHT_ANTELOPE(Rs2Food.COOKED_MOONLIGHT_ANTELOPE), + COOKED_DASHING_KEBBIT(Rs2Food.COOKED_DASHING_KEBBIT), + COOKED_SUNLIGHT_ANTELOPE(Rs2Food.COOKED_SUNLIGHT_ANTELOPE), + MONKFISH(Rs2Food.MONKFISH), + SWORDFISH(Rs2Food.SWORDFISH), + LOBSTER(Rs2Food.LOBSTER), + JUG_OF_WINE(Rs2Food.JUG_OF_WINE), + TUNA(Rs2Food.TUNA), + SALMON(Rs2Food.SALMON), + TROUT(Rs2Food.TROUT), + CAKE(Rs2Food.CAKE); + + private final String displayName; + private final Rs2Food food; + private final boolean auto; + + AgilityFoodOption(Rs2Food food) + { + this(food.getName(), food, false); + } + + AgilityFoodOption(String displayName, Rs2Food food, boolean auto) + { + this.displayName = displayName; + this.food = food; + this.auto = auto; + } + + public boolean isNone() + { + return this == NONE; + } + + @Override + public String toString() + { + return displayName; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/enums/AgilityPotionOption.java b/src/main/java/net/runelite/client/plugins/microbot/agility/enums/AgilityPotionOption.java new file mode 100644 index 0000000000..2dbcf1606c --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/enums/AgilityPotionOption.java @@ -0,0 +1,34 @@ +package net.runelite.client.plugins.microbot.agility.enums; + +import lombok.Getter; +import net.runelite.api.gameval.ItemID; + +@Getter +public enum AgilityPotionOption +{ + NONE("None", -1), + STAMINA_4("Stamina potion(4)", ItemID._4DOSESTAMINA), + STAMINA_3("Stamina potion(3)", ItemID._3DOSESTAMINA), + STAMINA_2("Stamina potion(2)", ItemID._2DOSESTAMINA), + STAMINA_1("Stamina potion(1)", ItemID._1DOSESTAMINA); + + private final String displayName; + private final int itemId; + + AgilityPotionOption(String displayName, int itemId) + { + this.displayName = displayName; + this.itemId = itemId; + } + + public boolean isNone() + { + return this == NONE; + } + + @Override + public String toString() + { + return displayName; + } +} From 273b38290a5607ed0fc1224b96da63f0268364fa Mon Sep 17 00:00:00 2001 From: infuse21 Date: Tue, 30 Jun 2026 02:20:19 +0100 Subject: [PATCH 05/13] Agility plugin bug fixes - moved supply state and all supply actions into AgilitySupplyManager --- .../microbot/agility/AgilityScript.java | 424 +-------------- .../agility/AgilitySupplyManager.java | 507 ++++++++++++++++++ .../microbot/agility/MicroAgilityPlugin.java | 2 +- 3 files changed, 528 insertions(+), 405 deletions(-) create mode 100644 src/main/java/net/runelite/client/plugins/microbot/agility/AgilitySupplyManager.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java b/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java index 4f92e94bd9..29e849f349 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java @@ -13,18 +13,13 @@ import net.runelite.client.plugins.microbot.agility.courses.PrifddinasCourse; import net.runelite.client.plugins.microbot.agility.courses.WerewolfCourse; import net.runelite.client.plugins.microbot.agility.enums.AgilityCourse; -import net.runelite.client.plugins.microbot.agility.enums.AgilityFoodOption; -import net.runelite.client.plugins.microbot.agility.enums.AgilityPotionOption; import net.runelite.client.plugins.microbot.api.tileitem.models.Rs2TileItemModel; import net.runelite.client.plugins.microbot.util.antiban.Rs2Antiban; import net.runelite.client.plugins.microbot.util.antiban.Rs2AntibanSettings; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; import net.runelite.client.plugins.microbot.util.camera.Rs2Camera; import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; 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.magic.Rs2Magic; -import net.runelite.client.plugins.microbot.util.misc.Rs2Food; import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; @@ -50,19 +45,10 @@ public class AgilityScript extends Script private AgilityCourseHandler activeHandler = null; private static final int MARK_OF_GRACE_SEARCH_DISTANCE = 30; private static final int MARK_OF_GRACE_PICKUP_TIMEOUT = 5000; - private static final long BANK_RETRY_COOLDOWN_MS = 30_000; - private static final int SUMMER_PIE_ID = ItemID.SUMMER_PIE; - private static final int[] STAMINA_POTION_IDS = { - ItemID._4DOSESTAMINA, - ItemID._3DOSESTAMINA, - ItemID._2DOSESTAMINA, - ItemID._1DOSESTAMINA - }; private WorldPoint pendingMarkOfGraceLocation = null; private int pendingMarkOfGraceCount = 0; private long pendingMarkOfGraceStartedAt = 0; - private long lastBankFailureAt = 0; - private boolean lastFoodBankUnavailable = false; + private final AgilitySupplyManager supplyManager; private volatile boolean shuttingDown = false; @Inject @@ -70,6 +56,7 @@ public AgilityScript(MicroAgilityPlugin plugin, MicroAgilityConfig config) { this.plugin = plugin; this.config = config; + this.supplyManager = new AgilitySupplyManager(plugin, config, this::isShuttingDown, this::shutdown); } @Override @@ -85,8 +72,7 @@ public void shutdown() activeHandler = null; startPoint = null; initialPlayerLocation = null; - lastBankFailureAt = 0; - lastFoodBankUnavailable = false; + supplyManager.reset(); clearPendingMarkOfGrace(); if (mainScheduledFuture != null && !mainScheduledFuture.isDone()) @@ -152,18 +138,31 @@ public boolean run() return; } - if (handleSummerPies(courseHandler)) + if (supplyManager.handleSummerPies(courseHandler)) { Microbot.log("Early return: Handling summer pies"); return; } - if (!plugin.hasRequiredLevel() && handleBanking(courseHandler, playerWorldLocation)) + if (supplyManager.handleFoodOrHealthSafety()) + { + Microbot.log("Early return: Handling agility safety"); + return; + } + + boolean hasRequiredLevel = plugin.hasRequiredLevel(); + if (supplyManager.handlePreLevelCheck(courseHandler, playerWorldLocation, hasRequiredLevel)) { Microbot.log("Early return: Banking agility supplies"); return; } + if (supplyManager.handleBeforeObstacle(courseHandler, playerWorldLocation)) + { + Microbot.log("Early return: Handling agility supplies"); + return; + } + if (!plugin.hasRequiredLevel()) { Microbot.log("Early return: Required level not met"); @@ -171,7 +170,7 @@ public boolean run() shutdown(); return; } - + // Check coin requirement for BrimhavenSpike course (only before payment) if (courseHandler instanceof BrimhavenSpikeCourse) { @@ -191,21 +190,6 @@ public boolean run() } final int currentAgilityXp = Microbot.getClient().getSkillExperience(Skill.AGILITY); - if (handleFood()) - { - Microbot.log("Early return: Handling food"); - return; - } - if (handleBanking(courseHandler, playerWorldLocation)) - { - Microbot.log("Early return: Banking agility supplies"); - return; - } - if (handleHealthSafety()) - { - Microbot.log("Early return: Waiting for safe hitpoints"); - return; - } if (lootMarksOfGrace(courseHandler)) { Microbot.log("Early return: Looting marks of grace"); @@ -406,8 +390,7 @@ private AgilityCourseHandler getActiveHandler() activeHandler.reset(); startPoint = activeHandler.getStartPoint(); lastAgilityXp = Microbot.getClient().getSkillExperience(Skill.AGILITY); - lastBankFailureAt = 0; - lastFoodBankUnavailable = false; + supplyManager.reset(); } return activeHandler; } @@ -524,373 +507,6 @@ private boolean hasLootableMarkAt(WorldPoint markLocation) .first() != null; } - private boolean handleFood() - { - if (config.hitpoints() <= 0) - { - return false; - } - - if (Rs2Player.getHealthPercentage() > config.hitpoints()) - { - return false; - } - - List foodItems = plugin.getInventoryFood(); - if (foodItems.isEmpty()) - { - if (needsFoodBanking() || config.hpSafetyWait()) - { - return false; - } - Microbot.showMessage("Hitpoints are below the configured threshold and no food was found. Stopping agility."); - shutdown(); - return true; - } - Rs2ItemModel foodItem = foodItems.get(0); - - Rs2Inventory.interact(foodItem, foodItem.getName().toLowerCase().contains("jug of wine") ? "drink" : "eat"); - Rs2Inventory.waitForInventoryChanges(1800); - - if (Rs2Inventory.contains(ItemID.JUG_EMPTY)) - { - Rs2Inventory.dropAll(ItemID.JUG_EMPTY); - } - return true; - } - - private boolean handleHealthSafety() - { - if (!shouldWaitForHealthInsteadOfBanking()) - { - return false; - } - - int resumeAt = Math.max(config.hpSafetyResumeAt(), config.hpSafetyWaitBelow()); - Microbot.status = "Waiting for hitpoints"; - sleepUntil(() -> shuttingDown - || !plugin.getInventoryFood().isEmpty() - || Rs2Player.getHealthPercentage() >= resumeAt, 10_000); - return true; - } - - private boolean handleBanking(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation) - { - if (!shouldBankForSupplies(courseHandler)) - { - return false; - } - if (shouldWaitForHealthInsteadOfBanking()) - { - return false; - } - if (Rs2Player.isMoving() || Rs2Player.isAnimating()) - { - return true; - } - if (!canAttemptBanking(courseHandler, playerWorldLocation)) - { - Microbot.status = "Finishing course before banking"; - return false; - } - - long now = System.currentTimeMillis(); - if (lastBankFailureAt > 0 && now - lastBankFailureAt < BANK_RETRY_COOLDOWN_MS) - { - Microbot.status = "Waiting before retrying bank"; - return true; - } - - Microbot.status = "Banking agility supplies"; - lastFoodBankUnavailable = false; - if (!Rs2Bank.walkToBankAndUseBank()) - { - lastBankFailureAt = now; - Microbot.log("Unable to reach or open bank for agility supplies. Retrying after cooldown."); - return true; - } - - boolean success = withdrawConfiguredSupplies(courseHandler); - Rs2Bank.closeBank(); - lastBankFailureAt = success ? 0 : System.currentTimeMillis(); - return true; - } - - private boolean withdrawConfiguredSupplies(AgilityCourseHandler courseHandler) - { - boolean success = true; - if (needsFoodBanking()) - { - success &= withdrawConfiguredFood(); - } - if (needsPotionBanking()) - { - success &= withdrawConfiguredPotion(); - } - if (needsSummerPieBanking(courseHandler)) - { - success &= withdrawSummerPies(); - } - return success; - } - - private boolean withdrawConfiguredFood() - { - int targetAmount = config.foodWithdrawAmount(); - int currentFood = plugin.getInventoryFood().size(); - int missingFood = targetAmount - currentFood; - if (missingFood <= 0) - { - return true; - } - if (Rs2Inventory.emptySlotCount() <= 0) - { - Microbot.showMessage("Inventory is full. Unable to withdraw agility food."); - return false; - } - - int amountToWithdraw = Math.min(missingFood, Rs2Inventory.emptySlotCount()); - - AgilityFoodOption option = config.bankFood(); - if (option.isAuto()) - { - return withdrawAutoFood(amountToWithdraw, currentFood); - } - - Rs2Food food = option.getFood(); - if (food == null) - { - return false; - } - if (!Rs2Bank.hasBankItem(food.getId(), amountToWithdraw)) - { - lastFoodBankUnavailable = true; - Microbot.showMessage("No " + food.getName() + " found in the bank."); - return false; - } - - if (!Rs2Bank.withdrawX(food.getId(), amountToWithdraw)) - { - return false; - } - return sleepUntil(() -> plugin.getInventoryFood().size() >= currentFood + amountToWithdraw, 2400); - } - - private boolean withdrawAutoFood(int amountToWithdraw, int currentFood) - { - int remaining = amountToWithdraw; - int expectedFood = currentFood; - - List foodOptions = Arrays.stream(AgilityFoodOption.values()) - .filter(foodOption -> !foodOption.isNone() && !foodOption.isAuto()) - .filter(foodOption -> foodOption.getFood() != null) - .sorted(Comparator.comparingInt((AgilityFoodOption foodOption) -> foodOption.getFood().getHeal()).reversed()) - .collect(Collectors.toList()); - - for (AgilityFoodOption option : foodOptions) - { - if (remaining <= 0 || Rs2Inventory.emptySlotCount() <= 0) - { - break; - } - - Rs2Food food = option.getFood(); - int available = getBankItemQuantity(food.getId()); - if (available <= 0) - { - continue; - } - - int withdrawAmount = Math.min(Math.min(remaining, available), Rs2Inventory.emptySlotCount()); - if (!Rs2Bank.withdrawX(food.getId(), withdrawAmount)) - { - return false; - } - - int expectedAfterWithdraw = expectedFood + withdrawAmount; - if (!sleepUntil(() -> plugin.getInventoryFood().size() >= expectedAfterWithdraw, 2400)) - { - return false; - } - expectedFood = expectedAfterWithdraw; - remaining = currentFood + amountToWithdraw - plugin.getInventoryFood().size(); - } - - if (remaining > 0) - { - lastFoodBankUnavailable = true; - Microbot.showMessage("Not enough configured food was found in the bank."); - return false; - } - return true; - } - - private boolean withdrawConfiguredPotion() - { - AgilityPotionOption option = config.bankPotion(); - int targetAmount = config.potionWithdrawAmount(); - int currentPotions = getStaminaPotionCount(); - int missingPotions = targetAmount - currentPotions; - if (missingPotions <= 0) - { - return true; - } - if (Rs2Inventory.emptySlotCount() <= 0) - { - Microbot.showMessage("Inventory is full. Unable to withdraw agility potions."); - return false; - } - - int amountToWithdraw = Math.min(missingPotions, Rs2Inventory.emptySlotCount()); - if (option.isNone() || option.getItemId() <= 0) - { - return false; - } - if (!Rs2Bank.hasBankItem(option.getItemId(), amountToWithdraw)) - { - Microbot.showMessage("No " + option + " found in the bank."); - return false; - } - - if (!Rs2Bank.withdrawX(option.getItemId(), amountToWithdraw)) - { - return false; - } - return sleepUntil(() -> getStaminaPotionCount() >= currentPotions + amountToWithdraw, 2400); - } - - private boolean withdrawSummerPies() - { - int targetAmount = config.summerPieWithdrawAmount(); - int currentPies = plugin.getSummerPies().size(); - int missingPies = targetAmount - currentPies; - if (missingPies <= 0) - { - return true; - } - if (Rs2Inventory.emptySlotCount() <= 0) - { - Microbot.showMessage("Inventory is full. Unable to withdraw summer pies."); - return false; - } - - int amountToWithdraw = Math.min(missingPies, Rs2Inventory.emptySlotCount()); - if (!Rs2Bank.hasBankItem(SUMMER_PIE_ID, amountToWithdraw)) - { - Microbot.showMessage("No summer pies found in the bank."); - return false; - } - if (!Rs2Bank.withdrawX(SUMMER_PIE_ID, amountToWithdraw)) - { - return false; - } - return sleepUntil(() -> plugin.getSummerPies().size() >= currentPies + amountToWithdraw, 2400); - } - - private boolean shouldBankForSupplies(AgilityCourseHandler courseHandler) - { - return needsFoodBanking() || needsPotionBanking() || needsSummerPieBanking(courseHandler); - } - - private boolean needsFoodBanking() - { - AgilityFoodOption food = config.bankFood(); - return isFoodBankingConfigured() - && plugin.getInventoryFood().size() <= config.bankFoodAt() - && plugin.getInventoryFood().size() < config.foodWithdrawAmount(); - } - - private boolean isFoodBankingConfigured() - { - AgilityFoodOption food = config.bankFood(); - return !food.isNone() - && config.foodWithdrawAmount() > 0 - && config.bankFoodAt() > 0; - } - - private boolean needsPotionBanking() - { - AgilityPotionOption potion = config.bankPotion(); - return !potion.isNone() - && config.potionWithdrawAmount() > 0 - && getStaminaPotionCount() < config.potionWithdrawAmount(); - } - - private boolean needsSummerPieBanking(AgilityCourseHandler courseHandler) - { - return courseHandler != null - && courseHandler.canBeBoosted() - && config.summerPieWithdrawAmount() > 0 - && plugin.getSummerPies().size() < config.summerPieWithdrawAmount(); - } - - private int getStaminaPotionCount() - { - return Rs2Inventory.count(item -> item != null && Arrays.stream(STAMINA_POTION_IDS).anyMatch(id -> id == item.getId())); - } - - private boolean shouldWaitForHealthInsteadOfBanking() - { - return config.hpSafetyWait() - && plugin.getInventoryFood().isEmpty() - && Rs2Player.getHealthPercentage() < config.hpSafetyWaitBelow() - && (!isFoodBankingConfigured() || lastFoodBankUnavailable); - } - - private boolean canAttemptBanking(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation) - { - if (!config.bankOnlyAtCourseStart()) - { - return true; - } - if (courseHandler == null || playerWorldLocation == null || courseHandler.getStartPoint() == null) - { - return false; - } - return courseHandler.getClientPlane() == 0 - && courseHandler.getCurrentObstacleIndex() == 0 - && playerWorldLocation.distanceTo(courseHandler.getStartPoint()) <= 12; - } - - private int getBankItemQuantity(int itemId) - { - return Rs2Bank.bankItems().stream() - .filter(item -> item != null && item.getId() == itemId) - .mapToInt(Rs2ItemModel::getQuantity) - .sum(); - } - - private boolean handleSummerPies(AgilityCourseHandler courseHandler) - { - if (!courseHandler.canBeBoosted()) - { - return false; - } - if (courseHandler.getCurrentObstacleIndex() > 0) - { - return false; - } - if (Rs2Player.getBoostedSkillLevel(Skill.AGILITY) >= courseHandler.getRequiredLevel()) - { - return false; - } - - List summerPies = plugin.getSummerPies(); - if (summerPies.isEmpty()) - { - return false; - } - Rs2ItemModel summerPie = summerPies.get(0); - - Rs2Inventory.interact(summerPie, "eat"); - Rs2Inventory.waitForInventoryChanges(1800); - if (Rs2Inventory.contains(ItemID.PIEDISH)) - { - Rs2Inventory.dropAll(ItemID.PIEDISH); - } - return true; - } - private boolean shouldPerformAlch() { if (!config.alchemy()) diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/AgilitySupplyManager.java b/src/main/java/net/runelite/client/plugins/microbot/agility/AgilitySupplyManager.java new file mode 100644 index 0000000000..3690542169 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/AgilitySupplyManager.java @@ -0,0 +1,507 @@ +package net.runelite.client.plugins.microbot.agility; + +import net.runelite.api.Skill; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.gameval.ItemID; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.agility.courses.AgilityCourseHandler; +import net.runelite.client.plugins.microbot.agility.enums.AgilityFoodOption; +import net.runelite.client.plugins.microbot.agility.enums.AgilityPotionOption; +import net.runelite.client.plugins.microbot.util.Global; +import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; +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.misc.Rs2Food; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.function.BooleanSupplier; +import java.util.stream.Collectors; + +public class AgilitySupplyManager +{ + private static final long BANK_RETRY_COOLDOWN_MS = 30_000; + private static final long FOOD_BANK_UNAVAILABLE_RETRY_MS = 300_000; + private static final int SUMMER_PIE_ID = ItemID.SUMMER_PIE; + private static final int[] STAMINA_POTION_IDS = { + ItemID._4DOSESTAMINA, + ItemID._3DOSESTAMINA, + ItemID._2DOSESTAMINA, + ItemID._1DOSESTAMINA + }; + + private final MicroAgilityPlugin plugin; + private final MicroAgilityConfig config; + private final BooleanSupplier shuttingDown; + private final Runnable shutdown; + + private long lastBankFailureAt = 0; + private long lastFoodBankUnavailableAt = 0; + private long lastPreLevelBankBlockedMessageAt = 0; + + public AgilitySupplyManager(MicroAgilityPlugin plugin, MicroAgilityConfig config, BooleanSupplier shuttingDown, Runnable shutdown) + { + this.plugin = plugin; + this.config = config; + this.shuttingDown = shuttingDown; + this.shutdown = shutdown; + } + + public void reset() + { + lastBankFailureAt = 0; + lastFoodBankUnavailableAt = 0; + lastPreLevelBankBlockedMessageAt = 0; + } + + public boolean handleSummerPies(AgilityCourseHandler courseHandler) + { + if (!courseHandler.canBeBoosted()) + { + return false; + } + if (courseHandler.getCurrentObstacleIndex() > 0) + { + return false; + } + if (Rs2Player.getBoostedSkillLevel(Skill.AGILITY) >= courseHandler.getRequiredLevel()) + { + return false; + } + + List summerPies = plugin.getSummerPies(); + if (summerPies.isEmpty()) + { + return false; + } + Rs2ItemModel summerPie = summerPies.get(0); + + Rs2Inventory.interact(summerPie, "eat"); + Rs2Inventory.waitForInventoryChanges(1800); + if (Rs2Inventory.contains(ItemID.PIEDISH)) + { + Rs2Inventory.dropAll(ItemID.PIEDISH); + } + return true; + } + + public boolean handlePreLevelCheck(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation, boolean hasRequiredLevel) + { + if (hasRequiredLevel || !needsSummerPieBanking(courseHandler)) + { + return false; + } + if (!canAttemptBanking(courseHandler, playerWorldLocation)) + { + throttledPreLevelBankMessage(); + return false; + } + return handleBanking(courseHandler, playerWorldLocation); + } + + public boolean handleFoodOrHealthSafety() + { + return handleFood() || handleHealthSafety(); + } + + public boolean handleBeforeObstacle(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation) + { + return handleBanking(courseHandler, playerWorldLocation) + || handleHealthSafety(); + } + + private boolean handleFood() + { + if (config.hitpoints() <= 0) + { + return false; + } + + if (Rs2Player.getHealthPercentage() > config.hitpoints()) + { + return false; + } + + List foodItems = plugin.getInventoryFood(); + if (foodItems.isEmpty()) + { + if (needsFoodBanking() || config.hpSafetyWait()) + { + return false; + } + Microbot.showMessage("Hitpoints are below the configured threshold and no food was found. Stopping agility."); + shutdown.run(); + return true; + } + Rs2ItemModel foodItem = foodItems.get(0); + + Rs2Inventory.interact(foodItem, foodItem.getName().toLowerCase().contains("jug of wine") ? "drink" : "eat"); + Rs2Inventory.waitForInventoryChanges(1800); + + if (Rs2Inventory.contains(ItemID.JUG_EMPTY)) + { + Rs2Inventory.dropAll(ItemID.JUG_EMPTY); + } + return true; + } + + private boolean handleHealthSafety() + { + if (!shouldWaitForHealthInsteadOfBanking()) + { + return false; + } + + int resumeAt = Math.max(config.hpSafetyResumeAt(), config.hpSafetyWaitBelow()); + Microbot.status = "Waiting for hitpoints"; + Global.sleepUntil(() -> shuttingDown.getAsBoolean() + || !plugin.getInventoryFood().isEmpty() + || Rs2Player.getHealthPercentage() >= resumeAt, 10_000); + return true; + } + + private boolean handleBanking(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation) + { + return handleBanking(courseHandler, playerWorldLocation, false); + } + + private boolean handleBanking(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation, boolean blockUntilBankable) + { + if (!shouldBankForSupplies(courseHandler)) + { + return false; + } + if (shouldWaitForHealthInsteadOfBanking()) + { + return false; + } + if (Rs2Player.isMoving() || Rs2Player.isAnimating()) + { + return true; + } + if (!canAttemptBanking(courseHandler, playerWorldLocation)) + { + Microbot.status = "Finishing course before banking"; + return blockUntilBankable || shouldPauseForUnsafeSupplyBanking(); + } + + long now = System.currentTimeMillis(); + if (lastBankFailureAt > 0 && now - lastBankFailureAt < BANK_RETRY_COOLDOWN_MS) + { + Microbot.status = "Waiting before retrying bank"; + return true; + } + + Microbot.status = "Banking agility supplies"; + boolean bankOpen = Rs2Bank.walkToBankAndUseBank(); + if (!bankOpen) + { + Global.sleepUntil(() -> Rs2Bank.isOpen() || Rs2Player.isMoving(), 1200); + bankOpen = Rs2Bank.isOpen(); + if (!bankOpen && Rs2Player.isMoving()) + { + lastBankFailureAt = 0; + Microbot.status = "Walking to bank"; + return true; + } + } + if (!bankOpen) + { + lastBankFailureAt = now; + Microbot.log("Unable to reach or open bank for agility supplies. Retrying after cooldown."); + return true; + } + + boolean success = withdrawConfiguredSupplies(courseHandler); + Rs2Bank.closeBank(); + lastBankFailureAt = success ? 0 : System.currentTimeMillis(); + return true; + } + + private boolean withdrawConfiguredSupplies(AgilityCourseHandler courseHandler) + { + boolean success = true; + if (needsFoodBanking()) + { + success &= withdrawConfiguredFood(); + } + if (needsPotionBanking()) + { + success &= withdrawConfiguredPotion(); + } + if (needsSummerPieBanking(courseHandler)) + { + success &= withdrawSummerPies(); + } + return success; + } + + private boolean withdrawConfiguredFood() + { + int targetAmount = config.foodWithdrawAmount(); + int currentFood = plugin.getInventoryFood().size(); + int missingFood = targetAmount - currentFood; + if (missingFood <= 0) + { + return true; + } + if (Rs2Inventory.emptySlotCount() <= 0) + { + Microbot.showMessage("Inventory is full. Unable to withdraw agility food."); + return false; + } + + int amountToWithdraw = Math.min(missingFood, Rs2Inventory.emptySlotCount()); + + AgilityFoodOption option = config.bankFood(); + if (option.isAuto()) + { + return withdrawAutoFood(amountToWithdraw, currentFood); + } + + Rs2Food food = option.getFood(); + if (food == null) + { + return false; + } + if (!Rs2Bank.hasBankItem(food.getId(), amountToWithdraw)) + { + markFoodBankUnavailable(); + Microbot.showMessage("No " + food.getName() + " found in the bank."); + return false; + } + + if (!Rs2Bank.withdrawX(food.getId(), amountToWithdraw)) + { + return false; + } + boolean receivedFood = Global.sleepUntil(() -> plugin.getInventoryFood().size() >= currentFood + amountToWithdraw, 2400); + if (receivedFood) + { + clearFoodBankUnavailable(); + } + return receivedFood; + } + + private boolean withdrawAutoFood(int amountToWithdraw, int currentFood) + { + int remaining = amountToWithdraw; + int expectedFood = currentFood; + + List foodOptions = Arrays.stream(AgilityFoodOption.values()) + .filter(foodOption -> !foodOption.isNone() && !foodOption.isAuto()) + .filter(foodOption -> foodOption.getFood() != null) + .sorted(Comparator.comparingInt((AgilityFoodOption foodOption) -> foodOption.getFood().getHeal()).reversed()) + .collect(Collectors.toList()); + + for (AgilityFoodOption option : foodOptions) + { + if (remaining <= 0 || Rs2Inventory.emptySlotCount() <= 0) + { + break; + } + + Rs2Food food = option.getFood(); + int available = getBankItemQuantity(food.getId()); + if (available <= 0) + { + continue; + } + + int withdrawAmount = Math.min(Math.min(remaining, available), Rs2Inventory.emptySlotCount()); + if (!Rs2Bank.withdrawX(food.getId(), withdrawAmount)) + { + return false; + } + + int expectedAfterWithdraw = expectedFood + withdrawAmount; + if (!Global.sleepUntil(() -> plugin.getInventoryFood().size() >= expectedAfterWithdraw, 2400)) + { + return false; + } + expectedFood = expectedAfterWithdraw; + remaining = currentFood + amountToWithdraw - plugin.getInventoryFood().size(); + } + + if (remaining > 0) + { + markFoodBankUnavailable(); + Microbot.showMessage("Not enough configured food was found in the bank."); + return false; + } + clearFoodBankUnavailable(); + return true; + } + + private boolean withdrawConfiguredPotion() + { + AgilityPotionOption option = config.bankPotion(); + int targetAmount = config.potionWithdrawAmount(); + int currentPotions = getStaminaPotionCount(); + int missingPotions = targetAmount - currentPotions; + if (missingPotions <= 0) + { + return true; + } + if (Rs2Inventory.emptySlotCount() <= 0) + { + Microbot.showMessage("Inventory is full. Unable to withdraw agility potions."); + return false; + } + + int amountToWithdraw = Math.min(missingPotions, Rs2Inventory.emptySlotCount()); + if (option.isNone() || option.getItemId() <= 0) + { + return false; + } + if (!Rs2Bank.hasBankItem(option.getItemId(), amountToWithdraw)) + { + Microbot.showMessage("No " + option + " found in the bank."); + return false; + } + + if (!Rs2Bank.withdrawX(option.getItemId(), amountToWithdraw)) + { + return false; + } + return Global.sleepUntil(() -> getStaminaPotionCount() >= currentPotions + amountToWithdraw, 2400); + } + + private boolean withdrawSummerPies() + { + int targetAmount = config.summerPieWithdrawAmount(); + int currentPies = plugin.getSummerPies().size(); + int missingPies = targetAmount - currentPies; + if (missingPies <= 0) + { + return true; + } + if (Rs2Inventory.emptySlotCount() <= 0) + { + Microbot.showMessage("Inventory is full. Unable to withdraw summer pies."); + return false; + } + + int amountToWithdraw = Math.min(missingPies, Rs2Inventory.emptySlotCount()); + if (!Rs2Bank.hasBankItem(SUMMER_PIE_ID, amountToWithdraw)) + { + Microbot.showMessage("No summer pies found in the bank."); + return false; + } + if (!Rs2Bank.withdrawX(SUMMER_PIE_ID, amountToWithdraw)) + { + return false; + } + return Global.sleepUntil(() -> plugin.getSummerPies().size() >= currentPies + amountToWithdraw, 2400); + } + + private boolean shouldBankForSupplies(AgilityCourseHandler courseHandler) + { + return needsFoodBanking() || needsPotionBanking() || needsSummerPieBanking(courseHandler); + } + + private boolean needsFoodBanking() + { + return isFoodBankingConfigured() + && plugin.getInventoryFood().size() <= config.bankFoodAt() + && plugin.getInventoryFood().size() < config.foodWithdrawAmount(); + } + + private boolean isFoodBankingConfigured() + { + AgilityFoodOption food = config.bankFood(); + return !food.isNone() + && config.foodWithdrawAmount() > 0 + && config.bankFoodAt() > 0; + } + + private boolean needsPotionBanking() + { + AgilityPotionOption potion = config.bankPotion(); + return !potion.isNone() + && config.potionWithdrawAmount() > 0 + && getStaminaPotionCount() < config.potionWithdrawAmount(); + } + + private boolean needsSummerPieBanking(AgilityCourseHandler courseHandler) + { + return courseHandler != null + && courseHandler.canBeBoosted() + && config.summerPieWithdrawAmount() > 0 + && plugin.getSummerPies().size() < config.summerPieWithdrawAmount(); + } + + private int getStaminaPotionCount() + { + return Rs2Inventory.count(item -> item != null && Arrays.stream(STAMINA_POTION_IDS).anyMatch(id -> id == item.getId())); + } + + private boolean shouldWaitForHealthInsteadOfBanking() + { + return config.hpSafetyWait() + && plugin.getInventoryFood().isEmpty() + && Rs2Player.getHealthPercentage() < config.hpSafetyWaitBelow() + && (!isFoodBankingConfigured() || isFoodBankUnavailable()); + } + + private boolean shouldPauseForUnsafeSupplyBanking() + { + return config.hpSafetyWait() + && isFoodBankingConfigured() + && plugin.getInventoryFood().isEmpty() + && Rs2Player.getHealthPercentage() < config.hpSafetyWaitBelow(); + } + + private boolean canAttemptBanking(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation) + { + if (!config.bankOnlyAtCourseStart()) + { + return true; + } + if (courseHandler == null || playerWorldLocation == null || courseHandler.getStartPoint() == null) + { + return false; + } + return courseHandler.getClientPlane() == 0 + && courseHandler.getCurrentObstacleIndex() == 0 + && playerWorldLocation.distanceTo(courseHandler.getStartPoint()) <= 12; + } + + private int getBankItemQuantity(int itemId) + { + return Rs2Bank.bankItems().stream() + .filter(item -> item != null && item.getId() == itemId) + .mapToInt(Rs2ItemModel::getQuantity) + .sum(); + } + + private void markFoodBankUnavailable() + { + lastFoodBankUnavailableAt = System.currentTimeMillis(); + } + + private void clearFoodBankUnavailable() + { + lastFoodBankUnavailableAt = 0; + } + + private boolean isFoodBankUnavailable() + { + return lastFoodBankUnavailableAt > 0 + && System.currentTimeMillis() - lastFoodBankUnavailableAt < FOOD_BANK_UNAVAILABLE_RETRY_MS; + } + + private void throttledPreLevelBankMessage() + { + long now = System.currentTimeMillis(); + if (now - lastPreLevelBankBlockedMessageAt < 30_000) + { + return; + } + lastPreLevelBankBlockedMessageAt = now; + Microbot.showMessage("Summer pie banking is configured, but the script is not at a bankable course-start position."); + Microbot.status = "Move to course start to bank summer pies"; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityPlugin.java index 62abbd281b..70903ef231 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityPlugin.java @@ -34,7 +34,7 @@ @Slf4j public class MicroAgilityPlugin extends Plugin { - public static final String version = "1.3.0"; + public static final String version = "1.3.2"; @Inject private MicroAgilityConfig config; @Inject From c629a10bd0c0e9becb7000453b1395487f9e9430 Mon Sep 17 00:00:00 2001 From: infuse21 Date: Tue, 30 Jun 2026 16:07:02 +0100 Subject: [PATCH 06/13] Agility plugin features and bug fixes --- .../microbot/agility/AgilityScript.java | 137 ++++---- .../agility/AgilitySupplyManager.java | 303 +++++++++++++++--- .../microbot/agility/MicroAgilityConfig.java | 15 +- .../microbot/agility/MicroAgilityOverlay.java | 2 +- .../microbot/agility/MicroAgilityPlugin.java | 26 +- .../agility/courses/AgilityCourseHandler.java | 21 +- .../agility/courses/BrimhavenSpikeCourse.java | 88 +++-- .../courses/GnomeStrongholdCourse.java | 6 + .../agility/courses/PrifddinasCourse.java | 6 + .../agility/courses/WerewolfCourse.java | 8 + .../agility/enums/AgilityEquipmentOption.java | 31 ++ 11 files changed, 493 insertions(+), 150 deletions(-) create mode 100644 src/main/java/net/runelite/client/plugins/microbot/agility/enums/AgilityEquipmentOption.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java b/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java index 29e849f349..ad7aa89a39 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java @@ -8,10 +8,6 @@ import net.runelite.client.plugins.microbot.Script; import net.runelite.client.plugins.microbot.agentserver.handler.ScriptHeartbeatRegistry; import net.runelite.client.plugins.microbot.agility.courses.AgilityCourseHandler; -import net.runelite.client.plugins.microbot.agility.courses.BrimhavenSpikeCourse; -import net.runelite.client.plugins.microbot.agility.courses.GnomeStrongholdCourse; -import net.runelite.client.plugins.microbot.agility.courses.PrifddinasCourse; -import net.runelite.client.plugins.microbot.agility.courses.WerewolfCourse; import net.runelite.client.plugins.microbot.agility.enums.AgilityCourse; import net.runelite.client.plugins.microbot.api.tileitem.models.Rs2TileItemModel; import net.runelite.client.plugins.microbot.util.antiban.Rs2Antiban; @@ -29,6 +25,7 @@ import java.util.Comparator; import java.util.List; import java.util.Optional; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -43,11 +40,18 @@ public class AgilityScript extends Script long lastTimeoutWarning = 0; // For throttled timeout warnings private AgilityCourse activeCourse = null; private AgilityCourseHandler activeHandler = null; + private static final long MAIN_LOOP_DELAY_MS = 250; + private static final long MARK_OF_GRACE_SCAN_INTERVAL_MS = 750; private static final int MARK_OF_GRACE_SEARCH_DISTANCE = 30; private static final int MARK_OF_GRACE_PICKUP_TIMEOUT = 5000; + private volatile int currentObstacleIndex = -1; private WorldPoint pendingMarkOfGraceLocation = null; private int pendingMarkOfGraceCount = 0; private long pendingMarkOfGraceStartedAt = 0; + private long lastMarkOfGraceScanAt = 0; + private WorldPoint alchDecisionObstacleLocation = null; + private int alchDecisionObstacleId = -1; + private boolean alchDecisionShouldAlch = false; private final AgilitySupplyManager supplyManager; private volatile boolean shuttingDown = false; @@ -72,8 +76,10 @@ public void shutdown() activeHandler = null; startPoint = null; initialPlayerLocation = null; + currentObstacleIndex = -1; supplyManager.reset(); clearPendingMarkOfGrace(); + clearAlchDecision(); if (mainScheduledFuture != null && !mainScheduledFuture.isDone()) { @@ -85,10 +91,6 @@ public void shutdown() } clearWalkingRouteForShutdown(); - if (Microbot.getClientThread().scheduledFuture != null) - { - Microbot.getClientThread().scheduledFuture.cancel(true); - } Microbot.pauseAllScripts.set(false); Rs2Walker.disableTeleports = false; Microbot.getSpecialAttackConfigs().reset(); @@ -138,50 +140,48 @@ public boolean run() return; } - if (supplyManager.handleSummerPies(courseHandler)) + boolean hasRequiredLevel = plugin.hasRequiredLevel(courseHandler); + currentObstacleIndex = courseHandler.getCurrentObstacleIndex(); + AgilitySupplyManager.InventorySnapshot inventorySnapshot = supplyManager.createInventorySnapshot(); + if (supplyManager.handleSummerPies(courseHandler, playerWorldLocation, currentObstacleIndex, inventorySnapshot)) { Microbot.log("Early return: Handling summer pies"); return; } - if (supplyManager.handleFoodOrHealthSafety()) + if (supplyManager.handleFoodOrHealthSafety(inventorySnapshot)) { Microbot.log("Early return: Handling agility safety"); return; } - boolean hasRequiredLevel = plugin.hasRequiredLevel(); - if (supplyManager.handlePreLevelCheck(courseHandler, playerWorldLocation, hasRequiredLevel)) + if (supplyManager.handlePreLevelCheck(courseHandler, playerWorldLocation, currentObstacleIndex, hasRequiredLevel, inventorySnapshot)) { Microbot.log("Early return: Banking agility supplies"); return; } - if (supplyManager.handleBeforeObstacle(courseHandler, playerWorldLocation)) + if (supplyManager.handleBeforeObstacle(courseHandler, playerWorldLocation, currentObstacleIndex, inventorySnapshot)) { Microbot.log("Early return: Handling agility supplies"); return; } - if (!plugin.hasRequiredLevel()) + if (!hasRequiredLevel) { Microbot.log("Early return: Required level not met"); Microbot.showMessage("You do not have the required level for this course."); + Rs2Player.logout(); shutdown(); return; } - // Check coin requirement for BrimhavenSpike course (only before payment) - if (courseHandler instanceof BrimhavenSpikeCourse) + if (!courseHandler.hasRequiredCourseItems()) { - BrimhavenSpikeCourse course = (BrimhavenSpikeCourse) courseHandler; - if (!course.hasPaid() && !course.hasRequiredCoins()) - { - Microbot.log("Early return: Not enough coins for BrimhavenSpike course"); - Microbot.showMessage("You need 200 coins to enter the Brimhaven Spike course!"); - shutdown(); - return; - } + Microbot.log("Early return: Missing required course items"); + Microbot.showMessage(courseHandler.getMissingRequiredCourseItemsMessage()); + shutdown(); + return; } if (Rs2AntibanSettings.actionCooldownActive) { @@ -196,11 +196,11 @@ public boolean run() return; } - if (handleCourseSpecificActions(courseHandler, playerWorldLocation)) + if (courseHandler.handleCourseActions(playerWorldLocation)) { return; } - final int agilityExp = Microbot.getClient().getSkillExperience(Skill.AGILITY); + final int agilityExp = currentAgilityXp; TileObject gameObject = courseHandler.getCurrentObstacle(); @@ -228,7 +228,7 @@ public boolean run() } // Handle alchemy if enabled - if (shouldPerformAlch()) + if (shouldPerformAlch(gameObject)) { Optional alchItem = getAlchItem(); if (alchItem.isPresent()) @@ -249,7 +249,10 @@ public boolean run() else { // Still do normal alch if far enough but efficient alching is disabled - performNormalAlch(alchItem.get()); + if (performNormalAlch(alchItem.get())) + { + return; + } } } // Skip alching if obstacle is too close @@ -265,7 +268,10 @@ public boolean run() } } // Fall back to normal alching - performNormalAlch(alchItem.get()); + if (performNormalAlch(alchItem.get())) + { + return; + } } } } @@ -288,6 +294,7 @@ public boolean run() // XP tracking is already updated before clicking (line 137) // Don't update here to avoid losing early action state + clearAlchDecision(); // If we're still animating after XP, don't add delays - proceed immediately if (!Rs2Player.isAnimating() && !Rs2Player.isMoving()) { @@ -305,7 +312,7 @@ public boolean run() } Microbot.log("An error occurred: " + ex.getMessage(), ex); } - }, 0, 100, TimeUnit.MILLISECONDS); + }, 0, MAIN_LOOP_DELAY_MS, TimeUnit.MILLISECONDS); return true; } @@ -314,6 +321,11 @@ public boolean isShuttingDown() return shuttingDown; } + public int getCurrentObstacleIndex() + { + return currentObstacleIndex; + } + private boolean isExpectedShutdownInterrupt(Exception ex) { if (!shuttingDown) @@ -390,7 +402,9 @@ private AgilityCourseHandler getActiveHandler() activeHandler.reset(); startPoint = activeHandler.getStartPoint(); lastAgilityXp = Microbot.getClient().getSkillExperience(Skill.AGILITY); + currentObstacleIndex = -1; supplyManager.reset(); + clearAlchDecision(); } return activeHandler; } @@ -430,6 +444,13 @@ else if (Rs2Player.isMoving() || Rs2Player.isAnimating()) return false; } + long now = System.currentTimeMillis(); + if (now - lastMarkOfGraceScanAt < MARK_OF_GRACE_SCAN_INTERVAL_MS) + { + return false; + } + lastMarkOfGraceScanAt = now; + Rs2TileItemModel markOfGrace = Microbot.getRs2TileItemCache().query() .fromWorldView() .withId(ItemID.GRACE) @@ -495,6 +516,7 @@ private void clearPendingMarkOfGrace() pendingMarkOfGraceLocation = null; pendingMarkOfGraceCount = 0; pendingMarkOfGraceStartedAt = 0; + lastMarkOfGraceScanAt = 0; } private boolean hasLootableMarkAt(WorldPoint markLocation) @@ -507,20 +529,24 @@ private boolean hasLootableMarkAt(WorldPoint markLocation) .first() != null; } - private boolean shouldPerformAlch() + private boolean shouldPerformAlch(TileObject gameObject) { if (!config.alchemy()) { + clearAlchDecision(); return false; } - - // Check if we should skip alching based on configured chance - if (Math.random() * 100 < config.alchSkipChance()) + + WorldPoint obstacleLocation = gameObject.getWorldLocation(); + if (gameObject.getId() == alchDecisionObstacleId && obstacleLocation.equals(alchDecisionObstacleLocation)) { - return false; + return alchDecisionShouldAlch; } - - return true; + + alchDecisionObstacleId = gameObject.getId(); + alchDecisionObstacleLocation = obstacleLocation; + alchDecisionShouldAlch = ThreadLocalRandom.current().nextInt(100) >= config.alchSkipChance(); + return alchDecisionShouldAlch; } private boolean performEfficientAlch(TileObject gameObject, String alchItem, int agilityExp) @@ -551,44 +577,27 @@ private boolean performEfficientAlch(TileObject gameObject, String alchItem, int Rs2Antiban.actionCooldown(); Rs2Antiban.takeMicroBreakByChance(); lastAgilityXp = Microbot.getClient().getSkillExperience(Skill.AGILITY); + clearAlchDecision(); return true; } } return false; } - private void performNormalAlch(String alchItem) + private boolean performNormalAlch(String alchItem) { - // Simple alch - waitForCompletion handles all timing + int initialCount = Rs2Inventory.itemQuantity(alchItem); Rs2Magic.alch(alchItem, 50, 75); + sleepUntil(() -> shuttingDown || Rs2Inventory.itemQuantity(alchItem) < initialCount, 1200); + alchDecisionShouldAlch = false; + return true; } - private boolean handleCourseSpecificActions(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation) + private void clearAlchDecision() { - if (courseHandler instanceof PrifddinasCourse) - { - PrifddinasCourse course = (PrifddinasCourse) courseHandler; - return course.handlePortal() || course.handleWalkToStart(playerWorldLocation); - } - else if (courseHandler instanceof WerewolfCourse) - { - WerewolfCourse course = (WerewolfCourse) courseHandler; - return course.handleFirstSteppingStone(playerWorldLocation) - || course.handleStickPickup(playerWorldLocation) - || course.handleSlide() - || course.handleStickReturn(playerWorldLocation); - } - else if (courseHandler instanceof BrimhavenSpikeCourse) - { - BrimhavenSpikeCourse course = (BrimhavenSpikeCourse) courseHandler; - boolean result = course.handleWalkToStart(playerWorldLocation); - return result; - } - else if (!(courseHandler instanceof GnomeStrongholdCourse)) - { - return courseHandler.handleWalkToStart(playerWorldLocation); - } - return false; + alchDecisionObstacleLocation = null; + alchDecisionObstacleId = -1; + alchDecisionShouldAlch = false; } private boolean interactWithObstacle(TileObject gameObject) diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/AgilitySupplyManager.java b/src/main/java/net/runelite/client/plugins/microbot/agility/AgilitySupplyManager.java index 3690542169..f1dfb06326 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/AgilitySupplyManager.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/AgilitySupplyManager.java @@ -5,15 +5,18 @@ import net.runelite.api.gameval.ItemID; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.agility.courses.AgilityCourseHandler; +import net.runelite.client.plugins.microbot.agility.enums.AgilityEquipmentOption; import net.runelite.client.plugins.microbot.agility.enums.AgilityFoodOption; import net.runelite.client.plugins.microbot.agility.enums.AgilityPotionOption; import net.runelite.client.plugins.microbot.util.Global; import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; +import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; 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.misc.Rs2Food; import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import java.util.Collections; import java.util.Arrays; import java.util.Comparator; import java.util.List; @@ -25,6 +28,29 @@ public class AgilitySupplyManager private static final long BANK_RETRY_COOLDOWN_MS = 30_000; private static final long FOOD_BANK_UNAVAILABLE_RETRY_MS = 300_000; private static final int SUMMER_PIE_ID = ItemID.SUMMER_PIE; + private static final String GRACEFUL_HOOD = "Graceful hood"; + private static final String GRACEFUL_CAPE = "Graceful cape"; + private static final String GRACEFUL_TOP = "Graceful top"; + private static final String GRACEFUL_LEGS = "Graceful legs"; + private static final String GRACEFUL_GLOVES = "Graceful gloves"; + private static final String GRACEFUL_BOOTS = "Graceful boots"; + private static final String AGILITY_CAPE = "Agility cape"; + private static final String[] GRACEFUL_WITH_CAPE = { + GRACEFUL_HOOD, + GRACEFUL_CAPE, + GRACEFUL_TOP, + GRACEFUL_LEGS, + GRACEFUL_GLOVES, + GRACEFUL_BOOTS + }; + private static final String[] GRACEFUL_WITH_AGILITY_CAPE = { + GRACEFUL_HOOD, + AGILITY_CAPE, + GRACEFUL_TOP, + GRACEFUL_LEGS, + GRACEFUL_GLOVES, + GRACEFUL_BOOTS + }; private static final int[] STAMINA_POTION_IDS = { ItemID._4DOSESTAMINA, ItemID._3DOSESTAMINA, @@ -39,7 +65,7 @@ public class AgilitySupplyManager private long lastBankFailureAt = 0; private long lastFoodBankUnavailableAt = 0; - private long lastPreLevelBankBlockedMessageAt = 0; + private boolean summerPieBankUnavailable = false; public AgilitySupplyManager(MicroAgilityPlugin plugin, MicroAgilityConfig config, BooleanSupplier shuttingDown, Runnable shutdown) { @@ -53,16 +79,21 @@ public void reset() { lastBankFailureAt = 0; lastFoodBankUnavailableAt = 0; - lastPreLevelBankBlockedMessageAt = 0; + summerPieBankUnavailable = false; } - public boolean handleSummerPies(AgilityCourseHandler courseHandler) + public InventorySnapshot createInventorySnapshot() + { + return new InventorySnapshot(plugin.getInventoryFood(), plugin.getSummerPies(), getStaminaPotionCount()); + } + + public boolean handleSummerPies(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation, int currentObstacleIndex, InventorySnapshot inventorySnapshot) { if (!courseHandler.canBeBoosted()) { return false; } - if (courseHandler.getCurrentObstacleIndex() > 0) + if (!isAtCourseStart(courseHandler, playerWorldLocation, currentObstacleIndex)) { return false; } @@ -71,7 +102,7 @@ public boolean handleSummerPies(AgilityCourseHandler courseHandler) return false; } - List summerPies = plugin.getSummerPies(); + List summerPies = inventorySnapshot.getSummerPies(); if (summerPies.isEmpty()) { return false; @@ -87,32 +118,73 @@ public boolean handleSummerPies(AgilityCourseHandler courseHandler) return true; } - public boolean handlePreLevelCheck(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation, boolean hasRequiredLevel) + public boolean handlePreLevelCheck(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation, int currentObstacleIndex, + boolean hasRequiredLevel, InventorySnapshot inventorySnapshot) { - if (hasRequiredLevel || !needsSummerPieBanking(courseHandler)) + boolean requiresBoost = courseHandler.canBeBoosted() && !plugin.hasRealRequiredLevel(courseHandler); + if (!requiresBoost) + { + return false; + } + if (!isAtCourseStart(courseHandler, playerWorldLocation, currentObstacleIndex)) + { + if (!hasRequiredLevel) + { + stopAndLogout("You do not have the required agility level for this course."); + return true; + } + return false; + } + if (hasRequiredLevel && inventorySnapshot.getSummerPies().isEmpty() && config.summerPieWithdrawAmount() <= 0) + { + stopAndLogout("You need summer pies in inventory before starting this boosted course."); + return true; + } + if (!needsSummerPieBanking(courseHandler, inventorySnapshot)) { + if (!hasRequiredLevel) + { + stopAndLogout("You do not have enough summer pies configured to start this course."); + return true; + } return false; } - if (!canAttemptBanking(courseHandler, playerWorldLocation)) + + summerPieBankUnavailable = false; + if (!handleBanking(courseHandler, playerWorldLocation, currentObstacleIndex, inventorySnapshot, true)) { - throttledPreLevelBankMessage(); + if (!hasRequiredLevel) + { + stopAndLogout("You do not have enough summer pies configured to start this course."); + return true; + } return false; } - return handleBanking(courseHandler, playerWorldLocation); + if (summerPieBankUnavailable && plugin.getSummerPies().size() < config.summerPieWithdrawAmount()) + { + stopAndLogout("No summer pies were available for the selected boosted course."); + return true; + } + return true; } - public boolean handleFoodOrHealthSafety() + public boolean handleFoodOrHealthSafety(InventorySnapshot inventorySnapshot) { - return handleFood() || handleHealthSafety(); + return handleFood(inventorySnapshot) || handleHealthSafety(inventorySnapshot); } - public boolean handleBeforeObstacle(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation) + public boolean handleBeforeObstacle(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation, int currentObstacleIndex, + InventorySnapshot inventorySnapshot) { - return handleBanking(courseHandler, playerWorldLocation) - || handleHealthSafety(); + if (wearConfiguredEquipmentFromInventory()) + { + return true; + } + return handleBanking(courseHandler, playerWorldLocation, currentObstacleIndex, inventorySnapshot) + || handleHealthSafety(inventorySnapshot); } - private boolean handleFood() + private boolean handleFood(InventorySnapshot inventorySnapshot) { if (config.hitpoints() <= 0) { @@ -124,10 +196,10 @@ private boolean handleFood() return false; } - List foodItems = plugin.getInventoryFood(); + List foodItems = inventorySnapshot.getFoodItems(); if (foodItems.isEmpty()) { - if (needsFoodBanking() || config.hpSafetyWait()) + if (needsFoodBanking(inventorySnapshot) || config.hpSafetyWait()) { return false; } @@ -147,9 +219,9 @@ private boolean handleFood() return true; } - private boolean handleHealthSafety() + private boolean handleHealthSafety(InventorySnapshot inventorySnapshot) { - if (!shouldWaitForHealthInsteadOfBanking()) + if (!shouldWaitForHealthInsteadOfBanking(inventorySnapshot)) { return false; } @@ -162,18 +234,20 @@ private boolean handleHealthSafety() return true; } - private boolean handleBanking(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation) + private boolean handleBanking(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation, int currentObstacleIndex, + InventorySnapshot inventorySnapshot) { - return handleBanking(courseHandler, playerWorldLocation, false); + return handleBanking(courseHandler, playerWorldLocation, currentObstacleIndex, inventorySnapshot, false); } - private boolean handleBanking(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation, boolean blockUntilBankable) + private boolean handleBanking(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation, int currentObstacleIndex, + InventorySnapshot inventorySnapshot, boolean blockUntilBankable) { - if (!shouldBankForSupplies(courseHandler)) + if (!shouldBankForSupplies(courseHandler, inventorySnapshot) && !needsEquipmentBanking()) { return false; } - if (shouldWaitForHealthInsteadOfBanking()) + if (shouldWaitForHealthInsteadOfBanking(inventorySnapshot)) { return false; } @@ -181,10 +255,10 @@ private boolean handleBanking(AgilityCourseHandler courseHandler, WorldPoint pla { return true; } - if (!canAttemptBanking(courseHandler, playerWorldLocation)) + if (!canAttemptBanking(courseHandler, playerWorldLocation, currentObstacleIndex)) { Microbot.status = "Finishing course before banking"; - return blockUntilBankable || shouldPauseForUnsafeSupplyBanking(); + return blockUntilBankable || shouldPauseForUnsafeSupplyBanking(inventorySnapshot); } long now = System.currentTimeMillis(); @@ -223,18 +297,23 @@ private boolean handleBanking(AgilityCourseHandler courseHandler, WorldPoint pla private boolean withdrawConfiguredSupplies(AgilityCourseHandler courseHandler) { boolean success = true; - if (needsFoodBanking()) + InventorySnapshot inventorySnapshot = createInventorySnapshot(); + if (needsFoodBanking(inventorySnapshot)) { success &= withdrawConfiguredFood(); } - if (needsPotionBanking()) + if (needsPotionBanking(inventorySnapshot)) { success &= withdrawConfiguredPotion(); } - if (needsSummerPieBanking(courseHandler)) + if (needsSummerPieBanking(courseHandler, inventorySnapshot)) { success &= withdrawSummerPies(); } + if (needsEquipmentBanking()) + { + success &= withdrawAndWearConfiguredEquipment(); + } return success; } @@ -376,6 +455,7 @@ private boolean withdrawSummerPies() int missingPies = targetAmount - currentPies; if (missingPies <= 0) { + summerPieBankUnavailable = false; return true; } if (Rs2Inventory.emptySlotCount() <= 0) @@ -387,26 +467,95 @@ private boolean withdrawSummerPies() int amountToWithdraw = Math.min(missingPies, Rs2Inventory.emptySlotCount()); if (!Rs2Bank.hasBankItem(SUMMER_PIE_ID, amountToWithdraw)) { + summerPieBankUnavailable = true; Microbot.showMessage("No summer pies found in the bank."); return false; } if (!Rs2Bank.withdrawX(SUMMER_PIE_ID, amountToWithdraw)) { + summerPieBankUnavailable = true; return false; } - return Global.sleepUntil(() -> plugin.getSummerPies().size() >= currentPies + amountToWithdraw, 2400); + boolean receivedPies = Global.sleepUntil(() -> plugin.getSummerPies().size() >= currentPies + amountToWithdraw, 2400); + summerPieBankUnavailable = !receivedPies; + return receivedPies; } - private boolean shouldBankForSupplies(AgilityCourseHandler courseHandler) + private boolean shouldBankForSupplies(AgilityCourseHandler courseHandler, InventorySnapshot inventorySnapshot) { - return needsFoodBanking() || needsPotionBanking() || needsSummerPieBanking(courseHandler); + return needsFoodBanking(inventorySnapshot) || needsPotionBanking(inventorySnapshot) || needsSummerPieBanking(courseHandler, inventorySnapshot); + } + + private boolean withdrawAndWearConfiguredEquipment() + { + boolean success = true; + for (String itemName : getConfiguredEquipmentNames()) + { + success &= ensureEquipmentPieceEquipped(itemName); + } + return success && !needsEquipmentBanking(); } - private boolean needsFoodBanking() + private boolean ensureEquipmentPieceEquipped(String itemName) + { + if (Rs2Equipment.isWearing(itemName)) + { + return true; + } + if (Rs2Inventory.hasItem(itemName)) + { + return Rs2Bank.wearItem(itemName); + } + if (!Rs2Bank.hasBankItem(itemName)) + { + Microbot.showMessage(itemName + " was not found in the bank."); + return false; + } + return Rs2Bank.withdrawXAndEquip(itemName, 1); + } + + private boolean wearConfiguredEquipmentFromInventory() + { + if (config.bankEquipment().isNone()) + { + return false; + } + for (String itemName : getConfiguredEquipmentNames()) + { + if (!Rs2Equipment.isWearing(itemName) && Rs2Inventory.hasItem(itemName)) + { + return Rs2Inventory.wear(itemName); + } + } + return false; + } + + private boolean needsEquipmentBanking() + { + if (config.bankEquipment().isNone()) + { + return false; + } + for (String itemName : getConfiguredEquipmentNames()) + { + if (!Rs2Equipment.isWearing(itemName) && !Rs2Inventory.hasItem(itemName)) + { + return true; + } + } + return false; + } + + private String[] getConfiguredEquipmentNames() + { + return config.bankEquipment().useAgilityCape() ? GRACEFUL_WITH_AGILITY_CAPE : GRACEFUL_WITH_CAPE; + } + + private boolean needsFoodBanking(InventorySnapshot inventorySnapshot) { return isFoodBankingConfigured() - && plugin.getInventoryFood().size() <= config.bankFoodAt() - && plugin.getInventoryFood().size() < config.foodWithdrawAmount(); + && inventorySnapshot.getFoodCount() <= config.bankFoodAt() + && inventorySnapshot.getFoodCount() < config.foodWithdrawAmount(); } private boolean isFoodBankingConfigured() @@ -417,20 +566,20 @@ private boolean isFoodBankingConfigured() && config.bankFoodAt() > 0; } - private boolean needsPotionBanking() + private boolean needsPotionBanking(InventorySnapshot inventorySnapshot) { AgilityPotionOption potion = config.bankPotion(); return !potion.isNone() && config.potionWithdrawAmount() > 0 - && getStaminaPotionCount() < config.potionWithdrawAmount(); + && inventorySnapshot.getStaminaPotionCount() < config.potionWithdrawAmount(); } - private boolean needsSummerPieBanking(AgilityCourseHandler courseHandler) + private boolean needsSummerPieBanking(AgilityCourseHandler courseHandler, InventorySnapshot inventorySnapshot) { return courseHandler != null && courseHandler.canBeBoosted() && config.summerPieWithdrawAmount() > 0 - && plugin.getSummerPies().size() < config.summerPieWithdrawAmount(); + && inventorySnapshot.getSummerPieCount() < config.summerPieWithdrawAmount(); } private int getStaminaPotionCount() @@ -438,23 +587,23 @@ private int getStaminaPotionCount() return Rs2Inventory.count(item -> item != null && Arrays.stream(STAMINA_POTION_IDS).anyMatch(id -> id == item.getId())); } - private boolean shouldWaitForHealthInsteadOfBanking() + private boolean shouldWaitForHealthInsteadOfBanking(InventorySnapshot inventorySnapshot) { return config.hpSafetyWait() - && plugin.getInventoryFood().isEmpty() + && inventorySnapshot.getFoodItems().isEmpty() && Rs2Player.getHealthPercentage() < config.hpSafetyWaitBelow() && (!isFoodBankingConfigured() || isFoodBankUnavailable()); } - private boolean shouldPauseForUnsafeSupplyBanking() + private boolean shouldPauseForUnsafeSupplyBanking(InventorySnapshot inventorySnapshot) { return config.hpSafetyWait() && isFoodBankingConfigured() - && plugin.getInventoryFood().isEmpty() + && inventorySnapshot.getFoodItems().isEmpty() && Rs2Player.getHealthPercentage() < config.hpSafetyWaitBelow(); } - private boolean canAttemptBanking(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation) + private boolean canAttemptBanking(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation, int currentObstacleIndex) { if (!config.bankOnlyAtCourseStart()) { @@ -465,7 +614,17 @@ private boolean canAttemptBanking(AgilityCourseHandler courseHandler, WorldPoint return false; } return courseHandler.getClientPlane() == 0 - && courseHandler.getCurrentObstacleIndex() == 0 + && currentObstacleIndex == 0 + && playerWorldLocation.distanceTo(courseHandler.getStartPoint()) <= 12; + } + + private boolean isAtCourseStart(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation, int currentObstacleIndex) + { + return courseHandler != null + && playerWorldLocation != null + && courseHandler.getStartPoint() != null + && courseHandler.getClientPlane() == 0 + && currentObstacleIndex == 0 && playerWorldLocation.distanceTo(courseHandler.getStartPoint()) <= 12; } @@ -493,15 +652,53 @@ private boolean isFoodBankUnavailable() && System.currentTimeMillis() - lastFoodBankUnavailableAt < FOOD_BANK_UNAVAILABLE_RETRY_MS; } - private void throttledPreLevelBankMessage() + private void stopAndLogout(String message) { - long now = System.currentTimeMillis(); - if (now - lastPreLevelBankBlockedMessageAt < 30_000) + Microbot.showMessage(message); + Microbot.status = "Stopping agility"; + if (Microbot.isLoggedIn()) + { + Rs2Player.logout(); + } + shutdown.run(); + } + + static final class InventorySnapshot + { + private final List foodItems; + private final List summerPies; + private final int staminaPotionCount; + + private InventorySnapshot(List foodItems, List summerPies, int staminaPotionCount) + { + this.foodItems = Collections.unmodifiableList(foodItems); + this.summerPies = Collections.unmodifiableList(summerPies); + this.staminaPotionCount = staminaPotionCount; + } + + List getFoodItems() + { + return foodItems; + } + + List getSummerPies() + { + return summerPies; + } + + int getFoodCount() + { + return foodItems.size(); + } + + int getSummerPieCount() + { + return summerPies.size(); + } + + int getStaminaPotionCount() { - return; + return staminaPotionCount; } - lastPreLevelBankBlockedMessageAt = now; - Microbot.showMessage("Summer pie banking is configured, but the script is not at a bankable course-start position."); - Microbot.status = "Move to course start to bank summer pies"; } } diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityConfig.java b/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityConfig.java index ebc99d3b02..0453628c7d 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityConfig.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityConfig.java @@ -2,6 +2,7 @@ import net.runelite.client.config.*; import net.runelite.client.plugins.microbot.agility.enums.AgilityCourse; +import net.runelite.client.plugins.microbot.agility.enums.AgilityEquipmentOption; import net.runelite.client.plugins.microbot.agility.enums.AgilityFoodOption; import net.runelite.client.plugins.microbot.agility.enums.AgilityPotionOption; @@ -143,11 +144,23 @@ default boolean bankOnlyAtCourseStart() return true; } + @ConfigItem( + keyName = "bankEquipment", + name = "Equipment", + description = "Optional equipment to withdraw and wear when banking from a safe course-start position.", + position = 7, + section = bankingSection + ) + default AgilityEquipmentOption bankEquipment() + { + return AgilityEquipmentOption.NONE; + } + @ConfigItem( keyName = "summerPieWithdrawAmount", name = "Summer pie amount", description = "Target amount of summer pies to withdraw when banking. 0 disables summer pie banking.", - position = 7, + position = 8, section = bankingSection ) @Range(min = 0, max = 28) diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityOverlay.java b/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityOverlay.java index aa94510c62..73a44353a3 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityOverlay.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityOverlay.java @@ -50,7 +50,7 @@ public Dimension render(Graphics2D graphics) panelComponent.getChildren().add(LineComponent.builder() .left("Current Obstacle") - .right(Integer.toString(config.agilityCourse().getHandler().getCurrentObstacleIndex())) + .right(Integer.toString(plugin.getAgilityScript().getCurrentObstacleIndex())) .build()); } diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityPlugin.java index 70903ef231..450a9eaa03 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/MicroAgilityPlugin.java @@ -8,6 +8,7 @@ import net.runelite.client.plugins.PluginDescriptor; import net.runelite.client.plugins.microbot.PluginConstants; import net.runelite.client.plugins.microbot.agility.courses.AgilityCourseHandler; +import net.runelite.api.gameval.ItemID; 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; @@ -34,7 +35,7 @@ @Slf4j public class MicroAgilityPlugin extends Plugin { - public static final String version = "1.3.2"; + public static final String version = "1.3.0"; @Inject private MicroAgilityConfig config; @Inject @@ -82,23 +83,28 @@ public AgilityCourseHandler getCourseHandler() public List getInventoryFood() { - return Rs2Inventory.getInventoryFood().stream().filter(i -> !(i.getName().toLowerCase().contains("summer pie"))).collect(Collectors.toList()); + return Rs2Inventory.getInventoryFood().stream().filter(i -> !isSummerPie(i)).collect(Collectors.toList()); } public List getSummerPies() { - return Rs2Inventory.getInventoryFood().stream().filter(i -> i.getName().toLowerCase().contains("summer pie")).collect(Collectors.toList()); + return Rs2Inventory.getInventoryFood().stream().filter(this::isSummerPie).collect(Collectors.toList()); } public boolean hasRequiredLevel() { - int requiredLevel = getCourseHandler().getRequiredLevel(); + return hasRequiredLevel(getCourseHandler()); + } + + public boolean hasRequiredLevel(AgilityCourseHandler courseHandler) + { + int requiredLevel = courseHandler.getRequiredLevel(); if (Rs2Player.getRealSkillLevel(Skill.AGILITY) >= requiredLevel) { return true; } - if (getSummerPies().isEmpty() || !getCourseHandler().canBeBoosted()) + if (getSummerPies().isEmpty() || !courseHandler.canBeBoosted()) { return false; } @@ -106,6 +112,16 @@ public boolean hasRequiredLevel() return Rs2Player.getBoostedSkillLevel(Skill.AGILITY) >= requiredLevel; } + public boolean hasRealRequiredLevel(AgilityCourseHandler courseHandler) + { + return Rs2Player.getRealSkillLevel(Skill.AGILITY) >= courseHandler.getRequiredLevel(); + } + + private boolean isSummerPie(Rs2ItemModel item) + { + return item != null && (item.getId() == ItemID.SUMMER_PIE || item.getId() == ItemID.HALF_SUMMER_PIE); + } + public AgilityScript getAgilityScript() { return agilityScript; } diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/AgilityCourseHandler.java b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/AgilityCourseHandler.java index 67204f3838..1f9256573b 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/AgilityCourseHandler.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/AgilityCourseHandler.java @@ -51,6 +51,21 @@ default boolean canBeBoosted() return true; } + default boolean hasRequiredCourseItems() + { + return true; + } + + default String getMissingRequiredCourseItemsMessage() + { + return "You do not have the required items for this course."; + } + + default boolean handleCourseActions(WorldPoint playerWorldLocation) + { + return handleWalkToStart(playerWorldLocation); + } + default TileObject getCurrentObstacle() { WorldPoint playerLocation = getPlayerWorldLocation(); @@ -94,7 +109,9 @@ default TileObject getCurrentObstacle() return true; }; - return Rs2GameObject.getAll(validObjectPredicate).stream().findFirst().orElse(null); + return Rs2GameObject.getAll(validObjectPredicate).stream() + .min(Comparator.comparingInt(obj -> obj.getWorldLocation().distanceTo(playerLocation))) + .orElse(null); } // Simple method to check if we should click or wait @@ -202,7 +219,7 @@ default int getCurrentObstacleIndex() } } - return (closestIndex != -1) ? closestIndex : 0; + return closestIndex; } default boolean handleWalkToStart(WorldPoint playerWorldLocation) diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/BrimhavenSpikeCourse.java b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/BrimhavenSpikeCourse.java index 9696326812..cda3a62ec6 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/BrimhavenSpikeCourse.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/BrimhavenSpikeCourse.java @@ -1,5 +1,6 @@ package net.runelite.client.plugins.microbot.agility.courses; +import lombok.extern.slf4j.Slf4j; import net.runelite.api.coords.WorldPoint; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.agility.models.AgilityObstacleModel; @@ -10,6 +11,7 @@ import java.util.List; +@Slf4j public class BrimhavenSpikeCourse implements AgilityCourseHandler { private static final WorldPoint START_POINT = new WorldPoint(2809, 3192, 0); @@ -66,7 +68,19 @@ public boolean hasRequiredCoins() { return Rs2Inventory.itemQuantity(COINS_ID) >= REQUIRED_COINS; } + @Override + public boolean hasRequiredCourseItems() { + syncStateFromLocation(Rs2Player.getWorldLocation()); + return hasPaid || hasClimbedLadder || hasRequiredCoins() || canSeeEntryLadder(); + } + + @Override + public String getMissingRequiredCourseItemsMessage() { + return "You need 200 coins to enter the Brimhaven Spike course!"; + } + public boolean hasPaid() { + syncStateFromLocation(Rs2Player.getWorldLocation()); return hasPaid; } @@ -147,61 +161,63 @@ public boolean handleLadderDescent() { */ @Override public boolean handleWalkToStart(WorldPoint playerWorldLocation) { - Microbot.log("BrimhavenSpike handleWalkToStart called - Player at: " + playerWorldLocation + ", hasPaid: " + hasPaid + ", hasClimbedLadder: " + hasClimbedLadder); + syncStateFromLocation(playerWorldLocation); + log.debug("BrimhavenSpike handleWalkToStart: player={}, hasPaid={}, hasClimbedLadder={}, obstacleIndex={}", + playerWorldLocation, hasPaid, hasClimbedLadder, currentObstacleIndex); // If we're not at the start point (ground level), walk there first if (playerWorldLocation.getPlane() != 0 || playerWorldLocation.distanceTo(START_POINT) > 5) { // If we're already in the course area (plane 3), check if we need to handle obstacle 3 if (playerWorldLocation.getPlane() == 3) { - Microbot.log("Already in the spike course area on plane 3 - checking obstacle 3"); + log.debug("Already in the spike course area on plane 3"); // If we've completed obstacles 1 and 2, force index to 2 for obstacle 3 (spikes) if (currentObstacleIndex >= 2) { currentObstacleIndex = 2; // Ensure we're on obstacle 3 - Microbot.log("Forcing to obstacle 3 (spikes) - using timed tile-walking"); + log.debug("Using timed tile-walking for Brimhaven spike obstacle"); return handleSpikeTileWalking(); } - Microbot.log("In the spike course area on plane 3 - ready for obstacles (currentObstacleIndex: " + currentObstacleIndex + ")"); + log.debug("Ready for Brimhaven spike obstacle {}", currentObstacleIndex); return false; // Let the main script handle obstacles } - Microbot.log("Walking to Brimhaven Spike course start point"); + log.debug("Walking to Brimhaven Spike course start point"); Rs2Walker.walkTo(START_POINT, 2); return true; } // If we haven't paid yet, handle payment first if (!hasPaid) { - Microbot.log("Attempting to pay Cap'n Izzy..."); + log.debug("Attempting to pay Cap'n Izzy"); boolean paymentResult = handlePayment(); - Microbot.log("Payment result: " + paymentResult); + log.debug("Payment result: {}", paymentResult); return paymentResult; } // If we haven't climbed the ladder yet, handle ladder descent if (!hasClimbedLadder) { - Microbot.log("Attempting to climb down ladder..."); + log.debug("Attempting to climb down ladder"); boolean ladderResult = handleLadderDescent(); - Microbot.log("Ladder result: " + ladderResult); + log.debug("Ladder result: {}", ladderResult); return ladderResult; } // If we're on plane 3, we're in the course area if (playerWorldLocation.getPlane() == 3) { - Microbot.log("On plane 3 - currentObstacleIndex: " + currentObstacleIndex); + log.debug("On plane 3 - currentObstacleIndex: {}", currentObstacleIndex); // If we've completed obstacles 1 and 2, force index to 2 for obstacle 3 (spikes) if (currentObstacleIndex >= 2) { currentObstacleIndex = 2; // Ensure we're on obstacle 3 - Microbot.log("Forcing to obstacle 3 (spikes) - using timed tile-walking"); + log.debug("Using timed tile-walking for Brimhaven spike obstacle"); return handleSpikeTileWalking(); } - Microbot.log("In the spike course area on plane 3 - ready for obstacles (currentObstacleIndex: " + currentObstacleIndex + ")"); + log.debug("Ready for Brimhaven spike obstacle {}", currentObstacleIndex); return false; // Let the main script handle obstacles } - Microbot.log("BrimhavenSpike handleWalkToStart returning false - no action needed"); + log.debug("BrimhavenSpike handleWalkToStart returning false - no action needed"); return false; } @@ -216,20 +232,20 @@ public boolean handleSpikeTileWalking() { : new WorldPoint(2799, 9568, 3); double initialHealth = Rs2Player.getHealthPercentage(); - Microbot.log("Walking to spike tile: " + target); + log.debug("Walking to spike tile: {}", target); Rs2Walker.walkFastCanvas(target); // Check if the player actually moved Global.sleep(500); // Give a moment for movement to start WorldPoint currentPos = Rs2Player.getWorldLocation(); if (currentPos.distanceTo(target) > 2) { - Microbot.log("Walk failed - player didn't move to target tile, retrying"); + log.debug("Walk failed - player did not move to target tile, retrying"); return false; // Let the script retry } // Check for damage (if player took damage, retry immediately) if (Rs2Player.getHealthPercentage() < initialHealth) { - Microbot.log("Player took damage - retrying obstacle immediately"); + log.debug("Player took damage - retrying obstacle immediately"); return false; // Let the script retry } @@ -241,18 +257,18 @@ public boolean handleSpikeTileWalking() { }, 4000); // 4 second timeout if (!xpGained) { - Microbot.log("No XP gained within 4 seconds - walk may have failed, retrying"); + log.debug("No XP gained within 4 seconds - walk may have failed, retrying"); return false; // Let the script retry } // Wait for animation to finish (animation ID changes to -1) - Microbot.log("XP gained! Waiting for animation to finish..."); + log.debug("XP gained, waiting for animation to finish"); Global.sleepUntil(() -> { int animationId = Rs2Player.getAnimation(); return animationId == -1; // Animation finished when ID is -1 }, 10000); // 10 second timeout for animation to finish - Microbot.log("Animation finished, ready for next obstacle attempt"); + log.debug("Animation finished, ready for next obstacle attempt"); return true; // handled this tick } @@ -267,6 +283,7 @@ public net.runelite.api.TileObject getCurrentObstacle() { // Only look for obstacles if we're actually in the spike course (plane 3) // Before that, we need to pay and climb down the ladder first if (playerLocation.getPlane() == 3) { + syncStateFromLocation(playerLocation); // Get the current obstacle based on the index List obstacles = getObstacles(); if (currentObstacleIndex < obstacles.size()) { @@ -274,13 +291,13 @@ public net.runelite.api.TileObject getCurrentObstacle() { // For obstacle 3 (spikes), there's no game object - return null to trigger timed tile-walking if (currentObstacleIndex == obstacles.size() - 1) { - Microbot.log("On obstacle 3 (spikes) - using timed tile-walking instead of game object"); + log.debug("On obstacle 3 (spikes) - using timed tile-walking instead of game object"); return null; // This will trigger the timed tile-walking logic in handleWalkToStart } var gameObject = Microbot.getRs2TileObjectCache().query().withId(currentObstacle.getObjectID()).within(playerLocation, 10).nearest(); if (gameObject != null) { - Microbot.log("Looking for obstacle " + (currentObstacleIndex + 1) + "/" + obstacles.size() + " (ID: " + currentObstacle.getObjectID() + ")"); + log.debug("Looking for obstacle {}/{} (ID: {})", currentObstacleIndex + 1, obstacles.size(), currentObstacle.getObjectID()); return gameObject; } } @@ -309,14 +326,14 @@ public boolean isObstacleComplete(int currentXp, int previousXp, long lastMoving // Advance to next obstacle currentObstacleIndex++; List obstacles = getObstacles(); - Microbot.log("XP gained! Current obstacle index: " + currentObstacleIndex + ", obstacles size: " + obstacles.size()); + log.debug("XP gained. Current obstacle index: {}, obstacles size: {}", currentObstacleIndex, obstacles.size()); if (currentObstacleIndex >= obstacles.size()) { // For BrimhavenSpike, stay on obstacle 3 (spikes) forever instead of looping back currentObstacleIndex = obstacles.size() - 1; // Stay on last obstacle (spikes) - Microbot.log("Completed all obstacles, staying on obstacle 3 (spikes) for timed tile-walking"); + log.debug("Completed all obstacles, staying on obstacle 3 for timed tile-walking"); } else { - Microbot.log("Obstacle completed! Moving to obstacle " + (currentObstacleIndex + 1) + "/" + obstacles.size()); + log.debug("Obstacle completed. Moving to obstacle {}/{}", currentObstacleIndex + 1, obstacles.size()); } return true; @@ -330,4 +347,27 @@ public boolean isObstacleComplete(int currentXp, int previousXp, long lastMoving // Check if we've waited long enough after movement stopped return System.currentTimeMillis() - lastMovingTime >= waitDelay; } + + private void syncStateFromLocation(WorldPoint playerLocation) { + if (isInsideSpikeCourse(playerLocation)) { + hasPaid = true; + hasClimbedLadder = true; + } + } + + private boolean canSeeEntryLadder() { + WorldPoint playerLocation = Rs2Player.getWorldLocation(); + return playerLocation != null + && playerLocation.distanceTo(START_POINT) <= 10 + && Microbot.getRs2TileObjectCache().query().withId(3617).within(playerLocation, 12).nearest() != null; + } + + private boolean isInsideSpikeCourse(WorldPoint playerLocation) { + return playerLocation != null + && playerLocation.getPlane() == 3 + && playerLocation.getX() >= 2788 + && playerLocation.getX() <= 2820 + && playerLocation.getY() >= 9548 + && playerLocation.getY() <= 9585; + } } diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/GnomeStrongholdCourse.java b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/GnomeStrongholdCourse.java index 354781ad6e..e9b15d8254 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/GnomeStrongholdCourse.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/GnomeStrongholdCourse.java @@ -34,4 +34,10 @@ public Integer getRequiredLevel() { return 1; } + + @Override + public boolean handleCourseActions(WorldPoint playerWorldLocation) + { + return false; + } } diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/PrifddinasCourse.java b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/PrifddinasCourse.java index 0c8b7a91fa..967ead9c67 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/PrifddinasCourse.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/PrifddinasCourse.java @@ -138,6 +138,12 @@ public TileObject getCurrentObstacle() return AgilityCourseHandler.super.getCurrentObstacle(); } + @Override + public boolean handleCourseActions(WorldPoint playerWorldLocation) + { + return handlePortal() || handleWalkToStart(playerWorldLocation); + } + @Override public boolean handleWalkToStart(WorldPoint playerWorldLocation) { diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/WerewolfCourse.java b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/WerewolfCourse.java index 224e44b744..16e126be6c 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/courses/WerewolfCourse.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/courses/WerewolfCourse.java @@ -135,6 +135,14 @@ public boolean handleFirstSteppingStone(WorldPoint playerWorldLocation) { return false; } + @Override + public boolean handleCourseActions(WorldPoint playerWorldLocation) { + return handleFirstSteppingStone(playerWorldLocation) + || handleStickPickup(playerWorldLocation) + || handleSlide() + || handleStickReturn(playerWorldLocation); + } + public boolean handleStickPickup(WorldPoint playerWorldLocation) { if(matchingObject instanceof GameObject && matchingObject.getId() == ObjectID.WAA_PIPE && playerWorldLocation.getY() > matchingObstacle.getRequiredY()) { var stickTile = AgilityPlugin.getStickTile(); diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/enums/AgilityEquipmentOption.java b/src/main/java/net/runelite/client/plugins/microbot/agility/enums/AgilityEquipmentOption.java new file mode 100644 index 0000000000..fe0694ec8f --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/enums/AgilityEquipmentOption.java @@ -0,0 +1,31 @@ +package net.runelite.client.plugins.microbot.agility.enums; + +public enum AgilityEquipmentOption +{ + NONE("None"), + GRACEFUL("Graceful"), + GRACEFUL_WITH_AGILITY_CAPE("Graceful + agility cape"); + + private final String displayName; + + AgilityEquipmentOption(String displayName) + { + this.displayName = displayName; + } + + public boolean isNone() + { + return this == NONE; + } + + public boolean useAgilityCape() + { + return this == GRACEFUL_WITH_AGILITY_CAPE; + } + + @Override + public String toString() + { + return displayName; + } +} From 2691a9c1aa7f8bfc5da373dcdb12d4742898441f Mon Sep 17 00:00:00 2001 From: infuse21 Date: Wed, 1 Jul 2026 00:49:10 +0100 Subject: [PATCH 07/13] Agility plugin features and bug fixes --- .../microbot/agility/AgilityScript.java | 96 ++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java b/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java index ad7aa89a39..30d55260e8 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/AgilityScript.java @@ -2,6 +2,10 @@ import net.runelite.api.Skill; import net.runelite.api.TileObject; +import net.runelite.api.ItemComposition; +import net.runelite.api.MenuAction; +import net.runelite.api.Perspective; +import net.runelite.api.coords.LocalPoint; import net.runelite.api.coords.WorldPoint; import net.runelite.api.gameval.ItemID; import net.runelite.client.plugins.microbot.Microbot; @@ -16,10 +20,14 @@ import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; import net.runelite.client.plugins.microbot.util.magic.Rs2Magic; +import net.runelite.client.plugins.microbot.util.menu.NewMenuEntry; import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.reflection.Rs2Reflection; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; import javax.inject.Inject; +import java.awt.Polygon; +import java.awt.Rectangle; import java.awt.EventQueue; import java.util.Arrays; import java.util.Comparator; @@ -488,7 +496,7 @@ else if (Rs2Player.isMoving() || Rs2Player.isAnimating()) } int markCount = Rs2Inventory.itemQuantity(ItemID.GRACE); - if (!markOfGrace.pickup()) + if (!pickupMarkOfGrace(markOfGrace)) { return false; } @@ -529,6 +537,92 @@ private boolean hasLootableMarkAt(WorldPoint markLocation) .first() != null; } + private boolean pickupMarkOfGrace(Rs2TileItemModel markOfGrace) + { + try + { + ItemComposition item = Microbot.getClientThread() + .runOnClientThreadOptional(() -> Microbot.getClient().getItemDefinition(markOfGrace.getId())) + .orElse(null); + if (item == null) + { + return false; + } + + LocalPoint localPoint = markOfGrace.getLocalLocation(); + if (localPoint == null) + { + return false; + } + + MenuAction menuAction = getGroundItemMenuAction(item, "Take"); + if (menuAction == null) + { + return false; + } + + if (!Rs2Camera.isTileOnScreen(localPoint)) + { + Rs2Camera.turnTo(localPoint); + } + + Polygon canvasTile = Perspective.getCanvasTilePoly(Microbot.getClient(), localPoint); + Rectangle clickBounds = canvasTile == null + ? new Rectangle(1, 1, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight()) + : canvasTile.getBounds(); + + Microbot.doInvoke(new NewMenuEntry() + .param0(localPoint.getSceneX()) + .param1(localPoint.getSceneY()) + .opcode(menuAction.getId()) + .identifier(markOfGrace.getId()) + .itemId(-1) + .option("Take") + .target("" + item.getName()) + .worldViewId(localPoint.getWorldView()), + clickBounds); + return true; + } + catch (Exception ex) + { + Microbot.log("Failed to pick up Mark of grace: " + ex.getMessage()); + return false; + } + } + + private MenuAction getGroundItemMenuAction(ItemComposition item, String action) + { + String[] groundActions = Rs2Reflection.getGroundItemActions(item); + for (int i = 0; i < groundActions.length; i++) + { + String groundAction = groundActions[i]; + if (groundAction != null && groundAction.equalsIgnoreCase(action)) + { + return groundItemMenuAction(i); + } + } + return null; + } + + private MenuAction groundItemMenuAction(int index) + { + switch (index) + { + case 0: + return MenuAction.GROUND_ITEM_FIRST_OPTION; + case 1: + return MenuAction.GROUND_ITEM_SECOND_OPTION; + case 2: + return MenuAction.GROUND_ITEM_THIRD_OPTION; + case 3: + return MenuAction.GROUND_ITEM_FOURTH_OPTION; + case 4: + return MenuAction.GROUND_ITEM_FIFTH_OPTION; + default: + return null; + } + } + private boolean shouldPerformAlch(TileObject gameObject) { if (!config.alchemy()) From 78df28812ba80ebdbb40d514d52a8e6a617637ac Mon Sep 17 00:00:00 2001 From: infuse21 Date: Wed, 1 Jul 2026 00:50:06 +0100 Subject: [PATCH 08/13] Agility plugin features and bug fixes --- .../AutoEssenceMiningConfig.java | 42 +- .../AutoEssenceMiningPlugin.java | 4 +- .../AutoEssenceMiningScript.java | 637 ++++++++++++++++-- 3 files changed, 614 insertions(+), 69 deletions(-) diff --git a/src/main/java/net/runelite/client/plugins/microbot/autoessencemining/AutoEssenceMiningConfig.java b/src/main/java/net/runelite/client/plugins/microbot/autoessencemining/AutoEssenceMiningConfig.java index a444eba2e1..6d073d4376 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/autoessencemining/AutoEssenceMiningConfig.java +++ b/src/main/java/net/runelite/client/plugins/microbot/autoessencemining/AutoEssenceMiningConfig.java @@ -4,6 +4,36 @@ @ConfigGroup("EssenceMining") public interface AutoEssenceMiningConfig extends Config { + enum PickaxeOverride { + AUTO("Auto"), + BRONZE("Bronze pickaxe"), + IRON("Iron pickaxe"), + STEEL("Steel pickaxe"), + BLACK("Black pickaxe"), + MITHRIL("Mithril pickaxe"), + ADAMANT("Adamant pickaxe"), + RUNE("Rune pickaxe"), + GILDED("Gilded pickaxe"), + DRAGON("Dragon pickaxe"), + DRAGON_OR("Dragon pickaxe (or)"), + INFERNAL("Infernal pickaxe"), + INFERNAL_EMPTY("Infernal pickaxe (uncharged)"), + CRYSTAL("Crystal pickaxe"), + THIRD_AGE("3rd age pickaxe"), + TRAILBLAZER("Trailblazer pickaxe"); + + private final String name; + + PickaxeOverride(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + @ConfigItem( keyName = "Guide", name = "Usage guide", @@ -13,4 +43,14 @@ public interface AutoEssenceMiningConfig extends Config { default String GUIDE() { return "Begin anywhere with a pickaxe wielded or in your inventory..."; } -} \ No newline at end of file + + @ConfigItem( + keyName = "manualPickaxeOverride", + name = "Manual Pickaxe Override", + description = "Choose a specific pickaxe, or Auto to use the best usable pickaxe found in your bank, inventory, or weapon slot.", + position = 2 + ) + default PickaxeOverride manualPickaxeOverride() { + return PickaxeOverride.AUTO; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/autoessencemining/AutoEssenceMiningPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/autoessencemining/AutoEssenceMiningPlugin.java index de7bafc1b1..766fce63d9 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/autoessencemining/AutoEssenceMiningPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/autoessencemining/AutoEssenceMiningPlugin.java @@ -25,7 +25,7 @@ ) @Slf4j public class AutoEssenceMiningPlugin extends Plugin { - static final String version = "1.0.2"; + static final String version = "1.0.3"; @Inject private AutoEssenceMiningConfig config; @@ -56,4 +56,4 @@ protected void shutDown() { autoEssenceMiningScript.shutdown(); overlayManager.remove(autoEssenceMiningOverlay); } -} \ No newline at end of file +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/autoessencemining/AutoEssenceMiningScript.java b/src/main/java/net/runelite/client/plugins/microbot/autoessencemining/AutoEssenceMiningScript.java index f72a9cb168..0035b67f87 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/autoessencemining/AutoEssenceMiningScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/autoessencemining/AutoEssenceMiningScript.java @@ -3,33 +3,131 @@ import lombok.extern.slf4j.Slf4j; import net.runelite.api.Skill; import net.runelite.api.coords.WorldPoint; +import net.runelite.api.gameval.ItemID; +import net.runelite.api.gameval.NpcID; +import net.runelite.api.gameval.ObjectID1; +import net.runelite.client.plugins.microbot.agentserver.handler.ScriptHeartbeatRegistry; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.Script; import net.runelite.client.plugins.microbot.autoessencemining.enums.AutoEssenceMiningState; import net.runelite.client.plugins.microbot.util.antiban.Rs2Antiban; import net.runelite.client.plugins.microbot.util.antiban.Rs2AntibanSettings; import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; +import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; +import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; +import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; 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.walker.Rs2Walker; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; @Slf4j public class AutoEssenceMiningScript extends Script { private static final WorldPoint AUBURY_LOCATION = new WorldPoint(3253, 3399, 0); + private static final WorldPoint VARROCK_EAST_BANK_LOCATION = new WorldPoint(3253, 3420, 0); private static final int ESSENCE_MINE_REGION = 11595; // Rune essence mine region ID + private static final String AUBURY_TELEPORT_ACTION = "Teleport"; + private static final long AUBURY_TELEPORT_RETRY_DELAY_MS = 1500; + private static final long AUBURY_WALK_RETRY_DELAY_MS = 1500; + private static final long BANK_WALK_RETRY_DELAY_MS = 1500; + private static final long BUSY_WAIT_LOG_INTERVAL_MS = 5000; + private static final long STATE_EVALUATION_LOG_INTERVAL_MS = 5000; + private static final int AUBURY_INTERACTION_DISTANCE = 14; + private static final int AUBURY_STEP_DISTANCE = 12; + private static final int BANK_INTERACTION_DISTANCE = 10; + private static final int BANK_STEP_DISTANCE = 12; + private static final int PICKAXE_INVENTORY_SLOT = 0; + private static final int[] RUNECRAFTING_POUCH_IDS = { + ItemID.RCU_POUCH_SMALL, + ItemID.RCU_POUCH_MEDIUM, + ItemID.RCU_POUCH_MEDIUM_DEGRADE, + ItemID.RCU_POUCH_LARGE, + ItemID.RCU_POUCH_LARGE_DEGRADE, + ItemID.RCU_POUCH_GIANT, + ItemID.RCU_POUCH_GIANT_DEGRADE, + ItemID.RCU_POUCH_COLOSSAL, + ItemID.RCU_POUCH_COLOSSAL_DEGRADE, + ItemID.DEVIOUS_GLOWINGPOUCH_COLOSSAL + }; + private static final String[] RUNECRAFTING_POUCH_NAMES = { + "Small pouch", + "Medium pouch", + "Large pouch", + "Giant pouch", + "Colossal pouch" + }; + private static final int[] ESSENCE_MINE_EXIT_PORTAL_IDS = { + NpcID.ESSENCEMINE_PORTAL_1, + NpcID.ESSENCEMINE_PORTAL_2 + }; + private static final int[] ESSENCE_MINE_EXIT_PORTAL_OBJECT_IDS = { + ObjectID1.BLANKRUNESTONE_EXIT_PORTAL_2, + ObjectID1.ESSENCEMINE_PORTAL_1, + ObjectID1.ESSENCEMINE_PORTAL_2, + ObjectID1.ESSENCEMINE_PORTAL_3, + ObjectID1.ESSENCEMINE_PORTAL_4, + ObjectID1.ESSENCEMINE_PORTAL_5, + ObjectID1.BLANKRUNESTONE_EXIT_PORTAL + }; private AutoEssenceMiningState state = AutoEssenceMiningState.WALKING_TO_AUBURY; private boolean hasTeleportedWithAubury = false; private boolean isInEssenceMine = false; private boolean needsToBank = false; - private long stateStartTime = System.currentTimeMillis(); // track state timeout + private long lastAuburyTeleportAttempt = 0; + private long lastAuburyWalkAttempt = 0; + private long lastBankWalkAttempt = 0; + private long lastStateEvaluationLog = 0; + private long lastBusyWaitLog = 0; + private long lastAntibanCooldownLog = 0; + private final AtomicBoolean stopRequested = new AtomicBoolean(false); + + private enum PickaxeTool { + BRONZE(AutoEssenceMiningConfig.PickaxeOverride.BRONZE, ItemID.BRONZE_PICKAXE, "Bronze pickaxe", 1, 1, 10), + IRON(AutoEssenceMiningConfig.PickaxeOverride.IRON, ItemID.IRON_PICKAXE, "Iron pickaxe", 1, 1, 20), + STEEL(AutoEssenceMiningConfig.PickaxeOverride.STEEL, ItemID.STEEL_PICKAXE, "Steel pickaxe", 6, 5, 30), + BLACK(AutoEssenceMiningConfig.PickaxeOverride.BLACK, ItemID.BLACK_PICKAXE, "Black pickaxe", 11, 10, 40), + MITHRIL(AutoEssenceMiningConfig.PickaxeOverride.MITHRIL, ItemID.MITHRIL_PICKAXE, "Mithril pickaxe", 21, 20, 50), + ADAMANT(AutoEssenceMiningConfig.PickaxeOverride.ADAMANT, ItemID.ADAMANT_PICKAXE, "Adamant pickaxe", 31, 30, 60), + RUNE(AutoEssenceMiningConfig.PickaxeOverride.RUNE, ItemID.RUNE_PICKAXE, "Rune pickaxe", 41, 40, 70), + GILDED(AutoEssenceMiningConfig.PickaxeOverride.GILDED, ItemID.TRAIL_GILDED_PICKAXE, "Gilded pickaxe", 41, 40, 71), + DRAGON(AutoEssenceMiningConfig.PickaxeOverride.DRAGON, ItemID.DRAGON_PICKAXE, "Dragon pickaxe", 61, 60, 80), + DRAGON_OR(AutoEssenceMiningConfig.PickaxeOverride.DRAGON_OR, ItemID.DRAGON_PICKAXE_PRETTY, "Dragon pickaxe (or)", 61, 60, 81), + INFERNAL_EMPTY(AutoEssenceMiningConfig.PickaxeOverride.INFERNAL_EMPTY, ItemID.INFERNAL_PICKAXE_EMPTY, "Infernal pickaxe (uncharged)", 61, 60, 82), + INFERNAL(AutoEssenceMiningConfig.PickaxeOverride.INFERNAL, ItemID.INFERNAL_PICKAXE, "Infernal pickaxe", 61, 60, 83), + THIRD_AGE(AutoEssenceMiningConfig.PickaxeOverride.THIRD_AGE, ItemID._3A_PICKAXE, "3rd age pickaxe", 61, 65, 84), + TRAILBLAZER(AutoEssenceMiningConfig.PickaxeOverride.TRAILBLAZER, ItemID.TRAILBLAZER_PICKAXE, "Trailblazer pickaxe", 61, 60, 85), + CRYSTAL(AutoEssenceMiningConfig.PickaxeOverride.CRYSTAL, ItemID.CRYSTAL_PICKAXE, "Crystal pickaxe", 71, 70, 90); + + private final AutoEssenceMiningConfig.PickaxeOverride override; + private final int id; + private final String itemName; + private final int miningLevel; + private final int attackLevel; + private final int priority; + + PickaxeTool(AutoEssenceMiningConfig.PickaxeOverride override, int id, String itemName, int miningLevel, int attackLevel, int priority) { + this.override = override; + this.id = id; + this.itemName = itemName; + this.miningLevel = miningLevel; + this.attackLevel = attackLevel; + this.priority = priority; + } + } public boolean run(AutoEssenceMiningConfig config) { log.info("Starting essence mining script"); + cancelScheduledTasks(); + stopRequested.set(false); + resetState(); initialPlayerLocation = Rs2Player.getWorldLocation(); Rs2Antiban.resetAntibanSettings(); Rs2Antiban.antibanSetupTemplates.applyMiningSetup(); @@ -37,42 +135,47 @@ public boolean run(AutoEssenceMiningConfig config) { mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { try { + if (shouldStop()) { + return; + } + if (!super.run()) { log.info("Super.run() returned false, stopping"); return; } + if (shouldStop()) { + return; + } if (!Microbot.isLoggedIn()) { log.info("Not logged in, waiting"); return; } if (Rs2AntibanSettings.actionCooldownActive) { - log.info("Antiban cooldown active, waiting"); + logAntibanCooldownWait(); return; } // track loop performance long startTime = System.currentTimeMillis(); - // state timeout protection - if (System.currentTimeMillis() - stateStartTime > 30000) { - log.info("State timeout after 30 seconds, resetting to WALKING_TO_AUBURY"); - changeState(AutoEssenceMiningState.WALKING_TO_AUBURY); + // determine current location and state + isInEssenceMine = isInEssenceMine(); + needsToBank = needsBankingBeforeTrip(config); + + if (shouldStop()) { return; } - if (Rs2Player.isMoving() || Rs2Player.isAnimating()) { - log.info("Player is moving or animating, waiting"); + if (!isInEssenceMine && !needsToBank && attemptAuburyTeleportIfVisible()) { return; } - // determine current location and state - isInEssenceMine = (Rs2Player.getWorldLocation().getRegionID() == ESSENCE_MINE_REGION); - needsToBank = Rs2Inventory.isFull(); + if (Rs2Player.isMoving() || Rs2Player.isAnimating()) { + logBusyWait(); + return; + } - log.info("=== State Evaluation ==="); - log.info("In essence mine: {}", isInEssenceMine); - log.info("Inventory full: {}", needsToBank); - log.info("Distance to Aubury: {}", Rs2Player.getWorldLocation().distanceTo(AUBURY_LOCATION)); + logStateEvaluation(); if (isInEssenceMine) { if (!needsToBank) { @@ -115,7 +218,7 @@ public boolean run(AutoEssenceMiningConfig config) { handleUsingPortal(); break; case BANKING: - handleBanking(); + handleBanking(config); break; } @@ -125,6 +228,11 @@ public boolean run(AutoEssenceMiningConfig config) { log.info("Total time for loop: {}ms", totalTime); } catch (Exception ex) { + if (isClientThreadInterrupted(ex)) { + log.info("Essence mining loop interrupted during shutdown"); + return; + } + log.error("Error in main essence mining loop: {}", ex.getMessage(), ex); } }, 0, 100, TimeUnit.MILLISECONDS); @@ -132,54 +240,55 @@ public boolean run(AutoEssenceMiningConfig config) { } private void handleWalkingToAubury() { + if (shouldStop()) { + return; + } + log.info("State: WALKING_TO_AUBURY"); Microbot.status = "Walking to Aubury"; + + if (attemptAuburyTeleportIfVisible()) { + return; + } // validate current distance before walking int currentDistance = Rs2Player.getWorldLocation().distanceTo(AUBURY_LOCATION); log.info("Current distance to Aubury: {} tiles", currentDistance); - if (currentDistance <= 8) { + if (currentDistance <= AUBURY_INTERACTION_DISTANCE) { log.info("Already near Aubury, no need to walk"); return; } - // attempt to walk to Aubury - if (Rs2Walker.walkTo(AUBURY_LOCATION)) { - log.info("Started walking to Aubury location"); + if (walkTowardAuburyNonBlocking()) { + log.info("Issued short walk step toward Aubury"); } else { - log.info("Failed to start walking to Aubury"); + log.info("Failed short Aubury step, will retry next loop"); } } private void handleTeleportingWithAubury() { - log.info("State: TELEPORTING_WITH_AUBURY"); - Microbot.status = "Teleporting with Aubury"; - - // validate we're close enough to Aubury before attempting teleport - if (Rs2Player.getWorldLocation().distanceTo(AUBURY_LOCATION) > 8) { - log.info("Too far from Aubury for teleport, distance: {}", Rs2Player.getWorldLocation().distanceTo(AUBURY_LOCATION)); + if (shouldStop()) { return; } - + + log.info("State: TELEPORTING_WITH_AUBURY"); + Microbot.status = "Teleporting with Aubury"; + // find Aubury NPC Rs2NpcModel aubury = Microbot.getRs2NpcCache().query().withName("Aubury").nearestOnClientThread(); if (aubury != null) { - log.info("Found Aubury, attempting teleport"); - if (aubury.click("Teleport")) { - log.info("Clicked teleport, waiting for animation"); - Rs2Player.waitForAnimation(3000); - log.info("Teleport animation completed"); - hasTeleportedWithAubury = true; - } else { - log.info("Failed to interact with Aubury for teleport"); - } + teleportWithAubury(aubury); } else { log.info("Aubury NPC not found nearby"); } } private void handleMiningEssence() { + if (shouldStop()) { + return; + } + log.info("State: MINING_ESSENCE"); Microbot.status = "Mining essence"; @@ -220,6 +329,10 @@ private void handleMiningEssence() { } private void handleUsingPortal() { + if (shouldStop()) { + return; + } + log.info("State: USING_PORTAL"); Microbot.status = "Using portal to exit"; @@ -229,14 +342,19 @@ private void handleUsingPortal() { return; } - // find the portal to exit - var portal = Microbot.getRs2TileObjectCache().query().withName("Portal").nearestOnClientThread(); + if (interactWithPortalObject(null) || interactWithPortalObject("Use") || interactWithPortalObject("Exit")) { + return; + } + + // Some clients expose these as NPCs; keep this as a fallback after object lookup. + Rs2NpcModel portal = Microbot.getRs2NpcCache().query().withIds(ESSENCE_MINE_EXIT_PORTAL_IDS).nearestOnClientThread(); + if (portal == null) { + portal = Microbot.getRs2NpcCache().query().withName("Portal").nearestOnClientThread(); + } if (portal != null) { log.info("Found portal, attempting to use it"); - if (portal.click()) { - log.info("Clicked portal, waiting for teleport animation"); - Rs2Player.waitForAnimation(3000); + if (portal.click() && sleepUntil(() -> !isInEssenceMine(), 5000)) { log.info("Successfully used portal to exit essence mine"); hasTeleportedWithAubury = false; } else { @@ -247,13 +365,29 @@ private void handleUsingPortal() { } } + private boolean interactWithPortalObject(String action) { + String actionDescription = action == null || action.isBlank() ? "default" : action; + log.info("Attempting portal object interaction with {} action", actionDescription); + + if (Rs2GameObject.interact(ESSENCE_MINE_EXIT_PORTAL_OBJECT_IDS, action) && sleepUntil(() -> !isInEssenceMine(), 5000)) { + log.info("Successfully used portal object with {} action", actionDescription); + hasTeleportedWithAubury = false; + return true; + } + + return false; + } + + + private void handleBanking(AutoEssenceMiningConfig config) { + if (shouldStop()) { + return; + } - private void handleBanking() { log.info("State: BANKING"); Microbot.status = "Banking at Varrock East"; - WorldPoint varrockEastBank = new WorldPoint(3253, 3420, 0); - int distanceToBank = Rs2Player.getWorldLocation().distanceTo(varrockEastBank); + int distanceToBank = Rs2Player.getWorldLocation().distanceTo(VARROCK_EAST_BANK_LOCATION); log.info("=== Banking State Check ==="); log.info("Distance to bank: {} tiles", distanceToBank); @@ -262,55 +396,426 @@ private void handleBanking() { // walk to bank if too far if (!Rs2Bank.isOpen()) { - if (distanceToBank > 10) { + if (distanceToBank > BANK_INTERACTION_DISTANCE) { log.info("Too far from bank, walking there"); - Rs2Walker.walkTo(varrockEastBank); + walkTowardBankNonBlocking(); return; } // attempt to open bank log.info("Near bank, attempting to open"); - if (!Rs2Bank.walkToBankAndUseBank()) { + if (!Rs2Bank.openBank()) { log.info("Failed to open bank"); + walkTowardBankNonBlocking(); return; } } // perform banking operations if (Rs2Bank.isOpen()) { - log.info("Bank is open, depositing essence except pickaxe"); - Rs2Bank.depositAllExcept("pickaxe"); - - // verify items were deposited - boolean inventoryCleared = sleepUntil(() -> !Rs2Inventory.isFull(), 3000); - if (inventoryCleared) { - log.info("Successfully deposited essence"); - } else { - log.info("Inventory still full after deposit attempt"); + if (prepareInventoryForMining(config)) { + hasTeleportedWithAubury = false; } - - log.info("Closing bank after deposit"); - Rs2Bank.closeBank(); - - // reset teleport flag for next trip - hasTeleportedWithAubury = false; } } + private boolean prepareInventoryForMining(AutoEssenceMiningConfig config) { + if (shouldStop()) { + return false; + } + + log.info("Bank is open, preparing mining inventory"); + + Rs2Bank.depositAllExcept(this::isAllowedDuringInitialBankCleanup); + sleepUntil(this::inventoryOnlyHasInitialBankAllowedItems, 3000); + + Rs2Bank.depositAll(this::isEssenceItem); + sleepUntil(() -> Rs2Inventory.items().noneMatch(this::isEssenceItem), 3000); + + Optional selectedPickaxe = selectPickaxe(config); + if (selectedPickaxe.isEmpty()) { + Microbot.showMessage("No usable pickaxe found for Auto Essence Mining."); + log.info("No usable pickaxe found in bank, inventory, or equipment"); + return false; + } + + PickaxeTool pickaxe = selectedPickaxe.get(); + log.info("Selected pickaxe: {}", pickaxe.itemName); + + if (!ensurePickaxeAvailable(pickaxe)) { + Microbot.showMessage("Could not withdraw selected pickaxe: " + pickaxe.itemName); + log.info("Failed to make selected pickaxe available: {}", pickaxe.itemName); + return false; + } + + cleanInventoryBeforeMining(pickaxe); + + if (canWieldPickaxe(pickaxe)) { + if (!equipPickaxe(pickaxe)) { + Microbot.showMessage("Could not equip selected pickaxe: " + pickaxe.itemName); + log.info("Failed to equip pickaxe: {}", pickaxe.itemName); + return false; + } + cleanInventoryBeforeMining(pickaxe); + } + + if (!finalInventoryReady(pickaxe)) { + log.info("Final inventory check failed after bank prep"); + return false; + } + + log.info("Closing bank after inventory prep"); + if (!Rs2Bank.closeBank()) { + log.info("Failed to close bank after inventory prep"); + return false; + } + + if (!canWieldPickaxe(pickaxe)) { + moveInventoryPickaxeToSlotOne(pickaxe); + } + + return true; + } + + private Optional selectPickaxe(AutoEssenceMiningConfig config) { + AutoEssenceMiningConfig.PickaxeOverride override = config.manualPickaxeOverride(); + if (override != AutoEssenceMiningConfig.PickaxeOverride.AUTO) { + return Arrays.stream(PickaxeTool.values()) + .filter(pickaxe -> pickaxe.override == override) + .filter(this::canUsePickaxe) + .filter(this::hasPickaxeAvailable) + .findFirst(); + } + + return Arrays.stream(PickaxeTool.values()) + .filter(this::canUsePickaxe) + .filter(this::hasPickaxeAvailable) + .max(Comparator.comparingInt(pickaxe -> pickaxe.priority)); + } + + private boolean ensurePickaxeAvailable(PickaxeTool pickaxe) { + if (Rs2Equipment.isWearing(pickaxe.id) || Rs2Inventory.hasItem(pickaxe.id)) { + return true; + } + + if (!Rs2Bank.hasItem(pickaxe.id)) { + return false; + } + + Rs2Bank.withdrawItem(pickaxe.id); + return sleepUntil(() -> Rs2Inventory.hasItem(pickaxe.id), 3000); + } + + private boolean equipPickaxe(PickaxeTool pickaxe) { + if (Rs2Equipment.isWearing(pickaxe.id)) { + return true; + } + + if (!Rs2Inventory.hasItem(pickaxe.id) && !ensurePickaxeAvailable(pickaxe)) { + return false; + } + + Rs2Inventory.wield(pickaxe.id); + return sleepUntil(() -> Rs2Equipment.isWearing(pickaxe.id), 3000); + } + + private void cleanInventoryBeforeMining(PickaxeTool selectedPickaxe) { + Rs2Bank.depositAllExcept(this::isAllowedDuringInitialBankCleanup); + sleepUntil(this::inventoryOnlyHasInitialBankAllowedItems, 3000); + Rs2Bank.depositAll(this::isEssenceItem); + sleepUntil(() -> Rs2Inventory.items().noneMatch(this::isEssenceItem), 3000); + depositUnselectedInventoryPickaxes(selectedPickaxe); + } + + private void depositUnselectedInventoryPickaxes(PickaxeTool selectedPickaxe) { + Rs2Bank.depositAll(item -> isPickaxeItem(item) && item.getId() != selectedPickaxe.id); + sleepUntil(() -> Rs2Inventory.items().noneMatch(item -> isPickaxeItem(item) && item.getId() != selectedPickaxe.id), 3000); + } + + private void moveInventoryPickaxeToSlotOne(PickaxeTool pickaxe) { + Rs2ItemModel inventoryPickaxe = Rs2Inventory.get(pickaxe.id); + if (inventoryPickaxe == null || inventoryPickaxe.getSlot() == PICKAXE_INVENTORY_SLOT) { + return; + } + + log.info("Moving {} to inventory slot 1", pickaxe.itemName); + Rs2Inventory.moveItemToSlot(inventoryPickaxe, PICKAXE_INVENTORY_SLOT); + sleepUntil(() -> { + Rs2ItemModel movedPickaxe = Rs2Inventory.get(pickaxe.id); + return movedPickaxe != null && movedPickaxe.getSlot() == PICKAXE_INVENTORY_SLOT; + }, 3000); + } + + private boolean finalInventoryReady(PickaxeTool pickaxe) { + return Rs2Inventory.items().allMatch(item -> isRunecraftingPouch(item) + || (!canWieldPickaxe(pickaxe) && item.getId() == pickaxe.id)); + } + + private boolean needsBankingBeforeTrip(AutoEssenceMiningConfig config) { + if (Rs2Inventory.isFull()) { + return true; + } + + if (isInEssenceMine) { + return false; + } + + if (inventoryHasItemsToBankBeforeTrip()) { + return true; + } + + return !hasUsablePickaxeOnPlayer(config); + } + + private boolean inventoryHasItemsToBankBeforeTrip() { + return Rs2Inventory.items().anyMatch(item -> isEssenceItem(item) + || (!isRunecraftingPouch(item) && !isPickaxeItem(item))); + } + + private boolean hasUsablePickaxeOnPlayer(AutoEssenceMiningConfig config) { + AutoEssenceMiningConfig.PickaxeOverride override = config.manualPickaxeOverride(); + if (override != AutoEssenceMiningConfig.PickaxeOverride.AUTO) { + return Arrays.stream(PickaxeTool.values()) + .filter(pickaxe -> pickaxe.override == override) + .anyMatch(pickaxe -> canUsePickaxe(pickaxe) + && (Rs2Equipment.isWearing(pickaxe.id) || Rs2Inventory.hasItem(pickaxe.id))); + } + + return Arrays.stream(PickaxeTool.values()) + .anyMatch(pickaxe -> canUsePickaxe(pickaxe) + && (Rs2Equipment.isWearing(pickaxe.id) || Rs2Inventory.hasItem(pickaxe.id))); + } + + private boolean inventoryOnlyHasInitialBankAllowedItems() { + return Rs2Inventory.items().allMatch(this::isAllowedDuringInitialBankCleanup); + } + + private boolean isAllowedDuringInitialBankCleanup(Rs2ItemModel item) { + return isEssenceItem(item) || isRunecraftingPouch(item) || isPickaxeItem(item); + } + + private boolean isEssenceItem(Rs2ItemModel item) { + if (item == null || item.getName() == null) { + return false; + } + + String name = item.getName().toLowerCase(); + return name.equals("rune essence") || name.equals("pure essence"); + } + + private boolean isRunecraftingPouch(Rs2ItemModel item) { + if (item == null || item.getName() == null) { + return false; + } + + return Arrays.stream(RUNECRAFTING_POUCH_IDS).anyMatch(pouchId -> pouchId == item.getId()) + || Arrays.stream(RUNECRAFTING_POUCH_NAMES) + .anyMatch(pouchName -> pouchName.equalsIgnoreCase(item.getName())); + } + + private boolean isPickaxeItem(Rs2ItemModel item) { + if (item == null || item.getName() == null) { + return false; + } + + return Arrays.stream(PickaxeTool.values()).anyMatch(pickaxe -> pickaxe.id == item.getId()) + || item.getName().toLowerCase().contains("pickaxe"); + } + + private boolean hasPickaxeAvailable(PickaxeTool pickaxe) { + return Rs2Equipment.isWearing(pickaxe.id) + || Rs2Inventory.hasItem(pickaxe.id) + || Rs2Bank.hasItem(pickaxe.id); + } + + private boolean canUsePickaxe(PickaxeTool pickaxe) { + return Rs2Player.getSkillRequirement(Skill.MINING, pickaxe.miningLevel); + } + + private boolean canWieldPickaxe(PickaxeTool pickaxe) { + return Rs2Player.getSkillRequirement(Skill.ATTACK, pickaxe.attackLevel); + } + // helper method to change state with timeout reset private void changeState(AutoEssenceMiningState newState) { if (newState != state) { log.info("State change: {} -> {}", state, newState); state = newState; - stateStartTime = System.currentTimeMillis(); } } + private void resetState() { + state = AutoEssenceMiningState.WALKING_TO_AUBURY; + hasTeleportedWithAubury = false; + isInEssenceMine = false; + needsToBank = false; + lastAuburyTeleportAttempt = 0; + lastAuburyWalkAttempt = 0; + lastBankWalkAttempt = 0; + lastStateEvaluationLog = 0; + lastBusyWaitLog = 0; + lastAntibanCooldownLog = 0; + } + + private boolean isClientThreadInterrupted(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + if (current instanceof InterruptedException) { + return true; + } + + String message = current.getMessage(); + if (message != null && message.contains("Interrupted waiting for client thread")) { + return true; + } + + current = current.getCause(); + } + + return false; + } + + private boolean isInEssenceMine() { + return Rs2Player.getWorldLocation().getRegionID() == ESSENCE_MINE_REGION; + } + + private boolean waitUntilInEssenceMine() { + return sleepUntil(() -> shouldStop() || isInEssenceMine(), 5000) && !shouldStop(); + } + + private boolean attemptAuburyTeleportIfVisible() { + if (shouldStop()) { + return false; + } + + long now = System.currentTimeMillis(); + if (now - lastAuburyTeleportAttempt < AUBURY_TELEPORT_RETRY_DELAY_MS) { + return false; + } + + Rs2NpcModel aubury = Microbot.getRs2NpcCache().query().withName("Aubury").nearestOnClientThread(); + if (aubury == null) { + return false; + } + + lastAuburyTeleportAttempt = now; + changeState(AutoEssenceMiningState.TELEPORTING_WITH_AUBURY); + teleportWithAubury(aubury); + return true; + } + + private boolean walkTowardAuburyNonBlocking() { + long now = System.currentTimeMillis(); + if (now - lastAuburyWalkAttempt < AUBURY_WALK_RETRY_DELAY_MS) { + return true; + } + + WorldPoint currentLocation = Rs2Player.getWorldLocation(); + WorldPoint nextStep = stepToward(currentLocation, AUBURY_LOCATION, AUBURY_STEP_DISTANCE); + lastAuburyWalkAttempt = now; + + log.info("Walking toward Aubury with short step: {}", nextStep); + return !shouldStop() && Rs2Walker.walkMiniMap(nextStep); + } + + private boolean walkTowardBankNonBlocking() { + long now = System.currentTimeMillis(); + if (now - lastBankWalkAttempt < BANK_WALK_RETRY_DELAY_MS) { + return true; + } + + WorldPoint currentLocation = Rs2Player.getWorldLocation(); + WorldPoint nextStep = stepToward(currentLocation, VARROCK_EAST_BANK_LOCATION, BANK_STEP_DISTANCE); + lastBankWalkAttempt = now; + + log.info("Walking toward bank with short step: {}", nextStep); + return !shouldStop() && Rs2Walker.walkMiniMap(nextStep); + } + + private WorldPoint stepToward(WorldPoint currentLocation, WorldPoint targetLocation, int maxStepDistance) { + int deltaX = targetLocation.getX() - currentLocation.getX(); + int deltaY = targetLocation.getY() - currentLocation.getY(); + int stepX = Math.min(Math.abs(deltaX), maxStepDistance) * Integer.signum(deltaX); + int stepY = Math.min(Math.abs(deltaY), maxStepDistance) * Integer.signum(deltaY); + + return new WorldPoint( + currentLocation.getX() + stepX, + currentLocation.getY() + stepY, + currentLocation.getPlane() + ); + } + + private void logBusyWait() { + long now = System.currentTimeMillis(); + if (now - lastBusyWaitLog < BUSY_WAIT_LOG_INTERVAL_MS) { + return; + } + + lastBusyWaitLog = now; + log.info("Player is moving or animating, waiting"); + } + + private void logAntibanCooldownWait() { + long now = System.currentTimeMillis(); + if (now - lastAntibanCooldownLog < BUSY_WAIT_LOG_INTERVAL_MS) { + return; + } + + lastAntibanCooldownLog = now; + log.info("Antiban cooldown active, waiting"); + } + + private void logStateEvaluation() { + long now = System.currentTimeMillis(); + if (now - lastStateEvaluationLog < STATE_EVALUATION_LOG_INTERVAL_MS) { + return; + } + + lastStateEvaluationLog = now; + log.info("=== State Evaluation ==="); + log.info("In essence mine: {}", isInEssenceMine); + log.info("Needs banking: {}", needsToBank); + log.info("Inventory full: {}", Rs2Inventory.isFull()); + log.info("Distance to Aubury: {}", Rs2Player.getWorldLocation().distanceTo(AUBURY_LOCATION)); + } + + private void teleportWithAubury(Rs2NpcModel aubury) { + if (shouldStop()) { + return; + } + + log.info("Found Aubury, attempting right-click teleport"); + if (aubury.click(AUBURY_TELEPORT_ACTION) && waitUntilInEssenceMine()) { + log.info("Teleported to essence mine with Aubury"); + hasTeleportedWithAubury = true; + return; + } + + log.info("Right-click teleport did not complete, will retry"); + } + @Override public void shutdown() { log.info("Shutting down essence mining script"); - super.shutdown(); + stopRequested.set(true); + cancelScheduledTasks(); + ScriptHeartbeatRegistry.remove(getClass().getName()); + initialPlayerLocation = null; Rs2Antiban.resetAntibanSettings(); log.info("Essence mining script shutdown complete"); } -} \ No newline at end of file + + private boolean shouldStop() { + return stopRequested.get() || Thread.currentThread().isInterrupted(); + } + + private void cancelScheduledTasks() { + if (mainScheduledFuture != null && !mainScheduledFuture.isDone()) { + mainScheduledFuture.cancel(false); + } + + if (scheduledFuture != null && !scheduledFuture.isDone()) { + scheduledFuture.cancel(false); + } + } +} From aebe6dc4ea784d1f0022b132c3fc8201af2fb902 Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 2 Jul 2026 11:40:51 -0700 Subject: [PATCH 09/13] feat(WineMaker): add stop-before-99 option leaving final wines for the user --- .../microbot/natewinemaker/WineConfig.java | 24 +++++- .../microbot/natewinemaker/WinePlugin.java | 2 +- .../microbot/natewinemaker/WineScript.java | 73 +++++++++++++++---- 3 files changed, 82 insertions(+), 17 deletions(-) diff --git a/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineConfig.java b/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineConfig.java index 3ca9bc757e..16e5d60494 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineConfig.java +++ b/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineConfig.java @@ -2,9 +2,31 @@ import net.runelite.client.config.Config; import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.Range; -@ConfigGroup("PieMaking") +@ConfigGroup("WineMaker") public interface WineConfig extends Config { + @ConfigItem( + keyName = "stopBeforeMax", + name = "Stop before 99", + description = "Log out and stop the plugin just before 99 Cooking so you can craft the final wines yourself", + position = 0 + ) + default boolean stopBeforeMax() { + return false; + } + + @Range(min = 1, max = 14) + @ConfigItem( + keyName = "winesToLeave", + name = "Wines to leave", + description = "How many wines to leave for you to craft manually (each jug of wine is 200 xp)", + position = 1 + ) + default int winesToLeave() { + return 2; + } } diff --git a/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WinePlugin.java b/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WinePlugin.java index 8903cc1c87..20a630379b 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WinePlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WinePlugin.java @@ -30,7 +30,7 @@ ) @Slf4j public class WinePlugin extends Plugin { - public final static String version = "1.2.0"; + public final static String version = "1.3.0"; @Inject private Client client; diff --git a/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineScript.java b/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineScript.java index 833c94151d..b8307e2949 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineScript.java @@ -1,5 +1,6 @@ package net.runelite.client.plugins.microbot.natewinemaker; +import net.runelite.api.Skill; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.Script; @@ -15,7 +16,13 @@ public class WineScript extends Script { + private static final int MAX_XP = 13_034_431; // 99 Cooking + private static final int WINE_XP = 200; // xp per jug of wine + + private WineConfig config; + public boolean run(WineConfig config) { + this.config = config; // Apply the cooking template as a baseline, then overlay the user's saved // antiban panel settings so anything toggled there wins over the template. Rs2Antiban.resetAntibanSettings(); @@ -28,7 +35,18 @@ public boolean run(WineConfig config) { if (!Microbot.isLoggedIn()) return; if (Rs2AntibanSettings.actionCooldownActive) return; if (Rs2AntibanSettings.microBreakActive) return; + if (config.stopBeforeMax() && winesLeftToCraft() <= 0) { + logoutAndStop("close enough to 99 Cooking - " + config.winesToLeave() + " wine(s) left for you"); + return; + } if (Rs2Inventory.count("grapes") > 0 && (Rs2Inventory.count("jug of water") > 0)) { + // Make-all crafts every pair in the inventory, so if the inventory + // holds more pairs than the 99 budget allows, rebalance via the bank + int pairs = Math.min(Rs2Inventory.count("grapes"), Rs2Inventory.count("jug of water")); + if (pairs > winesLeftToCraft()) { + bank(); + return; + } Rs2Inventory.combine("jug of water", "grapes"); sleepUntil(() -> Rs2Widget.getWidget(17694734) != null); Rs2Keyboard.keyPress('1'); @@ -51,40 +69,65 @@ public boolean run(WineConfig config) { return true; } + /** + * How many wines the script may still craft before it should hand over to the + * user. Unlimited when the stop-before-99 option is off. Batches are controlled + * at withdraw time (make-all crafts everything in the inventory), so this caps + * the final withdraw and triggers the stop once the budget is spent. + */ + private int winesLeftToCraft() { + if (!config.stopBeforeMax()) return Integer.MAX_VALUE; + int xp = Microbot.getClient().getSkillExperience(Skill.COOKING); + if (xp >= MAX_XP) return 0; + int winesToMax = (MAX_XP - xp + WINE_XP - 1) / WINE_XP; // ceil - wines until 99 + return Math.max(0, winesToMax - config.winesToLeave()); + } + private void bank(){ Rs2Bank.openBank(); if(Rs2Bank.isOpen()){ Rs2Bank.depositAll(); int jugsInBank = Rs2Bank.count("jug of water"); int grapesInBank = Rs2Bank.count("grapes"); - if(jugsInBank > 0 && grapesInBank > 0) { - // Withdraw up to 14 of each, or whatever is left for a final partial batch - int amount = Math.min(14, Math.min(jugsInBank, grapesInBank)); + // Withdraw up to 14 of each, capped by what is left in the bank (final + // partial batch) and by the stop-before-99 budget (final batch before 99) + int amount = Math.min(14, Math.min(jugsInBank, grapesInBank)); + amount = Math.min(amount, winesLeftToCraft()); + if (amount > 0) { Rs2Bank.withdrawDeficit("jug of water", amount); sleepUntil(() -> Rs2Inventory.hasItem("jug of water")); Rs2Bank.withdrawDeficit("grapes", amount); sleepUntil(() -> Rs2Inventory.hasItem("grapes")); - } else { + } else if (jugsInBank <= 0 || grapesInBank <= 0) { // Out of grapes or jugs of water: log out and stop the plugin Microbot.getNotifier().notify("Run out of Materials"); - Microbot.status = "[Shutting down] - Reason: out of grapes or jugs of water."; - Rs2Bank.closeBank(); - sleepUntil(() -> !Rs2Bank.isOpen()); - Rs2Player.logout(); - sleepUntil(() -> !Microbot.isLoggedIn(), 10000); - Plugin wineMakerPlugin = Microbot.getPluginManager().getPlugins().stream() - .filter(x -> x.getClass().getName().equals(WinePlugin.class.getName())) - .findFirst() - .orElse(null); - Microbot.stopPlugin(wineMakerPlugin); - shutdown(); + logoutAndStop("out of grapes or jugs of water"); return; } + // amount == 0 with materials available means the 99 gate stops us on the + // next loop tick - just close the bank and let the loop handle it } Rs2Bank.closeBank(); sleepUntil(() -> !Rs2Bank.isOpen()); } + private void logoutAndStop(String reason) { + Microbot.status = "[Shutting down] - Reason: " + reason + "."; + Microbot.getNotifier().notify("Wine Maker stopping: " + reason); + if (Rs2Bank.isOpen()) { + Rs2Bank.closeBank(); + sleepUntil(() -> !Rs2Bank.isOpen()); + } + Rs2Player.logout(); + sleepUntil(() -> !Microbot.isLoggedIn(), 10000); + Plugin wineMakerPlugin = Microbot.getPluginManager().getPlugins().stream() + .filter(x -> x.getClass().getName().equals(WinePlugin.class.getName())) + .findFirst() + .orElse(null); + Microbot.stopPlugin(wineMakerPlugin); + shutdown(); + } + @Override public void shutdown() { Rs2Antiban.resetAntibanSettings(); From db1808a33e757baec6e010a149fbe645946ed804 Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 2 Jul 2026 11:44:46 -0700 Subject: [PATCH 10/13] refactor(WineMaker): simplify stop-before-99 to stop one full batch early --- .../microbot/natewinemaker/WineConfig.java | 14 +------ .../microbot/natewinemaker/WineScript.java | 37 ++++++------------- 2 files changed, 12 insertions(+), 39 deletions(-) diff --git a/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineConfig.java b/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineConfig.java index 16e5d60494..9df3064a65 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineConfig.java +++ b/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineConfig.java @@ -3,7 +3,6 @@ import net.runelite.client.config.Config; import net.runelite.client.config.ConfigGroup; import net.runelite.client.config.ConfigItem; -import net.runelite.client.config.Range; @ConfigGroup("WineMaker") @@ -12,21 +11,10 @@ public interface WineConfig extends Config { @ConfigItem( keyName = "stopBeforeMax", name = "Stop before 99", - description = "Log out and stop the plugin just before 99 Cooking so you can craft the final wines yourself", + description = "Log out and stop once 99 Cooking is within one batch (14 wines), leaving the final batch for you to craft", position = 0 ) default boolean stopBeforeMax() { return false; } - - @Range(min = 1, max = 14) - @ConfigItem( - keyName = "winesToLeave", - name = "Wines to leave", - description = "How many wines to leave for you to craft manually (each jug of wine is 200 xp)", - position = 1 - ) - default int winesToLeave() { - return 2; - } } diff --git a/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineScript.java b/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineScript.java index b8307e2949..b6d64e7944 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineScript.java @@ -18,6 +18,7 @@ public class WineScript extends Script { private static final int MAX_XP = 13_034_431; // 99 Cooking private static final int WINE_XP = 200; // xp per jug of wine + private static final int BATCH_SIZE = 14; // wines per inventory batch private WineConfig config; @@ -35,18 +36,11 @@ public boolean run(WineConfig config) { if (!Microbot.isLoggedIn()) return; if (Rs2AntibanSettings.actionCooldownActive) return; if (Rs2AntibanSettings.microBreakActive) return; - if (config.stopBeforeMax() && winesLeftToCraft() <= 0) { - logoutAndStop("close enough to 99 Cooking - " + config.winesToLeave() + " wine(s) left for you"); + if (config.stopBeforeMax() && isMaxWithinOneBatch()) { + logoutAndStop("99 Cooking is within one batch - the final wines are yours"); return; } if (Rs2Inventory.count("grapes") > 0 && (Rs2Inventory.count("jug of water") > 0)) { - // Make-all crafts every pair in the inventory, so if the inventory - // holds more pairs than the 99 budget allows, rebalance via the bank - int pairs = Math.min(Rs2Inventory.count("grapes"), Rs2Inventory.count("jug of water")); - if (pairs > winesLeftToCraft()) { - bank(); - return; - } Rs2Inventory.combine("jug of water", "grapes"); sleepUntil(() -> Rs2Widget.getWidget(17694734) != null); Rs2Keyboard.keyPress('1'); @@ -70,17 +64,12 @@ public boolean run(WineConfig config) { } /** - * How many wines the script may still craft before it should hand over to the - * user. Unlimited when the stop-before-99 option is off. Batches are controlled - * at withdraw time (make-all crafts everything in the inventory), so this caps - * the final withdraw and triggers the stop once the budget is spent. + * True once 99 Cooking is reachable within a single 14-wine batch, i.e. the + * script should stop and leave the final batch for the user to craft. */ - private int winesLeftToCraft() { - if (!config.stopBeforeMax()) return Integer.MAX_VALUE; + private boolean isMaxWithinOneBatch() { int xp = Microbot.getClient().getSkillExperience(Skill.COOKING); - if (xp >= MAX_XP) return 0; - int winesToMax = (MAX_XP - xp + WINE_XP - 1) / WINE_XP; // ceil - wines until 99 - return Math.max(0, winesToMax - config.winesToLeave()); + return xp >= MAX_XP - BATCH_SIZE * WINE_XP; } private void bank(){ @@ -89,23 +78,19 @@ private void bank(){ Rs2Bank.depositAll(); int jugsInBank = Rs2Bank.count("jug of water"); int grapesInBank = Rs2Bank.count("grapes"); - // Withdraw up to 14 of each, capped by what is left in the bank (final - // partial batch) and by the stop-before-99 budget (final batch before 99) - int amount = Math.min(14, Math.min(jugsInBank, grapesInBank)); - amount = Math.min(amount, winesLeftToCraft()); - if (amount > 0) { + if (jugsInBank > 0 && grapesInBank > 0) { + // Withdraw up to 14 of each, or whatever is left for a final partial batch + int amount = Math.min(BATCH_SIZE, Math.min(jugsInBank, grapesInBank)); Rs2Bank.withdrawDeficit("jug of water", amount); sleepUntil(() -> Rs2Inventory.hasItem("jug of water")); Rs2Bank.withdrawDeficit("grapes", amount); sleepUntil(() -> Rs2Inventory.hasItem("grapes")); - } else if (jugsInBank <= 0 || grapesInBank <= 0) { + } else { // Out of grapes or jugs of water: log out and stop the plugin Microbot.getNotifier().notify("Run out of Materials"); logoutAndStop("out of grapes or jugs of water"); return; } - // amount == 0 with materials available means the 99 gate stops us on the - // next loop tick - just close the bank and let the loop handle it } Rs2Bank.closeBank(); sleepUntil(() -> !Rs2Bank.isOpen()); From c7e440f07dfe200af4f12ad143d6fd32289be75e Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 2 Jul 2026 12:23:12 -0700 Subject: [PATCH 11/13] feat(WineMaker): show wines made this session on the overlay --- .../microbot/natewinemaker/WineOverlay.java | 5 +++++ .../microbot/natewinemaker/WineScript.java | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineOverlay.java b/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineOverlay.java index 5b0a74fc77..93737af620 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineOverlay.java +++ b/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineOverlay.java @@ -33,6 +33,11 @@ public Dimension render(Graphics2D graphics) { .right("version: " + WinePlugin.version) .build()); + panelComponent.getChildren().add(LineComponent.builder() + .left("Wines made:") + .right(String.valueOf(WineScript.getWinesMade())) + .build()); + } catch (Exception ex) { Microbot.logStackTrace(this.getClass().getSimpleName(), ex); diff --git a/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineScript.java b/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineScript.java index b6d64e7944..9db612b8a9 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineScript.java @@ -20,10 +20,24 @@ public class WineScript extends Script { private static final int WINE_XP = 200; // xp per jug of wine private static final int BATCH_SIZE = 14; // wines per inventory batch + // Cooking xp at session start; -1 until the first logged-in tick. + // Static fields leak across plugin restarts, so run() resets it. + private static int startXp = -1; + private WineConfig config; + /** + * Wines made this session, derived from cooking xp gained (200 xp each). + * Safe to call from the overlay (client thread). + */ + public static int getWinesMade() { + if (startXp < 0 || !Microbot.isLoggedIn()) return 0; + return Math.max(0, (Microbot.getClient().getSkillExperience(Skill.COOKING) - startXp) / WINE_XP); + } + public boolean run(WineConfig config) { this.config = config; + startXp = -1; // Apply the cooking template as a baseline, then overlay the user's saved // antiban panel settings so anything toggled there wins over the template. Rs2Antiban.resetAntibanSettings(); @@ -34,6 +48,7 @@ public boolean run(WineConfig config) { try { if (!super.run()) return; if (!Microbot.isLoggedIn()) return; + if (startXp < 0) startXp = Microbot.getClient().getSkillExperience(Skill.COOKING); if (Rs2AntibanSettings.actionCooldownActive) return; if (Rs2AntibanSettings.microBreakActive) return; if (config.stopBeforeMax() && isMaxWithinOneBatch()) { From 36b0c75bca98f5042e9292967f106d1ab0294bb3 Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 2 Jul 2026 12:29:03 -0700 Subject: [PATCH 12/13] feat(WineMaker): overlay xp/wines remaining to 99 and session-rate ETA --- .../microbot/natewinemaker/WineOverlay.java | 21 ++++++++- .../microbot/natewinemaker/WineScript.java | 44 +++++++++++++++++-- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineOverlay.java b/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineOverlay.java index 93737af620..c79a20a49a 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineOverlay.java +++ b/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineOverlay.java @@ -8,6 +8,8 @@ import javax.inject.Inject; import java.awt.*; +import java.text.NumberFormat; +import java.util.Locale; public class WineOverlay extends OverlayPanel { @@ -33,9 +35,26 @@ public Dimension render(Graphics2D graphics) { .right("version: " + WinePlugin.version) .build()); + NumberFormat fmt = NumberFormat.getIntegerInstance(Locale.ENGLISH); + panelComponent.getChildren().add(LineComponent.builder() .left("Wines made:") - .right(String.valueOf(WineScript.getWinesMade())) + .right(fmt.format(WineScript.getWinesMade())) + .build()); + + panelComponent.getChildren().add(LineComponent.builder() + .left("XP to 99:") + .right(fmt.format(WineScript.getXpToMax())) + .build()); + + panelComponent.getChildren().add(LineComponent.builder() + .left("Wines to 99:") + .right(fmt.format(WineScript.getWinesToMax())) + .build()); + + panelComponent.getChildren().add(LineComponent.builder() + .left("Est. time to 99:") + .right(WineScript.getTimeToMax()) .build()); diff --git a/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineScript.java b/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineScript.java index 9db612b8a9..f8b1b2e324 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineScript.java @@ -20,9 +20,10 @@ public class WineScript extends Script { private static final int WINE_XP = 200; // xp per jug of wine private static final int BATCH_SIZE = 14; // wines per inventory batch - // Cooking xp at session start; -1 until the first logged-in tick. - // Static fields leak across plugin restarts, so run() resets it. + // Cooking xp / wall clock at session start; -1 until the first logged-in tick. + // Static fields leak across plugin restarts, so run() resets them. private static int startXp = -1; + private static long startTimeMillis = -1; private WineConfig config; @@ -35,9 +36,43 @@ public static int getWinesMade() { return Math.max(0, (Microbot.getClient().getSkillExperience(Skill.COOKING) - startXp) / WINE_XP); } + /** Cooking xp remaining to 99, or 0 when maxed. */ + public static int getXpToMax() { + if (!Microbot.isLoggedIn()) return 0; + return Math.max(0, MAX_XP - Microbot.getClient().getSkillExperience(Skill.COOKING)); + } + + /** Wines remaining to 99 (rounded up), or 0 when maxed. */ + public static int getWinesToMax() { + return (getXpToMax() + WINE_XP - 1) / WINE_XP; + } + + /** + * Estimated time to 99 based on this session's xp rate (includes breaks and + * cooldowns, which is what makes the projection honest). "-" until there is + * enough data to project from. + */ + public static String getTimeToMax() { + if (startXp < 0 || !Microbot.isLoggedIn()) return "-"; + int xpToMax = getXpToMax(); + if (xpToMax == 0) return "Maxed!"; + long elapsed = System.currentTimeMillis() - startTimeMillis; + int xpGained = Microbot.getClient().getSkillExperience(Skill.COOKING) - startXp; + if (xpGained <= 0 || elapsed < 60_000) return "-"; // need a minute of data + long msLeft = (long) (xpToMax / ((double) xpGained / elapsed)); + long totalMinutes = msLeft / 60_000; + long days = totalMinutes / (24 * 60); + long hours = (totalMinutes / 60) % 24; + long minutes = totalMinutes % 60; + if (days > 0) return days + "d " + hours + "h"; + if (hours > 0) return hours + "h " + minutes + "m"; + return Math.max(1, minutes) + "m"; + } + public boolean run(WineConfig config) { this.config = config; startXp = -1; + startTimeMillis = -1; // Apply the cooking template as a baseline, then overlay the user's saved // antiban panel settings so anything toggled there wins over the template. Rs2Antiban.resetAntibanSettings(); @@ -48,7 +83,10 @@ public boolean run(WineConfig config) { try { if (!super.run()) return; if (!Microbot.isLoggedIn()) return; - if (startXp < 0) startXp = Microbot.getClient().getSkillExperience(Skill.COOKING); + if (startXp < 0) { + startXp = Microbot.getClient().getSkillExperience(Skill.COOKING); + startTimeMillis = System.currentTimeMillis(); + } if (Rs2AntibanSettings.actionCooldownActive) return; if (Rs2AntibanSettings.microBreakActive) return; if (config.stopBeforeMax() && isMaxWithinOneBatch()) { From b899405ae32fa28cfb1b0841516e6bc95456d1b5 Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 2 Jul 2026 12:55:30 -0700 Subject: [PATCH 13/13] fix(WineMaker): track wines by mix count and project deferred fermentation xp Wine xp does not drop per wine - the fermentation timer is global and resets to ~13s on every mix, so xp for all pending wines arrives in one burst 13s after the last mix. Counting mixes keeps the overlay live, and the stop-before-99 gate now projects pending xp so it cannot overshoot while fermentation is deferred. --- .../microbot/natewinemaker/WineScript.java | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineScript.java b/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineScript.java index f8b1b2e324..ce4a270bf9 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/natewinemaker/WineScript.java @@ -25,21 +25,35 @@ public class WineScript extends Script { private static int startXp = -1; private static long startTimeMillis = -1; + // Wines mixed this session, counted at mix time. Wine xp does NOT drop per + // wine: the fermentation timer is global and resets to ~13s on every mix, so + // while crafting continuously the xp for ALL pending wines arrives in one + // burst 13s after the last mix. Counting mixes keeps the overlay live and + // lets the stop-before-99 gate project xp that hasn't dropped yet. + private static int winesMixed = 0; + private WineConfig config; /** - * Wines made this session, derived from cooking xp gained (200 xp each). - * Safe to call from the overlay (client thread). + * Cooking xp including wines that are mixed but not yet fermented (the xp for + * those is guaranteed but deferred by the global 13s fermentation timer). + * Uses max() so fermentation bursts and manual wines never regress the value. */ + private static int projectedXp() { + int current = Microbot.getClient().getSkillExperience(Skill.COOKING); + if (startXp < 0) return current; + return Math.max(current, startXp + winesMixed * WINE_XP); + } + + /** Wines mixed this session. Safe to call from the overlay (client thread). */ public static int getWinesMade() { - if (startXp < 0 || !Microbot.isLoggedIn()) return 0; - return Math.max(0, (Microbot.getClient().getSkillExperience(Skill.COOKING) - startXp) / WINE_XP); + return winesMixed; } - /** Cooking xp remaining to 99, or 0 when maxed. */ + /** Cooking xp remaining to 99 (counting pending unfermented wines), or 0 when maxed. */ public static int getXpToMax() { if (!Microbot.isLoggedIn()) return 0; - return Math.max(0, MAX_XP - Microbot.getClient().getSkillExperience(Skill.COOKING)); + return Math.max(0, MAX_XP - projectedXp()); } /** Wines remaining to 99 (rounded up), or 0 when maxed. */ @@ -48,16 +62,16 @@ public static int getWinesToMax() { } /** - * Estimated time to 99 based on this session's xp rate (includes breaks and - * cooldowns, which is what makes the projection honest). "-" until there is - * enough data to project from. + * Estimated time to 99 based on this session's mixing rate (includes breaks + * and cooldowns, which is what makes the projection honest). "-" until there + * is enough data to project from. */ public static String getTimeToMax() { if (startXp < 0 || !Microbot.isLoggedIn()) return "-"; int xpToMax = getXpToMax(); if (xpToMax == 0) return "Maxed!"; long elapsed = System.currentTimeMillis() - startTimeMillis; - int xpGained = Microbot.getClient().getSkillExperience(Skill.COOKING) - startXp; + int xpGained = projectedXp() - startXp; if (xpGained <= 0 || elapsed < 60_000) return "-"; // need a minute of data long msLeft = (long) (xpToMax / ((double) xpGained / elapsed)); long totalMinutes = msLeft / 60_000; @@ -73,6 +87,7 @@ public boolean run(WineConfig config) { this.config = config; startXp = -1; startTimeMillis = -1; + winesMixed = 0; // Apply the cooking template as a baseline, then overlay the user's saved // antiban panel settings so anything toggled there wins over the template. Rs2Antiban.resetAntibanSettings(); @@ -94,6 +109,7 @@ public boolean run(WineConfig config) { return; } if (Rs2Inventory.count("grapes") > 0 && (Rs2Inventory.count("jug of water") > 0)) { + int jugsBefore = Rs2Inventory.count("jug of water"); Rs2Inventory.combine("jug of water", "grapes"); sleepUntil(() -> Rs2Widget.getWidget(17694734) != null); Rs2Keyboard.keyPress('1'); @@ -106,6 +122,7 @@ public boolean run(WineConfig config) { Rs2Antiban.takeMicroBreakByChance(); } sleepUntil(() -> !Rs2Inventory.hasItem("jug of water"),25000); + winesMixed += Math.max(0, jugsBefore - Rs2Inventory.count("jug of water")); } else { bank(); } @@ -119,10 +136,11 @@ public boolean run(WineConfig config) { /** * True once 99 Cooking is reachable within a single 14-wine batch, i.e. the * script should stop and leave the final batch for the user to craft. + * Uses projected xp: raw xp can lag hundreds of wines behind because the + * fermentation timer resets on every mix, and gating on it would overshoot. */ private boolean isMaxWithinOneBatch() { - int xp = Microbot.getClient().getSkillExperience(Skill.COOKING); - return xp >= MAX_XP - BATCH_SIZE * WINE_XP; + return projectedXp() >= MAX_XP - BATCH_SIZE * WINE_XP; } private void bank(){