diff --git a/src/main/java/net/runelite/client/plugins/microbot/PluginConstants.java b/src/main/java/net/runelite/client/plugins/microbot/PluginConstants.java index 3c28f7f994..b7bd5eed0d 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/PluginConstants.java +++ b/src/main/java/net/runelite/client/plugins/microbot/PluginConstants.java @@ -34,6 +34,8 @@ private PluginConstants() public static final String NATE = "[N] "; public static final String SYN = "[Syn] "; public static final String BIGL = "[BL] "; + public static final String PERT = "[P] "; + public static final String DV = "[DV] "; public static final boolean DEFAULT_ENABLED = false; public static final boolean IS_EXTERNAL = true; //test diff --git a/src/main/java/net/runelite/client/plugins/microbot/aiofighter/AIOFighterPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/aiofighter/AIOFighterPlugin.java index 09c118ff5e..81b1d0da69 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/aiofighter/AIOFighterPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/aiofighter/AIOFighterPlugin.java @@ -61,7 +61,7 @@ ) @Slf4j public class AIOFighterPlugin extends Plugin { - public static final String version = "2.1.5"; + public static final String version = "2.1.6"; public static boolean needShopping = false; private static final String SET = "Set"; private static final String CENTER_TILE = ColorUtil.wrapWithColorTag("Center Tile", JagexColors.MENU_TARGET); diff --git a/src/main/java/net/runelite/client/plugins/microbot/aiofighter/safety/SafetyScript.java b/src/main/java/net/runelite/client/plugins/microbot/aiofighter/safety/SafetyScript.java index 3abb174e14..94884eb16f 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/aiofighter/safety/SafetyScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/aiofighter/safety/SafetyScript.java @@ -6,6 +6,7 @@ import net.runelite.client.plugins.microbot.Script; import net.runelite.client.plugins.microbot.aiofighter.AIOFighterConfig; import net.runelite.client.plugins.microbot.aiofighter.AIOFighterPlugin; +import net.runelite.client.plugins.microbot.aiofighter.enums.State; 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; @@ -25,6 +26,7 @@ public boolean run(AIOFighterConfig config) { if (!Microbot.isLoggedIn()) return; if (!super.run()) return; if (!config.useSafety()) return; + if (isBankingOrWalking()) return; if (config.missingRunes() && config.useMagic() && !Rs2Magic.hasRequiredRunes(config.magicSpell())){ stopAndLog("Missing runes for spell: " + config.magicSpell()); } @@ -58,11 +60,18 @@ public boolean run(AIOFighterConfig config) { public void stopAndLog(String reason) { log(reason, Level.WARNING); - if(Rs2Bank.walkToBank()){ - Rs2Player.logout(); - Plugin PlayerAssistPlugin = Microbot.getPlugin(AIOFighterPlugin.class.getName()); - Microbot.stopPlugin(PlayerAssistPlugin); + // Avoid competing with BankerScript's walker while it is already controlling movement. + if (!isBankingOrWalking() && !Rs2Player.isMoving()) { + Rs2Bank.walkToBank(); } + Rs2Player.logout(); + Plugin PlayerAssistPlugin = Microbot.getPlugin(AIOFighterPlugin.class.getName()); + Microbot.stopPlugin(PlayerAssistPlugin); + } + + private boolean isBankingOrWalking() { + State state = AIOFighterPlugin.getState(); + return state == State.BANKING || state == State.WALKING; } @Override diff --git a/src/main/java/net/runelite/client/plugins/microbot/barrows/BarrowsPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/barrows/BarrowsPlugin.java index 6b7106c6cc..9827169efb 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/barrows/BarrowsPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/barrows/BarrowsPlugin.java @@ -32,7 +32,7 @@ ) @Slf4j public class BarrowsPlugin extends Plugin { - public static final String version = "2.0.4"; + public static final String version = "2.0.9"; @Inject private BarrowsConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/barrows/BarrowsScript.java b/src/main/java/net/runelite/client/plugins/microbot/barrows/BarrowsScript.java index 635c11d2e3..71a3c9d55a 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/barrows/BarrowsScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/barrows/BarrowsScript.java @@ -18,6 +18,7 @@ import net.runelite.client.plugins.microbot.util.combat.Rs2Combat; import net.runelite.client.plugins.microbot.util.coords.Rs2WorldArea; import net.runelite.client.plugins.microbot.util.dialogues.Rs2Dialogue; +import net.runelite.client.plugins.microbot.util.equipment.JewelleryLocationEnum; 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; @@ -149,6 +150,10 @@ public boolean run(BarrowsConfig config, BarrowsPlugin plugin) { if(config.selectedToBarrowsTPMethod().getToBarrowsTPMethodItemID() == ItemID.TELEPORT_TO_HOUSE) { if (!inTunnels && !shouldBank && Rs2Player.getWorldLocation().distanceTo(new WorldPoint(3573, 3296, 0)) > 60) { + if(Rs2Bank.isOpen()){ + closeBank(); + return; + } //needed to intercept the walker if(rs2TileObjectCache.query().withId(4525).nearest() == null){ Rs2Inventory.interact("Teleport to house", "Inside"); @@ -447,6 +452,10 @@ public boolean run(BarrowsConfig config, BarrowsPlugin plugin) { WhoisTun = "Unknown"; inTunnels = false; } else { + if(Rs2Bank.isOpen()){ + closeBank(); + return; + } Rs2Inventory.interact("Teleport to house", "Inside"); sleepUntil(() -> Rs2Player.getWorldLocation().getY() < 9600 || Rs2Player.getWorldLocation().getY() > 9730, Rs2Random.between(6000, 10000)); ChestsOpened++; @@ -467,7 +476,6 @@ public boolean run(BarrowsConfig config, BarrowsPlugin plugin) { stopFutureWalker(); //tele out outOfSupplies(config); - //walk to and open the bank Rs2Bank.walkToBankAndUseBank(BankLocation.FEROX_ENCLAVE); BreakHandlerScript.lockState.set(false); } else { @@ -693,36 +701,61 @@ public void closeBank(){ } public void handlePOH(BarrowsConfig config){ - if(config.selectedToBarrowsTPMethod().getToBarrowsTPMethodItemID() == ItemID.TELEPORT_TO_HOUSE){ - Rs2TileObjectModel pohThing = rs2TileObjectCache.query().withId(4525).nearest(); - if(pohThing != null){ - Microbot.log("We're in our POH"); - Rs2TileObjectModel rejPool = rs2TileObjectCache.query().withIds(29238,29239,29241,29240).nearest(); - if(rejPool != null){ - if(rejPool.click("Drink")){ - sleepUntil(()-> Rs2Player.isMoving(), Rs2Random.between(2000,4000)); - sleepUntil(()-> !Rs2Player.isMoving(), Rs2Random.between(10000,15000)); - } + if(config.selectedToBarrowsTPMethod().getToBarrowsTPMethodItemID() != ItemID.TELEPORT_TO_HOUSE){ + return; + } + Client client = Microbot.getClient(); + if(client == null){ + return; + } + WorldView worldView = client.getTopLevelWorldView(); + if(worldView == null){ + return; + } + if(!worldView.isInstance()){ + return; + } + Rs2TileObjectModel pohThing = rs2TileObjectCache.query().withId(4525).nearestOnClientThread(); + if(pohThing == null){ + return; + } + Microbot.log("We're in our POH"); + Rs2TileObjectModel rejPool = rs2TileObjectCache.query().withIds(29238,29239,29241,29240).nearestOnClientThread(); + if(rejPool != null){ + if(rejPool.click("Drink")){ + sleepUntil(()-> Rs2Player.isMoving(), Rs2Random.between(2000,4000)); + sleepUntil(()-> !Rs2Player.isMoving(), Rs2Random.between(10000,15000)); + } + } + Rs2TileObjectModel regularPortal = rs2TileObjectCache.query().withIds(37603,37615,37591).nearestOnClientThread(); + if(regularPortal != null){ + for(int pohPortalAttempts = 0; pohPortalAttempts < 40; pohPortalAttempts++){ + if(!super.isRunning()){ + break; } - Rs2TileObjectModel regularPortal = rs2TileObjectCache.query().withIds(37603,37615,37591).nearest(); - if(regularPortal != null){ - while(pohThing != null){ - if(!super.isRunning()){break;} - if(!Rs2Player.isMoving()){ - if(regularPortal.click("Enter")){ - sleepUntil(()-> Rs2Player.isMoving(), Rs2Random.between(2000,4000)); - sleepUntil(()-> !Rs2Player.isMoving(), Rs2Random.between(10000,15000)); - sleepUntil(()-> rs2TileObjectCache.query().withIds(37603,37615,37591).nearest() == null, Rs2Random.between(10000,15000)); - } - } - } - + pohThing = rs2TileObjectCache.query().withId(4525).nearestOnClientThread(); + if(pohThing == null){ + break; + } + regularPortal = rs2TileObjectCache.query().withIds(37603,37615,37591).nearestOnClientThread(); + if(regularPortal == null){ + break; + } + if(Rs2Player.isMoving()){ + sleep(Rs2Random.between(200, 600)); + continue; + } + if(regularPortal.click("Enter")){ + sleepUntil(()-> Rs2Player.isMoving(), Rs2Random.between(2000,4000)); + sleepUntil(()-> !Rs2Player.isMoving(), Rs2Random.between(10000,15000)); + sleepUntil(()-> rs2TileObjectCache.query().withIds(37603,37615,37591).nearestOnClientThread() == null, Rs2Random.between(10000,15000)); } else { - // we have a nexus 33410 - Microbot.log("No nexus support yet, shutting down"); - super.shutdown(); + break; } } + } else { + Microbot.log("No nexus support yet, shutting down"); + super.shutdown(); } } @@ -1105,15 +1138,98 @@ public void antiPatternDropVials(){ } public void outOfSupplies(BarrowsConfig config){ suppliesCheck(config); - // Needed because the walker won't teleport to the enclave while in the tunnels or in a barrow - if(shouldBank && (inTunnels || Rs2Player.getWorldLocation().getPlane() == 3)){ - if(Rs2Equipment.interact(EquipmentInventorySlot.RING, "Ferox Enclave")){ - Microbot.log("We're out of supplies. Teleporting."); - if(inTunnels) inTunnels=false; - sleepUntil(() -> Rs2Player.isAnimating(), Rs2Random.between(2000, 4000)); - sleepUntil(() -> !Rs2Player.isAnimating(), Rs2Random.between(6000, 10000)); + if(!shouldBank){ + return; + } + boolean needFeroxRingTeleport = false; + if(inTunnels){ + needFeroxRingTeleport = true; + } + if(Rs2Player.getWorldLocation().getPlane() == 3){ + needFeroxRingTeleport = true; + } + if(isInPlayerOwnedHouse()){ + needFeroxRingTeleport = true; + } + if(!needFeroxRingTeleport){ + return; + } + if(tryFeroxTeleportViaRingOfDueling()){ + Microbot.log("We're out of supplies. Teleporting to Ferox Enclave."); + if(inTunnels){ + inTunnels = false; } + sleepUntil(() -> Rs2Player.isAnimating(), Rs2Random.between(2000, 4000)); + sleepUntil(() -> !Rs2Player.isAnimating(), Rs2Random.between(6000, 10000)); + } + } + + private boolean isInPlayerOwnedHouse(){ + Client c = Microbot.getClient(); + if(c == null){ + return false; } + WorldView wv = c.getTopLevelWorldView(); + if(wv == null){ + return false; + } + if(!wv.isInstance()){ + return false; + } + if(inTunnels){ + return false; + } + Rs2TileObjectModel portal = rs2TileObjectCache.query().withId(4525).nearestOnClientThread(); + return portal != null; + } + + private boolean tryFeroxTeleportViaRingOfDueling(){ + Rs2ItemModel equippedRing = Rs2Equipment.get(EquipmentInventorySlot.RING); + if(equippedRing != null){ + String equippedName = equippedRing.getName(); + if(equippedName != null){ + if(equippedName.contains("Ring of dueling")){ + if(Rs2Equipment.interact(EquipmentInventorySlot.RING, "Ferox Enclave")){ + return true; + } + } + } + } + int[] duelingRingIds = new int[]{ + ItemID.RING_OF_DUELING1, + ItemID.RING_OF_DUELING2, + ItemID.RING_OF_DUELING3, + ItemID.RING_OF_DUELING4, + ItemID.RING_OF_DUELING5, + ItemID.RING_OF_DUELING6, + ItemID.RING_OF_DUELING7, + ItemID.RING_OF_DUELING8 + }; + for(int idx = duelingRingIds.length - 1; idx >= 0; idx--){ + int ringId = duelingRingIds[idx]; + if(!Rs2Inventory.hasItem(ringId)){ + continue; + } + if(tryRubInventoryRingToFerox(ringId)){ + return true; + } + } + return false; + } + + private boolean tryRubInventoryRingToFerox(int ringId){ + String feroxLabel = JewelleryLocationEnum.FEROX_ENCLAVE.getDestination(); + if(Rs2Inventory.interact(ringId, feroxLabel)){ + return true; + } + if(Rs2Inventory.interact(ringId, "Rub")){ + sleepUntil(() -> Rs2Dialogue.hasDialogueOption(feroxLabel), Rs2Random.between(1500, 3500)); + if(Rs2Dialogue.clickOption(feroxLabel)){ + return true; + } + return Rs2Dialogue.clickOption(feroxLabel, false); + } + return false; } public void disablePrayer(){ if(Rs2Random.between(0,100) >= Rs2Random.between(0,5)) { diff --git a/src/main/java/net/runelite/client/plugins/microbot/birdhunter/BirdHunterConfig.java b/src/main/java/net/runelite/client/plugins/microbot/birdhunter/BirdHunterConfig.java index a2e547e407..4a8bddb9c3 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/birdhunter/BirdHunterConfig.java +++ b/src/main/java/net/runelite/client/plugins/microbot/birdhunter/BirdHunterConfig.java @@ -17,16 +17,6 @@ default boolean buryBones() { return true; } - @ConfigItem( - keyName = "keepItemNames", - name = "Keep Item Names", - description = "Comma-separated list of item names that should not be dropped", - position = 3 - ) - default String keepItemNames() { - return "Bird snare"; - } - @ConfigItem( keyName = "huntingRadiusValue", name = "Hunting radius", diff --git a/src/main/java/net/runelite/client/plugins/microbot/birdhunter/BirdHunterPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/birdhunter/BirdHunterPlugin.java index 57f7045b26..75425fddb5 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/birdhunter/BirdHunterPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/birdhunter/BirdHunterPlugin.java @@ -1,16 +1,32 @@ package net.runelite.client.plugins.microbot.birdhunter; import com.google.inject.Provides; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.GameObject; +import net.runelite.api.Player; +import net.runelite.api.Tile; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.GameObjectSpawned; +import net.runelite.api.events.GameTick; +import net.runelite.api.gameval.ObjectID; import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.Subscribe; import net.runelite.client.events.ConfigChanged; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.plugins.hunter.HunterTrap; +import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.PluginConstants; import net.runelite.client.ui.overlay.OverlayManager; import javax.inject.Inject; +import java.time.Instant; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; @PluginDescriptor( name = PluginDescriptor.zerozero + "Bird Hunter", @@ -26,7 +42,10 @@ @Slf4j public class BirdHunterPlugin extends Plugin { - public final static String version = "1.0.1"; + public final static String version = "1.0.2"; + + @Inject + private Client client; @Inject private BirdHunterConfig config; @@ -40,6 +59,10 @@ public class BirdHunterPlugin extends Plugin { @Inject private OverlayManager overlayManager; + @Getter + private final Map traps = new HashMap<>(); + private WorldPoint lastTickLocalPlayerLocation; + @Provides BirdHunterConfig provideConfig(ConfigManager configManager) { return configManager.getConfig(BirdHunterConfig.class); @@ -47,8 +70,20 @@ BirdHunterConfig provideConfig(ConfigManager configManager) { @Override protected void startUp() { + // Seed lastTickLocalPlayerLocation on the client thread before the + // script loop can fire a layBirdSnare — otherwise the first snare's + // GameObjectSpawned event sees a null baseline, the trap is never + // recorded as owned, and the filter/setTrap path puts the bot in an + // infinite movePlayerOffObject loop on its own untracked trap. + // startUp runs on the AWT EDT; reading player location from there + // throws "must be called on client thread". + lastTickLocalPlayerLocation = Microbot.getClientThread().runOnClientThreadOptional(() -> { + Player lp = client.getLocalPlayer(); + return lp != null ? lp.getWorldLocation() : null; + }).orElse(null); + if (config.startScript()) { - birdHunterScript.run(config); + birdHunterScript.run(config, this); this.overlayManager.add(this.birdHunterOverlay); } } @@ -57,13 +92,14 @@ protected void startUp() { protected void shutDown() { this.overlayManager.remove(this.birdHunterOverlay); birdHunterScript.shutdown(); + traps.clear(); } @Subscribe public void onConfigChanged(ConfigChanged event) { if (event.getGroup().equals("birdhunter") && event.getKey().equals("startScript")) { if (config.startScript()) { - birdHunterScript.run(config); + birdHunterScript.run(config, this); } else { birdHunterScript.shutdown(); } @@ -72,4 +108,82 @@ public void onConfigChanged(ConfigChanged event) { birdHunterScript.updateHuntingArea(config); } } + + @Subscribe + public void onGameObjectSpawned(GameObjectSpawned event) { + final GameObject go = event.getGameObject(); + final WorldPoint trapLocation = go.getWorldLocation(); + final HunterTrap myTrap = traps.get(trapLocation); + + switch (go.getId()) { + // Empty placed snare — ownership decision point. Player location is + // updated before this event fires, so we compare the spawn tile to + // the PREVIOUS tick's player location. distance == 0 means the snare + // spawned on the exact tile the player stood on last tick, i.e. ours. + case ObjectID.HUNTING_OJIBWAY_TRAP: + if (lastTickLocalPlayerLocation != null + && trapLocation.distanceTo(lastTickLocalPlayerLocation) == 0) { + traps.put(trapLocation, new HunterTrap(go)); + } + break; + + case ObjectID.HUNTING_OJIBWAY_TRAP_FULL_JUNGLE: + case ObjectID.HUNTING_OJIBWAY_TRAP_FULL_POLAR: + case ObjectID.HUNTING_OJIBWAY_TRAP_FULL_DESERT: + case ObjectID.HUNTING_OJIBWAY_TRAP_FULL_WOODLAND: + case ObjectID.HUNTING_OJIBWAY_TRAP_FULL_COLOURED: + if (myTrap != null) { + myTrap.setState(HunterTrap.State.FULL); + myTrap.resetTimer(); + } + break; + + case ObjectID.HUNTING_OJIBWAY_TRAP_BROKEN: + if (myTrap != null) { + myTrap.setState(HunterTrap.State.EMPTY); + myTrap.resetTimer(); + } + break; + + case ObjectID.HUNTING_OJIBWAY_TRAP_FAILING: + case ObjectID.HUNTING_OJIBWAY_TRAP_TRAPPING_JUNGLE: + case ObjectID.HUNTING_OJIBWAY_TRAP_TRAPPING_COLOURED: + case ObjectID.HUNTING_OJIBWAY_TRAP_TRAPPING_DESERT: + case ObjectID.HUNTING_OJIBWAY_TRAP_TRAPPING_WOODLAND: + case ObjectID.HUNTING_OJIBWAY_TRAP_TRAPPING_POLAR: + if (myTrap != null) { + myTrap.setState(HunterTrap.State.TRANSITION); + } + break; + } + } + + @Subscribe + public void onGameTick(GameTick event) { + Iterator> it = traps.entrySet().iterator(); + Tile[][][] tiles = client.getScene().getTiles(); + Instant expire = Instant.now().minus(HunterTrap.TRAP_TIME.multipliedBy(2)); + + while (it.hasNext()) { + Map.Entry entry = it.next(); + HunterTrap trap = entry.getValue(); + WorldPoint world = entry.getKey(); + LocalPoint local = LocalPoint.fromWorld(client, world); + + if (local == null) { + if (trap.getPlacedOn().isBefore(expire)) it.remove(); + continue; + } + + GameObject[] objects = tiles[world.getPlane()][local.getSceneX()][local.getSceneY()].getGameObjects(); + boolean anyObject = false; + for (GameObject o : objects) { + if (o != null) { anyObject = true; break; } + } + if (!anyObject) it.remove(); + } + + Player lp = client.getLocalPlayer(); + if (lp != null) lastTickLocalPlayerLocation = lp.getWorldLocation(); + } } diff --git a/src/main/java/net/runelite/client/plugins/microbot/birdhunter/BirdHunterScript.java b/src/main/java/net/runelite/client/plugins/microbot/birdhunter/BirdHunterScript.java index d7f79fd966..724b7711f0 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/birdhunter/BirdHunterScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/birdhunter/BirdHunterScript.java @@ -23,8 +23,10 @@ import java.util.ArrayList; import java.util.List; import java.util.Random; +import java.util.Set; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; public class BirdHunterScript extends Script { @@ -41,13 +43,12 @@ public class BirdHunterScript extends Script { private final Pair boneThresholdRange = Pair.of(3, 10); private final Pair HandleInventoryThresholdRange = Pair.of(18, 25); - public boolean run(BirdHunterConfig config) { + private BirdHunterPlugin plugin; + + public boolean run(BirdHunterConfig config, BirdHunterPlugin plugin) { + this.plugin = plugin; Microbot.log("Bird Hunter script started."); - if (!hasRequiredSnares()) { - Microbot.log("Not enough bird snares in inventory. Stopping the script."); - return false; - } initialStartTile = Rs2Player.getWorldLocation(); randomBoneThreshold = ThreadLocalRandom.current().nextInt(boneThresholdRange.getLeft(), boneThresholdRange.getRight()); @@ -64,6 +65,14 @@ public boolean run(BirdHunterConfig config) { try { if (!super.run() || !Microbot.isLoggedIn()) return; + if (!hasRequiredSnares()) { + int required = getAvailableTraps(Rs2Player.getRealSkillLevel(Skill.HUNTER)); + Microbot.showMessage("Bird Hunter needs at least " + required + + " bird snares in inventory for your Hunter level. Stopping plugin."); + Microbot.stopPlugin(plugin); + return; + } + if (!isInHuntingArea()) { Microbot.log("Player is outside the designated hunting area."); walkBackToArea(); @@ -83,20 +92,21 @@ public boolean run(BirdHunterConfig config) { private boolean hasRequiredSnares() { int hunterLevel = Rs2Player.getRealSkillLevel(Skill.HUNTER); - int allowedSnares = getAvailableTraps(hunterLevel); // Calculate the allowed number of snares + int allowedSnares = getAvailableTraps(hunterLevel); int snaresInInventory = Rs2Inventory.itemQuantity(ItemID.HUNTING_OJIBWAY_BIRD_SNARE); Microbot.log("Allowed snares: " + allowedSnares + ", Snares in inventory: " + snaresInInventory); - return snaresInInventory >= allowedSnares; // Return true if enough snares, false otherwise + return snaresInInventory >= allowedSnares; } public void updateHuntingArea(BirdHunterConfig config) { huntingRadius = config.huntingRadiusValue(); + int side = (2 * huntingRadius) + 1; dynamicHuntingArea = new WorldArea( initialStartTile.getX() - huntingRadius, initialStartTile.getY() - huntingRadius, - (huntingRadius * huntingRadius) + 1, (huntingRadius * huntingRadius) + 1, + side, side, initialStartTile.getPlane() ); } @@ -107,7 +117,7 @@ private boolean isInHuntingArea() { } private void walkBackToArea() { - WorldPoint walkableTile = getSafeWalkableTile(dynamicHuntingArea); + WorldPoint walkableTile = getNearestSafeWalkableTileInArea(dynamicHuntingArea); if (walkableTile != null) { Rs2Walker.walkFastCanvas(walkableTile); @@ -117,6 +127,28 @@ private void walkBackToArea() { } } + private WorldPoint getNearestSafeWalkableTileInArea(WorldArea huntingArea) { + WorldPoint from = Rs2Player.getWorldLocation(); + WorldPoint nearest = null; + int bestDist = Integer.MAX_VALUE; + + for (int x = initialStartTile.getX() - huntingRadius; x <= initialStartTile.getX() + huntingRadius; x++) { + for (int y = initialStartTile.getY() - huntingRadius; y <= initialStartTile.getY() + huntingRadius; y++) { + WorldPoint candidate = new WorldPoint(x, y, huntingArea.getPlane()); + LocalPoint localPoint = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), candidate); + if (localPoint == null || !huntingArea.contains(candidate)) continue; + if (!Rs2Tile.isWalkable(localPoint) || isGameObjectAt(candidate)) continue; + + int dist = from.distanceTo(candidate); + if (dist < bestDist) { + bestDist = dist; + nearest = candidate; + } + } + } + return nearest; + } + private void handleTraps(BirdHunterConfig config) { List successfulTraps = new ArrayList<>(); successfulTraps.addAll(Microbot.getRs2TileObjectCache().query().withId(ObjectID.HUNTING_OJIBWAY_TRAP_TRAPPING_JUNGLE).toList()); @@ -134,10 +166,20 @@ private void handleTraps(BirdHunterConfig config) { catchingTraps.addAll(Microbot.getRs2TileObjectCache().query().withId(ObjectID.HUNTING_OJIBWAY_TRAP_TRAPPING_POLAR).toList()); catchingTraps.addAll(Microbot.getRs2TileObjectCache().query().withId(ObjectID.HUNTING_OJIBWAY_TRAP_FULL_JUNGLE).toList()); - List failedTraps = Microbot.getRs2TileObjectCache().query().withId(ObjectID.HUNTING_OJIBWAY_TRAP_BROKEN).toList(); + List failedTraps = new ArrayList<>(Microbot.getRs2TileObjectCache().query().withId(ObjectID.HUNTING_OJIBWAY_TRAP_BROKEN).toList()); List idleTraps = new ArrayList<>(Microbot.getRs2TileObjectCache().query().withId(ObjectID.HUNTING_OJIBWAY_TRAP).toList()); idleTraps.addAll(Microbot.getRs2TileObjectCache().query().withId(ObjectID.HUNTING_OJIBWAY_TRAP_FAILING).toList()); + // Ownership filter: the plugin records a trap's WorldPoint when it spawns + // on the player's previous-tick tile. Skip everything else — other players' + // snares should not be clicked, and they must not inflate totalTraps below. + Set owned = plugin.getTraps().keySet(); + Predicate mine = t -> owned.contains(t.getWorldLocation()); + successfulTraps.removeIf(mine.negate()); + catchingTraps.removeIf(mine.negate()); + failedTraps.removeIf(mine.negate()); + idleTraps.removeIf(mine.negate()); + int availableTraps = getAvailableTraps(Rs2Player.getRealSkillLevel(Skill.HUNTER)); int totalTraps = successfulTraps.size() + failedTraps.size() + idleTraps.size() + catchingTraps.size(); @@ -174,7 +216,11 @@ private void handleTraps(BirdHunterConfig config) { private void setTrap(BirdHunterConfig config) { if (!Rs2Inventory.contains(ItemID.HUNTING_OJIBWAY_BIRD_SNARE)) return; - if (Rs2Player.isStandingOnGameObject()) { + // Rs2Player.isStandingOnGameObject() also returns true for ground items + // (dropped loot), which don't actually block snare placement in-game. + // Only skip the tile when there's a real game object on it (existing + // trap, tree, rock). + if (isGameObjectAt(Rs2Player.getWorldLocation())) { if (!movePlayerOffObject()) return; } @@ -245,11 +291,17 @@ private boolean movePlayerOffObject() { private boolean interactWithTrap(Rs2TileObjectModel birdSnare) { - sleep(Rs2Random.randomGaussian(2000, 1250)); - birdSnare.click(); - sleepUntil(() -> Rs2Inventory.waitForInventoryChanges(7000)); - sleep(Rs2Random.randomGaussian(2000, 1250)); - + if (!plugin.getTraps().containsKey(birdSnare.getWorldLocation())) return false; + + // Retry the click until inventory changes (snare returned / loot received). + // Previously a single click with a 7s inventory-changes wait and 2×2s + // gaussian sleeps meant ~13s of stall on a missed click. + int invBefore = Rs2Inventory.count(); + for (int attempt = 0; attempt < 3; attempt++) { + birdSnare.click(); + if (sleepUntil(() -> Rs2Inventory.count() != invBefore, 2500)) break; + } + sleep(Rs2Random.randomGaussian(600, 200)); return false; } @@ -274,11 +326,9 @@ private void checkForBonesAndHandleInventory(BirdHunterConfig config) { } private void handleInventory(BirdHunterConfig config) { - if (config.buryBones() && Rs2Inventory.count("Bones") > randomBoneThreshold) { + if (config.buryBones()) { buryBones(config); - } - buryBones(config); dropItems(config); } @@ -294,14 +344,16 @@ private void buryBones(BirdHunterConfig config) { } } + // Strict drop whitelist. Replaces an earlier dropAllExcept(keepList) that would + // nuke the entire inventory if the keep list was misconfigured. Bird snaring + // only produces Raw bird meat, Bones, and feathers — feathers stack so we let + // them ride; bones are buried when the config is enabled, dropped otherwise. private void dropItems(BirdHunterConfig config) { - String keepItemsConfig = config.keepItemNames(); - List keepItemNames = List.of(keepItemsConfig.split("\\s*,\\s*")); - - if (!keepItemNames.contains("Bird snare")) { - keepItemNames.add("Bird snare"); + if (config.buryBones()) { + Rs2Inventory.dropAll("Raw bird meat"); + } else { + Rs2Inventory.dropAll("Raw bird meat", "Bones"); } - Rs2Inventory.dropAllExcept(keepItemNames.toArray(new String[0])); } public int getAvailableTraps(int hunterLevel) { diff --git a/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunConfig.java b/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunConfig.java index 640e838801..4c98ec618c 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunConfig.java +++ b/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunConfig.java @@ -26,6 +26,8 @@ "
    \n" + "
  1. Taverley teleport tab
  2. \n" + "
  3. Skills necklace (2 to 6)
  4. \n" + + "
  5. Construction cape (at 99 Construction) - auto-grabbed for POH teleports
  6. \n" + + "
  7. Crystal tree sapling (for Prifddinas patch, requires Song of the Elves)
  8. \n" + "
