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 @@ -51,7 +51,11 @@ protected void shutDown() {

@Subscribe
public void onAutoLoginSuppressionRequest(AutoLoginSuppressionRequest event) {
accountSelectorScript.suppressAutoLogin();
if (event.isRenewal()) {
accountSelectorScript.renewAutoLoginSuppression();
} else {
accountSelectorScript.suppressAutoLogin();
}
}

@Subscribe
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ void suppressAutoLogin() {
suppression.request();
}

void renewAutoLoginSuppression() {
suppression.renew();
}

void resumeAutoLogin() {
suppression.clear();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
final class AutoLoginSuppression {
private enum State { ALLOWED, AWAITING_LOGOUT, LOGGED_OUT }

static final long AWAIT_LOGOUT_MILLIS = 15_000L;
static final long AWAIT_LOGOUT_MILLIS = AutoLoginSuppressionRequest.PENDING_LOGOUT_MILLIS;
private final LongSupplier clock;
private State state = State.ALLOWED;
private long requestedAt;
Expand All @@ -29,6 +29,13 @@ synchronized void request() {
}
}

synchronized void renew() {
expirePendingRequest();
if (state == State.AWAITING_LOGOUT) {
requestedAt = clock.getAsLong();
}
}

synchronized void observe(GameState gameState) {
expirePendingRequest();
if (state == State.AWAITING_LOGOUT && gameState == GameState.LOGIN_SCREEN) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,29 @@
/**
* Scripts post this event before a terminal logout to prevent automatic re-login.
* AutoLogin resumes when another script starts, after a manual login, or on AutoLogin restart.
* A request that does not reach the login screen within {@link #PENDING_LOGOUT_MILLIS} expires.
* A renewal restarts that deadline only while the earlier request is still pending, so it never
* re-blocks AutoLogin after a restart or a manual login cleared it.
* Ordinary logouts must not post this event.
*/
public final class AutoLoginSuppressionRequest {
public static final long PENDING_LOGOUT_MILLIS = 15_000L;

private final boolean renewal;

public AutoLoginSuppressionRequest() {
this(false);
}

private AutoLoginSuppressionRequest(boolean renewal) {
this.renewal = renewal;
}

public static AutoLoginSuppressionRequest renewal() {
return new AutoLoginSuppressionRequest(true);
}

public boolean isRenewal() {
return renewal;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1975,8 +1975,8 @@ static TerminalLogoutState logoutWithoutAutoLoginStep(Runnable requestSuppressio
GameState state = Microbot.getClientThread().runOnClientThreadOptional(
() -> Microbot.getClient().getGameState()).orElse(GameState.UNKNOWN);
if (state != GameState.LOGGED_IN && state != GameState.LOGIN_SCREEN) return TerminalLogoutState.INPUT_BLOCKED;
requestSuppression.run();
if (state == GameState.LOGIN_SCREEN) return TerminalLogoutState.LOGGED_OUT;
requestSuppression.run();
if (Rs2Bank.isOpen()) {
return Rs2Widget.clickChildWidget(786434, 11)
? TerminalLogoutState.BANK_CLOSING : TerminalLogoutState.INPUT_BLOCKED;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,54 +1,119 @@
/* Copyright (c) 2026, DRO (droplugins). SPDX-License-Identifier: BSD-2-Clause */
package net.runelite.client.plugins.microbot.util.player;

import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.LongSupplier;
import java.util.function.Supplier;
import net.runelite.client.plugins.microbot.Microbot;
import net.runelite.client.plugins.microbot.accountselector.AutoLoginSuppressionRequest;

/** Keeps terminal logout separate from notification; the host pauses gameplay until completed. */
/**
* Keeps terminal logout separate from notification; the host pauses gameplay until completed.
* Safe to begin or reset from one thread while another thread ticks.
*/
public final class TerminalLogoutPrompt {
static final long RETRY_MILLIS = 600L;
static final long RENEW_SUPPRESSION_MILLIS = AutoLoginSuppressionRequest.PENDING_LOGOUT_MILLIS / 3;

private final LongSupplier clock;
private String message;
private long nextAttemptAt;
private long stop;
private boolean attempted;
private long lastAttemptAt;
private boolean suppressionRequested;
private long suppressionRequestedAt;

public TerminalLogoutPrompt() {
this(() -> TimeUnit.NANOSECONDS.toMillis(System.nanoTime()));
}

public void begin(String text) {
TerminalLogoutPrompt(LongSupplier clock) {
this.clock = clock;
}

public synchronized void begin(String text) {
if (message == null) {
message = text;
suppressionRequested = false;
startNewStop();
}
}

public boolean active() { return message != null; }
public String message() { return message; }
public void reset() { message = null; nextAttemptAt = 0L; suppressionRequested = false; }
public synchronized boolean active() { return message != null; }
public synchronized String message() { return message; }

public synchronized void reset() {
message = null;
startNewStop();
}

public boolean tick() {
long now = System.currentTimeMillis();
if (now < nextAttemptAt) return false;
nextAttemptAt = now + 600L;
return tick(Rs2Player::logoutWithoutAutoLoginStep, Microbot::showMessage,
() -> Microbot.getEventBus().post(new AutoLoginSuppressionRequest()));
return throttledTick(Rs2Player::logoutWithoutAutoLoginStep, Microbot::showMessage,
() -> Microbot.getEventBus().post(new AutoLoginSuppressionRequest()),
() -> Microbot.getEventBus().post(AutoLoginSuppressionRequest.renewal()));
}

public boolean tick(Supplier<Rs2Player.TerminalLogoutState> logout, Consumer<String> notify) {
return tick(ignored -> logout.get(), notify, () -> {});
return tick(ignored -> logout.get(), notify, () -> {}, () -> {});
}

boolean throttledTick(Function<Runnable, Rs2Player.TerminalLogoutState> logout, Consumer<String> notify,
Runnable requestSuppression, Runnable renewSuppression) {
synchronized (this) {
if (message == null) return false;
long now = clock.getAsLong();
if (attempted && now - lastAttemptAt < RETRY_MILLIS) return false;
attempted = true;
lastAttemptAt = now;
}
return tick(logout, notify, requestSuppression, renewSuppression);
}

boolean tick(Function<Runnable, Rs2Player.TerminalLogoutState> logout, Consumer<String> notify,
Runnable requestSuppression) {
if (message == null) return false;
Rs2Player.TerminalLogoutState state = logout.apply(() -> {
if (!suppressionRequested) {
requestSuppression.run();
suppressionRequested = true;
}
});
Runnable requestSuppression, Runnable renewSuppression) {
long tickStop;
synchronized (this) {
if (message == null) return false;
tickStop = stop;
}
Rs2Player.TerminalLogoutState state = logout.apply(
() -> suppress(tickStop, requestSuppression, renewSuppression));
if (state != Rs2Player.TerminalLogoutState.LOGGED_OUT) return false;
String completedMessage = message;
message = null;
String completedMessage;
synchronized (this) {
if (stop != tickStop || message == null) return false;
completedMessage = message;
message = null;
startNewStop();
}
notify.accept(completedMessage);
return true;
}

private void suppress(long tickStop, Runnable requestSuppression, Runnable renewSuppression) {
Runnable action;
synchronized (this) {
if (stop != tickStop) return;
if (!suppressionRequested) {
action = requestSuppression;
} else if (clock.getAsLong() - suppressionRequestedAt >= RENEW_SUPPRESSION_MILLIS) {
action = renewSuppression;
} else {
return;
}
}
action.run();
synchronized (this) {
if (stop != tickStop) return;
suppressionRequested = true;
suppressionRequestedAt = clock.getAsLong();
}
}

private void startNewStop() {
stop++;
attempted = false;
suppressionRequested = false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ public class AutoLoginPluginSupplyStopTest {
plugin.accountSelectorScript = mock(AutoLoginScript.class);
plugin.onAutoLoginSuppressionRequest(new AutoLoginSuppressionRequest());
verify(plugin.accountSelectorScript).suppressAutoLogin();
plugin.onAutoLoginSuppressionRequest(AutoLoginSuppressionRequest.renewal());
verify(plugin.accountSelectorScript).renewAutoLoginSuppression();
verify(plugin.accountSelectorScript).suppressAutoLogin();
GameStateChanged event = new GameStateChanged();
event.setGameState(GameState.LOGIN_SCREEN);
plugin.onGameStateChanged(event);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,44 @@ public class AutoLoginSuppressionTest {
clock.set(AutoLoginSuppression.AWAIT_LOGOUT_MILLIS * 2);
assertFalse(suppression.isSuppressed());
}

@Test public void renewalRestartsThePendingDeadline() {
AtomicLong clock = new AtomicLong();
AutoLoginSuppression suppression = new AutoLoginSuppression(clock::get);
suppression.request();
clock.set(AutoLoginSuppression.AWAIT_LOGOUT_MILLIS - 1);
suppression.renew();
clock.set(AutoLoginSuppression.AWAIT_LOGOUT_MILLIS * 2 - 2);
assertTrue(suppression.isSuppressed());
suppression.observe(GameState.LOGIN_SCREEN);
clock.set(AutoLoginSuppression.AWAIT_LOGOUT_MILLIS * 10);
assertTrue(suppression.isSuppressed());
}

@Test public void renewalNeverReblocksAClearedOrExpiredRequest() {
AtomicLong clock = new AtomicLong();
AutoLoginSuppression suppression = new AutoLoginSuppression(clock::get);
suppression.renew();
assertFalse(suppression.isSuppressed());
suppression.request();
suppression.clear();
suppression.renew();
assertFalse(suppression.isSuppressed());
suppression.request();
clock.set(AutoLoginSuppression.AWAIT_LOGOUT_MILLIS);
suppression.renew();
assertFalse(suppression.isSuppressed());
}

@Test public void renewalAfterLogoutKeepsWaitingForManualLogin() {
AtomicLong clock = new AtomicLong();
AutoLoginSuppression suppression = new AutoLoginSuppression(clock::get);
suppression.request();
suppression.observe(GameState.LOGIN_SCREEN);
suppression.renew();
clock.set(AutoLoginSuppression.AWAIT_LOGOUT_MILLIS * 10);
assertTrue(suppression.isSuppressed());
suppression.observe(GameState.LOGGED_IN);
assertFalse(suppression.isSuppressed());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import net.runelite.client.plugins.microbot.accountselector.AutoLoginSuppressionRequest;
import org.junit.Test;
import static org.junit.Assert.*;

Expand Down Expand Up @@ -41,20 +43,99 @@ public class TerminalLogoutPromptTest {
assertFalse(prompt.tick(request -> {
request.run();
return Rs2Player.TerminalLogoutState.BANK_CLOSING;
}, messages::add, () -> requests[0]++));
}, messages::add, () -> requests[0]++, () -> { throw new AssertionError("renewed without delay"); }));
}
assertEquals(1, requests[0]);
assertTrue(prompt.tick(request -> {
request.run();
return Rs2Player.TerminalLogoutState.LOGGED_OUT;
}, messages::add, () -> requests[0]++));
}, messages::add, () -> requests[0]++, () -> { throw new AssertionError("renewed without delay"); }));
assertEquals(1, requests[0]);
prompt.begin("Second stop");
assertTrue(prompt.tick(request -> {
request.run();
return Rs2Player.TerminalLogoutState.LOGGED_OUT;
}, messages::add, () -> requests[0]++));
}, messages::add, () -> requests[0]++, () -> { throw new AssertionError("renewed without delay"); }));
assertEquals(2, requests[0]);
assertEquals(List.of("First stop", "Second stop"), messages);
}

@Test public void delayedLogoutKeepsRenewingSuppressionUntilTheLoginScreen() {
AtomicLong clock = new AtomicLong();
TerminalLogoutPrompt prompt = new TerminalLogoutPrompt(clock::get);
int[] requests = {0};
long[] lastSuppressionAt = {Long.MIN_VALUE};
List<String> messages = new ArrayList<>();
prompt.begin("Ran out of supplies");
long logoutAt = AutoLoginSuppressionRequest.PENDING_LOGOUT_MILLIS * 3;
for (; clock.get() < logoutAt; clock.addAndGet(TerminalLogoutPrompt.RETRY_MILLIS)) {
assertFalse(prompt.throttledTick(request -> {
request.run();
return Rs2Player.TerminalLogoutState.LOGOUT_REQUESTED;
}, messages::add, () -> {
requests[0]++;
lastSuppressionAt[0] = clock.get();
}, () -> lastSuppressionAt[0] = clock.get()));
assertTrue(clock.get() - lastSuppressionAt[0] < AutoLoginSuppressionRequest.PENDING_LOGOUT_MILLIS);
}
assertEquals(1, requests[0]);
assertTrue(prompt.throttledTick(request -> Rs2Player.TerminalLogoutState.LOGGED_OUT, messages::add,
() -> requests[0]++, () -> { throw new AssertionError("renewed after logout"); }));
assertTrue(clock.get() - lastSuppressionAt[0] < AutoLoginSuppressionRequest.PENDING_LOGOUT_MILLIS);
assertEquals(List.of("Ran out of supplies"), messages);
}

@Test public void throttleUsesTheInjectedClockEvenWhenItStartsNegative() {
AtomicLong clock = new AtomicLong(-1_000_000L);
TerminalLogoutPrompt prompt = new TerminalLogoutPrompt(clock::get);
int[] attempts = {0};
prompt.begin("Stop");
Runnable tick = () -> prompt.throttledTick(request -> {
attempts[0]++;
return Rs2Player.TerminalLogoutState.LOGOUT_REQUESTED;
}, text -> {}, () -> {}, () -> {});
tick.run();
assertEquals(1, attempts[0]);
clock.addAndGet(TerminalLogoutPrompt.RETRY_MILLIS - 1);
tick.run();
assertEquals(1, attempts[0]);
clock.incrementAndGet();
tick.run();
assertEquals(2, attempts[0]);
prompt.reset();
prompt.begin("Next stop");
tick.run();
assertEquals(3, attempts[0]);
}

@Test public void restartFromAnotherThreadDuringLogoutIsNotCompletedOrSuppressedByTheOldTick() {
TerminalLogoutPrompt prompt = new TerminalLogoutPrompt();
int[] requests = {0};
List<String> messages = new ArrayList<>();
prompt.begin("Old stop");
assertFalse(prompt.tick(request -> {
Thread restart = new Thread(() -> {
prompt.reset();
prompt.begin("New stop");
});
restart.start();
try {
restart.join(5_000L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
assertFalse(restart.isAlive());
request.run();
return Rs2Player.TerminalLogoutState.LOGGED_OUT;
}, messages::add, () -> requests[0]++, () -> requests[0]++));
assertEquals(0, requests[0]);
assertTrue(messages.isEmpty());
assertEquals("New stop", prompt.message());
assertTrue(prompt.tick(request -> {
request.run();
return Rs2Player.TerminalLogoutState.LOGGED_OUT;
}, messages::add, () -> requests[0]++, () -> { throw new AssertionError("renewed a new stop"); }));
assertEquals(1, requests[0]);
assertEquals(List.of("New stop"), messages);
}
}
Loading
Loading