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..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,29 +2,38 @@ 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; import net.runelite.client.plugins.microbot.Script; -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.agentserver.handler.ScriptHeartbeatRegistry; +import net.runelite.client.plugins.microbot.agility.courses.AgilityCourseHandler; +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; 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.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; import java.util.List; import java.util.Optional; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -37,33 +46,85 @@ 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 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; @Inject public AgilityScript(MicroAgilityPlugin plugin, MicroAgilityConfig config) { this.plugin = plugin; this.config = config; + this.supplyManager = new AgilitySupplyManager(plugin, config, this::isShuttingDown, this::shutdown); } @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; + currentObstacleIndex = -1; + supplyManager.reset(); + clearPendingMarkOfGrace(); + clearAlchDecision(); + + if (mainScheduledFuture != null && !mainScheduledFuture.isDone()) + { + mainScheduledFuture.cancel(true); + } + if (scheduledFuture != null && !scheduledFuture.isDone()) + { + scheduledFuture.cancel(true); + } + + clearWalkingRouteForShutdown(); + 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,75 +137,80 @@ public boolean run() { return; } - - // Debug log to see if main loop is running - Microbot.log("AgilityScript main loop running - Course: " + config.agilityCourse().getTooltip()); - if (!plugin.hasRequiredLevel()) + AgilityCourseHandler courseHandler = getActiveHandler(); + final WorldPoint playerWorldLocation = Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation()); + + if (startPoint == null) { - Microbot.log("Early return: Required level not met"); - Microbot.showMessage("You do not have the required level for this course."); - shutdown(); + Microbot.log("Early return: Start point is null"); + Microbot.showMessage("Agility course: " + config.agilityCourse().getTooltip() + " is not supported."); + sleep(10000); return; } - - // Check coin requirement for BrimhavenSpike course (only before payment) - if (plugin.getCourseHandler() instanceof BrimhavenSpikeCourse) + + boolean hasRequiredLevel = plugin.hasRequiredLevel(courseHandler); + currentObstacleIndex = courseHandler.getCurrentObstacleIndex(); + AgilitySupplyManager.InventorySnapshot inventorySnapshot = supplyManager.createInventorySnapshot(); + if (supplyManager.handleSummerPies(courseHandler, playerWorldLocation, currentObstacleIndex, inventorySnapshot)) { - BrimhavenSpikeCourse course = (BrimhavenSpikeCourse) plugin.getCourseHandler(); - 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: Handling summer pies"); + return; } - if (Rs2AntibanSettings.actionCooldownActive) + + if (supplyManager.handleFoodOrHealthSafety(inventorySnapshot)) { - Microbot.log("Early return: Action cooldown active"); + Microbot.log("Early return: Handling agility safety"); return; } - if (startPoint == null) + + if (supplyManager.handlePreLevelCheck(courseHandler, playerWorldLocation, currentObstacleIndex, hasRequiredLevel, inventorySnapshot)) { - Microbot.log("Early return: Start point is null"); - Microbot.showMessage("Agility course: " + config.agilityCourse().getTooltip() + " is not supported."); - sleep(10000); + Microbot.log("Early return: Banking agility supplies"); return; } - final WorldPoint playerWorldLocation = Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation()); - final int currentAgilityXp = Microbot.getClient().getSkillExperience(Skill.AGILITY); + if (supplyManager.handleBeforeObstacle(courseHandler, playerWorldLocation, currentObstacleIndex, inventorySnapshot)) + { + Microbot.log("Early return: Handling agility supplies"); + return; + } - if (handleFood()) + if (!hasRequiredLevel) { - Microbot.log("Early return: Handling food"); + Microbot.log("Early return: Required level not met"); + Microbot.showMessage("You do not have the required level for this course."); + Rs2Player.logout(); + shutdown(); return; } - if (handleSummerPies()) + + if (!courseHandler.hasRequiredCourseItems()) { - Microbot.log("Early return: Handling summer pies"); + Microbot.log("Early return: Missing required course items"); + Microbot.showMessage(courseHandler.getMissingRequiredCourseItemsMessage()); + shutdown(); + return; + } + if (Rs2AntibanSettings.actionCooldownActive) + { + Microbot.log("Early return: Action cooldown active"); return; } + final int currentAgilityXp = Microbot.getClient().getSkillExperience(Skill.AGILITY); - if (lootMarksOfGrace()) + if (lootMarksOfGrace(courseHandler)) { Microbot.log("Early return: Looting marks of grace"); return; } - if (handleCourseSpecificActions(playerWorldLocation)) + if (courseHandler.handleCourseActions(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 = currentAgilityXp; - final int agilityExp = Microbot.getClient().getSkillExperience(Skill.AGILITY); - - TileObject gameObject = plugin.getCourseHandler().getCurrentObstacle(); + TileObject gameObject = courseHandler.getCurrentObstacle(); if (gameObject == null) { @@ -158,7 +224,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 } @@ -170,7 +236,7 @@ public boolean run() } // Handle alchemy if enabled - if (shouldPerformAlch()) + if (shouldPerformAlch(gameObject)) { Optional alchItem = getAlchItem(); if (alchItem.isPresent()) @@ -191,7 +257,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 @@ -207,15 +276,18 @@ public boolean run() } } // Fall back to normal alching - performNormalAlch(alchItem.get()); + if (performNormalAlch(alchItem.get())) + { + return; + } } } } // Normal obstacle interaction - if (Rs2GameObject.interact(gameObject)) { + if (interactWithObstacle(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) { @@ -230,6 +302,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()) { @@ -241,12 +314,55 @@ public boolean run() } catch (Exception ex) { + if (isExpectedShutdownInterrupt(ex)) + { + return; + } Microbot.log("An error occurred: " + ex.getMessage(), ex); } - }, 0, 100, TimeUnit.MILLISECONDS); + }, 0, MAIN_LOOP_DELAY_MS, TimeUnit.MILLISECONDS); return true; } + public boolean isShuttingDown() + { + return shuttingDown; + } + + public int getCurrentObstacleIndex() + { + return currentObstacleIndex; + } + + 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,100 +395,252 @@ private Optional getAlchItem() return Optional.empty(); } - private boolean lootMarksOfGrace() + private AgilityCourseHandler getActiveHandler() { - final int lootDistance = plugin.getCourseHandler().getLootDistance(); + 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); + currentObstacleIndex = -1; + supplyManager.reset(); + clearAlchDecision(); + } + return activeHandler; + } + + 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; } + WorldPoint playerLocation = courseHandler.getPlayerWorldLocation(); + if (playerLocation == null) + { + 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) .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 -> Rs2Walker.canReach(item.getWorldLocation())) + .toList() + .stream() + .min(Comparator.comparingInt(item -> item.getWorldLocation().distanceTo(playerLocation))) + .orElse(null); if (markOfGrace == null) { return false; } - if (!markOfGrace.pickup()) + if (Rs2Player.isMoving() || Rs2Player.isAnimating()) { - return false; + return true; } - Rs2Player.waitForWalking(); - return true; - } - - private boolean handleFood() - { - if (Rs2Player.getHealthPercentage() > config.hitpoints()) + WorldPoint markLocation = markOfGrace.getWorldLocation(); + var markLocalLocation = markOfGrace.getLocalLocation(); + if (markLocation == null || markLocalLocation == null) { return false; } - List foodItems = plugin.getInventoryFood(); - if (foodItems.isEmpty()) + if (!Rs2Camera.isTileOnScreen(markLocalLocation)) { - return false; + Rs2Camera.turnTo(markLocalLocation); + sleep(300, 600); + return true; } - Rs2ItemModel foodItem = foodItems.get(0); - Rs2Inventory.interact(foodItem, foodItem.getName().toLowerCase().contains("jug of wine") ? "drink" : "eat"); - Rs2Inventory.waitForInventoryChanges(1800); + int markCount = Rs2Inventory.itemQuantity(ItemID.GRACE); + if (!pickupMarkOfGrace(markOfGrace)) + { + return false; + } + pendingMarkOfGraceLocation = markLocation; + pendingMarkOfGraceCount = markCount; + pendingMarkOfGraceStartedAt = System.currentTimeMillis(); - if (Rs2Inventory.contains(ItemID.JUG_EMPTY)) + sleepUntil(() -> shuttingDown || markPickupResolved() || Rs2Player.isMoving(), 1800); + if (markPickupResolved() || shuttingDown) { - Rs2Inventory.dropAll(ItemID.JUG_EMPTY); + clearPendingMarkOfGrace(); } return true; } - private boolean handleSummerPies() + private boolean markPickupResolved() + { + return pendingMarkOfGraceLocation != null + && (Rs2Inventory.itemQuantity(ItemID.GRACE) > pendingMarkOfGraceCount + || !hasLootableMarkAt(pendingMarkOfGraceLocation)); + } + + private void clearPendingMarkOfGrace() + { + pendingMarkOfGraceLocation = null; + pendingMarkOfGraceCount = 0; + pendingMarkOfGraceStartedAt = 0; + lastMarkOfGraceScanAt = 0; + } + + private boolean hasLootableMarkAt(WorldPoint markLocation) { - if (plugin.getCourseHandler().getCurrentObstacleIndex() > 0) + return Microbot.getRs2TileItemCache().query() + .fromWorldView() + .withId(ItemID.GRACE) + .where(Rs2TileItemModel::isLootAble) + .where(item -> markLocation.equals(item.getWorldLocation())) + .first() != null; + } + + private boolean pickupMarkOfGrace(Rs2TileItemModel markOfGrace) + { + try { - return false; + 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; } - if (Rs2Player.getBoostedSkillLevel(Skill.AGILITY) > plugin.getCourseHandler().getRequiredLevel()) + catch (Exception ex) { + Microbot.log("Failed to pick up Mark of grace: " + ex.getMessage()); return false; } + } - List summerPies = plugin.getSummerPies(); - if (summerPies.isEmpty()) + private MenuAction getGroundItemMenuAction(ItemComposition item, String action) + { + String[] groundActions = Rs2Reflection.getGroundItemActions(item); + for (int i = 0; i < groundActions.length; i++) { - return false; + String groundAction = groundActions[i]; + if (groundAction != null && groundAction.equalsIgnoreCase(action)) + { + return groundItemMenuAction(i); + } } - Rs2ItemModel summerPie = summerPies.get(0); + return null; + } - Rs2Inventory.interact(summerPie, "eat"); - Rs2Inventory.waitForInventoryChanges(1800); - if (Rs2Inventory.contains(ItemID.PIEDISH)) + private MenuAction groundItemMenuAction(int index) + { + switch (index) { - Rs2Inventory.dropAll(ItemID.PIEDISH); + 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; } - return true; } - 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) @@ -382,12 +650,12 @@ 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); - boolean completed = plugin.getCourseHandler().waitForCompletion(agilityExp, + interactWithObstacle(gameObject); + boolean completed = getActiveHandler().waitForCompletion(agilityExp, Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation()).getPlane()); if (!completed) { @@ -403,47 +671,31 @@ 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(WorldPoint playerWorldLocation) + private void clearAlchDecision() { - Microbot.log("handleCourseSpecificActions called for: " + plugin.getCourseHandler().getClass().getSimpleName()); - - if (plugin.getCourseHandler() instanceof PrifddinasCourse) - { - PrifddinasCourse course = (PrifddinasCourse) plugin.getCourseHandler(); - return course.handlePortal() || course.handleWalkToStart(playerWorldLocation); - } - else if (plugin.getCourseHandler() instanceof WerewolfCourse) - { - WerewolfCourse course = (WerewolfCourse) plugin.getCourseHandler(); - return course.handleFirstSteppingStone(playerWorldLocation) - || course.handleStickPickup(playerWorldLocation) - || course.handleSlide() - || course.handleStickReturn(playerWorldLocation); - } - else if (plugin.getCourseHandler() instanceof BrimhavenSpikeCourse) - { - Microbot.log("BrimhavenSpikeCourse detected, calling handleWalkToStart"); - BrimhavenSpikeCourse course = (BrimhavenSpikeCourse) plugin.getCourseHandler(); - boolean result = course.handleWalkToStart(playerWorldLocation); - Microbot.log("BrimhavenSpikeCourse handleWalkToStart returned: " + result); - return result; - } - else if (!(plugin.getCourseHandler() instanceof GnomeStrongholdCourse)) - { - return plugin.getCourseHandler().handleWalkToStart(playerWorldLocation); - } - return false; + alchDecisionObstacleLocation = null; + alchDecisionObstacleId = -1; + alchDecisionShouldAlch = false; + } + + private boolean interactWithObstacle(TileObject gameObject) + { + return Rs2GameObject.interact(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 new file mode 100644 index 0000000000..f1dfb06326 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/agility/AgilitySupplyManager.java @@ -0,0 +1,704 @@ +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.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; +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 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, + 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 boolean summerPieBankUnavailable = false; + + 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; + summerPieBankUnavailable = false; + } + + 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 (!isAtCourseStart(courseHandler, playerWorldLocation, currentObstacleIndex)) + { + return false; + } + if (Rs2Player.getBoostedSkillLevel(Skill.AGILITY) >= courseHandler.getRequiredLevel()) + { + return false; + } + + List summerPies = inventorySnapshot.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, int currentObstacleIndex, + boolean hasRequiredLevel, InventorySnapshot inventorySnapshot) + { + 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; + } + + summerPieBankUnavailable = false; + if (!handleBanking(courseHandler, playerWorldLocation, currentObstacleIndex, inventorySnapshot, true)) + { + if (!hasRequiredLevel) + { + stopAndLogout("You do not have enough summer pies configured to start this course."); + return true; + } + return false; + } + 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(InventorySnapshot inventorySnapshot) + { + return handleFood(inventorySnapshot) || handleHealthSafety(inventorySnapshot); + } + + public boolean handleBeforeObstacle(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation, int currentObstacleIndex, + InventorySnapshot inventorySnapshot) + { + if (wearConfiguredEquipmentFromInventory()) + { + return true; + } + return handleBanking(courseHandler, playerWorldLocation, currentObstacleIndex, inventorySnapshot) + || handleHealthSafety(inventorySnapshot); + } + + private boolean handleFood(InventorySnapshot inventorySnapshot) + { + if (config.hitpoints() <= 0) + { + return false; + } + + if (Rs2Player.getHealthPercentage() > config.hitpoints()) + { + return false; + } + + List foodItems = inventorySnapshot.getFoodItems(); + if (foodItems.isEmpty()) + { + if (needsFoodBanking(inventorySnapshot) || 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(InventorySnapshot inventorySnapshot) + { + if (!shouldWaitForHealthInsteadOfBanking(inventorySnapshot)) + { + 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, int currentObstacleIndex, + InventorySnapshot inventorySnapshot) + { + return handleBanking(courseHandler, playerWorldLocation, currentObstacleIndex, inventorySnapshot, false); + } + + private boolean handleBanking(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation, int currentObstacleIndex, + InventorySnapshot inventorySnapshot, boolean blockUntilBankable) + { + if (!shouldBankForSupplies(courseHandler, inventorySnapshot) && !needsEquipmentBanking()) + { + return false; + } + if (shouldWaitForHealthInsteadOfBanking(inventorySnapshot)) + { + return false; + } + if (Rs2Player.isMoving() || Rs2Player.isAnimating()) + { + return true; + } + if (!canAttemptBanking(courseHandler, playerWorldLocation, currentObstacleIndex)) + { + Microbot.status = "Finishing course before banking"; + return blockUntilBankable || shouldPauseForUnsafeSupplyBanking(inventorySnapshot); + } + + 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; + InventorySnapshot inventorySnapshot = createInventorySnapshot(); + if (needsFoodBanking(inventorySnapshot)) + { + success &= withdrawConfiguredFood(); + } + if (needsPotionBanking(inventorySnapshot)) + { + success &= withdrawConfiguredPotion(); + } + if (needsSummerPieBanking(courseHandler, inventorySnapshot)) + { + success &= withdrawSummerPies(); + } + if (needsEquipmentBanking()) + { + success &= withdrawAndWearConfiguredEquipment(); + } + 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) + { + summerPieBankUnavailable = false; + 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)) + { + summerPieBankUnavailable = true; + Microbot.showMessage("No summer pies found in the bank."); + return false; + } + if (!Rs2Bank.withdrawX(SUMMER_PIE_ID, amountToWithdraw)) + { + summerPieBankUnavailable = true; + return false; + } + boolean receivedPies = Global.sleepUntil(() -> plugin.getSummerPies().size() >= currentPies + amountToWithdraw, 2400); + summerPieBankUnavailable = !receivedPies; + return receivedPies; + } + + private boolean shouldBankForSupplies(AgilityCourseHandler courseHandler, InventorySnapshot inventorySnapshot) + { + 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 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() + && inventorySnapshot.getFoodCount() <= config.bankFoodAt() + && inventorySnapshot.getFoodCount() < config.foodWithdrawAmount(); + } + + private boolean isFoodBankingConfigured() + { + AgilityFoodOption food = config.bankFood(); + return !food.isNone() + && config.foodWithdrawAmount() > 0 + && config.bankFoodAt() > 0; + } + + private boolean needsPotionBanking(InventorySnapshot inventorySnapshot) + { + AgilityPotionOption potion = config.bankPotion(); + return !potion.isNone() + && config.potionWithdrawAmount() > 0 + && inventorySnapshot.getStaminaPotionCount() < config.potionWithdrawAmount(); + } + + private boolean needsSummerPieBanking(AgilityCourseHandler courseHandler, InventorySnapshot inventorySnapshot) + { + return courseHandler != null + && courseHandler.canBeBoosted() + && config.summerPieWithdrawAmount() > 0 + && inventorySnapshot.getSummerPieCount() < config.summerPieWithdrawAmount(); + } + + private int getStaminaPotionCount() + { + return Rs2Inventory.count(item -> item != null && Arrays.stream(STAMINA_POTION_IDS).anyMatch(id -> id == item.getId())); + } + + private boolean shouldWaitForHealthInsteadOfBanking(InventorySnapshot inventorySnapshot) + { + return config.hpSafetyWait() + && inventorySnapshot.getFoodItems().isEmpty() + && Rs2Player.getHealthPercentage() < config.hpSafetyWaitBelow() + && (!isFoodBankingConfigured() || isFoodBankUnavailable()); + } + + private boolean shouldPauseForUnsafeSupplyBanking(InventorySnapshot inventorySnapshot) + { + return config.hpSafetyWait() + && isFoodBankingConfigured() + && inventorySnapshot.getFoodItems().isEmpty() + && Rs2Player.getHealthPercentage() < config.hpSafetyWaitBelow(); + } + + private boolean canAttemptBanking(AgilityCourseHandler courseHandler, WorldPoint playerWorldLocation, int currentObstacleIndex) + { + if (!config.bankOnlyAtCourseStart()) + { + return true; + } + if (courseHandler == null || playerWorldLocation == null || courseHandler.getStartPoint() == null) + { + return false; + } + return courseHandler.getClientPlane() == 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; + } + + 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 stopAndLogout(String message) + { + 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 staminaPotionCount; + } + } +} 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..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,9 @@ 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; @ConfigGroup("MicroAgility") @ConfigInformation("Enable the plugin near the start of your selected agility course.
" + @@ -26,6 +29,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 +69,144 @@ 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 = "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 = 8, + 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 +267,4 @@ default int alchSkipChance() { return 5; } -} \ No newline at end of file +} 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..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 @@ -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)); @@ -45,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 60ffdb09cb..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.2.7"; + public static final String version = "1.3.0"; @Inject private MicroAgilityConfig config; @Inject @@ -65,10 +66,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() @@ -78,22 +83,43 @@ 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() { - if (getSummerPies().isEmpty() || !getCourseHandler().canBeBoosted()) + return hasRequiredLevel(getCourseHandler()); + } + + public boolean hasRequiredLevel(AgilityCourseHandler courseHandler) + { + int requiredLevel = courseHandler.getRequiredLevel(); + if (Rs2Player.getRealSkillLevel(Skill.AGILITY) >= requiredLevel) + { + return true; + } + + if (getSummerPies().isEmpty() || !courseHandler.canBeBoosted()) { - return Rs2Player.getRealSkillLevel(Skill.AGILITY) >= getCourseHandler().getRequiredLevel(); + return false; } - return Rs2Player.getBoostedSkillLevel(Skill.AGILITY) >= getCourseHandler().getRequiredLevel(); + 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() { 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..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 @@ -27,14 +27,48 @@ 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; } + 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 = 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())) @@ -75,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 @@ -118,8 +154,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 +170,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) { @@ -183,12 +219,12 @@ default int getCurrentObstacleIndex() } } - return (closestIndex != -1) ? closestIndex : 0; + return closestIndex; } 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..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); @@ -26,13 +28,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 @@ -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; } @@ -214,22 +230,22 @@ 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); + 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) - double initialHealth = Rs2Player.getHealthPercentage(); 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/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/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 79c6e72c57..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 @@ -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) @@ -132,15 +138,21 @@ public TileObject getCurrentObstacle() return AgilityCourseHandler.super.getCurrentObstacle(); } + @Override + public boolean handleCourseActions(WorldPoint playerWorldLocation) + { + return handlePortal() || handleWalkToStart(playerWorldLocation); + } + @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 +160,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..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 @@ -60,6 +62,12 @@ public List getObstacles() { new AgilityObstacleModel(10855) // Doorway ); } + + @Override + public void reset() { + state.reset(); + lastStartWalkFailureAt = 0; + } @Override public TileObject getCurrentObstacle() { @@ -838,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) { @@ -878,9 +908,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 +1231,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..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 @@ -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())) @@ -129,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; + } +} 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; + } +} 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); + } + } +} 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..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 @@ -2,9 +2,19 @@ import net.runelite.client.config.Config; import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; -@ConfigGroup("PieMaking") +@ConfigGroup("WineMaker") public interface WineConfig extends Config { + @ConfigItem( + keyName = "stopBeforeMax", + name = "Stop before 99", + 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; + } } 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..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,6 +35,28 @@ 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(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()); + } catch (Exception ex) { Microbot.logStackTrace(this.getClass().getSimpleName(), ex); 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..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 @@ -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,78 @@ 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 + + // 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; + + // 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; + + /** + * 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() { + return winesMixed; + } + + /** 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 - projectedXp()); + } + + /** 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 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 = 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; + 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; + 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(); @@ -26,9 +98,18 @@ public boolean run(WineConfig config) { try { if (!super.run()) return; if (!Microbot.isLoggedIn()) return; + if (startXp < 0) { + startXp = Microbot.getClient().getSkillExperience(Skill.COOKING); + startTimeMillis = System.currentTimeMillis(); + } if (Rs2AntibanSettings.actionCooldownActive) return; if (Rs2AntibanSettings.microBreakActive) return; + 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)) { + int jugsBefore = Rs2Inventory.count("jug of water"); Rs2Inventory.combine("jug of water", "grapes"); sleepUntil(() -> Rs2Widget.getWidget(17694734) != null); Rs2Keyboard.keyPress('1'); @@ -41,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(); } @@ -51,15 +133,25 @@ public boolean run(WineConfig config) { return true; } + /** + * 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() { + return projectedXp() >= MAX_XP - BATCH_SIZE * WINE_XP; + } + 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) { + 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)); + 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); @@ -67,17 +159,7 @@ private void bank(){ } else { // 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; } } @@ -85,6 +167,23 @@ private void bank(){ 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();