From 069af0b5aa1e17793495b2fd675a0582f452bf36 Mon Sep 17 00:00:00 2001 From: pjmarz Date: Tue, 9 Jun 2026 18:59:36 -0400 Subject: [PATCH 1/2] fix(antiban): prevent action-cooldown stall when play-style tick intervals cross Co-Authored-By: Claude Fable 5 --- .../microbot/util/antiban/Rs2Antiban.java | 8 ++- .../util/antiban/enums/PlayStyle.java | 7 ++- .../PlayStyleRandomTickIntervalTest.java | 58 +++++++++++++++++++ 3 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/antiban/enums/PlayStyleRandomTickIntervalTest.java diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/antiban/Rs2Antiban.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/antiban/Rs2Antiban.java index 1859f30294..c5ff529749 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/antiban/Rs2Antiban.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/antiban/Rs2Antiban.java @@ -287,9 +287,6 @@ private static void logDebug(String message) { } private static void performActionCooldown() { - if (Rs2AntibanSettings.universalAntiban) - Microbot.pauseAllScripts.compareAndSet(false, true); - if (Rs2AntibanSettings.nonLinearIntervals) playStyle.evolvePlayStyle(); @@ -298,6 +295,11 @@ private static void performActionCooldown() { else TIMEOUT = playStyle.getPrimaryTickInterval(); + // Pause and set the active flag together, only after TIMEOUT is computed. If computing the + // interval ever throws, scripts must not be left paused with no countdown to clear them. + if (Rs2AntibanSettings.universalAntiban) + Microbot.pauseAllScripts.compareAndSet(false, true); + Rs2AntibanSettings.actionCooldownActive = true; if (Rs2AntibanSettings.moveMouseRandomly && Rs2Random.diceFractional(Rs2AntibanSettings.moveMouseRandomlyChance)) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/antiban/enums/PlayStyle.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/antiban/enums/PlayStyle.java index 772a1ec202..3a365337ff 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/antiban/enums/PlayStyle.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/antiban/enums/PlayStyle.java @@ -111,7 +111,12 @@ public static PlayStyle random() { } public int getRandomTickInterval() { - return ThreadLocalRandom.current().nextInt(primaryTickInterval, secondaryTickInterval + 1); + // primaryTickInterval and secondaryTickInterval drift independently in evolvePlayStyle() + // and can cross (primary > secondary). nextInt(origin, bound) requires bound > origin, so + // order the bounds defensively to avoid IllegalArgumentException. + int lo = Math.min(primaryTickInterval, secondaryTickInterval); + int hi = Math.max(primaryTickInterval, secondaryTickInterval); + return ThreadLocalRandom.current().nextInt(lo, hi + 1); } public void evolvePlayStyle() { diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/antiban/enums/PlayStyleRandomTickIntervalTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/antiban/enums/PlayStyleRandomTickIntervalTest.java new file mode 100644 index 0000000000..23cad1ee0c --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/antiban/enums/PlayStyleRandomTickIntervalTest.java @@ -0,0 +1,58 @@ +package net.runelite.client.plugins.microbot.util.antiban.enums; + +import org.junit.Test; + +import java.lang.reflect.Field; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Regression test for the action-cooldown stall. + * + *

evolvePlayStyle() drifts primaryTickInterval and secondaryTickInterval independently, so over + * time they can cross (primary greater than secondary). getRandomTickInterval() must not throw + * IllegalArgumentException from ThreadLocalRandom.nextInt(origin, bound) when that happens; before + * the fix it did, which left Microbot.pauseAllScripts stuck true and froze running scripts. + */ +public class PlayStyleRandomTickIntervalTest { + + private static void setInterval(PlayStyle style, String field, int value) throws Exception { + Field f = PlayStyle.class.getDeclaredField(field); + f.setAccessible(true); + f.setInt(style, value); + } + + private static int getInterval(PlayStyle style, String field) throws Exception { + Field f = PlayStyle.class.getDeclaredField(field); + f.setAccessible(true); + return f.getInt(style); + } + + @Test + public void getRandomTickIntervalDoesNotThrowWhenIntervalsCross() throws Exception { + PlayStyle style = PlayStyle.values()[0]; + int origPrimary = getInterval(style, "primaryTickInterval"); + int origSecondary = getInterval(style, "secondaryTickInterval"); + try { + // Crossed bounds (primary greater than secondary): this is what previously threw. + setInterval(style, "primaryTickInterval", 10); + setInterval(style, "secondaryTickInterval", 5); + for (int i = 0; i < 10_000; i++) { + int v = style.getRandomTickInterval(); + assertTrue("interval " + v + " must fall within [5,10]", v >= 5 && v <= 10); + } + + // Equal bounds edge case (primary == secondary). + setInterval(style, "primaryTickInterval", 7); + setInterval(style, "secondaryTickInterval", 7); + assertEquals(7, style.getRandomTickInterval()); + } catch (IllegalArgumentException e) { + fail("getRandomTickInterval threw on crossed intervals: " + e.getMessage()); + } finally { + setInterval(style, "primaryTickInterval", origPrimary); + setInterval(style, "secondaryTickInterval", origSecondary); + } + } +} From b26278162cb74047a63835ca90f078676b2b4658 Mon Sep 17 00:00:00 2001 From: pjmarz Date: Tue, 9 Jun 2026 19:25:49 -0400 Subject: [PATCH 2/2] fix(antiban): overflow-safe interval bound, named test constant, accurate comment Addresses Copilot review feedback on #1795. Co-Authored-By: Claude Fable 5 --- .../client/plugins/microbot/util/antiban/Rs2Antiban.java | 5 +++-- .../plugins/microbot/util/antiban/enums/PlayStyle.java | 5 +++-- .../util/antiban/enums/PlayStyleRandomTickIntervalTest.java | 5 ++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/antiban/Rs2Antiban.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/antiban/Rs2Antiban.java index c5ff529749..a8268a483e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/antiban/Rs2Antiban.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/antiban/Rs2Antiban.java @@ -295,8 +295,9 @@ private static void performActionCooldown() { else TIMEOUT = playStyle.getPrimaryTickInterval(); - // Pause and set the active flag together, only after TIMEOUT is computed. If computing the - // interval ever throws, scripts must not be left paused with no countdown to clear them. + // The pause (universal antiban only) and the always-set active flag both happen only after + // TIMEOUT is computed: if computing the interval ever throws, scripts must not be left + // paused with no countdown to clear them. if (Rs2AntibanSettings.universalAntiban) Microbot.pauseAllScripts.compareAndSet(false, true); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/antiban/enums/PlayStyle.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/antiban/enums/PlayStyle.java index 3a365337ff..62c9e66825 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/antiban/enums/PlayStyle.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/antiban/enums/PlayStyle.java @@ -113,10 +113,11 @@ public static PlayStyle random() { public int getRandomTickInterval() { // primaryTickInterval and secondaryTickInterval drift independently in evolvePlayStyle() // and can cross (primary > secondary). nextInt(origin, bound) requires bound > origin, so - // order the bounds defensively to avoid IllegalArgumentException. + // order the bounds defensively to avoid IllegalArgumentException. The bound is computed in + // long so an inclusive upper bound of Integer.MAX_VALUE cannot overflow. int lo = Math.min(primaryTickInterval, secondaryTickInterval); int hi = Math.max(primaryTickInterval, secondaryTickInterval); - return ThreadLocalRandom.current().nextInt(lo, hi + 1); + return (int) ThreadLocalRandom.current().nextLong(lo, (long) hi + 1L); } public void evolvePlayStyle() { diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/antiban/enums/PlayStyleRandomTickIntervalTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/antiban/enums/PlayStyleRandomTickIntervalTest.java index 23cad1ee0c..c337fa747f 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/antiban/enums/PlayStyleRandomTickIntervalTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/antiban/enums/PlayStyleRandomTickIntervalTest.java @@ -32,7 +32,10 @@ private static int getInterval(PlayStyle style, String field) throws Exception { @Test public void getRandomTickIntervalDoesNotThrowWhenIntervalsCross() throws Exception { - PlayStyle style = PlayStyle.values()[0]; + // A specific constant (not values()[0]) so enum reordering cannot change what is tested. + // The intervals are restored in the finally block, so the shared enum singleton is left + // untouched for other tests; the unit-test suite runs single-threaded. + PlayStyle style = PlayStyle.MODERATE; int origPrimary = getInterval(style, "primaryTickInterval"); int origSecondary = getInterval(style, "secondaryTickInterval"); try {