diff --git a/gradle.properties b/gradle.properties index 43c0b2ee4d4..aad34d263dd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -31,7 +31,7 @@ project.build.group=net.runelite project.build.version=1.12.22 glslang.path= -microbot.version=2.1.29 +microbot.version=2.1.30 microbot.commit.sha=nogit microbot.repo.url=http://138.201.81.246:8081/repository/microbot-snapshot/ microbot.repo.username= diff --git a/runelite-client/build.gradle.kts b/runelite-client/build.gradle.kts index e6b0a223de8..4513a21bd35 100644 --- a/runelite-client/build.gradle.kts +++ b/runelite-client/build.gradle.kts @@ -108,6 +108,32 @@ tasks.register("runTests") { } } +tasks.register("runIntegrationTest") { + group = "verification" + description = "Run Rs2ActorModel integration test with live client" + enabled = true + + testClassesDirs = sourceSets.test.get().output.classesDirs + classpath = sourceSets.test.get().runtimeClasspath + + jvmArgs( + "-Dfile.encoding=UTF-8", + "-Duser.timezone=Europe/Brussels", + "-ea" + ) + + include("**/Rs2ActorModelIntegrationTest.class") + include("**/Rs2WalkerIntegrationTest.class") + + useJUnit() + + testLogging { + events("passed", "skipped", "failed") + showStandardStreams = true + exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL + } +} + lombok.version = libs.versions.lombok.get() java { @@ -303,7 +329,9 @@ tasks.checkstyleMain { } tasks.withType { - enabled = false + if (name != "runIntegrationTest" && name != "runTests" && name != "runDebugTests") { + enabled = false + } systemProperty("glslang.path", providers.gradleProperty("glslangPath").getOrElse("")) } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/actor/Rs2ActorModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/actor/Rs2ActorModel.java index b4e145816a2..79ed15df01d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/actor/Rs2ActorModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/actor/Rs2ActorModel.java @@ -71,11 +71,13 @@ public int getHealthScale() @Override public WorldPoint getWorldLocation() { - if (getWorldView() != null && getWorldView().getId() != -1) { - return Microbot.getClientThread().invoke(this::projectActorLocationToMainWorld); - } - - return actor.getWorldLocation(); + return Microbot.getClientThread().invoke(() -> { + WorldView worldView = actor.getWorldView(); + if (worldView != null && !worldView.isTopLevel()) { + return projectActorLocationToMainWorld(); + } + return actor.getWorldLocation(); + }); } @Override @@ -450,17 +452,15 @@ public long getHash() public WorldPoint projectActorLocationToMainWorld() { WorldPoint actorLocation = actor.getWorldLocation(); - LocalPoint localPoint = LocalPoint.fromWorld( - getWorldView(), - actorLocation - ); + WorldView wv = actor.getWorldView(); + LocalPoint localPoint = LocalPoint.fromWorld(wv, actorLocation); if (localPoint == null) { return actorLocation; } - var mainWorldProjection = getWorldView().getMainWorldProjection(); + var mainWorldProjection = wv.getMainWorldProjection(); if (mainWorldProjection == null) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/example/ExampleScript.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/example/ExampleScript.java index f21dbca2388..38694003093 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/example/ExampleScript.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/example/ExampleScript.java @@ -18,6 +18,7 @@ import net.runelite.client.plugins.microbot.api.player.Rs2PlayerCache; import net.runelite.client.plugins.microbot.util.player.Rs2PlayerModel; import net.runelite.client.plugins.microbot.util.reachable.Rs2Reachable; +import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; import javax.inject.Inject; import java.util.ArrayList; @@ -56,59 +57,26 @@ public boolean run() { try { if (!Microbot.isLoggedIn()) return; -/* - if (Microbot.getClient().getTopLevelWorldView().getScene().isInstance()) { - LocalPoint l = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), Microbot.getClient().getLocalPlayer().getWorldLocation()); - System.out.println("was here"); - WorldPoint.fromLocalInstance(Microbot.getClient(), l); - } else { - System.out.println("was here lol"); - // this needs to ran on client threaad if we are on the sea - var a = Microbot.getClient().getLocalPlayer().getWorldLocation(); - System.out.println(a); - }*/ - - var shipwreck = rs2TileObjectCache.query() - .where(x -> x.getName() != null && x.getName().toLowerCase().contains("shipwreck")) - .within(5) - .nearestOnClientThread(); - var player = new Rs2PlayerModel(); - - var isInvFull = Rs2Inventory.count() >= Rs2Random.between(24, 28); - if (isInvFull && Rs2Inventory.count("salvage") > 0 && player.getAnimation() == -1) { - // Rs2Inventory.dropAll("large salvage"); - rs2TileObjectCache.query() - .fromWorldView() - .where(x -> x.getName() != null && x.getName().equalsIgnoreCase("salvaging station")) - .where(x -> x.getWorldView().getId() == new Rs2PlayerModel().getWorldView().getId()) - .nearestOnClientThread() - .click(); - sleepUntil(() -> Rs2Inventory.count("salvage") == 0, 60000); - } else if (isInvFull) { - dropJunk(); - } else { - if (player.getAnimation() != -1) { - log.info("Currently salvaging, waiting..."); - sleep(5000, 10000); - return; - } + if (Rs2Inventory.isFull()) { + Rs2Inventory.dropAll("Logs"); + return; + } - if (shipwreck == null) { - log.info("No shipwreck found nearby"); - sleep(5000); - dropJunk(); - return; - } + if (Rs2Player.isAnimating()) return; - rs2TileObjectCache.query().fromWorldView().where(x -> x.getName() != null && x.getName().toLowerCase().contains("salvaging hook")).nearestOnClientThread().click("Deploy"); - sleepUntil(() -> player.getAnimation() != -1, 5000); + var tree = Microbot.getRs2TileObjectCache().query() + .withName("Tree") + .nearest(); + if (tree != null) { + tree.click("Chop down"); + sleepUntil(Rs2Player::isAnimating, 3000); } } catch (Exception ex) { - log.error("Error in performance test loop", ex); + log.error("Error in example script", ex); } - }, 0, 1000, TimeUnit.MILLISECONDS); + }, 0, 600, TimeUnit.MILLISECONDS); return true; } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/QuestScript.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/QuestScript.java index 4bd18e00917..413941e0581 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/QuestScript.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/QuestScript.java @@ -319,7 +319,7 @@ private boolean attemptToAcquireRequirementItem(DetailedQuestStep questStep, Ite if (worldPoint != null) { if ((Rs2Walker.canReach(worldPoint) && worldPoint.distanceTo(Rs2Player.getWorldLocation()) < 2) - || worldPoint.toWorldArea().hasLineOfSightTo(Microbot.getClient().getTopLevelWorldView(), Microbot.getClient().getLocalPlayer().getWorldLocation().toWorldArea()) + || worldPoint.toWorldArea().hasLineOfSightTo(Microbot.getClient().getTopLevelWorldView(), Rs2Player.getWorldLocation().toWorldArea()) && Rs2Camera.isTileOnScreen(LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), worldPoint))) { lootGroundItem(targetItemId, 10); } else { @@ -464,7 +464,7 @@ public boolean applyNpcStep(NpcStep step) { } else if (npc != null && (!npc.hasLineOfSight() || !Rs2Walker.canReach(npc.getWorldLocation()))) { Rs2Walker.walkTo(npc.getWorldLocation(), 2); } else { - if (step.getDefinedPoint().getWorldPoint().distanceTo(Microbot.getClient().getLocalPlayer().getWorldLocation()) > 3) { + if (step.getDefinedPoint().getWorldPoint().distanceTo(Rs2Player.getWorldLocation()) > 3) { Rs2Walker.walkTo(step.getDefinedPoint().getWorldPoint(), 2); return false; } @@ -517,7 +517,7 @@ public boolean applyObjectStep(ObjectStep step) { return false; } - if (step.getDefinedPoint().getWorldPoint() != null && Microbot.getClient().getLocalPlayer().getWorldLocation().distanceTo2D(step.getDefinedPoint().getWorldPoint()) > 1 + if (step.getDefinedPoint().getWorldPoint() != null && Rs2Player.getWorldLocation().distanceTo2D(step.getDefinedPoint().getWorldPoint()) > 1 && (object == null || !Rs2Walker.canReach(object.getWorldLocation()))) { WorldPoint targetTile = null; WorldPoint stepLocation = object == null ? step.getDefinedPoint().getWorldPoint() : object.getWorldLocation(); @@ -641,7 +641,7 @@ private boolean hasLineOfSightToObject(Rs2TileObjectModel object) { } WorldArea objectArea = object.getWorldLocation().toWorldArea(); - WorldArea playerArea = Microbot.getClient().getLocalPlayer().getWorldLocation().toWorldArea(); + WorldArea playerArea = Rs2Player.getWorldLocation().toWorldArea(); return Microbot.getClient().getTopLevelWorldView() != null && playerArea.hasLineOfSightTo(Microbot.getClient().getTopLevelWorldView(), objectArea); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/runeliteobjects/Cheerer.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/runeliteobjects/Cheerer.java index e0f725ef4b7..b1a1726aaa0 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/runeliteobjects/Cheerer.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/runeliteobjects/Cheerer.java @@ -45,6 +45,10 @@ public class Cheerer public static void createCheerers(RuneliteObjectManager runeliteObjectManager, Client client, ConfigManager configManager) { cheerers.clear(); + if (client.getLocalPlayer() == null) + { + return; + } createWOM(runeliteObjectManager, client); createZoinkwiz(runeliteObjectManager, client); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/runeliteobjects/extendedruneliteobjects/RuneliteObjectManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/runeliteobjects/extendedruneliteobjects/RuneliteObjectManager.java index 027788ac707..68e1af5f9f5 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/runeliteobjects/extendedruneliteobjects/RuneliteObjectManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/runeliteobjects/extendedruneliteobjects/RuneliteObjectManager.java @@ -822,6 +822,10 @@ public void onClientTick(ClientTick event) { redClickAnimationFrame++; } + if (client.getLocalPlayer() == null) + { + return; + } WorldPoint playerPosition = WorldPoint.fromLocalInstance(client, client.getLocalPlayer().getLocalLocation()); runeliteObjectGroups.forEach((groupID, extendedRuneliteObjectGroup) -> { for (ExtendedRuneliteObject extendedRuneliteObject : extendedRuneliteObjectGroup.extendedRuneliteObjects) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/CollisionMap.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/CollisionMap.java index b708077fe2d..85dedca242e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/CollisionMap.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/CollisionMap.java @@ -99,6 +99,25 @@ private static int packedPointFromOrdinal(int startPacked, OrdinalDirection dire new WorldPoint(3672, 3862, 0) ); + private volatile int cachedRegionId = -1; + private volatile long cachedRegionIdTime = 0; + private static final long REGION_CACHE_MS = 5000; + private static final int TOA_PUZZLE_REGION = 14162; + + private int getCachedRegionId() { + long now = System.currentTimeMillis(); + if (now - cachedRegionIdTime > REGION_CACHE_MS) { + try { + WorldPoint loc = Rs2Player.getWorldLocation(); + cachedRegionId = loc != null ? loc.getRegionID() : -1; + } catch (Exception e) { + cachedRegionId = -1; + } + cachedRegionIdTime = now; + } + return cachedRegionId; + } + public List getNeighbors(Node node, VisitedTiles visited, PathfinderConfig config, Set targets) { final int x = WorldPointUtil.unpackWorldX(node.packedPosition); final int y = WorldPointUtil.unpackWorldY(node.packedPosition); @@ -167,7 +186,7 @@ public List getNeighbors(Node node, VisitedTiles visited, PathfinderConfig * This piece of code is designed to allow web walker to be used in toa puzzle room * it will dodge specific tiles in the sequence room */ - if (Rs2Player.getWorldLocation().getRegionID() == 14162) { //toa puzzle room + if (getCachedRegionId() == TOA_PUZZLE_REGION) { if (!targets.contains(neighborPacked)) { WorldPoint globalWorldPoint = Rs2WorldPoint.convertInstancedWorldPoint(WorldPointUtil.unpackWorldPoint(neighborPacked)); if (globalWorldPoint != null) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java index 09482ad564b..641975462ee 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java @@ -123,91 +123,93 @@ private void addNeighbors(Node node) { @Override public void run() { - stats.start(); - boundary.addFirst(new Node(start, null)); - - int bestDistance = Integer.MAX_VALUE; - long bestHeuristic = Integer.MAX_VALUE; - long cutoffDurationMillis = config.getCalculationCutoffMillis(); - long cutoffTimeMillis = System.currentTimeMillis() + cutoffDurationMillis; - - config.refreshTeleports(start, 31); - while (!cancelled && (!boundary.isEmpty() || !pending.isEmpty())) { - Node node = boundary.peekFirst(); - Node p = pending.peek(); - - if (p != null && (node == null || p.cost < node.cost)) { - node = pending.poll(); - } else { - node = boundary.removeFirst(); - } - - if (wildernessLevel > 0) { - // We don't need to remove teleports when going from 20 to 21 or higher, - // because the teleport is either used at the very start of the - // path or when going from 31 or higher to 30, or from 21 or higher to 20. - - boolean update = false; - - // These are overlapping boundaries, so if the node isn't in level 30, it's in 0-29 - // likewise, if the node isn't in level 20, it's in 0-19 - if (wildernessLevel > 29 && !config.isInLevel29Wilderness(node.packedPosition)) { - wildernessLevel = 29; - update = true; + log.info("[Pathfinder] run() started: src={}, dst={}, cutoff={}ms", + WorldPointUtil.toString(start), WorldPointUtil.toString(targets), config.getCalculationCutoffMillis()); + try { + stats.start(); + boundary.addFirst(new Node(start, null)); + + int bestDistance = Integer.MAX_VALUE; + long bestHeuristic = Integer.MAX_VALUE; + long cutoffDurationMillis = config.getCalculationCutoffMillis(); + long cutoffTimeMillis = System.currentTimeMillis() + cutoffDurationMillis; + + config.refreshTeleports(start, 31); + while (!cancelled && (!boundary.isEmpty() || !pending.isEmpty())) { + Node node = boundary.peekFirst(); + Node p = pending.peek(); + + if (p != null && (node == null || p.cost < node.cost)) { + node = pending.poll(); + } else { + node = boundary.removeFirst(); } - if (wildernessLevel > 19 && !config.isInLevel19Wilderness(node.packedPosition)) { - wildernessLevel = 19; - update = true; - } - if (wildernessLevel > 0 && !PathfinderConfig.isInWilderness(node.packedPosition)) { - wildernessLevel = 0; - update = true; + + if (wildernessLevel > 0) { + boolean update = false; + + if (wildernessLevel > 29 && !config.isInLevel29Wilderness(node.packedPosition)) { + wildernessLevel = 29; + update = true; + } + if (wildernessLevel > 19 && !config.isInLevel19Wilderness(node.packedPosition)) { + wildernessLevel = 19; + update = true; + } + if (wildernessLevel > 0 && !PathfinderConfig.isInWilderness(node.packedPosition)) { + wildernessLevel = 0; + update = true; + } + if (update) { + config.refreshTeleports(node.packedPosition, wildernessLevel); + } } - if (update) { - config.refreshTeleports(node.packedPosition, wildernessLevel); + + if (targets.contains(node.packedPosition)) { + bestLastNode = node; + pathNeedsUpdate = true; + break; } - } - if (targets.contains(node.packedPosition)) { - bestLastNode = node; - pathNeedsUpdate = true; - break; - } + for (int target : targets) { + int distance = WorldPointUtil.distanceBetween(node.packedPosition, target); + long heuristic = distance + (long) WorldPointUtil.distanceBetween(node.packedPosition, target, 2); - for (int target : targets) { - int distance = WorldPointUtil.distanceBetween(node.packedPosition, target); - long heuristic = distance + (long) WorldPointUtil.distanceBetween(node.packedPosition, target, 2); + if (heuristic < bestHeuristic || (heuristic <= bestHeuristic && distance < bestDistance)) { - if (heuristic < bestHeuristic || (heuristic <= bestHeuristic && distance < bestDistance)) { + bestLastNode = node; + pathNeedsUpdate = true; + bestDistance = distance; + bestHeuristic = heuristic; + cutoffTimeMillis = System.currentTimeMillis() + cutoffDurationMillis; + } + } - bestLastNode = node; - pathNeedsUpdate = true; - bestDistance = distance; - bestHeuristic = heuristic; - cutoffTimeMillis = System.currentTimeMillis() + cutoffDurationMillis; + if (System.currentTimeMillis() > cutoffTimeMillis) { + log.info("[Pathfinder] Cutoff reached. bestDistance={}, nodesChecked={}", bestDistance, stats.getNodesChecked()); + break; } - } - if (System.currentTimeMillis() > cutoffTimeMillis) { - break; + addNeighbors(node); } - - addNeighbors(node); - } - done = !cancelled; + log.info("[Pathfinder] Loop exited. cancelled={}, boundaryEmpty={}, pendingEmpty={}, bestLastNode={}", + cancelled, boundary.isEmpty(), pending.isEmpty(), + bestLastNode == null ? "null" : WorldPointUtil.toString(bestLastNode.packedPosition)); + } catch (Exception e) { + log.error("[Pathfinder] Exception in run(): ", e); + } finally { + done = !cancelled; - boundary.clear(); - visited.clear(); - pending.clear(); + boundary.clear(); + visited.clear(); + pending.clear(); - stats.end(); // Include cleanup in stats to get the total cost of pathfinding + stats.end(); - log.debug("Pathfinding completed DstNode={} src={} dst={} Stats={}", - bestLastNode == null ? "null" : WorldPointUtil.toString(bestLastNode.packedPosition), - WorldPointUtil.toString(start), - WorldPointUtil.toString(targets), - getStats().toString()); + log.info("[Pathfinder] run() completed. done={}, cancelled={}, stats={}", + done, cancelled, getStats() != null ? getStats().toString() : "null"); + } } public static class PathfinderStats { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/bank/Rs2Bank.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/bank/Rs2Bank.java index 7bf23d91a3d..9e3b06abeae 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/bank/Rs2Bank.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/bank/Rs2Bank.java @@ -1936,7 +1936,7 @@ private static Rs2ItemModel findBankItem(Collection names, boolean exact * @return the nearest {@link BankLocation}, or {@code null} if none was reachable */ public static BankLocation getNearestBank() { - return getNearestBank(Microbot.getClient().getLocalPlayer().getWorldLocation()); + return getNearestBank(Rs2Player.getWorldLocation()); } /** @@ -1982,7 +1982,7 @@ private static AbstractMap.SimpleEntry, BankLocation> getPathAn Set allBanks = Arrays.stream(BankLocation.values()) .collect(Collectors.toSet()); - if (Objects.equals(Microbot.getClient().getLocalPlayer().getWorldLocation(), worldPoint)) { + if (Objects.equals(Rs2Player.getWorldLocation(), worldPoint)) { List bankObjs = Stream.concat( Stream.of(Rs2GameObject.findBank(maxObjectSearchRadius)), Stream.of(Rs2GameObject.findGrandExchangeBooth(maxObjectSearchRadius)) @@ -2138,7 +2138,7 @@ public static boolean walkToBank(BankLocation bankLocation, boolean toggleRun) { Rs2Player.toggleRunEnergy(toggleRun); Microbot.status = "Walking to nearest bank " + bankLocation.toString(); Rs2Walker.walkTo(bankLocation.getWorldPoint(), 4); - return bankLocation.getWorldPoint().distanceTo2D(Microbot.getClient().getLocalPlayer().getWorldLocation()) <= 4; + return bankLocation.getWorldPoint().distanceTo2D(Rs2Player.getWorldLocation()) <= 4; } /** @@ -2159,7 +2159,7 @@ public static boolean isNearBank(int distance) { * @return true if player location is less than distance away from the bank location */ public static boolean isNearBank(BankLocation bankLocation, int distance) { - int distanceToBank = Rs2Walker.getDistanceBetween(Microbot.getClient().getLocalPlayer().getWorldLocation(), bankLocation.getWorldPoint()); + int distanceToBank = Rs2Walker.getDistanceBetween(Rs2Player.getWorldLocation(), bankLocation.getWorldPoint()); return distanceToBank <= distance; } @@ -2200,7 +2200,7 @@ public static boolean walkToBankAndUseBank(BankLocation bankLocation, boolean to if (Rs2Bank.isOpen()) return true; Rs2Player.toggleRunEnergy(toggleRun); Microbot.status = "Walking to nearest bank " + bankLocation.toString(); - boolean result = Rs2Walker.getDistanceBetween(Microbot.getClient().getLocalPlayer().getWorldLocation(), bankLocation.getWorldPoint()) <= 8; + boolean result = Rs2Walker.getDistanceBetween(Rs2Player.getWorldLocation(), bankLocation.getWorldPoint()) <= 8; if (!result) { Rs2Walker.walkTo(bankLocation.getWorldPoint()); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/bank/enums/BankLocation.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/bank/enums/BankLocation.java index af2d3fb40cd..38c353ce5ac 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/bank/enums/BankLocation.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/bank/enums/BankLocation.java @@ -154,7 +154,7 @@ public boolean hasRequirements() { case FARMING_GUILD: return Rs2Player.getSkillRequirement(Skill.FARMING, 45, true); case MINING_GUILD: - boolean inRegion = Microbot.getClient().getLocalPlayer().getWorldLocation().getRegionID() == 12183 || Microbot.getClient().getLocalPlayer().getWorldLocation().getRegionID() == 12184; + boolean inRegion = Rs2Player.getWorldLocation().getRegionID() == 12183 || Rs2Player.getWorldLocation().getRegionID() == 12184; return inRegion && Rs2Player.getSkillRequirement(Skill.MINING, 60, true); case FISHING_GUILD: return Rs2Player.getSkillRequirement(Skill.FISHING, 68, true); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/depositbox/Rs2DepositBox.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/depositbox/Rs2DepositBox.java index b7a93c2868b..930000e6f8e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/depositbox/Rs2DepositBox.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/depositbox/Rs2DepositBox.java @@ -416,7 +416,7 @@ public static Rectangle itemBounds(Rs2ItemModel rs2Item) { * @return the nearest {@link DepositBoxLocation}, or {@code null} if no accessible deposit box was found */ public static DepositBoxLocation getNearestDepositBox() { - return getNearestDepositBox(Microbot.getClient().getLocalPlayer().getWorldLocation()); + return getNearestDepositBox(Rs2Player.getWorldLocation()); } /** @@ -457,7 +457,7 @@ public static DepositBoxLocation getNearestDepositBox(WorldPoint worldPoint, int return null; } - if (Objects.equals(Microbot.getClient().getLocalPlayer().getWorldLocation(), worldPoint)) { + if (Objects.equals(Rs2Player.getWorldLocation(), worldPoint)) { List bankObjs = List.of(Rs2GameObject.findDepositBox(maxObjectSearchRadius)); Optional byObject = bankObjs.stream() @@ -524,7 +524,7 @@ public static boolean walkToDepositBox(DepositBoxLocation depositBoxLocation) { Rs2Player.toggleRunEnergy(true); Microbot.status = "Walking to nearest deposit box " + depositBoxLocation.name(); Rs2Walker.walkTo(depositBoxLocation.getWorldPoint(), 4); - return depositBoxLocation.getWorldPoint().distanceTo2D(Microbot.getClient().getLocalPlayer().getWorldLocation()) <= 4; + return depositBoxLocation.getWorldPoint().distanceTo2D(Rs2Player.getWorldLocation()) <= 4; } /** @@ -555,7 +555,7 @@ public static boolean walkToAndUseDepositBox(DepositBoxLocation depositBoxLocati if (isOpen()) return true; Rs2Player.toggleRunEnergy(true); Microbot.status = "Walking to nearest deposit box " + depositBoxLocation.name(); - boolean result = depositBoxLocation.getWorldPoint().distanceTo(Microbot.getClient().getLocalPlayer().getWorldLocation()) <= 8; + boolean result = depositBoxLocation.getWorldPoint().distanceTo(Rs2Player.getWorldLocation()) <= 8; if (result) { return openDepositBox(); } else { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/equipment/Rs2Equipment.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/equipment/Rs2Equipment.java index 98315b8b868..da5c1541a26 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/equipment/Rs2Equipment.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/equipment/Rs2Equipment.java @@ -23,7 +23,7 @@ import java.util.stream.Stream; public class Rs2Equipment { - private static List equipmentItems = Collections.emptyList(); + private static volatile List equipmentItems = Collections.emptyList(); public static ItemContainer equipment() { return Microbot.getClient().getItemContainer(InventoryID.WORN); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java index 5462395fd3f..833aa30519d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java @@ -528,7 +528,7 @@ public static List getAll(Predicate List getAll(Predicate predicate, int distance) { - WorldPoint worldPoint = Microbot.getClient().getLocalPlayer().getWorldLocation(); + WorldPoint worldPoint = Rs2Player.getWorldLocation(); return getAll(predicate, worldPoint, distance); } @@ -662,11 +662,7 @@ public static TileObject getTileObject(Predicate predicate, LocalPoi } public static TileObject getTileObject(Predicate predicate, WorldPoint anchor, int distance) { - Player player = Microbot.getClient().getLocalPlayer(); - if (player == null) { - return null; - } - LocalPoint anchorLocal = LocalPoint.fromWorld(player.getWorldView(), anchor); + LocalPoint anchorLocal = localPointFromWorldSafe(anchor); if (anchorLocal == null) { return null; } @@ -854,11 +850,7 @@ public static GameObject getGameObject(Predicate predicate, LocalPoi } public static GameObject getGameObject(Predicate predicate, WorldPoint anchor, int distance) { - Player player = Microbot.getClient().getLocalPlayer(); - if (player == null) { - return null; - } - LocalPoint anchorLocal = LocalPoint.fromWorld(player.getWorldView(), anchor); + LocalPoint anchorLocal = localPointFromWorldSafe(anchor); if (anchorLocal == null) { return null; } @@ -1040,11 +1032,7 @@ public static GroundObject getGroundObject(Predicate predicate, Lo } public static GroundObject getGroundObject(Predicate predicate, WorldPoint anchor, int distance) { - Player player = Microbot.getClient().getLocalPlayer(); - if (player == null) { - return null; - } - LocalPoint anchorLocal = LocalPoint.fromWorld(player.getWorldView(), anchor); + LocalPoint anchorLocal = localPointFromWorldSafe(anchor); if (anchorLocal == null) { return null; } @@ -1226,11 +1214,7 @@ public static WallObject getWallObject(Predicate predicate, LocalPoi } public static WallObject getWallObject(Predicate predicate, WorldPoint anchor, int distance) { - Player player = Microbot.getClient().getLocalPlayer(); - if (player == null) { - return null; - } - LocalPoint anchorLocal = LocalPoint.fromWorld(player.getWorldView(), anchor); + LocalPoint anchorLocal = localPointFromWorldSafe(anchor); if (anchorLocal == null) { return null; } @@ -1282,11 +1266,7 @@ public static List getWallObjects(Predicate predicate, L } public static List getWallObjects(Predicate predicate, WorldPoint anchor, int distance) { - Player player = Microbot.getClient().getLocalPlayer(); - if (player == null) { - return Collections.emptyList(); - } - LocalPoint anchorLocal = LocalPoint.fromWorld(player.getWorldView(), anchor); + LocalPoint anchorLocal = localPointFromWorldSafe(anchor); if (anchorLocal == null) { return Collections.emptyList(); } @@ -1416,11 +1396,7 @@ public static DecorativeObject getDecorativeObject(Predicate p } public static DecorativeObject getDecorativeObject(Predicate predicate, WorldPoint anchor, int distance) { - Player player = Microbot.getClient().getLocalPlayer(); - if (player == null) { - return null; - } - LocalPoint anchorLocal = LocalPoint.fromWorld(player.getWorldView(), anchor); + LocalPoint anchorLocal = localPointFromWorldSafe(anchor); if (anchorLocal == null) { return null; } @@ -1472,11 +1448,7 @@ public static List getDecorativeObjects(Predicate getDecorativeObjects(Predicate predicate, WorldPoint anchor, int distance) { - Player player = Microbot.getClient().getLocalPlayer(); - if (player == null) { - return Collections.emptyList(); - } - LocalPoint anchorLocal = LocalPoint.fromWorld(player.getWorldView(), anchor); + LocalPoint anchorLocal = localPointFromWorldSafe(anchor); if (anchorLocal == null) { return Collections.emptyList(); } @@ -1601,6 +1573,14 @@ private static Predicate withinTilesPredicate(int dist return to -> isWithinTiles(anchor, to.getLocalLocation(), distance); } + private static LocalPoint localPointFromWorldSafe(WorldPoint anchor) { + return Microbot.getClientThread().runOnClientThreadOptional(() -> { + Player player = Microbot.getClient().getLocalPlayer(); + if (player == null) return null; + return LocalPoint.fromWorld(player.getWorldView(), anchor); + }).orElse(null); + } + public static Optional getCompositionName(TileObject obj) { ObjectComposition comp = convertToObjectComposition(obj); if (comp == null) { @@ -1750,7 +1730,7 @@ private static boolean clickObject(TileObject object) { public static boolean clickObject(TileObject object, String action) { if (object == null) return false; - if (Microbot.getClient().getLocalPlayer().getWorldLocation().distanceTo(object.getWorldLocation()) > 51) { + if (Rs2Player.getWorldLocation().distanceTo(object.getWorldLocation()) > 51) { Microbot.log("Object with id " + object.getId() + " is not close enough to interact with. Walking to the object...."); Rs2Walker.walkTo(object.getWorldLocation()); return false; @@ -1842,9 +1822,9 @@ public static boolean clickObject(TileObject object, String action) { param1 = 4; }*/ - int worldViewId = -1; + int worldViewId = WorldView.TOPLEVEL; - if (object.getWorldView().getId() != -1) { + if (!object.getWorldView().isTopLevel()) { var worldView =Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldView()); if (worldView == null) { worldViewId = Microbot.getClient().getTopLevelWorldView().getId(); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2ObjectModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2ObjectModel.java index 05c143b44fb..2ab087ac6fc 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2ObjectModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2ObjectModel.java @@ -6,6 +6,7 @@ import net.runelite.api.*; import net.runelite.api.coords.WorldPoint; import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; /** * Enhanced model for game objects with caching and tick tracking. @@ -228,7 +229,7 @@ public long getTimeSinceCreation() { */ public int getDistanceFromPlayer() { return Microbot.getClientThread().runOnClientThreadOptional(() -> { - WorldPoint playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); + WorldPoint playerLocation = Rs2Player.getWorldLocation(); return playerLocation.distanceTo(getLocation()); }).orElse(Integer.MAX_VALUE); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItem.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItem.java index fd225052cab..0cd37f4f726 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItem.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItem.java @@ -187,7 +187,7 @@ public static RS2Item[] getAllAt(int x, int y) { } public static RS2Item[] getAll(int range) { - return getAllFromWorldPoint(range, Microbot.getClient().getLocalPlayer().getWorldLocation()); + return getAllFromWorldPoint(range, Rs2Player.getWorldLocation()); } /** @@ -311,7 +311,8 @@ private static boolean validateLoot(Predicate filter) { private static Predicate baseRangeAndOwnershipFilter(LootingParameters params) { - final WorldPoint me = Microbot.getClient().getLocalPlayer().getWorldLocation(); + final WorldPoint me = Rs2Player.getWorldLocation(); + if (me == null) return gi -> false; final boolean anti = params.isAntiLureProtection(); return gi -> gi.getLocation().distanceTo(me) < params.getRange() && @@ -543,7 +544,7 @@ public static boolean exists(String itemName, int range) { public static boolean hasLineOfSight(Tile tile) { if (tile == null) return false; return tile.getWorldLocation().toWorldArea() - .hasLineOfSightTo(Microbot.getClient().getTopLevelWorldView(), Microbot.getClient().getLocalPlayer().getWorldLocation().toWorldArea()); + .hasLineOfSightTo(Microbot.getClient().getTopLevelWorldView(), Rs2Player.getWorldLocation().toWorldArea()); } /** diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2LootEngine.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2LootEngine.java index 055941efc11..c6cfe3631a7 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2LootEngine.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2LootEngine.java @@ -141,7 +141,7 @@ public Builder addCustom(String label, Predicate predicate, Set unique = new LinkedHashMap<>(); for (List list : candidateBuckets.values()) { @@ -206,7 +206,7 @@ private void collect(String label, Predicate itemPredicate, Set baseRangeAndOwnershipFilter(LootingParameters params) { - final WorldPoint me = Microbot.getClient().getLocalPlayer().getWorldLocation(); + final WorldPoint me = Rs2Player.getWorldLocation(); final boolean anti = params.isAntiLureProtection(); return gi -> gi.getLocation().distanceTo(me) < params.getRange() diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/inventory/Rs2Inventory.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/inventory/Rs2Inventory.java index fe92d144231..80b0a94fbd2 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/inventory/Rs2Inventory.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/inventory/Rs2Inventory.java @@ -50,7 +50,7 @@ public class Rs2Inventory { private static final int CAPACITY = COLUMNS * ROWS; private static final String[] EMPTY_ARRAY = new String[0]; - private static List inventoryItems = Collections.emptyList(); + private static volatile List inventoryItems = Collections.emptyList(); public static ItemContainer inventory() { return Microbot.getClient().getItemContainer(InventoryID.INV); @@ -74,6 +74,21 @@ public static void storeInventoryItemsInMemory(ItemContainerChanged e) { } public static Stream items() { + if (inventoryItems.isEmpty() && Microbot.isLoggedIn()) { + Microbot.getClientThread().runOnClientThreadOptional(() -> { + final ItemContainer itemContainer = Microbot.getClient().getItemContainer(InventoryID.INV); + if (itemContainer == null) return null; + List _inventoryItems = new ArrayList<>(); + for (int i = 0; i < itemContainer.getItems().length; i++) { + final Item item = itemContainer.getItems()[i]; + if (item.getId() == -1) continue; + final ItemComposition itemComposition = Microbot.getClient().getItemDefinition(item.getId()); + _inventoryItems.add(new Rs2ItemModel(item, itemComposition, i)); + } + inventoryItems = Collections.unmodifiableList(_inventoryItems); + return null; + }); + } return inventoryItems.stream(); } @@ -1940,6 +1955,10 @@ private static void invokeMenu(Rs2ItemModel rs2Item, String action) { } + /* if (identifier > 5) { + menuAction = MenuAction.CC_OP_LOW_PRIORITY; + }*/ + if (isItemSelected()) { menuAction = MenuAction.WIDGET_TARGET_ON_WIDGET; } else if (action.equalsIgnoreCase("use")) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/menu/NewMenuEntry.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/menu/NewMenuEntry.java index 3d85a39fee4..191e58a460a 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/menu/NewMenuEntry.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/menu/NewMenuEntry.java @@ -28,7 +28,7 @@ public class NewMenuEntry implements MenuEntry { private Actor actor; private TileObject gameObject; private Widget widget; - private int worldViewId = -1; + private int worldViewId = WorldView.TOPLEVEL; private NewMenuEntry(int param0, int param1, MenuAction type, int identifier) { this.param0 = param0; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2Npc.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2Npc.java index 481fbb44cbf..ed19f22dd01 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2Npc.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2Npc.java @@ -404,7 +404,7 @@ public static Stream getAttackableNpcs() { * @return A sorted {@link Stream} of {@link Rs2NpcModel} objects that the player can attack. */ public static Stream getAttackableNpcs(boolean reachable) { - Rs2WorldPoint playerLocation = new Rs2WorldPoint(Microbot.getClient().getLocalPlayer().getWorldLocation()); + Rs2WorldPoint playerLocation = new Rs2WorldPoint(Rs2Player.getWorldLocation()); return getNpcs(npc -> npc.getCombatLevel() > 0 && !npc.isDead() @@ -1218,7 +1218,7 @@ public static Rs2NpcModel getNpcInLineOfSight(String name) { * @return The nearest {@link Rs2NpcModel} that has the specified action, or {@code null} if none are found. */ public static Rs2NpcModel getNearestNpcWithAction(String action) { - Rs2WorldPoint playerLocation = new Rs2WorldPoint(Microbot.getClient().getLocalPlayer().getWorldLocation()); + Rs2WorldPoint playerLocation = new Rs2WorldPoint(Rs2Player.getWorldLocation()); boolean isInstance = Microbot.getClient().getTopLevelWorldView().getScene().isInstance(); return getNpcs() .filter(value -> value.getComposition() != null diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java index a1911bbc311..814d612608e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java @@ -963,11 +963,13 @@ private static Stream getPlayersMatchingCombatCriteria() { * @return The {@link WorldPoint} representing the player's current location. */ public static WorldPoint getWorldLocation() { - if (Microbot.getClient().getTopLevelWorldView().getScene().isInstance()) { - LocalPoint l = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), Microbot.getClient().getLocalPlayer().getWorldLocation()); - return WorldPoint.fromLocalInstance(Microbot.getClient(), l); - } - return Microbot.getClient().getLocalPlayer().getWorldLocation(); + return Microbot.getClientThread().runOnClientThreadOptional(() -> { + if (Microbot.getClient().getTopLevelWorldView().getScene().isInstance()) { + LocalPoint l = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), Microbot.getClient().getLocalPlayer().getWorldLocation()); + return WorldPoint.fromLocalInstance(Microbot.getClient(), l); + } + return Microbot.getClient().getLocalPlayer().getWorldLocation(); + }).orElse(null); } /** @@ -1434,8 +1436,10 @@ public static boolean isStandingOnGroundItem() { * @return The animation ID of the player's current action, or {@code -1} if the player is null. */ public static int getAnimation() { - if (Microbot.getClient() == null || Microbot.getClient().getLocalPlayer() == null) return -1; - return Microbot.getClient().getLocalPlayer().getAnimation(); + return Microbot.getClientThread().runOnClientThreadOptional(() -> { + if (Microbot.getClient() == null || Microbot.getClient().getLocalPlayer() == null) return -1; + return Microbot.getClient().getLocalPlayer().getAnimation(); + }).orElse(-1); } /** @@ -1444,7 +1448,9 @@ public static int getAnimation() { * @return The pose animation ID of the player. */ public static int getPoseAnimation() { - return Microbot.getClient().getLocalPlayer().getPoseAnimation(); + return Microbot.getClientThread().runOnClientThreadOptional(() -> + Microbot.getClient().getLocalPlayer().getPoseAnimation() + ).orElse(-1); } /** @@ -1464,7 +1470,9 @@ public static QuestState getQuestState(Quest quest) { * @return The player's real level for the specified skill. */ public static int getRealSkillLevel(Skill skill) { - return Microbot.getClient().getRealSkillLevel(skill); + return Microbot.getClientThread().runOnClientThreadOptional(() -> + Microbot.getClient().getRealSkillLevel(skill) + ).orElse(0); } /** @@ -1473,8 +1481,10 @@ public static int getRealSkillLevel(Skill skill) { * @param skill The {@link Skill} to check. * @return The player's boosted level for the specified skill. */ - public static int getBoostedSkillLevel(Skill skill) { - return Microbot.getClient().getBoostedSkillLevel(skill); + public static int getBoostedSkillLevel(Skill skill) { + return Microbot.getClientThread().runOnClientThreadOptional(() -> + Microbot.getClient().getBoostedSkillLevel(skill) + ).orElse(0); } /** diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Pvp.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Pvp.java index a2f6bd5d938..6ac36db1f94 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Pvp.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Pvp.java @@ -30,6 +30,7 @@ import net.runelite.api.geometry.Cuboid; import net.runelite.client.game.ItemManager; import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.util.QuantityFormatter; import org.apache.commons.lang3.ArrayUtils; @@ -133,7 +134,7 @@ public static boolean isAttackable(Rs2PlayerModel rs2Player) { wildernessLevel += 15; } if (Microbot.getVarbitValue(Varbits.IN_WILDERNESS) == 1) { - wildernessLevel += getWildernessLevelFrom(Microbot.getClient().getLocalPlayer().getWorldLocation()); + wildernessLevel += getWildernessLevelFrom(Rs2Player.getWorldLocation()); } return wildernessLevel != 0 && Math.abs(Microbot.getClient().getLocalPlayer().getCombatLevel() - rs2Player.getCombatLevel()) <= wildernessLevel; } @@ -162,7 +163,7 @@ public static boolean isAttackable() { boolean isDeadManWorld = WorldType.isDeadmanWorld(Microbot.getClient().getWorldType()); boolean isPVPWorld = WorldType.isPvpWorld(Microbot.getClient().getWorldType()); if (Microbot.getVarbitValue(Varbits.IN_WILDERNESS) == 1) { - wildernessLevel += getWildernessLevelFrom(Microbot.getClient().getLocalPlayer().getWorldLocation()); + wildernessLevel += getWildernessLevelFrom(Rs2Player.getWorldLocation()); } for (Rs2PlayerModel player: players) { if (!isAttackable(player, isDeadManWorld, isPVPWorld, wildernessLevel)) continue; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/security/LoginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/security/LoginManager.java index 7e9cf7fe1cf..1741328697b 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/security/LoginManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/security/LoginManager.java @@ -271,6 +271,12 @@ private static void submitLogin() { Rs2Keyboard.keyPress(KeyEvent.VK_ENTER); } + @com.google.common.annotations.VisibleForTesting + public static void submitLoginForTest() { + triggerLoginScreen(); + submitLogin(); + } + private static void handleBlockingDialogs(Client client) { if (client == null) { return; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java index e3e0b1e2d49..f21b03942be 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java @@ -7,6 +7,7 @@ import net.runelite.api.coords.WorldPoint; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.util.camera.Rs2Camera; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; import net.runelite.client.plugins.microbot.util.menu.NewMenuEntry; import net.runelite.client.plugins.microbot.util.misc.Rs2UiHelper; @@ -168,7 +169,7 @@ public ObjectComposition getObjectComposition() { * @return true if the interaction was successful, false otherwise */ public boolean click(String action) { - if (Microbot.getClient().getLocalPlayer().getWorldLocation().distanceTo(getWorldLocation()) > 51) { + if (Rs2Player.getWorldLocation().distanceTo(getWorldLocation()) > 51) { Microbot.log("Object with id " + getId() + " is not close enough to interact with. Walking to the object...."); Rs2Walker.walkTo(getWorldLocation()); return false; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2Walker.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2Walker.java index 5e97769a763..59fb51fcb3e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2Walker.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2Walker.java @@ -108,6 +108,9 @@ public static boolean walkTo(WorldPoint target, int distance) { return walkWithState(target, distance) == WalkerState.ARRIVED; } public static WalkerState walkWithState(WorldPoint target, int distance) { + if (config == null) { + return WalkerState.EXIT; + } boolean walkWithBankedTransports = config.walkWithBankedTransports(); if (walkWithBankedTransports){ return walkWithBankedTransportsAndState(target, distance,false); @@ -123,15 +126,22 @@ public static WalkerState walkWithState(WorldPoint target, int distance) { * @return */ private static WalkerState walkWithStateInternal(WorldPoint target, int distance) { - if (Rs2Tile.getReachableTilesFromTile(Rs2Player.getWorldLocation(), distance).containsKey(target) - || !Rs2Tile.isWalkable(LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), target)) && Rs2Player.getWorldLocation().distanceTo(target) <= distance) { + boolean reachableTileCheck = Rs2Tile.getReachableTilesFromTile(Rs2Player.getWorldLocation(), distance).containsKey(target); + LocalPoint localTarget = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), target); + boolean walkableCheck = Rs2Tile.isWalkable(localTarget); + int distToTarget = Rs2Player.getWorldLocation().distanceTo(target); + + if (reachableTileCheck || (!walkableCheck && distToTarget <= distance)) { return WalkerState.ARRIVED; } + final Pathfinder pathfinder = ShortestPathPlugin.getPathfinder(); - if (pathfinder != null && !pathfinder.isDone()) + if (pathfinder != null && !pathfinder.isDone()) { return WalkerState.MOVING; - if ((currentTarget != null && currentTarget.equals(target)) && ShortestPathPlugin.getMarker() != null) + } + if ((currentTarget != null && currentTarget.equals(target)) && ShortestPathPlugin.getMarker() != null) { return WalkerState.MOVING; + } setTarget(target); ShortestPathPlugin.setReachedDistance(distance); stuckCount = 0; @@ -141,17 +151,7 @@ private static WalkerState walkWithStateInternal(WorldPoint target, int distance return WalkerState.EXIT; } - /* - Close worldmap if it's open, this can happen when the walker is called from the panel or worldmap set target - */ closeWorldMap(); - /** - * When running the walkTo method from scripts - * the code will run on the script thread - * If you really like to run this on a seperate thread because you want to do - * other actions while walking you can wrap the walkTo from within the script - * on a seperate thread - */ return processWalk(target, distance); } @@ -172,7 +172,6 @@ public static WalkerState walkWithState(WorldPoint target) { */ private static WalkerState processWalk(WorldPoint target, int distance) { if (debug) { - log.info("Pathfinder in debug, exiting"); return WalkerState.EXIT; } try { @@ -180,7 +179,6 @@ private static WalkerState processWalk(WorldPoint target, int distance) { setTarget(null); } - // storing the reference ensures pathfinder cannot become null during Pathfinder pathfinder = ShortestPathPlugin.getPathfinder(); if (pathfinder == null) { if (ShortestPathPlugin.getMarker() == null) { @@ -188,7 +186,6 @@ private static WalkerState processWalk(WorldPoint target, int distance) { } pathfinder = sleepUntilNotNull(ShortestPathPlugin::getPathfinder, 2_000); if (pathfinder == null) { - log.error("Pathfinder took to long to initialize, exiting walker: 140"); setTarget(null); return WalkerState.EXIT; } @@ -197,14 +194,12 @@ private static WalkerState processWalk(WorldPoint target, int distance) { if (!pathfinder.isDone()) { boolean isDone = sleepUntilTrue(pathfinder::isDone, 100, 10_000); if (!isDone) { - log.error("Pathfinder took to long to calculate path, exiting: 149"); setTarget(null); return WalkerState.EXIT; } } if (ShortestPathPlugin.getMarker() == null) { - log.error("marker is null, exiting: 156"); setTarget(null); return WalkerState.EXIT; } @@ -212,14 +207,12 @@ private static WalkerState processWalk(WorldPoint target, int distance) { final List path = pathfinder.getPath(); final WorldPoint dst; if (path == null || path.isEmpty()) { - log.debug("Path is {}, using current location as destination", path == null ? "null" : "empty"); dst = Rs2Player.getWorldLocation(); } else { dst = path.get(path.size()-1); } if (dst == null || dst.distanceTo(target) > distance) { - log.warn("Location {} impossible to reach", dst); setTarget(null); return WalkerState.UNREACHABLE; } @@ -228,7 +221,8 @@ private static WalkerState processWalk(WorldPoint target, int distance) { return WalkerState.ARRIVED; } - if (isNear(dst)) { + boolean nearDst = isNear(dst); + if (nearDst) { setTarget(null); } @@ -244,9 +238,7 @@ private static WalkerState processWalk(WorldPoint target, int distance) { int indexOfStartPoint = getClosestTileIndex(path); if (indexOfStartPoint == -1) { - log.error("The walker is confused, unable to find our starting point in the web, exiting."); setTarget(null); - log.error("pathfinder is null, exiting: 255"); return WalkerState.EXIT; } @@ -342,11 +334,13 @@ private static WalkerState processWalk(WorldPoint target, int distance) { break; } - if (!Rs2Tile.isTileReachable(currentWorldPoint) && !Microbot.getClient().getTopLevelWorldView().isInstance()) { + boolean tileReachable = Rs2Tile.isTileReachable(currentWorldPoint); + if (!tileReachable && !inInstance) { continue; } nextWalkingDistance = Rs2Random.between(7, 11); - if (currentWorldPoint.distanceTo2D(Rs2Player.getWorldLocation()) > nextWalkingDistance) { + int dist2d = currentWorldPoint.distanceTo2D(Rs2Player.getWorldLocation()); + if (dist2d > nextWalkingDistance) { if (Microbot.getClient().getTopLevelWorldView().isInstance()) { if (Rs2Walker.walkMiniMap(currentWorldPoint)) { final WorldPoint b = currentWorldPoint; @@ -377,7 +371,8 @@ private static WalkerState processWalk(WorldPoint target, int distance) { } } - if (Rs2Player.getWorldLocation().distanceTo(target) < distance) { + int finalDist = Rs2Player.getWorldLocation().distanceTo(target); + if (finalDist < distance) { setTarget(null); return WalkerState.ARRIVED; } else { @@ -977,21 +972,21 @@ private static List applyTransportFiltering(List transport public static boolean isCloseToRegion(int distance, int regionX, int regionY) { - WorldPoint worldPoint = WorldPoint.fromRegion(Microbot.getClient().getLocalPlayer().getWorldLocation().getRegionID(), + WorldPoint worldPoint = WorldPoint.fromRegion(Rs2Player.getWorldLocation().getRegionID(), regionX, regionY, Microbot.getClient().getTopLevelWorldView().getPlane()); - return worldPoint.distanceTo(Microbot.getClient().getLocalPlayer().getWorldLocation()) < distance; + return worldPoint.distanceTo(Rs2Player.getWorldLocation()) < distance; } public static int distanceToRegion(int regionX, int regionY) { - WorldPoint worldPoint = WorldPoint.fromRegion(Microbot.getClient().getLocalPlayer().getWorldLocation().getRegionID(), + WorldPoint worldPoint = WorldPoint.fromRegion(Rs2Player.getWorldLocation().getRegionID(), regionX, regionY, Microbot.getClient().getTopLevelWorldView().getPlane()); - return worldPoint.distanceTo(Microbot.getClient().getLocalPlayer().getWorldLocation()); + return worldPoint.distanceTo(Rs2Player.getWorldLocation()); } private static boolean handleRockfall(List path, int index) { @@ -1003,7 +998,8 @@ private static boolean handleRockfall(List path, int index) { if(Microbot.getClient().getTopLevelWorldView().isInstance()) return false; // If we are not inside of the Motherloade mine, ignore the following logic - if (Rs2Player.getWorldLocation().getRegionID() != 14936 || currentTarget.getRegionID() != 14936) return false; + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + if (playerLoc == null || playerLoc.getRegionID() != 14936 || currentTarget == null || currentTarget.getRegionID() != 14936) return false; // We kill the path if no pickaxe is found to avoid walking around like an idiot if (!Rs2Inventory.hasItem("pickaxe")) { @@ -1083,7 +1079,8 @@ private static boolean handleDoors(List path, int index) { for (WorldPoint probe : probes) { boolean adjacentToPath = probe.distanceTo(fromWp) <= 1 || probe.distanceTo(toWp) <= 1; - if (!adjacentToPath || !Objects.equals(probe.getPlane(), Microbot.getClient().getLocalPlayer().getWorldLocation().getPlane())) continue; + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + if (!adjacentToPath || playerLoc == null || !Objects.equals(probe.getPlane(), playerLoc.getPlane())) continue; WallObject wall = Rs2GameObject.getWallObject(o -> o.getWorldLocation().equals(probe), probe, 3); @@ -1325,7 +1322,7 @@ public static void setTarget(WorldPoint target) { Microbot.getWorldMapPointManager().add(ShortestPathPlugin.getMarker()); WorldPoint start = Microbot.getClient().getTopLevelWorldView().isInstance() ? - WorldPoint.fromLocalInstance(Microbot.getClient(), localPlayer.getLocalLocation()) : localPlayer.getWorldLocation(); + WorldPoint.fromLocalInstance(Microbot.getClient(), Rs2Player.getLocalLocation()) : Rs2Player.getWorldLocation(); ShortestPathPlugin.setLastLocation(start); final Pathfinder pathfinder = ShortestPathPlugin.getPathfinder(); if (ShortestPathPlugin.isStartPointSet() && pathfinder != null) { @@ -1349,12 +1346,16 @@ public static boolean restartPathfinding(WorldPoint start, WorldPoint end) { } public static boolean restartPathfinding(WorldPoint start, Set ends) { - if (Microbot.getClient().isClientThread()) return false; + if (Microbot.getClient().isClientThread()) { + return false; + } Pathfinder pathfinder = ShortestPathPlugin.getPathfinder(); if (pathfinder != null) { pathfinder.cancel(); - ShortestPathPlugin.getPathfinderFuture().cancel(true); + if (ShortestPathPlugin.getPathfinderFuture() != null) { + ShortestPathPlugin.getPathfinderFuture().cancel(true); + } } if (ShortestPathPlugin.getPathfindingExecutor() == null) { @@ -1599,7 +1600,7 @@ private static boolean handleTransports(List path, int indexOfStartP TileObject object = objects.stream().findFirst().orElse(null); if (object instanceof GroundObject) { object = objects.stream() - .filter(o -> !Objects.equals(o.getWorldLocation(), Microbot.getClient().getLocalPlayer().getWorldLocation())) + .filter(o -> !Objects.equals(o.getWorldLocation(), Rs2Player.getWorldLocation())) .min(Comparator.comparing(o -> ((TileObject) o).getWorldLocation().distanceTo(transport.getOrigin())) .thenComparing(o -> ((TileObject) o).getWorldLocation().distanceTo(transport.getDestination()))).orElse(null); } @@ -1718,7 +1719,7 @@ private static boolean handleObjectExceptions(Transport transport, TileObject ti if (tileObject.getId() == ObjectID.BIGWEB_SLASHABLE && !Rs2Equipment.isWearing(ItemID.ARANEA_BOOTS)) { sleepUntil(() -> !Rs2Player.isMoving() && !Rs2Player.isAnimating(1200)); final WorldPoint webLocation = tileObject.getWorldLocation(); - final WorldPoint currentPlayerPoint = Microbot.getClient().getLocalPlayer().getWorldLocation(); + final WorldPoint currentPlayerPoint = Rs2Player.getWorldLocation(); boolean doesWebStillExist = Rs2GameObject.getAll(o -> Objects.equals(webLocation, o.getWorldLocation()) && o.getId() == ObjectID.BIGWEB_SLASHABLE).stream().findFirst().isPresent(); if (doesWebStillExist) { sleepUntil(() -> Rs2GameObject.getAll(o -> Objects.equals(webLocation, o.getWorldLocation()) && o.getId() == ObjectID.BIGWEB_SLASHABLE).stream().findFirst().isEmpty(), @@ -1728,7 +1729,7 @@ private static boolean handleObjectExceptions(Transport transport, TileObject ti }, 8000, 1200); } Rs2Walker.walkFastCanvas(transport.getDestination()); - return sleepUntil(() -> !Objects.equals(currentPlayerPoint, Microbot.getClient().getLocalPlayer().getWorldLocation())); + return sleepUntil(() -> !Objects.equals(currentPlayerPoint, Rs2Player.getWorldLocation())); } // Handle Brimhaven Dungeon Entrance diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/mta/telekinetic/TelekineticRoom.java b/runelite-client/src/main/java/net/runelite/client/plugins/mta/telekinetic/TelekineticRoom.java index aa93c849d68..adedaf9cf01 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/mta/telekinetic/TelekineticRoom.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/mta/telekinetic/TelekineticRoom.java @@ -48,6 +48,7 @@ import net.runelite.api.gameval.ObjectID; import net.runelite.client.eventbus.Subscribe; import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.mta.MTAConfig; import net.runelite.client.plugins.mta.MTARoom; @@ -274,7 +275,7 @@ else if (moves.peek() == getPosition()) public static WorldPoint optimal() { - WorldPoint current = Microbot.getClient().getLocalPlayer().getWorldLocation(); + WorldPoint current = Rs2Player.getWorldLocation(); Direction next = moves.pop(); WorldArea areaNext = getIndicatorLine(next); @@ -297,7 +298,7 @@ public static WorldPoint optimal() public static WorldPoint optimal(int index) { - WorldPoint current = Microbot.getClient().getLocalPlayer().getWorldLocation(); + WorldPoint current = Rs2Player.getWorldLocation(); Direction next = moves.get(moves.size() - 1 - index); WorldArea areaNext = getIndicatorLine(next); diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/api/actor/Rs2ActorModelIntegrationTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/api/actor/Rs2ActorModelIntegrationTest.java new file mode 100644 index 00000000000..205b7e2d87d --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/api/actor/Rs2ActorModelIntegrationTest.java @@ -0,0 +1,401 @@ +package net.runelite.client.plugins.microbot.api.actor; + +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.*; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.RuneLite; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.security.LoginManager; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; + +@Slf4j +public class Rs2ActorModelIntegrationTest { + + private static final int TARGET_WORLD = 382; + private static final long LOGGED_IN_SETTLE_MS = 5000; + private static final int MAX_RETRIES = 3; + private static final int MAX_LOGIN_ATTEMPTS = 5; + private static final int SCREENSHOT_ANALYZE_AFTER_ATTEMPT = 2; + + @BeforeClass + public static void startClient() throws Exception { + log.info("=== Starting RuneLite client for integration test ==="); + Thread clientThread = new Thread(() -> { + try { + RuneLite.main(new String[]{"--developer-mode"}); + } catch (Exception e) { + log.error("Failed to start RuneLite", e); + } + }, "RuneLite-Test-Launcher"); + clientThread.setDaemon(true); + clientThread.start(); + + log.info("Waiting for Microbot to fully initialize (clientThread injection)..."); + waitForCondition("Microbot.getClientThread() != null", 90, () -> + Microbot.getClientThread() != null + ); + log.info("Microbot initialized."); + + log.info("Waiting for login screen..."); + waitForCondition("Login screen", 90, () -> { + Client client = Microbot.getClient(); + return client != null && ( + client.getGameState() == GameState.LOGIN_SCREEN || + client.getGameState() == GameState.LOGGED_IN + ); + }); + + if (!Microbot.isLoggedIn()) { + log.info("On login screen. Will attempt login to world {}...", TARGET_WORLD); + Thread.sleep(5000); + performLoginWithWatchdog(); + } + + log.info("Logged in! Waiting {}ms for game state to settle...", LOGGED_IN_SETTLE_MS); + Thread.sleep(LOGGED_IN_SETTLE_MS); + log.info("=== Client ready for tests ==="); + } + + private static void performLoginWithWatchdog() throws Exception { + for (int attempt = 1; attempt <= MAX_LOGIN_ATTEMPTS; attempt++) { + log.info("Login attempt {}/{}...", attempt, MAX_LOGIN_ATTEMPTS); + + try { + LoginManager.setWorld(TARGET_WORLD); + Thread.sleep(1000); + LoginManager.submitLoginForTest(); + } catch (Exception e) { + log.warn("Login attempt {} threw: {}", attempt, e.getMessage()); + } + + boolean loggedIn = waitForConditionSafe("Login", 20, Microbot::isLoggedIn); + if (loggedIn) { + log.info("Login successful on attempt {}", attempt); + return; + } + + log.warn("Login attempt {} did not succeed.", attempt); + + if (attempt >= SCREENSHOT_ANALYZE_AFTER_ATTEMPT) { + log.info("Taking screenshot for Claude analysis..."); + Path screenshot = ScreenshotAnalyzer.captureScreenshot("login_stuck_attempt_" + attempt); + ScreenshotAnalyzer.AnalysisResult analysis = ScreenshotAnalyzer.analyzeStuckState( + screenshot, + "The integration test is stuck trying to log in. " + + "This is attempt " + attempt + " of " + MAX_LOGIN_ATTEMPTS + ". " + + "The test sets world " + TARGET_WORLD + " and presses Enter twice to submit login (Jagex account, no password). " + + "Game state: " + getGameStateString() + ); + + log.info("Claude suggests: {} - {}", analysis.action, analysis.explanation); + + switch (analysis.action) { + case ABORT: + throw new RuntimeException("Claude analysis recommends ABORT: " + analysis.explanation + + "\nFull analysis:\n" + analysis.rawResponse); + + case WAIT: + log.info("Claude says to wait. Waiting 10s before next attempt..."); + Thread.sleep(10000); + boolean loggedInAfterWait = waitForConditionSafe("Login after wait", 10, Microbot::isLoggedIn); + if (loggedInAfterWait) { + log.info("Login succeeded after waiting (Claude's advice worked)"); + return; + } + break; + + case RETRY_LOGIN: + case UNKNOWN: + default: + log.info("Retrying login in 5s..."); + Thread.sleep(5000); + break; + } + } else { + log.info("Retrying in 5s..."); + Thread.sleep(5000); + } + + if (attempt == MAX_LOGIN_ATTEMPTS) { + log.error("All login attempts exhausted. Taking final screenshot..."); + Path finalScreenshot = ScreenshotAnalyzer.captureScreenshot("login_final_failure"); + ScreenshotAnalyzer.AnalysisResult finalAnalysis = ScreenshotAnalyzer.analyzeStuckState( + finalScreenshot, + "FINAL FAILURE: All " + MAX_LOGIN_ATTEMPTS + " login attempts failed. " + + "Game state: " + getGameStateString() + ". " + + "Provide a detailed explanation of what's visible on screen and why login might be failing." + ); + throw new RuntimeException( + "Failed to login after " + MAX_LOGIN_ATTEMPTS + " attempts.\n" + + "Claude's final analysis: " + finalAnalysis.explanation + "\n" + + "Full response:\n" + finalAnalysis.rawResponse + ); + } + } + } + + private static String getGameStateString() { + try { + Client client = Microbot.getClient(); + if (client == null) return "CLIENT_NULL"; + GameState gs = client.getGameState(); + String extra = ""; + if (gs == GameState.LOGIN_SCREEN) { + extra = ", loginIndex=" + client.getLoginIndex(); + } + return gs.name() + extra; + } catch (Exception e) { + return "ERROR(" + e.getMessage() + ")"; + } + } + + @Test + public void testLocalPlayerGetWorldLocation() { + log.info("--- Test: Local Player getWorldLocation ---"); + + retryUntilSuccess("localPlayer.getWorldLocation()", () -> { + Player localPlayer = Microbot.getClient().getLocalPlayer(); + assertNotNull("Local player should not be null", localPlayer); + + Rs2ActorModel model = new Rs2ActorModel(localPlayer); + + WorldPoint location = model.getWorldLocation(); + assertNotNull("World location should not be null", location); + assertTrue("X coordinate should be positive", location.getX() > 0); + assertTrue("Y coordinate should be positive", location.getY() > 0); + + log.info(" Location: {}", location); + }); + } + + @Test + public void testLocalPlayerGetWorldView() { + log.info("--- Test: Local Player getWorldView ---"); + + retryUntilSuccess("localPlayer.getWorldView()", () -> { + Player localPlayer = Microbot.getClient().getLocalPlayer(); + assertNotNull("Local player should not be null", localPlayer); + + Rs2ActorModel model = new Rs2ActorModel(localPlayer); + + WorldView wv = model.getWorldView(); + assertNotNull("WorldView should not be null", wv); + log.info(" WorldView id={}, isTopLevel={}", wv.getId(), wv.isTopLevel()); + }); + } + + @Test + public void testLocalPlayerGetWorldLocationConsistency() { + log.info("--- Test: Location consistency across 10 rapid calls ---"); + + retryUntilSuccess("rapid getWorldLocation() calls", () -> { + Player localPlayer = Microbot.getClient().getLocalPlayer(); + assertNotNull("Local player should not be null", localPlayer); + + Rs2ActorModel model = new Rs2ActorModel(localPlayer); + List locations = new ArrayList<>(); + + for (int i = 0; i < 10; i++) { + WorldPoint loc = model.getWorldLocation(); + assertNotNull("Location call " + i + " should not be null", loc); + locations.add(loc); + } + + WorldPoint first = locations.get(0); + for (int i = 1; i < locations.size(); i++) { + int dist = first.distanceTo(locations.get(i)); + assertTrue("Rapid calls should return nearby locations (got distance " + dist + ")", dist <= 5); + } + + log.info(" All 10 calls returned consistent locations near {}", first); + }); + } + + @Test + public void testAllVisiblePlayersGetWorldLocation() { + log.info("--- Test: All visible players getWorldLocation ---"); + + retryUntilSuccess("visible players getWorldLocation()", () -> { + List players = Microbot.getClient().getPlayers(); + log.info(" Found {} visible players", players.size()); + assertTrue("Should see at least 1 player (self)", players.size() >= 1); + + int successCount = 0; + List errors = new ArrayList<>(); + + for (Player player : players) { + try { + Rs2ActorModel model = new Rs2ActorModel(player); + WorldPoint loc = model.getWorldLocation(); + assertNotNull("Location should not be null for player", loc); + successCount++; + } catch (Exception e) { + String name = "unknown"; + try { name = player.getName(); } catch (Exception ignored) {} + errors.add(name + ": " + e.getMessage()); + } + } + + log.info(" {}/{} players returned valid locations", successCount, players.size()); + if (!errors.isEmpty()) { + log.error(" Failures: {}", errors); + fail("Some players failed getWorldLocation(): " + errors); + } + }); + } + + @Test + public void testAllVisibleNpcsGetWorldLocation() { + log.info("--- Test: All visible NPCs getWorldLocation ---"); + + retryUntilSuccess("visible NPCs getWorldLocation()", () -> { + List npcs = Microbot.getClient().getNpcs(); + log.info(" Found {} visible NPCs", npcs.size()); + assertTrue("Should see at least 1 NPC", npcs.size() >= 1); + + int successCount = 0; + List errors = new ArrayList<>(); + + for (NPC npc : npcs) { + try { + Rs2ActorModel model = new Rs2ActorModel(npc); + WorldPoint loc = model.getWorldLocation(); + assertNotNull("Location should not be null for NPC", loc); + successCount++; + } catch (Exception e) { + String name = "unknown"; + try { name = npc.getName(); } catch (Exception ignored) {} + errors.add(name + ": " + e.getMessage()); + } + } + + log.info(" {}/{} NPCs returned valid locations", successCount, npcs.size()); + if (!errors.isEmpty()) { + log.error(" Failures: {}", errors); + fail("Some NPCs failed getWorldLocation(): " + errors); + } + }); + } + + @Test + public void testActorModelDelegationMethods() { + log.info("--- Test: Rs2ActorModel delegation methods ---"); + + retryUntilSuccess("delegation methods", () -> { + Player localPlayer = Microbot.getClient().getLocalPlayer(); + assertNotNull("Local player should not be null", localPlayer); + + Rs2ActorModel model = new Rs2ActorModel(localPlayer); + + WorldView wv = model.getWorldView(); + assertNotNull("getWorldView() should not be null", wv); + + WorldPoint loc = model.getWorldLocation(); + assertNotNull("getWorldLocation() should not be null", loc); + + int combatLevel = model.getCombatLevel(); + assertTrue("Combat level should be > 0", combatLevel > 0); + + String name = model.getName(); + assertNotNull("Player name should not be null", name); + assertFalse("Player name should not be empty", name.isEmpty()); + + int animation = model.getAnimation(); + + log.info(" name={}, combat={}, animation={}, location={}, worldViewId={}", + name, combatLevel, animation, loc, wv.getId()); + }); + } + + private void retryUntilSuccess(String testName, Runnable test) { + List failures = new ArrayList<>(); + + for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + test.run(); + if (attempt > 1) { + log.info(" [{}] Succeeded on attempt {}/{}", testName, attempt, MAX_RETRIES); + } + return; + } catch (Throwable t) { + failures.add(t); + log.warn(" [{}] Attempt {}/{} failed: {}", testName, attempt, MAX_RETRIES, t.getMessage()); + + if (attempt == MAX_RETRIES) { + log.info(" [{}] Final attempt failed, capturing screenshot for analysis...", testName); + Path screenshot = ScreenshotAnalyzer.captureScreenshot("test_failure_" + testName); + ScreenshotAnalyzer.AnalysisResult analysis = ScreenshotAnalyzer.analyzeStuckState( + screenshot, + "Test '" + testName + "' failed after " + MAX_RETRIES + " attempts. " + + "Last error: " + t.getMessage() + ". " + + "The test exercises Rs2ActorModel methods on a live RuneLite client. " + + "Game state: " + getGameStateString() + ); + log.info(" Claude analysis of failure: {}", analysis); + } + + if (attempt < MAX_RETRIES) { + try { + Thread.sleep(2000); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted during retry wait", ie); + } + } + } + } + + log.error(" [{}] All {} attempts failed", testName, MAX_RETRIES); + for (int i = 0; i < failures.size(); i++) { + log.error(" Attempt {} error:", i + 1, failures.get(i)); + } + throw new AssertionError( + testName + " failed after " + MAX_RETRIES + " attempts. Last error: " + failures.get(failures.size() - 1).getMessage(), + failures.get(failures.size() - 1) + ); + } + + private static void waitForCondition(String description, long timeoutSeconds, BooleanSupplier condition) throws InterruptedException { + if (!waitForConditionSafe(description, timeoutSeconds, condition)) { + log.error("Timed out waiting for '{}', capturing screenshot...", description); + Path screenshot = ScreenshotAnalyzer.captureScreenshot("timeout_" + description); + ScreenshotAnalyzer.AnalysisResult analysis = ScreenshotAnalyzer.analyzeStuckState( + screenshot, + "The test timed out waiting for condition: '" + description + "' after " + timeoutSeconds + "s. " + + "Game state: " + getGameStateString() + ); + throw new RuntimeException( + "Timed out waiting for: " + description + " (after " + timeoutSeconds + "s)\n" + + "Claude analysis: " + analysis.explanation + "\n" + + "Full response:\n" + analysis.rawResponse + ); + } + } + + private static boolean waitForConditionSafe(String description, long timeoutSeconds, BooleanSupplier condition) throws InterruptedException { + long deadline = System.currentTimeMillis() + (timeoutSeconds * 1000); + while (System.currentTimeMillis() < deadline) { + try { + if (condition.getAsBoolean()) { + return true; + } + } catch (Exception e) { + log.debug("Condition check for '{}' threw: {}", description, e.getMessage()); + } + Thread.sleep(500); + } + return false; + } + + @FunctionalInterface + private interface BooleanSupplier { + boolean getAsBoolean() throws Exception; + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/api/actor/ScreenshotAnalyzer.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/api/actor/ScreenshotAnalyzer.java new file mode 100644 index 00000000000..65ec025c964 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/api/actor/ScreenshotAnalyzer.java @@ -0,0 +1,188 @@ +package net.runelite.client.plugins.microbot.api.actor; + +import lombok.extern.slf4j.Slf4j; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class ScreenshotAnalyzer { + + private static final long CLAUDE_TIMEOUT_SECONDS = 60; + private static final Path SCREENSHOT_DIR = Path.of(System.getProperty("java.io.tmpdir"), "microbot-test-screenshots"); + + public enum SuggestedAction { + RETRY_LOGIN, + WAIT, + ABORT, + UNKNOWN + } + + public static class AnalysisResult { + public final SuggestedAction action; + public final String explanation; + public final String rawResponse; + + AnalysisResult(SuggestedAction action, String explanation, String rawResponse) { + this.action = action; + this.explanation = explanation; + this.rawResponse = rawResponse; + } + + @Override + public String toString() { + return "AnalysisResult{action=" + action + ", explanation='" + explanation + "'}"; + } + } + + public static Path captureScreenshot(String label) { + try { + Files.createDirectories(SCREENSHOT_DIR); + String filename = label.replaceAll("[^a-zA-Z0-9_-]", "_") + "_" + System.currentTimeMillis() + ".png"; + Path screenshotPath = SCREENSHOT_DIR.resolve(filename); + + Robot robot = new Robot(); + Rectangle screenRect = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize()); + BufferedImage capture = robot.createScreenCapture(screenRect); + ImageIO.write(capture, "png", screenshotPath.toFile()); + + log.info("Screenshot saved to: {}", screenshotPath); + return screenshotPath; + } catch (Exception e) { + log.error("Failed to capture screenshot", e); + return null; + } + } + + public static Path captureWindow(Window window, String label) { + try { + Files.createDirectories(SCREENSHOT_DIR); + String filename = label.replaceAll("[^a-zA-Z0-9_-]", "_") + "_" + System.currentTimeMillis() + ".png"; + Path screenshotPath = SCREENSHOT_DIR.resolve(filename); + + Robot robot = new Robot(); + Rectangle bounds = window.getBounds(); + BufferedImage capture = robot.createScreenCapture(bounds); + ImageIO.write(capture, "png", screenshotPath.toFile()); + + log.info("Window screenshot saved to: {}", screenshotPath); + return screenshotPath; + } catch (Exception e) { + log.error("Failed to capture window screenshot, falling back to full screen", e); + return captureScreenshot(label); + } + } + + public static AnalysisResult analyzeStuckState(Path screenshotPath, String context) { + if (screenshotPath == null || !Files.exists(screenshotPath)) { + log.warn("No screenshot available for analysis"); + return new AnalysisResult(SuggestedAction.RETRY_LOGIN, "No screenshot available", ""); + } + + String prompt = buildPrompt(context, screenshotPath); + + try { + log.info("Asking Claude CLI to analyze screenshot..."); + String response = invokeClaude(prompt); + + if (response == null || response.isEmpty()) { + log.warn("Empty response from Claude CLI"); + return new AnalysisResult(SuggestedAction.RETRY_LOGIN, "Empty response from Claude", ""); + } + + log.info("Claude analysis:\n{}", response); + return parseResponse(response); + + } catch (Exception e) { + log.error("Failed to invoke Claude CLI", e); + return new AnalysisResult(SuggestedAction.RETRY_LOGIN, "Claude CLI invocation failed: " + e.getMessage(), ""); + } + } + + private static String buildPrompt(String context, Path screenshotPath) { + return "You are analyzing a screenshot from a RuneLite (Old School RuneScape) client during an automated integration test.\n\n" + + "Context: " + context + "\n\n" + + "Look at the screenshot at: " + screenshotPath.toAbsolutePath() + "\n\n" + + "Based on what you see, determine the best course of action. Common scenarios:\n" + + "- Login screen visible: the test should retry clicking login (RETRY_LOGIN)\n" + + "- Game is loading/connecting: the test should wait (WAIT)\n" + + "- Error dialog or ban screen: the test should abort (ABORT)\n" + + "- Already logged in: the test should wait for state to propagate (WAIT)\n" + + "- World switcher confirmation dialog: the test should retry (RETRY_LOGIN)\n" + + "- Client crashed or frozen: the test should abort (ABORT)\n\n" + + "Respond with EXACTLY this format (3 lines, no markdown):\n" + + "ACTION: \n" + + "REASON: \n" + + "DETAIL: "; + } + + private static String invokeClaude(String prompt) throws IOException, InterruptedException { + ProcessBuilder pb = new ProcessBuilder( + "claude", + "-p", prompt, + "--allowedTools", "Read", + "--no-input" + ); + pb.redirectErrorStream(true); + pb.environment().put("TERM", "dumb"); + + Process process = pb.start(); + + StringBuilder output = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + } + + boolean finished = process.waitFor(CLAUDE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (!finished) { + process.destroyForcibly(); + log.warn("Claude CLI timed out after {}s", CLAUDE_TIMEOUT_SECONDS); + return null; + } + + int exitCode = process.exitValue(); + if (exitCode != 0) { + log.warn("Claude CLI exited with code {}: {}", exitCode, output); + } + + return output.toString().trim(); + } + + private static AnalysisResult parseResponse(String response) { + String upper = response.toUpperCase(); + + SuggestedAction action = SuggestedAction.UNKNOWN; + String reason = response; + + if (upper.contains("ACTION: RETRY_LOGIN") || upper.contains("ACTION:RETRY_LOGIN")) { + action = SuggestedAction.RETRY_LOGIN; + } else if (upper.contains("ACTION: WAIT") || upper.contains("ACTION:WAIT")) { + action = SuggestedAction.WAIT; + } else if (upper.contains("ACTION: ABORT") || upper.contains("ACTION:ABORT")) { + action = SuggestedAction.ABORT; + } else if (upper.contains("RETRY") || upper.contains("LOGIN")) { + action = SuggestedAction.RETRY_LOGIN; + } else if (upper.contains("WAIT") || upper.contains("LOADING")) { + action = SuggestedAction.WAIT; + } else if (upper.contains("ABORT") || upper.contains("ERROR") || upper.contains("BAN")) { + action = SuggestedAction.ABORT; + } + + for (String line : response.split("\n")) { + if (line.toUpperCase().startsWith("REASON:")) { + reason = line.substring("REASON:".length()).trim(); + break; + } + } + + return new AnalysisResult(action, reason, response); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerIntegrationTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerIntegrationTest.java new file mode 100644 index 00000000000..6fb2f57b55d --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerIntegrationTest.java @@ -0,0 +1,222 @@ +package net.runelite.client.plugins.microbot.util.walker; + +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.RuneLite; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.shortestpath.ShortestPathPlugin; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.security.LoginManager; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.Assert.*; + +@Slf4j +public class Rs2WalkerIntegrationTest { + + private static final int TARGET_WORLD = 382; + private static final long LOGGED_IN_SETTLE_MS = 5000; + private static final int MAX_LOGIN_ATTEMPTS = 5; + + @BeforeClass + public static void startClient() throws Exception { + log.info("=== Starting RuneLite client for Walker integration test ==="); + Thread clientThread = new Thread(() -> { + try { + RuneLite.main(new String[]{"--developer-mode"}); + } catch (Exception e) { + log.error("Failed to start RuneLite", e); + } + }, "RuneLite-Test-Launcher"); + clientThread.setDaemon(true); + clientThread.start(); + + log.info("Waiting for Microbot to fully initialize..."); + waitForCondition("Microbot.getClientThread() != null", 90, () -> + Microbot.getClientThread() != null + ); + log.info("Microbot initialized."); + + log.info("Waiting for login screen..."); + waitForCondition("Login screen", 90, () -> { + Client client = Microbot.getClient(); + return client != null && ( + client.getGameState() == GameState.LOGIN_SCREEN || + client.getGameState() == GameState.LOGGED_IN + ); + }); + + if (!Microbot.isLoggedIn()) { + log.info("On login screen. Will attempt login to world {}...", TARGET_WORLD); + Thread.sleep(5000); + performLogin(); + } + + log.info("Logged in! Waiting {}ms for game state to settle...", LOGGED_IN_SETTLE_MS); + Thread.sleep(LOGGED_IN_SETTLE_MS); + + log.info("Waiting for ShortestPathPlugin to initialize..."); + waitForCondition("ShortestPathPlugin config", 30, () -> + Rs2Walker.config != null + ); + log.info("ShortestPathPlugin config ready."); + + waitForCondition("PathfinderConfig", 30, () -> + ShortestPathPlugin.getPathfinderConfig() != null + && ShortestPathPlugin.getPathfinderConfig().getMap() != null + ); + log.info("PathfinderConfig and collision map ready."); + + log.info("=== Client ready for walker tests ==="); + } + + @Test + public void testWalkDoesNotCrashOnClientThread() throws Exception { + log.info("--- Test: Walker processes path without client thread errors ---"); + + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + log.info("Player location: {}", playerLoc); + assertNotNull("Player location should not be null", playerLoc); + + WorldPoint target = new WorldPoint(3222, 3218, 0); + if (playerLoc.distanceTo(target) <= 4) { + target = new WorldPoint(3207, 3210, 0); + } + + final WorldPoint walkTarget = target; + AtomicReference errorRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + + ExecutorService scriptExecutor = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "WalkerTest-Script"); + t.setDaemon(true); + return t; + }); + + log.info("Walking to {} (will let it run 15s to verify no client thread crashes)...", walkTarget); + scriptExecutor.submit(() -> { + try { + Rs2Walker.walkWithState(walkTarget, 4); + } catch (Throwable t) { + if (!(t.getCause() instanceof InterruptedException) + && !t.getMessage().contains("Interrupted")) { + log.error("Walker threw unexpected exception", t); + errorRef.set(t); + } + } finally { + latch.countDown(); + } + }); + + latch.await(15, TimeUnit.SECONDS); + scriptExecutor.shutdownNow(); + latch.await(5, TimeUnit.SECONDS); + + Throwable error = errorRef.get(); + if (error != null) { + log.error("Unexpected walker error: ", error); + } + assertNull("Walker should not throw client thread errors: " + error, error); + log.info("Walker ran for 15s without client thread errors - PASS"); + } + + @Test + public void testPathfinderCreationAndCompletion() throws Exception { + log.info("--- Test: Pathfinder creation and completion ---"); + + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + WorldPoint nearbyTarget = new WorldPoint(playerLoc.getX() + 10, playerLoc.getY() + 10, playerLoc.getPlane()); + log.info("Player at: {}, target: {}", playerLoc, nearbyTarget); + + Rs2Walker.setTarget(null); + Thread.sleep(500); + + log.info("Setting target..."); + Rs2Walker.setTarget(nearbyTarget); + + log.info("Waiting for pathfinder to be created..."); + long deadline = System.currentTimeMillis() + 5000; + while (ShortestPathPlugin.getPathfinder() == null && System.currentTimeMillis() < deadline) { + Thread.sleep(100); + } + assertNotNull("Pathfinder should be created after setTarget", ShortestPathPlugin.getPathfinder()); + log.info("Pathfinder created: {}", ShortestPathPlugin.getPathfinder()); + + log.info("Waiting for pathfinder.isDone()..."); + deadline = System.currentTimeMillis() + 15000; + while (!ShortestPathPlugin.getPathfinder().isDone() && System.currentTimeMillis() < deadline) { + Thread.sleep(100); + } + + boolean done = ShortestPathPlugin.getPathfinder().isDone(); + log.info("Pathfinder isDone: {}", done); + + if (done) { + var path = ShortestPathPlugin.getPathfinder().getPath(); + log.info("Path size: {}, first: {}, last: {}", + path != null ? path.size() : "null", + path != null && !path.isEmpty() ? path.get(0) : "N/A", + path != null && !path.isEmpty() ? path.get(path.size() - 1) : "N/A"); + } else { + log.error("Pathfinder did NOT complete within 15 seconds!"); + } + + Rs2Walker.setTarget(null); + + assertTrue("Pathfinder should complete within 15 seconds", done); + } + + private static void performLogin() throws Exception { + for (int attempt = 1; attempt <= MAX_LOGIN_ATTEMPTS; attempt++) { + log.info("Login attempt {}/{}...", attempt, MAX_LOGIN_ATTEMPTS); + try { + LoginManager.setWorld(TARGET_WORLD); + Thread.sleep(1000); + LoginManager.submitLoginForTest(); + } catch (Exception e) { + log.warn("Login attempt {} threw: {}", attempt, e.getMessage()); + } + + boolean loggedIn = waitForConditionSafe("Login", 20, Microbot::isLoggedIn); + if (loggedIn) { + log.info("Login successful on attempt {}", attempt); + return; + } + log.warn("Login attempt {} did not succeed.", attempt); + if (attempt < MAX_LOGIN_ATTEMPTS) { + Thread.sleep(5000); + } + } + throw new RuntimeException("Failed to login after " + MAX_LOGIN_ATTEMPTS + " attempts"); + } + + private static void waitForCondition(String description, long timeoutSeconds, BooleanSupplier condition) throws InterruptedException { + if (!waitForConditionSafe(description, timeoutSeconds, condition)) { + throw new RuntimeException("Timed out waiting for: " + description + " (after " + timeoutSeconds + "s)"); + } + } + + private static boolean waitForConditionSafe(String description, long timeoutSeconds, BooleanSupplier condition) throws InterruptedException { + long deadline = System.currentTimeMillis() + (timeoutSeconds * 1000); + while (System.currentTimeMillis() < deadline) { + try { + if (condition.getAsBoolean()) return true; + } catch (Exception e) { + log.debug("Condition check for '{}' threw: {}", description, e.getMessage()); + } + Thread.sleep(500); + } + return false; + } + + @FunctionalInterface + private interface BooleanSupplier { + boolean getAsBoolean() throws Exception; + } +}