Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// 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;
Expand All @@ -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");
Expand All @@ -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.
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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();
Expand All @@ -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());
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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> 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> microbot;
private MockedStatic<LoginManager> 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;
}
}
}
Loading