From 10b79402fc8a138051b3be959f2a9d95f93fdb02 Mon Sep 17 00:00:00 2001 From: chsami Date: Wed, 14 Oct 2026 17:08:06 +0200 Subject: [PATCH 01/38] fix(revkiller): avoid off-thread Actor.getName and empty equipment slot NPEs (#613) isPkerAround compared the raw interacting Actor by name, which calls Actor.getName on the script thread and throws "must be called on client thread". Compare the interacting actor against the client's local player by identity instead; the previous equals check against the Rs2PlayerModel wrapper could never match. The Bankfortrip sleepUntil predicates dereferenced Rs2Equipment.get for the ring and ammo slots, which returns null until the equip lands, so each poll threw an NPE. Null-check them via small helpers. Bump revKillerPlugin to 2.0.12. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../microbot/revkiller/revKillerPlugin.java | 2 +- .../microbot/revkiller/revKillerScript.java | 22 ++-- .../revkiller/RevKillerScriptSafetyTest.java | 116 ++++++++++++++++++ 3 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/revkiller/RevKillerScriptSafetyTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/revkiller/revKillerPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/revkiller/revKillerPlugin.java index 617f578efa..e4a5af7415 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/revkiller/revKillerPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/revkiller/revKillerPlugin.java @@ -32,7 +32,7 @@ ) @Slf4j public class revKillerPlugin extends Plugin { - public static final String version = "2.0.11"; + public static final String version = "2.0.12"; @Inject private net.runelite.client.plugins.microbot.revkiller.revKillerConfig config; @Provides diff --git a/src/main/java/net/runelite/client/plugins/microbot/revkiller/revKillerScript.java b/src/main/java/net/runelite/client/plugins/microbot/revkiller/revKillerScript.java index 058841d316..49f83e62d3 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/revkiller/revKillerScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/revkiller/revKillerScript.java @@ -1154,7 +1154,7 @@ public void Bankfortrip(){ } if(Rs2Inventory.contains(ItemID.RING_OF_DUELING8)){ if(Rs2Inventory.interact(ItemID.RING_OF_DUELING8, "Wear")){ - sleepUntil(()-> Rs2Equipment.get(EquipmentInventorySlot.RING).getName().contains("dueling"), generateRandomNumber(5000,15000)); + sleepUntil(()-> isWearingDuelingRing(), generateRandomNumber(5000,15000)); } } } @@ -1234,7 +1234,7 @@ public void Bankfortrip(){ } if(Rs2Inventory.contains(selectedArrow)){ if(Rs2Inventory.interact(selectedArrow, "Wield")){ - sleepUntil(()-> Rs2Equipment.get(EquipmentInventorySlot.AMMO).getQuantity() > 50, generateRandomNumber(5000,15000)); + sleepUntil(()-> hasEquippedAmmoAbove(50), generateRandomNumber(5000,15000)); } } } @@ -1534,12 +1534,8 @@ public boolean isPkerAround(){ } - if ((player.getInteracting() != null) && (player.getInteracting().equals(Rs2Player.getLocalPlayer()))) { - Microbot.log(player.getName() + " is attacking us"); - return true; - } - - if((player.getInteracting() != null && player.getInteracting().getName() != null) && player.getInteracting().getName().equals(Rs2Player.getLocalPlayer().getName())){ + Actor interacting = player.getInteracting(); + if (interacting != null && interacting == Microbot.getClient().getLocalPlayer()) { Microbot.log(player.getName() + " is attacking us"); return true; } @@ -1547,4 +1543,14 @@ public boolean isPkerAround(){ } return false; } + + static boolean isWearingDuelingRing() { + Rs2ItemModel ring = Rs2Equipment.get(EquipmentInventorySlot.RING); + return ring != null && ring.getName() != null && ring.getName().contains("dueling"); + } + + static boolean hasEquippedAmmoAbove(int quantity) { + Rs2ItemModel ammo = Rs2Equipment.get(EquipmentInventorySlot.AMMO); + return ammo != null && ammo.getQuantity() > quantity; + } } diff --git a/src/test/java/net/runelite/client/plugins/microbot/revkiller/RevKillerScriptSafetyTest.java b/src/test/java/net/runelite/client/plugins/microbot/revkiller/RevKillerScriptSafetyTest.java new file mode 100644 index 0000000000..6d49a43e34 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/revkiller/RevKillerScriptSafetyTest.java @@ -0,0 +1,116 @@ +package net.runelite.client.plugins.microbot.revkiller; + +import net.runelite.api.Actor; +import net.runelite.api.Client; +import net.runelite.api.EquipmentInventorySlot; +import net.runelite.api.Player; +import net.runelite.api.kit.KitType; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; +import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.player.Rs2PlayerModel; +import net.runelite.client.plugins.microbot.util.player.Rs2Pvp; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class RevKillerScriptSafetyTest { + + @Test + void emptyRingSlotIsNotADuelingRing() { + try (MockedStatic equipment = mockStatic(Rs2Equipment.class)) { + equipment.when(() -> Rs2Equipment.get(EquipmentInventorySlot.RING)).thenReturn(null); + + assertFalse(revKillerScript.isWearingDuelingRing()); + } + } + + @Test + void equippedDuelingRingIsDetected() { + Rs2ItemModel ring = mock(Rs2ItemModel.class); + when(ring.getName()).thenReturn("Ring of dueling(8)"); + try (MockedStatic equipment = mockStatic(Rs2Equipment.class)) { + equipment.when(() -> Rs2Equipment.get(EquipmentInventorySlot.RING)).thenReturn(ring); + + assertTrue(revKillerScript.isWearingDuelingRing()); + } + } + + @Test + void emptyAmmoSlotHasNoAmmo() { + try (MockedStatic equipment = mockStatic(Rs2Equipment.class)) { + equipment.when(() -> Rs2Equipment.get(EquipmentInventorySlot.AMMO)).thenReturn(null); + + assertFalse(revKillerScript.hasEquippedAmmoAbove(50)); + } + } + + @Test + void ammoQuantityIsComparedAgainstThreshold() { + Rs2ItemModel ammo = mock(Rs2ItemModel.class); + when(ammo.getQuantity()).thenReturn(51); + try (MockedStatic equipment = mockStatic(Rs2Equipment.class)) { + equipment.when(() -> Rs2Equipment.get(EquipmentInventorySlot.AMMO)).thenReturn(ammo); + + assertTrue(revKillerScript.hasEquippedAmmoAbove(50)); + assertFalse(revKillerScript.hasEquippedAmmoAbove(51)); + } + } + + @Test + void playerAttackingUsIsAPkerWithoutReadingRawActorName() { + Player local = mock(Player.class); + Rs2PlayerModel attacker = attackerInteractingWith(local); + + assertTrue(isPkerAround(local, attacker)); + verify(local, never()).getName(); + } + + @Test + void playerAttackingSomeoneElseIsNotAPker() { + Player local = mock(Player.class); + Actor other = mock(Actor.class); + Rs2PlayerModel attacker = attackerInteractingWith(other); + + assertFalse(isPkerAround(local, attacker)); + verify(other, never()).getName(); + } + + private static Rs2PlayerModel attackerInteractingWith(Actor target) { + Rs2PlayerModel attacker = mock(Rs2PlayerModel.class); + when(attacker.getName()).thenReturn("Attacker"); + when(attacker.getInteracting()).thenReturn(target); + return attacker; + } + + private static boolean isPkerAround(Player local, Rs2PlayerModel attacker) { + Client client = mock(Client.class); + when(client.getLocalPlayer()).thenReturn(local); + Map gear = new EnumMap<>(KitType.class); + gear.put(KitType.WEAPON, "Rune scimitar"); + gear.put(KitType.TORSO, "Rune platebody"); + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic pvp = mockStatic(Rs2Pvp.class); + MockedStatic player = mockStatic(Rs2Player.class)) { + microbot.when(Microbot::isLoggedIn).thenReturn(true); + microbot.when(Microbot::getClient).thenReturn(client); + pvp.when(Rs2Pvp::isInWilderness).thenReturn(true); + player.when(Rs2Player::getPlayersInCombatLevelRange).thenReturn(List.of(attacker)); + player.when(() -> Rs2Player.getPlayerEquipmentNames(attacker)).thenReturn(gear); + + return new revKillerScript().isPkerAround(); + } + } +} From 47badaabdab206503c042f9d49d36c5327d29e6c Mon Sep 17 00:00:00 2001 From: chsami Date: Wed, 14 Oct 2026 17:16:43 +0200 Subject: [PATCH 02/38] fix(motherloadmine): fall back to mining intensity when antiban activity is null (1.9.9) (#614) Rs2Antiban.getActivity() is null when overwriteScriptSettings makes applyMiningSetup() return early or after another script resets antiban settings, so every MINING tick threw an NPE and aborted the workflow. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../motherloadmine/MotherloadMinePlugin.java | 2 +- .../motherloadmine/MotherloadMineScript.java | 9 ++++++++- .../MotherloadMineScriptTest.java | 18 ++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/motherloadmine/MotherloadMineScriptTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/motherloadmine/MotherloadMinePlugin.java b/src/main/java/net/runelite/client/plugins/microbot/motherloadmine/MotherloadMinePlugin.java index d4e6c70244..2c563e8711 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/motherloadmine/MotherloadMinePlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/motherloadmine/MotherloadMinePlugin.java @@ -29,7 +29,7 @@ ) public class MotherloadMinePlugin extends Plugin { - static final String version = "1.9.8"; + static final String version = "1.9.9"; @Inject private MotherloadMineConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/motherloadmine/MotherloadMineScript.java b/src/main/java/net/runelite/client/plugins/microbot/motherloadmine/MotherloadMineScript.java index d817bc5add..828a7af356 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/motherloadmine/MotherloadMineScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/motherloadmine/MotherloadMineScript.java @@ -39,6 +39,7 @@ import net.runelite.client.plugins.microbot.util.antiban.AntibanPlugin; 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.antiban.enums.Activity; import net.runelite.client.plugins.microbot.util.antiban.enums.ActivityIntensity; import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; import net.runelite.client.plugins.microbot.util.camera.Rs2Camera; @@ -158,7 +159,7 @@ private void executeTask() case IDLE: break; case MINING: - Rs2Antiban.setActivityIntensity(Rs2Antiban.getActivity().getActivityIntensity()); + Rs2Antiban.setActivityIntensity(miningIntensity(Rs2Antiban.getActivity())); handleMining(); break; case EMPTY_SACK: @@ -180,6 +181,12 @@ private void executeTask() break; } } + + static ActivityIntensity miningIntensity(Activity activity) + { + return (activity != null ? activity : Activity.GENERAL_MINING).getActivityIntensity(); + } + private String[] SPEC_PICKAXES = {"dragon pickaxe", "crystal pickaxe", "infernal pickaxe"}; private void handlePickaxeSpec() { diff --git a/src/test/java/net/runelite/client/plugins/microbot/motherloadmine/MotherloadMineScriptTest.java b/src/test/java/net/runelite/client/plugins/microbot/motherloadmine/MotherloadMineScriptTest.java new file mode 100644 index 0000000000..0e2a6a741d --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/motherloadmine/MotherloadMineScriptTest.java @@ -0,0 +1,18 @@ +package net.runelite.client.plugins.microbot.motherloadmine; + +import net.runelite.client.plugins.microbot.util.antiban.enums.Activity; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public final class MotherloadMineScriptTest { + @Test + void missingAntibanActivityFallsBackToGeneralMining() { + assertEquals(Activity.GENERAL_MINING.getActivityIntensity(), MotherloadMineScript.miningIntensity(null)); + } + + @Test + void usesConfiguredAntibanActivityIntensity() { + assertEquals(Activity.GENERAL_COMBAT.getActivityIntensity(), MotherloadMineScript.miningIntensity(Activity.GENERAL_COMBAT)); + } +} From ad0992d0583b53c645a16ae3c52f34d14f0442d4 Mon Sep 17 00:00:00 2001 From: chsami Date: Wed, 14 Oct 2026 17:27:18 +0200 Subject: [PATCH 03/38] fix(nmz): guard prayer potion threshold against zero real prayer level (2.4.3) (#615) PrayerPotionScript.run(NmzConfig) divided boosted prayer by the real prayer level with no logged-in check, throwing ArithmeticException every 600 ms while logged out or loading. Route both loops through a helper that skips drinking when the real level is not yet known, and add the missing login guard. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../plugins/microbot/nmz/NmzPlugin.java | 2 +- .../microbot/nmz/PrayerPotionScript.java | 10 +++++-- .../microbot/nmz/PrayerPotionScriptTest.java | 26 +++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/nmz/PrayerPotionScriptTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/nmz/NmzPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/nmz/NmzPlugin.java index 28f0aa4273..c9ed30f2c6 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/nmz/NmzPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/nmz/NmzPlugin.java @@ -31,7 +31,7 @@ ) @Slf4j public class NmzPlugin extends Plugin { - final static String version = "2.4.2"; + final static String version = "2.4.3"; @Inject private NmzConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/nmz/PrayerPotionScript.java b/src/main/java/net/runelite/client/plugins/microbot/nmz/PrayerPotionScript.java index 037b55f042..ee8503c116 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/nmz/PrayerPotionScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/nmz/PrayerPotionScript.java @@ -16,7 +16,7 @@ public boolean run() { try { if (!Microbot.isLoggedIn()) return; if (!super.run()) return; - if ((Microbot.getClient().getBoostedSkillLevel(Skill.PRAYER) * 100) / Microbot.getClient().getRealSkillLevel(Skill.PRAYER) > Rs2Random.between(25, 30)) + if (isPrayerAboveThreshold(Microbot.getClient().getBoostedSkillLevel(Skill.PRAYER), Microbot.getClient().getRealSkillLevel(Skill.PRAYER), Rs2Random.between(25, 30))) return; List potions = Microbot.getClientThread().runOnClientThreadOptional(Rs2Inventory::getPotions).orElse(null); if (potions == null || potions.isEmpty()) { @@ -40,9 +40,10 @@ public boolean run() { public boolean run(NmzConfig config) { mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { try { + if (!Microbot.isLoggedIn()) return; if (!super.run()) return; if (!config.togglePrayerPotions()) return; - if ((Microbot.getClient().getBoostedSkillLevel(Skill.PRAYER) * 100) / Microbot.getClient().getRealSkillLevel(Skill.PRAYER) > Rs2Random.between(25, 30)) + if (isPrayerAboveThreshold(Microbot.getClient().getBoostedSkillLevel(Skill.PRAYER), Microbot.getClient().getRealSkillLevel(Skill.PRAYER), Rs2Random.between(25, 30))) return; List potions = Microbot.getClientThread().runOnClientThreadOptional(Rs2Inventory::getPotions).orElse(null); if (potions == null || potions.isEmpty()) { @@ -62,4 +63,9 @@ public boolean run(NmzConfig config) { }, 0, 600, TimeUnit.MILLISECONDS); return true; } + + static boolean isPrayerAboveThreshold(int boostedLevel, int realLevel, int thresholdPercent) { + if (realLevel <= 0) return true; + return (boostedLevel * 100) / realLevel > thresholdPercent; + } } diff --git a/src/test/java/net/runelite/client/plugins/microbot/nmz/PrayerPotionScriptTest.java b/src/test/java/net/runelite/client/plugins/microbot/nmz/PrayerPotionScriptTest.java new file mode 100644 index 0000000000..6d5c41d86b --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/nmz/PrayerPotionScriptTest.java @@ -0,0 +1,26 @@ +package net.runelite.client.plugins.microbot.nmz; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PrayerPotionScriptTest { + + @Test + void zeroRealLevelSkipsDrinkingInsteadOfDividingByZero() { + assertTrue(PrayerPotionScript.isPrayerAboveThreshold(0, 0, 25)); + assertTrue(PrayerPotionScript.isPrayerAboveThreshold(10, 0, 30)); + } + + @Test + void prayerAboveThresholdSkipsDrinking() { + assertTrue(PrayerPotionScript.isPrayerAboveThreshold(50, 70, 30)); + } + + @Test + void prayerAtOrBelowThresholdAllowsDrinking() { + assertFalse(PrayerPotionScript.isPrayerAboveThreshold(21, 70, 30)); + assertFalse(PrayerPotionScript.isPrayerAboveThreshold(0, 70, 25)); + } +} From 87754de8a5bbde4b90b797203c8a7ed988529608 Mon Sep 17 00:00:00 2001 From: chsami Date: Wed, 14 Oct 2026 19:34:37 +0200 Subject: [PATCH 04/38] fix(aiofighter): treat missing slayer task as no protective equipment needed (2.2.4) (#616) Rs2Slayer.getSlayerTaskProtectiveEquipment() returns null when there is no slayer task, which reached isBypassedBySlayerHelmet's switch and threw an NPE on every BankerScript loop in slayer mode. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../microbot/aiofighter/AIOFighterPlugin.java | 2 +- .../aiofighter/bank/BankerScript.java | 11 ++++- .../BankerScriptProtectiveEquipmentTest.java | 40 +++++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/aiofighter/bank/BankerScriptProtectiveEquipmentTest.java 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 34411b59be..9dc3d3bbd8 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 @@ -62,7 +62,7 @@ ) @Slf4j public class AIOFighterPlugin extends Plugin { - public static final String version = "2.2.3"; + public static final String version = "2.2.4"; 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/bank/BankerScript.java b/src/main/java/net/runelite/client/plugins/microbot/aiofighter/bank/BankerScript.java index 5f4fe45ed8..7296abcd60 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/aiofighter/bank/BankerScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/aiofighter/bank/BankerScript.java @@ -323,7 +323,7 @@ public boolean hasSlayerItems() { public boolean hasProtectiveSlayerEquipment() { String needed = Rs2Slayer.getSlayerTaskProtectiveEquipment(); - if ("None".equals(needed)) { + if (!requiresProtectiveEquipment(needed)) { return true; } @@ -349,7 +349,14 @@ public boolean hasProtectiveSlayerEquipment() { return result; } - private boolean isBypassedBySlayerHelmet(String item) { + static boolean requiresProtectiveEquipment(String needed) { + return needed != null && !"None".equals(needed); + } + + static boolean isBypassedBySlayerHelmet(String item) { + if (item == null) { + return false; + } switch (item) { case "Earmuffs": case "Facemask": diff --git a/src/test/java/net/runelite/client/plugins/microbot/aiofighter/bank/BankerScriptProtectiveEquipmentTest.java b/src/test/java/net/runelite/client/plugins/microbot/aiofighter/bank/BankerScriptProtectiveEquipmentTest.java new file mode 100644 index 0000000000..ebb05c456d --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/aiofighter/bank/BankerScriptProtectiveEquipmentTest.java @@ -0,0 +1,40 @@ +package net.runelite.client.plugins.microbot.aiofighter.bank; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public final class BankerScriptProtectiveEquipmentTest { + + @Test + void noSlayerTaskRequiresNoProtectiveEquipment() { + assertFalse(BankerScript.requiresProtectiveEquipment(null)); + } + + @Test + void taskWithoutProtectiveEquipmentRequiresNone() { + assertFalse(BankerScript.requiresProtectiveEquipment("None")); + } + + @Test + void taskWithProtectiveEquipmentRequiresIt() { + assertTrue(BankerScript.requiresProtectiveEquipment("Earmuffs")); + assertTrue(BankerScript.requiresProtectiveEquipment("Rock hammer")); + } + + @Test + void slayerHelmetBypassHandlesNullItem() { + assertDoesNotThrow(() -> BankerScript.isBypassedBySlayerHelmet(null)); + assertFalse(BankerScript.isBypassedBySlayerHelmet(null)); + } + + @Test + void slayerHelmetBypassesHeadProtection() { + assertTrue(BankerScript.isBypassedBySlayerHelmet("Earmuffs")); + assertTrue(BankerScript.isBypassedBySlayerHelmet("Nose peg")); + assertFalse(BankerScript.isBypassedBySlayerHelmet("Rock hammer")); + assertFalse(BankerScript.isBypassedBySlayerHelmet("Mirror shield")); + } +} From 078287157e0247f929c8fe52021cb9851c4dd05e Mon Sep 17 00:00:00 2001 From: chsami Date: Wed, 14 Oct 2026 21:02:54 +0200 Subject: [PATCH 05/38] fix(stallthieving): ignore non-numeric always-drop entries instead of throwing (1.0.1) (#617) BankInventoryStrategy parsed the free-text "Always Drop Item ids" config with Integer::parseInt, so any item name in the list threw NumberFormatException on every loop tick with a full inventory and the script never banked or dropped. Trim each entry and keep only numeric ids. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../thievingstalls/StallThievingPlugin.java | 2 +- .../model/BankInventoryStrategy.java | 9 +++- .../model/BankInventoryStrategyTest.java | 46 +++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/thievingstalls/model/BankInventoryStrategyTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/thievingstalls/StallThievingPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/thievingstalls/StallThievingPlugin.java index 477445194d..d2ad7eab1b 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/thievingstalls/StallThievingPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/thievingstalls/StallThievingPlugin.java @@ -25,7 +25,7 @@ @Slf4j public class StallThievingPlugin extends Plugin { - public final static String version = "1.0.0"; + public final static String version = "1.0.1"; @Inject private StallThievingConfig config; @Provides diff --git a/src/main/java/net/runelite/client/plugins/microbot/thievingstalls/model/BankInventoryStrategy.java b/src/main/java/net/runelite/client/plugins/microbot/thievingstalls/model/BankInventoryStrategy.java index 1f1ba749a8..418cef5e41 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/thievingstalls/model/BankInventoryStrategy.java +++ b/src/main/java/net/runelite/client/plugins/microbot/thievingstalls/model/BankInventoryStrategy.java @@ -41,10 +41,15 @@ public void execute(IStallThievingSpot stallThievingSpot) { private int[] getItemIdsToAlwaysDrop() { - if (config.alwaysDrop().isEmpty()) + final String alwaysDrop = config.alwaysDrop(); + if (alwaysDrop == null || alwaysDrop.isBlank()) { return new int[0]; } - return Arrays.stream(config.alwaysDrop().split("\\s*,\\s*")).mapToInt(Integer::parseInt).toArray(); + return Arrays.stream(alwaysDrop.split(",")) + .map(String::trim) + .filter(token -> token.matches("\\d{1,9}")) + .mapToInt(Integer::parseInt) + .toArray(); } } diff --git a/src/test/java/net/runelite/client/plugins/microbot/thievingstalls/model/BankInventoryStrategyTest.java b/src/test/java/net/runelite/client/plugins/microbot/thievingstalls/model/BankInventoryStrategyTest.java new file mode 100644 index 0000000000..f76885defa --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/thievingstalls/model/BankInventoryStrategyTest.java @@ -0,0 +1,46 @@ +package net.runelite.client.plugins.microbot.thievingstalls.model; + +import net.runelite.client.plugins.microbot.thievingstalls.StallThievingConfig; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class BankInventoryStrategyTest { + + private void runWithAlwaysDrop(String alwaysDrop, int... expectedIds) { + BotApi botApi = mock(BotApi.class); + DropInventoryStrategy dropInventoryStrategy = mock(DropInventoryStrategy.class); + StallThievingConfig config = mock(StallThievingConfig.class); + IStallThievingSpot spot = mock(IStallThievingSpot.class); + when(config.alwaysDrop()).thenReturn(alwaysDrop); + when(botApi.isInventoryFull()).thenReturn(true, false); + + BankInventoryStrategy strategy = new BankInventoryStrategy(botApi, dropInventoryStrategy, config); + + assertDoesNotThrow(() -> strategy.execute(spot)); + verify(botApi).dropAll(expectedIds); + } + + @Test + void itemNamesAreIgnoredInsteadOfThrowing() { + runWithAlwaysDrop("Bread Chocolate slice "); + } + + @Test + void numericIdsAreKeptAlongsideInvalidEntries() { + runWithAlwaysDrop(" 2309 , Bread,1901, 99999999999 ,", 2309, 1901); + } + + @Test + void defaultIdListIsParsed() { + runWithAlwaysDrop("2309,1901", 2309, 1901); + } + + @Test + void blankConfigDropsNothing() { + runWithAlwaysDrop(" "); + } +} From 9fd4d2a4230d2fa313a5f9d6964f9493a3ddecbe Mon Sep 17 00:00:00 2001 From: chsami Date: Wed, 14 Oct 2026 23:27:15 +0200 Subject: [PATCH 06/38] fix(sulphurnagua): read getInteracting() once per null check (2.0.3) (#619) determineState called Rs2Player.getInteracting() twice, once for the null check and again for isDead(). The target can clear between the two calls, so the second read returned null and threw an NPE. The NPC query filters in handleFighting had the same double read on n.getInteracting() before equals(). Move each check into a small helper that reads the interacting actor once and checks and uses that value. Bump SulphurNaguaPlugin to 2.0.3. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../SulphurNaguaPlugin.java | 2 +- .../SulphurNaguaScript.java | 21 ++++- .../SulphurNaguaInteractingTest.java | 78 +++++++++++++++++++ 3 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/sulphurnaguafigther/SulphurNaguaInteractingTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/sulphurnaguafigther/SulphurNaguaPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/sulphurnaguafigther/SulphurNaguaPlugin.java index 6022ed8529..12fd1b2c14 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/sulphurnaguafigther/SulphurNaguaPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/sulphurnaguafigther/SulphurNaguaPlugin.java @@ -29,7 +29,7 @@ @Slf4j public class SulphurNaguaPlugin extends Plugin { - public static final String version = "2.0.2"; + public static final String version = "2.0.3"; @Inject SulphurNaguaScript sulphurNaguaScript; diff --git a/src/main/java/net/runelite/client/plugins/microbot/sulphurnaguafigther/SulphurNaguaScript.java b/src/main/java/net/runelite/client/plugins/microbot/sulphurnaguafigther/SulphurNaguaScript.java index 29faf582d8..be83b19046 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/sulphurnaguafigther/SulphurNaguaScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/sulphurnaguafigther/SulphurNaguaScript.java @@ -2,6 +2,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; +import net.runelite.api.Actor; import net.runelite.api.Client; import net.runelite.api.Skill; import net.runelite.api.coords.WorldArea; @@ -236,7 +237,7 @@ private void determineState(SulphurNaguaConfig config) { return; } - boolean isAvailableForAction = !Rs2Player.isInCombat() || Rs2Player.getInteracting() == null || Rs2Player.getInteracting().isDead(); + boolean isAvailableForAction = !Rs2Player.isInCombat() || isMissingOrDead(Rs2Player.getInteracting()); if (!dynamicLootIds.isEmpty() && isAvailableForAction && !Rs2Inventory.isFull() && isStackableLootNearby()) { currentState = SulphurNaguaState.LOOTING; @@ -539,7 +540,7 @@ private void handleFighting(SulphurNaguaConfig config) { var attackingNagua = Microbot.getRs2NpcCache().query() .withName("Sulphur Nagua") .where(n -> !n.isDead()) - .where(n -> n.getInteracting() != null && n.getInteracting().equals(client.getLocalPlayer())) + .where(n -> isInteractingWith(n, client.getLocalPlayer())) .nearest(); boolean attackInitiated = false; @@ -552,7 +553,7 @@ private void handleFighting(SulphurNaguaConfig config) { .withName("Sulphur Nagua") .where(n -> !n.isDead()) .where(n -> combatArea.contains(n.getWorldLocation())) - .where(n -> n.getInteracting() == null || n.getInteracting().equals(client.getLocalPlayer())) + .where(n -> isFreeOrInteractingWith(n, client.getLocalPlayer())) .interact("Attack"); } @@ -719,4 +720,18 @@ private int countFixedItems() { return fixedItemCount; } + + static boolean isMissingOrDead(Actor target) { + return target == null || target.isDead(); + } + + static boolean isInteractingWith(Actor actor, Actor other) { + Actor target = actor.getInteracting(); + return target != null && target.equals(other); + } + + static boolean isFreeOrInteractingWith(Actor actor, Actor other) { + Actor target = actor.getInteracting(); + return target == null || target.equals(other); + } } \ No newline at end of file diff --git a/src/test/java/net/runelite/client/plugins/microbot/sulphurnaguafigther/SulphurNaguaInteractingTest.java b/src/test/java/net/runelite/client/plugins/microbot/sulphurnaguafigther/SulphurNaguaInteractingTest.java new file mode 100644 index 0000000000..a0d2912689 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/sulphurnaguafigther/SulphurNaguaInteractingTest.java @@ -0,0 +1,78 @@ +package net.runelite.client.plugins.microbot.sulphurnaguafigther; + +import net.runelite.api.Actor; +import net.runelite.api.Player; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class SulphurNaguaInteractingTest { + + @Test + void missingTargetIsAvailable() { + assertTrue(SulphurNaguaScript.isMissingOrDead(null)); + } + + @Test + void deadTargetIsAvailable() { + Actor target = mock(Actor.class); + when(target.isDead()).thenReturn(true); + + assertTrue(SulphurNaguaScript.isMissingOrDead(target)); + } + + @Test + void liveTargetIsNotAvailable() { + Actor target = mock(Actor.class); + + assertFalse(SulphurNaguaScript.isMissingOrDead(target)); + } + + @Test + void npcWhoseTargetDropsAfterFirstReadIsNotInteractingWithUs() { + Player local = mock(Player.class); + Actor npc = mock(Actor.class); + when(npc.getInteracting()).thenReturn(local, (Actor) null); + + assertTrue(SulphurNaguaScript.isInteractingWith(npc, local)); + assertFalse(SulphurNaguaScript.isInteractingWith(npc, local)); + verify(npc, times(2)).getInteracting(); + } + + @Test + void npcInteractingWithSomeoneElseIsNotInteractingWithUs() { + Player local = mock(Player.class); + Player other = mock(Player.class); + Actor npc = mock(Actor.class); + when(npc.getInteracting()).thenReturn(other); + + assertFalse(SulphurNaguaScript.isInteractingWith(npc, local)); + } + + @Test + void npcWhoseTargetDropsMidCheckIsReadOnce() { + Player local = mock(Player.class); + Player other = mock(Player.class); + Actor npc = mock(Actor.class); + when(npc.getInteracting()).thenReturn(other, (Actor) null); + + assertFalse(SulphurNaguaScript.isFreeOrInteractingWith(npc, local)); + verify(npc, times(1)).getInteracting(); + } + + @Test + void freeNpcOrNpcOnUsIsTargetable() { + Player local = mock(Player.class); + Actor freeNpc = mock(Actor.class); + Actor npcOnUs = mock(Actor.class); + when(npcOnUs.getInteracting()).thenReturn(local); + + assertTrue(SulphurNaguaScript.isFreeOrInteractingWith(freeNpc, local)); + assertTrue(SulphurNaguaScript.isFreeOrInteractingWith(npcOnUs, local)); + } +} From 15021bfa1730df7705743f447e98d453bb1e37da Mon Sep 17 00:00:00 2001 From: chsami Date: Wed, 14 Oct 2026 23:38:54 +0200 Subject: [PATCH 07/38] fix(jad): read NPC name once per null check (1.0.11) (#620) The Jad and healer query filters called Rs2NpcModel.getName() twice, once for the null check and again for toLowerCase(). Each call is a separate client-thread read, so the second one can return null after the first passed and throw an NPE. handleHealerInteraction also called getName() on the interacting NPC with no null check. Add a nameContains helper that reads the name once, and null-check the interacting NPC's name before using it. Rs2NpcModel.matches(false, ...) is not used because it only lowercases the NPC name in exact mode, so it would not match "TzTok-Jad". Bump JadPlugin to 1.0.11. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../plugins/microbot/jad/JadPlugin.java | 2 +- .../plugins/microbot/jad/JadScript.java | 17 +++++--- .../plugins/microbot/jad/JadNpcNameTest.java | 41 +++++++++++++++++++ 3 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/jad/JadNpcNameTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/jad/JadPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/jad/JadPlugin.java index a5acdeef74..0ce3b785e9 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/jad/JadPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/jad/JadPlugin.java @@ -32,7 +32,7 @@ ) @Slf4j public class JadPlugin extends Plugin { - static final String version = "1.0.10"; + static final String version = "1.0.11"; @Inject private JadConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/jad/JadScript.java b/src/main/java/net/runelite/client/plugins/microbot/jad/JadScript.java index a67567e2fd..30891e748a 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/jad/JadScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/jad/JadScript.java @@ -1,5 +1,6 @@ package net.runelite.client.plugins.microbot.jad; +import net.runelite.api.Actor; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.Script; import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; @@ -41,7 +42,7 @@ public boolean run(JadConfig config) { int currentTick = Microbot.getClient().getTickCount(); List jadNpcs = Microbot.getRs2NpcCache().query() - .where(n -> n.getName() != null && n.getName().toLowerCase().contains("jad")) + .where(n -> nameContains(n, "jad")) .toList(); Set activeJadIndexes = new HashSet<>(); @@ -83,16 +84,17 @@ public boolean run(JadConfig config) { private void handleHealerInteraction() { var healer = Microbot.getRs2NpcCache().query() - .where(n -> n.getName() != null && n.getName().toLowerCase().contains("hurkot") && !n.isInteractingWithPlayer()) + .where(n -> nameContains(n, "hurkot") && !n.isInteractingWithPlayer()) .nearest(); if (healer != null) { healer.click("Attack"); } else { var npc = Rs2Player.getInteracting(); - if (npc == null || npc.getName().contains("hurkot")) { + String npcName = npc == null ? null : npc.getName(); + if (npc == null || (npcName != null && npcName.contains("hurkot"))) { var jad = Microbot.getRs2NpcCache().query() - .where(n -> n.getName() != null && n.getName().toLowerCase().contains("jad")) + .where(n -> nameContains(n, "jad")) .nearest(); if (jad != null) { jad.click("Attack"); @@ -102,6 +104,11 @@ private void handleHealerInteraction() { } + static boolean nameContains(Actor actor, String part) { + String name = actor.getName(); + return name != null && name.toLowerCase().contains(part); + } + @Override public void shutdown() { super.shutdown(); @@ -207,7 +214,7 @@ synchronized void onTrackedProjectileImpact() { boolean isInJadFight() { var jad = Microbot.getRs2NpcCache().query() - .where(n -> n.getName() != null && n.getName().toLowerCase().contains("jad")) + .where(n -> nameContains(n, "jad")) .nearest(); return jad != null; } diff --git a/src/test/java/net/runelite/client/plugins/microbot/jad/JadNpcNameTest.java b/src/test/java/net/runelite/client/plugins/microbot/jad/JadNpcNameTest.java new file mode 100644 index 0000000000..05d12f955b --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/jad/JadNpcNameTest.java @@ -0,0 +1,41 @@ +package net.runelite.client.plugins.microbot.jad; + +import net.runelite.api.Actor; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class JadNpcNameTest { + + @Test + void nameThatDropsAfterFirstReadIsReadOnce() { + Actor npc = mock(Actor.class); + when(npc.getName()).thenReturn("TzTok-Jad", (String) null); + + assertTrue(JadScript.nameContains(npc, "jad")); + assertFalse(JadScript.nameContains(npc, "jad")); + verify(npc, times(2)).getName(); + } + + @Test + void nullNameDoesNotMatch() { + Actor npc = mock(Actor.class); + + assertFalse(JadScript.nameContains(npc, "jad")); + verify(npc, times(1)).getName(); + } + + @Test + void matchIgnoresCase() { + Actor healer = mock(Actor.class); + when(healer.getName()).thenReturn("Yt-HurKot"); + + assertTrue(JadScript.nameContains(healer, "hurkot")); + assertFalse(JadScript.nameContains(healer, "jad")); + } +} From 4f547da40475a616addb4d149f1b8613e536415f Mon Sep 17 00:00:00 2001 From: chsami Date: Wed, 14 Oct 2026 23:47:44 +0200 Subject: [PATCH 08/38] fix(qol): fall back to unit bounds for recorded entries without widget (1.8.18) (#621) Bank, furnace and anvil replay called Objects.requireNonNull on the recorded entry's widget, which is null for clicks not made on a widget. The NPE escaped before executeBankActions was cleared, so it repeated every loop. Use the widget bounds when present and a 1x1 rectangle otherwise, matching the workbench replay. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../microbot/qualityoflife/QoLPlugin.java | 2 +- .../microbot/qualityoflife/QoLScript.java | 13 +++++--- .../QoLScriptInvokeBoundsTest.java | 31 +++++++++++++++++++ 3 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/qualityoflife/QoLScriptInvokeBoundsTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/qualityoflife/QoLPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/qualityoflife/QoLPlugin.java index f3bcaa937b..c82dfbc940 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/qualityoflife/QoLPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/qualityoflife/QoLPlugin.java @@ -91,7 +91,7 @@ ) @Slf4j public class QoLPlugin extends Plugin implements KeyListener { - public static final String version = "1.8.17"; + public static final String version = "1.8.18"; public static final List bankMenuEntries = new LinkedList<>(); public static final List furnaceMenuEntries = new LinkedList<>(); public static final List anvilMenuEntries = new LinkedList<>(); diff --git a/src/main/java/net/runelite/client/plugins/microbot/qualityoflife/QoLScript.java b/src/main/java/net/runelite/client/plugins/microbot/qualityoflife/QoLScript.java index 418467bd5b..89e5710193 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/qualityoflife/QoLScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/qualityoflife/QoLScript.java @@ -2,6 +2,7 @@ import lombok.extern.slf4j.Slf4j; import net.runelite.api.Player; +import net.runelite.api.widgets.Widget; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.Script; import net.runelite.client.plugins.microbot.util.Rs2InventorySetup; @@ -15,7 +16,6 @@ import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; import java.awt.*; -import java.util.Objects; import java.util.concurrent.TimeUnit; @Slf4j @@ -225,7 +225,7 @@ private void processBankMenuEntry(NewMenuEntry menuEntry) { } } } - Microbot.doInvoke(menuEntry, Objects.requireNonNull(menuEntry.getWidget()).getBounds()); + Microbot.doInvoke(menuEntry, invokeBounds(menuEntry)); //Rs2Random.wait(200, 500); } @@ -267,7 +267,7 @@ private boolean openAnvil() { private void processFurnaceMenuEntry(NewMenuEntry menuEntry) { log.info("Executing action: {} {}", menuEntry.getOption(), menuEntry.getTarget()); - Microbot.doInvoke(menuEntry, Objects.requireNonNull(menuEntry.getWidget()).getBounds()); + Microbot.doInvoke(menuEntry, invokeBounds(menuEntry)); Rs2Random.wait(200, 500); } @@ -286,10 +286,15 @@ private void handleAnvilActions() { private void processAnvilMenuEntry(NewMenuEntry menuEntry) { log.info("Executing action: {} {}", menuEntry.getOption(), menuEntry.getTarget()); - Microbot.doInvoke(menuEntry, Objects.requireNonNull(menuEntry.getWidget()).getBounds()); + Microbot.doInvoke(menuEntry, invokeBounds(menuEntry)); Rs2Random.wait(200, 500); } + static Rectangle invokeBounds(NewMenuEntry menuEntry) { + Widget widget = menuEntry.getWidget(); + return widget != null ? widget.getBounds() : new Rectangle(1, 1); + } + // reset all stored menu entries //Decrepatated use resetMenuEntries method in main class @Deprecated(since = "1.5.8 Use resetMenuEntries method in main class", forRemoval = true) diff --git a/src/test/java/net/runelite/client/plugins/microbot/qualityoflife/QoLScriptInvokeBoundsTest.java b/src/test/java/net/runelite/client/plugins/microbot/qualityoflife/QoLScriptInvokeBoundsTest.java new file mode 100644 index 0000000000..c59def8531 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/qualityoflife/QoLScriptInvokeBoundsTest.java @@ -0,0 +1,31 @@ +package net.runelite.client.plugins.microbot.qualityoflife; + +import java.awt.Rectangle; +import net.runelite.api.MenuAction; +import net.runelite.api.widgets.Widget; +import net.runelite.client.plugins.microbot.util.menu.NewMenuEntry; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class QoLScriptInvokeBoundsTest { + @Test + void recordedEntryWithoutWidgetFallsBackToUnitRectangle() { + NewMenuEntry entry = new NewMenuEntry("Walk here", "", 0, MenuAction.WALK, 0, 0, false); + + assertEquals(new Rectangle(1, 1), QoLScript.invokeBounds(entry)); + } + + @Test + void recordedEntryWithWidgetUsesWidgetBounds() { + Rectangle bounds = new Rectangle(40, 60, 36, 32); + Widget widget = mock(Widget.class); + when(widget.getBounds()).thenReturn(bounds); + NewMenuEntry entry = new NewMenuEntry("Withdraw-1", "Coins", 1, MenuAction.CC_OP, 3, 786445, false); + entry.setWidget(widget); + + assertEquals(bounds, QoLScript.invokeBounds(entry)); + } +} From b5f8eb4de8d0bd04d3945c3eb30b83f3858d6352 Mon Sep 17 00:00:00 2001 From: chsami Date: Thu, 15 Oct 2026 00:54:32 +0200 Subject: [PATCH 09/38] fix(aiofighter): snapshot target sort keys before sorting (2.2.5) (#622) The attackable NPC sort read npc.getInteracting() and the live NPC path distance on every comparison. NPCs that moved or retargeted mid-sort gave inconsistent keys and TimSort threw "Comparison method violates its general contract!". Compute both keys once per NPC, then sort. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../microbot/aiofighter/AIOFighterPlugin.java | 2 +- .../aiofighter/combat/AttackNpcScript.java | 25 ++++++++--- .../combat/AttackNpcScriptTest.java | 45 +++++++++++++++++++ 3 files changed, 65 insertions(+), 7 deletions(-) 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 9dc3d3bbd8..de5adc20c6 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 @@ -62,7 +62,7 @@ ) @Slf4j public class AIOFighterPlugin extends Plugin { - public static final String version = "2.2.4"; + public static final String version = "2.2.5"; 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/combat/AttackNpcScript.java b/src/main/java/net/runelite/client/plugins/microbot/aiofighter/combat/AttackNpcScript.java index bc045039a7..7fbb9fc19a 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/aiofighter/combat/AttackNpcScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/aiofighter/combat/AttackNpcScript.java @@ -44,6 +44,7 @@ import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.ToIntFunction; import java.util.stream.Collectors; import static net.runelite.api.gameval.VarbitID.*; @@ -158,12 +159,10 @@ public void run(AIOFighterConfig config) { String name = npc.getName(); return name != null && !npcsToAttack.isEmpty() && npcsToAttack.contains(name.toLowerCase()); }) - .toList() - .stream() - .sorted(Comparator - .comparingInt((Rs2NpcModel npc) -> Objects.equals(npc.getInteracting(), localPlayer) ? 0 : 1) - .thenComparingInt(npc -> rs2PlayerPoint.distanceToPath(npcWorldLocation(npc, instanced)))) - .collect(Collectors.toList()); + .toList(); + attackableNpcs = sortBySnapshotKeys(attackableNpcs, + npc -> Objects.equals(npc.getInteracting(), localPlayer) ? 0 : 1, + npc -> rs2PlayerPoint.distanceToPath(npcWorldLocation(npc, instanced))); filteredAttackableNpcs.set(attackableNpcs); @@ -296,6 +295,20 @@ public void run(AIOFighterConfig config) { } + static List sortBySnapshotKeys(List items, ToIntFunction primary, ToIntFunction secondary) { + List keyed = new ArrayList<>(items.size()); + for (int i = 0; i < items.size(); i++) { + T item = items.get(i); + keyed.add(new int[]{primary.applyAsInt(item), secondary.applyAsInt(item), i}); + } + keyed.sort(Comparator.comparingInt(k -> k[0]).thenComparingInt(k -> k[1])); + List sorted = new ArrayList<>(keyed.size()); + for (int[] k : keyed) { + sorted.add(items.get(k[2])); + } + return sorted; + } + static boolean shouldHoldAttack(boolean inCombat, boolean antibanEnabled, double actionCooldownChance, boolean actionCooldownActive) { if (inCombat) return true; return antibanEnabled && actionCooldownChance > 0 && actionCooldownActive; diff --git a/src/test/java/net/runelite/client/plugins/microbot/aiofighter/combat/AttackNpcScriptTest.java b/src/test/java/net/runelite/client/plugins/microbot/aiofighter/combat/AttackNpcScriptTest.java index 4b328ed5ae..ff32d9043b 100644 --- a/src/test/java/net/runelite/client/plugins/microbot/aiofighter/combat/AttackNpcScriptTest.java +++ b/src/test/java/net/runelite/client/plugins/microbot/aiofighter/combat/AttackNpcScriptTest.java @@ -2,6 +2,16 @@ import org.junit.jupiter.api.Test; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -34,4 +44,39 @@ void attacksWhenIdleAndNoCooldown() { assertFalse(AttackNpcScript.shouldHoldAttack(false, true, 1.0, false)); assertFalse(AttackNpcScript.shouldHoldAttack(false, false, 0.0, false)); } + + @Test + void sortsByPrimaryThenSecondaryKey() { + List items = Arrays.asList("far-free", "near-free", "far-mine", "near-mine"); + Map primary = Map.of("far-free", 1, "near-free", 1, "far-mine", 0, "near-mine", 0); + Map secondary = Map.of("far-free", 9, "near-free", 2, "far-mine", 7, "near-mine", 1); + + List sorted = AttackNpcScript.sortBySnapshotKeys(items, primary::get, secondary::get); + + assertEquals(Arrays.asList("near-mine", "far-mine", "near-free", "far-free"), sorted); + } + + @Test + void readsEachKeyOncePerItemSoLiveKeysCannotBreakTheSort() { + List items = IntStream.range(0, 500).boxed().collect(Collectors.toList()); + Random random = new Random(42); + Map primaryReads = new HashMap<>(); + Map secondaryReads = new HashMap<>(); + + List sorted = AttackNpcScript.sortBySnapshotKeys(items, + item -> { + primaryReads.merge(item, 1, Integer::sum); + return random.nextInt(2); + }, + item -> { + secondaryReads.merge(item, 1, Integer::sum); + return random.nextInt(50); + }); + + assertEquals(items.size(), sorted.size()); + assertEquals(new ArrayList<>(items), sorted.stream().sorted().collect(Collectors.toList())); + assertTrue(primaryReads.values().stream().allMatch(count -> count == 1)); + assertTrue(secondaryReads.values().stream().allMatch(count -> count == 1)); + assertEquals(items.size(), primaryReads.size()); + } } From d2a25c72e14a883295dcd1db2b097f7aacc51c7a Mon Sep 17 00:00:00 2001 From: chsami Date: Thu, 15 Oct 2026 01:03:52 +0200 Subject: [PATCH 10/38] fix(bankorganizer): stop throwing on the client thread when the bank is closed (1.0.1) (#623) BankSnapshotReader threw IllegalStateException inside the runOnClientThreadOptional callable when the bank was closed, so ClientThread logged every BankActuator poll at ERROR. The callable now returns an empty Optional and read() raises the same "Open your bank" error on the caller's thread, which ends the organizer run without client-thread error spam. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../bankorganizer/BankOrganizerPlugin.java | 2 +- .../bankorganizer/BankSnapshotReader.java | 10 ++- .../bankorganizer/BankSnapshotReaderTest.java | 86 +++++++++++++++++++ 3 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/bankorganizer/BankSnapshotReaderTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/bankorganizer/BankOrganizerPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/bankorganizer/BankOrganizerPlugin.java index ab7c52d416..48e7c88dc6 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/bankorganizer/BankOrganizerPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/bankorganizer/BankOrganizerPlugin.java @@ -42,7 +42,7 @@ @Slf4j public class BankOrganizerPlugin extends Plugin { - public static final String version = "1.0.0"; + public static final String version = "1.0.1"; @Inject private BankOrganizerConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/bankorganizer/BankSnapshotReader.java b/src/main/java/net/runelite/client/plugins/microbot/bankorganizer/BankSnapshotReader.java index 22de962226..8b96ae1891 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/bankorganizer/BankSnapshotReader.java +++ b/src/main/java/net/runelite/client/plugins/microbot/bankorganizer/BankSnapshotReader.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Optional; import javax.inject.Inject; import net.runelite.api.Client; import net.runelite.api.ItemComposition; @@ -44,14 +45,15 @@ final class BankSnapshotReader BankSnapshot read() { return Microbot.getClientThread().runOnClientThreadOptional(this::readOnClientThread) - .orElseThrow(() -> new IllegalStateException("Could not read bank snapshot on the client thread.")); + .orElseThrow(() -> new IllegalStateException("Could not read bank snapshot on the client thread.")) + .orElseThrow(() -> new IllegalStateException("Open your bank before running Bank Organizer.")); } - private BankSnapshot readOnClientThread() + private Optional readOnClientThread() { if (!Rs2Bank.isOpen()) { - throw new IllegalStateException("Open your bank before running Bank Organizer."); + return Optional.empty(); } List bankItems = new ArrayList<>(Rs2Bank.bankItems()); @@ -83,7 +85,7 @@ private BankSnapshot readOnClientThread() isEquipable(composition))); } - return new BankSnapshot(stacks, tabCounts, client.getVarbitValue(Varbits.CURRENT_BANK_TAB)); + return Optional.of(new BankSnapshot(stacks, tabCounts, client.getVarbitValue(Varbits.CURRENT_BANK_TAB))); } private int[] readTabCounts() diff --git a/src/test/java/net/runelite/client/plugins/microbot/bankorganizer/BankSnapshotReaderTest.java b/src/test/java/net/runelite/client/plugins/microbot/bankorganizer/BankSnapshotReaderTest.java new file mode 100644 index 0000000000..250edca99d --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/bankorganizer/BankSnapshotReaderTest.java @@ -0,0 +1,86 @@ +package net.runelite.client.plugins.microbot.bankorganizer; + +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicReference; +import net.runelite.api.Client; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.game.ItemManager; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +class BankSnapshotReaderTest +{ + private MockedStatic microbot; + private MockedStatic bank; + private final AtomicReference clientThreadFailure = new AtomicReference<>(); + private BankSnapshotReader reader; + + @BeforeEach + void setUp() + { + ClientThread clientThread = mock(ClientThread.class); + when(clientThread.runOnClientThreadOptional(any())).thenAnswer(invocation -> { + try + { + return Optional.ofNullable(((Callable) invocation.getArgument(0)).call()); + } + catch (Throwable t) + { + clientThreadFailure.set(t); + return Optional.empty(); + } + }); + + microbot = mockStatic(Microbot.class); + microbot.when(Microbot::getClientThread).thenReturn(clientThread); + bank = mockStatic(Rs2Bank.class); + + reader = new BankSnapshotReader(mock(Client.class), mock(ItemManager.class)); + } + + @AfterEach + void tearDown() + { + bank.close(); + microbot.close(); + } + + @Test + void closedBankFailsOffTheClientThread() + { + bank.when(Rs2Bank::isOpen).thenReturn(false); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, reader::read); + + assertEquals("Open your bank before running Bank Organizer.", thrown.getMessage()); + assertNull(clientThreadFailure.get()); + } + + @Test + void openBankReturnsSnapshot() + { + bank.when(Rs2Bank::isOpen).thenReturn(true); + bank.when(Rs2Bank::bankItems).thenReturn(Collections.emptyList()); + + BankSnapshot snapshot = reader.read(); + + assertNotNull(snapshot); + assertEquals(0, snapshot.stackCount()); + assertNull(clientThreadFailure.get()); + } +} From ff5554eaaa293972c5bfba90e376d6693dc67a30 Mon Sep 17 00:00:00 2001 From: chsami Date: Thu, 15 Oct 2026 02:59:32 +0200 Subject: [PATCH 11/38] fix(cluesolver): keep requirement banking off the client thread (1.0.6) (#624) * fix(cluesolver): keep requirement banking off the client thread (1.0.6) RequirementHandlerTask ran the whole missing-item flow inside ClientThread.invokeLater, so Rs2Bank.getNearestBank planned the bank route on the client thread and threw "synchronous route planning must not run on the client thread". The onGameTick handler also opened the bank, withdrew and closed it on the client thread. Only the inventory/equipment read and item names now hop to the client thread; bank lookup, walking, opening and withdrawing run on the task executor behind a single in-flight guard. A null nearest bank fails the task instead of throwing an NPE, and the tick handler reuses the chosen bank instead of re-planning a route every tick. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV * fix(cluesolver): fail requirement task instead of hanging when walking to the bank fails Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV --------- Co-authored-by: Claude Opus 5.5 --- .../microbot/cluesolver/ClueSolverPlugin.java | 2 +- .../cluetask/RequirementHandlerTask.java | 101 ++++++--- .../RequirementHandlerTaskTest.java | 192 ++++++++++++++++++ 3 files changed, 267 insertions(+), 28 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/cluesolver/RequirementHandlerTaskTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/cluesolver/ClueSolverPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/cluesolver/ClueSolverPlugin.java index 5ae92659d6..e1f24acd9d 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/cluesolver/ClueSolverPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/cluesolver/ClueSolverPlugin.java @@ -29,7 +29,7 @@ @PluginDependency(ClueScrollPlugin.class) public class ClueSolverPlugin extends Plugin { - final static String version = "1.0.5"; + final static String version = "1.0.6"; @Inject private ClueSolverScript clueSolverScript; diff --git a/src/main/java/net/runelite/client/plugins/microbot/cluesolver/cluetask/RequirementHandlerTask.java b/src/main/java/net/runelite/client/plugins/microbot/cluesolver/cluetask/RequirementHandlerTask.java index 9231599ee4..778d709310 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/cluesolver/cluetask/RequirementHandlerTask.java +++ b/src/main/java/net/runelite/client/plugins/microbot/cluesolver/cluetask/RequirementHandlerTask.java @@ -5,6 +5,7 @@ import net.runelite.api.Client; import net.runelite.api.InventoryID; import net.runelite.api.Item; +import net.runelite.api.coords.WorldPoint; import net.runelite.api.events.GameTick; import net.runelite.api.events.ItemContainerChanged; import net.runelite.client.callback.ClientThread; @@ -15,11 +16,13 @@ import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.cluesolver.ClueSolverPlugin; import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; +import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; @Slf4j @@ -28,8 +31,10 @@ public class RequirementHandlerTask extends ClueTask { private final EventBus eventBus; private final ExecutorService backgroundExecutor; private Iterator requirementIterator; - private CompletableFuture itemReceivedFuture; - private ItemRequirement currentRequirement; + private volatile CompletableFuture itemReceivedFuture; + private volatile ItemRequirement currentRequirement; + private volatile BankLocation targetBank; + private final AtomicBoolean bankStepRunning = new AtomicBoolean(false); @Inject private ClientThread clientThread; @@ -45,22 +50,49 @@ public RequirementHandlerTask(Client client, List requirements, protected boolean executeTask() { eventBus.register(this); log.info("Checking for missing items."); - checkForMissingItemsAsync(); + runBankStep(this::checkForMissingItems); return true; } - /** - * Retrieves a list of missing items based on the requirements asynchronously on the client thread. - */ - private void checkForMissingItemsAsync() { - Microbot.getClientThread().invokeLater(() -> { - List missingItems = getMissingItems(requirements); - if (missingItems.isEmpty()) { - completeTask(true); - } else { - fetchMissingItemsFromBank(missingItems); - } - }); + private void runBankStep(Runnable step) { + if (!bankStepRunning.compareAndSet(false, true)) { + return; + } + try { + backgroundExecutor.submit(() -> { + try { + step.run(); + } catch (Exception e) { + log.warn("Requirement bank step failed: {}", e.getMessage(), e); + completeTask(false); + } finally { + bankStepRunning.set(false); + } + }); + } catch (RejectedExecutionException e) { + bankStepRunning.set(false); + completeTask(false); + } + } + + private void checkForMissingItems() { + List missingItems = Microbot.getClientThread() + .runOnClientThreadOptional(() -> getMissingItems(requirements)) + .orElse(null); + if (missingItems == null) { + log.warn("Unable to read inventory and equipment for item requirements."); + completeTask(false); + } else if (missingItems.isEmpty()) { + completeTask(true); + } else { + fetchMissingItemsFromBank(missingItems); + } + } + + private String getItemName(ItemRequirement requirement) { + return Microbot.getClientThread() + .runOnClientThreadOptional(() -> requirement.getCollectiveName(client)) + .orElse("N/A"); } private List getMissingItems(List requirements) { @@ -78,7 +110,7 @@ private List getMissingItems(List requirements private void fetchMissingItemsFromBank(List missingItems) { this.requirementIterator = missingItems.iterator(); - log.info("Missing items: {}", missingItems.stream().map(e -> e.getCollectiveName(client)).collect(Collectors.joining(", "))); + log.info("Missing items: {}", missingItems.stream().map(this::getItemName).collect(Collectors.joining(", "))); if (requirementIterator == null || !requirementIterator.hasNext()) { completeTask(true); @@ -96,13 +128,24 @@ private void walkToBank() { // be called from off-client-thread code paths (executeTask flow). Direct // client.getLocalPlayer().getWorldLocation() throws IllegalStateException, which // cascaded into "Failed to open the bank" + walker deadlock. - net.runelite.api.coords.WorldPoint playerLocation = getPlayerLocationSafe(); + WorldPoint playerLocation = getPlayerLocationSafe(); if (playerLocation == null) { - log.debug("Player location unavailable in walkToBank; will retry next tick"); + log.warn("Player location unavailable in walkToBank."); + completeTask(false); return; } - if (playerLocation.distanceTo(Rs2Bank.getNearestBank().getWorldPoint()) > 10) { - Rs2Bank.walkToBankAndUseBank(); + BankLocation nearestBank = Rs2Bank.getNearestBank(); + if (nearestBank == null) { + log.warn("No reachable bank found."); + completeTask(false); + return; + } + targetBank = nearestBank; + if (playerLocation.distanceTo(nearestBank.getWorldPoint()) > 10) { + if (!Rs2Bank.walkToBankAndUseBank()) { + log.warn("Failed to walk to the bank."); + completeTask(false); + } } else { openBank(); } @@ -123,7 +166,7 @@ private void fetchNextItem() { } currentRequirement = requirementIterator.next(); - String itemName = currentRequirement.getCollectiveName(client); + String itemName = getItemName(currentRequirement); if (!Rs2Bank.hasItem(itemName)) { log.warn("Item {} not found in bank.", itemName); @@ -132,8 +175,8 @@ private void fetchNextItem() { return; } - Rs2Bank.withdrawAndEquip(itemName); itemReceivedFuture = new CompletableFuture<>(); + Rs2Bank.withdrawAndEquip(itemName); backgroundExecutor.submit(() -> { try { @@ -158,16 +201,20 @@ private void fetchNextItem() { @Subscribe public void onGameTick(GameTick event) { + if (bankStepRunning.get()) { + return; + } if (Rs2Bank.isOpen() && currentRequirement == null) { - fetchNextItem(); + runBankStep(this::fetchNextItem); return; } // v1.0.4 fix: defensive client-thread-safe read. onGameTick is @Subscribe-annotated so // it normally runs on the client thread, but using the helper is harmless and consistent. - net.runelite.api.coords.WorldPoint playerLocation = getPlayerLocationSafe(); - if (playerLocation == null) return; - if (!Rs2Bank.isOpen() && playerLocation.distanceTo(Rs2Bank.getNearestBank().getWorldPoint()) <= 5) { - openBank(); + WorldPoint playerLocation = getPlayerLocationSafe(); + BankLocation bank = targetBank; + if (playerLocation == null || bank == null) return; + if (!Rs2Bank.isOpen() && playerLocation.distanceTo(bank.getWorldPoint()) <= 5) { + runBankStep(this::openBank); } } diff --git a/src/test/java/net/runelite/client/plugins/microbot/cluesolver/RequirementHandlerTaskTest.java b/src/test/java/net/runelite/client/plugins/microbot/cluesolver/RequirementHandlerTaskTest.java new file mode 100644 index 0000000000..7d57de30df --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/cluesolver/RequirementHandlerTaskTest.java @@ -0,0 +1,192 @@ +package net.runelite.client.plugins.microbot.cluesolver; + +import net.runelite.api.Client; +import net.runelite.api.Item; +import net.runelite.api.Player; +import net.runelite.api.events.GameTick; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.eventbus.EventBus; +import net.runelite.client.plugins.cluescrolls.clues.item.ItemRequirement; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.cluesolver.cluetask.RequirementHandlerTask; +import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; +import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.when; + +class RequirementHandlerTaskTest { + + private final AtomicBoolean onClientThread = new AtomicBoolean(false); + private final AtomicInteger bankCallsOnClientThread = new AtomicInteger(); + private MockedStatic microbot; + private MockedStatic bank; + private DeferredExecutor executor; + private CompletableFuture result; + private RequirementHandlerTask task; + + @BeforeEach + void setUp() { + ClientThread clientThread = mock(ClientThread.class); + when(clientThread.runOnClientThreadOptional(any())).thenAnswer(invocation -> { + onClientThread.set(true); + try { + return Optional.ofNullable(((Callable) invocation.getArgument(0)).call()); + } finally { + onClientThread.set(false); + } + }); + microbot = mockStatic(Microbot.class); + microbot.when(Microbot::getClientThread).thenReturn(clientThread); + bank = mockStatic(Rs2Bank.class, invocation -> { + if (onClientThread.get()) { + bankCallsOnClientThread.incrementAndGet(); + } + return invocation.getMethod().getReturnType() == boolean.class ? false : null; + }); + + Player player = mock(Player.class); + when(player.getWorldLocation()).thenReturn(BankLocation.AL_KHARID.getWorldPoint()); + Client client = mock(Client.class); + when(client.getLocalPlayer()).thenReturn(player); + + ItemRequirement spade = mock(ItemRequirement.class); + when(spade.fulfilledBy(any(Item[].class))).thenReturn(false); + when(spade.getCollectiveName(client)).thenReturn("Spade"); + + executor = new DeferredExecutor(); + result = new CompletableFuture<>(); + task = new RequirementHandlerTask(client, Collections.singletonList(spade), mock(EventBus.class), null, null, executor); + task.setFuture(result); + } + + @AfterEach + void tearDown() { + bank.close(); + microbot.close(); + } + + @Test + void executeTaskHandsTheBankWorkToTheExecutor() { + task.run(); + + assertEquals(1, executor.queued.size()); + bank.verifyNoInteractions(); + assertFalse(result.isDone()); + } + + @Test + void nearestBankIsResolvedOutsideTheClientThread() { + bank.when(Rs2Bank::getNearestBank).thenReturn(BankLocation.AL_KHARID); + + task.run(); + executor.runAll(); + + bank.verify(Rs2Bank::getNearestBank); + bank.verify(Rs2Bank::openBank); + assertEquals(0, bankCallsOnClientThread.get()); + assertTrue(result.isDone()); + assertFalse(result.join()); + } + + @Test + void unreachableBankFailsTheTaskWithoutWalking() { + task.run(); + executor.runAll(); + + bank.verify(Rs2Bank::getNearestBank); + bank.verify(Rs2Bank::walkToBankAndUseBank, never()); + bank.verify(Rs2Bank::openBank, never()); + assertTrue(result.isDone()); + assertFalse(result.join()); + } + + @Test + void gameTickRunsBankFetchOnTheExecutorAndDropsOverlappingTicks() { + bank.when(Rs2Bank::isOpen).thenReturn(true); + + task.onGameTick(new GameTick()); + task.onGameTick(new GameTick()); + + assertEquals(1, executor.queued.size()); + bank.verify(Rs2Bank::closeBank, never()); + + executor.runAll(); + + bank.verify(Rs2Bank::closeBank); + assertTrue(result.join()); + + task.onGameTick(new GameTick()); + assertEquals(1, executor.queued.size()); + } + + @Test + void gameTickDoesNotPlanARouteBeforeABankIsChosen() { + bank.when(Rs2Bank::isOpen).thenReturn(false); + + task.onGameTick(new GameTick()); + + bank.verify(Rs2Bank::getNearestBank, never()); + assertEquals(0, executor.queued.size()); + } + + private static final class DeferredExecutor extends AbstractExecutorService { + private final List queued = new ArrayList<>(); + + @Override + public void execute(Runnable command) { + queued.add(command); + } + + void runAll() { + while (!queued.isEmpty()) { + queued.remove(0).run(); + } + } + + @Override + public void shutdown() { + } + + @Override + public List shutdownNow() { + return Collections.emptyList(); + } + + @Override + public boolean isShutdown() { + return false; + } + + @Override + public boolean isTerminated() { + return false; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) { + return true; + } + } +} From 4578b678bbaf2aeb4a3e4eae88db24eae7bd0b2e Mon Sep 17 00:00:00 2001 From: chsami Date: Thu, 15 Oct 2026 04:08:14 +0200 Subject: [PATCH 12/38] fix(wildernessagility): skip null ladder re-query in pit recovery (1.6.2) (#625) recoverFromPit re-queried ladder 17385 inside the 2s throttle and clicked the result unconditionally, throwing an NPE when the cache returned no ladder. Keep the ladder from the first lookup when the re-query is empty, and only start the throttle after a click actually succeeds. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../WildernessAgilityPlugin.java | 2 +- .../WildernessAgilityScript.java | 11 ++- .../WildernessAgilityPitRecoveryTest.java | 87 +++++++++++++++++++ 3 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/wildernessagility/WildernessAgilityPitRecoveryTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/wildernessagility/WildernessAgilityPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/wildernessagility/WildernessAgilityPlugin.java index 5925a1fd9c..23c81a253a 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/wildernessagility/WildernessAgilityPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/wildernessagility/WildernessAgilityPlugin.java @@ -26,7 +26,7 @@ ) public class WildernessAgilityPlugin extends Plugin { - static final String version = "1.6.1"; + static final String version = "1.6.2"; @Inject private OverlayManager overlayManager; @Inject diff --git a/src/main/java/net/runelite/client/plugins/microbot/wildernessagility/WildernessAgilityScript.java b/src/main/java/net/runelite/client/plugins/microbot/wildernessagility/WildernessAgilityScript.java index df689eb559..da6f6aa5f8 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/wildernessagility/WildernessAgilityScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/wildernessagility/WildernessAgilityScript.java @@ -509,7 +509,7 @@ private Rs2TileObjectModel getObstacleObj(int index) { private boolean isInUndergroundPit() { return Microbot.getRs2TileObjectCache().query().withId(UNDERGROUND_OBJECT_ID).nearest() != null; } - private void recoverFromPit() { + void recoverFromPit() { // First check if we're still in the pit using game object detection if (isInUndergroundPit()) { // Immediately refresh ladder object before attempting to interact @@ -521,9 +521,12 @@ private void recoverFromPit() { if (now - lastLadderInteractTime > 2000) { // Refresh ladder object again just before interaction List laddersNow = Microbot.getRs2TileObjectCache().query().withId(17385).toList(); - ladderObj = laddersNow.isEmpty() ? null : laddersNow.get(0); - ladderObj.click("Climb-up"); - lastLadderInteractTime = now; + if (!laddersNow.isEmpty()) { + ladderObj = laddersNow.get(0); + } + if (ladderObj.click("Climb-up")) { + lastLadderInteractTime = now; + } } } return; // Return here to let the next tick handle the state transition diff --git a/src/test/java/net/runelite/client/plugins/microbot/wildernessagility/WildernessAgilityPitRecoveryTest.java b/src/test/java/net/runelite/client/plugins/microbot/wildernessagility/WildernessAgilityPitRecoveryTest.java new file mode 100644 index 0000000000..883957c7c0 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/wildernessagility/WildernessAgilityPitRecoveryTest.java @@ -0,0 +1,87 @@ +package net.runelite.client.plugins.microbot.wildernessagility; + +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.api.tileobject.Rs2TileObjectCache; +import net.runelite.client.plugins.microbot.api.tileobject.Rs2TileObjectQueryable; +import net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class WildernessAgilityPitRecoveryTest { + + private static final WorldPoint PIT_TILE = new WorldPoint(3004, 10362, 0); + + @Test + void emptyLadderRequeryFallsBackToFirstLookup() { + Rs2TileObjectModel ladder = ladder(true); + Rs2TileObjectQueryable query = pitQuery(List.of(ladder), Collections.emptyList()); + + runRecovery(query, 1); + + verify(ladder).click("Climb-up"); + } + + @Test + void failedClickRetriesWithoutWaitingForThrottle() { + Rs2TileObjectModel ladder = ladder(false); + Rs2TileObjectQueryable query = pitQuery(List.of(ladder)); + + runRecovery(query, 2); + + verify(ladder, times(2)).click("Climb-up"); + } + + @Test + void successfulClickIsThrottled() { + Rs2TileObjectModel ladder = ladder(true); + Rs2TileObjectQueryable query = pitQuery(List.of(ladder)); + + runRecovery(query, 2); + + verify(ladder, times(1)).click("Climb-up"); + } + + private static Rs2TileObjectModel ladder(boolean clickSucceeds) { + Rs2TileObjectModel ladder = mock(Rs2TileObjectModel.class); + when(ladder.getWorldLocation()).thenReturn(PIT_TILE); + when(ladder.click("Climb-up")).thenReturn(clickSucceeds); + return ladder; + } + + @SafeVarargs + private static Rs2TileObjectQueryable pitQuery(List first, List... rest) { + Rs2TileObjectQueryable query = mock(Rs2TileObjectQueryable.class); + when(query.withId(anyInt())).thenReturn(query); + when(query.nearest()).thenReturn(mock(Rs2TileObjectModel.class)); + when(query.toList()).thenReturn(first, rest); + return query; + } + + private static void runRecovery(Rs2TileObjectQueryable query, int ticks) { + Rs2TileObjectCache cache = mock(Rs2TileObjectCache.class); + when(cache.query()).thenReturn(query); + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic player = mockStatic(Rs2Player.class)) { + microbot.when(Microbot::getRs2TileObjectCache).thenReturn(cache); + player.when(Rs2Player::getWorldLocation).thenReturn(PIT_TILE); + + WildernessAgilityScript script = new WildernessAgilityScript(); + for (int i = 0; i < ticks; i++) { + assertDoesNotThrow(script::recoverFromPit); + } + } + } +} From d33f19574c8290f5bf3af9518bdcbb3a98b92dda Mon Sep 17 00:00:00 2001 From: chsami Date: Thu, 15 Oct 2026 05:31:11 +0200 Subject: [PATCH 13/38] fix(qol): read Wintertodt energy widget and parse it defensively (1.8.19) (#626) * fix(qol): parse Wintertodt energy text defensively (1.8.19) WintertodtScript read the energy widget with split("\\D+")[1], which threw ArrayIndexOutOfBoundsException when the text had no digits. Take the first run of digits instead and fall back to -1 for null or digit-less text. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV * fix(qol): read Wintertodt energy from ENERGY_TITLE widget The script read widget 25952276 (WintStatus.WARMTH_TITLE) for the boss energy, so wintertodtHp tracked the player's warmth. Read InterfaceID.WintStatus.ENERGY_TITLE instead, matching the other Wintertodt plugins, and share one compiled digit pattern. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV --------- Co-authored-by: Claude Opus 5.5 --- .../microbot/qualityoflife/QoLPlugin.java | 2 +- .../scripts/wintertodt/WintertodtScript.java | 21 ++++++++++++---- .../wintertodt/WintertodtHpParseTest.java | 25 +++++++++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/qualityoflife/scripts/wintertodt/WintertodtHpParseTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/qualityoflife/QoLPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/qualityoflife/QoLPlugin.java index c82dfbc940..9ec4864232 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/qualityoflife/QoLPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/qualityoflife/QoLPlugin.java @@ -91,7 +91,7 @@ ) @Slf4j public class QoLPlugin extends Plugin implements KeyListener { - public static final String version = "1.8.18"; + public static final String version = "1.8.19"; public static final List bankMenuEntries = new LinkedList<>(); public static final List furnaceMenuEntries = new LinkedList<>(); public static final List anvilMenuEntries = new LinkedList<>(); diff --git a/src/main/java/net/runelite/client/plugins/microbot/qualityoflife/scripts/wintertodt/WintertodtScript.java b/src/main/java/net/runelite/client/plugins/microbot/qualityoflife/scripts/wintertodt/WintertodtScript.java index 66ef7dac7f..536c085853 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/qualityoflife/scripts/wintertodt/WintertodtScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/qualityoflife/scripts/wintertodt/WintertodtScript.java @@ -5,6 +5,7 @@ import net.runelite.api.events.NpcChanged; import net.runelite.api.events.NpcDespawned; import net.runelite.api.events.NpcSpawned; +import net.runelite.api.gameval.InterfaceID; import net.runelite.api.gameval.ItemID; import net.runelite.api.gameval.ObjectID; import net.runelite.api.widgets.Widget; @@ -29,6 +30,7 @@ import java.util.regex.Pattern; public class WintertodtScript extends Script { + private static final Pattern DIGITS = Pattern.compile("\\d+"); public static QoLConfig config; public static Rs2TileObjectModel unlitBrazier; public static Rs2TileObjectModel brokenBrazier; @@ -64,11 +66,10 @@ public boolean run(QoLConfig config) { return; } - Widget wintertodtHealthbar = Rs2Widget.getWidget(25952276); + Widget wintertodtHealthbar = Rs2Widget.getWidget(InterfaceID.WintStatus.ENERGY_TITLE); isWintertodtAlive = Rs2Widget.hasWidget("Wintertodt's Energy"); if (wintertodtHealthbar != null && isWintertodtAlive) { - String widgetText = wintertodtHealthbar.getText(); - wintertodtHp = Integer.parseInt(widgetText.split("\\D+")[1]); + wintertodtHp = parseWintertodtHp(wintertodtHealthbar.getText()); } else { wintertodtHp = -1; } @@ -238,11 +239,21 @@ public void onChatMessage(ChatMessage chatMessage) { } } + static int parseWintertodtHp(String widgetText) { + if (widgetText == null) { + return -1; + } + Matcher matcher = DIGITS.matcher(widgetText); + if (matcher.find()) { + return Integer.parseInt(matcher.group()); + } + return -1; + } + public int getWarmthLevel() { String warmthWidgetText = Rs2Widget.getChildWidgetText(396, 20); - Pattern pattern = Pattern.compile("\\d+"); - Matcher matcher = pattern.matcher(warmthWidgetText); + Matcher matcher = DIGITS.matcher(warmthWidgetText); if (matcher.find()) { return Integer.parseInt(matcher.group()); diff --git a/src/test/java/net/runelite/client/plugins/microbot/qualityoflife/scripts/wintertodt/WintertodtHpParseTest.java b/src/test/java/net/runelite/client/plugins/microbot/qualityoflife/scripts/wintertodt/WintertodtHpParseTest.java new file mode 100644 index 0000000000..94537f1d5f --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/qualityoflife/scripts/wintertodt/WintertodtHpParseTest.java @@ -0,0 +1,25 @@ +package net.runelite.client.plugins.microbot.qualityoflife.scripts.wintertodt; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class WintertodtHpParseTest { + @Test + void parsesEnergyPercentage() { + assertEquals(45, WintertodtScript.parseWintertodtHp("Wintertodt's Energy: 45%")); + assertEquals(100, WintertodtScript.parseWintertodtHp("Wintertodt's Energy: 100%")); + assertEquals(0, WintertodtScript.parseWintertodtHp("Wintertodt's Energy: 0%")); + } + + @Test + void parsesTextStartingWithDigits() { + assertEquals(72, WintertodtScript.parseWintertodtHp("72%")); + } + + @Test + void returnsMinusOneWhenTextHasNoDigits() { + assertEquals(-1, WintertodtScript.parseWintertodtHp("")); + assertEquals(-1, WintertodtScript.parseWintertodtHp("Wintertodt's Energy")); + assertEquals(-1, WintertodtScript.parseWintertodtHp(null)); + } +} From 0613cfe0b45a4c06b28252ec247842572d355968 Mon Sep 17 00:00:00 2001 From: chsami Date: Thu, 15 Oct 2026 05:51:26 +0200 Subject: [PATCH 14/38] fix(aiofighter): null-safe state check in SafeSpot loop (2.2.6) (#627) SafeSpot called AIOFighterPlugin.getState().equals(...) on every tick, but getState() is a raw ConfigManager read that returns null before the state is first set (startup, after reset), so the loop threw an NPE. Compare the state by identity through a small helper instead, matching LootScript and SafetyScript. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../microbot/aiofighter/AIOFighterPlugin.java | 2 +- .../microbot/aiofighter/combat/SafeSpot.java | 6 +++- .../aiofighter/combat/SafeSpotTest.java | 30 +++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/aiofighter/combat/SafeSpotTest.java 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 de5adc20c6..2d4e37e9ab 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 @@ -62,7 +62,7 @@ ) @Slf4j public class AIOFighterPlugin extends Plugin { - public static final String version = "2.2.5"; + public static final String version = "2.2.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/combat/SafeSpot.java b/src/main/java/net/runelite/client/plugins/microbot/aiofighter/combat/SafeSpot.java index ac6380f48b..909bbae950 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/aiofighter/combat/SafeSpot.java +++ b/src/main/java/net/runelite/client/plugins/microbot/aiofighter/combat/SafeSpot.java @@ -19,7 +19,7 @@ public class SafeSpot extends Script { public boolean run(AIOFighterConfig config) { mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { try { - if (AIOFighterPlugin.getState().equals(State.BANKING) || AIOFighterPlugin.getState().equals(State.WALKING)) return; + if (isBlockedByState(AIOFighterPlugin.getState())) return; if (!Microbot.isLoggedIn() || !super.run() || !config.toggleSafeSpot() || Rs2Player.isMoving()) return; currentSafeSpot = config.safeSpot(); @@ -48,6 +48,10 @@ public boolean run(AIOFighterConfig config) { return true; } +static boolean isBlockedByState(State state) { + return state == State.BANKING || state == State.WALKING; +} + private boolean isDefaultSafeSpot(WorldPoint safeSpot) { return safeSpot.getX() == 0 && safeSpot.getY() == 0; } diff --git a/src/test/java/net/runelite/client/plugins/microbot/aiofighter/combat/SafeSpotTest.java b/src/test/java/net/runelite/client/plugins/microbot/aiofighter/combat/SafeSpotTest.java new file mode 100644 index 0000000000..d8ce1d0353 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/aiofighter/combat/SafeSpotTest.java @@ -0,0 +1,30 @@ +package net.runelite.client.plugins.microbot.aiofighter.combat; + +import net.runelite.client.plugins.microbot.aiofighter.enums.State; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public final class SafeSpotTest { + + @Test + void unsetStateDoesNotBlockSafeSpot() { + assertFalse(SafeSpot.isBlockedByState(null)); + } + + @Test + void bankingAndWalkingBlockSafeSpot() { + assertTrue(SafeSpot.isBlockedByState(State.BANKING)); + assertTrue(SafeSpot.isBlockedByState(State.WALKING)); + } + + @Test + void otherStatesDoNotBlockSafeSpot() { + for (State state : State.values()) { + if (state != State.BANKING && state != State.WALKING) { + assertFalse(SafeSpot.isBlockedByState(state)); + } + } + } +} From fdf8738a19b14e6198acf6eec44f67c258ab37d1 Mon Sep 17 00:00:00 2001 From: chsami Date: Thu, 15 Oct 2026 06:00:10 +0200 Subject: [PATCH 15/38] fix(sandcrabs): guard null inventory setup before use (1.5.7) (#628) SandCrabScript built Rs2InventorySetup from config.inventorySetup().getName() on every loop, but inventorySetup() defaults to null, so with no setup selected (or the setup disabled) the loop threw an NPE before reaching its own "Please setup your Inventory Setup" check. Validate the configured setup first and only construct Rs2InventorySetup when inventory setups are enabled, checking DisableInventorySetup before the equipment match. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../microbot/sandcrabs/SandCrabPlugin.java | 2 +- .../microbot/sandcrabs/SandCrabScript.java | 12 +++++-- .../sandcrabs/SandCrabScriptTest.java | 34 +++++++++++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabScriptTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabPlugin.java index 22ac529283..6219280c0b 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabPlugin.java @@ -26,7 +26,7 @@ ) @Slf4j public class SandCrabPlugin extends Plugin { - public final static String version = "1.5.6"; + public final static String version = "1.5.7"; @Inject private SandCrabConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabScript.java b/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabScript.java index 6639fb7089..3cf5185c26 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabScript.java @@ -4,6 +4,7 @@ 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.inventorysetups.InventorySetup; import net.runelite.client.plugins.microbot.sandcrabs.enums.State; import net.runelite.client.plugins.microbot.sandcrabs.models.ScanLocation; import net.runelite.client.plugins.microbot.util.Rs2InventorySetup; @@ -48,6 +49,10 @@ public class SandCrabScript extends Script { List sandCrabLocations = new ArrayList<>(); + static boolean hasInventorySetup(InventorySetup setup) { + return setup != null && setup.getName() != null && !setup.getName().isEmpty(); + } + public boolean run(SandCrabConfig config, SandCrabPlugin plugin) { initialPlayerLocation = null; if (config.threeNpcs()) { @@ -63,19 +68,20 @@ public boolean run(SandCrabConfig config, SandCrabPlugin plugin) { initialPlayerLocation = Rs2Player.getWorldLocation(); } long startTime = System.currentTimeMillis(); - var inventorySetup = new Rs2InventorySetup(config.inventorySetup().getName(), mainScheduledFuture); + Rs2InventorySetup inventorySetup = null; if (!config.DisableInventorySetup()) { - if (inventorySetup == null || config.inventorySetup().getName().isEmpty()) { + if (!hasInventorySetup(config.inventorySetup())) { Microbot.showMessage("Please setup your Inventory Setup for sand crabs! Stopping..."); shutdown(); return; } + inventorySetup = new Rs2InventorySetup(config.inventorySetup().getName(), mainScheduledFuture); } Rs2Combat.enableAutoRetialiate(); - if (!inventorySetup.doesEquipmentMatch() && !config.DisableInventorySetup()) { + if (!config.DisableInventorySetup() && !inventorySetup.doesEquipmentMatch()) { if (Rs2Bank.walkToBankAndUseBank()) { if (inventorySetup.loadEquipment()) { Microbot.log("We're setup for sand crabs!"); diff --git a/src/test/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabScriptTest.java b/src/test/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabScriptTest.java new file mode 100644 index 0000000000..9cfcf0c83c --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/sandcrabs/SandCrabScriptTest.java @@ -0,0 +1,34 @@ +package net.runelite.client.plugins.microbot.sandcrabs; + +import net.runelite.client.plugins.microbot.inventorysetups.InventorySetup; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class SandCrabScriptTest { + + @Test + void unselectedInventorySetupIsMissing() { + assertFalse(SandCrabScript.hasInventorySetup(null)); + } + + @Test + void setupWithoutNameIsMissing() { + InventorySetup setup = mock(InventorySetup.class); + assertFalse(SandCrabScript.hasInventorySetup(setup)); + + when(setup.getName()).thenReturn(""); + assertFalse(SandCrabScript.hasInventorySetup(setup)); + } + + @Test + void namedSetupIsPresent() { + InventorySetup setup = mock(InventorySetup.class); + when(setup.getName()).thenReturn("Sand crabs"); + + assertTrue(SandCrabScript.hasInventorySetup(setup)); + } +} From fd14702e8f0fa10d16e4a2fd07889cd67d859ad0 Mon Sep 17 00:00:00 2001 From: chsami Date: Thu, 15 Oct 2026 06:13:38 +0200 Subject: [PATCH 16/38] fix(nmz): guard null widgets and local player (2.4.4) (#629) * fix(nmz): guard null widgets and local player in lobby handling (2.4.4) isOutside now reads the cached Rs2Player world location instead of dereferencing getLocalPlayer() on the client thread. consumeEmptyVial skips the dream confirm when widget 129:6 is not loaded, and handleStore stops buying when the reward shop potion child is missing instead of passing null to Rs2Widget.clickWidgetFast. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV * fix(nmz): guard null local player when walking to power-up orbs Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV --------- Co-authored-by: Claude Opus 5.5 --- .../plugins/microbot/nmz/NmzPlugin.java | 2 +- .../plugins/microbot/nmz/NmzScript.java | 27 ++++++++++++++----- .../plugins/microbot/nmz/NmzScriptTest.java | 25 +++++++++++++++++ 3 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/nmz/NmzScriptTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/nmz/NmzPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/nmz/NmzPlugin.java index c9ed30f2c6..2a4afc7bfc 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/nmz/NmzPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/nmz/NmzPlugin.java @@ -31,7 +31,7 @@ ) @Slf4j public class NmzPlugin extends Plugin { - final static String version = "2.4.3"; + final static String version = "2.4.4"; @Inject private NmzConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/nmz/NmzScript.java b/src/main/java/net/runelite/client/plugins/microbot/nmz/NmzScript.java index 8624f37853..f168cca81b 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/nmz/NmzScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/nmz/NmzScript.java @@ -2,6 +2,7 @@ import lombok.Getter; import lombok.Setter; +import net.runelite.api.Player; import net.runelite.api.Skill; import net.runelite.api.coords.WorldPoint; import net.runelite.api.gameval.ItemID; @@ -146,7 +147,10 @@ public void shutdown() { } public boolean isOutside() { - WorldPoint loc = Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation()); + return isNearLobby(Rs2Player.getWorldLocation()); + } + + static boolean isNearLobby(WorldPoint loc) { return loc != null && loc.distanceTo(new WorldPoint(2602, 3116, 0)) < 20; } @@ -236,15 +240,22 @@ public boolean useOrbs() { return orbHasSpawned; } + private static WorldPoint getLocalPlayerWorldLocation() { + return Microbot.getClientThread().invoke(() -> { + Player player = Microbot.getClient().getLocalPlayer(); + return player == null ? null : player.getWorldLocation(); + }); + } + public boolean interactWithObject(int objectId) { Rs2TileObjectModel obj = tileObjectCache.query().withId(objectId).nearest(); if (obj != null) { sleep(1000, 15000); - WorldPoint playerLoc = Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation()); + WorldPoint playerLoc = getLocalPlayerWorldLocation(); if (playerLoc != null && playerLoc.distanceTo(obj.getWorldLocation()) >= 15) { Rs2Walker.walkFastLocal(obj.getLocalLocation()); sleepUntil(() -> { - WorldPoint loc = Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation()); + WorldPoint loc = getLocalPlayerWorldLocation(); return loc != null && loc.distanceTo(obj.getWorldLocation()) < 15; }, 10000); } @@ -379,7 +390,7 @@ public void consumeEmptyVial() { } sleep(2000, 4000); Widget widget = Rs2Widget.getWidget(129, 6); - if (!Microbot.getClientThread().runOnClientThreadOptional(widget::isHidden).orElse(false)) { + if (widget != null && !Microbot.getClientThread().runOnClientThreadOptional(widget::isHidden).orElse(false)) { Rs2Widget.clickWidget(widget.getId()); sleep(300); Rs2Widget.clickWidget(widget.getId()); @@ -438,14 +449,18 @@ public void handleStore() { for (int i = 0; i < overloadToBuy; i++) { Widget nmzRewardShop = Rs2Widget.getWidget(206, 6); if (nmzRewardShop == null) break; - Rs2Widget.clickWidgetFast(nmzRewardShop.getChild(6), 6, 4); + Widget potionButton = nmzRewardShop.getChild(6); + if (potionButton == null) break; + Rs2Widget.clickWidgetFast(potionButton, 6, 4); sleep(600, 1000); } for (int i = 0; i < absorptionToBuy; i++) { Widget nmzRewardShop = Rs2Widget.getWidget(206, 6); if (nmzRewardShop == null) break; - Rs2Widget.clickWidgetFast(nmzRewardShop.getChild(9), 9, 4); + Widget potionButton = nmzRewardShop.getChild(9); + if (potionButton == null) break; + Rs2Widget.clickWidgetFast(potionButton, 9, 4); sleep(600, 1000); } } diff --git a/src/test/java/net/runelite/client/plugins/microbot/nmz/NmzScriptTest.java b/src/test/java/net/runelite/client/plugins/microbot/nmz/NmzScriptTest.java new file mode 100644 index 0000000000..8ea1adf077 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/nmz/NmzScriptTest.java @@ -0,0 +1,25 @@ +package net.runelite.client.plugins.microbot.nmz; + +import net.runelite.api.coords.WorldPoint; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class NmzScriptTest { + + @Test + void unknownPlayerLocationIsNotOutside() { + assertFalse(NmzScript.isNearLobby(null)); + } + + @Test + void locationNearLobbyIsOutside() { + assertTrue(NmzScript.isNearLobby(new WorldPoint(2609, 3114, 0))); + } + + @Test + void locationInsideDreamIsNotOutside() { + assertFalse(NmzScript.isNearLobby(new WorldPoint(2273, 4694, 0))); + } +} From 5561590a96911905adc9cf5026f1f9808f652194 Mon Sep 17 00:00:00 2001 From: chsami Date: Thu, 15 Oct 2026 09:12:24 +0200 Subject: [PATCH 17/38] fix(woodcutting): guard null return point in overlay and walk-back (1.8.7) (#630) * fix(woodcutting): guard null return point in overlay and walk-back (1.8.7) getReturnPoint falls back to Rs2Player.getWorldLocation(), which is null while the local player is not loaded. The overlay passed it straight into LocalPoint.fromWorld and threw an NPE every frame; walkBack dereferenced it the same way. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV * test(woodcutting): isolate overlay test from leaked static return points Other plugin tests in the full suite leave Script.initialPlayerLocation set, so getReturnPoint returned a stale non-null point in CI. Clear and restore AutoWoodcuttingScript's static return points around the render. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV --------- Co-authored-by: Claude Opus 5.5 --- .../woodcutting/AutoWoodcuttingOverlay.java | 6 +- .../woodcutting/AutoWoodcuttingPlugin.java | 2 +- .../woodcutting/AutoWoodcuttingScript.java | 11 ++- .../AutoWoodcuttingOverlayTest.java | 82 +++++++++++++++++++ 4 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingOverlayTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingOverlay.java b/src/main/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingOverlay.java index 2175fe955b..5fcf0192f4 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingOverlay.java +++ b/src/main/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingOverlay.java @@ -4,6 +4,7 @@ import net.runelite.api.Perspective; import net.runelite.api.Skill; import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.woodcutting.enums.ForestryEvents; import net.runelite.client.plugins.microbot.woodcutting.enums.WoodcuttingTree; @@ -185,8 +186,9 @@ public Dimension render(Graphics2D graphics) { .build()); // Display area boundary - if (config.distanceToStray() < 21) { - LocalPoint lp = LocalPoint.fromWorld(client, AutoWoodcuttingScript.getReturnPoint(config)); + WorldPoint returnPoint = AutoWoodcuttingScript.getReturnPoint(config); + if (config.distanceToStray() < 21 && returnPoint != null) { + LocalPoint lp = LocalPoint.fromWorld(client, returnPoint); if (lp != null) { Polygon poly = Perspective.getCanvasTileAreaPoly(client, lp, config.distanceToStray() * 2); diff --git a/src/main/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingPlugin.java index 310c8707b4..7ecc604ffe 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingPlugin.java @@ -50,7 +50,7 @@ ) @Slf4j public class AutoWoodcuttingPlugin extends Plugin { - public static final String version = "1.8.6"; + public static final String version = "1.8.7"; @Inject @Getter(AccessLevel.MODULE) public AutoWoodcuttingScript autoWoodcuttingScript; diff --git a/src/main/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingScript.java b/src/main/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingScript.java index 1be35c8536..15da87dccb 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingScript.java @@ -495,8 +495,15 @@ private boolean isFlectching() { } private void walkBack(AutoWoodcuttingConfig config) { - Rs2Walker.walkTo(new WorldPoint(getReturnPoint(config).getX() - Rs2Random.between(-1, 1), getReturnPoint(config).getY() - Rs2Random.between(-1, 1), getReturnPoint(config).getPlane())); - sleepUntil(() -> Rs2Player.getWorldLocation().distanceTo(getReturnPoint(config)) <= 4); + WorldPoint target = getReturnPoint(config); + if (target == null) { + return; + } + Rs2Walker.walkTo(new WorldPoint(target.getX() - Rs2Random.between(-1, 1), target.getY() - Rs2Random.between(-1, 1), target.getPlane())); + sleepUntil(() -> { + WorldPoint playerLocation = Rs2Player.getWorldLocation(); + return playerLocation != null && playerLocation.distanceTo(target) <= 4; + }); } /** diff --git a/src/test/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingOverlayTest.java b/src/test/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingOverlayTest.java new file mode 100644 index 0000000000..1b55a80e5c --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingOverlayTest.java @@ -0,0 +1,82 @@ +package net.runelite.client.plugins.microbot.woodcutting; + +import net.runelite.api.Client; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.woodcutting.enums.WoodcuttingPrimaryAction; +import net.runelite.client.plugins.microbot.woodcutting.enums.WoodcuttingScriptState; +import net.runelite.client.plugins.microbot.woodcutting.enums.WoodcuttingSecondaryAction; +import net.runelite.client.plugins.microbot.woodcutting.enums.WoodcuttingWalkBack; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.lang.reflect.Field; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class AutoWoodcuttingOverlayTest { + + @Test + void renderSkipsAreaWhenReturnPointIsUnavailable() throws Exception { + Client client = mock(Client.class); + when(client.findWorldViewFromWorldPoint(isNull())).thenThrow(new NullPointerException()); + + AutoWoodcuttingConfig config = mock(AutoWoodcuttingConfig.class); + when(config.distanceToStray()).thenReturn(10); + when(config.walkBack()).thenReturn(WoodcuttingWalkBack.INITIAL_LOCATION); + when(config.primaryAction()).thenReturn(WoodcuttingPrimaryAction.BANK); + when(config.secondaryAction()).thenReturn(WoodcuttingSecondaryAction.NONE); + + AutoWoodcuttingPlugin plugin = mock(AutoWoodcuttingPlugin.class); + AutoWoodcuttingScript script = mock(AutoWoodcuttingScript.class); + script.woodcuttingScriptState = WoodcuttingScriptState.WOODCUTTING; + plugin.autoWoodcuttingScript = script; + + Field initialLocation = findField(AutoWoodcuttingScript.class, "initialPlayerLocation"); + Field returnPoint = findField(AutoWoodcuttingScript.class, "returnPoint"); + Object savedInitialLocation = initialLocation.get(null); + Object savedReturnPoint = returnPoint.get(null); + initialLocation.set(null, null); + returnPoint.set(null, null); + + Graphics2D graphics = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).createGraphics(); + try (MockedStatic microbot = mockStatic(Microbot.class, CALLS_REAL_METHODS); + MockedStatic player = mockStatic(Rs2Player.class)) { + microbot.when(Microbot::isLoggedIn).thenReturn(true); + microbot.when(() -> Microbot.logStackTrace(anyString(), any(Exception.class))).thenAnswer(invocation -> null); + player.when(Rs2Player::getWorldLocation).thenReturn(null); + + new AutoWoodcuttingOverlay(plugin, config, client).render(graphics); + + microbot.verify(() -> Microbot.logStackTrace(anyString(), any(Exception.class)), never()); + verify(client, never()).findWorldViewFromWorldPoint(any(WorldPoint.class)); + } finally { + graphics.dispose(); + initialLocation.set(null, savedInitialLocation); + returnPoint.set(null, savedReturnPoint); + } + } + + private static Field findField(Class type, String name) throws NoSuchFieldException { + for (Class current = type; current != null; current = current.getSuperclass()) { + try { + Field field = current.getDeclaredField(name); + field.setAccessible(true); + return field; + } catch (NoSuchFieldException ignored) { + } + } + throw new NoSuchFieldException(name); + } +} From c72e51a889068db585d1a7851104217db1cee16c Mon Sep 17 00:00:00 2001 From: chsami Date: Thu, 15 Oct 2026 09:41:38 +0200 Subject: [PATCH 18/38] fix(mahoganyhomes): skip tick while player location is unknown (0.0.14) (#631) * fix(mahoganyhomes): skip contract lookup while player location is unknown (0.0.14) getClosestContractLocation compared each office with Rs2Player.getWorldLocation(), which is null during loading or a world hop, so WorldPoint.distanceTo2D threw an NPE and failed the loop iteration. Read the player location once, return null from the helper when it is missing, and skip the tick in getNewContract. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV * fix(mahoganyhomes): skip the whole tick while player location is unknown bank() and walkToHome() pass the player location to distanceBetween and fix()/finish() read its plane, so they also threw during loading or a world hop. Return early from the loop when the location is null; the getClosestContractLocation null check stays as a second guard. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV --------- Co-authored-by: Claude Opus 5.5 --- .../mahoganyhomez/MahoganyHomesPlugin.java | 2 +- .../mahoganyhomez/MahoganyHomesScript.java | 16 ++++++++--- .../MahoganyHomesScriptTest.java | 27 +++++++++++++++++++ 3 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScriptTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesPlugin.java index 772d69c1cc..5b2326c9ba 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesPlugin.java @@ -55,7 +55,7 @@ isExternal = PluginConstants.IS_EXTERNAL ) public class MahoganyHomesPlugin extends Plugin { - public static final String version = "0.0.13"; + public static final String version = "0.0.14"; private static final List PLANKS = Arrays.asList(ItemID.PLANK, ItemID.OAK_PLANK, ItemID.TEAK_PLANK, ItemID.MAHOGANY_PLANK); private static final List PLANK_NAMES = Arrays.asList("Plank", "Oak plank", "Teak plank", "Mahogany plank"); private static final Map MAHOGANY_HOMES_REPAIRS = new HashMap<>(); diff --git a/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScript.java b/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScript.java index d78aaa70ad..7fa515249e 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScript.java @@ -53,6 +53,7 @@ public boolean run(MahoganyHomesConfig config) { try { if (!Microbot.isLoggedIn()) return; if (!super.run()) return; + if (Rs2Player.getWorldLocation() == null) return; checkPlankSack(); fix(); finish(); @@ -393,8 +394,12 @@ private void getNewContract() { } return; } - WorldPoint contractLocation = getClosestContractLocation(); - if (contractLocation.distanceTo2D(Rs2Player.getWorldLocation()) > 10) { + WorldPoint playerLocation = Rs2Player.getWorldLocation(); + WorldPoint contractLocation = getClosestContractLocation(playerLocation); + if (contractLocation == null) { + return; + } + if (contractLocation.distanceTo2D(playerLocation) > 10) { log("Walking to contract NPC"); Rs2Walker.walkWithState(contractLocation, 5); @@ -642,7 +647,10 @@ private int steelBarsInInventory() { } // Get closest contract location - private WorldPoint getClosestContractLocation() { + static WorldPoint getClosestContractLocation(WorldPoint playerLocation) { + if (playerLocation == null) { + return null; + } List contractLocations = new ArrayList<>(); contractLocations.add(ContractLocation.MAHOGANY_HOMES_ARDOUGNE.getLocation()); contractLocations.add(ContractLocation.MAHOGANY_HOMES_FALADOR.getLocation()); @@ -650,7 +658,7 @@ private WorldPoint getClosestContractLocation() { contractLocations.add(ContractLocation.MAHOGANY_HOMES_VARROCK.getLocation()); return contractLocations.stream() - .min(Comparator.comparingInt(wp -> wp.distanceTo2D(Rs2Player.getWorldLocation()))) + .min(Comparator.comparingInt(wp -> wp.distanceTo2D(playerLocation))) .orElse(null); } diff --git a/src/test/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScriptTest.java b/src/test/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScriptTest.java new file mode 100644 index 0000000000..fa6272ff6c --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScriptTest.java @@ -0,0 +1,27 @@ +package net.runelite.client.plugins.microbot.mahoganyhomez; + +import net.runelite.api.coords.WorldPoint; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class MahoganyHomesScriptTest { + + @Test + void closestContractLocationIsNullWithoutPlayerLocation() { + assertNull(MahoganyHomesScript.getClosestContractLocation(null)); + } + + @Test + void closestContractLocationPicksNearestOffice() { + assertEquals(ContractLocation.MAHOGANY_HOMES_VARROCK.getLocation(), + MahoganyHomesScript.getClosestContractLocation(new WorldPoint(3213, 3424, 0))); + assertEquals(ContractLocation.MAHOGANY_HOMES_FALADOR.getLocation(), + MahoganyHomesScript.getClosestContractLocation(new WorldPoint(2965, 3380, 0))); + assertEquals(ContractLocation.MAHOGANY_HOMES_ARDOUGNE.getLocation(), + MahoganyHomesScript.getClosestContractLocation(new WorldPoint(2662, 3305, 0))); + assertEquals(ContractLocation.MAHOGANY_HOMES_HOSIDIUS.getLocation(), + MahoganyHomesScript.getClosestContractLocation(new WorldPoint(1760, 3600, 0))); + } +} From a0759486e0617cb41552f41091275c814513978c Mon Sep 17 00:00:00 2001 From: chsami Date: Thu, 15 Oct 2026 10:01:12 +0200 Subject: [PATCH 19/38] fix(aiofighter): skip attack style lookup when enum or struct is unavailable (2.2.7) (#632) * fix(aiofighter): skip attack style lookup when enum or struct is unavailable (2.2.7) getWeaponTypeStyles dereferenced Microbot.getEnum and getStructComposition results without null checks; both return null when the client-thread lookup fails, throwing NPEs from the AttackStyleScript task. Return an empty style array instead so updateAttackStyle leaves the current style untouched. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV * test(aiofighter): cover null StructComposition before a named style Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV --------- Co-authored-by: Claude Opus 5.5 --- .../microbot/aiofighter/AIOFighterPlugin.java | 2 +- .../aiofighter/skill/AttackStyleScript.java | 19 ++++- .../skill/AttackStyleScriptTest.java | 84 +++++++++++++++++++ 3 files changed, 100 insertions(+), 5 deletions(-) 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 2d4e37e9ab..1c3f7a7d5c 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 @@ -62,7 +62,7 @@ ) @Slf4j public class AIOFighterPlugin extends Plugin { - public static final String version = "2.2.6"; + public static final String version = "2.2.7"; 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/skill/AttackStyleScript.java b/src/main/java/net/runelite/client/plugins/microbot/aiofighter/skill/AttackStyleScript.java index 61c8e1aa8d..45b6d4f17b 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/aiofighter/skill/AttackStyleScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/aiofighter/skill/AttackStyleScript.java @@ -197,14 +197,25 @@ private void updateAttackStyle(int equippedWeaponType, int attackStyleIndex, int } } - private AttackStyle[] getWeaponTypeStyles(int weaponType) { - int weaponStyleEnum = Microbot.getEnum(EnumID.WEAPON_STYLES).getIntValue(weaponType); - int[] weaponStyleStructs = Microbot.getEnum(weaponStyleEnum).getIntVals(); + static AttackStyle[] getWeaponTypeStyles(int weaponType) { + EnumComposition weaponStylesEnum = Microbot.getEnum(EnumID.WEAPON_STYLES); + if (weaponStylesEnum == null) { + return new AttackStyle[0]; + } + EnumComposition weaponStyleEnum = Microbot.getEnum(weaponStylesEnum.getIntValue(weaponType)); + if (weaponStyleEnum == null) { + return new AttackStyle[0]; + } + int[] weaponStyleStructs = weaponStyleEnum.getIntVals(); AttackStyle[] styles = new AttackStyle[weaponStyleStructs.length]; int i = 0; for (int style : weaponStyleStructs) { - String attackStyleName = Microbot.getStructComposition(style).getStringValue(ParamID.ATTACK_STYLE_NAME); + StructComposition struct = Microbot.getStructComposition(style); + String attackStyleName = struct == null ? null : struct.getStringValue(ParamID.ATTACK_STYLE_NAME); + if (attackStyleName == null) { + return new AttackStyle[0]; + } AttackStyle attackStyle = AttackStyle.valueOf(attackStyleName.toUpperCase()); if (attackStyle == AttackStyle.OTHER) { diff --git a/src/test/java/net/runelite/client/plugins/microbot/aiofighter/skill/AttackStyleScriptTest.java b/src/test/java/net/runelite/client/plugins/microbot/aiofighter/skill/AttackStyleScriptTest.java index 83b2ee3a07..5d39231428 100644 --- a/src/test/java/net/runelite/client/plugins/microbot/aiofighter/skill/AttackStyleScriptTest.java +++ b/src/test/java/net/runelite/client/plugins/microbot/aiofighter/skill/AttackStyleScriptTest.java @@ -1,16 +1,28 @@ package net.runelite.client.plugins.microbot.aiofighter.skill; +import net.runelite.api.EnumComposition; +import net.runelite.api.EnumID; +import net.runelite.api.ParamID; +import net.runelite.api.StructComposition; import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.plugins.microbot.Microbot; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Random; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; public final class AttackStyleScriptTest { @@ -33,4 +45,76 @@ void pickComponentReturnsOneOfCandidates() { assertTrue(candidates.contains(AttackStyleScript.pickComponent(candidates, new Random(seed)))); } } + + @Test + void weaponTypeStylesEmptyWhenWeaponStylesEnumMissing() { + try (MockedStatic microbot = mockStatic(Microbot.class)) { + microbot.when(() -> Microbot.getEnum(anyInt())).thenReturn(null); + assertEquals(0, AttackStyleScript.getWeaponTypeStyles(0).length); + } + } + + @Test + void weaponTypeStylesEmptyWhenWeaponEnumMissing() { + EnumComposition weaponStyles = mock(EnumComposition.class); + when(weaponStyles.getIntValue(0)).thenReturn(5000); + try (MockedStatic microbot = mockStatic(Microbot.class)) { + microbot.when(() -> Microbot.getEnum(EnumID.WEAPON_STYLES)).thenReturn(weaponStyles); + microbot.when(() -> Microbot.getEnum(5000)).thenReturn(null); + assertEquals(0, AttackStyleScript.getWeaponTypeStyles(0).length); + } + } + + @Test + void weaponTypeStylesEmptyWhenStructOrNameMissing() { + EnumComposition weaponStyles = mock(EnumComposition.class); + when(weaponStyles.getIntValue(0)).thenReturn(5000); + EnumComposition weaponEnum = mock(EnumComposition.class); + when(weaponEnum.getIntVals()).thenReturn(new int[]{10, 11}); + StructComposition unnamed = mock(StructComposition.class); + try (MockedStatic microbot = mockStatic(Microbot.class)) { + microbot.when(() -> Microbot.getEnum(EnumID.WEAPON_STYLES)).thenReturn(weaponStyles); + microbot.when(() -> Microbot.getEnum(5000)).thenReturn(weaponEnum); + microbot.when(() -> Microbot.getStructComposition(10)).thenReturn(unnamed); + microbot.when(() -> Microbot.getStructComposition(11)).thenReturn(null); + assertEquals(0, AttackStyleScript.getWeaponTypeStyles(0).length); + } + } + + @Test + void weaponTypeStylesEmptyWhenStructMissing() { + EnumComposition weaponStyles = mock(EnumComposition.class); + when(weaponStyles.getIntValue(0)).thenReturn(5000); + EnumComposition weaponEnum = mock(EnumComposition.class); + when(weaponEnum.getIntVals()).thenReturn(new int[]{10, 11}); + StructComposition named = mock(StructComposition.class); + when(named.getStringValue(ParamID.ATTACK_STYLE_NAME)).thenReturn("Accurate"); + try (MockedStatic microbot = mockStatic(Microbot.class)) { + microbot.when(() -> Microbot.getEnum(EnumID.WEAPON_STYLES)).thenReturn(weaponStyles); + microbot.when(() -> Microbot.getEnum(5000)).thenReturn(weaponEnum); + microbot.when(() -> Microbot.getStructComposition(10)).thenReturn(null); + microbot.when(() -> Microbot.getStructComposition(11)).thenReturn(named); + assertEquals(0, AttackStyleScript.getWeaponTypeStyles(0).length); + } + } + + @Test + void weaponTypeStylesResolvedFromStructs() { + EnumComposition weaponStyles = mock(EnumComposition.class); + when(weaponStyles.getIntValue(0)).thenReturn(5000); + EnumComposition weaponEnum = mock(EnumComposition.class); + when(weaponEnum.getIntVals()).thenReturn(new int[]{10, 11}); + StructComposition accurate = mock(StructComposition.class); + when(accurate.getStringValue(ParamID.ATTACK_STYLE_NAME)).thenReturn("Accurate"); + StructComposition aggressive = mock(StructComposition.class); + when(aggressive.getStringValue(ParamID.ATTACK_STYLE_NAME)).thenReturn("Aggressive"); + try (MockedStatic microbot = mockStatic(Microbot.class)) { + microbot.when(() -> Microbot.getEnum(EnumID.WEAPON_STYLES)).thenReturn(weaponStyles); + microbot.when(() -> Microbot.getEnum(5000)).thenReturn(weaponEnum); + microbot.when(() -> Microbot.getStructComposition(10)).thenReturn(accurate); + microbot.when(() -> Microbot.getStructComposition(11)).thenReturn(aggressive); + assertArrayEquals(new AttackStyle[]{AttackStyle.ACCURATE, AttackStyle.AGGRESSIVE}, + AttackStyleScript.getWeaponTypeStyles(0)); + } + } } From 99235d8bb71a4dd828e7610491f5c39e50f28d11 Mon Sep 17 00:00:00 2001 From: chsami Date: Thu, 15 Oct 2026 10:11:06 +0200 Subject: [PATCH 20/38] fix(aiofighter): skip slayer finishing item when dying NPC has no name (2.2.8) (#633) handleItemOnNpcToKill called npc.getName().equalsIgnoreCase(...) on a dead NPC interacting with the player. getName() returns null once the NPC's composition is gone during death/despawn, throwing an NPE on the AttackNpcScript thread. Move the item choice into a pure helper that returns early on a null name, and cover it with unit tests. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../microbot/aiofighter/AIOFighterPlugin.java | 2 +- .../aiofighter/combat/AttackNpcScript.java | 27 ++++++++++++------- .../combat/AttackNpcScriptTest.java | 26 ++++++++++++++++++ 3 files changed, 44 insertions(+), 11 deletions(-) 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 1c3f7a7d5c..842016ac5f 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 @@ -62,7 +62,7 @@ ) @Slf4j public class AIOFighterPlugin extends Plugin { - public static final String version = "2.2.7"; + public static final String version = "2.2.8"; 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/combat/AttackNpcScript.java b/src/main/java/net/runelite/client/plugins/microbot/aiofighter/combat/AttackNpcScript.java index 7fbb9fc19a..277f3cb784 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/aiofighter/combat/AttackNpcScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/aiofighter/combat/AttackNpcScript.java @@ -44,6 +44,7 @@ import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.IntUnaryOperator; import java.util.function.ToIntFunction; import java.util.stream.Collectors; @@ -323,20 +324,26 @@ private void handleItemOnNpcToKill(AIOFighterConfig config) { .where(n -> n.isDead() && Objects.equals(n.getInteracting(), localPlayer)) .first(); if (npc == null) return; - List lizardVariants = new ArrayList<>(Arrays.asList("Lizard", "Desert Lizard", "Small Lizard")); + int itemId = finishingItemFor(npc.getName(), npc.getHealthRatio(), Microbot::getVarbitValue); + if (itemId == -1) return; // Rs2Inventory.useItemOnNpc only accepts the legacy util.npc.Rs2NpcModel — wrap our underlying NPC at the call site. net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel legacyNpc = new net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel(npc.getNpc()); - if (Microbot.getVarbitValue(SLAYER_AUTOKILL_DESERTLIZARDS) == 0 && lizardVariants.contains(npc.getName()) && npc.getHealthRatio() < 5) { - Rs2Inventory.useItemOnNpc(ItemID.SLAYER_ICY_WATER, legacyNpc); - Rs2Player.waitForAnimation(); - } else if (Microbot.getVarbitValue(SLAYER_AUTOKILL_ROCKSLUGS) == 0 && npc.getName().equalsIgnoreCase("rockslug") && npc.getHealthRatio() < 5) { - Rs2Inventory.useItemOnNpc(ItemID.SLAYER_BAG_OF_SALT, legacyNpc); - Rs2Player.waitForAnimation(); - } else if (Microbot.getVarbitValue(SLAYER_AUTOKILL_GARGOYLES) == 0 && npc.getName().equalsIgnoreCase("gargoyle") && npc.getHealthRatio() < 3) { - Rs2Inventory.useItemOnNpc(ItemID.SLAYER_ROCK_HAMMER, legacyNpc); - Rs2Player.waitForAnimation(); + Rs2Inventory.useItemOnNpc(itemId, legacyNpc); + Rs2Player.waitForAnimation(); + } + + static int finishingItemFor(String name, int healthRatio, IntUnaryOperator varbits) { + if (name == null) return -1; + List lizardVariants = Arrays.asList("Lizard", "Desert Lizard", "Small Lizard"); + if (varbits.applyAsInt(SLAYER_AUTOKILL_DESERTLIZARDS) == 0 && lizardVariants.contains(name) && healthRatio < 5) { + return ItemID.SLAYER_ICY_WATER; + } else if (varbits.applyAsInt(SLAYER_AUTOKILL_ROCKSLUGS) == 0 && name.equalsIgnoreCase("rockslug") && healthRatio < 5) { + return ItemID.SLAYER_BAG_OF_SALT; + } else if (varbits.applyAsInt(SLAYER_AUTOKILL_GARGOYLES) == 0 && name.equalsIgnoreCase("gargoyle") && healthRatio < 3) { + return ItemID.SLAYER_ROCK_HAMMER; } + return -1; } /** diff --git a/src/test/java/net/runelite/client/plugins/microbot/aiofighter/combat/AttackNpcScriptTest.java b/src/test/java/net/runelite/client/plugins/microbot/aiofighter/combat/AttackNpcScriptTest.java index ff32d9043b..0e1ce00795 100644 --- a/src/test/java/net/runelite/client/plugins/microbot/aiofighter/combat/AttackNpcScriptTest.java +++ b/src/test/java/net/runelite/client/plugins/microbot/aiofighter/combat/AttackNpcScriptTest.java @@ -1,5 +1,6 @@ package net.runelite.client.plugins.microbot.aiofighter.combat; +import net.runelite.api.gameval.ItemID; import org.junit.jupiter.api.Test; import java.util.ArrayList; @@ -8,6 +9,7 @@ import java.util.List; import java.util.Map; import java.util.Random; +import java.util.function.IntUnaryOperator; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -79,4 +81,28 @@ void readsEachKeyOncePerItemSoLiveKeysCannotBreakTheSort() { assertTrue(secondaryReads.values().stream().allMatch(count -> count == 1)); assertEquals(items.size(), primaryReads.size()); } + + private static final IntUnaryOperator AUTOKILL_OFF = varbit -> 0; + private static final IntUnaryOperator AUTOKILL_ON = varbit -> 1; + + @Test + void skipsNpcWhoseNameIsGoneWhileDespawning() { + assertEquals(-1, AttackNpcScript.finishingItemFor(null, 1, AUTOKILL_OFF)); + assertEquals(-1, AttackNpcScript.finishingItemFor(null, 0, AUTOKILL_ON)); + } + + @Test + void picksFinishingItemForSlayerNpcsNearDeath() { + assertEquals(ItemID.SLAYER_ICY_WATER, AttackNpcScript.finishingItemFor("Desert Lizard", 4, AUTOKILL_OFF)); + assertEquals(ItemID.SLAYER_BAG_OF_SALT, AttackNpcScript.finishingItemFor("Rockslug", 4, AUTOKILL_OFF)); + assertEquals(ItemID.SLAYER_ROCK_HAMMER, AttackNpcScript.finishingItemFor("Gargoyle", 2, AUTOKILL_OFF)); + } + + @Test + void skipsFinishingItemWhenHealthyAutokillEnabledOrOtherNpc() { + assertEquals(-1, AttackNpcScript.finishingItemFor("Rockslug", 5, AUTOKILL_OFF)); + assertEquals(-1, AttackNpcScript.finishingItemFor("Gargoyle", 3, AUTOKILL_OFF)); + assertEquals(-1, AttackNpcScript.finishingItemFor("Rockslug", 1, AUTOKILL_ON)); + assertEquals(-1, AttackNpcScript.finishingItemFor("Goblin", 0, AUTOKILL_OFF)); + } } From 2d5cd87d0b9ee9401807c699fc24abd01f749fdf Mon Sep 17 00:00:00 2001 From: chsami Date: Thu, 15 Oct 2026 10:19:50 +0200 Subject: [PATCH 21/38] fix(cluesolver): skip typing when anagram has no answer (1.0.7) (#634) AnagramClueTask.handleDialogue passed clue.getAnswer() straight to Rs2Keyboard.typeString, which NPEs for anagrams without a challenge answer. Only type and submit when the answer is non-blank; otherwise just click through the dialogue. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../microbot/cluesolver/ClueSolverPlugin.java | 2 +- .../cluesolver/cluetask/AnagramClueTask.java | 8 +- .../cluesolver/AnagramClueTaskTest.java | 143 ++++++++++++++++++ 3 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/cluesolver/AnagramClueTaskTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/cluesolver/ClueSolverPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/cluesolver/ClueSolverPlugin.java index e1f24acd9d..f84df3f516 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/cluesolver/ClueSolverPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/cluesolver/ClueSolverPlugin.java @@ -29,7 +29,7 @@ @PluginDependency(ClueScrollPlugin.class) public class ClueSolverPlugin extends Plugin { - final static String version = "1.0.6"; + final static String version = "1.0.7"; @Inject private ClueSolverScript clueSolverScript; diff --git a/src/main/java/net/runelite/client/plugins/microbot/cluesolver/cluetask/AnagramClueTask.java b/src/main/java/net/runelite/client/plugins/microbot/cluesolver/cluetask/AnagramClueTask.java index 6f97c3ccfb..0cad7036cb 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/cluesolver/cluetask/AnagramClueTask.java +++ b/src/main/java/net/runelite/client/plugins/microbot/cluesolver/cluetask/AnagramClueTask.java @@ -183,9 +183,11 @@ private boolean handleDialogue() { Rs2Dialogue.clickContinue(); } - Rs2Keyboard.typeString(answer); - Rs2Keyboard.enter(); - Rs2Dialogue.sleepUntilHasContinue(); + if (answer != null && !answer.isBlank()) { + Rs2Keyboard.typeString(answer); + Rs2Keyboard.enter(); + Rs2Dialogue.sleepUntilHasContinue(); + } if (Rs2Dialogue.hasContinue()) { Rs2Dialogue.clickContinue(); diff --git a/src/test/java/net/runelite/client/plugins/microbot/cluesolver/AnagramClueTaskTest.java b/src/test/java/net/runelite/client/plugins/microbot/cluesolver/AnagramClueTaskTest.java new file mode 100644 index 0000000000..866e84e6c0 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/cluesolver/AnagramClueTaskTest.java @@ -0,0 +1,143 @@ +package net.runelite.client.plugins.microbot.cluesolver; + +import net.runelite.api.Client; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.GameTick; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.eventbus.EventBus; +import net.runelite.client.plugins.cluescrolls.ClueScrollPlugin; +import net.runelite.client.plugins.cluescrolls.clues.AnagramClue; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.cluesolver.cluetask.AnagramClueTask; +import net.runelite.client.plugins.microbot.util.dialogues.Rs2Dialogue; +import net.runelite.client.plugins.microbot.util.keyboard.Rs2Keyboard; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.when; + +class AnagramClueTaskTest { + + private static final WorldPoint LOCATION = new WorldPoint(3222, 3218, 0); + + private MockedStatic microbot; + private MockedStatic dialogue; + private MockedStatic keyboard; + private ClueScrollPlugin clueScrollPlugin; + private AnagramClue clue; + + @BeforeEach + void setUp() { + ClientThread clientThread = mock(ClientThread.class); + when(clientThread.runOnClientThreadOptional(any())).thenReturn(Optional.of(LOCATION)); + microbot = mockStatic(Microbot.class); + microbot.when(Microbot::getClientThread).thenReturn(clientThread); + dialogue = mockStatic(Rs2Dialogue.class); + dialogue.when(Rs2Dialogue::isInDialogue).thenReturn(true); + keyboard = mockStatic(Rs2Keyboard.class); + + clueScrollPlugin = mock(ClueScrollPlugin.class); + clue = mock(AnagramClue.class); + when(clue.getLocation(clueScrollPlugin)).thenReturn(LOCATION); + } + + @AfterEach + void tearDown() { + keyboard.close(); + dialogue.close(); + microbot.close(); + } + + @Test + void anagramWithoutAnswerClicksThroughDialogueWithoutTyping() throws Exception { + when(clue.getAnswer(clueScrollPlugin)).thenReturn(null); + CompletableFuture result = runDialogueTick(); + + keyboard.verify(() -> Rs2Keyboard.typeString(any()), never()); + keyboard.verify(Rs2Keyboard::enter, never()); + assertTrue(result.isDone()); + assertTrue(result.join()); + } + + @Test + void anagramWithBlankAnswerDoesNotType() throws Exception { + when(clue.getAnswer(clueScrollPlugin)).thenReturn(" "); + CompletableFuture result = runDialogueTick(); + + keyboard.verify(() -> Rs2Keyboard.typeString(any()), never()); + assertTrue(result.join()); + } + + @Test + void anagramWithAnswerTypesItAndSubmits() throws Exception { + when(clue.getAnswer(clueScrollPlugin)).thenReturn("6"); + CompletableFuture result = runDialogueTick(); + + keyboard.verify(() -> Rs2Keyboard.typeString("6")); + keyboard.verify(Rs2Keyboard::enter); + assertTrue(result.join()); + } + + private CompletableFuture runDialogueTick() throws Exception { + AnagramClueTask task = new AnagramClueTask(mock(Client.class), clue, clueScrollPlugin, null, + mock(EventBus.class), new DirectExecutor()); + CompletableFuture result = new CompletableFuture<>(); + task.setFuture(result); + setState(task, "HANDLING_DIALOGUE"); + task.onGameTick(new GameTick()); + return result; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static void setState(AnagramClueTask task, String name) throws Exception { + Field field = AnagramClueTask.class.getDeclaredField("state"); + field.setAccessible(true); + field.set(task, Enum.valueOf((Class) field.getType(), name)); + } + + private static final class DirectExecutor extends AbstractExecutorService { + @Override + public void execute(Runnable command) { + command.run(); + } + + @Override + public void shutdown() { + } + + @Override + public List shutdownNow() { + return Collections.emptyList(); + } + + @Override + public boolean isShutdown() { + return false; + } + + @Override + public boolean isTerminated() { + return false; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) { + return true; + } + } +} From 1c6caed19d00195e2f9dac02ae52bb1ec0e9e745 Mon Sep 17 00:00:00 2001 From: chsami Date: Thu, 15 Oct 2026 11:54:04 +0200 Subject: [PATCH 22/38] fix(scurrius): tolerate inverted eat/prayer ranges and null player location (1.0.3) (#635) * fix(scurrius): tolerate inverted eat/prayer ranges and null player location (1.0.3) Order the configured min/max eat and prayer percents before picking a random threshold so min > max no longer throws IllegalArgumentException, and treat a null player location as outside the fight room during load or hop. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV * fix(scurrius): skip the tick while the player location is unknown Return early at the top of the loop when Rs2Player.getWorldLocation() is null during load or hop, so the state machine does not treat an unknown location as outside the fight room and the dodge loop does not dereference it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV --------- Co-authored-by: Claude Opus 5.5 --- .../microbot/scurrius/ScurriusPlugin.java | 2 +- .../microbot/scurrius/ScurriusScript.java | 21 ++++++--- .../microbot/scurrius/ScurriusScriptTest.java | 46 +++++++++++++++++++ 3 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/scurrius/ScurriusScriptTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/scurrius/ScurriusPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/scurrius/ScurriusPlugin.java index b23f8b71e2..b9b3279915 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/scurrius/ScurriusPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/scurrius/ScurriusPlugin.java @@ -32,7 +32,7 @@ ) @Slf4j public class ScurriusPlugin extends Plugin { - public static final String version = "1.0.2"; + public static final String version = "1.0.3"; @Inject private ScurriusConfig config; @Provides diff --git a/src/main/java/net/runelite/client/plugins/microbot/scurrius/ScurriusScript.java b/src/main/java/net/runelite/client/plugins/microbot/scurrius/ScurriusScript.java index 28619c4ed7..03cb0ca51a 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/scurrius/ScurriusScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/scurrius/ScurriusScript.java @@ -59,6 +59,7 @@ public boolean run(ScurriusConfig config) { try { if (!Microbot.isLoggedIn()) return; if (!super.run()) return; + if (Rs2Player.getWorldLocation() == null) return; long startTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis(); @@ -169,9 +170,7 @@ public boolean run(ScurriusConfig config) { } if (currentTime - lastEatTime > EAT_COOLDOWN_MS) { - int minEat = config.minEatPercent(); - int maxEat = config.maxEatPercent(); - int randomEatThreshold = ThreadLocalRandom.current().nextInt(minEat, maxEat + 1); + int randomEatThreshold = randomThreshold(config.minEatPercent(), config.maxEatPercent()); if (Microbot.getClient().getBoostedSkillLevel(Skill.HITPOINTS) < randomEatThreshold && !Rs2Player.isAnimating()) { Rs2Player.eatAt(randomEatThreshold); @@ -181,9 +180,7 @@ public boolean run(ScurriusConfig config) { } if (currentTime - lastPrayerTime > PRAYER_COOLDOWN_MS) { - int minPrayer = config.minPrayerPercent(); - int maxPrayer = config.maxPrayerPercent(); - int randomPrayerThreshold = ThreadLocalRandom.current().nextInt(minPrayer, maxPrayer + 1); + int randomPrayerThreshold = randomThreshold(config.minPrayerPercent(), config.maxPrayerPercent()); if (Microbot.getClient().getBoostedSkillLevel(Skill.PRAYER) < randomPrayerThreshold && !Rs2Player.isAnimating()) { Rs2Player.drinkPrayerPotionAt(randomPrayerThreshold); @@ -271,7 +268,17 @@ private String getStateDescription(State state) { } private boolean isInFightRoom() { - return fightRoom.contains(Rs2Player.getWorldLocation()); + return isInArea(fightRoom, Rs2Player.getWorldLocation()); + } + + static boolean isInArea(WorldArea area, WorldPoint location) { + return location != null && area.contains(location); + } + + static int randomThreshold(int first, int second) { + int low = Math.min(first, second); + int high = Math.max(first, second); + return ThreadLocalRandom.current().nextInt(low, high + 1); } private WorldPoint findSafeTile(WorldPoint playerLocation, List dangerousWorldPoints) { diff --git a/src/test/java/net/runelite/client/plugins/microbot/scurrius/ScurriusScriptTest.java b/src/test/java/net/runelite/client/plugins/microbot/scurrius/ScurriusScriptTest.java new file mode 100644 index 0000000000..e4432ab7e2 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/scurrius/ScurriusScriptTest.java @@ -0,0 +1,46 @@ +package net.runelite.client.plugins.microbot.scurrius; + +import net.runelite.api.coords.WorldArea; +import net.runelite.api.coords.WorldPoint; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ScurriusScriptTest { + + private final WorldArea fightRoom = new WorldArea(new WorldPoint(3290, 9860, 0), 17, 17); + + @Test + void thresholdStaysInRangeWhenMinExceedsMax() { + for (int i = 0; i < 500; i++) { + int threshold = ScurriusScript.randomThreshold(60, 40); + assertTrue(threshold >= 40 && threshold <= 60); + } + } + + @Test + void thresholdStaysInRangeWhenOrdered() { + for (int i = 0; i < 500; i++) { + int threshold = ScurriusScript.randomThreshold(40, 60); + assertTrue(threshold >= 40 && threshold <= 60); + } + } + + @Test + void equalBoundsReturnThatValue() { + assertEquals(50, ScurriusScript.randomThreshold(50, 50)); + } + + @Test + void nullLocationIsNotInFightRoom() { + assertFalse(ScurriusScript.isInArea(fightRoom, null)); + } + + @Test + void locationInsideFightRoomIsDetected() { + assertTrue(ScurriusScript.isInArea(fightRoom, new WorldPoint(3295, 9865, 0))); + assertFalse(ScurriusScript.isInArea(fightRoom, new WorldPoint(3279, 9869, 0))); + } +} From ba968d991ca44c098c92d05631cc49bd9e131c29 Mon Sep 17 00:00:00 2001 From: chsami Date: Thu, 15 Oct 2026 12:17:16 +0200 Subject: [PATCH 23/38] fix(gotr): guard altar renderable cast to DynamicObject (1.5.10) (#636) getAvailableAltars cast every altar renderable to DynamicObject, throwing ClassCastException when the renderable is a plain Model. Check the type first and read the animation once. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../plugins/microbot/gotr/GotrPlugin.java | 2 +- .../plugins/microbot/gotr/GotrScript.java | 13 +++-- .../gotr/GotrAltarRenderableTest.java | 55 +++++++++++++++++++ 3 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/gotr/GotrAltarRenderableTest.java 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 0a47d3a605..3f38de67f0 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.9"; + public static final String version = "1.5.10"; @Inject private GotrConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrScript.java b/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrScript.java index cd27633d52..dff9f6b40c 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 @@ -678,6 +678,14 @@ public boolean isInHugeMine() { && Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation().getX()) <= hugeMineX; } + static boolean isActiveAltarRenderable(Renderable renderable) { + if (!(renderable instanceof DynamicObject)) { + return false; + } + Animation animation = ((DynamicObject) renderable).getAnimation(); + return animation != null && animation.getId() == 9363; + } + public static boolean isGuardianPortal(GameObject gameObject) { return guardianPortalInfo.containsKey(gameObject.getId()); } @@ -745,10 +753,7 @@ public static List getAvailableAltars() { return false; } - if (((DynamicObject) x.getRenderable()).getAnimation() == null) { - return false; - } - if (((DynamicObject) x.getRenderable()).getAnimation().getId() != 9363) { + if (!isActiveAltarRenderable(x.getRenderable())) { return false; } Microbot.log("Adding " + portalInfo.getName() + " to list of available altars"); diff --git a/src/test/java/net/runelite/client/plugins/microbot/gotr/GotrAltarRenderableTest.java b/src/test/java/net/runelite/client/plugins/microbot/gotr/GotrAltarRenderableTest.java new file mode 100644 index 0000000000..94346759af --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/gotr/GotrAltarRenderableTest.java @@ -0,0 +1,55 @@ +package net.runelite.client.plugins.microbot.gotr; + +import net.runelite.api.Animation; +import net.runelite.api.DynamicObject; +import net.runelite.api.Model; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class GotrAltarRenderableTest +{ + @Test + void plainModelRenderableIsNotActive() + { + assertFalse(GotrScript.isActiveAltarRenderable(mock(Model.class))); + } + + @Test + void nullRenderableIsNotActive() + { + assertFalse(GotrScript.isActiveAltarRenderable(null)); + } + + @Test + void dynamicObjectWithoutAnimationIsNotActive() + { + DynamicObject altar = mock(DynamicObject.class); + when(altar.getAnimation()).thenReturn(null); + assertFalse(GotrScript.isActiveAltarRenderable(altar)); + } + + @Test + void dynamicObjectWithOtherAnimationIsNotActive() + { + assertFalse(GotrScript.isActiveAltarRenderable(animated(9362))); + } + + @Test + void dynamicObjectWithActiveAnimationIsActive() + { + assertTrue(GotrScript.isActiveAltarRenderable(animated(9363))); + } + + private static DynamicObject animated(int animationId) + { + Animation animation = mock(Animation.class); + when(animation.getId()).thenReturn(animationId); + DynamicObject altar = mock(DynamicObject.class); + when(altar.getAnimation()).thenReturn(animation); + return altar; + } +} From f20ac8b824394a463ff6fae87b3240b9461428c4 Mon Sep 17 00:00:00 2001 From: chsami Date: Thu, 15 Oct 2026 12:39:01 +0200 Subject: [PATCH 24/38] fix(playermonitor): create click log dir before saving on stop (1.0.5) (#637) saveMouseClicks wrote to ~/.runelite/PlayerMonitor/click_count.log without creating the directory, which is only created by loadMouseClicks and is skipped in lite mode. Stopping the plugin then threw FileNotFoundException and aborted shutDown before the listener and script were cleaned up. Create the parent directory before writing, close the writer with try-with-resources, and catch the IOException in shutDown so cleanup runs. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../playermonitor/PlayerMonitorPlugin.java | 29 +++++++------- .../PlayerMonitorPluginTest.java | 39 +++++++++++++++++++ 2 files changed, 55 insertions(+), 13 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/playermonitor/PlayerMonitorPluginTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/playermonitor/PlayerMonitorPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/playermonitor/PlayerMonitorPlugin.java index a433ff89a5..ec63f46ddc 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/playermonitor/PlayerMonitorPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/playermonitor/PlayerMonitorPlugin.java @@ -37,7 +37,7 @@ import java.util.stream.Collectors; @PluginDescriptor( - version = "1.0.4", + version = "1.0.5", name = PluginDescriptor.eXioStorm + "Player Monitor", enabledByDefault = false, minClientVersion = "2.0.0", @@ -215,7 +215,11 @@ protected void shutDown() throws Exception { if (this.flashOverlay != null) { overlayManager.remove(flashOverlay); } - saveMouseClicks(); + try { + saveMouseClicks(); + } catch (IOException e) { + log.warn("Failed to save mouse clicks", e); + } this.mouseManager.unregisterMouseListener((net.runelite.client.input.MouseListener)this.mouseListener); this.mouseListener = null; if (this.overlayOn) { @@ -275,17 +279,16 @@ public static void resetMouseClickCounterListener() { } } public static void saveMouseClicks() throws IOException { - if (!CLICK_TOTAL_FILE.exists()) - try { - if (!CLICK_TOTAL_FILE.createNewFile()) - System.out.println("Failed to create log file"); - } catch (IOException e) { - e.printStackTrace(); - } - FileWriter writer = new FileWriter(CLICK_TOTAL_FILE); - Integer[] totals = { Integer.valueOf(getLeftClickCounter()) }; - writer.write("" + totals[FILE_CLICK_TYPE_INDICES.LEFT.getValue()] + " "); - writer.close(); + writeClickCount(CLICK_TOTAL_FILE, getLeftClickCounter()); + } + static void writeClickCount(File file, int leftClicks) throws IOException { + File dir = file.getParentFile(); + if (dir != null && !dir.isDirectory() && !dir.mkdirs() && !dir.isDirectory()) + throw new IOException("Failed to create directory " + dir); + Integer[] totals = { Integer.valueOf(leftClicks) }; + try (FileWriter writer = new FileWriter(file)) { + writer.write("" + totals[FILE_CLICK_TYPE_INDICES.LEFT.getValue()] + " "); + } } public void loadMouseClicks() throws FileNotFoundException { if (!this.CLICK_TOTAL_DIR.mkdir() && CLICK_TOTAL_FILE.exists()) { diff --git a/src/test/java/net/runelite/client/plugins/microbot/playermonitor/PlayerMonitorPluginTest.java b/src/test/java/net/runelite/client/plugins/microbot/playermonitor/PlayerMonitorPluginTest.java new file mode 100644 index 0000000000..e29ce1c7f2 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/playermonitor/PlayerMonitorPluginTest.java @@ -0,0 +1,39 @@ +package net.runelite.client.plugins.microbot.playermonitor; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PlayerMonitorPluginTest { + + @TempDir + Path tempDir; + + @Test + void writeClickCountCreatesMissingDirectory() throws IOException { + File file = tempDir.resolve("PlayerMonitor").resolve("click_count.log").toFile(); + + PlayerMonitorPlugin.writeClickCount(file, 42); + + assertTrue(file.isFile()); + assertEquals("42 ", new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8)); + } + + @Test + void writeClickCountOverwritesExistingFile() throws IOException { + File file = tempDir.resolve("click_count.log").toFile(); + Files.write(file.toPath(), "999 ".getBytes(StandardCharsets.UTF_8)); + + PlayerMonitorPlugin.writeClickCount(file, 7); + + assertEquals("7 ", new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8)); + } +} From 6f54d34f85e8d5c88097975806ba77939f0154e7 Mon Sep 17 00:00:00 2001 From: chsami Date: Thu, 15 Oct 2026 14:48:22 +0200 Subject: [PATCH 25/38] fix(aiofighter): guard null cache inventory setup after equipping slayer gear (2.2.9) (#638) addInventorySetup("cache") registers the setup on the client thread, so the immediate lookup could miss it and pass null to setCurrentSlayerInventorySetup, which threw an NPE on getName(). Wait for the cache setup to register before looking it up, and make the setter ignore a null setup. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../microbot/aiofighter/AIOFighterPlugin.java | 6 ++- .../aiofighter/bank/BankerScript.java | 14 +++++- .../bank/BankerScriptCacheSetupTest.java | 49 +++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/aiofighter/bank/BankerScriptCacheSetupTest.java 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 842016ac5f..1df5bc78cc 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 @@ -62,7 +62,7 @@ ) @Slf4j public class AIOFighterPlugin extends Plugin { - public static final String version = "2.2.8"; + public static final String version = "2.2.9"; public static boolean needShopping = false; private static final String SET = "Set"; private static final String CENTER_TILE = ColorUtil.wrapWithColorTag("Center Tile", JagexColors.MENU_TARGET); @@ -251,6 +251,10 @@ public static void setSlayerHasTaskWeakness(boolean slayerHasTaskWeakness) { } public static void setCurrentSlayerInventorySetup(InventorySetup currentInventorySetup) { + if (currentInventorySetup == null) { + Microbot.log("Inventory setup not found; keeping current inventory setup"); + return; + } Microbot.log("Setting current inventory setup to: " + currentInventorySetup.getName()); setConfig("currentInventorySetup", currentInventorySetup); } diff --git a/src/main/java/net/runelite/client/plugins/microbot/aiofighter/bank/BankerScript.java b/src/main/java/net/runelite/client/plugins/microbot/aiofighter/bank/BankerScript.java index 7296abcd60..b78be6187c 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/aiofighter/bank/BankerScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/aiofighter/bank/BankerScript.java @@ -242,6 +242,17 @@ static boolean isSetupResolvable(Collection setupNames, String setupName .anyMatch(name -> name.equalsIgnoreCase(setupName) || name.equals("default")); } + static InventorySetup findSetupByName(Collection setups, String setupName) { + if (setups == null || setupName == null) { + return null; + } + return setups.stream() + .filter(Objects::nonNull) + .filter(setup -> setupName.equalsIgnoreCase(setup.getName())) + .findFirst() + .orElse(null); + } + /** * Checks if the given setup requires any variant of a specific potion but the player does not have it. */ @@ -479,7 +490,8 @@ public void withdrawUpkeepItems(AIOFighterConfig config) { Rs2Bank.withdrawAndEquip(Rs2Slayer.getSlayerTaskProtectiveEquipment()); sleep(3000); inventorySetupsPlugin.addInventorySetup("cache"); - InventorySetup inventorySetup1 = MInventorySetupsPlugin.getInventorySetups().stream().filter(Objects::nonNull).filter(x -> x.getName().equalsIgnoreCase("cache")).findFirst().orElse(null); + sleepUntil(() -> findSetupByName(MInventorySetupsPlugin.getInventorySetups(), "cache") != null, 3000); + InventorySetup inventorySetup1 = findSetupByName(MInventorySetupsPlugin.getInventorySetups(), "cache"); AIOFighterPlugin.setCurrentSlayerInventorySetup(inventorySetup1); } else { ShopScript.shopItems.add(new ShopItem(Rs2Slayer.getSlayerTaskProtectiveEquipment(), 1, Microbot.getRs2ItemManager().getItemId(Rs2Slayer.getSlayerTaskProtectiveEquipment()), ShopType.SLAYER_SHOP)); diff --git a/src/test/java/net/runelite/client/plugins/microbot/aiofighter/bank/BankerScriptCacheSetupTest.java b/src/test/java/net/runelite/client/plugins/microbot/aiofighter/bank/BankerScriptCacheSetupTest.java new file mode 100644 index 0000000000..5df3cea82e --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/aiofighter/bank/BankerScriptCacheSetupTest.java @@ -0,0 +1,49 @@ +package net.runelite.client.plugins.microbot.aiofighter.bank; + +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.aiofighter.AIOFighterPlugin; +import net.runelite.client.plugins.microbot.inventorysetups.InventorySetup; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.when; + +public final class BankerScriptCacheSetupTest { + + private static InventorySetup setup(String name) { + InventorySetup setup = mock(InventorySetup.class); + when(setup.getName()).thenReturn(name); + return setup; + } + + @Test + void findsSetupByNameIgnoringCase() { + InventorySetup cache = setup("Cache"); + assertSame(cache, BankerScript.findSetupByName(Arrays.asList(setup("Melee"), null, setup(null), cache), "cache")); + } + + @Test + void returnsNullWhenSetupNotRegisteredYet() { + assertNull(BankerScript.findSetupByName(Collections.singletonList(setup("Melee")), "cache")); + assertNull(BankerScript.findSetupByName(Collections.emptyList(), "cache")); + assertNull(BankerScript.findSetupByName(null, "cache")); + assertNull(BankerScript.findSetupByName(Collections.singletonList(setup("cache")), null)); + } + + @Test + void settingNullInventorySetupIsIgnored() { + try (MockedStatic microbot = mockStatic(Microbot.class)) { + assertDoesNotThrow(() -> AIOFighterPlugin.setCurrentSlayerInventorySetup(null)); + microbot.verify(Microbot::getConfigManager, never()); + } + } +} From 351284e24a11534c988db40afae02d321915da5e Mon Sep 17 00:00:00 2001 From: chsami Date: Thu, 15 Oct 2026 14:58:41 +0200 Subject: [PATCH 26/38] fix(FarmTreeRun): build bank withdrawal list locally to avoid CME (1.2.3) (#639) bank() iterated the shared items field while the stop path cleared it from the plugin-stop thread, throwing ConcurrentModificationException. The withdrawal list is now a local variable passed to the travel-item helpers, the merge is a static helper, and stopping the script no longer touches it. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../farmtreerun/FarmTreeRunPlugin.java | 2 +- .../farmtreerun/FarmTreeRunScript.java | 44 ++++++++-------- .../farmtreerun/FarmTreeRunBankItemsTest.java | 50 +++++++++++++++++++ 3 files changed, 74 insertions(+), 22 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunBankItemsTest.java 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 98a3e0aa5e..2780851b2f 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.2.2"; + public static final String version = "1.2.3"; @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 1833743fcc..f349d18695 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 @@ -42,7 +42,6 @@ public class FarmTreeRunScript extends Script { public static net.runelite.client.plugins.microbot.farmtreerun.enums.FarmTreeRunState botStatus; public static boolean test = false; public static Integer compostItemId = null; - private List items = new ArrayList<>(); private final FarmTreeRunPlugin plugin; private final FarmTreeRunConfig config; @@ -558,7 +557,7 @@ private static boolean isInterruption(Throwable error) { } private void bank(FarmTreeRunConfig config) { - items.clear(); + List items = new ArrayList<>(); if (Rs2Bank.openBank() || Rs2Bank.walkToBank()) { sleepUntil(Rs2Bank::isOpen, 5000); if (!Rs2Bank.isOpen()) @@ -704,11 +703,11 @@ private void bank(FarmTreeRunConfig config) { } if (config.travelTablets()) { - addAvailableTravelItem(net.runelite.api.ItemID.VARROCK_TELEPORT, 2); - addAvailableTravelItem(net.runelite.api.ItemID.LUMBRIDGE_TELEPORT, 1); - addAvailableTravelItem(net.runelite.api.ItemID.FALADOR_TELEPORT, 2); - addAvailableTravelItem(net.runelite.api.ItemID.CAMELOT_TELEPORT, 1); - if (!config.preferHouseTravel()) addAvailableTravelItem(net.runelite.api.ItemID.TELEPORT_TO_HOUSE, 3); + addAvailableTravelItem(items, net.runelite.api.ItemID.VARROCK_TELEPORT, 2); + addAvailableTravelItem(items, net.runelite.api.ItemID.LUMBRIDGE_TELEPORT, 1); + addAvailableTravelItem(items, net.runelite.api.ItemID.FALADOR_TELEPORT, 2); + addAvailableTravelItem(items, net.runelite.api.ItemID.CAMELOT_TELEPORT, 1); + if (!config.preferHouseTravel()) addAvailableTravelItem(items, net.runelite.api.ItemID.TELEPORT_TO_HOUSE, 3); } else { items.add(new FarmingItem(ItemID.LAW_RUNE, 10)); items.add(new FarmingItem(ItemID.FIRE_RUNE, 30)); @@ -717,31 +716,25 @@ private void bank(FarmTreeRunConfig config) { items.add(new FarmingItem(ItemID.WATER_RUNE, 30)); } if (config.travelKourendBook()) { - addFirstTravelItem(net.runelite.api.ItemID.BOOK_OF_THE_DEAD, net.runelite.api.ItemID.KHAREDSTS_MEMOIRS); + addFirstTravelItem(items, net.runelite.api.ItemID.BOOK_OF_THE_DEAD, net.runelite.api.ItemID.KHAREDSTS_MEMOIRS); } if (config.preferHouseTravel()) { items.add(new FarmingItem(net.runelite.api.ItemID.TELEPORT_TO_HOUSE, 10)); } if (config.travelQuetzal()) { - addFirstTravelItem(net.runelite.api.ItemID.PERFECTED_QUETZAL_WHISTLEI, + addFirstTravelItem(items, net.runelite.api.ItemID.PERFECTED_QUETZAL_WHISTLEI, net.runelite.api.ItemID.PERFECTED_QUETZAL_WHISTLE, net.runelite.api.ItemID.ENHANCED_QUETZAL_WHISTLE, net.runelite.api.ItemID.BASIC_QUETZAL_WHISTLE); } if (config.travelFairyStaff()) { - addFirstTravelItem(net.runelite.api.ItemID.DRAMEN_STAFF, net.runelite.api.ItemID.LUNAR_STAFF); + addFirstTravelItem(items, net.runelite.api.ItemID.DRAMEN_STAFF, net.runelite.api.ItemID.LUNAR_STAFF); } // TODO: Need to handle what happens if a required item does not exist // Merge entries with the same itemId + noted flag so withdrawal doesn't under-count - Map merged = new LinkedHashMap<>(); - for (FarmingItem item : items) { - long key = ((long) item.getItemId() << 1) | (item.isNoted() ? 1 : 0); - merged.merge(key, item, (a, b) -> - new FarmingItem(a.getItemId(), a.getQuantity() + b.getQuantity(), a.isNoted(), a.isOptional())); - } - items = new ArrayList<>(merged.values()); + items = mergeItems(items); // Deposit only what we don't need: keep desired ids and their noted variants Set keepIds = new HashSet<>(); @@ -824,16 +817,26 @@ private void checkIfPlayerHasItem(int itemId, int quantity, boolean optional) { } } - private void addAvailableTravelItem(int id, int quantity) { + static List mergeItems(List items) { + Map merged = new LinkedHashMap<>(); + for (FarmingItem item : items) { + long key = ((long) item.getItemId() << 1) | (item.isNoted() ? 1 : 0); + merged.merge(key, item, (a, b) -> + new FarmingItem(a.getItemId(), a.getQuantity() + b.getQuantity(), a.isNoted(), a.isOptional())); + } + return new ArrayList<>(merged.values()); + } + + private void addAvailableTravelItem(List items, int id, int quantity) { int available = Rs2Inventory.itemQuantity(id) + Rs2Bank.count(id); if (available > 0) items.add(new FarmingItem(id, Math.min(quantity, available), false, true)); } - private void addFirstTravelItem(int... ids) { + private void addFirstTravelItem(List items, int... ids) { for (int id : ids) { if (Rs2Equipment.isWearing(id)) return; if (Rs2Inventory.hasItem(id) || Rs2Bank.hasItem(id)) { - addAvailableTravelItem(id, 1); + addAvailableTravelItem(items, id, 1); return; } } @@ -1271,7 +1274,6 @@ private void handleExoticGardeners() { @Override public void shutdown() { if(isRunning()) { - items.clear(); super.shutdown(); Microbot.stopPlugin(plugin); } diff --git a/src/test/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunBankItemsTest.java b/src/test/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunBankItemsTest.java new file mode 100644 index 0000000000..f83ad9cb07 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunBankItemsTest.java @@ -0,0 +1,50 @@ +package net.runelite.client.plugins.microbot.farmtreerun; + +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class FarmTreeRunBankItemsTest { + + @Test + void scriptKeepsNoSharedWithdrawalCollection() { + for (Field field : FarmTreeRunScript.class.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers())) continue; + assertFalse(Collection.class.isAssignableFrom(field.getType()), + "FarmTreeRunScript must not share collection state across threads: " + field.getName()); + } + } + + @Test + void mergeSumsSameItemAndNotedFlag() { + List input = new ArrayList<>(); + input.add(new FarmingItem(563, 10)); + input.add(new FarmingItem(554, 30)); + input.add(new FarmingItem(563, 5, false, true)); + input.add(new FarmingItem(995, 100, true)); + input.add(new FarmingItem(995, 50, false)); + + List merged = FarmTreeRunScript.mergeItems(input); + + assertNotSame(input, merged); + assertEquals(5, input.size()); + assertEquals(4, merged.size()); + assertEquals(563, merged.get(0).getItemId()); + assertEquals(15, merged.get(0).getQuantity()); + assertFalse(merged.get(0).isOptional()); + assertEquals(554, merged.get(1).getItemId()); + assertTrue(merged.get(2).isNoted()); + assertEquals(100, merged.get(2).getQuantity()); + assertFalse(merged.get(3).isNoted()); + assertEquals(50, merged.get(3).getQuantity()); + } +} From f600bb261baef774a461d4549387ea9cdd5985fa Mon Sep 17 00:00:00 2001 From: chsami Date: Thu, 15 Oct 2026 15:07:34 +0200 Subject: [PATCH 27/38] fix(MageTrainingArena): guard buyReward against unloaded reward shop widget (1.1.9) (#640) Rs2Widget.getWidget(197, 11) returns null when the reward shop child is not built yet or the shop has just closed, which threw an NPE inside waitForInventoryChanges. Return early when the container is null and skip the click and confirm when the reward item is not in the list. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../MageTrainingArenaPlugin.java | 2 +- .../MageTrainingArenaScript.java | 19 ++++++++--- .../MageTrainingArenaScriptTest.java | 34 +++++++++++++++++++ 3 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/magetrainingarena/MageTrainingArenaScriptTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/magetrainingarena/MageTrainingArenaPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/magetrainingarena/MageTrainingArenaPlugin.java index e03170c3ca..fcaf3ff779 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/magetrainingarena/MageTrainingArenaPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/magetrainingarena/MageTrainingArenaPlugin.java @@ -26,7 +26,7 @@ ) @Slf4j public class MageTrainingArenaPlugin extends Plugin { - public static final String version = "1.1.8"; + public static final String version = "1.1.9"; @Inject private MageTrainingArenaConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/magetrainingarena/MageTrainingArenaScript.java b/src/main/java/net/runelite/client/plugins/microbot/magetrainingarena/MageTrainingArenaScript.java index b0b0bbeddb..cb2f03012f 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/magetrainingarena/MageTrainingArenaScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/magetrainingarena/MageTrainingArenaScript.java @@ -6,6 +6,7 @@ import net.runelite.api.gameval.*; import net.runelite.api.coords.LocalPoint; import net.runelite.api.coords.WorldPoint; +import net.runelite.api.widgets.Widget; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.Script; import net.runelite.client.plugins.microbot.globval.enums.InterfaceTab; @@ -584,10 +585,12 @@ private void buyReward(Rewards reward) { } Rs2Inventory.waitForInventoryChanges(() -> { - var rewardWidgets = Rs2Widget.getWidget(197, 11).getDynamicChildren(); - if (rewardWidgets == null) return; - var widget = Arrays.stream(rewardWidgets).filter(x -> x.getItemId() == reward.getItemId()).findFirst().orElse(null); - Rs2Widget.clickWidgetFast(widget, Arrays.asList(rewardWidgets).indexOf(widget)); + var rewardContainer = Rs2Widget.getWidget(197, 11); + if (rewardContainer == null) return; + var rewardWidgets = rewardContainer.getDynamicChildren(); + var index = findRewardIndex(rewardWidgets, reward.getItemId()); + if (index < 0) return; + Rs2Widget.clickWidgetFast(rewardWidgets[index], index); sleepGaussian(600, 150); Rs2Widget.clickWidget(197, 9); sleepGaussian(600, 150); @@ -605,6 +608,14 @@ private void buyReward(Rewards reward) { } } + static int findRewardIndex(Widget[] rewardWidgets, int itemId) { + if (rewardWidgets == null) return -1; + for (int i = 0; i < rewardWidgets.length; i++) { + if (rewardWidgets[i] != null && rewardWidgets[i].getItemId() == itemId) return i; + } + return -1; + } + public static Rooms getCurrentRoom() { for (var room : Rooms.values()) { if (room == Rooms.TELEKINETIC && Arrays.stream(TelekineticRooms.values()).anyMatch(x -> Rs2Player.getWorldLocation().distanceTo(x.getArea()) == 0) diff --git a/src/test/java/net/runelite/client/plugins/microbot/magetrainingarena/MageTrainingArenaScriptTest.java b/src/test/java/net/runelite/client/plugins/microbot/magetrainingarena/MageTrainingArenaScriptTest.java new file mode 100644 index 0000000000..b4e4aa52e8 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/magetrainingarena/MageTrainingArenaScriptTest.java @@ -0,0 +1,34 @@ +package net.runelite.client.plugins.microbot.magetrainingarena; + +import net.runelite.api.widgets.Widget; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class MageTrainingArenaScriptTest { + + private static Widget item(int itemId) { + Widget widget = mock(Widget.class); + when(widget.getItemId()).thenReturn(itemId); + return widget; + } + + @Test + void unloadedRewardListHasNoReward() { + assertEquals(-1, MageTrainingArenaScript.findRewardIndex(null, 6908)); + } + + @Test + void missingRewardIsNotClicked() { + Widget[] widgets = {item(6908), item(6910)}; + assertEquals(-1, MageTrainingArenaScript.findRewardIndex(widgets, 6912)); + } + + @Test + void matchingRewardReturnsItsIndex() { + Widget[] widgets = {item(6908), null, item(6910)}; + assertEquals(2, MageTrainingArenaScript.findRewardIndex(widgets, 6910)); + } +} From 0cab91482064e8cdbdb1c8b28e4ec9d7d2295655 Mon Sep 17 00:00:00 2001 From: chsami Date: Thu, 15 Oct 2026 21:17:13 +0200 Subject: [PATCH 28/38] fix(Scurrius): read giant rat name once to avoid NPE in rat query (1.0.4) (#641) The giant-rat query predicate called getName() twice; when the NPC despawned between the null check and toLowerCase() the second call returned null and the NPE aborted the Scurrius tick. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../microbot/scurrius/ScurriusPlugin.java | 2 +- .../microbot/scurrius/ScurriusScript.java | 8 +++- .../microbot/scurrius/ScurriusScriptTest.java | 39 +++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/runelite/client/plugins/microbot/scurrius/ScurriusPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/scurrius/ScurriusPlugin.java index b9b3279915..7cacaa75e8 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/scurrius/ScurriusPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/scurrius/ScurriusPlugin.java @@ -32,7 +32,7 @@ ) @Slf4j public class ScurriusPlugin extends Plugin { - public static final String version = "1.0.3"; + public static final String version = "1.0.4"; @Inject private ScurriusConfig config; @Provides diff --git a/src/main/java/net/runelite/client/plugins/microbot/scurrius/ScurriusScript.java b/src/main/java/net/runelite/client/plugins/microbot/scurrius/ScurriusScript.java index 03cb0ca51a..2753c16f8c 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/scurrius/ScurriusScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/scurrius/ScurriusScript.java @@ -190,8 +190,7 @@ public boolean run(ScurriusConfig config) { } Rs2NpcModel giantRatModel = Microbot.getRs2NpcCache().query() - .where(n -> n.getName() != null && n.getName().toLowerCase().contains("giant rat") - && !n.getNpc().isDead()) + .where(ScurriusScript::isLiveGiantRat) .nearest(); if (giantRatModel != null) { boolean didWeAttackAGiantRat = scurrius != null && config.prioritizeRats() && giantRatModel.click("Attack"); @@ -275,6 +274,11 @@ static boolean isInArea(WorldArea area, WorldPoint location) { return location != null && area.contains(location); } + static boolean isLiveGiantRat(Rs2NpcModel npc) { + String name = npc.getName(); + return name != null && name.toLowerCase().contains("giant rat") && !npc.getNpc().isDead(); + } + static int randomThreshold(int first, int second) { int low = Math.min(first, second); int high = Math.max(first, second); diff --git a/src/test/java/net/runelite/client/plugins/microbot/scurrius/ScurriusScriptTest.java b/src/test/java/net/runelite/client/plugins/microbot/scurrius/ScurriusScriptTest.java index e4432ab7e2..ff5515d902 100644 --- a/src/test/java/net/runelite/client/plugins/microbot/scurrius/ScurriusScriptTest.java +++ b/src/test/java/net/runelite/client/plugins/microbot/scurrius/ScurriusScriptTest.java @@ -1,12 +1,17 @@ package net.runelite.client.plugins.microbot.scurrius; import net.runelite.api.coords.WorldArea; +import net.runelite.api.NPC; import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; class ScurriusScriptTest { @@ -43,4 +48,38 @@ void locationInsideFightRoomIsDetected() { assertTrue(ScurriusScript.isInArea(fightRoom, new WorldPoint(3295, 9865, 0))); assertFalse(ScurriusScript.isInArea(fightRoom, new WorldPoint(3279, 9869, 0))); } + + @Test + void giantRatNameIsReadOnceSoALaterNullCannotThrow() { + Rs2NpcModel rat = mock(Rs2NpcModel.class); + NPC npc = mock(NPC.class); + when(rat.getName()).thenReturn("Giant rat", (String) null); + when(rat.getNpc()).thenReturn(npc); + when(npc.isDead()).thenReturn(false); + + assertTrue(ScurriusScript.isLiveGiantRat(rat)); + verify(rat).getName(); + } + + @Test + void nullNameIsNotAGiantRat() { + Rs2NpcModel npc = mock(Rs2NpcModel.class); + when(npc.getName()).thenReturn(null); + + assertFalse(ScurriusScript.isLiveGiantRat(npc)); + } + + @Test + void deadOrOtherNpcsAreNotLiveGiantRats() { + Rs2NpcModel deadRat = mock(Rs2NpcModel.class); + NPC deadNpc = mock(NPC.class); + when(deadRat.getName()).thenReturn("Giant rat"); + when(deadRat.getNpc()).thenReturn(deadNpc); + when(deadNpc.isDead()).thenReturn(true); + assertFalse(ScurriusScript.isLiveGiantRat(deadRat)); + + Rs2NpcModel scurrius = mock(Rs2NpcModel.class); + when(scurrius.getName()).thenReturn("Scurrius"); + assertFalse(ScurriusScript.isLiveGiantRat(scurrius)); + } } From dcfc5f02a5da67ee8bd639e12865ec67586e422a Mon Sep 17 00:00:00 2001 From: chsami Date: Fri, 16 Oct 2026 04:35:02 +0200 Subject: [PATCH 29/38] fix(AmmoniteCrabs): guard null local player location during hop or load (1.1.7) (#642) Read the player location through Rs2Player.getWorldLocation() and skip the check when it is null, instead of dereferencing getLocalPlayer() inside client-thread lambdas in preStateChecks, scanAmmoniteCrabLocation, walkBack, resetAggro and otherPlayerDetected. Use Rs2Player.isInteracting() in the FIGHT case. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../AmmoniteCrabs/AmmoniteCrabPlugin.java | 2 +- .../AmmoniteCrabs/AmmoniteCrabScript.java | 31 +++++++++++----- .../AmmoniteCrabs/AmmoniteCrabScriptTest.java | 36 +++++++++++++++++++ 3 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabScriptTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabPlugin.java index 6233dd29c1..d23df32335 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabPlugin.java @@ -28,7 +28,7 @@ @Slf4j public class AmmoniteCrabPlugin extends Plugin { - public final static String version = "1.1.6"; + public final static String version = "1.1.7"; @Inject public AmmoniteCrabScript ammoniteCrabScript; @Inject diff --git a/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabScript.java b/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabScript.java index 4706db437b..d7da445bcd 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabScript.java @@ -46,11 +46,12 @@ public boolean run(AmmoniteCrabConfig config) { switch (ammoniteCrabState) { case FIGHT: - if (!Microbot.getClient().getLocalPlayer().isInteracting() && !Rs2Combat.inCombat()) { + if (!Rs2Player.isInteracting() && !Rs2Combat.inCombat()) { Rs2Combat.enableAutoRetialiate(); } - if (Rs2Player.getWorldLocation().distanceTo(config.crabLocation().getFightLocation()) > 0) { + WorldPoint fightPlayerLocation = Rs2Player.getWorldLocation(); + if (fightPlayerLocation != null && fightPlayerLocation.distanceTo(config.crabLocation().getFightLocation()) > 0) { Rs2Walker.walkFastCanvas(config.crabLocation().getFightLocation()); } @@ -132,7 +133,8 @@ private void preStateChecks(AmmoniteCrabConfig config) { ammoniteCrabState = AmmoniteCrabState.HOP_WORLD; } - if (Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation()).distanceTo(config.crabLocation().getWorldhopLocation()) > 10 && (ammoniteCrabState != AmmoniteCrabState.RESET_AGGRO && ammoniteCrabState != AmmoniteCrabState.WALK_BACK && ammoniteCrabState != AmmoniteCrabState.BANK)) { + WorldPoint playerLocation = Rs2Player.getWorldLocation(); + if (playerLocation != null && playerLocation.distanceTo(config.crabLocation().getWorldhopLocation()) > 10 && (ammoniteCrabState != AmmoniteCrabState.RESET_AGGRO && ammoniteCrabState != AmmoniteCrabState.WALK_BACK && ammoniteCrabState != AmmoniteCrabState.BANK)) { ammoniteCrabState = AmmoniteCrabState.WALK_BACK; resetAggro(config); resetAfkTimer(); @@ -178,7 +180,11 @@ private void scanAmmoniteCrabLocation(AmmoniteCrabConfig config) { ammoniteCrabState = AmmoniteCrabState.FIGHT; return; } - if (config.crabLocation().getWorldhopLocation().distanceTo(Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation())) > 2) { + WorldPoint playerLocation = Rs2Player.getWorldLocation(); + if (playerLocation == null) { + return; + } + if (config.crabLocation().getWorldhopLocation().distanceTo(playerLocation) > 2) { Rs2Walker.walkTo(config.crabLocation().getWorldhopLocation()); return; } @@ -188,7 +194,7 @@ private void scanAmmoniteCrabLocation(AmmoniteCrabConfig config) { return; } else { Rs2Walker.walkTo(config.crabLocation().getFightLocation(), 0); - if (Rs2Player.getWorldLocation().distanceTo(config.crabLocation().getFightLocation()) <= 3) { + if (isWithin(Rs2Player.getWorldLocation(), config.crabLocation().getFightLocation(), 3)) { resetAfkTimer(); ammoniteCrabState = AmmoniteCrabState.FIGHT; } else { @@ -208,7 +214,7 @@ private void usePotions(AmmoniteCrabConfig config) { private void walkBack(AmmoniteCrabConfig config) { Rs2Walker.walkTo(config.crabLocation().getFightLocation()); - if (Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation()).distanceTo(config.crabLocation().getFightLocation()) <= 3) { + if (isWithin(Rs2Player.getWorldLocation(), config.crabLocation().getFightLocation(), 3)) { attackScatteredCrabs(config); resetAfkTimer(); ammoniteCrabState = AmmoniteCrabState.FIGHT; @@ -256,7 +262,7 @@ private boolean isNpcAggressive() { private void resetAggro(AmmoniteCrabConfig config) { Rs2Walker.walkTo(config.crabLocation().getResetLocation()); - if (Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation()).distanceTo(config.crabLocation().getResetLocation()) <= 3) { + if (isWithin(Rs2Player.getWorldLocation(), config.crabLocation().getResetLocation(), 3)) { ammoniteCrabState = AmmoniteCrabState.WALK_BACK; } } @@ -273,10 +279,17 @@ private void resetAfkTimer() { } private boolean otherPlayerDetected() { - return otherPlayerDetected(Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation())); + return otherPlayerDetected(Rs2Player.getWorldLocation()); } - private boolean otherPlayerDetected(WorldPoint worldPoint) { + static boolean isWithin(WorldPoint location, WorldPoint target, int distance) { + return location != null && location.distanceTo(target) <= distance; + } + + static boolean otherPlayerDetected(WorldPoint worldPoint) { + if (worldPoint == null) { + return false; + } for (Rs2PlayerModel player : Rs2Player.getPlayers(player -> true).collect(Collectors.toList())) { if (player.getWorldLocation().distanceTo(worldPoint) > 3) continue; diff --git a/src/test/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabScriptTest.java b/src/test/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabScriptTest.java new file mode 100644 index 0000000000..6b1d2e8e70 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/AmmoniteCrabs/AmmoniteCrabScriptTest.java @@ -0,0 +1,36 @@ +package net.runelite.client.plugins.microbot.AmmoniteCrabs; + +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; + +class AmmoniteCrabScriptTest { + + private final WorldPoint fightLocation = new WorldPoint(3733, 3846, 0); + + @Test + void nullPlayerLocationIsNotWithinRange() { + assertFalse(AmmoniteCrabScript.isWithin(null, fightLocation, 3)); + } + + @Test + void playerLocationWithinRangeIsDetected() { + assertTrue(AmmoniteCrabScript.isWithin(new WorldPoint(3735, 3846, 0), fightLocation, 3)); + assertFalse(AmmoniteCrabScript.isWithin(new WorldPoint(3740, 3846, 0), fightLocation, 3)); + } + + @Test + void nullLocationDetectsNoOtherPlayerWithoutQueryingPlayers() { + try (MockedStatic player = mockStatic(Rs2Player.class)) { + assertFalse(AmmoniteCrabScript.otherPlayerDetected(null)); + player.verify(() -> Rs2Player.getPlayers(any()), never()); + } + } +} From a8be3b6dbd9233841d528fdc85ba85a3672ddded Mon Sep 17 00:00:00 2001 From: chsami Date: Fri, 16 Oct 2026 04:48:15 +0200 Subject: [PATCH 30/38] fix(Thieving): guard null local player in setStartXp during overlay render (2.1.6) (#643) ThievingOverlay.render calls xpGained() every frame, which called setStartXp() and dereferenced Rs2Player.getLocalPlayer() without a null check. getLocalPlayer() returns null while the local player is missing from the player cache (login, world hop, loading), so the overlay threw an NPE on every frame. Skip the start-xp update when the local player is null and leave startXp untouched. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../microbot/thieving/ThievingPlugin.java | 9 +++- .../thieving/ThievingPluginXpGainedTest.java | 50 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/thieving/ThievingPluginXpGainedTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java index 0e8848eb7c..23e44867a1 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java @@ -14,6 +14,7 @@ import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.PluginConstants; import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.player.Rs2PlayerModel; import net.runelite.client.ui.overlay.OverlayManager; import javax.inject.Inject; @@ -34,7 +35,7 @@ ) @Slf4j public class ThievingPlugin extends Plugin { - public static final String version = "2.1.5"; + public static final String version = "2.1.6"; @Inject @Getter @@ -80,7 +81,11 @@ protected void shutDown() { private void setStartXp() { // this should handle changing accounts - final String name = Rs2Player.getLocalPlayer().getName(); + final Rs2PlayerModel localPlayer = Rs2Player.getLocalPlayer(); + if (localPlayer == null) { + return; + } + final String name = localPlayer.getName(); if (startXp == 0 || (name != null && !name.equals(this.name))) { this.name = name; startXp = Microbot.getClient().getSkillExperience(Skill.THIEVING); diff --git a/src/test/java/net/runelite/client/plugins/microbot/thieving/ThievingPluginXpGainedTest.java b/src/test/java/net/runelite/client/plugins/microbot/thieving/ThievingPluginXpGainedTest.java new file mode 100644 index 0000000000..66b41bccd7 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/thieving/ThievingPluginXpGainedTest.java @@ -0,0 +1,50 @@ +package net.runelite.client.plugins.microbot.thieving; + +import net.runelite.api.Client; +import net.runelite.api.Skill; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.player.Rs2PlayerModel; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +class ThievingPluginXpGainedTest { + + @Test + void xpGainedDoesNotThrowWhenLocalPlayerIsMissing() { + Client client = mock(Client.class); + when(client.getSkillExperience(Skill.THIEVING)).thenReturn(1000); + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic player = mockStatic(Rs2Player.class)) { + microbot.when(Microbot::getClient).thenReturn(client); + player.when(Rs2Player::getLocalPlayer).thenReturn(null); + + ThievingPlugin plugin = new ThievingPlugin(); + assertDoesNotThrow(plugin::xpGained); + } + } + + @Test + void xpGainedKeepsStartXpAcrossMissingLocalPlayer() { + Client client = mock(Client.class); + Rs2PlayerModel localPlayer = mock(Rs2PlayerModel.class); + when(localPlayer.getName()).thenReturn("Thief"); + when(client.getSkillExperience(Skill.THIEVING)).thenReturn(1000, 1000, 1200, 1500); + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic player = mockStatic(Rs2Player.class)) { + microbot.when(Microbot::getClient).thenReturn(client); + player.when(Rs2Player::getLocalPlayer).thenReturn(localPlayer, null, localPlayer); + + ThievingPlugin plugin = new ThievingPlugin(); + assertEquals(0, plugin.xpGained()); + assertEquals(200, plugin.xpGained()); + assertEquals(500, plugin.xpGained()); + } + } +} From a213d1d00d6b35e21cf9e61624a8fcb3835b3be6 Mon Sep 17 00:00:00 2001 From: chsami Date: Fri, 16 Oct 2026 16:42:58 +0200 Subject: [PATCH 31/38] fix(TutorialIsland): walk to and open rat pen gate before melee attack (1.3.17) (#644) The melee rat step only opened gate 9719 when the player already stood on the x=3111 gate strip, and otherwise gated the attack on Rs2Walker.canReach, which routes through the gate transport and tolerates a 2-tile endpoint offset. Rats were clicked from outside the closed pen (spam-clicking) or the step idled. Now the attack is gated on a strict collision reachability check; when the rat is not reachable the player walks to the gate and opens it. The ranged step attacks the rat over the fence without a reachability check. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../microbot/tutorialisland/RatPen.java | 29 ++++++++++++++ .../tutorialisland/TutorialIslandPlugin.java | 2 +- .../tutorialisland/TutorialIslandScript.java | 31 ++++++++------ .../microbot/tutorialisland/RatPenTest.java | 40 +++++++++++++++++++ 4 files changed, 88 insertions(+), 14 deletions(-) create mode 100644 src/main/java/net/runelite/client/plugins/microbot/tutorialisland/RatPen.java create mode 100644 src/test/java/net/runelite/client/plugins/microbot/tutorialisland/RatPenTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/tutorialisland/RatPen.java b/src/main/java/net/runelite/client/plugins/microbot/tutorialisland/RatPen.java new file mode 100644 index 0000000000..ebaa147fde --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/tutorialisland/RatPen.java @@ -0,0 +1,29 @@ +package net.runelite.client.plugins.microbot.tutorialisland; + +import net.runelite.api.coords.WorldPoint; + +final class RatPen { + static final int GATE_ID = 9719; + static final WorldPoint GATE_OUTSIDE = new WorldPoint(3111, 9518, 0); + static final WorldPoint GATE_INSIDE = new WorldPoint(3110, 9518, 0); + + enum Step { + ATTACK, + OPEN_GATE, + WALK_TO_GATE + } + + private RatPen() { + } + + static Step next(WorldPoint playerLoc, boolean ratReachable) { + if (ratReachable) { + return Step.ATTACK; + } + if (playerLoc != null && playerLoc.getPlane() == GATE_OUTSIDE.getPlane() + && playerLoc.distanceTo2D(GATE_OUTSIDE) <= 1) { + return Step.OPEN_GATE; + } + return Step.WALK_TO_GATE; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/tutorialisland/TutorialIslandPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/tutorialisland/TutorialIslandPlugin.java index d99aac9841..7fa7200298 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/tutorialisland/TutorialIslandPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/tutorialisland/TutorialIslandPlugin.java @@ -25,7 +25,7 @@ isExternal = PluginConstants.IS_EXTERNAL ) public class TutorialIslandPlugin extends Plugin { - public static final String version = "1.3.16"; + public static final String version = "1.3.17"; private boolean toggleMusic; private boolean toggleRoofs; diff --git a/src/main/java/net/runelite/client/plugins/microbot/tutorialisland/TutorialIslandScript.java b/src/main/java/net/runelite/client/plugins/microbot/tutorialisland/TutorialIslandScript.java index a8e567a28d..a22b63e6ac 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/tutorialisland/TutorialIslandScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/tutorialisland/TutorialIslandScript.java @@ -334,21 +334,26 @@ private boolean walkAndAct(Rs2NpcModel npc, int reach, String action, Runnable a } private boolean walkAndAttackRat() { - // The rat pit gate (id 9719) blocks both Rs2Walker and native pathfinder. - // Explicitly open it if we're standing adjacent; after passing through, attack directly. - WorldPoint playerLoc = Rs2Player.getWorldLocation(); - if (playerLoc != null && playerLoc.getX() == 3111 && playerLoc.getY() >= 9516 && playerLoc.getY() <= 9519) { - if (Microbot.getRs2TileObjectCache().query().withId(9719).interact("Open")) { - sleepUntil(() -> { - WorldPoint p = Rs2Player.getWorldLocation(); - return p != null && p.getY() < 9516; - }, 3000); + Rs2NpcModel rat = Microbot.getRs2NpcCache().query().withName("Giant rat").nearest(); + if (rat == null || rat.getWorldLocation() == null) return false; + switch (RatPen.next(Rs2Player.getWorldLocation(), Rs2Tile.isTileReachable(rat.getWorldLocation()))) { + case ATTACK: + return rat.click("Attack"); + case OPEN_GATE: + if (Microbot.getRs2TileObjectCache().query().withId(RatPen.GATE_ID).interact("Open")) { + sleepUntil(() -> Rs2Tile.isTileReachable(RatPen.GATE_INSIDE), 3000); + } + return false; + default: + Rs2Walker.walkTo(RatPen.GATE_OUTSIDE, 1); + Rs2Player.waitForWalking(); return false; - } } + } + + private boolean attackRatAtRange() { Rs2NpcModel rat = Microbot.getRs2NpcCache().query().withName("Giant rat").nearest(); - if (rat == null || rat.getWorldLocation() == null) return false; - if (!Rs2Walker.canReach(rat.getWorldLocation())) return false; + if (rat == null) return false; return rat.click("Attack"); } @@ -655,7 +660,7 @@ public void CombatGuide() { Rs2Inventory.wield("Bronze arrow"); Rs2Random.waitEx(600, 100); } - walkAndAttackRat(); + attackRatAtRange(); } else if (Microbot.getVarbitPlayerValue(281) == 470) { if (npc == null) return; walkAndTalk(npc); diff --git a/src/test/java/net/runelite/client/plugins/microbot/tutorialisland/RatPenTest.java b/src/test/java/net/runelite/client/plugins/microbot/tutorialisland/RatPenTest.java new file mode 100644 index 0000000000..b5e7b42909 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/tutorialisland/RatPenTest.java @@ -0,0 +1,40 @@ +package net.runelite.client.plugins.microbot.tutorialisland; + +import net.runelite.api.coords.WorldPoint; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public final class RatPenTest { + @Test + void attacksWhenRatIsReachable() { + assertEquals(RatPen.Step.ATTACK, RatPen.next(new WorldPoint(3111, 9526, 0), true)); + } + + @Test + void walksToGateWhenRatIsUnreachableFromLadder() { + assertEquals(RatPen.Step.WALK_TO_GATE, RatPen.next(new WorldPoint(3111, 9526, 0), false)); + } + + @Test + void walksToGateWhenRatIsUnreachableFromInstructor() { + assertEquals(RatPen.Step.WALK_TO_GATE, RatPen.next(new WorldPoint(3107, 9509, 0), false)); + } + + @Test + void opensGateWhenStandingAtEitherGateTile() { + assertEquals(RatPen.Step.OPEN_GATE, RatPen.next(new WorldPoint(3111, 9518, 0), false)); + assertEquals(RatPen.Step.OPEN_GATE, RatPen.next(new WorldPoint(3111, 9519, 0), false)); + assertEquals(RatPen.Step.OPEN_GATE, RatPen.next(new WorldPoint(3112, 9517, 0), false)); + } + + @Test + void walksToGateWhenPlayerLocationIsUnknown() { + assertEquals(RatPen.Step.WALK_TO_GATE, RatPen.next(null, false)); + } + + @Test + void doesNotOpenGateFromAnotherPlane() { + assertEquals(RatPen.Step.WALK_TO_GATE, RatPen.next(new WorldPoint(3111, 9518, 1), false)); + } +} From 01117fe513faeb69889e0cab7cf2415dcbfb3808 Mon Sep 17 00:00:00 2001 From: chsami Date: Fri, 16 Oct 2026 18:05:03 +0200 Subject: [PATCH 32/38] fix(PVirewatchKiller): skip loop and overlay until fight area is set (1.0.5) (#645) * fix(PVirewatchKiller): skip script loop until fight area is set (1.0.5) The script loop starts with no initial delay and dereferenced plugin.fightArea before the first GameTick assigned it, and again after the plugin stop hook cleared it. Read the player location once with a null guard, snapshot fightArea and startingLocation, and return early while any of them is missing. Compare WorldPoints with equals instead of identity so the return-to-start branch is no longer always taken. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV * fix(PVirewatchKiller): skip NPC outline in overlay until fight area is set Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV * fix(PVirewatchKiller): read fight area once per frame for the tile loop Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV --------- Co-authored-by: Claude Opus 5.5 --- .../virewatch/PVirewatchKillerOverlay.java | 14 ++++--- .../virewatch/PVirewatchKillerPlugin.java | 2 +- .../microbot/virewatch/PVirewatchScript.java | 27 +++++++++++-- .../virewatch/PVirewatchScriptTest.java | 40 +++++++++++++++++++ 4 files changed, 72 insertions(+), 11 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/virewatch/PVirewatchScriptTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/virewatch/PVirewatchKillerOverlay.java b/src/main/java/net/runelite/client/plugins/microbot/virewatch/PVirewatchKillerOverlay.java index 8e58ec4794..a00a297c40 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/virewatch/PVirewatchKillerOverlay.java +++ b/src/main/java/net/runelite/client/plugins/microbot/virewatch/PVirewatchKillerOverlay.java @@ -5,6 +5,7 @@ import net.runelite.api.Perspective; import net.runelite.api.TileObject; import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldArea; import net.runelite.api.coords.WorldPoint; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; @@ -55,10 +56,11 @@ public Dimension render(Graphics2D graphics) { renderOutline(graphics, alterStatue); } - if (plugin.fightArea != null && !config.disableFightArea()) { - for (int x = plugin.fightArea.getX(); x < plugin.fightArea.getX() + plugin.fightArea.getWidth(); x++) { - for (int y = plugin.fightArea.getY(); y < plugin.fightArea.getY() + plugin.fightArea.getHeight(); y++) { - WorldPoint worldPoint = new WorldPoint(x, y, plugin.fightArea.getPlane()); + WorldArea fightArea = plugin.fightArea; + if (fightArea != null && !config.disableFightArea()) { + for (int x = fightArea.getX(); x < fightArea.getX() + fightArea.getWidth(); x++) { + for (int y = fightArea.getY(); y < fightArea.getY() + fightArea.getHeight(); y++) { + WorldPoint worldPoint = new WorldPoint(x, y, fightArea.getPlane()); drawTile(graphics, worldPoint, Color.YELLOW); } } @@ -75,11 +77,11 @@ public Dimension render(Graphics2D graphics) { } } - if (!config.disableNPCOutline()) { + if (!config.disableNPCOutline() && fightArea != null) { for (var npcModel : Microbot.getRs2NpcCache().query().withName("Vyrewatch Sentinel").where(n -> !n.isDead()).toList()) { NPC npc = npcModel.getNpc(); if (npc != null && npc.getCanvasTilePoly() != null) { - if (!plugin.fightArea.contains(npc.getWorldLocation())) continue; + if (!fightArea.contains(npc.getWorldLocation())) continue; try { modelOutlineRenderer.drawOutline(npc, 2, Color.ORANGE, 4); diff --git a/src/main/java/net/runelite/client/plugins/microbot/virewatch/PVirewatchKillerPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/virewatch/PVirewatchKillerPlugin.java index d50994b3e3..b0d71abbee 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/virewatch/PVirewatchKillerPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/virewatch/PVirewatchKillerPlugin.java @@ -47,7 +47,7 @@ ) public class PVirewatchKillerPlugin extends Plugin { - public final static String version = "1.0.4"; + public final static String version = "1.0.5"; private PLooter looterScript = new PLooter(); private PAlcher alchScript = new PAlcher(); diff --git a/src/main/java/net/runelite/client/plugins/microbot/virewatch/PVirewatchScript.java b/src/main/java/net/runelite/client/plugins/microbot/virewatch/PVirewatchScript.java index 403ae30d68..6862254bf3 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/virewatch/PVirewatchScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/virewatch/PVirewatchScript.java @@ -1,6 +1,9 @@ package net.runelite.client.plugins.microbot.virewatch; +import net.runelite.api.Player; import net.runelite.api.Skill; +import net.runelite.api.coords.WorldArea; +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.combat.Rs2Combat; @@ -19,13 +22,21 @@ public boolean run(PVirewatchKillerConfig config, PVirewatchKillerPlugin plugin) if (!super.run()) return; Rs2Combat.enableAutoRetialiate(); - if(plugin.fightArea.contains(Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation()))) { + WorldPoint playerLocation = Microbot.getClientThread().invoke(() -> { + Player localPlayer = Microbot.getClient().getLocalPlayer(); + return localPlayer == null ? null : localPlayer.getWorldLocation(); + }); + WorldArea fightArea = plugin.fightArea; + WorldPoint startingLocation = plugin.startingLocation; + if (!hasFightState(playerLocation, fightArea, startingLocation)) return; + + if(fightArea.contains(playerLocation)) { Microbot.status = "Figthing"; } - if(Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation()) != plugin.startingLocation) { + if(isAwayFromStart(playerLocation, startingLocation)) { if(plugin.ticksOutOfArea > config.tickToReturn() || plugin.countedTicks > config.tickToReturnCombat()) { - Rs2Walker.walkTo(plugin.startingLocation, 0); + Rs2Walker.walkTo(startingLocation, 0); } } @@ -45,7 +56,7 @@ public boolean run(PVirewatchKillerConfig config, PVirewatchKillerPlugin plugin) if(Rs2Player.isInteracting()) { sleepUntil(() -> Microbot.getClient().getBoostedSkillLevel(Skill.PRAYER) > config.prayAt()); Microbot.status = "WALKING TO STARTING POINT"; - Rs2Walker.walkTo(plugin.startingLocation, 0); + Rs2Walker.walkTo(startingLocation, 0); } } @@ -60,6 +71,14 @@ public boolean run(PVirewatchKillerConfig config, PVirewatchKillerPlugin plugin) return true; } + static boolean hasFightState(WorldPoint playerLocation, WorldArea fightArea, WorldPoint startingLocation) { + return playerLocation != null && fightArea != null && startingLocation != null; + } + + static boolean isAwayFromStart(WorldPoint playerLocation, WorldPoint startingLocation) { + return !playerLocation.equals(startingLocation); + } + @Override public void shutdown() { super.shutdown(); diff --git a/src/test/java/net/runelite/client/plugins/microbot/virewatch/PVirewatchScriptTest.java b/src/test/java/net/runelite/client/plugins/microbot/virewatch/PVirewatchScriptTest.java new file mode 100644 index 0000000000..a6357795a9 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/virewatch/PVirewatchScriptTest.java @@ -0,0 +1,40 @@ +package net.runelite.client.plugins.microbot.virewatch; + +import net.runelite.api.coords.WorldArea; +import net.runelite.api.coords.WorldPoint; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PVirewatchScriptTest { + + private final WorldPoint start = new WorldPoint(3600, 3360, 0); + private final WorldArea fightArea = new WorldArea(3598, 3358, 5, 5, 0); + + @Test + void fightAreaNotYetSetByFirstGameTickSkipsTheLoop() { + assertFalse(PVirewatchScript.hasFightState(start, null, null)); + assertFalse(PVirewatchScript.hasFightState(start, null, start)); + } + + @Test + void missingLocalPlayerSkipsTheLoop() { + assertFalse(PVirewatchScript.hasFightState(null, fightArea, start)); + } + + @Test + void fightStateIsReadyOncePlayerAreaAndStartAreSet() { + assertTrue(PVirewatchScript.hasFightState(start, fightArea, start)); + } + + @Test + void standingOnAnEqualStartTileIsNotAwayFromStart() { + assertFalse(PVirewatchScript.isAwayFromStart(new WorldPoint(3600, 3360, 0), start)); + } + + @Test + void anotherTileIsAwayFromStart() { + assertTrue(PVirewatchScript.isAwayFromStart(new WorldPoint(3601, 3360, 0), start)); + } +} From 293dc8e5842b885205fd314a8ed64cc2c783a206 Mon Sep 17 00:00:00 2001 From: chsami Date: Fri, 16 Oct 2026 18:33:58 +0200 Subject: [PATCH 33/38] fix(MoonsOfPeril): fish the river fishing spot during resupply (1.0.5) (#647) The rev239 cache update removed PMOON_FISHING_SPOT_MULTI (51367) and moved the Moons of Peril bream spot to ObjectID.RIVER_FISHING_SPOT; the old id now belongs to a sailing gangplank, so the Fish interaction matched nothing and the resupply loop idled beside the stream forever. Target RIVER_FISHING_SPOT (the id the quest helper uses at the same stream) and stop fishing with a log line when no bream is caught for 60 seconds. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../moonsofperil/MoonsOfPerilPlugin.java | 2 +- .../handlers/ResupplyHandler.java | 25 +++++++++++++++++-- .../handlers/ResupplyHandlerTest.java | 25 +++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/moonsofperil/handlers/ResupplyHandlerTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/moonsofperil/MoonsOfPerilPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/moonsofperil/MoonsOfPerilPlugin.java index 7b791187b1..e76c3d1e7a 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/moonsofperil/MoonsOfPerilPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/moonsofperil/MoonsOfPerilPlugin.java @@ -35,7 +35,7 @@ ) @Slf4j public class MoonsOfPerilPlugin extends Plugin { - static final String version = "1.0.4"; + static final String version = "1.0.5"; @Inject private MoonsOfPerilConfig config; @Provides diff --git a/src/main/java/net/runelite/client/plugins/microbot/moonsofperil/handlers/ResupplyHandler.java b/src/main/java/net/runelite/client/plugins/microbot/moonsofperil/handlers/ResupplyHandler.java index d10598a152..a151f5e16f 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/moonsofperil/handlers/ResupplyHandler.java +++ b/src/main/java/net/runelite/client/plugins/microbot/moonsofperil/handlers/ResupplyHandler.java @@ -17,6 +17,8 @@ public class ResupplyHandler implements BaseHandler { + static final int BREAM_FISHING_SPOT_ID = ObjectID.RIVER_FISHING_SPOT; + static final long FISHING_STALL_TIMEOUT_MS = 60_000; private final int minMoonlightPotions; private final int minCookedBream; @@ -173,6 +175,8 @@ private void fishBream() { Rs2Walker.walkFastCanvas(new WorldPoint(1520, 9689, 0)); + int lastFreeSlots = Rs2Inventory.emptySlotCount(); + long lastProgressAt = System.currentTimeMillis(); while (!Rs2Inventory.isFull()) { // If for any reason we lost the net mid-loop, bail if (!Rs2Inventory.contains(ItemID.BIG_NET)) { @@ -180,9 +184,22 @@ private void fishBream() { return; } + int freeSlots = Rs2Inventory.emptySlotCount(); + long now = System.currentTimeMillis(); + if (freeSlots != lastFreeSlots) { + lastFreeSlots = freeSlots; + lastProgressAt = now; + } else if (isFishingStalled(lastProgressAt, now)) { + Microbot.log("No bream caught in " + FISHING_STALL_TIMEOUT_MS / 1000 + "s; giving up on fishing this resupply."); + return; + } + if (!Rs2Player.isAnimating()) { - Microbot.getRs2TileObjectCache().query().interact(51367, "Fish"); - sleep(3000, 4000); + if (Microbot.getRs2TileObjectCache().query().interact(BREAM_FISHING_SPOT_ID, "Fish")) { + sleep(3000, 4000); + } else if (debugLogging) { + Microbot.log("Bream fishing spot not found."); + } } sleep(300, 500); } @@ -194,6 +211,10 @@ private void fishBream() { // Rs2Inventory.drop(ItemID.BIG_NET); } + static boolean isFishingStalled(long lastProgressAt, long now) { + return now - lastProgressAt >= FISHING_STALL_TIMEOUT_MS; + } + private void cookBream() { if (debugLogging) {Microbot.log("Walking to cooking stove");} Rs2Walker.walkFastCanvas(new WorldPoint(1512, 9693, 0)); diff --git a/src/test/java/net/runelite/client/plugins/microbot/moonsofperil/handlers/ResupplyHandlerTest.java b/src/test/java/net/runelite/client/plugins/microbot/moonsofperil/handlers/ResupplyHandlerTest.java new file mode 100644 index 0000000000..bee663051e --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/moonsofperil/handlers/ResupplyHandlerTest.java @@ -0,0 +1,25 @@ +package net.runelite.client.plugins.microbot.moonsofperil.handlers; + +import net.runelite.api.gameval.ObjectID; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ResupplyHandlerTest { + @Test + void fishesTheRiverFishingSpotNotTheRetiredMoonsId() { + assertEquals(ObjectID.RIVER_FISHING_SPOT, ResupplyHandler.BREAM_FISHING_SPOT_ID); + assertNotEquals(ObjectID.SAILING_GANGPLANK_PROXY_WIDE_VIKING, ResupplyHandler.BREAM_FISHING_SPOT_ID); + } + + @Test + void fishingStallsOnlyAfterTimeoutWithoutProgress() { + long start = 1_000_000L; + assertFalse(ResupplyHandler.isFishingStalled(start, start)); + assertFalse(ResupplyHandler.isFishingStalled(start, start + ResupplyHandler.FISHING_STALL_TIMEOUT_MS - 1)); + assertTrue(ResupplyHandler.isFishingStalled(start, start + ResupplyHandler.FISHING_STALL_TIMEOUT_MS)); + } +} From 95d4b7dae4484bcd8c1a4eafd31d341ef91d71bf Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 17 Oct 2026 15:52:22 +0200 Subject: [PATCH 34/38] fix(MahoganyHomes): guard null home and player location in walkToHome and bank (0.0.15) (#649) walkToHome and bank null-checked a local copy of the current home but then re-read plugin.getCurrentHome() through isMissingItems, planksNeeded and steelBarsNeeded and for the walk target, so a contract cleared on the client thread mid-loop threw an NPE. Rs2Player.getWorldLocation() was also passed to distanceBetween unchecked. Snapshot both once, guard them in isAwayFromHome, and pass the Home through to the material helpers. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../mahoganyhomez/MahoganyHomesPlugin.java | 2 +- .../mahoganyhomez/MahoganyHomesScript.java | 41 +++++++++++-------- .../MahoganyHomesScriptTest.java | 23 +++++++++++ 3 files changed, 47 insertions(+), 19 deletions(-) diff --git a/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesPlugin.java index 5b2326c9ba..9ad95f8239 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesPlugin.java @@ -55,7 +55,7 @@ isExternal = PluginConstants.IS_EXTERNAL ) public class MahoganyHomesPlugin extends Plugin { - public static final String version = "0.0.14"; + public static final String version = "0.0.15"; private static final List PLANKS = Arrays.asList(ItemID.PLANK, ItemID.OAK_PLANK, ItemID.TEAK_PLANK, ItemID.MAHOGANY_PLANK); private static final List PLANK_NAMES = Arrays.asList("Plank", "Oak plank", "Teak plank", "Mahogany plank"); private static final Map MAHOGANY_HOMES_REPAIRS = new HashMap<>(); diff --git a/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScript.java b/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScript.java index 7fa515249e..d6c4bc6fa6 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScript.java @@ -541,9 +541,8 @@ public void handleContractDialogue() { // Bank if we need to private void bank() { Home currentHome = plugin.getCurrentHome(); - if (currentHome != null - && plugin.distanceBetween(currentHome.getArea(), Rs2Player.getWorldLocation()) > 0 - && isMissingItems()) { + if (isAwayFromHome(currentHome, Rs2Player.getWorldLocation()) + && isMissingItems(currentHome)) { ShortestPathPlugin.getPathfinderConfig().setIgnoreTeleportAndItems(true); BankLocation bankLocation = Rs2Bank.getNearestBank(currentHome.getLocation()); ShortestPathPlugin.getPathfinderConfig().setIgnoreTeleportAndItems(false); @@ -582,19 +581,19 @@ && isMissingItems()) { } } else { // Withdraw steel bars first if needed - if (steelBarsNeeded() > steelBarsInInventory()) { - Rs2Bank.withdrawX(ItemID.STEEL_BAR, steelBarsNeeded() - steelBarsInInventory()); + if (steelBarsNeeded(currentHome) > steelBarsInInventory()) { + Rs2Bank.withdrawX(ItemID.STEEL_BAR, steelBarsNeeded(currentHome) - steelBarsInInventory()); Rs2Inventory.waitForInventoryChanges(5000); } // Calculate if we'll have enough space for planks after steel bars int freeSlots = Rs2Inventory.emptySlotCount(); int currentPlanks = planksInInventory() + planksInPlankSack(); - int additionalPlanksNeeded = planksNeeded() - currentPlanks; + int additionalPlanksNeeded = planksNeeded(currentHome) - currentPlanks; if (additionalPlanksNeeded <= 0) { // We already have enough planks - log("Already have sufficient planks: %d/%d", currentPlanks, planksNeeded()); + log("Already have sufficient planks: %d/%d", currentPlanks, planksNeeded(currentHome)); } else if (freeSlots >= additionalPlanksNeeded) { // Withdraw all planks to fill inventory Rs2Bank.withdrawAll(plugin.getConfig().currentTier().getPlankSelection().getPlankId()); @@ -617,25 +616,31 @@ && isMissingItems()) { // Walk to current home private void walkToHome() { Home currentHome = plugin.getCurrentHome(); - if (currentHome != null - && plugin.distanceBetween(currentHome.getArea(), Rs2Player.getWorldLocation()) > 0 - && !isMissingItems()) { - Rs2Walker.walkWithStateUntil(plugin.getCurrentHome().getLocation(), 3, + if (isAwayFromHome(currentHome, Rs2Player.getWorldLocation()) + && !isMissingItems(currentHome)) { + Rs2Walker.walkWithStateUntil(currentHome.getLocation(), 3, () -> readyFurniture() != null); if (readyFurniture() != null) fix(); } } - private boolean isMissingItems() { - return (planksInInventory() + planksInPlankSack()) < planksNeeded() - || steelBarsInInventory() < steelBarsNeeded(); + + boolean isAwayFromHome(Home home, WorldPoint playerLocation) { + return home != null + && playerLocation != null + && plugin.distanceBetween(home.getArea(), playerLocation) > 0; + } + + private boolean isMissingItems(Home home) { + return (planksInInventory() + planksInPlankSack()) < planksNeeded(home) + || steelBarsInInventory() < steelBarsNeeded(home); } - private int planksNeeded() { - return plugin.getCurrentHome().getRequiredPlanks(plugin.getContractTier()); + private int planksNeeded(Home home) { + return home.getRequiredPlanks(plugin.getContractTier()); } - private int steelBarsNeeded() { - return plugin.getCurrentHome().getRequiredSteelBars(plugin.getContractTier()); + private int steelBarsNeeded(Home home) { + return home.getRequiredSteelBars(plugin.getContractTier()); } private int planksInInventory() { diff --git a/src/test/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScriptTest.java b/src/test/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScriptTest.java index fa6272ff6c..3544b0d6ba 100644 --- a/src/test/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScriptTest.java +++ b/src/test/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScriptTest.java @@ -4,7 +4,9 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; class MahoganyHomesScriptTest { @@ -24,4 +26,25 @@ void closestContractLocationPicksNearestOffice() { assertEquals(ContractLocation.MAHOGANY_HOMES_HOSIDIUS.getLocation(), MahoganyHomesScript.getClosestContractLocation(new WorldPoint(1760, 3600, 0))); } + + @Test + void isAwayFromHomeIsFalseWithoutHomeOrPlayerLocation() { + MahoganyHomesScript script = scriptWithPlugin(); + assertFalse(script.isAwayFromHome(null, new WorldPoint(3213, 3424, 0))); + assertFalse(script.isAwayFromHome(Home.JESS, null)); + assertFalse(script.isAwayFromHome(null, null)); + } + + @Test + void isAwayFromHomeComparesPlayerLocationToHomeArea() { + MahoganyHomesScript script = scriptWithPlugin(); + assertTrue(script.isAwayFromHome(Home.JESS, new WorldPoint(3213, 3424, 0))); + assertFalse(script.isAwayFromHome(Home.JESS, new WorldPoint(2615, 3293, 1))); + } + + private static MahoganyHomesScript scriptWithPlugin() { + MahoganyHomesScript script = new MahoganyHomesScript(); + script.plugin = new MahoganyHomesPlugin(); + return script; + } } From 1445d2a98f5b11d75db9bdd810c28fde4be6121b Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 17 Oct 2026 18:15:49 +0200 Subject: [PATCH 35/38] fix(MahoganyHomes): skip hotspots beyond home object count in getFixableObjects (0.0.16) (#650) getFixableObjects indexed HotspotObjects.objects by Hotspot ordinal without a bounds check, so a broken hotspot 8 varbit at a 7-object home (or 7/8 at SARAH) threw ArrayIndexOutOfBoundsException and aborted the script loop. Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../mahoganyhomez/MahoganyHomesPlugin.java | 2 +- .../mahoganyhomez/MahoganyHomesScript.java | 12 ++++++--- .../MahoganyHomesScriptTest.java | 25 +++++++++++++++++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesPlugin.java index 9ad95f8239..41f53b416c 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesPlugin.java @@ -55,7 +55,7 @@ isExternal = PluginConstants.IS_EXTERNAL ) public class MahoganyHomesPlugin extends Plugin { - public static final String version = "0.0.15"; + public static final String version = "0.0.16"; private static final List PLANKS = Arrays.asList(ItemID.PLANK, ItemID.OAK_PLANK, ItemID.TEAK_PLANK, ItemID.MAHOGANY_PLANK); private static final List PLANK_NAMES = Arrays.asList("Plank", "Oak plank", "Teak plank", "Mahogany plank"); private static final Map MAHOGANY_HOMES_REPAIRS = new HashMap<>(); diff --git a/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScript.java b/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScript.java index d6c4bc6fa6..2ee29bf5cb 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScript.java @@ -74,10 +74,7 @@ private List getFixableObjects() { List fixableHotspots = Hotspot.getBrokenHotspots(); HotspotObjects hotspotObjects = plugin.getCurrentHome().getHotspotObjects(); - // Precompute the set of IDs - Set ids = fixableHotspots.stream() - .map(hotspot -> hotspotObjects.objects[hotspot.ordinal()].getObjectId()) - .collect(Collectors.toSet()); + Set ids = getFixableObjectIds(fixableHotspots, hotspotObjects); // Filter using the precomputed set return objects.stream() @@ -86,6 +83,13 @@ private List getFixableObjects() { .collect(Collectors.toList()); } + static Set getFixableObjectIds(List hotspots, HotspotObjects hotspotObjects) { + return hotspots.stream() + .filter(hotspot -> hotspot.ordinal() < hotspotObjects.objects.length) + .map(hotspot -> hotspotObjects.objects[hotspot.ordinal()].getObjectId()) + .collect(Collectors.toSet()); + } + // Custom logging methods private void log(String message) { if (plugin.getConfig().logMessages()) { diff --git a/src/test/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScriptTest.java b/src/test/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScriptTest.java index 3544b0d6ba..b2b80988c2 100644 --- a/src/test/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScriptTest.java +++ b/src/test/java/net/runelite/client/plugins/microbot/mahoganyhomez/MahoganyHomesScriptTest.java @@ -3,6 +3,9 @@ import net.runelite.api.coords.WorldPoint; import org.junit.jupiter.api.Test; +import java.util.Arrays; +import java.util.Set; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; @@ -42,6 +45,28 @@ void isAwayFromHomeComparesPlayerLocationToHomeArea() { assertFalse(script.isAwayFromHome(Home.JESS, new WorldPoint(2615, 3293, 1))); } + @Test + void fixableObjectIdsSkipsHotspotsBeyondHomeObjectCount() { + Set ids = MahoganyHomesScript.getFixableObjectIds(Arrays.asList(Hotspot.values()), HotspotObjects.ROSS); + assertEquals(HotspotObjects.ROSS.objects.length, ids.size()); + assertTrue(ids.contains(HotspotObjects.ROSS.objects[6].getObjectId())); + } + + @Test + void fixableObjectIdsSkipsHotspotsBeyondSixObjectHome() { + Set ids = MahoganyHomesScript.getFixableObjectIds( + Arrays.asList(Hotspot.MAHOGANY_HOMES_HOTSPOT_1, Hotspot.MAHOGANY_HOMES_HOTSPOT_7, Hotspot.MAHOGANY_HOMES_HOTSPOT_8), + HotspotObjects.SARAH); + assertEquals(Set.of(HotspotObjects.SARAH.objects[0].getObjectId()), ids); + } + + @Test + void fixableObjectIdsMapsAllHotspotsForEightObjectHome() { + Set ids = MahoganyHomesScript.getFixableObjectIds(Arrays.asList(Hotspot.values()), HotspotObjects.JESS); + assertEquals(8, ids.size()); + assertTrue(ids.contains(HotspotObjects.JESS.objects[7].getObjectId())); + } + private static MahoganyHomesScript scriptWithPlugin() { MahoganyHomesScript script = new MahoganyHomesScript(); script.plugin = new MahoganyHomesPlugin(); From 79f32473257422c203ed42fd55901bb0b9784de3 Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 17 Oct 2026 19:38:14 +0200 Subject: [PATCH 36/38] fix(FarmTreeRun): guard null object composition in isPatchEmpty and handlePatch (1.2.5) (#651) * fix(FarmTreeRun): treat null object composition as empty patch in isPatchEmpty (1.2.4) Rs2GameObject.getObjectComposition is nullable and returns null on a client-thread read timeout or a null impostor while the patch object transforms after planting, which threw an NPE from handlePlantingTree. A null composition now reads as an empty patch so the post-plant wait keeps polling and planting is retried instead of the patch being skipped. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV * fix(FarmTreeRun): guard null tree composition in handlePatch and skip composition read when protection is off (1.2.5) handlePatch dereferenced Rs2GameObject.findObjectComposition(...).getActions() without a null check; the composition is null on the same client-thread read failure or null impostor that caused the isPatchEmpty NPE. Return false so the loop retries on the next tick. handlePayment's protection opt-out checks no longer read the patch composition, so a transient null can no longer fall through to paying the gardener when protection is disabled. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV --------- Co-authored-by: Claude Opus 5.5 --- .../farmtreerun/FarmTreeRunPlugin.java | 2 +- .../farmtreerun/FarmTreeRunScript.java | 20 ++-- .../FarmTreeRunHandlePatchTest.java | 99 +++++++++++++++++++ .../FarmTreeRunPatchEmptyTest.java | 40 ++++++++ 4 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunHandlePatchTest.java create mode 100644 src/test/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunPatchEmptyTest.java 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 2780851b2f..276d08f405 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.2.3"; + public static final String version = "1.2.5"; @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 f349d18695..119f417a7e 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 @@ -868,7 +868,11 @@ private boolean handlePatch(FarmTreeRunConfig config, Patch patch) { // Gagex named actions differently, sometimes it's Pick-fruit and sometimes Pick-banana. // Also seen "Chop down" and "Chop-down". - List exactTreeActions = Arrays.stream(Rs2GameObject.findObjectComposition(treePatch.getId()).getActions()).collect(Collectors.toList()); + ObjectComposition treeComposition = Rs2GameObject.findObjectComposition(treePatch.getId()); + if (treeComposition == null || treeComposition.getActions() == null) { + return false; + } + List exactTreeActions = Arrays.stream(treeComposition.getActions()).collect(Collectors.toList()); for (String action : exactTreeActions) { if (action == null) continue; @@ -954,13 +958,13 @@ private void handleNotingFruit(Patch patch) { * @return {@code true} if payment was successful, else {@code false} */ private boolean handlePayment(FarmTreeRunConfig config, Patch patch, PaymentKind action) { - if (isTreePatch(patch) && !isPatchEmpty(patch) && !shouldProtectTree(config) && action != PaymentKind.CLEAR) + if (isTreePatch(patch) && !shouldProtectTree(config) && action != PaymentKind.CLEAR) return true; - if (isFruitTreePatch(patch) && !isPatchEmpty(patch) && !shouldProtectFruitTree(config) && action != PaymentKind.CLEAR) + if (isFruitTreePatch(patch) && !shouldProtectFruitTree(config) && action != PaymentKind.CLEAR) return true; - if (isHardTreePatch(patch) && !isPatchEmpty(patch) && !shouldProtectHardTree(config) && action != PaymentKind.CLEAR) + if (isHardTreePatch(patch) && !shouldProtectHardTree(config) && action != PaymentKind.CLEAR) return true; int paymentId = isHardTreePatch(patch) ? config.selectedHardTree().getPaymentId() @@ -1207,8 +1211,12 @@ private boolean isCompostEnabled(FarmTreeRunConfig config) { } private boolean isPatchEmpty(Patch patch) { - String name = Rs2GameObject.getObjectComposition(patch.getId()).getName().toLowerCase(); - return name.endsWith("patch"); + return isEmptyPatchComposition(Rs2GameObject.getObjectComposition(patch.getId())); + } + + static boolean isEmptyPatchComposition(ObjectComposition composition) { + if (composition == null || composition.getName() == null) return true; + return composition.getName().toLowerCase().endsWith("patch"); } private Integer getLinkedId(int id) { diff --git a/src/test/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunHandlePatchTest.java b/src/test/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunHandlePatchTest.java new file mode 100644 index 0000000000..59f54c92d9 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunHandlePatchTest.java @@ -0,0 +1,99 @@ +package net.runelite.client.plugins.microbot.farmtreerun; + +import net.runelite.api.GameObject; +import net.runelite.api.ObjectComposition; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.Script; +import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; +import net.runelite.client.plugins.microbot.util.npc.Rs2Npc; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.concurrent.ScheduledFuture; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.when; + +class FarmTreeRunHandlePatchTest { + + private static FarmTreeRunScript runningScript(FarmTreeRunConfig config) throws Exception { + FarmTreeRunScript script = new FarmTreeRunScript(mock(FarmTreeRunPlugin.class), config); + ScheduledFuture future = mock(ScheduledFuture.class); + when(future.isDone()).thenReturn(false); + Field field = Script.class.getDeclaredField("mainScheduledFuture"); + field.setAccessible(true); + field.set(script, future); + return script; + } + + @Test + void nullTreeCompositionSkipsPatchInsteadOfThrowing() throws Exception { + FarmTreeRunConfig config = mock(FarmTreeRunConfig.class); + FarmTreeRunScript script = runningScript(config); + GameObject treePatch = mock(GameObject.class); + when(treePatch.getId()).thenReturn(8392); + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic gameObject = mockStatic(Rs2GameObject.class)) { + microbot.when(Microbot::isLoggedIn).thenReturn(true); + gameObject.when(() -> Rs2GameObject.findObjectByImposter(anyInt(), anyString(), eq(false))).thenReturn(treePatch); + gameObject.when(() -> Rs2GameObject.findObjectComposition(anyInt())).thenReturn(null); + + Method handlePatch = FarmTreeRunScript.class.getDeclaredMethod("handlePatch", FarmTreeRunConfig.class, FarmTreeRunScript.Patch.class); + handlePatch.setAccessible(true); + Object result = handlePatch.invoke(script, config, FarmTreeRunScript.Patch.GNOME_STRONGHOLD_TREE_PATCH); + + assertEquals(Boolean.FALSE, result); + } + } + + @Test + void nullTreeActionsSkipsPatchInsteadOfThrowing() throws Exception { + FarmTreeRunConfig config = mock(FarmTreeRunConfig.class); + FarmTreeRunScript script = runningScript(config); + GameObject treePatch = mock(GameObject.class); + when(treePatch.getId()).thenReturn(8392); + ObjectComposition composition = mock(ObjectComposition.class); + when(composition.getActions()).thenReturn(null); + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic gameObject = mockStatic(Rs2GameObject.class)) { + microbot.when(Microbot::isLoggedIn).thenReturn(true); + gameObject.when(() -> Rs2GameObject.findObjectByImposter(anyInt(), anyString(), eq(false))).thenReturn(treePatch); + gameObject.when(() -> Rs2GameObject.findObjectComposition(anyInt())).thenReturn(composition); + + Method handlePatch = FarmTreeRunScript.class.getDeclaredMethod("handlePatch", FarmTreeRunConfig.class, FarmTreeRunScript.Patch.class); + handlePatch.setAccessible(true); + Object result = handlePatch.invoke(script, config, FarmTreeRunScript.Patch.GNOME_STRONGHOLD_TREE_PATCH); + + assertEquals(Boolean.FALSE, result); + } + } + + @Test + void disabledProtectionSkipsPaymentWithoutReadingComposition() throws Exception { + FarmTreeRunConfig config = mock(FarmTreeRunConfig.class); + when(config.protectTrees()).thenReturn(false); + FarmTreeRunScript script = runningScript(config); + try (MockedStatic gameObject = mockStatic(Rs2GameObject.class); + MockedStatic npc = mockStatic(Rs2Npc.class)) { + gameObject.when(() -> Rs2GameObject.getObjectComposition(anyInt())).thenReturn(null); + + Class paymentKind = Class.forName(FarmTreeRunScript.class.getName() + "$PaymentKind"); + Object protect = java.util.Arrays.stream(paymentKind.getEnumConstants()).filter(k -> k.toString().equals("PROTECT")).findFirst().orElseThrow(); + Method handlePayment = FarmTreeRunScript.class.getDeclaredMethod("handlePayment", FarmTreeRunConfig.class, FarmTreeRunScript.Patch.class, paymentKind); + handlePayment.setAccessible(true); + Object result = handlePayment.invoke(script, config, FarmTreeRunScript.Patch.GNOME_STRONGHOLD_TREE_PATCH, protect); + + assertEquals(Boolean.TRUE, result); + gameObject.verify(() -> Rs2GameObject.getObjectComposition(anyInt()), never()); + npc.verify(() -> Rs2Npc.getNearestNpcWithAction(anyString()), never()); + } + } +} diff --git a/src/test/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunPatchEmptyTest.java b/src/test/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunPatchEmptyTest.java new file mode 100644 index 0000000000..71e73d2a36 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunPatchEmptyTest.java @@ -0,0 +1,40 @@ +package net.runelite.client.plugins.microbot.farmtreerun; + +import net.runelite.api.ObjectComposition; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class FarmTreeRunPatchEmptyTest { + + private static ObjectComposition named(String name) { + ObjectComposition composition = mock(ObjectComposition.class); + when(composition.getName()).thenReturn(name); + return composition; + } + + @Test + void nullCompositionIsTreatedAsEmptyInsteadOfThrowing() { + assertTrue(FarmTreeRunScript.isEmptyPatchComposition(null)); + } + + @Test + void nullNameIsTreatedAsEmpty() { + assertTrue(FarmTreeRunScript.isEmptyPatchComposition(named(null))); + } + + @Test + void patchNameIsEmpty() { + assertTrue(FarmTreeRunScript.isEmptyPatchComposition(named("Tree patch"))); + assertTrue(FarmTreeRunScript.isEmptyPatchComposition(named("Fruit Tree Patch"))); + } + + @Test + void grownTreeIsNotEmpty() { + assertFalse(FarmTreeRunScript.isEmptyPatchComposition(named("Magic tree"))); + assertFalse(FarmTreeRunScript.isEmptyPatchComposition(named("Papaya tree"))); + } +} From 10db07ab87beba88569c28e9b3207b2d82e9d50a Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 17 Oct 2026 21:03:07 +0200 Subject: [PATCH 37/38] fix(ShootingStar): close stop race on null panel and null state (1.4.11) (#652) * fix(ShootingStar): close stop race on null panel and null state (1.4.11) On "Oh dear, you are dead!" the plugin called its own shutDown() directly, which removed and nulled the panel while the plugin stayed registered, so tick, event and config handlers kept queueing updatePanelList() runnables that dereferenced the null panel on the EDT. Even a runnable queued just before a normal stop read the field after removePanel() had cleared it. The script loop null-checked the static state field and then switched on it again after several client calls, so a concurrent stop that set state to null threw on ordinal(). - stop through Microbot.stopPlugin(this) so the plugin is unregistered - skip updatePanelList when there is no panel, and re-read the panel on the EDT so queued updates skip a removed panel - read state into a local once per loop iteration and switch on it - treat a missing antiban category as not busy Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV * fix(ShootingStar): reset loop state on start instead of on stop ShootingStarScript.shutdown() nulled state, currentStar and pickaxe while mainScheduledFuture.cancel(true) does not wait for a running iteration, so a loop that had already passed its null check went on to dereference the cleared fields (switch on state, currentStar in the WALKING, MINING and BANKING cases). The previous local snapshot of state only moved the NPE to the currentStar reads. - stop clearing state, currentStar and pickaxe in shutdown() - clear them at the top of run() with the other per-run resets - drop the now unneeded state snapshot in the loop - read the static state once in the overlay before rendering it Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV --------- Co-authored-by: Claude Opus 5.5 --- .../shootingstar/ShootingStarOverlay.java | 6 +- .../shootingstar/ShootingStarPlugin.java | 33 +++-- .../shootingstar/ShootingStarScript.java | 14 +- .../ShootingStarStopRaceTest.java | 135 ++++++++++++++++++ 4 files changed, 172 insertions(+), 16 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarStopRaceTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarOverlay.java b/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarOverlay.java index f0b6cb63bb..2a624756a0 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarOverlay.java +++ b/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarOverlay.java @@ -15,6 +15,7 @@ import net.runelite.client.plugins.microbot.shootingstar.ShootingStarConfig; import net.runelite.client.plugins.microbot.shootingstar.ShootingStarPlugin; import net.runelite.client.plugins.microbot.shootingstar.ShootingStarScript; +import net.runelite.client.plugins.microbot.shootingstar.enums.ShootingStarState; import net.runelite.client.plugins.microbot.util.misc.TimeUtils; import net.runelite.client.ui.overlay.OverlayPanel; import net.runelite.client.ui.overlay.OverlayPosition; @@ -77,11 +78,12 @@ public Dimension render(Graphics2D graphics) if (!config.isHideDevOverlay()) { - if (ShootingStarScript.state != null) + ShootingStarState state = ShootingStarScript.state; + if (state != null) { panelComponent.getChildren().add(LineComponent.builder() .left("Current State:") - .right(ShootingStarScript.state.name()) + .right(state.name()) .build()); } diff --git a/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarPlugin.java index 45d9ed1082..41a2e68d58 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarPlugin.java @@ -76,7 +76,7 @@ public class ShootingStarPlugin extends Plugin { - static final String version = "1.4.10"; + static final String version = "1.4.11"; @Getter private final CopyOnWriteArrayList starList = new CopyOnWriteArrayList<>(); @@ -278,7 +278,7 @@ public void onChatMessage(ChatMessage event) { if (event.getMessage().equalsIgnoreCase("oh dear, you are dead!") && config.shutdownOnDeath()) { Rs2Walker.setTarget(null); - shutDown(); + Microbot.stopPlugin(this); } } @@ -412,16 +412,29 @@ private void removePanel() public void updatePanelList(boolean fullUpdate) { - List stars = new ArrayList<>(starList); - - if (fullUpdate) - { - SwingUtilities.invokeLater(() -> panel.updateList(stars)); - } - else + if (panel == null) { - SwingUtilities.invokeLater(() -> panel.refreshList(stars)); + return; } + + List stars = new ArrayList<>(starList); + + SwingUtilities.invokeLater(() -> { + ShootingStarPanel currentPanel = panel; + if (currentPanel == null) + { + return; + } + + if (fullUpdate) + { + currentPanel.updateList(stars); + } + else + { + currentPanel.refreshList(stars); + } + }); } private void toggleOverlay(boolean hideOverlay) diff --git a/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarScript.java b/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarScript.java index 5b8f02c5a2..7eb0415816 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarScript.java @@ -23,6 +23,7 @@ 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.antiban.enums.Activity; +import net.runelite.client.plugins.microbot.util.antiban.enums.Category; import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; import net.runelite.client.plugins.microbot.util.combat.Rs2Combat; @@ -74,6 +75,9 @@ public boolean run() startTime = Instant.now(); Microbot.enableAutoRunOn = true; initialPlayerLocation = null; + state = null; + currentStar = null; + pickaxe = null; hasEquipment = false; hasInventory = false; walkBeforeHopStar = null; @@ -110,7 +114,7 @@ public boolean run() return; } - if (Rs2Player.isMoving() || Rs2Antiban.getCategory().isBusy()) + if (Rs2Player.isMoving() || isAntibanBusy(Rs2Antiban.getCategory())) { return; } @@ -302,12 +306,14 @@ public boolean run() public void shutdown() { super.shutdown(); - state = null; - currentStar = null; - pickaxe = null; Rs2Antiban.resetAntibanSettings(); } + static boolean isAntibanBusy(Category category) + { + return category != null && category.isBusy(); + } + void handleWalking(Star star) { toggleLockState(true); diff --git a/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarStopRaceTest.java b/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarStopRaceTest.java new file mode 100644 index 0000000000..7e21c001b4 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarStopRaceTest.java @@ -0,0 +1,135 @@ +package net.runelite.client.plugins.microbot.shootingstar; + +import java.lang.reflect.Field; +import java.util.concurrent.atomic.AtomicReference; +import javax.swing.SwingUtilities; +import net.runelite.client.plugins.microbot.shootingstar.enums.ShootingStarState; +import net.runelite.client.plugins.microbot.shootingstar.model.Star; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +class ShootingStarStopRaceTest +{ + private final AtomicReference edtFailure = new AtomicReference<>(); + private Thread.UncaughtExceptionHandler previousHandler; + private ShootingStarPlugin plugin; + + @BeforeEach + void setUp() + { + previousHandler = Thread.getDefaultUncaughtExceptionHandler(); + Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> edtFailure.set(throwable)); + plugin = new ShootingStarPlugin(); + } + + @AfterEach + void tearDown() + { + Thread.setDefaultUncaughtExceptionHandler(previousHandler); + } + + @Test + void updatePanelListIsNoOpWithoutPanel() throws Exception + { + plugin.updatePanelList(true); + plugin.updatePanelList(false); + flushEdt(); + + assertNull(edtFailure.get()); + } + + @Test + void queuedPanelUpdateSkipsRemovedPanel() throws Exception + { + ShootingStarPanel panel = mock(ShootingStarPanel.class); + setPanel(panel); + + SwingUtilities.invokeAndWait(() -> { + plugin.updatePanelList(true); + plugin.updatePanelList(false); + setPanelUnchecked(null); + }); + flushEdt(); + + assertNull(edtFailure.get()); + verify(panel, never()).updateList(anyList()); + verify(panel, never()).refreshList(anyList()); + } + + @Test + void queuedPanelUpdateReachesLivePanel() throws Exception + { + ShootingStarPanel panel = mock(ShootingStarPanel.class); + setPanel(panel); + + plugin.updatePanelList(true); + plugin.updatePanelList(false); + flushEdt(); + + assertNull(edtFailure.get()); + verify(panel).updateList(anyList()); + verify(panel).refreshList(anyList()); + } + + @Test + void missingAntibanCategoryIsNotBusy() + { + assertFalse(ShootingStarScript.isAntibanBusy(null)); + } + + @Test + void stoppingScriptKeepsLoopStateForInFlightIteration() throws Exception + { + ShootingStarScript script = new ShootingStarScript(plugin, mock(ShootingStarConfig.class)); + Star star = mock(Star.class); + Field currentStar = ShootingStarScript.class.getDeclaredField("currentStar"); + currentStar.setAccessible(true); + currentStar.set(script, star); + ShootingStarScript.state = ShootingStarState.MINING; + + try + { + script.shutdown(); + + assertSame(ShootingStarState.MINING, ShootingStarScript.state); + assertSame(star, currentStar.get(script)); + } + finally + { + ShootingStarScript.state = null; + } + } + + private static void flushEdt() throws Exception + { + SwingUtilities.invokeAndWait(() -> { }); + } + + private void setPanelUnchecked(ShootingStarPanel value) + { + try + { + setPanel(value); + } + catch (ReflectiveOperationException e) + { + throw new IllegalStateException(e); + } + } + + private void setPanel(ShootingStarPanel value) throws ReflectiveOperationException + { + Field field = ShootingStarPlugin.class.getDeclaredField("panel"); + field.setAccessible(true); + field.set(plugin, value); + } +} From d7f5501775991fb7d0168e25765cfab050110758 Mon Sep 17 00:00:00 2001 From: chsami Date: Sun, 18 Oct 2026 09:26:04 +0200 Subject: [PATCH 38/38] fix(TowerOfLifeCC): run summon query on client thread (1.1.5) (#653) The summoned-creature lookup filtered NPCs on Actor.getInteracting(), which asserts the client thread, but the query was evaluated by nearest() on the script executor inside sleepUntil, throwing IllegalStateException "must be called on client thread" and never detecting the summoned creature. Evaluate it with nearestOnClientThread(). Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV Co-authored-by: Claude Opus 5.5 --- .../TowerOfLifeCCPlugin.java | 2 +- .../TowerOfLifeCCScript.java | 13 ++- .../TowerOfLifeCCSummonQueryTest.java | 109 ++++++++++++++++++ 3 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCSummonQueryTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCPlugin.java index 2a98412040..252fefae0e 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCPlugin.java @@ -39,7 +39,7 @@ TowerOfLifeCCConfig provideConfig(ConfigManager configManager) { @Inject TowerOfLifeCCScript towerOfLifeCCScript; - static final String version = "1.1.4"; + static final String version = "1.1.5"; @Override protected void startUp() throws AWTException { diff --git a/src/main/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCScript.java b/src/main/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCScript.java index f0bf31a13d..65d71f12b4 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCScript.java @@ -405,10 +405,7 @@ void HandleCreature(LootingParameters params, int disposableItem, int secondaryI //Rs2Inventory.useItemOnObject(secondaryItem, altarObjectId); //Rs2Inventory.waitForInventoryChanges(3000); Microbot.getRs2TileObjectCache().query().interact(altarObjectId, "Activate"); - sleepUntil(() -> { summonedCreature = Microbot.getRs2NpcCache().query() - .where(npc -> npc.getNpc() != null - && (npc.getNpc().getInteracting() == null || npc.getNpc().getInteracting() == Microbot.getClient().getLocalPlayer())) - .nearest(); + sleepUntil(() -> { summonedCreature = findSummonedCreature(); return summonedCreature != null; }, 5000); //Microbot.log("Summoned creature"); @@ -437,4 +434,12 @@ else if (!Rs2Combat.inCombat()) } } } + + Rs2NpcModel findSummonedCreature() + { + return Microbot.getRs2NpcCache().query() + .where(npc -> npc.getNpc() != null + && (npc.getNpc().getInteracting() == null || npc.getNpc().getInteracting() == Microbot.getClient().getLocalPlayer())) + .nearestOnClientThread(); + } } diff --git a/src/test/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCSummonQueryTest.java b/src/test/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCSummonQueryTest.java new file mode 100644 index 0000000000..eaacd436d6 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCSummonQueryTest.java @@ -0,0 +1,109 @@ +package net.runelite.client.plugins.microbot.toweroflife_creaturecreation; + +import net.runelite.api.Actor; +import net.runelite.api.Client; +import net.runelite.api.NPC; +import net.runelite.api.Player; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.api.npc.Rs2NpcCache; +import net.runelite.client.plugins.microbot.api.npc.Rs2NpcQueryable; +import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class TowerOfLifeCCSummonQueryTest { + + private final AtomicBoolean onClientThread = new AtomicBoolean(false); + private final Player localPlayer = mock(Player.class); + + @Test + void summonQueryEvaluatesInteractingOnClientThread() { + Rs2NpcModel idle = npc(null); + try (MockedStatic microbot = mockStatic(Microbot.class)) { + Rs2NpcQueryable query = stubQuery(microbot, List.of(idle)); + + assertSame(idle, new TowerOfLifeCCScript().findSummonedCreature()); + verify(query, never()).nearest(); + } + } + + @Test + void summonQueryAcceptsCreatureTargetingLocalPlayer() { + Rs2NpcModel busy = npc(mock(Actor.class)); + Rs2NpcModel mine = npc(localPlayer); + try (MockedStatic microbot = mockStatic(Microbot.class)) { + stubQuery(microbot, List.of(busy, mine)); + + assertSame(mine, new TowerOfLifeCCScript().findSummonedCreature()); + } + } + + @Test + void summonQueryRejectsCreatureTargetingSomeoneElse() { + Rs2NpcModel busy = npc(mock(Actor.class)); + try (MockedStatic microbot = mockStatic(Microbot.class)) { + stubQuery(microbot, List.of(busy)); + + assertNull(new TowerOfLifeCCScript().findSummonedCreature()); + } + } + + private Rs2NpcModel npc(Actor interacting) { + NPC npc = mock(NPC.class); + when(npc.getInteracting()).thenAnswer(invocation -> { + if (!onClientThread.get()) { + throw new IllegalStateException("must be called on client thread"); + } + return interacting; + }); + Rs2NpcModel model = mock(Rs2NpcModel.class); + when(model.getNpc()).thenReturn(npc); + return model; + } + + @SuppressWarnings("unchecked") + private Rs2NpcQueryable stubQuery(MockedStatic microbot, List npcs) { + Client client = mock(Client.class); + when(client.getLocalPlayer()).thenReturn(localPlayer); + microbot.when(Microbot::getClient).thenReturn(client); + + AtomicReference> filter = new AtomicReference<>(npc -> true); + Rs2NpcQueryable query = mock(Rs2NpcQueryable.class); + when(query.where(any())).thenAnswer(invocation -> { + filter.set(filter.get().and(invocation.getArgument(0))); + return query; + }); + when(query.nearest()).thenAnswer(invocation -> first(npcs, filter.get())); + when(query.nearestOnClientThread()).thenAnswer(invocation -> { + onClientThread.set(true); + try { + return first(npcs, filter.get()); + } finally { + onClientThread.set(false); + } + }); + + Rs2NpcCache cache = mock(Rs2NpcCache.class); + when(cache.query()).thenReturn(query); + microbot.when(Microbot::getRs2NpcCache).thenReturn(cache); + return query; + } + + private static Rs2NpcModel first(List npcs, Predicate filter) { + return npcs.stream().filter(filter).findFirst().orElse(null); + } +}