From b641f373f74a84b424206666b29c49c8709c1458 Mon Sep 17 00:00:00 2001 From: chsami <23292806+chsami@users.noreply.github.com> Date: Sun, 18 Oct 2026 09:18:41 +0200 Subject: [PATCH] fix(TowerOfLifeCC): run summon query on client thread (1.1.5) The summoned-creature lookup filtered NPCs on Actor.getInteracting(), which asserts the client thread, but the query was evaluated by nearest() on the script executor inside sleepUntil, throwing IllegalStateException "must be called on client thread" and never detecting the summoned creature. Evaluate it with nearestOnClientThread(). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjtEMzQHGuq4M1Mpqk9pPV --- .../TowerOfLifeCCPlugin.java | 2 +- .../TowerOfLifeCCScript.java | 13 ++- .../TowerOfLifeCCSummonQueryTest.java | 109 ++++++++++++++++++ 3 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 src/test/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCSummonQueryTest.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCPlugin.java index 2a98412040..252fefae0e 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCPlugin.java @@ -39,7 +39,7 @@ TowerOfLifeCCConfig provideConfig(ConfigManager configManager) { @Inject TowerOfLifeCCScript towerOfLifeCCScript; - static final String version = "1.1.4"; + static final String version = "1.1.5"; @Override protected void startUp() throws AWTException { diff --git a/src/main/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCScript.java b/src/main/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCScript.java index f0bf31a13d..65d71f12b4 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCScript.java @@ -405,10 +405,7 @@ void HandleCreature(LootingParameters params, int disposableItem, int secondaryI //Rs2Inventory.useItemOnObject(secondaryItem, altarObjectId); //Rs2Inventory.waitForInventoryChanges(3000); Microbot.getRs2TileObjectCache().query().interact(altarObjectId, "Activate"); - sleepUntil(() -> { summonedCreature = Microbot.getRs2NpcCache().query() - .where(npc -> npc.getNpc() != null - && (npc.getNpc().getInteracting() == null || npc.getNpc().getInteracting() == Microbot.getClient().getLocalPlayer())) - .nearest(); + sleepUntil(() -> { summonedCreature = findSummonedCreature(); return summonedCreature != null; }, 5000); //Microbot.log("Summoned creature"); @@ -437,4 +434,12 @@ else if (!Rs2Combat.inCombat()) } } } + + Rs2NpcModel findSummonedCreature() + { + return Microbot.getRs2NpcCache().query() + .where(npc -> npc.getNpc() != null + && (npc.getNpc().getInteracting() == null || npc.getNpc().getInteracting() == Microbot.getClient().getLocalPlayer())) + .nearestOnClientThread(); + } } diff --git a/src/test/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCSummonQueryTest.java b/src/test/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCSummonQueryTest.java new file mode 100644 index 0000000000..eaacd436d6 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/toweroflife_creaturecreation/TowerOfLifeCCSummonQueryTest.java @@ -0,0 +1,109 @@ +package net.runelite.client.plugins.microbot.toweroflife_creaturecreation; + +import net.runelite.api.Actor; +import net.runelite.api.Client; +import net.runelite.api.NPC; +import net.runelite.api.Player; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.api.npc.Rs2NpcCache; +import net.runelite.client.plugins.microbot.api.npc.Rs2NpcQueryable; +import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +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.verify; +import static org.mockito.Mockito.when; + +class TowerOfLifeCCSummonQueryTest { + + private final AtomicBoolean onClientThread = new AtomicBoolean(false); + private final Player localPlayer = mock(Player.class); + + @Test + void summonQueryEvaluatesInteractingOnClientThread() { + Rs2NpcModel idle = npc(null); + try (MockedStatic microbot = mockStatic(Microbot.class)) { + Rs2NpcQueryable query = stubQuery(microbot, List.of(idle)); + + assertSame(idle, new TowerOfLifeCCScript().findSummonedCreature()); + verify(query, never()).nearest(); + } + } + + @Test + void summonQueryAcceptsCreatureTargetingLocalPlayer() { + Rs2NpcModel busy = npc(mock(Actor.class)); + Rs2NpcModel mine = npc(localPlayer); + try (MockedStatic microbot = mockStatic(Microbot.class)) { + stubQuery(microbot, List.of(busy, mine)); + + assertSame(mine, new TowerOfLifeCCScript().findSummonedCreature()); + } + } + + @Test + void summonQueryRejectsCreatureTargetingSomeoneElse() { + Rs2NpcModel busy = npc(mock(Actor.class)); + try (MockedStatic microbot = mockStatic(Microbot.class)) { + stubQuery(microbot, List.of(busy)); + + assertNull(new TowerOfLifeCCScript().findSummonedCreature()); + } + } + + private Rs2NpcModel npc(Actor interacting) { + NPC npc = mock(NPC.class); + when(npc.getInteracting()).thenAnswer(invocation -> { + if (!onClientThread.get()) { + throw new IllegalStateException("must be called on client thread"); + } + return interacting; + }); + Rs2NpcModel model = mock(Rs2NpcModel.class); + when(model.getNpc()).thenReturn(npc); + return model; + } + + @SuppressWarnings("unchecked") + private Rs2NpcQueryable stubQuery(MockedStatic microbot, List npcs) { + Client client = mock(Client.class); + when(client.getLocalPlayer()).thenReturn(localPlayer); + microbot.when(Microbot::getClient).thenReturn(client); + + AtomicReference> filter = new AtomicReference<>(npc -> true); + Rs2NpcQueryable query = mock(Rs2NpcQueryable.class); + when(query.where(any())).thenAnswer(invocation -> { + filter.set(filter.get().and(invocation.getArgument(0))); + return query; + }); + when(query.nearest()).thenAnswer(invocation -> first(npcs, filter.get())); + when(query.nearestOnClientThread()).thenAnswer(invocation -> { + onClientThread.set(true); + try { + return first(npcs, filter.get()); + } finally { + onClientThread.set(false); + } + }); + + Rs2NpcCache cache = mock(Rs2NpcCache.class); + when(cache.query()).thenReturn(query); + microbot.when(Microbot::getRs2NpcCache).thenReturn(cache); + return query; + } + + private static Rs2NpcModel first(List npcs, Predicate filter) { + return npcs.stream().filter(filter).findFirst().orElse(null); + } +}