From 383a739ede261caecb0b1e1d989e7cc4a550caa3 Mon Sep 17 00:00:00 2001 From: chsami <23292806+chsami@users.noreply.github.com> Date: Sat, 17 Oct 2026 20:49:35 +0200 Subject: [PATCH 1/2] 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 --- .../shootingstar/ShootingStarPlugin.java | 33 ++++-- .../shootingstar/ShootingStarScript.java | 13 ++- .../ShootingStarStopRaceTest.java | 109 ++++++++++++++++++ 3 files changed, 142 insertions(+), 13 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/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..093d7166ca 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; @@ -103,14 +104,15 @@ public boolean run() state = updateStarState(); } - if (state == null) + ShootingStarState currentState = state; + if (currentState == null) { Microbot.showMessage("Unable to evaluate state"); Microbot.stopPlugin(plugin); return; } - if (Rs2Player.isMoving() || Rs2Antiban.getCategory().isBusy()) + if (Rs2Player.isMoving() || isAntibanBusy(Rs2Antiban.getCategory())) { return; } @@ -125,7 +127,7 @@ public boolean run() return; } - switch (state) + switch (currentState) { case WAITING_FOR_STAR: if (config.useNearestHighTierStar()) @@ -308,6 +310,11 @@ public void shutdown() 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..c9d0abfda1 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarStopRaceTest.java @@ -0,0 +1,109 @@ +package net.runelite.client.plugins.microbot.shootingstar; + +import java.lang.reflect.Field; +import java.util.concurrent.atomic.AtomicReference; +import javax.swing.SwingUtilities; +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.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)); + } + + 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 918abb87241039f09d5eecbd0959350baf9ebde0 Mon Sep 17 00:00:00 2001 From: chsami <23292806+chsami@users.noreply.github.com> Date: Sat, 17 Oct 2026 20:54:43 +0200 Subject: [PATCH 2/2] 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 --- .../shootingstar/ShootingStarOverlay.java | 6 +++-- .../shootingstar/ShootingStarScript.java | 11 ++++---- .../ShootingStarStopRaceTest.java | 26 +++++++++++++++++++ 3 files changed, 35 insertions(+), 8 deletions(-) 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/ShootingStarScript.java b/src/main/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarScript.java index 093d7166ca..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 @@ -75,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; @@ -104,8 +107,7 @@ public boolean run() state = updateStarState(); } - ShootingStarState currentState = state; - if (currentState == null) + if (state == null) { Microbot.showMessage("Unable to evaluate state"); Microbot.stopPlugin(plugin); @@ -127,7 +129,7 @@ public boolean run() return; } - switch (currentState) + switch (state) { case WAITING_FOR_STAR: if (config.useNearestHighTierStar()) @@ -304,9 +306,6 @@ public boolean run() public void shutdown() { super.shutdown(); - state = null; - currentStar = null; - pickaxe = null; Rs2Antiban.resetAntibanSettings(); } 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 index c9d0abfda1..7e21c001b4 100644 --- a/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarStopRaceTest.java +++ b/src/test/java/net/runelite/client/plugins/microbot/shootingstar/ShootingStarStopRaceTest.java @@ -3,12 +3,15 @@ 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; @@ -83,6 +86,29 @@ 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(() -> { });