From 056e22437f1b977b850019000d3e85af415a588f Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 16 Apr 2026 13:27:37 -0700 Subject: [PATCH] feat(LeaguesToolkit): add Toci's Gem Cutter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new Gem Cutter section to Leagues Toolkit. Walks to Toci in Aldarin, opens the shop, mass-clicks "Buy 1" on uncut gems until inventory is full or shop runs out, closes the shop, uses chisel on uncut gems with SPACE for the "how many" dialog, waits for cutting to finish, then reopens the shop and rapid-sells each cut gem from the bottom inventory slot. Loops indefinitely. Config: - Enable gem cutter (toggle) - Gem dropdown: Sapphire, Emerald, Ruby - Min coins to keep (auto-banks when below) - Use Bank Heist briefcase (Leagues relic instant banking) Shop stays open across sell→buy transitions to save time. Bump version to 1.1.0. --- .../microbot/leaguestoolkit/GemCutter.java | 262 ++++++++++++++++++ .../leaguestoolkit/GemCutterState.java | 9 + .../microbot/leaguestoolkit/GemType.java | 27 ++ .../leaguestoolkit/LeaguesToolkitConfig.java | 53 ++++ .../leaguestoolkit/LeaguesToolkitPlugin.java | 2 +- .../leaguestoolkit/LeaguesToolkitScript.java | 16 ++ 6 files changed, 368 insertions(+), 1 deletion(-) create mode 100644 src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemCutter.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemCutterState.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemType.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemCutter.java b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemCutter.java new file mode 100644 index 0000000000..7e4aba37a7 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemCutter.java @@ -0,0 +1,262 @@ +package net.runelite.client.plugins.microbot.leaguestoolkit; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; +import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; +import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; +import net.runelite.client.plugins.microbot.util.keyboard.Rs2Keyboard; +import net.runelite.client.plugins.microbot.util.math.Rs2Random; +import net.runelite.client.plugins.microbot.util.npc.Rs2Npc; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.shop.Rs2Shop; +import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; + +import java.awt.event.KeyEvent; + +import static net.runelite.client.plugins.microbot.util.Global.sleep; +import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; + +@Slf4j +public class GemCutter { + + // Toci's actual tile in Aldarin, Varlamore + private static final WorldPoint TOCI_LOCATION = new WorldPoint(1428, 2975, 0); + private static final String TOCI_NPC_NAME = "Toci"; + private static final String CHISEL_NAME = "Chisel"; + private static final int COINS_ID = 995; + private static final String BRIEFCASE_NAME = "Banker's briefcase"; + + @Getter + private GemCutterState state = GemCutterState.WALKING_TO_SHOP; + @Getter + private String status = "Idle"; + + public void reset() { + state = GemCutterState.WALKING_TO_SHOP; + status = "Idle"; + } + + public boolean tick(LeaguesToolkitConfig config) { + GemType gem = config.gemType(); + if (gem == null) { + status = "No gem selected"; + return false; + } + + if (!gem.hasRequiredLevel()) { + status = "Crafting level too low for " + gem.getCutName() + " (need " + gem.getCraftingLevel() + ")"; + return false; + } + + if (!Rs2Inventory.hasItem(CHISEL_NAME)) { + status = "No chisel in inventory"; + return false; + } + + // Need to bank for coins if we're idle (no gems in hand) and low on coins + if (state == GemCutterState.WALKING_TO_SHOP + && Rs2Inventory.itemQuantity(COINS_ID) < config.gemCutterMinCoins() + && !Rs2Inventory.hasItem(gem.getUncutName()) + && !Rs2Inventory.hasItem(gem.getCutName())) { + state = GemCutterState.BANKING; + } + + switch (state) { + case BANKING: + return handleBanking(config); + case WALKING_TO_SHOP: + return handleWalkingToShop(); + case BUYING: + return handleBuying(gem); + case CUTTING: + return handleCutting(gem); + case SELLING: + return handleSelling(gem, config); + } + return true; + } + + private boolean handleBanking(LeaguesToolkitConfig config) { + if (Rs2Shop.isOpen()) { + Rs2Shop.closeShop(); + return true; + } + + if (config.gemCutterUseBriefcase() && Rs2Inventory.hasItem(BRIEFCASE_NAME)) { + if (!Rs2Bank.isOpen()) { + status = "Using briefcase to bank"; + Rs2Inventory.interact(BRIEFCASE_NAME, "Bank"); + sleepUntil(Rs2Bank::isOpen, 5000); + return true; + } + } else { + if (!Rs2Bank.isOpen()) { + status = "Walking to bank"; + if (!Rs2Bank.walkToBankAndUseBank()) return true; + } + } + + if (!Rs2Bank.isOpen()) return true; + + status = "Withdrawing coins"; + if (!Rs2Bank.hasItem(COINS_ID)) { + status = "No coins in bank — stopping"; + Rs2Bank.closeBank(); + return false; + } + + Rs2Bank.withdrawAll(COINS_ID); + Rs2Bank.closeBank(); + state = GemCutterState.WALKING_TO_SHOP; + return true; + } + + private boolean handleWalkingToShop() { + if (Rs2Npc.getNpc(TOCI_NPC_NAME) != null) { + status = "At Toci"; + state = GemCutterState.BUYING; + return true; + } + status = "Walking to Toci"; + if (!Rs2Player.isMoving()) { + Rs2Walker.walkTo(TOCI_LOCATION, 6); + } + return true; + } + + private boolean handleBuying(GemType gem) { + if (!Rs2Shop.isOpen()) { + status = "Opening Toci's shop"; + if (!Rs2Shop.openShop(TOCI_NPC_NAME)) { + status = "Could not open shop"; + return true; + } + sleepUntil(Rs2Shop::isOpen, 3000); + return true; + } + + int uncutCount = Rs2Inventory.count(gem.getUncutName()); + + if (Rs2Inventory.isFull()) { + status = "Inventory full — moving to cut"; + Rs2Shop.closeShop(); + state = GemCutterState.CUTTING; + return true; + } + + if (!Rs2Shop.hasStock(gem.getUncutName())) { + if (uncutCount > 0) { + status = "Shop out of stock — cutting what we have"; + Rs2Shop.closeShop(); + state = GemCutterState.CUTTING; + return true; + } + status = "Shop out of " + gem.getUncutName() + " — waiting"; + sleep(1500, 2500); + return true; + } + + // Mass-click buy at 100-250ms intervals. Stop when 2 consecutive clicks + // fail to add a ruby to inventory (inventory full OR shop out of stock). + status = "Rapid-buying " + gem.getUncutName(); + int safetyMax = 32; + int missedInRow = 0; + for (int i = 0; i < safetyMax; i++) { + if (!Rs2Shop.isOpen()) break; + int before = Rs2Inventory.count(gem.getUncutName()); + Rs2Shop.buyItem(gem.getUncutName(), "1"); + sleep(Rs2Random.between(100, 250)); + if (Rs2Inventory.count(gem.getUncutName()) > before) { + missedInRow = 0; + } else { + missedInRow++; + if (missedInRow >= 2) break; + } + } + return true; + } + + private boolean handleCutting(GemType gem) { + if (!Rs2Inventory.hasItem(gem.getUncutName())) { + status = "All gems cut — moving to sell"; + state = GemCutterState.SELLING; + return true; + } + + // Start the cut: chisel on uncut gem + status = "Starting to cut " + gem.getCutName(); + Rs2Inventory.use(CHISEL_NAME); + sleep(300, 500); + Rs2Inventory.use(gem.getUncutName()); + + // Wait for "How many do you wish to make?" dialog, then press space for All + sleep(600, 900); + Rs2Keyboard.keyPress(KeyEvent.VK_SPACE); + + // Wait for cutting to finish (XP stops flowing OR all uncut gems gone) + sleep(2000, 3000); + status = "Cutting " + gem.getCutName() + "..."; + sleepUntil(() -> !Microbot.isGainingExp || !Rs2Inventory.hasItem(gem.getUncutName()), 60000); + + return true; + } + + private boolean handleSelling(GemType gem, LeaguesToolkitConfig config) { + if (!Rs2Inventory.hasItem(gem.getCutName())) { + status = "All cut gems sold — looping"; + if (Rs2Inventory.itemQuantity(COINS_ID) < config.gemCutterMinCoins()) { + // Only close when we need to walk somewhere (banking) + if (Rs2Shop.isOpen()) Rs2Shop.closeShop(); + state = GemCutterState.BANKING; + } else { + // Leave shop open — handleBuying will use the already-open shop next tick + state = GemCutterState.BUYING; + } + return true; + } + + if (!Rs2Shop.isOpen()) { + status = "Reopening shop to sell"; + if (!Rs2Shop.openShop(TOCI_NPC_NAME)) { + status = "Could not reopen shop"; + return true; + } + sleepUntil(Rs2Shop::isOpen, 3000); + return true; + } + + // Mass-click sell at 100-250ms intervals — click the LAST slot containing + // a cut gem, repeat until inventory runs out. Two consecutive misses = stop. + int initialCount = Rs2Inventory.count(gem.getCutName()); + status = "Rapid-selling " + gem.getCutName() + " x" + initialCount; + + int safetyMax = initialCount + 5; + int missedInRow = 0; + for (int i = 0; i < safetyMax; i++) { + if (!Rs2Shop.isOpen()) break; + if (!Rs2Inventory.hasItem(gem.getCutName())) break; + + Rs2ItemModel last = Rs2Inventory.items(item -> + gem.getCutName().equalsIgnoreCase(item.getName())) + .reduce((a, b) -> b) + .orElse(null); + if (last == null) break; + int before = Rs2Inventory.count(gem.getCutName()); + + Rs2Inventory.slotInteract(last.getSlot(), "Sell 1"); + sleep(Rs2Random.between(100, 250)); + + if (Rs2Inventory.count(gem.getCutName()) < before) { + missedInRow = 0; + } else { + missedInRow++; + if (missedInRow >= 2) break; + } + } + + return true; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemCutterState.java b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemCutterState.java new file mode 100644 index 0000000000..d08593c9f6 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemCutterState.java @@ -0,0 +1,9 @@ +package net.runelite.client.plugins.microbot.leaguestoolkit; + +public enum GemCutterState { + BANKING, + WALKING_TO_SHOP, + BUYING, + CUTTING, + SELLING +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemType.java b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemType.java new file mode 100644 index 0000000000..9e29e52f1e --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemType.java @@ -0,0 +1,27 @@ +package net.runelite.client.plugins.microbot.leaguestoolkit; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.Skill; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; + +@Getter +@RequiredArgsConstructor +public enum GemType { + SAPPHIRE("Uncut sapphire", "Sapphire", 20), + EMERALD("Uncut emerald", "Emerald", 27), + RUBY("Uncut ruby", "Ruby", 34); + + private final String uncutName; + private final String cutName; + private final int craftingLevel; + + public boolean hasRequiredLevel() { + return Rs2Player.getSkillRequirement(Skill.CRAFTING, craftingLevel); + } + + @Override + public String toString() { + return cutName; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitConfig.java b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitConfig.java index 0e6dbc4fb3..68b324196d 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitConfig.java +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitConfig.java @@ -66,4 +66,57 @@ default int antiAfkBufferMin() { default int antiAfkBufferMax() { return 1500; } + + @ConfigSection( + name = "Toci's Gem Cutter", + description = "Buys uncut gems from Toci in Aldarin, cuts them, sells them back", + position = 1, + closedByDefault = true + ) + String gemCutterSection = "gemCutterSection"; + + @ConfigItem( + keyName = "enableGemCutter", + name = "Enable gem cutter", + description = "Walks to Toci, buys uncut gems, cuts them, sells cut gems back — repeats", + position = 0, + section = gemCutterSection + ) + default boolean enableGemCutter() { + return false; + } + + @ConfigItem( + keyName = "gemType", + name = "Gem", + description = "Which gem to cut (requires chisel + coins + crafting level)", + position = 1, + section = gemCutterSection + ) + default GemType gemType() { + return GemType.RUBY; + } + + @Range(min = 1000, max = 1_000_000) + @ConfigItem( + keyName = "gemCutterMinCoins", + name = "Min coins to keep", + description = "When coins drop below this, withdraw more from the bank", + position = 2, + section = gemCutterSection + ) + default int gemCutterMinCoins() { + return 10_000; + } + + @ConfigItem( + keyName = "gemCutterUseBriefcase", + name = "Use Bank Heist briefcase", + description = "Use the banker's briefcase to bank (Leagues relic) instead of walking to a bank", + position = 3, + section = gemCutterSection + ) + default boolean gemCutterUseBriefcase() { + return false; + } } diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitPlugin.java index bbec460109..e766bbf2fd 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitPlugin.java @@ -20,7 +20,7 @@ ) @Slf4j public class LeaguesToolkitPlugin extends Plugin { - public static final String version = "1.0.0"; + public static final String version = "1.1.0"; @Inject private LeaguesToolkitConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitScript.java b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitScript.java index fafd6f3760..604a68cc3a 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitScript.java @@ -1,5 +1,6 @@ package net.runelite.client.plugins.microbot.leaguestoolkit; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.Script; @@ -18,6 +19,11 @@ public class LeaguesToolkitScript extends Script { KeyEvent.VK_LEFT, KeyEvent.VK_RIGHT, KeyEvent.VK_UP, KeyEvent.VK_DOWN }; + @Getter + private final GemCutter gemCutter = new GemCutter(); + + private boolean gemCutterWasEnabled = false; + public boolean run(LeaguesToolkitConfig config) { mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { try { @@ -27,6 +33,16 @@ public boolean run(LeaguesToolkitConfig config) { if (config.enableAntiAfk()) { runAntiAfk(config); } + + if (config.enableGemCutter()) { + if (!gemCutterWasEnabled) { + gemCutter.reset(); + gemCutterWasEnabled = true; + } + gemCutter.tick(config); + } else { + gemCutterWasEnabled = false; + } } catch (Exception ex) { log.error("LeaguesToolkitScript loop error", ex); }