Repository navigation
Let scripts confirm terminal supply logout before stopping #1944
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
chsami
merged 5 commits into
chsami:development
from
droplugins:dro-terminal-logout-owner
Oct 13, 2026
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
7abcc56
fix(supplies): let owning scripts confirm terminal logout before stop…
droplugins 511b7d4
fix(logout): use ClientThread predicate for terminal input guard
droplugins da0a623
fix(logout): confirm login screen and arm suppression once per stop
droplugins 7cdf412
docs(logout): clarify confirmed state and single suppression request
droplugins 6209ab9
fix(supplies): stop equipment load once the missing-supply handler ta…
chsami File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
54 changes: 54 additions & 0 deletions
54
.../src/main/java/net/runelite/client/plugins/microbot/util/player/TerminalLogoutPrompt.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| /* Copyright (c) 2026, DRO (droplugins). SPDX-License-Identifier: BSD-2-Clause */ | ||
| package net.runelite.client.plugins.microbot.util.player; | ||
|
|
||
| import java.util.function.Consumer; | ||
| import java.util.function.Function; | ||
| 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. */ | ||
| public final class TerminalLogoutPrompt { | ||
| private String message; | ||
| private long nextAttemptAt; | ||
| private boolean suppressionRequested; | ||
|
|
||
| public void begin(String text) { | ||
| if (message == null) { | ||
| message = text; | ||
| suppressionRequested = false; | ||
| } | ||
| } | ||
|
|
||
| public boolean active() { return message != null; } | ||
| public String message() { return message; } | ||
| public void reset() { message = null; nextAttemptAt = 0L; suppressionRequested = false; } | ||
|
|
||
| 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())); | ||
| } | ||
|
|
||
| public boolean tick(Supplier<Rs2Player.TerminalLogoutState> logout, Consumer<String> notify) { | ||
| return tick(ignored -> logout.get(), notify, () -> {}); | ||
| } | ||
|
|
||
| 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; | ||
| } | ||
| }); | ||
| if (state != Rs2Player.TerminalLogoutState.LOGGED_OUT) return false; | ||
| String completedMessage = message; | ||
| message = null; | ||
| notify.accept(completedMessage); | ||
| return true; | ||
| } | ||
| } |
75 changes: 75 additions & 0 deletions
75
.../test/java/net/runelite/client/plugins/microbot/util/InventorySetupTerminalOwnerTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| /* Copyright (c) 2026, DRO (droplugins). SPDX-License-Identifier: BSD-2-Clause */ | ||
| package net.runelite.client.plugins.microbot.util; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Optional; | ||
| import java.util.concurrent.ScheduledFuture; | ||
| import java.util.function.Predicate; | ||
| import java.util.stream.Stream; | ||
| import net.runelite.api.Client; | ||
| import net.runelite.client.callback.ClientThread; | ||
| import net.runelite.client.plugins.microbot.Microbot; | ||
| import net.runelite.client.plugins.microbot.inventorysetups.InventorySetup; | ||
| import net.runelite.client.plugins.microbot.inventorysetups.InventorySetupsItem; | ||
| import net.runelite.client.plugins.microbot.inventorysetups.InventorySetupsStackCompareID; | ||
| import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; | ||
| import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; | ||
| import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; | ||
| import net.runelite.client.plugins.microbot.util.player.Rs2Player; | ||
| import org.junit.Test; | ||
| import static org.junit.Assert.*; | ||
| import static org.mockito.Mockito.*; | ||
|
|
||
| public class InventorySetupTerminalOwnerTest { | ||
| @Test public void missingSupplyRetainsHostSchedulerForObservedLogout() throws Exception { | ||
| ScheduledFuture<?> scheduler = mock(ScheduledFuture.class); | ||
| Rs2InventorySetup loader = new Rs2InventorySetup(mock(InventorySetup.class), scheduler); | ||
| List<String> missing = new ArrayList<>(); | ||
| loader.withMissingSupplyPolicy(Rs2InventorySetup.MissingSupplyPolicy.LOGOUT_AND_STOP) | ||
| .withMissingSupplyHandler(missing::add); | ||
| var method = Rs2InventorySetup.class.getDeclaredMethod("stopForMissingRequiredSupply", String.class); | ||
| method.setAccessible(true); | ||
| method.invoke(loader, "Super restore(4)"); | ||
| assertEquals(List.of("Super restore(4)"), missing); | ||
| verifyNoInteractions(scheduler); | ||
| } | ||
|
|
||
| @Test public void handlerWithDefaultPolicyStopsEquipmentLoadAtFirstMissingGear() { | ||
| boolean pausedBefore = Microbot.pauseAllScripts.getAndSet(false); | ||
| InventorySetup setup = mock(InventorySetup.class); | ||
| InventorySetupsItem missingGear = new InventorySetupsItem(100, "Missing helm", 1, false, | ||
| InventorySetupsStackCompareID.None, false, -1); | ||
| InventorySetupsItem bankedGear = new InventorySetupsItem(200, "Banked body", 1, false, | ||
| InventorySetupsStackCompareID.None, false, -1); | ||
| when(setup.getInventory()).thenReturn(List.of()); | ||
| when(setup.getEquipment()).thenReturn(List.of(missingGear, bankedGear)); | ||
| ClientThread thread = mock(ClientThread.class); | ||
| when(thread.runOnClientThreadOptional(any())).thenReturn(Optional.empty()); | ||
| try (var microbot = mockStatic(Microbot.class); | ||
| var bank = mockStatic(Rs2Bank.class); | ||
| var inventory = mockStatic(Rs2Inventory.class); | ||
| var equipment = mockStatic(Rs2Equipment.class); | ||
| var player = mockStatic(Rs2Player.class)) { | ||
| microbot.when(Microbot::getClient).thenReturn(mock(Client.class)); | ||
| microbot.when(Microbot::getClientThread).thenReturn(thread); | ||
| bank.when(Rs2Bank::isOpen).thenReturn(true); | ||
| bank.when(() -> Rs2Bank.verifyBankMirrorAfterOpen(true, 0)).thenReturn(true); | ||
| bank.when(() -> Rs2Bank.hasItem(200)).thenReturn(true); | ||
| inventory.when(Rs2Inventory::items).thenAnswer(call -> Stream.empty()); | ||
| inventory.when(() -> Rs2Inventory.items(any(Predicate.class))).thenAnswer(call -> Stream.empty()); | ||
| equipment.when(Rs2Equipment::all).thenAnswer(call -> Stream.empty()); | ||
| ScheduledFuture<?> scheduler = mock(ScheduledFuture.class); | ||
| List<String> missing = new ArrayList<>(); | ||
| Rs2InventorySetup loader = new Rs2InventorySetup(setup, scheduler).withMissingSupplyHandler(missing::add); | ||
| assertFalse(loader.loadEquipment(false)); | ||
| assertEquals(List.of("Missing helm"), missing); | ||
| bank.verify(() -> Rs2Bank.withdrawAndEquip(anyInt()), never()); | ||
| player.verify(Rs2Player::logoutWithoutAutoLogin, never()); | ||
| verify(scheduler, never()).cancel(anyBoolean()); | ||
| assertFalse(Microbot.pauseAllScripts.get()); | ||
| } finally { | ||
| Microbot.pauseAllScripts.set(pausedBefore); | ||
| } | ||
| } | ||
| } |
60 changes: 60 additions & 0 deletions
60
.../test/java/net/runelite/client/plugins/microbot/util/player/TerminalLogoutPromptTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| /* Copyright (c) 2026, DRO (droplugins). SPDX-License-Identifier: BSD-2-Clause */ | ||
| package net.runelite.client.plugins.microbot.util.player; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import org.junit.Test; | ||
| import static org.junit.Assert.*; | ||
|
|
||
| public class TerminalLogoutPromptTest { | ||
| @Test public void waitsThroughBankClosureAndBlockedLogoutBeforeOneNotification() { | ||
| TerminalLogoutPrompt prompt = new TerminalLogoutPrompt(); | ||
| List<String> messages = new ArrayList<>(); | ||
| prompt.begin("Ran out of supplies: box traps"); | ||
| for (Rs2Player.TerminalLogoutState state : new Rs2Player.TerminalLogoutState[]{ | ||
| Rs2Player.TerminalLogoutState.BANK_CLOSING, Rs2Player.TerminalLogoutState.INPUT_BLOCKED, | ||
| Rs2Player.TerminalLogoutState.LOGOUT_REQUESTED}) { | ||
| assertFalse(prompt.tick(() -> state, messages::add)); | ||
| assertTrue(prompt.active()); | ||
| assertTrue(messages.isEmpty()); | ||
| } | ||
| assertTrue(prompt.tick(() -> Rs2Player.TerminalLogoutState.LOGGED_OUT, messages::add)); | ||
| assertFalse(prompt.active()); | ||
| assertEquals(List.of("Ran out of supplies: box traps"), messages); | ||
| assertFalse(prompt.tick(() -> { throw new AssertionError("completed stop retried"); }, messages::add)); | ||
| } | ||
|
|
||
| @Test public void explicitRestartCancelsOldLogoutAndMessage() { | ||
| TerminalLogoutPrompt prompt = new TerminalLogoutPrompt(); | ||
| prompt.begin("Old stop"); | ||
| prompt.reset(); | ||
| assertFalse(prompt.tick(() -> { throw new AssertionError("old stop continued"); }, | ||
| text -> { throw new AssertionError("old popup appeared"); })); | ||
| } | ||
|
|
||
| @Test public void suppressionIsRequestedOnceAcrossRetriesAndAgainForANewStop() { | ||
| TerminalLogoutPrompt prompt = new TerminalLogoutPrompt(); | ||
| int[] requests = {0}; | ||
| List<String> messages = new ArrayList<>(); | ||
| prompt.begin("First stop"); | ||
| for (int i = 0; i < 3; i++) { | ||
| assertFalse(prompt.tick(request -> { | ||
| request.run(); | ||
| return Rs2Player.TerminalLogoutState.BANK_CLOSING; | ||
| }, messages::add, () -> requests[0]++)); | ||
| } | ||
| assertEquals(1, requests[0]); | ||
| assertTrue(prompt.tick(request -> { | ||
| request.run(); | ||
| return Rs2Player.TerminalLogoutState.LOGGED_OUT; | ||
| }, messages::add, () -> requests[0]++)); | ||
| assertEquals(1, requests[0]); | ||
| prompt.begin("Second stop"); | ||
| assertTrue(prompt.tick(request -> { | ||
| request.run(); | ||
| return Rs2Player.TerminalLogoutState.LOGGED_OUT; | ||
| }, messages::add, () -> requests[0]++)); | ||
| assertEquals(2, requests[0]); | ||
| assertEquals(List.of("First stop", "Second stop"), messages); | ||
| } | ||
| } |
51 changes: 51 additions & 0 deletions
51
...rc/test/java/net/runelite/client/plugins/microbot/util/player/TerminalLogoutStepTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| /* Copyright (c) 2026, DRO (droplugins). SPDX-License-Identifier: BSD-2-Clause */ | ||
| package net.runelite.client.plugins.microbot.util.player; | ||
|
|
||
| import java.lang.reflect.Field; | ||
| import java.util.Optional; | ||
| import java.util.concurrent.Callable; | ||
| import net.runelite.api.Client; | ||
| import net.runelite.api.GameState; | ||
| import net.runelite.client.callback.ClientThread; | ||
| import net.runelite.client.plugins.microbot.Microbot; | ||
| import org.junit.Test; | ||
| import static org.junit.Assert.*; | ||
| import static org.mockito.Mockito.*; | ||
|
|
||
| public class TerminalLogoutStepTest { | ||
| private void check(GameState state, boolean onClientThread, Rs2Player.TerminalLogoutState expected, | ||
| int expectedRequests) throws Exception { | ||
| Client client=mock(Client.class); | ||
| ClientThread thread=mock(ClientThread.class); | ||
| when(client.getGameState()).thenReturn(state); | ||
| when(thread.isClientThread()).thenReturn(onClientThread); | ||
| when(thread.runOnClientThreadOptional(any(Callable.class))).thenAnswer(invocation -> | ||
| Optional.ofNullable(((Callable<?>)invocation.getArgument(0)).call())); | ||
| Field cf=Microbot.class.getDeclaredField("client"),tf=Microbot.class.getDeclaredField("clientThread"); | ||
| cf.setAccessible(true);tf.setAccessible(true); | ||
| Object oldClient=cf.get(null),oldThread=tf.get(null); | ||
| try { | ||
| cf.set(null,client);tf.set(null,thread); | ||
| int[] requests={0}; | ||
| assertEquals(expected,Rs2Player.logoutWithoutAutoLoginStep(() -> requests[0]++)); | ||
| assertEquals(expectedRequests,requests[0]); | ||
| } finally { | ||
| cf.set(null,oldClient);tf.set(null,oldThread); | ||
| } | ||
| } | ||
|
|
||
| @Test public void worldAndLoginTransitionsAreNotConfirmedLogout() throws Exception { | ||
| for(GameState state:new GameState[]{GameState.HOPPING,GameState.LOADING,GameState.CONNECTION_LOST, | ||
| GameState.LOGGING_IN,GameState.LOGIN_SCREEN_AUTHENTICATOR,GameState.UNKNOWN}) { | ||
| check(state,false,Rs2Player.TerminalLogoutState.INPUT_BLOCKED,0); | ||
| } | ||
| } | ||
|
|
||
| @Test public void loginScreenConfirmsLogoutWithSuppression() throws Exception { | ||
| check(GameState.LOGIN_SCREEN,false,Rs2Player.TerminalLogoutState.LOGGED_OUT,1); | ||
| } | ||
|
|
||
| @Test public void clientThreadCannotDispatchOrRequestSuppression() throws Exception { | ||
| check(GameState.LOGGED_IN,true,Rs2Player.TerminalLogoutState.INPUT_BLOCKED,0); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.