From c4907e656e7feaa841f59f10c0ac910f038f8ce3 Mon Sep 17 00:00:00 2001 From: chsami <23292806+chsami@users.noreply.github.com> Date: Mon, 19 Oct 2026 01:57:11 +0200 Subject: [PATCH] fix(ClueSolverPlugin): resolve cipher clue location on client thread (1.0.8) CipherClueTask.walkToLocation runs on the script executor and called CipherClue.getLocation directly. For the Eluned cipher the location provider calls Quest.getState, which asserts the client thread and threw IllegalStateException, failing the task. Resolve the location through runOnClientThreadOptional and keep the null check for a clean failure. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV --- .../microbot/cluesolver/ClueSolverPlugin.java | 2 +- .../cluesolver/cluetask/CipherClueTask.java | 4 +- .../cluesolver/CipherClueTaskTest.java | 135 ++++++++++++++++++ 3 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/cluesolver/CipherClueTaskTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/cluesolver/ClueSolverPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/cluesolver/ClueSolverPlugin.java index f84df3f516..69c151a16b 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/cluesolver/ClueSolverPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/cluesolver/ClueSolverPlugin.java @@ -29,7 +29,7 @@ @PluginDependency(ClueScrollPlugin.class) public class ClueSolverPlugin extends Plugin { - final static String version = "1.0.7"; + final static String version = "1.0.8"; @Inject private ClueSolverScript clueSolverScript; diff --git a/src/main/java/net/runelite/client/plugins/microbot/cluesolver/cluetask/CipherClueTask.java b/src/main/java/net/runelite/client/plugins/microbot/cluesolver/cluetask/CipherClueTask.java index b3403e3968..8036afc92c 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/cluesolver/cluetask/CipherClueTask.java +++ b/src/main/java/net/runelite/client/plugins/microbot/cluesolver/cluetask/CipherClueTask.java @@ -52,7 +52,9 @@ protected boolean executeTask() { } private void walkToLocation() { - WorldPoint location = clue.getLocation(clueScrollPlugin); + WorldPoint location = Microbot.getClientThread() + .runOnClientThreadOptional(() -> clue.getLocation(clueScrollPlugin)) + .orElse(null); if (location == null) { log.error("Clue location is null."); completeTask(false); diff --git a/src/test/java/net/runelite/client/plugins/microbot/cluesolver/CipherClueTaskTest.java b/src/test/java/net/runelite/client/plugins/microbot/cluesolver/CipherClueTaskTest.java new file mode 100644 index 0000000000..00201afb60 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/cluesolver/CipherClueTaskTest.java @@ -0,0 +1,135 @@ +package net.runelite.client.plugins.microbot.cluesolver; + +import net.runelite.api.Client; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.eventbus.EventBus; +import net.runelite.client.plugins.cluescrolls.ClueScrollPlugin; +import net.runelite.client.plugins.cluescrolls.clues.CipherClue; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.cluesolver.cluetask.CipherClueTask; +import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.when; + +class CipherClueTaskTest { + + private static final WorldPoint LOCATION = new WorldPoint(3229, 6045, 0); + + private final AtomicBoolean onClientThread = new AtomicBoolean(false); + private MockedStatic microbot; + private MockedStatic walker; + private ClueScrollPlugin clueScrollPlugin; + private CipherClue clue; + + @BeforeEach + void setUp() { + ClientThread clientThread = mock(ClientThread.class); + when(clientThread.runOnClientThreadOptional(any())).thenAnswer(invocation -> { + onClientThread.set(true); + try { + return Optional.ofNullable(((Callable) invocation.getArgument(0)).call()); + } finally { + onClientThread.set(false); + } + }); + microbot = mockStatic(Microbot.class); + microbot.when(Microbot::getClientThread).thenReturn(clientThread); + walker = mockStatic(Rs2Walker.class); + walker.when(() -> Rs2Walker.walkTo(any(WorldPoint.class))).thenReturn(true); + + clueScrollPlugin = mock(ClueScrollPlugin.class); + clue = mock(CipherClue.class); + } + + @AfterEach + void tearDown() { + walker.close(); + microbot.close(); + } + + @Test + void locationIsResolvedOnClientThreadBeforeWalking() { + when(clue.getLocation(clueScrollPlugin)).thenAnswer(invocation -> { + if (!onClientThread.get()) { + throw new IllegalStateException("must be called on client thread"); + } + return LOCATION; + }); + + CompletableFuture result = runTask(); + + walker.verify(() -> Rs2Walker.walkTo(LOCATION)); + assertFalse(result.isDone()); + } + + @Test + void missingLocationFailsTaskWithoutWalking() { + when(clue.getLocation(clueScrollPlugin)).thenReturn(null); + + CompletableFuture result = runTask(); + + walker.verify(() -> Rs2Walker.walkTo(any(WorldPoint.class)), never()); + assertTrue(result.isDone()); + assertFalse(result.join()); + } + + private CompletableFuture runTask() { + CipherClueTask task = new CipherClueTask(mock(Client.class), clue, clueScrollPlugin, null, + mock(EventBus.class), new DirectExecutor()); + CompletableFuture result = new CompletableFuture<>(); + task.setFuture(result); + task.run(); + return result; + } + + private static final class DirectExecutor extends AbstractExecutorService { + @Override + public void execute(Runnable command) { + command.run(); + } + + @Override + public void shutdown() { + } + + @Override + public List shutdownNow() { + return Collections.emptyList(); + } + + @Override + public boolean isShutdown() { + return false; + } + + @Override + public boolean isTerminated() { + return false; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) { + return true; + } + } +}