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..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 @@ -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,12 @@ private static void performActionCooldown() { else TIMEOUT = playStyle.getPrimaryTickInterval(); + // 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); + 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..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 @@ -111,7 +111,13 @@ 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. 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 (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 new file mode 100644 index 0000000000..c337fa747f --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/antiban/enums/PlayStyleRandomTickIntervalTest.java @@ -0,0 +1,61 @@ +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 { + // 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 { + // 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); + } + } +}