diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/accountselector/AutoLoginPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/accountselector/AutoLoginPlugin.java index ec0670f94d..830b0c83d7 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/accountselector/AutoLoginPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/accountselector/AutoLoginPlugin.java @@ -23,7 +23,7 @@ @PluginDescriptor( name = PluginDescriptor.Mocrosoft + "AutoLogin", description = "Microbot autologin plugin", - version = "1.0.5", + version = "1.0.6", tags = {"account", "microbot", "login"}, enabledByDefault = false ) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/accountselector/AutoLoginScript.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/accountselector/AutoLoginScript.java index 4fde3c6c6f..cb4ef480ef 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/accountselector/AutoLoginScript.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/accountselector/AutoLoginScript.java @@ -15,6 +15,7 @@ import net.runelite.client.plugins.microbot.util.security.LoginManager; import net.runelite.client.plugins.microbot.util.world.Rs2WorldUtil; +import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.util.concurrent.ThreadLocalRandom; @@ -73,6 +74,19 @@ private enum LoginState { private long lastExtendedSleepLoggedMinute = -1; private boolean lockoutLogged = false; private final AutoLoginSuppression suppression = new AutoLoginSuppression(); + private Clock clock = Clock.systemUTC(); + + void setClock(Clock clock) { + this.clock = clock; + } + + String getLoginStateName() { + return loginState.name(); + } + + int getRetryCount() { + return retryCount; + } void suppressAutoLogin() { suppression.request(); @@ -116,7 +130,7 @@ public boolean run(AutoLoginConfig autoLoginConfig) { /** * Main state machine for auto login processing. */ - private void processAutoLoginStateMachine(AutoLoginConfig config) { + void processAutoLoginStateMachine(AutoLoginConfig config) { if (suppression.isSuppressed()) { resetLoginState(); return; @@ -175,25 +189,29 @@ private void handleAttemptingLoginState(AutoLoginConfig config) { } if (waitForLoginLockout()) { - lastLoginAttemptTime = Instant.now(); + lastLoginAttemptTime = Instant.now(clock); loginWatchdogStartTime = null; return; } // check login watchdog timeout if enabled if (config.enableLoginWatchdog() && loginWatchdogStartTime != null) { - long watchdogTime = Duration.between(loginWatchdogStartTime, Instant.now()).toMinutes(); + long watchdogTime = Duration.between(loginWatchdogStartTime, Instant.now(clock)).toMinutes(); if (watchdogTime >= config.loginWatchdogTimeout()) { log.warn("Login watchdog timeout reached after {} minutes, entering extended sleep", watchdogTime); - extendedSleepStartTime = Instant.now(); + extendedSleepStartTime = Instant.now(clock); transitionToState(LoginState.LOGIN_EXTENDED_SLEEP); return; } } + if (LoginManager.getGameState() != GameState.LOGIN_SCREEN) { + return; + } + // check if enough time has passed for retry — uses exponential backoff if (lastLoginAttemptTime != null) { - long timeSinceLastAttempt = Duration.between(lastLoginAttemptTime, Instant.now()).toSeconds(); + long timeSinceLastAttempt = Duration.between(lastLoginAttemptTime, Instant.now(clock)).toSeconds(); int backoff = computeRetryBackoffSeconds(retryCount, config.loginRetryDelay()); if (timeSinceLastAttempt < backoff) { return; @@ -204,7 +222,7 @@ private void handleAttemptingLoginState(AutoLoginConfig config) { if (retryCount >= config.maxLoginRetries()) { if (config.enableLoginWatchdog()) { log.warn("Max login retries reached, entering extended sleep"); - extendedSleepStartTime = Instant.now(); + extendedSleepStartTime = Instant.now(clock); transitionToState(LoginState.LOGIN_EXTENDED_SLEEP); } else { log.warn("Max login retries reached, resetting retry count"); @@ -213,8 +231,7 @@ private void handleAttemptingLoginState(AutoLoginConfig config) { return; } - // check if still on login screen - if (Microbot.getClient() != null && Microbot.getClient().getGameState() == GameState.LOGIN_SCREEN) { + if (Microbot.getClient() != null) { log.info("Retrying login attempt {} of {}", retryCount + 1, config.maxLoginRetries()); int currentLoginIndex = Microbot.getClient().getLoginIndex(); if (Microbot.getClient().getLoginIndex() == 3 || Microbot.getClient().getLoginIndex() == 24) { // you were disconnected from the server. @@ -244,12 +261,6 @@ private void handleAttemptingLoginState(AutoLoginConfig config) { // we have to find out other indexes that mean we cannot login initiateLogin(config); - } else { - - - // not on login screen anymore, return to waiting - resetLoginState(); - transitionToState(LoginState.WAITING_FOR_LOGIN_SCREEN); } } @@ -279,13 +290,13 @@ private void handleLoginExtendedSleepState(AutoLoginConfig config) { } if (extendedSleepStartTime == null) { - extendedSleepStartTime = Instant.now(); + extendedSleepStartTime = Instant.now(clock); log.info("Extended sleep started for {} minutes", config.extendedSleepDuration()); return; } // check if extended sleep period is complete - long sleepTime = Duration.between(extendedSleepStartTime, Instant.now()).toMinutes(); + long sleepTime = Duration.between(extendedSleepStartTime, Instant.now(clock)).toMinutes(); if (sleepTime >= config.extendedSleepDuration()) { log.info("Extended sleep period completed after {} minutes, resuming login attempts", sleepTime); resetLoginState(); @@ -308,7 +319,7 @@ private void initiateLogin(AutoLoginConfig config) { if (suppression.isSuppressed()) return; // start login watchdog if enabled and not already started if (config.enableLoginWatchdog() && loginWatchdogStartTime == null) { - loginWatchdogStartTime = Instant.now(); + loginWatchdogStartTime = Instant.now(clock); log.info("Login watchdog started for {} minutes", config.loginWatchdogTimeout()); } @@ -390,7 +401,7 @@ private void initiateLogin(AutoLoginConfig config) { // perform login attempt and track retry state retryCount++; - lastLoginAttemptTime = Instant.now(); + lastLoginAttemptTime = Instant.now(clock); boolean loginInitiated; if (targetWorld != -1) { @@ -410,7 +421,7 @@ private void initiateLogin(AutoLoginConfig config) { } catch (Exception ex) { log.error("Error during intelligent login", ex); retryCount++; - lastLoginAttemptTime = Instant.now(); + lastLoginAttemptTime = Instant.now(clock); } } diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/accountselector/AutoLoginRetryStateTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/accountselector/AutoLoginRetryStateTest.java new file mode 100644 index 0000000000..b653706eda --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/accountselector/AutoLoginRetryStateTest.java @@ -0,0 +1,190 @@ +package net.runelite.client.plugins.microbot.accountselector; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.security.LoginLockout; +import net.runelite.client.plugins.microbot.util.security.LoginManager; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.*; + +public class AutoLoginRetryStateTest { + private static final int WORLD = 383; + + private final AtomicReference gameState = new AtomicReference<>(GameState.LOGIN_SCREEN); + private final AtomicInteger loginIndex = new AtomicInteger(2); + private final AtomicInteger loginCalls = new AtomicInteger(); + private final MutableClock clock = new MutableClock(); + private Client client; + private AutoLoginConfig config; + private AutoLoginScript script; + private MockedStatic microbot; + private MockedStatic loginManager; + + @Before public void setUp() { + LoginLockout.getInstance().recordSuccessfulLogin(); + client = mock(Client.class); + when(client.getGameState()).thenAnswer(call -> gameState.get()); + when(client.getLoginIndex()).thenAnswer(call -> loginIndex.get()); + + config = mock(AutoLoginConfig.class); + when(config.usePreferredWorld()).thenReturn(true); + when(config.world()).thenReturn(WORLD); + when(config.loginRetryDelay()).thenReturn(5); + when(config.maxLoginRetries()).thenReturn(5); + when(config.enableLoginWatchdog()).thenReturn(false); + + microbot = mockStatic(Microbot.class); + microbot.when(Microbot::getClient).thenReturn(client); + microbot.when(Microbot::isLoggedIn).thenAnswer(call -> gameState.get() == GameState.LOGGED_IN); + + loginManager = mockStatic(LoginManager.class, CALLS_REAL_METHODS); + loginManager.when(() -> LoginManager.login(anyInt())).thenAnswer(call -> { + loginCalls.incrementAndGet(); + gameState.set(GameState.LOGGING_IN); + return true; + }); + + script = new AutoLoginScript(); + script.setClock(clock); + } + + @After public void tearDown() { + loginManager.close(); + microbot.close(); + LoginLockout.getInstance().recordSuccessfulLogin(); + } + + @Test public void failedLoginWaitsForBackoffBeforeRetrying() { + tick(); + assertEquals(1, loginCalls()); + assertEquals("ATTEMPTING_LOGIN", script.getLoginStateName()); + + for (int i = 0; i < 5; i++) { + advance(Duration.ofMillis(600)); + tick(); + } + assertEquals("ATTEMPTING_LOGIN", script.getLoginStateName()); + assertEquals(1, script.getRetryCount()); + + gameState.set(GameState.LOGIN_SCREEN); + tick(); + advance(Duration.ofSeconds(1)); + tick(); + assertEquals(1, loginCalls()); + assertEquals(1, script.getRetryCount()); + + advance(Duration.ofSeconds(15)); + tick(); + assertEquals(2, loginCalls()); + assertEquals(2, script.getRetryCount()); + + gameState.set(GameState.LOGIN_SCREEN); + advance(Duration.ofSeconds(9)); + tick(); + assertEquals(2, loginCalls()); + + advance(Duration.ofSeconds(20)); + tick(); + assertEquals(3, loginCalls()); + } + + @Test public void maxRetriesStillEntersExtendedSleepWithWatchdog() { + when(config.enableLoginWatchdog()).thenReturn(true); + when(config.loginWatchdogTimeout()).thenReturn(60); + when(config.maxLoginRetries()).thenReturn(2); + + tick(); + gameState.set(GameState.LOGIN_SCREEN); + advance(Duration.ofSeconds(30)); + tick(); + assertEquals(2, loginCalls()); + + gameState.set(GameState.LOGIN_SCREEN); + advance(Duration.ofMinutes(5)); + tick(); + assertEquals(2, loginCalls()); + assertEquals("LOGIN_EXTENDED_SLEEP", script.getLoginStateName()); + } + + @Test public void loggingInThenLoggedInResetsRetryState() { + tick(); + assertEquals(1, script.getRetryCount()); + + gameState.set(GameState.LOADING); + advance(Duration.ofMillis(600)); + tick(); + assertEquals("ATTEMPTING_LOGIN", script.getLoginStateName()); + assertEquals(1, script.getRetryCount()); + + gameState.set(GameState.LOGGED_IN); + advance(Duration.ofMillis(600)); + tick(); + assertEquals("WAITING_FOR_LOGIN_SCREEN", script.getLoginStateName()); + assertEquals(0, script.getRetryCount()); + + gameState.set(GameState.LOGIN_SCREEN); + advance(Duration.ofMillis(600)); + tick(); + assertEquals(2, loginCalls()); + assertEquals(1, script.getRetryCount()); + } + + @Test public void lockoutScreenUsesCooldownInsteadOfBackoff() { + tick(); + gameState.set(GameState.LOGIN_SCREEN); + loginIndex.set(LoginLockout.LOGIN_INDEX_TOO_MANY_ATTEMPTS); + tick(); + assertTrue(LoginLockout.getInstance().isActive()); + + loginIndex.set(2); + for (int i = 0; i < 5; i++) { + advance(Duration.ofMinutes(1)); + tick(); + } + + assertEquals(1, loginCalls()); + assertTrue(LoginLockout.getInstance().isActive()); + assertEquals("ATTEMPTING_LOGIN", script.getLoginStateName()); + } + + private void tick() { + script.processAutoLoginStateMachine(config); + } + + private void advance(Duration duration) { + clock.now = clock.now.plus(duration); + } + + private int loginCalls() { + return loginCalls.get(); + } + + private static final class MutableClock extends Clock { + private Instant now = Instant.parse("2026-01-01T00:00:00Z"); + + @Override public ZoneId getZone() { + return ZoneOffset.UTC; + } + + @Override public Clock withZone(ZoneId zone) { + return this; + } + + @Override public Instant instant() { + return now; + } + } +}