" + "
Optional:\n" + "
    \n" + @@ -261,6 +263,15 @@ public interface FarmTreeRunConfig extends Config { ) default boolean auburnTreePatch() { return true; } + @ConfigItem( + keyName = "priffddinasCrystalTree", + name = "Prifddinas (Crystal)", + description = "Prifddinas Crystal tree patch (requires 74 Farming and Song of the Elves). Uses a Crystal tree sapling.", + position = 7, + section = treePatchesSection + ) + default boolean priffddinasCrystalTreePatch() { return false; } + /* ========================= * Fruit tree patches — ordered to match run: * GS Fruit → TGV Fruit → Farming Guild Fruit → Brimhaven → Catherby → Lletya → Kastori diff --git a/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunPlugin.java index 3b16a62c44..203b784346 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunPlugin.java @@ -30,7 +30,7 @@ ) @Slf4j public class FarmTreeRunPlugin extends Plugin { - public static final String version = "1.1.1"; + public static final String version = "1.1.2"; @Inject private FarmTreeRunConfig config; @Provides diff --git a/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunScript.java b/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunScript.java index 7caf0272ae..db370cec82 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunScript.java @@ -82,6 +82,7 @@ public enum Patch { FOSSIL_TREE_PATCH_C(30481, new WorldPoint(3701, 3840, 0), TreeKind.HARD_TREE, 1, 0), AUBURNVALE_TREE_PATCH(56953, new WorldPoint(1365, 3320, 0), TreeKind.TREE, 1, 0), KASTORI_FRUIT_TREE_PATCH(56955, new WorldPoint(1349, 3058, 0), TreeKind.FRUIT_TREE, 1, 12765), + PRIFFDDINAS_CRYSTAL_TREE_PATCH(34906, new WorldPoint(3291, 6117, 0), TreeKind.TREE, 74, 0), AVIUM_SAVANNAH_HARDWOOD_PATCH(50692, new WorldPoint(1684, 2974, 0), TreeKind.HARD_TREE,1,0); private final int id; @@ -119,6 +120,7 @@ public boolean run(FarmTreeRunConfig config) { } calculatePatches(config); checkSaplingLevelRequirement(config); + if (!validateSpecialPatches(config)) return; dropEmptyPlantPots(); Patch patch = null; @@ -318,6 +320,17 @@ public boolean run(FarmTreeRunConfig config) { } if (!handledPatch) return; } + botStatus = net.runelite.client.plugins.microbot.farmtreerun.enums.FarmTreeRunState.HANDLE_PRIFFDDINAS_CRYSTAL_TREE_PATCH; + break; + } + case HANDLE_PRIFFDDINAS_CRYSTAL_TREE_PATCH: { + patch = Patch.PRIFFDDINAS_CRYSTAL_TREE_PATCH; + if (config.priffddinasCrystalTreePatch() && patch.hasRequiredLevel()) { + if (walkToLocation(patch.getLocation())) { + handledPatch = handlePatch(config, patch); + } + if (!handledPatch) return; + } botStatus = net.runelite.client.plugins.microbot.farmtreerun.enums.FarmTreeRunState.HANDLE_AVIUM_SAVANNAH_HARDWOOD_PATCH; break; } @@ -365,6 +378,23 @@ private void calculatePatches(FarmTreeRunConfig config) { } } + private boolean validateSpecialPatches(FarmTreeRunConfig config) { + if (config.priffddinasCrystalTreePatch()) { + int farmingLevel = Rs2Player.getRealSkillLevel(Skill.FARMING); + if (farmingLevel < 74) { + Microbot.showMessage("Prifddinas Crystal tree requires 74 Farming (you have " + farmingLevel + "). Disable the Prifddinas patch or train Farming before starting. Shutting down."); + shutdown(); + return false; + } + if (Rs2Player.getQuestState(Quest.SONG_OF_THE_ELVES) != QuestState.FINISHED) { + Microbot.showMessage("Prifddinas Crystal tree requires Song of the Elves to be completed. Disable the Prifddinas patch before starting. Shutting down."); + shutdown(); + return false; + } + } + return true; + } + private void checkSaplingLevelRequirement(FarmTreeRunConfig config) { if (!getSelectedTreePatches(config).isEmpty()) config.selectedTree().hasRequiredLevel(); @@ -450,6 +480,15 @@ private void bank(FarmTreeRunConfig config) { } } + // Construction cape (99 Construction): useful for POH/teleport options + if (Rs2Player.getRealSkillLevel(Skill.CONSTRUCTION) >= 99) { + if (Rs2Bank.hasItem(ItemID.CONSTRUCT_CAPET)) { + items.add(new FarmingItem(ItemID.CONSTRUCT_CAPET, 1, false, true)); + } else if (Rs2Bank.hasItem(ItemID.CONSTRUCT_CAPE)) { + items.add(new FarmingItem(ItemID.CONSTRUCT_CAPE, 1, false, true)); + } + } + if (config.useSkillsNecklace() && (config.farmingGuildTreePatch() || config.farmingGuildFruitTreePatch())) { if (Rs2Bank.hasItem(ItemID.SKILLS_NECKLACE2)) { items.add(new FarmingItem(ItemID.SKILLS_NECKLACE2, 1)); @@ -475,8 +514,16 @@ private void bank(FarmTreeRunConfig config) { int fruitTreeSaplingsCount = getSelectedFruitTreePatches(config).size(); int hardTreeSaplingsCount = getSelectedHardTreePatches(config).size(); - if (treeSaplingsCount > 0) - items.add(new FarmingItem(selectedTree.getSaplingId(), treeSaplingsCount)); + // Crystal tree patch uses its own sapling, not the selected regular tree sapling + boolean priffEnabled = config.priffddinasCrystalTreePatch() + && Patch.PRIFFDDINAS_CRYSTAL_TREE_PATCH.hasRequiredLevel(); + int regularTreeSaplingsCount = priffEnabled ? treeSaplingsCount - 1 : treeSaplingsCount; + + if (regularTreeSaplingsCount > 0) + items.add(new FarmingItem(selectedTree.getSaplingId(), regularTreeSaplingsCount)); + + if (priffEnabled) + items.add(new FarmingItem(ItemID.CRYSTAL_SAPLING, 1)); if (fruitTreeSaplingsCount > 0) items.add(new FarmingItem(selectedFruitTree.getSaplingId(), fruitTreeSaplingsCount)); @@ -484,8 +531,8 @@ private void bank(FarmTreeRunConfig config) { if (hardTreeSaplingsCount > 0) items.add(new FarmingItem(selectedHardTree.getSaplingId(), hardTreeSaplingsCount)); - if (config.protectTrees()) - items.add(new FarmingItem(selectedTree.getPaymentId(), selectedTree.getPaymentAmount() * treeSaplingsCount, true)); + if (config.protectTrees() && regularTreeSaplingsCount > 0) + items.add(new FarmingItem(selectedTree.getPaymentId(), selectedTree.getPaymentAmount() * regularTreeSaplingsCount, true)); if (config.protectHardTrees()) items.add(new FarmingItem(selectedHardTree.getPaymentId(), selectedHardTree.getPaymentAmount() * hardTreeSaplingsCount, true)); @@ -899,7 +946,8 @@ private List getSelectedTreePatches(FarmTreeRunConfig config) { config::taverleyTreePatch, config::varrockTreePatch, config::farmingGuildTreePatch, - config::auburnTreePatch + config::auburnTreePatch, + config::priffddinasCrystalTreePatch ); // Filter the patches to include only those that return true @@ -986,6 +1034,9 @@ private static int getSaplingToUse(Patch patch, FarmTreeRunConfig config) { if (patch == Patch.FOSSIL_TREE_PATCH_A || patch == Patch.FOSSIL_TREE_PATCH_B || patch == Patch.FOSSIL_TREE_PATCH_C ) { return config.selectedHardTree().getSaplingId(); + } else if (patch == Patch.PRIFFDDINAS_CRYSTAL_TREE_PATCH) { + return ItemID.CRYSTAL_SAPLING; + } else return patch.kind == TreeKind.TREE ? config.selectedTree().getSaplingId() : config.selectedFruitTree().getSaplingId(); diff --git a/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/enums/FarmTreeRunState.java b/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/enums/FarmTreeRunState.java index 4e5b084968..e9cfc0c11a 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/enums/FarmTreeRunState.java +++ b/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/enums/FarmTreeRunState.java @@ -49,6 +49,8 @@ public enum FarmTreeRunState { HANDLE_KASTORI_FRUIT_TREE_PATCH, + HANDLE_PRIFFDDINAS_CRYSTAL_TREE_PATCH, + HANDLE_AVIUM_SAVANNAH_HARDWOOD_PATCH, FINISHED diff --git a/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/enums/HardTreeEnums.java b/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/enums/HardTreeEnums.java index cb183d0213..25d8321c61 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/enums/HardTreeEnums.java +++ b/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/enums/HardTreeEnums.java @@ -10,7 +10,7 @@ @RequiredArgsConstructor public enum HardTreeEnums { TEAK("Teak sapling", ItemID.PLANTPOT_TEAK_SAPLING, ItemID.LIMPWURT_ROOT, 15,75), - MAHOGANY("Mahogany sapling", ItemID.PLANTPOT_MAHOGANY_SAPLING, ItemID.LIMPWURT_ROOT, 15,75); + MAHOGANY("Mahogany sapling", ItemID.PLANTPOT_MAHOGANY_SAPLING, ItemID.YANILLIAN_HOPS, 25,75); private final String name; diff --git a/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrPlugin.java index 1cd50f0b57..7f552ea253 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrPlugin.java @@ -41,7 +41,7 @@ ) @Slf4j public class GotrPlugin extends Plugin { - public static final String version = "1.5.2"; + public static final String version = "1.5.4"; @Inject private GotrConfig config; @@ -51,6 +51,9 @@ GotrConfig provideConfig(ConfigManager configManager) { return configManager.getConfig(GotrConfig.class); } + @Inject + private ConfigManager configManager; + @Inject private OverlayManager overlayManager; @Inject @@ -71,6 +74,10 @@ public GotrScript getScript() { @Override protected void startUp() throws AWTException { + if (config.maxFragmentAmount() == 0) { + configManager.setConfiguration("gotr", "maxFragmentAmount", 100); + } + if (overlayManager != null) { overlayManager.add(pouchOverlay); overlayManager.add(gotrOverlay); diff --git a/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrScript.java b/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrScript.java index 9c4bc4a497..c36c592c6c 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrScript.java @@ -265,7 +265,7 @@ private boolean repairCells() { int cellTier = CellType.GetCellTier(cell.getId()); List shieldCells = Microbot.getRs2TileObjectCache().query() .where(o -> o.getName() != null && o.getName().toLowerCase().contains("cell_tile")) - .toList(); + .toListOnClientThread(); if (Rs2Inventory.hasItemAmount(GUARDIAN_ESSENCE, 10)) { for (Rs2TileObjectModel shieldCell : shieldCells) { diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/FireLine.java b/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/FireLine.java new file mode 100644 index 0000000000..4573b12c1f --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/FireLine.java @@ -0,0 +1,17 @@ +package net.runelite.client.plugins.microbot.leaguesfiremaking; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.coords.WorldPoint; + +@Getter +@RequiredArgsConstructor +public class FireLine { + private final WorldPoint westEnd; + private final WorldPoint eastEnd; + private final int length; + + public int getY() { + return westEnd.getY(); + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/LeaguesFiremakingConfig.java b/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/LeaguesFiremakingConfig.java new file mode 100644 index 0000000000..e221422694 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/LeaguesFiremakingConfig.java @@ -0,0 +1,76 @@ +package net.runelite.client.plugins.microbot.leaguesfiremaking; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigInformation; +import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.ConfigSection; +import net.runelite.client.config.Range; + +@ConfigGroup("LeaguesFiremaking") +@ConfigInformation("

    AI Firemaking

    " + + "

    Version: " + LeaguesFiremakingPlugin.version + "

    " + + "

    Withdraws logs from the bank, finds open space, and lights fires in lines.

    " + + "

    Scans surrounding tiles to pick the best row, adapts around existing fires and obstacles.

    " + + "

    Supports Bank Heist briefcase for instant banking.

    ") +public interface LeaguesFiremakingConfig extends Config { + + @ConfigSection( + name = "General", + description = "General settings", + position = 0 + ) + String generalSection = "general"; + + @ConfigSection( + name = "Banking", + description = "Banking settings", + position = 1 + ) + String bankingSection = "banking"; + + @ConfigItem( + keyName = "logType", + name = "Log type", + description = "Which logs to burn", + position = 0, + section = generalSection + ) + default LogType logType() { + return LogType.LOGS; + } + + @ConfigItem( + keyName = "progressiveMode", + name = "Progressive mode", + description = "Automatically pick the best log for your Firemaking level", + position = 1, + section = generalSection + ) + default boolean progressiveMode() { + return false; + } + + @Range(min = 10, max = 50) + @ConfigItem( + keyName = "scanRadius", + name = "Scan radius", + description = "How many tiles to scan around your starting position for open space", + position = 2, + section = generalSection + ) + default int scanRadius() { + return 25; + } + + @ConfigItem( + keyName = "useBriefcase", + name = "Use Bank Heist briefcase", + description = "Use the banker's briefcase to teleport to a bank instead of walking (Leagues relic)", + position = 0, + section = bankingSection + ) + default boolean useBriefcase() { + return false; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/LeaguesFiremakingOverlay.java b/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/LeaguesFiremakingOverlay.java new file mode 100644 index 0000000000..de87cab3ab --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/LeaguesFiremakingOverlay.java @@ -0,0 +1,57 @@ +package net.runelite.client.plugins.microbot.leaguesfiremaking; + +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.ui.overlay.OverlayPanel; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.components.LineComponent; +import net.runelite.client.ui.overlay.components.TitleComponent; + +import javax.inject.Inject; +import java.awt.*; + +public class LeaguesFiremakingOverlay extends OverlayPanel { + + @Inject + private LeaguesFiremakingScript script; + + @Inject + public LeaguesFiremakingOverlay() { + setPosition(OverlayPosition.TOP_LEFT); + setNaughty(); + } + + @Override + public Dimension render(Graphics2D graphics) { + panelComponent.setPreferredSize(new Dimension(200, 0)); + + panelComponent.getChildren().add(TitleComponent.builder() + .text("AI Firemaking v" + LeaguesFiremakingPlugin.version) + .color(Color.GREEN) + .build()); + + panelComponent.getChildren().add(LineComponent.builder() + .left("Status") + .right(script.getStatus()) + .build()); + + panelComponent.getChildren().add(LineComponent.builder() + .left("State") + .right(script.getState().name()) + .build()); + + FireLine line = script.getCurrentLine(); + if (line != null) { + panelComponent.getChildren().add(LineComponent.builder() + .left("Line length") + .right(String.valueOf(line.getLength())) + .build()); + } + + panelComponent.getChildren().add(LineComponent.builder() + .left("Microbot") + .right(Microbot.status) + .build()); + + return super.render(graphics); + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/LeaguesFiremakingPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/LeaguesFiremakingPlugin.java new file mode 100644 index 0000000000..6b8b1a6ce3 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/LeaguesFiremakingPlugin.java @@ -0,0 +1,54 @@ +package net.runelite.client.plugins.microbot.leaguesfiremaking; + +import com.google.inject.Provides; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.plugins.microbot.PluginConstants; +import net.runelite.client.ui.overlay.OverlayManager; + +import javax.inject.Inject; + +@PluginDescriptor( + name = PluginConstants.DV + "AI Firemaking", + description = "Lights fires in lines anywhere — withdraws logs, scans for open tiles, adapts around obstacles", + tags = {"firemaking", "leagues", "microbot", "skilling"}, + version = LeaguesFiremakingPlugin.version, + minClientVersion = "2.0.13", + enabledByDefault = PluginConstants.DEFAULT_ENABLED, + isExternal = PluginConstants.IS_EXTERNAL +) +@Slf4j +public class LeaguesFiremakingPlugin extends Plugin { + public static final String version = "1.0.0"; + + @Inject + private LeaguesFiremakingConfig config; + + @Inject + private LeaguesFiremakingScript script; + + @Inject + private LeaguesFiremakingOverlay overlay; + + @Inject + private OverlayManager overlayManager; + + @Provides + LeaguesFiremakingConfig provideConfig(ConfigManager configManager) { + return configManager.getConfig(LeaguesFiremakingConfig.class); + } + + @Override + protected void startUp() { + overlayManager.add(overlay); + script.run(config); + } + + @Override + protected void shutDown() { + script.shutdown(); + overlayManager.remove(overlay); + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/LeaguesFiremakingScript.java b/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/LeaguesFiremakingScript.java new file mode 100644 index 0000000000..5d34a7860d --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/LeaguesFiremakingScript.java @@ -0,0 +1,253 @@ +package net.runelite.client.plugins.microbot.leaguesfiremaking; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Skill; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.Script; +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.inventory.Rs2Inventory; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.tile.Rs2Tile; +import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; + +import java.util.concurrent.TimeUnit; + +@Slf4j +public class LeaguesFiremakingScript extends Script { + + private static final int TINDERBOX_ID = 590; + private static final String TINDERBOX_NAME = "Tinderbox"; + + @Getter + private State state = State.SCANNING; + @Getter + private String status = "Starting"; + @Getter + private FireLine currentLine; + + private WorldPoint startPosition; + private LogType activeLogType; + + public boolean run(LeaguesFiremakingConfig config) { + Rs2Antiban.resetAntibanSettings(); + Rs2Antiban.antibanSetupTemplates.applyFiremakingSetup(); + Rs2AntibanSettings.actionCooldownChance = 0.1; + + mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { + try { + if (!super.run()) return; + if (!Microbot.isLoggedIn()) return; + if (Rs2AntibanSettings.actionCooldownActive) return; + + if (startPosition == null) { + startPosition = Rs2Player.getWorldLocation(); + } + + activeLogType = config.progressiveMode() ? LogType.getBestForLevel() : config.logType(); + + if (activeLogType == null || !activeLogType.hasRequiredLevel()) { + status = "Level too low for " + (activeLogType != null ? activeLogType.getLogName() : "any logs"); + return; + } + + switch (state) { + case SCANNING: + handleScanning(config); + break; + case WALKING_TO_LINE: + handleWalkingToLine(); + break; + case BURNING: + handleBurning(); + break; + case BANKING: + handleBanking(config); + break; + case WALKING_BACK: + handleWalkingBack(config); + break; + } + } catch (Exception ex) { + log.error("LeaguesFiremaking loop error", ex); + Microbot.log(ex.getMessage()); + } + }, 0, 600, TimeUnit.MILLISECONDS); + return true; + } + + private void handleScanning(LeaguesFiremakingConfig config) { + status = "Scanning for open space"; + + if (!Rs2Inventory.hasItem(activeLogType.getItemId())) { + state = State.BANKING; + return; + } + + currentLine = TileScanner.findBestLine(startPosition, config.scanRadius()); + + if (currentLine == null) { + status = "No open space found — try moving to a more open area"; + return; + } + + status = "Found line: " + currentLine.getLength() + " tiles"; + state = State.WALKING_TO_LINE; + } + + private void handleWalkingToLine() { + if (currentLine == null) { + state = State.SCANNING; + return; + } + + WorldPoint eastEnd = currentLine.getEastEnd(); + status = "Walking to east end of line"; + + if (Rs2Player.getWorldLocation().distanceTo(eastEnd) <= 1) { + state = State.BURNING; + return; + } + + if (!Rs2Player.isMoving()) { + Rs2Walker.walkTo(eastEnd, 0); + } + } + + private void handleBurning() { + if (!Rs2Inventory.hasItem(activeLogType.getItemId())) { + status = "Out of logs"; + state = State.BANKING; + return; + } + + if (Rs2Player.isMoving()) { + status = "Walking after lighting..."; + return; + } + + if (Rs2Player.isAnimating()) { + status = "Lighting animation..."; + return; + } + + if (!Rs2Inventory.hasItem(TINDERBOX_NAME)) { + status = "No tinderbox — banking"; + state = State.BANKING; + return; + } + + // Check if we're standing on a fire — need to step west first + WorldPoint playerPos = Rs2Player.getWorldLocation(); + boolean standingOnFire = TileScanner.hasFire(playerPos); + + if (standingOnFire) { + // Step one tile west to get off the fire + WorldPoint westTile = new WorldPoint(playerPos.getX() - 1, playerPos.getY(), playerPos.getPlane()); + if (!Rs2Tile.isWalkable(westTile)) { + // Can't go west — line is done, rescan + status = "Blocked west — rescanning"; + state = State.SCANNING; + return; + } + Rs2Walker.walkTo(westTile, 0); + sleepUntil(() -> Rs2Player.getWorldLocation().distanceTo(westTile) <= 0, 3000); + return; + } + + if (!Rs2Tile.isWalkable(playerPos)) { + status = "Standing on blocked tile — rescanning"; + state = State.SCANNING; + return; + } + + status = "Lighting " + activeLogType.getLogName(); + WorldPoint beforeLight = Rs2Player.getWorldLocation(); + Rs2Inventory.combine(TINDERBOX_NAME, activeLogType.getLogName()); + + // Wait for XP drop (fire lit) then wait for auto-walk west + if (Rs2Player.waitForXpDrop(Skill.FIREMAKING, 10000)) { + sleepUntil(() -> !Rs2Player.getWorldLocation().equals(beforeLight), 3000); + sleep(200, 400); + } + + Rs2Antiban.actionCooldown(); + Rs2Antiban.takeMicroBreakByChance(); + } + + private void handleBanking(LeaguesFiremakingConfig config) { + if (config.useBriefcase()) { + status = "Using briefcase to bank"; + if (!Rs2Inventory.hasItem("Banker's briefcase")) { + status = "No briefcase found — walking to bank"; + if (!Rs2Bank.walkToBankAndUseBank()) return; + } else { + if (!Rs2Bank.isOpen()) { + Rs2Inventory.interact("Banker's briefcase", "Bank"); + sleepUntil(Rs2Bank::isOpen, 5000); + if (!Rs2Bank.isOpen()) return; + } + } + } else { + status = "Walking to bank"; + if (!Rs2Bank.isOpen()) { + if (!Rs2Bank.walkToBankAndUseBank()) return; + } + } + + status = "Depositing and withdrawing"; + + Rs2Bank.depositAll(); + sleep(150, 300); + + if (!Rs2Bank.hasItem(TINDERBOX_ID)) { + status = "No tinderbox in bank — stopping"; + Microbot.log("No tinderbox found in bank."); + shutdown(); + return; + } + + Rs2Bank.withdrawOne(TINDERBOX_ID); + sleepUntil(() -> Rs2Inventory.hasItem(TINDERBOX_NAME), 3000); + sleep(150, 300); + + if (!Rs2Bank.hasItem(activeLogType.getItemId())) { + status = "No " + activeLogType.getLogName() + " in bank — stopping"; + Microbot.log("No " + activeLogType.getLogName() + " found in bank."); + shutdown(); + return; + } + + Rs2Bank.withdrawAll(activeLogType.getItemId()); + sleepUntil(() -> Rs2Inventory.hasItem(activeLogType.getItemId()), 3000); + sleep(150, 300); + + Rs2Bank.closeBank(); + state = State.WALKING_BACK; + } + + private void handleWalkingBack(LeaguesFiremakingConfig config) { + status = "Walking back to fire area"; + + if (Rs2Player.getWorldLocation().distanceTo(startPosition) <= config.scanRadius()) { + state = State.SCANNING; + return; + } + + if (!Rs2Player.isMoving()) { + Rs2Walker.walkTo(startPosition, 3); + } + } + + @Override + public void shutdown() { + super.shutdown(); + Rs2Antiban.resetAntibanSettings(); + startPosition = null; + currentLine = null; + state = State.SCANNING; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/LogType.java b/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/LogType.java new file mode 100644 index 0000000000..007732bb01 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/LogType.java @@ -0,0 +1,44 @@ +package net.runelite.client.plugins.microbot.leaguesfiremaking; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.Skill; +import net.runelite.api.gameval.ItemID; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; + +@Getter +@RequiredArgsConstructor +public enum LogType { + LOGS("Logs", ItemID.LOGS, 1), + OAK("Oak logs", ItemID.OAK_LOGS, 15), + WILLOW("Willow logs", ItemID.WILLOW_LOGS, 30), + TEAK("Teak logs", ItemID.TEAK_LOGS, 35), + MAPLE("Maple logs", ItemID.MAPLE_LOGS, 45), + MAHOGANY("Mahogany logs", ItemID.MAHOGANY_LOGS, 50), + YEW("Yew logs", ItemID.YEW_LOGS, 60), + MAGIC("Magic logs", ItemID.MAGIC_LOGS, 75), + REDWOOD("Redwood logs", ItemID.REDWOOD_LOGS, 90); + + private final String logName; + private final int itemId; + private final int levelRequired; + + public boolean hasRequiredLevel() { + return Rs2Player.getSkillRequirement(Skill.FIREMAKING, levelRequired); + } + + public static LogType getBestForLevel() { + LogType best = LOGS; + for (LogType log : values()) { + if (log.hasRequiredLevel()) { + best = log; + } + } + return best; + } + + @Override + public String toString() { + return logName; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/State.java b/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/State.java new file mode 100644 index 0000000000..98e6caa0e2 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/State.java @@ -0,0 +1,9 @@ +package net.runelite.client.plugins.microbot.leaguesfiremaking; + +public enum State { + SCANNING, + WALKING_TO_LINE, + BURNING, + BANKING, + WALKING_BACK +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/TileScanner.java b/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/TileScanner.java new file mode 100644 index 0000000000..d41e0e531d --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguesfiremaking/TileScanner.java @@ -0,0 +1,125 @@ +package net.runelite.client.plugins.microbot.leaguesfiremaking; + +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.tile.Rs2Tile; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Slf4j +public class TileScanner { + + private static final int FIRE_ID = 26185; + private static final int FIRE_ID_ALT = 49927; + + public enum TileState { + OPEN, + FIRE, + BLOCKED + } + + public static TileState classifyTile(WorldPoint point, Set fireTiles, Set objectTiles) { + if (fireTiles.contains(point)) return TileState.FIRE; + if (objectTiles.contains(point)) return TileState.BLOCKED; + if (!Rs2Tile.isWalkable(point)) return TileState.BLOCKED; + return TileState.OPEN; + } + + public static List findFireLines(WorldPoint center, int radius) { + Set fireTiles = new HashSet<>(); + Set objectTiles = new HashSet<>(); + + Microbot.getRs2TileObjectCache().getStream() + .filter(obj -> obj.getWorldLocation().distanceTo(center) <= radius) + .forEach(obj -> { + int id = obj.getId(); + WorldPoint loc = obj.getWorldLocation(); + if (id == FIRE_ID || id == FIRE_ID_ALT) { + fireTiles.add(loc); + } else { + objectTiles.add(loc); + } + }); + + List lines = new ArrayList<>(); + int plane = center.getPlane(); + + for (int y = center.getY() - radius; y <= center.getY() + radius; y++) { + int runStartX = -1; + int runLength = 0; + + for (int x = center.getX() - radius; x <= center.getX() + radius; x++) { + WorldPoint point = new WorldPoint(x, y, plane); + TileState state = classifyTile(point, fireTiles, objectTiles); + + if (state == TileState.OPEN) { + if (runStartX == -1) { + runStartX = x; + } + runLength++; + } else { + if (runLength >= 5) { + lines.add(new FireLine( + new WorldPoint(runStartX, y, plane), + new WorldPoint(runStartX + runLength - 1, y, plane), + runLength + )); + } + runStartX = -1; + runLength = 0; + } + } + if (runLength >= 5) { + lines.add(new FireLine( + new WorldPoint(runStartX, y, plane), + new WorldPoint(runStartX + runLength - 1, y, plane), + runLength + )); + } + } + + // Score lines: balance length vs proximity to start position + // A nearby shorter line beats a far-away longer one + lines.sort(Comparator.comparingDouble((FireLine l) -> { + int distance = center.distanceTo(l.getEastEnd()); + // Penalize distance heavily: each tile away reduces effective score + return -(l.getLength() - distance * 0.5); + })); + + return lines; + } + + public static FireLine findBestLine(WorldPoint center, int radius) { + List lines = findFireLines(center, radius); + return lines.isEmpty() ? null : lines.get(0); + } + + public static boolean hasFire(WorldPoint point) { + return Microbot.getRs2TileObjectCache().getStream() + .anyMatch(obj -> obj.getWorldLocation().equals(point) + && (obj.getId() == FIRE_ID || obj.getId() == FIRE_ID_ALT)); + } + + public static Set buildFireSet(WorldPoint center, int radius) { + Set fireTiles = new HashSet<>(); + Microbot.getRs2TileObjectCache().getStream() + .filter(obj -> obj.getWorldLocation().distanceTo(center) <= radius) + .filter(obj -> obj.getId() == FIRE_ID || obj.getId() == FIRE_ID_ALT) + .forEach(obj -> fireTiles.add(obj.getWorldLocation())); + return fireTiles; + } + + public static Set buildObjectSet(WorldPoint center, int radius) { + Set objectTiles = new HashSet<>(); + Microbot.getRs2TileObjectCache().getStream() + .filter(obj -> obj.getWorldLocation().distanceTo(center) <= radius) + .filter(obj -> obj.getId() != FIRE_ID && obj.getId() != FIRE_ID_ALT) + .forEach(obj -> objectTiles.add(obj.getWorldLocation())); + return objectTiles; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastConfig.java b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastConfig.java new file mode 100644 index 0000000000..f387be7e69 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastConfig.java @@ -0,0 +1,203 @@ +package net.runelite.client.plugins.microbot.leftclickcast; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.ConfigSection; +import net.runelite.client.config.Keybind; + +@ConfigGroup("leftclickcast") +public interface LeftClickCastConfig extends Config +{ + @ConfigItem( + keyName = "enabled", + name = "Enabled", + description = "Replace the left-click Attack option on NPCs with Cast Spell", + position = 0 + ) + default boolean enabled() + { + return true; + } + + // Retained so existing stored config is not invalidated. Read once at startUp for migration into slot1Spell. + @ConfigItem( + keyName = "spell", + name = "Spell", + description = "Legacy single-spell setting — migrated into Slot 1 on startup.", + position = 1 + ) + default PertTargetSpell spell() + { + return PertTargetSpell.FIRE_STRIKE; + } + + @ConfigItem( + keyName = "requireMagicWeapon", + name = "Require magic weapon", + description = "When enabled, the Cast entry is only inserted while a staff, bladed staff, powered staff, or powered wand is equipped. Disable to cast regardless of equipped weapon.", + position = 2 + ) + default boolean requireMagicWeapon() + { + return true; + } + + @ConfigSection( + name = "Spell Slots", + description = "Up to five spells that can be bound to hotkeys for mid-fight swapping.", + position = 10 + ) + String spellSlotsSection = "spellSlots"; + + @ConfigSection( + name = "Hotkeys", + description = "Hotkey bindings that switch the active spell slot.", + position = 11 + ) + String hotkeysSection = "hotkeys"; + + @ConfigItem( + keyName = "slot1Spell", + name = "Slot 1 Spell", + description = "Spell for slot 1 (the startup-active slot).", + section = spellSlotsSection, + position = 0 + ) + default PertTargetSpell slot1Spell() + { + return PertTargetSpell.FIRE_STRIKE; + } + + @ConfigItem( + keyName = "slot2Spell", + name = "Slot 2 Spell", + description = "Spell for slot 2.", + section = spellSlotsSection, + position = 1 + ) + default PertTargetSpell slot2Spell() + { + return PertTargetSpell.FIRE_STRIKE; + } + + @ConfigItem( + keyName = "slot3Spell", + name = "Slot 3 Spell", + description = "Spell for slot 3.", + section = spellSlotsSection, + position = 2 + ) + default PertTargetSpell slot3Spell() + { + return PertTargetSpell.FIRE_STRIKE; + } + + @ConfigItem( + keyName = "slot4Spell", + name = "Slot 4 Spell", + description = "Spell for slot 4.", + section = spellSlotsSection, + position = 3 + ) + default PertTargetSpell slot4Spell() + { + return PertTargetSpell.FIRE_STRIKE; + } + + @ConfigItem( + keyName = "slot5Spell", + name = "Slot 5 Spell", + description = "Spell for slot 5.", + section = spellSlotsSection, + position = 4 + ) + default PertTargetSpell slot5Spell() + { + return PertTargetSpell.FIRE_STRIKE; + } + + @ConfigItem( + keyName = "enabledToggleHotkey", + name = "Enable/Disable Hotkey", + description = "Hotkey that toggles the plugin on and off.", + section = hotkeysSection, + position = 0 + ) + default Keybind enabledToggleHotkey() + { + return Keybind.NOT_SET; + } + + @ConfigItem( + keyName = "slot1Hotkey", + name = "Slot 1 Hotkey", + description = "Hotkey that activates slot 1.", + section = hotkeysSection, + position = 1 + ) + default Keybind slot1Hotkey() + { + return Keybind.NOT_SET; + } + + @ConfigItem( + keyName = "slot2Hotkey", + name = "Slot 2 Hotkey", + description = "Hotkey that activates slot 2.", + section = hotkeysSection, + position = 2 + ) + default Keybind slot2Hotkey() + { + return Keybind.NOT_SET; + } + + @ConfigItem( + keyName = "slot3Hotkey", + name = "Slot 3 Hotkey", + description = "Hotkey that activates slot 3.", + section = hotkeysSection, + position = 3 + ) + default Keybind slot3Hotkey() + { + return Keybind.NOT_SET; + } + + @ConfigItem( + keyName = "slot4Hotkey", + name = "Slot 4 Hotkey", + description = "Hotkey that activates slot 4.", + section = hotkeysSection, + position = 4 + ) + default Keybind slot4Hotkey() + { + return Keybind.NOT_SET; + } + + @ConfigItem( + keyName = "slot5Hotkey", + name = "Slot 5 Hotkey", + description = "Hotkey that activates slot 5.", + section = hotkeysSection, + position = 5 + ) + default Keybind slot5Hotkey() + { + return Keybind.NOT_SET; + } + + @ConfigItem( + keyName = "chatFeedback", + name = "Chat feedback", + description = "Post a game chat message on plugin events (active slot change, enable/disable toggle).", + section = hotkeysSection, + position = 6 + ) + default boolean chatFeedback() + { + return true; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java new file mode 100644 index 0000000000..0a0ed5f989 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java @@ -0,0 +1,391 @@ +package net.runelite.client.plugins.microbot.leftclickcast; + +import com.google.inject.Provides; +import java.util.concurrent.CompletableFuture; +import javax.inject.Inject; +import net.runelite.api.Actor; +import net.runelite.api.ChatMessageType; +import net.runelite.api.Client; +import net.runelite.api.EnumComposition; +import net.runelite.api.EnumID; +import net.runelite.api.MenuAction; +import net.runelite.api.MenuEntry; +import net.runelite.api.Menu; +import net.runelite.api.NPC; +import net.runelite.api.ParamID; +import net.runelite.api.Player; +import net.runelite.api.StructComposition; +import net.runelite.api.events.PostMenuSort; +import net.runelite.api.gameval.VarbitID; +import net.runelite.client.chat.ChatMessageManager; +import net.runelite.client.chat.QueuedMessage; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.config.Keybind; +import net.runelite.client.eventbus.EventBus; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.ConfigChanged; +import net.runelite.client.events.ExternalPluginsChanged; +import net.runelite.client.input.KeyManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.api.widgets.Widget; +import net.runelite.client.plugins.microbot.PluginConstants; +import net.runelite.client.plugins.microbot.util.magic.Rs2Magic; +import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; +import net.runelite.client.plugins.microbot.globval.enums.InterfaceTab; +import net.runelite.client.plugins.microbot.util.tabs.Rs2Tab; +import net.runelite.client.plugins.skillcalculator.skills.MagicAction; +import net.runelite.client.util.HotkeyListener; + +@PluginDescriptor( + name = PluginConstants.PERT + "Left-Click Cast", + description = "Replaces left-click Attack on NPCs with a preconfigured Cast Spell action.", + tags = {"magic", "combat", "spell", "left-click", "cast", "pvm", "pvp"}, + authors = {"Pert"}, + version = LeftClickCastPlugin.version, + minClientVersion = "2.0.13", + enabledByDefault = PluginConstants.DEFAULT_ENABLED, + isExternal = PluginConstants.IS_EXTERNAL +) +public class LeftClickCastPlugin extends Plugin +{ + static final String version = "1.3.0"; + + private static final int SLOT_COUNT = 5; + + @Inject + private Client client; + + @Inject + private LeftClickCastConfig config; + + @Inject + private KeyManager keyManager; + + @Inject + private ChatMessageManager chatMessageManager; + + @Inject + private ConfigManager configManager; + + @Inject + private EventBus eventBus; + + private volatile int activeSlot = 0; + + private final HotkeyListener[] hotkeyListeners = new HotkeyListener[SLOT_COUNT]; + + private HotkeyListener enabledToggleListener; + + @Provides + LeftClickCastConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(LeftClickCastConfig.class); + } + + @Override + protected void startUp() + { + // MicrobotConfigPanel renders boolean checkboxes from raw stored values; missing keys read as false + // even when the @ConfigItem default is true. Materialize defaults so the UI and the proxy agree. + configManager.setDefaultConfiguration(config, false); + activeSlot = 0; + for (int i = 0; i < SLOT_COUNT; i++) + { + final int slotIndex = i; + HotkeyListener listener = new HotkeyListener(() -> slotHotkeyFor(slotIndex)) + { + @Override + public void hotkeyPressed() + { + onSlotHotkey(slotIndex); + } + }; + hotkeyListeners[i] = listener; + keyManager.registerKeyListener(listener); + } + enabledToggleListener = new HotkeyListener(() -> config.enabledToggleHotkey()) + { + @Override + public void hotkeyPressed() + { + onEnabledToggleHotkey(); + } + }; + keyManager.registerKeyListener(enabledToggleListener); + migrateLegacySpellKey(); + } + + @Override + protected void shutDown() + { + for (int i = 0; i < hotkeyListeners.length; i++) + { + HotkeyListener listener = hotkeyListeners[i]; + if (listener != null) + { + keyManager.unregisterKeyListener(listener); + hotkeyListeners[i] = null; + } + } + if (enabledToggleListener != null) + { + keyManager.unregisterKeyListener(enabledToggleListener); + enabledToggleListener = null; + } + } + + @Subscribe + public void onPostMenuSort(PostMenuSort event) + { + // Don't mutate while the right-click menu is open — entries are frozen at open-time. + if (client.isMenuOpen()) + { + return; + } + if (!config.enabled()) + { + return; + } + PertTargetSpell spell = slotSpellFor(activeSlot); + if (spell == null) + { + return; + } + if (config.requireMagicWeapon() && !isMagicWeaponEquipped()) + { + return; + } + + Menu menu = client.getMenu(); + MenuEntry[] entries = menu.getMenuEntries(); + + // Find the top-most NPC or Player Attack entry (the game's already-sorted left-click candidate). + int attackIdx = -1; + Actor targetActor = null; + for (int i = entries.length - 1; i >= 0; i--) + { + MenuEntry e = entries[i]; + if (!"Attack".equals(e.getOption())) + { + continue; + } + if (e.getNpc() != null) + { + attackIdx = i; + targetActor = e.getNpc(); + break; + } + if (e.getPlayer() != null) + { + attackIdx = i; + targetActor = e.getPlayer(); + break; + } + } + if (attackIdx < 0) + { + return; + } + + MenuEntry attack = entries[attackIdx]; + final Actor dispatchTarget = targetActor; + final PertTargetSpell dispatchSpell = spell; + attack.setOption("Cast " + dispatchSpell.getDisplayName()); + attack.setType(MenuAction.RUNELITE); + attack.onClick(e -> castOnTargetFast(dispatchSpell, dispatchTarget)); + + // Move to the tail of the array — that slot is the left-click action in RuneLite's menu model. + if (attackIdx != entries.length - 1) + { + entries[attackIdx] = entries[entries.length - 1]; + entries[entries.length - 1] = attack; + menu.setMenuEntries(entries); + } + } + + private Keybind slotHotkeyFor(int index) + { + switch (index) + { + case 0: + return config.slot1Hotkey(); + case 1: + return config.slot2Hotkey(); + case 2: + return config.slot3Hotkey(); + case 3: + return config.slot4Hotkey(); + case 4: + return config.slot5Hotkey(); + default: + return Keybind.NOT_SET; + } + } + + private PertTargetSpell slotSpellFor(int index) + { + switch (index) + { + case 0: + return config.slot1Spell(); + case 1: + return config.slot2Spell(); + case 2: + return config.slot3Spell(); + case 3: + return config.slot4Spell(); + case 4: + return config.slot5Spell(); + default: + return config.slot1Spell(); + } + } + + private void onSlotHotkey(int index) + { + activeSlot = index; + if (config.chatFeedback()) + { + PertTargetSpell spell = slotSpellFor(index); + String display = spell != null ? spell.getDisplayName() : "(no spell)"; + chatMessageManager.queue(QueuedMessage.builder() + .type(ChatMessageType.GAMEMESSAGE) + .value("Left-Click Cast: now casting " + display) + .build()); + } + } + + private void onEnabledToggleHotkey() + { + boolean newValue = !config.enabled(); + configManager.setConfiguration("leftclickcast", "enabled", newValue); + // MicrobotConfigPanel doesn't subscribe to ConfigChanged for individual checkbox refresh, but it does + // rebuild on ExternalPluginsChanged. Posting that here makes the open config panel re-read this and + // every other config item, so the "Enabled" checkbox visually flips to match the keybind toggle. + eventBus.post(new ExternalPluginsChanged()); + // Chat feedback is emitted by onConfigChanged so checkbox clicks and hotkey presses share one path. + } + + @Subscribe + public void onConfigChanged(ConfigChanged event) + { + if (!"leftclickcast".equals(event.getGroup()) || !"enabled".equals(event.getKey())) + { + return; + } + if (!config.chatFeedback()) + { + return; + } + boolean enabled = "true".equals(event.getNewValue()); + chatMessageManager.queue(QueuedMessage.builder() + .type(ChatMessageType.GAMEMESSAGE) + .value("Left-Click Cast: " + (enabled ? "enabled" : "disabled")) + .build()); + } + + // Fast-path cast: fire two synchronous client.menuAction packets back-to-back so the server processes + // the spell selection and the spell-on-target dispatch on the same game tick. Falls back to + // Rs2Magic.castOn (tab switch + sleeps + clicks) if the spellbook widget isn't loaded yet or the + // spell isn't on the current spellbook. + private void castOnTargetFast(PertTargetSpell spell, Actor target) + { + if (target == null) + { + return; + } + MagicAction magic = spell.getMagicAction(); + Widget magicRoot = client.getWidget(218, 0); + boolean widgetReady = magicRoot != null && magicRoot.getStaticChildren() != null; + if (widgetReady) + { + try + { + int spellWidgetId = magic.getWidgetId(); + // Packet 1: select the spell client-side (WIDGET_TARGET on the spell widget). + client.menuAction(-1, spellWidgetId, MenuAction.WIDGET_TARGET, 1, -1, "Cast", magic.getName()); + // Packet 2: dispatch the selected spell on the target, same tick. + if (target instanceof NPC) + { + NPC npc = (NPC) target; + client.menuAction(0, 0, MenuAction.WIDGET_TARGET_ON_NPC, npc.getIndex(), -1, "Use", npc.getName()); + } + else if (target instanceof Player) + { + Player p = (Player) target; + client.menuAction(0, 0, MenuAction.WIDGET_TARGET_ON_PLAYER, p.getId(), -1, "Use", p.getName()); + } + return; + } + catch (Exception ignored) + { + // Spell not on the active spellbook (e.g., modern while on ancients) — fall through. + } + } + else + { + // Spellbook widget not yet loaded this session; nudge it open so the next click is fast. + Rs2Tab.switchTo(InterfaceTab.MAGIC); + } + // Slow fallback path. Rs2Magic.castOn uses sleepUntil which is a no-op on the client thread, so dispatch async. + final Actor dispatch = target instanceof NPC ? new Rs2NpcModel((NPC) target) : target; + CompletableFuture.runAsync(() -> Rs2Magic.castOn(magic, dispatch)); + } + + // Best-effort: if the user had previously set the legacy `spell` key to a non-default value and + // slot1Spell is still at its default, copy the legacy value into slot1Spell so existing configs keep working. + private void migrateLegacySpellKey() + { + try + { + PertTargetSpell legacy = configManager.getConfiguration( + "leftclickcast", "spell", PertTargetSpell.class); + if (legacy == null || legacy == PertTargetSpell.FIRE_STRIKE) + { + return; + } + if (config.slot1Spell() != PertTargetSpell.FIRE_STRIKE) + { + return; + } + configManager.setConfiguration("leftclickcast", "slot1Spell", legacy); + } + catch (Exception ignored) + { + // Migration is best-effort; ignore any deserialization or storage errors. + } + } + + // A weapon counts as "magic" when its style struct exposes Casting or Defensive Casting. + // Mirrors the core AttackStylesPlugin logic (EnumID.WEAPON_STYLES + ParamID.ATTACK_STYLE_NAME). + private boolean isMagicWeaponEquipped() + { + int weaponType = client.getVarbitValue(VarbitID.COMBAT_WEAPON_CATEGORY); + EnumComposition weaponStyles = client.getEnum(EnumID.WEAPON_STYLES); + if (weaponStyles == null) + { + return false; + } + int styleEnumId = weaponStyles.getIntValue(weaponType); + if (styleEnumId == -1) + { + return false; + } + int[] styleStructs = client.getEnum(styleEnumId).getIntVals(); + for (int structId : styleStructs) + { + StructComposition sc = client.getStructComposition(structId); + if (sc == null) + { + continue; + } + String name = sc.getStringValue(ParamID.ATTACK_STYLE_NAME); + if ("Casting".equalsIgnoreCase(name) || "Defensive Casting".equalsIgnoreCase(name)) + { + return true; + } + } + return false; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/PertTargetSpell.java b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/PertTargetSpell.java new file mode 100644 index 0000000000..521072613b --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/PertTargetSpell.java @@ -0,0 +1,96 @@ +package net.runelite.client.plugins.microbot.leftclickcast; + +import lombok.Getter; +import net.runelite.client.plugins.skillcalculator.skills.MagicAction; + +@Getter +public enum PertTargetSpell +{ + // Modern autocastable combat lines (Strike -> Surge) + WIND_STRIKE("Wind Strike", MagicAction.WIND_STRIKE), + WATER_STRIKE("Water Strike", MagicAction.WATER_STRIKE), + EARTH_STRIKE("Earth Strike", MagicAction.EARTH_STRIKE), + FIRE_STRIKE("Fire Strike", MagicAction.FIRE_STRIKE), + WIND_BOLT("Wind Bolt", MagicAction.WIND_BOLT), + WATER_BOLT("Water Bolt", MagicAction.WATER_BOLT), + EARTH_BOLT("Earth Bolt", MagicAction.EARTH_BOLT), + FIRE_BOLT("Fire Bolt", MagicAction.FIRE_BOLT), + WIND_BLAST("Wind Blast", MagicAction.WIND_BLAST), + WATER_BLAST("Water Blast", MagicAction.WATER_BLAST), + EARTH_BLAST("Earth Blast", MagicAction.EARTH_BLAST), + FIRE_BLAST("Fire Blast", MagicAction.FIRE_BLAST), + WIND_WAVE("Wind Wave", MagicAction.WIND_WAVE), + WATER_WAVE("Water Wave", MagicAction.WATER_WAVE), + EARTH_WAVE("Earth Wave", MagicAction.EARTH_WAVE), + FIRE_WAVE("Fire Wave", MagicAction.FIRE_WAVE), + WIND_SURGE("Wind Surge", MagicAction.WIND_SURGE), + WATER_SURGE("Water Surge", MagicAction.WATER_SURGE), + EARTH_SURGE("Earth Surge", MagicAction.EARTH_SURGE), + FIRE_SURGE("Fire Surge", MagicAction.FIRE_SURGE), + + // Ancient autocastable combat lines (Rush/Burst/Blitz/Barrage for each element) + SMOKE_RUSH("Smoke Rush", MagicAction.SMOKE_RUSH), + SHADOW_RUSH("Shadow Rush", MagicAction.SHADOW_RUSH), + BLOOD_RUSH("Blood Rush", MagicAction.BLOOD_RUSH), + ICE_RUSH("Ice Rush", MagicAction.ICE_RUSH), + SMOKE_BURST("Smoke Burst", MagicAction.SMOKE_BURST), + SHADOW_BURST("Shadow Burst", MagicAction.SHADOW_BURST), + BLOOD_BURST("Blood Burst", MagicAction.BLOOD_BURST), + ICE_BURST("Ice Burst", MagicAction.ICE_BURST), + SMOKE_BLITZ("Smoke Blitz", MagicAction.SMOKE_BLITZ), + SHADOW_BLITZ("Shadow Blitz", MagicAction.SHADOW_BLITZ), + BLOOD_BLITZ("Blood Blitz", MagicAction.BLOOD_BLITZ), + ICE_BLITZ("Ice Blitz", MagicAction.ICE_BLITZ), + SMOKE_BARRAGE("Smoke Barrage", MagicAction.SMOKE_BARRAGE), + SHADOW_BARRAGE("Shadow Barrage", MagicAction.SHADOW_BARRAGE), + BLOOD_BARRAGE("Blood Barrage", MagicAction.BLOOD_BARRAGE), + ICE_BARRAGE("Ice Barrage", MagicAction.ICE_BARRAGE), + + // Non-autocastable combat spells + CRUMBLE_UNDEAD("Crumble Undead", MagicAction.CRUMBLE_UNDEAD), + IBAN_BLAST("Iban Blast", MagicAction.IBAN_BLAST), + MAGIC_DART("Magic Dart", MagicAction.MAGIC_DART), + SARADOMIN_STRIKE("Saradomin Strike", MagicAction.SARADOMIN_STRIKE), + CLAWS_OF_GUTHIX("Claws of Guthix", MagicAction.CLAWS_OF_GUTHIX), + FLAMES_OF_ZAMORAK("Flames of Zamorak", MagicAction.FLAMES_OF_ZAMORAK), + + // Arceuus offensive target spells + GHOSTLY_GRASP("Ghostly Grasp", MagicAction.GHOSTLY_GRASP), + SKELETAL_GRASP("Skeletal Grasp", MagicAction.SKELETAL_GRASP), + UNDEAD_GRASP("Undead Grasp", MagicAction.UNDEAD_GRASP), + INFERIOR_DEMONBANE("Inferior Demonbane", MagicAction.INFERIOR_DEMONBANE), + SUPERIOR_DEMONBANE("Superior Demonbane", MagicAction.SUPERIOR_DEMONBANE), + DARK_DEMONBANE("Dark Demonbane", MagicAction.DARK_DEMONBANE), + LESSER_CORRUPTION("Lesser Corruption", MagicAction.LESSER_CORRUPTION), + GREATER_CORRUPTION("Greater Corruption", MagicAction.GREATER_CORRUPTION), + + // Utility target spells + CONFUSE("Confuse", MagicAction.CONFUSE), + WEAKEN("Weaken", MagicAction.WEAKEN), + CURSE("Curse", MagicAction.CURSE), + BIND("Bind", MagicAction.BIND), + SNARE("Snare", MagicAction.SNARE), + ENTANGLE("Entangle", MagicAction.ENTANGLE), + VULNERABILITY("Vulnerability", MagicAction.VULNERABILITY), + ENFEEBLE("Enfeeble", MagicAction.ENFEEBLE), + STUN("Stun", MagicAction.STUN), + TELE_BLOCK("Tele Block", MagicAction.TELE_BLOCK), + TELEOTHER_LUMBRIDGE("Tele Other Lumbridge", MagicAction.TELEOTHER_LUMBRIDGE), + TELEOTHER_FALADOR("Tele Other Falador", MagicAction.TELEOTHER_FALADOR), + TELEOTHER_CAMELOT("Tele Other Camelot", MagicAction.TELEOTHER_CAMELOT); + + private final String displayName; + private final MagicAction magicAction; + + PertTargetSpell(String displayName, MagicAction magicAction) + { + this.displayName = displayName; + this.magicAction = magicAction; + } + + @Override + public String toString() + { + return displayName; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/mining/AutoMiningConfig.java b/src/main/java/net/runelite/client/plugins/microbot/mining/AutoMiningConfig.java index 0c34142012..01f2345ecc 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/mining/AutoMiningConfig.java +++ b/src/main/java/net/runelite/client/plugins/microbot/mining/AutoMiningConfig.java @@ -86,6 +86,17 @@ default int maxPlayersInArea() { return 0; } + @ConfigItem( + keyName = "leagueMode", + name = "League mode (anti-AFK)", + description = "Periodically presses a key to reset the idle timer so you never get logged out", + position = 4, + section = generalSection + ) + default boolean leagueMode() { + return false; + } + @ConfigItem( keyName = "UseBank", name = "UseBank", diff --git a/src/main/java/net/runelite/client/plugins/microbot/mining/AutoMiningPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/mining/AutoMiningPlugin.java index 43f6db18b6..20384f2f44 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/mining/AutoMiningPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/mining/AutoMiningPlugin.java @@ -24,7 +24,7 @@ ) @Slf4j public class AutoMiningPlugin extends Plugin { - public static final String version = "1.0.11"; + public static final String version = "1.0.12"; @Inject private AutoMiningConfig config; @Provides diff --git a/src/main/java/net/runelite/client/plugins/microbot/mining/AutoMiningScript.java b/src/main/java/net/runelite/client/plugins/microbot/mining/AutoMiningScript.java index 3f3d861dde..8bd0eaad07 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/mining/AutoMiningScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/mining/AutoMiningScript.java @@ -19,12 +19,14 @@ 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.Rs2Inventory; +import net.runelite.client.plugins.microbot.util.keyboard.Rs2Keyboard; import net.runelite.client.plugins.microbot.util.math.Rs2Random; import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.microbot.util.security.Login; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; import java.util.ArrayList; +import java.awt.event.KeyEvent; import java.util.Arrays; import java.util.Comparator; import java.util.List; @@ -54,11 +56,20 @@ public boolean run(AutoMiningConfig config) { try { if (!super.run()) return; if (!Microbot.isLoggedIn()) return; + if (config.leagueMode() && Rs2Player.checkIdleLogout(Rs2Random.between(500, 1500))) { + int[] arrowKeys = { KeyEvent.VK_LEFT, KeyEvent.VK_RIGHT, KeyEvent.VK_UP, KeyEvent.VK_DOWN }; + Rs2Keyboard.keyPress(arrowKeys[Rs2Random.between(0, arrowKeys.length - 1)]); + } if (Rs2AntibanSettings.actionCooldownActive) return; if (initialPlayerLocation == null) { initialPlayerLocation = Rs2Player.getWorldLocation(); } + // Skip cycle if we don't have a valid location + if (initialPlayerLocation == null) { + return; + } + updateActiveRock(config); if (config.progressiveMode() && ensureProgressiveLocation(config)) { @@ -120,6 +131,16 @@ public boolean run(AutoMiningConfig config) { return; } + // Check if we're too far from mining location - walk back first + if (initialPlayerLocation != null) { + int distanceFromStart = Rs2Player.getWorldLocation().distanceTo(initialPlayerLocation); + if (distanceFromStart > config.distanceToStray()) { + Microbot.status = "Walking back to mining location..."; + Rs2Walker.walkTo(initialPlayerLocation, config.distanceToStray()); + return; + } + } + GameObject rock = Rs2GameObject.findReachableObject(activeRock.getName(), true, config.distanceToStray(), initialPlayerLocation); if (rock != null) { @@ -252,7 +273,9 @@ private boolean ensureProgressiveLocation(AutoMiningConfig config) { WorldPoint targetPoint = activeLocation.getWorldPoint(); - if (initialPlayerLocation == null || !initialPlayerLocation.equals(targetPoint)) { + // Only update initialPlayerLocation if it's null + // Don't update just because player is far away (e.g., at bank) - that breaks return-to-location + if (initialPlayerLocation == null) { initialPlayerLocation = targetPoint; } diff --git a/src/main/java/net/runelite/client/plugins/microbot/mining/data/LocationOption.java b/src/main/java/net/runelite/client/plugins/microbot/mining/data/LocationOption.java index 65b9342fb6..307e29e558 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/mining/data/LocationOption.java +++ b/src/main/java/net/runelite/client/plugins/microbot/mining/data/LocationOption.java @@ -131,8 +131,7 @@ public boolean hasRequirements() { // bolt ammo slot ? when the ids is any ammo if (numberOfItems+numberOfItemsInPouch +numberOfItemsInBank< requiredAmount) { - log.warn("Missing required item: {} x{} (have {})", itemId, requiredAmount, numberOfItems); - Microbot.log("Missing required item: " + itemId + " x" + requiredAmount + " (have " + numberOfItems + ")"); + log.debug("Missing required item: {} x{} (have {})", itemId, requiredAmount, numberOfItems); return false; } return true; diff --git a/src/main/java/net/runelite/client/plugins/microbot/mining/data/Rocks.java b/src/main/java/net/runelite/client/plugins/microbot/mining/data/Rocks.java index dd985e9f41..ad4e7e3b3f 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/mining/data/Rocks.java +++ b/src/main/java/net/runelite/client/plugins/microbot/mining/data/Rocks.java @@ -22,7 +22,8 @@ public enum Rocks { URT_SALT("Urt salt rocks", 72), EFH_SALT("Efh salt rocks", 72), TE_SALT("Te salt rocks", 72), - RUNITE("runite rocks", 85); + RUNITE("runite rocks", 85), + NONE("None", 1); private final String name; private final int miningLevel; diff --git a/src/main/resources/net/runelite/client/plugins/microbot/leftclickcast/docs/README.md b/src/main/resources/net/runelite/client/plugins/microbot/leftclickcast/docs/README.md new file mode 100644 index 0000000000..be60ed19ae --- /dev/null +++ b/src/main/resources/net/runelite/client/plugins/microbot/leftclickcast/docs/README.md @@ -0,0 +1,54 @@ +# Left-Click Cast + +Replaces the left-click **Attack** option on attackable NPCs and on players (wilderness / PvP) with a preconfigured **Cast Spell** action. The plugin stays invisible when you swap to a melee or ranged weapon, so leaving it enabled is safe. + +## How it works + +When an "Attack" menu entry is added for an NPC, the plugin inserts a new menu entry for the selected spell and places it above Attack, making the spell the left-click action. + +All casting is dispatched through the Microbot client's existing `Rs2Magic.castOn(MagicAction, Actor)` — the plugin does not re-implement rune checks, spellbook switching, or targeting. + +## Configuration + +| Option | Default | Description | +| --- | --- | --- | +| **Enabled** | `true` | Master switch. When off, no menu entries are inserted. | +| **Spell** | `Fire Strike` | Legacy single-spell setting. On startup the plugin migrates this into **Slot 1 Spell** if Slot 1 is still at its default — keeps existing configs working without any manual action. | +| **Require magic weapon** | `true` | When on, the Cast entry only shows while a staff, bladed staff, powered staff, or powered wand is equipped (detected via varbit `EQUIPPED_WEAPON_TYPE`). Disable to cast regardless of weapon. | + +### Spell slots and hotkeys + +The plugin exposes five independently configurable spell slots, grouped under two sections in the config panel: + +| Section | Options | +| --- | --- | +| **Spell Slots** | `Slot 1 Spell` … `Slot 5 Spell` — each picks any spell from the full supported-spells dropdown. All five default to `Fire Strike`. | +| **Hotkeys** | `Slot 1 Hotkey` … `Slot 5 Hotkey` — RuneLite-standard hotkey pickers, all unbound by default. `Chat feedback on slot change` — toggles the chat message posted when a hotkey switches slots (default on). | + +**How slot switching works:** + +- **Slot 1 is always active at startup.** Enabling the plugin (or restarting the client) resets the active slot to Slot 1. The active slot is runtime-only — it is never written to config. +- **Press a bound slot hotkey** (while the game window is focused and no text field is active) to make that slot the active slot. The next menu-sort uses the new slot's spell. +- **Unbound hotkeys are inert.** A slot whose hotkey is `Not set` cannot be activated by keypress. RuneLite's hotkey plumbing suppresses hotkeys while you're typing in a chat or search widget, so hotkey letters won't accidentally swap slots during text entry. +- **Slot 1 needs no hotkey.** Because it's the startup default, leave its hotkey unbound unless you want to explicitly return to it from another slot. +- **Chat feedback** (when enabled) prints `Left-Click Cast: now casting ` on every slot change. Toggle it off if it's noisy during combat rotations. + +## Limitations + +- **No rune auto-management.** If you run out of runes, the cast fails cleanly and you see the normal "You do not have enough ..." chat message. +- **No auto-spellbook switching.** If the selected spell is not on your current spellbook, the cast fails silently. Switch spellbooks manually. +- **The dropdown is the source of truth.** Spells not listed in the dropdown are not supported by this plugin. +- **Staff-only default.** With `Require magic weapon` enabled (default), non-magic weapon types produce normal Attack behavior. Disable the toggle if you want to cast from melee/ranged weapons as well. +- **Cooperative menu composition.** If another plugin inserts menu entries after this one on the same tick, its entry becomes the top entry instead — a known limitation of RuneLite's menu model. + +## Supported spells + +**Modern combat (Strike → Surge):** Wind Strike, Water Strike, Earth Strike, Fire Strike, Wind Bolt, Water Bolt, Earth Bolt, Fire Bolt, Wind Blast, Water Blast, Earth Blast, Fire Blast, Wind Wave, Water Wave, Earth Wave, Fire Wave, Wind Surge, Water Surge, Earth Surge, Fire Surge. + +**Ancient combat:** Smoke / Shadow / Blood / Ice — Rush, Burst, Blitz, Barrage. + +**Non-autocastable combat:** Crumble Undead, Iban Blast, Magic Dart, Saradomin Strike, Claws of Guthix, Flames of Zamorak. + +**Arceuus offensive:** Ghostly Grasp, Skeletal Grasp, Undead Grasp, Inferior Demonbane, Superior Demonbane, Dark Demonbane, Lesser Corruption, Greater Corruption. + +**Utility target spells:** Confuse, Weaken, Curse, Bind, Snare, Entangle, Vulnerability, Enfeeble, Stun, Tele Block, Tele Other (Lumbridge / Falador / Camelot). diff --git a/src/test/java/net/runelite/client/Microbot.java b/src/test/java/net/runelite/client/Microbot.java index 0bbce8830b..8933287b08 100644 --- a/src/test/java/net/runelite/client/Microbot.java +++ b/src/test/java/net/runelite/client/Microbot.java @@ -10,6 +10,7 @@ import net.runelite.client.plugins.microbot.astralrc.AstralRunesPlugin; import net.runelite.client.plugins.microbot.autofishing.AutoFishingPlugin; import net.runelite.client.plugins.microbot.example.ExamplePlugin; +import net.runelite.client.plugins.microbot.leftclickcast.LeftClickCastPlugin; import net.runelite.client.plugins.microbot.sailing.MSailingPlugin; import net.runelite.client.plugins.microbot.thieving.ThievingPlugin; import net.runelite.client.plugins.microbot.woodcutting.AutoWoodcuttingPlugin; @@ -20,7 +21,8 @@ public class Microbot private static final Class[] debugPlugins = { AIOFighterPlugin.class, - AgentServerPlugin.class + AgentServerPlugin.class, + LeftClickCastPlugin.class }; public static void main(String[] args) throws Exception