diff --git a/docs/entity-guides/README.md b/docs/entity-guides/README.md index cbf145245c0..d36ac59febd 100644 --- a/docs/entity-guides/README.md +++ b/docs/entity-guides/README.md @@ -23,6 +23,12 @@ Route camera turns only for unreachable targets: [movement gotcha 23](movement.m Camera turns wait for the yaw to settle before returning: [movement gotcha 24](movement.md#24-a-camera-turn-is-finished-only-when-the-yaw-stops-changing). +Scene tile loops on a boat worldview must use the tile array bounds: [movement gotcha 25](movement.md#25-bound-scene-tile-loops-by-the-tile-array-not-constantsscene_size). + +Instance location conversions must null-check `LocalPoint.fromWorld`: [movement gotcha 26](movement.md#26-null-check-localpointfromworld-before-converting-instance-coordinates). + +Bounds-check a `LocalPoint` against its worldview before projecting it to the canvas: [movement gotcha 27](movement.md#27-bounds-check-a-localpoint-against-its-own-worldview-before-projecting-it). + ## Format Each entity guide is a numbered list of gotchas. Each entry follows this structure: diff --git a/docs/entity-guides/items.md b/docs/entity-guides/items.md index a68163fd997..bdecc1dae1b 100644 --- a/docs/entity-guides/items.md +++ b/docs/entity-guides/items.md @@ -274,6 +274,20 @@ An ordinary logout can be a break or world hop, so it must keep normal AutoLogin **Defensive check:** Run `AutoLoginSuppressionTest`, `AutoLoginPluginSupplyStopTest`, `ScriptStartedTest`, `TerminalSupplyLogoutTest`, and `InventorySetupSupplyLogoutTest`. Verify live that a shortage stays logged out and restarting a gameplay script permits login. +## 16. Bank widgets can be missing or blank right after the bank opens + +`BANK_ITEM_CONTAINER` can be null and the `BANK_ITEM_COUNT_TOP` text can be `""` while the bank interface is opening, closing or rebuilding. Never `assert` a widget non-null (asserts are disabled at runtime) and never `Integer.parseInt` widget text without handling blank or non-numeric input. `Rs2Bank.getBankItemCount()` returns 0 for unpopulated text, and `scrollBankToSlot` returns false before computing a scroll when the container is missing. The looting bag view (`15:11`) can be unloaded or have null/empty children once the bag closes, so `depositLootingBag` waits on `Rs2Bank.isLootingBagViewEmpty()`, which reads the widget on the client thread and treats a missing widget or children as emptied. + +**Defensive check:** Run `Rs2BankUnloadedWidgetsTest` and `Rs2BankLootingBagViewTest`. + + +## 17. An inventory container may have no child for the item's slot + +While a shop, bank, deposit box or Grand Exchange inventory is open or rebuilding, the active container's children may not include a widget whose index matches `Rs2ItemModel.getSlot()`. Look the child up with `findFirst().orElse(null)` and fall back to `rs2Item.getInventoryActions()`; never pass `null` to `Optional.orElseGet`, which throws an NPE instead of returning null. + +**Defensive check:** Run `Rs2InventoryInvokeMenuTest`. + + ## Terminal supply stops need observed logout An owning script can set `withMissingSupplyHandler` on its Inventory Setup loader to keep its scheduled loop alive while `TerminalLogoutPrompt` closes the bank and requests logout. Do not show a blocking shortage notification or cancel that loop until `LOGGED_OUT` is observed. The default loader policy stays unchanged when no handler is supplied; ordinary breaks and world hops do not arm terminal Auto Login suppression. diff --git a/docs/entity-guides/movement.md b/docs/entity-guides/movement.md index 1c2cf04ed37..d07e1bc4a6e 100644 --- a/docs/entity-guides/movement.md +++ b/docs/entity-guides/movement.md @@ -481,3 +481,70 @@ north and a pitch of 0 sends `adjustPitch` holding UP, so a fake 0 looked like a **Defensive check:** `Rs2CameraSettleTest` simulates a camera that coasts after release. It also covers the timeout bound and the client-thread no-op. + +## 25. Bound scene tile loops by the tile array, not `Constants.SCENE_SIZE` + +The local player's worldview is not always the 104x104 top-level scene. On a sailing boat +the player stands in a sub-worldview whose `Scene.getTiles()` is only 8 or 16 tiles wide. +A loop that runs `x, y < Constants.SCENE_SIZE` over that array throws +`ArrayIndexOutOfBoundsException` at the boat's width. + +**Why this matters:** `Rs2GameObject.getSceneObjects` read tiles from +`player.getWorldView().getScene()` and looped to `SCENE_SIZE`. Every scene-object query made +from a boat threw `Index 8 out of bounds for length 8`, which aborted the walker inside +`handleNearbyRawPathSceneObjects` and crashed banking in scripts that called `findBank`. + +**Pattern to follow:** take the loop bounds from the array you index (`tiles[z].length`, +`tiles[z][0].length`), or from `worldView.getSizeX()` / `getSizeY()`, whenever the tiles come +from a worldview that is not guaranteed to be the top-level one. + +**Defensive check:** `Rs2GameObjectSmallWorldViewTest` queries wall objects on an 8x8 +worldview and expects the object on the last tile. + +## 26. Null-check `LocalPoint.fromWorld` before converting instance coordinates + +`LocalPoint.fromWorld(worldView, worldPoint)` returns `null` when the plane differs from the world view's plane or the tile is outside that scene, for example in an instance while the scene is still loading. The fallback below only prevents the crash: if the player is on a boat sub-worldview, `player.getLocalLocation()` is in boat space and does not give the real sea position. `WorldPoint.fromLocalInstance` dereferences its `LocalPoint` immediately, so the result must be checked first. + +**Why this matters:** `Rs2Player.getWorldLocation_Internal()` passed the `fromWorld` result straight into `fromLocalInstance`. Telemetry for 2.6.30 and 2.6.31 recorded about 585 NullPointerExceptions, most from `KourendLibraryOverlay.render` calling `Rs2Player.getWorldLocation()` every frame. + +**Pattern to follow:** + +```java +// Wrong: NPE when the world point does not resolve in the top-level scene. +LocalPoint l = LocalPoint.fromWorld(client.getTopLevelWorldView(), player.getWorldLocation()); +return WorldPoint.fromLocalInstance(client, l); + +// Right: fall back to the player's own local point, then to the raw world point. +LocalPoint l = LocalPoint.fromWorld(client.getTopLevelWorldView(), player.getWorldLocation()); +if (l == null) l = player.getLocalLocation(); +if (l == null) return player.getWorldLocation(); +return WorldPoint.fromLocalInstance(client, l); +``` + +**Where this applies:** `Rs2Player.getWorldLocation_Internal()` and any helper that converts an actor's world point to an instance template point. + +**Defensive check:** Mock a world view whose plane differs from the player's world point and assert the helper returns a fallback point, as in `Rs2PlayerInstanceWorldLocationTest`. + +## 27. Bounds-check a `LocalPoint` against its own worldview before projecting it + +`Perspective.getCanvasTilePoly` only rejects points outside the extended scene (`getSizeX() + 2 * ESCENE_OFFSET` on the top-level view). A tile with scene X/Y in 104..143, or a negative one, still reaches `WorldView.getTileHeight`, which indexes the normal scene height array and throws `ArrayIndexOutOfBoundsException`. Resolve the worldview from `localPoint.getWorldView()` and check the scene X/Y against `0..getSizeX()-1` / `0..getSizeY()-1` before projecting. + +**Why this matters:** `Rs2Camera.isTileOnScreen` passed NPC and tile-object local points straight to `getCanvasTilePoly`. Telemetry for 2.6.30 to 2.6.32 recorded `Index 109/111/126 out of bounds for length 105` from `Rs2NpcModel.click` and `Rs2TileObjectModel.click`, which aborted the interaction instead of turning the camera. + +**Pattern to follow:** + +```java +// Wrong: out-of-scene points throw inside Perspective. +Polygon poly = Perspective.getCanvasTilePoly(client, localPoint); + +// Right: treat a point outside its worldview's scene as off screen. +if (localPoint == null) return false; +WorldView wv = client.getWorldView(localPoint.getWorldView()); +if (wv == null || localPoint.getSceneX() < 0 || localPoint.getSceneY() < 0 + || localPoint.getSceneX() >= wv.getSizeX() || localPoint.getSceneY() >= wv.getSizeY()) return false; +Polygon poly = Perspective.getCanvasTilePoly(client, localPoint); +``` + +**Where this applies:** `Rs2Camera.isTileOnScreen(LocalPoint)`, `Rs2Camera.isTileOnScreen(TileObject)`, and any helper that projects an entity's local point to the canvas. + +**Defensive check:** Mock a worldview whose `getTileHeight` throws and assert out-of-scene points return false without calling it, as in `Rs2CameraTileOnScreenTest`. diff --git a/docs/entity-guides/worlds.md b/docs/entity-guides/worlds.md index b4a4ec3b824..b1a7eb6efc2 100644 --- a/docs/entity-guides/worlds.md +++ b/docs/entity-guides/worlds.md @@ -41,10 +41,10 @@ return Microbot.getClientThread().runOnClientThreadOptional(() -> { }).orElse(-1); ``` -**Where this applies:** `Rs2Player` accessors (`getWorldLocation_Internal`, `getPoseAnimation`, `getAnimation`, `getCombatLevel`, `getLocalPlayer`, `getLocalLocation`, `getGraphicId`, `hasSpotAnimation`), the position accessors of `Rs2ActorModel` (`getWorldView`, `getWorldLocation`, `getLocalLocation`, `projectActorLocationToMainWorld`) and its `getAnimation` (returns -1), which `new Rs2PlayerModel()` reaches with a null actor, and any new helper that reads the local player. +**Where this applies:** `Rs2Player` accessors (`getWorldLocation_Internal`, `getPoseAnimation`, `getAnimation`, `getCombatLevel`, `getLocalPlayer`, `getLocalLocation`, `getGraphicId`, `hasSpotAnimation`), the position accessors of `Rs2ActorModel` (`getWorldView`, `getWorldLocation`, `getLocalLocation`, `projectActorLocationToMainWorld`) and its `getAnimation` (returns -1), which `new Rs2PlayerModel()` reaches with a null actor, the callers of those accessors (`Rs2Camera.angleToTile` returns the current facing so a turn is a no-op), both `Rs2NpcModel` classes' `getDistanceFromPlayer` (returns `Integer.MAX_VALUE`) and `isWithinDistanceFromPlayer` (returns `false`), `Rs2GameObject.clickObject` (returns `false` when it falls back to world-point distance), and any new helper that reads the local player. -**Defensive check:** Mock `Client.getLocalPlayer()` to return `null` and assert each accessor returns its fallback, as in `Rs2PlayerNullLocalPlayerTest` and `Rs2PlayerModelNullLocalPlayerTest`. +**Defensive check:** Mock `Client.getLocalPlayer()` to return `null` and assert each accessor returns its fallback, as in `Rs2PlayerNullLocalPlayerTest`, `Rs2PlayerModelNullLocalPlayerTest` and `Rs2NpcModelDistanceFromPlayerTest`. **Wrapped locations:** `Rs2Player.getRs2WorldPoint()` still returns an `Rs2WorldPoint`, but it can wrap a `null` `WorldPoint` in these states. `pathTo` returns `null` and `distanceToPath` returns `Integer.MAX_VALUE` for it; check `getWorldPoint() != null` before calling `getX()`, `getY()` or `getPlane()`, as in `Rs2WorldPointNullPointTest`. -**Location consumers:** `Rs2Player.getWorldLocation()` itself returns `null` in these states. Guard it before passing it to `Rs2RouteRequest.to`/`toAny` (which reject a null start) or `WorldPoint.distanceTo`. `Rs2Bank.getNearestBank`, `getNearestBankRoute` and `getPathToNearestBank` return no bank, `isNearBank` returns `false`, and `Rs2Walker.getDistanceBetween` returns `Integer.MAX_VALUE` for a null endpoint, as in `Rs2BankNullPlayerLocationTest`. +**Location consumers:** `Rs2Player.getWorldLocation()` itself returns `null` in these states. Guard it before passing it to `Rs2RouteRequest.to`/`toAny` (which reject a null start) or `WorldPoint.distanceTo`. `Rs2Bank.getNearestBank`, `getNearestBankRoute` and `getPathToNearestBank` return no bank, `isNearBank` and `walkToBank` return `false`, `BankLocation.MINING_GUILD.hasRequirements` returns `false`, and `Rs2Walker.getDistanceBetween` returns `Integer.MAX_VALUE` for a null endpoint, as in `Rs2BankNullPlayerLocationTest`. diff --git a/gradle.properties b/gradle.properties index 6448772f354..5856373f7c6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -32,7 +32,7 @@ project.build.version=1.13.2 runelite.injected-client.version=1.13.2 glslang.path= -microbot.version=2.6.33 +microbot.version=2.6.34 microbot.commit.sha=nogit microbot.repo.url=http://138.201.81.246:8081/repository/microbot-snapshot/ microbot.repo.username= diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java index aaffe78ba3d..603b7111fa1 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java @@ -29,6 +29,7 @@ import com.google.common.base.Strings; import com.google.common.collect.ComparisonChain; import com.google.gson.Gson; +import com.google.gson.JsonParseException; import lombok.Getter; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; @@ -1259,7 +1260,7 @@ public void setDefaultConfiguration(T proxy, boolean override // null and the empty string are treated identically in sendConfig and treated as an unset // If a config value defaults to "" and the current value is null, it will cause an extra // unset to be sent, so treat them as equal - boolean isInventorySetup = item.keyName().toLowerCase().contains("inventorysetup"); + boolean isInventorySetup = method.getReturnType() == InventorySetup.class; if (Objects.equals(current, valueString) || (Strings.isNullOrEmpty(current) && Strings.isNullOrEmpty(valueString))) { if (isInventorySetup) { @@ -1274,9 +1275,16 @@ public void setDefaultConfiguration(T proxy, boolean override if (current != null && !current.isBlank()) { if (isInventorySetup) { var setups = configInventorySetupDataManager.loadV3Setups(this); - InventorySetup currentObj = gson.fromJson(current, InventorySetup.class); + InventorySetup currentObj; + try { + currentObj = gson.fromJson(current, InventorySetup.class); + } catch (JsonParseException | IllegalStateException e) { + log.debug("Unable to parse inventory setup for {}.{}", group.value(), item.keyName(), e); + currentObj = null; + } + String currentName = currentObj == null ? null : currentObj.getName(); if (!setups.isEmpty() && setups.stream() - .noneMatch(x -> x.getName().equalsIgnoreCase(currentObj.getName()))) { + .noneMatch(x -> x.getName().equalsIgnoreCase(currentName))) { setConfiguration(group.value(), item.keyName(), ""); } } diff --git a/runelite-client/src/main/java/net/runelite/client/config/ProfileManager.java b/runelite-client/src/main/java/net/runelite/client/config/ProfileManager.java index 91895b054e1..3c5e8852d76 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/ProfileManager.java +++ b/runelite-client/src/main/java/net/runelite/client/config/ProfileManager.java @@ -43,6 +43,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.Predicate; import javax.inject.Inject; import javax.inject.Singleton; @@ -57,6 +58,7 @@ public class ProfileManager { private static final File PROFILES_DIR = new File(RuneLite.RUNELITE_DIR, "microbot-profiles"); private static final File PROFILES = new File(PROFILES_DIR, "profiles.json"); + private static final ReentrantLock PROCESS_LOCK = new ReentrantLock(); private final Gson gson; @@ -73,11 +75,26 @@ public class Lock implements AutoCloseable { @SneakyThrows public Lock() { - lockFile = new File(PROFILES_DIR, "profiles.lck"); - lockOut = new FileOutputStream(lockFile); - lockChannel = lockOut.getChannel(); - lockChannel.lock(); - profiles = new ArrayList<>(load()); + PROCESS_LOCK.lock(); + FileOutputStream out = null; + try { + lockFile = new File(PROFILES_DIR, "profiles.lck"); + out = new FileOutputStream(lockFile); + out.getChannel().lock(); + lockOut = out; + lockChannel = out.getChannel(); + profiles = new ArrayList<>(load()); + } catch (Throwable t) { + if (out != null) { + try { + out.close(); + } catch (IOException e) { + t.addSuppressed(e); + } + } + PROCESS_LOCK.unlock(); + throw t; + } } private List load() { @@ -97,6 +114,19 @@ private List load() { @Override @SneakyThrows public void close() { + try { + save(); + } finally { + try { + lockOut.close(); + lockFile.delete(); + } finally { + PROCESS_LOCK.unlock(); + } + } + } + + private void save() throws IOException { if (modified) { log.debug("saving {} profiles", profiles.size()); @@ -119,9 +149,6 @@ public void close() { Files.move(tempFile.toPath(), PROFILES.toPath(), StandardCopyOption.REPLACE_EXISTING); } } - - lockOut.close(); - lockFile.delete(); } public List getProfiles() { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsPlugin.java index ac5fa38dd95..c98208a2b6b 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsPlugin.java @@ -190,7 +190,10 @@ static class PriceHighlight private final Map lootbeams = new HashMap<>(); public static Table getCollectedGroundItems() { - return ImmutableTable.copyOf(collectedGroundItems); + synchronized (collectedGroundItems) + { + return ImmutableTable.copyOf(collectedGroundItems); + } } @Provides diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/BlockingEventManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/BlockingEventManager.java index d7e19d3e1ad..8e8b7a2b305 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/BlockingEventManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/BlockingEventManager.java @@ -111,7 +111,7 @@ private void loopOnce() { try { validateAndEnqueueWithBackoff(); } catch (Throwable t) { - Microbot.log(Level.ERROR, "BlockingEvent loop error: %s", t); + log.error("BlockingEvent loop error", t); } finally { // re-schedule using the latest currentDelay (volatile) loopFuture = scheduler.schedule(this::loopOnce, currentDelay, TimeUnit.MILLISECONDS); @@ -138,10 +138,7 @@ private void validateAndEnqueueWithBackoff() { } } } catch (Exception ex) { - Microbot.log(Level.ERROR, - "Error validating BlockingEvent (%s): %s", - event.getName(), - ex); + Microbot.log("Error validating BlockingEvent (" + event.getName() + ")", Level.ERROR, ex); } } } @@ -194,10 +191,7 @@ public boolean shouldBlockAndProcess() } catch (Exception ex) { - Microbot.log(Level.ERROR, - "Error executing BlockingEvent (%s): %s", - event.getName(), - ex); + Microbot.log("Error executing BlockingEvent (" + event.getName() + ")", Level.ERROR, ex); } finally { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/accountselector/AutoLoginScript.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/accountselector/AutoLoginScript.java index 97c53740b27..f205d5f476d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/accountselector/AutoLoginScript.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/accountselector/AutoLoginScript.java @@ -480,8 +480,9 @@ private void updatePlayerNameCache() { // detect fresh login - update player name cache if (currentlyLoggedIn && !wasLoggedIn) { Rs2PlayerModel localPlayer = Rs2Player.getLocalPlayer(); - if (localPlayer != null && localPlayer.getName() != null) { - lastKnownPlayerName = localPlayer.getName(); + String playerName = localPlayer != null ? localPlayer.getName() : null; + if (playerName != null) { + lastKnownPlayerName = playerName; log.info("Updated cached player name: {}", lastKnownPlayerName); } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/AgentHandler.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/AgentHandler.java index a698341e40a..0b5f0c2adb5 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/AgentHandler.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/AgentHandler.java @@ -1,6 +1,7 @@ package net.runelite.client.plugins.microbot.agentserver.handler; import com.google.gson.Gson; +import com.google.gson.JsonParseException; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import lombok.extern.slf4j.Slf4j; @@ -65,20 +66,27 @@ public void handle(HttpExchange exchange) throws IOException { try { handleRequest(exchange); + } catch (BadRequestException e) { + log.debug("Bad request for {}: {}", exchange.getRequestURI(), e.getMessage()); + sendErrorResponse(exchange, 400, e.getMessage()); } catch (Exception e) { if (isClientDisconnect(e)) { log.debug("Client disconnected before {} response could be sent", exchange.getRequestURI()); return; } log.error("Error handling {}", exchange.getRequestURI(), e); - try { - sendJson(exchange, 500, errorResponse("Internal server error: " + e.getMessage())); - } catch (IOException ioe) { - if (!isClientDisconnect(ioe)) { - throw ioe; - } - log.debug("Client disconnected before error response could be sent for {}", exchange.getRequestURI()); + sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); + } + } + + private void sendErrorResponse(HttpExchange exchange, int statusCode, String message) throws IOException { + try { + sendJson(exchange, statusCode, errorResponse(message)); + } catch (IOException ioe) { + if (!isClientDisconnect(ioe)) { + throw ioe; } + log.debug("Client disconnected before error response could be sent for {}", exchange.getRequestURI()); } } @@ -108,6 +116,9 @@ private static boolean constantTimeEquals(String a, String b) { public static boolean isClientDisconnect(Throwable t) { while (t != null) { + if (t instanceof ClientDisconnectException) { + return true; + } if (t instanceof java.io.IOException) { String msg = t.getMessage(); if (msg != null) { @@ -115,6 +126,8 @@ public static boolean isClientDisconnect(Throwable t) { if (lower.contains("broken pipe") || lower.contains("connection reset") || lower.contains("connection closed") + || lower.contains("connection was aborted") + || lower.contains("forcibly closed") || lower.contains("insufficient bytes written")) { return true; } @@ -142,8 +155,14 @@ protected void sendJson(HttpExchange exchange, int statusCode, Object body) thro byte[] bytes = json.getBytes(StandardCharsets.UTF_8); exchange.getResponseHeaders().set("Content-Type", "application/json"); exchange.sendResponseHeaders(statusCode, bytes.length); + writeBody(exchange, bytes); + } + + protected static void writeBody(HttpExchange exchange, byte[] bytes) throws IOException { try (OutputStream os = exchange.getResponseBody()) { os.write(bytes); + } catch (IOException e) { + throw new ClientDisconnectException(e); } } @@ -154,9 +173,7 @@ protected void sendOpaqueNotFound(HttpExchange exchange) throws IOException { byte[] bytes = "Not Found".getBytes(StandardCharsets.UTF_8); exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8"); exchange.sendResponseHeaders(404, bytes.length); - try (OutputStream os = exchange.getResponseBody()) { - os.write(bytes); - } + writeBody(exchange, bytes); } protected Map errorResponse(String message) { @@ -180,25 +197,62 @@ protected Map parseQuery(URI uri) { return params; } + @SuppressWarnings("unchecked") protected Map readJsonBody(HttpExchange exchange) throws IOException { String cl = exchange.getRequestHeaders().getFirst("Content-Length"); if (cl != null) { try { long advertised = Long.parseLong(cl.trim()); if (advertised > MAX_BODY_SIZE) { - throw new IllegalArgumentException("Request body too large"); + throw new BadRequestException("Request body too large"); } } catch (NumberFormatException ignored) { } } byte[] bodyBytes = exchange.getRequestBody().readNBytes(MAX_BODY_SIZE + 1); if (bodyBytes.length > MAX_BODY_SIZE) { - throw new IllegalArgumentException("Request body too large"); + throw new BadRequestException("Request body too large"); } String body = new String(bodyBytes, StandardCharsets.UTF_8); - @SuppressWarnings("unchecked") - Map parsed = gson.fromJson(body, LinkedHashMap.class); - return parsed; + Map parsed; + try { + parsed = gson.fromJson(body, LinkedHashMap.class); + } catch (JsonParseException e) { + throw new BadRequestException("Invalid JSON body"); + } + return parsed != null ? parsed : new LinkedHashMap<>(); + } + + protected static Integer getIntField(Map body, String key) { + Object val = body.get(key); + if (val == null) return null; + if (val instanceof Number) return ((Number) val).intValue(); + if (val instanceof String) { + try { + return Integer.parseInt(((String) val).trim()); + } catch (NumberFormatException ignored) { + } + } + throw new BadRequestException("Field '" + key + "' must be a number"); + } + + protected static String getStringField(Map body, String key) { + Object val = body.get(key); + if (val == null) return null; + if (val instanceof String) return (String) val; + throw new BadRequestException("Field '" + key + "' must be a string"); + } + + protected static Boolean getBooleanField(Map body, String key) { + Object val = body.get(key); + if (val == null) return null; + if (val instanceof Boolean) return (Boolean) val; + if (val instanceof String) { + String s = ((String) val).trim(); + if ("true".equalsIgnoreCase(s)) return Boolean.TRUE; + if ("false".equalsIgnoreCase(s)) return Boolean.FALSE; + } + throw new BadRequestException("Field '" + key + "' must be a boolean"); } protected int getIntParam(Map params, String key, int defaultValue) { @@ -231,6 +285,18 @@ protected String getSubPath(HttpExchange exchange, String basePath) { return full.substring(basePath.length()); } + public static final class ClientDisconnectException extends IOException { + public ClientDisconnectException(IOException cause) { + super(cause.getMessage(), cause); + } + } + + public static class BadRequestException extends IllegalArgumentException { + public BadRequestException(String message) { + super(message); + } + } + public static class HttpMethodException extends Exception { public HttpMethodException(String message) { super(message); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/BankHandler.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/BankHandler.java index a53b18764b4..45939a75815 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/BankHandler.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/BankHandler.java @@ -122,8 +122,8 @@ private void handleDeposit(HttpExchange exchange) throws IOException { return; } - Boolean all = (Boolean) body.get("all"); - String name = (String) body.get("name"); + Boolean all = getBooleanField(body, "all"); + String name = getStringField(body, "name"); boolean success; if (Boolean.TRUE.equals(all) && (name == null || name.isEmpty())) { @@ -157,9 +157,9 @@ private void handleWithdraw(HttpExchange exchange) throws IOException { return; } - String name = (String) body.get("name"); - Number quantityNum = (Number) body.get("quantity"); - Number idNum = (Number) body.get("id"); + String name = getStringField(body, "name"); + Integer quantityNum = getIntField(body, "quantity"); + Integer idNum = getIntField(body, "id"); if (name == null && idNum == null) { sendJson(exchange, 400, errorResponse("Provide either name or id")); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/DialogueHandler.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/DialogueHandler.java index f5b02ac2b58..c02f09c3df2 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/DialogueHandler.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/DialogueHandler.java @@ -119,8 +119,8 @@ private void handleSelect(HttpExchange exchange) throws IOException { return; } - String option = (String) body.get("option"); - Number indexNum = (Number) body.get("index"); + String option = getStringField(body, "option"); + Integer indexNum = getIntField(body, "index"); Map response = new LinkedHashMap<>(); boolean success; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/GroundItemHandler.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/GroundItemHandler.java index cfa930a2413..e6ce5fba011 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/GroundItemHandler.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/GroundItemHandler.java @@ -97,8 +97,8 @@ private void handlePickup(HttpExchange exchange) throws IOException { return; } - String name = (String) body.get("name"); - Number idNum = (Number) body.get("id"); + String name = getStringField(body, "name"); + Integer idNum = getIntField(body, "id"); Rs2TileItemModel item = null; if (name != null && !name.isEmpty()) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/InventoryHandler.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/InventoryHandler.java index 123d067e28d..dd1876fd160 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/InventoryHandler.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/InventoryHandler.java @@ -77,14 +77,14 @@ private void handleInteract(HttpExchange exchange) throws IOException { return; } - String action = (String) body.get("action"); + String action = getStringField(body, "action"); if (action == null || action.isEmpty()) { sendJson(exchange, 400, errorResponse("Missing required field: action")); return; } - String name = (String) body.get("name"); - Number idNum = (Number) body.get("id"); + String name = getStringField(body, "name"); + Integer idNum = getIntField(body, "id"); boolean success; if (name != null && !name.isEmpty()) { @@ -120,13 +120,13 @@ private void handleDrop(HttpExchange exchange) throws IOException { return; } - String name = (String) body.get("name"); + String name = getStringField(body, "name"); if (name == null || name.isEmpty()) { sendJson(exchange, 400, errorResponse("Missing required field: name")); return; } - Boolean all = (Boolean) body.get("all"); + Boolean all = getBooleanField(body, "all"); boolean success; if (Boolean.TRUE.equals(all)) { success = Rs2Inventory.dropAll(name); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/KeyboardHandler.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/KeyboardHandler.java index cdcc371de08..33a98a9d0c7 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/KeyboardHandler.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/KeyboardHandler.java @@ -37,8 +37,8 @@ protected void handleRequest(HttpExchange exchange) throws IOException { return; } - String text = (String) request.get("text"); - String key = (String) request.get("key"); + String text = getStringField(request, "text"); + String key = getStringField(request, "key"); if (text == null && key == null) { sendJson(exchange, 400, errorResponse("Required: text (string to type) or key (enter/escape/backspace)")); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/LoginHandler.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/LoginHandler.java index 252b1c10cec..a22936694ad 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/LoginHandler.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/LoginHandler.java @@ -140,14 +140,16 @@ private void handleLogin(HttpExchange exchange) throws IOException { int timeoutSeconds = DEFAULT_TIMEOUT_SECONDS; if (body != null) { - if (body.containsKey("world")) { - targetWorld = ((Number) body.get("world")).intValue(); + Integer world = getIntField(body, "world"); + if (world != null) { + targetWorld = world; } if (body.containsKey("wait")) { - wait = Boolean.TRUE.equals(body.get("wait")); + wait = Boolean.TRUE.equals(getBooleanField(body, "wait")); } - if (body.containsKey("timeout")) { - timeoutSeconds = Math.min(((Number) body.get("timeout")).intValue(), MAX_TIMEOUT_SECONDS); + Integer timeout = getIntField(body, "timeout"); + if (timeout != null) { + timeoutSeconds = Math.min(timeout, MAX_TIMEOUT_SECONDS); if (timeoutSeconds <= 0) timeoutSeconds = DEFAULT_TIMEOUT_SECONDS; } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/LogoutHandler.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/LogoutHandler.java index e487e5260ff..e628ac6b634 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/LogoutHandler.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/LogoutHandler.java @@ -68,7 +68,7 @@ private void handleLogout(HttpExchange exchange) throws IOException { if (body != null) { if (body.containsKey("wait")) { - wait = Boolean.TRUE.equals(body.get("wait")); + wait = Boolean.TRUE.equals(getBooleanField(body, "wait")); } if (body.containsKey("timeout")) { Object raw = body.get("timeout"); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/NpcHandler.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/NpcHandler.java index c3171a2100f..b8d3c537c7b 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/NpcHandler.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/NpcHandler.java @@ -97,14 +97,14 @@ private void handleInteract(HttpExchange exchange) throws IOException { return; } - String action = (String) body.get("action"); + String action = getStringField(body, "action"); if (action == null || action.isEmpty()) { sendJson(exchange, 400, errorResponse("Missing required field: action")); return; } - String name = (String) body.get("name"); - Number idNum = (Number) body.get("id"); + String name = getStringField(body, "name"); + Integer idNum = getIntField(body, "id"); Rs2NpcModel npc = null; if (name != null && !name.isEmpty()) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/ObjectHandler.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/ObjectHandler.java index e19fb0b08c5..be267834e5e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/ObjectHandler.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/ObjectHandler.java @@ -97,14 +97,14 @@ private void handleInteract(HttpExchange exchange) throws IOException { return; } - String action = (String) body.get("action"); + String action = getStringField(body, "action"); if (action == null || action.isEmpty()) { sendJson(exchange, 400, errorResponse("Missing required field: action")); return; } - String name = (String) body.get("name"); - Number idNum = (Number) body.get("id"); + String name = getStringField(body, "name"); + Integer idNum = getIntField(body, "id"); Rs2TileObjectModel obj = null; if (name != null && !name.isEmpty()) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/QuestHelperHandler.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/QuestHelperHandler.java index c13da797e08..2f7114eb9db 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/QuestHelperHandler.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/QuestHelperHandler.java @@ -123,7 +123,7 @@ private void handleStart(HttpExchange exchange) throws IOException } Map body = readJsonBody(exchange); - String name = (String) body.get("name"); + String name = getStringField(body, "name"); if (name == null || name.isEmpty()) { sendJson(exchange, 400, errorResponse("name is required")); @@ -137,9 +137,16 @@ private void handleStart(HttpExchange exchange) throws IOException return; } + PluginManager pluginManager = Microbot.getPluginManager(); + if (pluginManager == null || !pluginManager.isActive(plugin)) + { + sendJson(exchange, 409, errorResponse("Quest Helper plugin is not active")); + return; + } + if (!plugin.startQuestHelper(name)) { - sendJson(exchange, 404, errorResponse("Quest helper not found: " + name)); + sendJson(exchange, 404, errorResponse("Quest helper not found or not loaded: " + name)); return; } @@ -173,7 +180,18 @@ private void handleStop(HttpExchange exchange) throws IOException return; } - plugin.getQuestManager().shutDownQuest(true); + boolean stopped = Microbot.getClientThread() + .runOnClientThreadOptional(() -> + { + plugin.getQuestManager().shutDownQuest(true); + return true; + }) + .orElse(false); + if (!stopped) + { + sendJson(exchange, 500, errorResponse("Failed to stop quest helper")); + return; + } Map result = new LinkedHashMap<>(); result.put("success", true); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/ScreenshotHandler.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/ScreenshotHandler.java index 712662fbac8..9751c85da5a 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/ScreenshotHandler.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/ScreenshotHandler.java @@ -12,7 +12,6 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import java.io.OutputStream; import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.CountDownLatch; @@ -76,9 +75,7 @@ protected void handleRequest(HttpExchange exchange) throws IOException { byte[] bytes = baos.toByteArray(); exchange.getResponseHeaders().set("Content-Type", "image/png"); exchange.sendResponseHeaders(200, bytes.length); - try (OutputStream os = exchange.getResponseBody()) { - os.write(bytes); - } + writeBody(exchange, bytes); } private BufferedImage captureFrame() { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/ScriptHandler.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/ScriptHandler.java index 5e81e48e226..9be192cd237 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/ScriptHandler.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/ScriptHandler.java @@ -288,7 +288,7 @@ private void handleResults(HttpExchange exchange) throws IOException { } else if ("POST".equals(method)) { Map body = readJsonBody(exchange); - String className = (String) body.get("className"); + String className = getStringField(body, "className"); if (className == null || className.isEmpty()) { sendJson(exchange, 400, errorResponse("className is required")); return; @@ -363,12 +363,12 @@ private Plugin resolvePlugin(Map body) { PluginManager pm = getPluginManager(); if (pm == null) return null; - String className = (String) body.get("className"); + String className = getStringField(body, "className"); if (className != null && !className.isEmpty()) { return Microbot.getPlugin(className); } - String name = (String) body.get("name"); + String name = getStringField(body, "name"); if (name != null && !name.isEmpty()) { return findPluginByName(name); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/SettingsHandler.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/SettingsHandler.java index 7a86cae4a24..55cea623cd3 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/SettingsHandler.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/SettingsHandler.java @@ -66,7 +66,7 @@ private void handleLevelUp(HttpExchange exchange) throws IOException { return; } - Boolean enable = (Boolean) request.get("enable"); + Boolean enable = getBooleanField(request, "enable"); if (enable == null) { sendJson(exchange, 400, errorResponse("Required: enable (true/false)")); return; @@ -123,7 +123,7 @@ private void handleConfig(HttpExchange exchange) throws IOException { return; } - String key = (String) request.get("key"); + String key = getStringField(request, "key"); Object value = request.get("value"); if (key == null || value == null) { sendJson(exchange, 400, errorResponse("Required: key, value")); @@ -192,8 +192,8 @@ private void handlePluginConfig(HttpExchange exchange) throws IOException { return; } - String group = (String) request.get("group"); - String key = (String) request.get("key"); + String group = getStringField(request, "group"); + String key = getStringField(request, "key"); Object value = request.get("value"); if (group == null || group.isEmpty() || key == null || key.isEmpty()) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/WalkHandler.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/WalkHandler.java index 063ab2d043a..e55bcb95358 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/WalkHandler.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/WalkHandler.java @@ -62,27 +62,29 @@ protected void handleRequest(HttpExchange exchange) throws IOException { return; } - Number xNum = (Number) body.get("x"); - Number yNum = (Number) body.get("y"); + Integer xNum = getIntField(body, "x"); + Integer yNum = getIntField(body, "y"); if (xNum == null || yNum == null) { sendJson(exchange, 400, errorResponse("Missing required fields: x, y")); return; } - Number planeNum = (Number) body.get("plane"); + Integer planeNum = getIntField(body, "plane"); int plane = planeNum != null ? planeNum.intValue() : 0; - boolean wait = Boolean.TRUE.equals(body.get("wait")); + boolean wait = Boolean.TRUE.equals(getBooleanField(body, "wait")); int timeoutSeconds = DEFAULT_TIMEOUT_SECONDS; - if (body.get("timeout") instanceof Number) { - timeoutSeconds = ((Number) body.get("timeout")).intValue(); + Integer timeoutNum = getIntField(body, "timeout"); + if (timeoutNum != null) { + timeoutSeconds = timeoutNum; if (timeoutSeconds <= 0) timeoutSeconds = DEFAULT_TIMEOUT_SECONDS; if (timeoutSeconds > MAX_TIMEOUT_SECONDS) timeoutSeconds = MAX_TIMEOUT_SECONDS; } int reachedDistance = DEFAULT_REACHED_DISTANCE; - if (body.get("distance") instanceof Number) { - reachedDistance = ((Number) body.get("distance")).intValue(); + Integer distanceNum = getIntField(body, "distance"); + if (distanceNum != null) { + reachedDistance = distanceNum; if (reachedDistance < 0) reachedDistance = DEFAULT_REACHED_DISTANCE; if (reachedDistance > MAX_REACHED_DISTANCE) reachedDistance = MAX_REACHED_DISTANCE; } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/WidgetClickHandler.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/WidgetClickHandler.java index bcfc8c873e2..362bcc60ef0 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/WidgetClickHandler.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/WidgetClickHandler.java @@ -36,9 +36,9 @@ protected void handleRequest(HttpExchange exchange) throws IOException { return; } - Number groupNum = (Number) request.get("groupId"); - Number childNum = (Number) request.get("childId"); - String text = (String) request.get("text"); + Integer groupNum = getIntField(request, "groupId"); + Integer childNum = getIntField(request, "childId"); + String text = getStringField(request, "text"); Map response = new LinkedHashMap<>(); boolean clicked; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/WidgetInvokeHandler.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/WidgetInvokeHandler.java index a42423e32e8..f5295202df1 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/WidgetInvokeHandler.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/WidgetInvokeHandler.java @@ -41,11 +41,11 @@ protected void handleRequest(HttpExchange exchange) throws IOException { return; } - Number groupNum = (Number) request.get("groupId"); - Number childNum = (Number) request.get("childId"); - Number param0Num = (Number) request.get("param0"); - String action = (String) request.get("action"); - Number identifierNum = (Number) request.get("identifier"); + Integer groupNum = getIntField(request, "groupId"); + Integer childNum = getIntField(request, "childId"); + Integer param0Num = getIntField(request, "param0"); + String action = getStringField(request, "action"); + Integer identifierNum = getIntField(request, "identifier"); if (groupNum == null || childNum == null) { sendJson(exchange, 400, errorResponse("Required: groupId, childId")); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/Rs2NpcCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/Rs2NpcCache.java index d5e6cff2f69..8154ff20f37 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/Rs2NpcCache.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/Rs2NpcCache.java @@ -9,6 +9,7 @@ import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -20,8 +21,8 @@ public final class Rs2NpcCache { private final Client client; private final ClientThread clientThread; - private int lastUpdateNpcs = 0; - private List npcs = new ArrayList<>(); + private volatile int lastUpdateNpcs = 0; + private volatile List npcs = new ArrayList<>(); @Inject public Rs2NpcCache(Client client, ClientThread clientThread) { @@ -39,28 +40,31 @@ public Rs2NpcQueryable query() { * @return Stream of Rs2NpcModel */ public Stream getStream() { - if (lastUpdateNpcs >= client.getTickCount()) { - return npcs.stream(); - } + return clientThread.runOnClientThreadOptional(() -> { + int tick = client.getTickCount(); + if (lastUpdateNpcs >= tick) { + return npcs; + } - List result = new ArrayList<>(); + List result = new ArrayList<>(); - for (var id : Microbot.getWorldViewIds()) { - WorldView worldView = client.getWorldView(id); - if (worldView == null) { - continue; - } + for (var id : Microbot.getWorldViewIds()) { + WorldView worldView = client.getWorldView(id); + if (worldView == null) { + continue; + } - result.addAll(worldView.npcs() - .stream() - .filter(Objects::nonNull) - .map(Rs2NpcModel::new) - .collect(Collectors.toList())); - } + result.addAll(worldView.npcs() + .stream() + .filter(Objects::nonNull) + .map(Rs2NpcModel::new) + .collect(Collectors.toList())); + } - npcs = result; - lastUpdateNpcs = client.getTickCount(); - return result.stream(); + npcs = result; + lastUpdateNpcs = tick; + return result; + }).orElse(Collections.emptyList()).stream(); } /** diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/models/Rs2NpcModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/models/Rs2NpcModel.java index 6d5183f4abe..a16d457e186 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/models/Rs2NpcModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/models/Rs2NpcModel.java @@ -36,7 +36,7 @@ public Rs2NpcModel(final NPC npc) @Override public int getId() { - return npc.getId(); + return Microbot.getClientThread().runOnClientThreadOptional(npc::getId).orElse(-1); } public int getIndex() @@ -56,8 +56,13 @@ public int getIndex() */ public boolean isWithinDistanceFromPlayer(int maxDistance) { return Microbot.getClientThread().runOnClientThreadOptional(() -> { - return this.getLocalLocation().distanceTo( - Microbot.getClient().getLocalPlayer().getLocalLocation()) <= maxDistance; + Player localPlayer = Microbot.getClient().getLocalPlayer(); + LocalPoint playerLocation = localPlayer == null ? null : localPlayer.getLocalLocation(); + LocalPoint npcLocation = this.getLocalLocation(); + if (playerLocation == null || npcLocation == null) { + return false; + } + return npcLocation.distanceTo(playerLocation) <= maxDistance; }).orElse(false); } @@ -69,8 +74,13 @@ public boolean isWithinDistanceFromPlayer(int maxDistance) { */ public int getDistanceFromPlayer() { return Microbot.getClientThread().runOnClientThreadOptional(() -> { - return this.getLocalLocation().distanceTo( - Microbot.getClient().getLocalPlayer().getLocalLocation()); + Player localPlayer = Microbot.getClient().getLocalPlayer(); + LocalPoint playerLocation = localPlayer == null ? null : localPlayer.getLocalLocation(); + LocalPoint npcLocation = this.getLocalLocation(); + if (playerLocation == null || npcLocation == null) { + return Integer.MAX_VALUE; + } + return npcLocation.distanceTo(playerLocation); }).orElse(Integer.MAX_VALUE); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/Rs2PlayerCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/Rs2PlayerCache.java index e1d2043dbc1..53864dec016 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/Rs2PlayerCache.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/Rs2PlayerCache.java @@ -9,6 +9,7 @@ import net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -20,8 +21,8 @@ public final class Rs2PlayerCache { private final Client client; private final ClientThread clientThread; - private int lastUpdatePlayers = 0; - private List players = new ArrayList<>(); + private volatile int lastUpdatePlayers = 0; + private volatile List players = new ArrayList<>(); @Inject public Rs2PlayerCache(Client client, ClientThread clientThread) { @@ -38,27 +39,30 @@ public Rs2PlayerQueryable query() { * @return Stream of Rs2PlayerModel */ public Stream getStream() { - if (lastUpdatePlayers >= client.getTickCount()) { - return players.stream(); - } + return clientThread.runOnClientThreadOptional(() -> { + int tick = client.getTickCount(); + if (lastUpdatePlayers >= tick) { + return players; + } - List result = new ArrayList<>(); + List result = new ArrayList<>(); - for (var id : Microbot.getWorldViewIds()) { - WorldView worldView = client.getWorldView(id); - if (worldView == null) { - continue; + for (var id : Microbot.getWorldViewIds()) { + WorldView worldView = client.getWorldView(id); + if (worldView == null) { + continue; + } + result.addAll(worldView.players() + .stream() + .filter(Objects::nonNull) + .map(Rs2PlayerModel::new) + .collect(Collectors.toList())); } - result.addAll(worldView.players() - .stream() - .filter(Objects::nonNull) - .map(Rs2PlayerModel::new) - .collect(Collectors.toList())); - } - players = result; - lastUpdatePlayers = client.getTickCount(); - return players.stream(); + players = result; + lastUpdatePlayers = tick; + return result; + }).orElse(Collections.emptyList()).stream(); } /** diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/models/Rs2TileObjectModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/models/Rs2TileObjectModel.java index 65245c83800..b9aaadf5ded 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/models/Rs2TileObjectModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/models/Rs2TileObjectModel.java @@ -112,7 +112,7 @@ public int getId() { public String getName() { return Microbot.getClientThread().invoke(() -> { ObjectComposition composition = Microbot.getClient().getObjectDefinition(tileObject.getId()); - if (composition.getImpostorIds() != null) { + if (composition != null && composition.getImpostorIds() != null) { composition = composition.getImpostor(); } if (composition == null) @@ -169,8 +169,21 @@ public boolean isOpShown(int index) { public ObjectComposition getObjectComposition() { return Microbot.getClientThread().invoke(() -> { ObjectComposition composition = Microbot.getClient().getObjectDefinition(tileObject.getId()); - if (composition.getImpostorIds() != null) { - composition = composition.getImpostor(); + if (composition != null && composition.getImpostorIds() != null) { + ObjectComposition impostor = composition.getImpostor(); + if (impostor != null) { + composition = impostor; + } + } + return composition; + }); + } + + private ObjectComposition getVisibleObjectComposition() { + return Microbot.getClientThread().invoke(() -> { + ObjectComposition composition = Microbot.getClient().getObjectDefinition(tileObject.getId()); + if (composition != null && composition.getImpostorIds() != null) { + return composition.getImpostor(); } return composition; }); @@ -256,13 +269,12 @@ public boolean click(String action) { String objName = ""; if (action != null) { //performance improvement to only get compoisiton if action has been specified - var objComp = getObjectComposition(); - String[] actions; - if (objComp.getImpostorIds() != null && objComp.getImpostor() != null) { - actions = objComp.getImpostor().getActions(); - } else { - actions = objComp.getActions(); + var objComp = getVisibleObjectComposition(); + if (objComp == null) { + log.warn("Failed to interact with object {} - object not currently visible", getId()); + return false; } + String[] actions = objComp.getActions(); for (int i = 0; i < actions.length; i++) { if (actions[i] == null) continue; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/BreakHandlerScript.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/BreakHandlerScript.java index 0068091d98c..97507950883 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/BreakHandlerScript.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/BreakHandlerScript.java @@ -933,8 +933,9 @@ private void updatePlayerNameCache() { // detect fresh login - update player name cache if (currentlyLoggedIn && !wasLoggedIn) { Rs2PlayerModel localPlayer = Rs2Player.getLocalPlayer(); - if (localPlayer != null && localPlayer.getName() != null) { - lastKnownPlayerName = localPlayer.getName(); + String playerName = localPlayer != null ? localPlayer.getName() : null; + if (playerName != null) { + lastKnownPlayerName = playerName; log.info("Updated cached player name: {}", lastKnownPlayerName); } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/QuestHelperPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/QuestHelperPlugin.java index 1a0d00be2d2..9b121ee49e1 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/QuestHelperPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/QuestHelperPlugin.java @@ -481,7 +481,7 @@ public List getPluginBankTagItemsForSections() public boolean startQuestHelper(String questName) { QuestHelper questHelper = QuestHelperQuest.getByName(questName); - if (questHelper == null) + if (questHelper == null || questHelper.getQuest() == null || questHelper.getConfig() == null) { return false; } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/helpers/quests/shadowofthestorm/IncantationStep.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/helpers/quests/shadowofthestorm/IncantationStep.java index d1813681982..f30c7cedc2b 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/helpers/quests/shadowofthestorm/IncantationStep.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/helpers/quests/shadowofthestorm/IncantationStep.java @@ -34,6 +34,7 @@ import net.runelite.api.gameval.ItemID; import net.runelite.api.gameval.VarbitID; import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetUtil; import net.runelite.client.eventbus.Subscribe; import java.util.Collections; @@ -127,7 +128,7 @@ public void onMenuOptionClicked(MenuOptionClicked event) return; } - if (widget.getId() != InterfaceID.INVENTORY) + if (WidgetUtil.componentToInterface(widget.getId()) != InterfaceID.INVENTORY) { return; } @@ -143,11 +144,16 @@ public void onMenuOptionClicked(MenuOptionClicked event) */ private void updateChoiceIfRequired() { - if (!shouldUpdateChoice()) + if (incantationOrder == null || !shouldUpdateChoice()) { return; } + if (incantationPosition >= incantationOrder.length) + { + incantationPosition = 0; + } + // As the incantations have all the same dialogs we want to reset the choices after each dialog // as we want only the correct one to be highlighted choices.resetChoices(); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/managers/QuestManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/managers/QuestManager.java index 85864b9f0f7..455c06c1a6d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/managers/QuestManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/managers/QuestManager.java @@ -481,16 +481,7 @@ private void getAllItemRequirements() SortedMap> newReqs = new TreeMap<>(); SortedMap> newRecommended = new TreeMap<>(); - filteredQuests.forEach((QuestHelper questHelper) -> { - if (questHelper.getItemRequirements() != null) - { - newReqs.put(questHelper.getQuest(), questHelper.getItemRequirements()); - } - if (questHelper.getItemRecommended() != null) - { - newRecommended.put(questHelper.getQuest(), questHelper.getItemRecommended()); - } - }); + collectItemRequirements(filteredQuests, newReqs, newRecommended); itemRequirements = newReqs; itemRecommended = newRecommended; @@ -504,6 +495,22 @@ private void getAllItemRequirements() }); } + static void collectItemRequirements(List quests, SortedMap> reqs, + SortedMap> recommended) + { + quests.forEach((QuestHelper questHelper) -> { + questHelper.initializeRequirements(); + if (questHelper.getItemRequirements() != null) + { + reqs.put(questHelper.getQuest(), questHelper.getItemRequirements()); + } + if (questHelper.getItemRecommended() != null) + { + recommended.put(questHelper.getQuest(), questHelper.getItemRecommended()); + } + }); + } + /** * Updates all items for the All Items helper. */ diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/panel/QuestOverviewPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/panel/QuestOverviewPanel.java index d2d63233d3b..3f392e4dc20 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/panel/QuestOverviewPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/panel/QuestOverviewPanel.java @@ -253,17 +253,18 @@ public void addQuest(QuestHelper quest, boolean isActive) allQuestStepPanelList.clear(); List steps = quest.getPanels(); + QuestStep questStep = quest.getCurrentStep(); QuestStep currentStep; - if (isActive) + if (isActive && questStep != null) { - currentStep = quest.getCurrentStep().getActiveStep(); + currentStep = questStep.getActiveStep(); } else { currentStep = new DetailedQuestStep(quest, "Fake step"); } - if (quest.getCurrentStep() != null) + if (questStep != null) { questNameLabel.setText(quest.getQuest().getName()); actionsContainer.setVisible(true); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/questhelpers/QuestHelper.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/questhelpers/QuestHelper.java index 18a590d939a..f4a01dc6846 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/questhelpers/QuestHelper.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/questhelpers/QuestHelper.java @@ -194,6 +194,7 @@ public QuestState getState(Client client) public boolean clientMeetsRequirements() { + initializeRequirements(); if (getGeneralRequirements() == null) { return true; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPanel.java index e700f13925a..1faefeb3392 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPanel.java @@ -362,7 +362,7 @@ private JPanel createSlayerMasterPanel() JButton startButton = new JButton("Start"); JButton stopButton = new JButton("Stop"); - startButton.addActionListener(e -> startWalking(getSelectedSlayerMaster().getWorldPoint())); + startButton.addActionListener(e -> startWalking(getSlayerMasterTarget())); stopButton.addActionListener(e -> stopWalking()); JPanel turaelSkipPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); @@ -720,6 +720,11 @@ private JPanel createHotkeyRow(String keyName, Keybind initial, String tooltip) void startWalking(WorldPoint point) { + if (point == null) + { + Microbot.log("WebWalker: no valid destination to walk to."); + return; + } Microbot.log("Web walking starting. Traveling to Custom Location (" + point.getX() + ", " + point.getY() + ", " + point.getPlane() + ")."); plugin.getShortestPathScript().setTriggerWalker(point); } @@ -1001,6 +1006,13 @@ WorldPoint getCurrentClueLocation() return null; } + return Microbot.getClientThread() + .runOnClientThreadOptional(() -> resolveClueLocation(clue, cluePlugin)) + .orElse(null); + } + + private static WorldPoint resolveClueLocation(ClueScroll clue, ClueScrollPlugin cluePlugin) + { // Check if clue implements LocationClueScroll (single location) if (clue instanceof LocationClueScroll) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotConfigPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotConfigPanel.java index 335e1190b63..80d6a69f58b 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotConfigPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotConfigPanel.java @@ -420,10 +420,10 @@ public void mouseClicked(MouseEvent e) { if (result == JOptionPane.YES_OPTION) { configManager.setDefaultConfiguration(pluginConfig.getConfig(), true); - MicrobotConfigManager.resetProfilePluginProperties(pluginConfig.getPlugin().getClass()); // Reset non-config panel keys Plugin plugin = pluginConfig.getPlugin(); if (plugin != null) { + MicrobotConfigManager.resetProfilePluginProperties(plugin.getClass()); plugin.resetConfiguration(); } @@ -665,12 +665,16 @@ private JPanel createDimension(ConfigDescriptor cd, ConfigItemDescriptor cid) { // in config as a JSON string. // --------------------------------------------------------------------------- private JComboBox createInventorySetupsComboBox(ConfigDescriptor cd, ConfigItemDescriptor cid) { - // Suppose MInventorySetupsPlugin.getInventorySetups() returns a List - List setups = MInventorySetupsPlugin.getInventorySetups(); + return createInventorySetupsComboBox(configManager, cd.getGroup().value(), cid.getItem().keyName(), MInventorySetupsPlugin.getInventorySetups()); + } + + static JComboBox createInventorySetupsComboBox(ConfigManager configManager, String group, String key, List setups) { + if (setups == null) { + return new JComboBox<>(); + } if (setups.isEmpty()) { // If there are no setups, return an empty combo box - configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), ""); - configManager.sendConfig(); + configManager.setConfiguration(group, key, ""); return new JComboBox<>(); } JComboBox box = new JComboBox<>(new DefaultComboBoxModel<>(setups.toArray(new InventorySetup[0]))); @@ -689,11 +693,10 @@ public Component getListCellRendererComponent(JList list, Object value, int i }); // 1) Retrieve dezerialized InventorySetup from config - InventorySetup dezerialized = configManager.getConfiguration(cd.getGroup().value(), cid.getItem().keyName(), InventorySetup.class); + InventorySetup dezerialized = configManager.getConfiguration(group, key, InventorySetup.class); if (dezerialized == null) { dezerialized = setups.get(0); - configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), dezerialized); - configManager.sendConfig(); + configManager.setConfiguration(group, key, dezerialized); } for (InventorySetup setup : setups) { if (setup.getName().equals(dezerialized.getName())) { @@ -708,7 +711,7 @@ public Component getListCellRendererComponent(JList list, Object value, int i if (e.getStateChange() == ItemEvent.SELECTED) { InventorySetup chosen = (InventorySetup) box.getSelectedItem(); if (chosen != null) { - configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), chosen); + configManager.setConfiguration(group, key, chosen); } } }); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginHubPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginHubPanel.java index 252ea6bdcb4..52777ea3b33 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginHubPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginHubPanel.java @@ -318,18 +318,17 @@ private class PluginItem extends JPanel implements SearchablePlugin { configure.setToolTipText("Configure"); configure.setBorder(null); if (!loadedPlugins.isEmpty()) { - String search = null; - if (loadedPlugins.size() > 1) { - search = manifest.getInternalName(); - } else { - Plugin plugin = loadedPlugins.iterator().next(); - configure.addActionListener(l -> topLevelConfigPanel.openConfigurationPanel(plugin)); - } + configure.addActionListener(l -> { + if (loadedPlugins.size() == 1) { + Plugin plugin = loadedPlugins.iterator().next(); + if (pluginManager.getPluginConfigProxy(plugin) != null) { + topLevelConfigPanel.openConfigurationPanel(plugin); + return; + } + } - if (search != null) { - final String _search = search; - configure.addActionListener(l -> topLevelConfigPanel.openWithFilter(_search)); - } + topLevelConfigPanel.openWithFilter(manifest.getInternalName()); + }); } else { configure.setVisible(false); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginListPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginListPanel.java index 335e13e25fe..2be0fafb341 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginListPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginListPanel.java @@ -286,6 +286,10 @@ void openConfigurationPanel(Plugin plugin) { } void openConfigurationPanel(MicrobotPluginConfigurationDescriptor plugin) { + if (plugin.getConfigDescriptor() == null) { + return; + } + MicrobotConfigPanel panel = configPanelProvider.get(); panel.init(plugin); muxer.pushState(this); 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 3f3ef5433f3..21a50973521 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 @@ -2551,13 +2551,17 @@ public static boolean walkToBank(BankLocation bankLocation) { /** * Walks to a specified bank location with an option to toggle run energy. - * If the bank is already open, the method exits immediately. + * Returns false if bankLocation is null; otherwise exits immediately if the bank is already open. * * @param bankLocation the target bank location to walk to. * @param toggleRun whether to toggle run energy during the walk. * @return true if the Rs2Player's location is within 4 tiles of the specified bank location. */ public static boolean walkToBank(BankLocation bankLocation, boolean toggleRun) { + if (bankLocation == null) { + log.warn("Bank location is null, cannot walk to bank."); + return false; + } if (Rs2Bank.isOpen()) return true; Rs2Player.toggleRunEnergy(toggleRun); Microbot.status = "Walking to nearest bank " + bankLocation.toString(); @@ -2925,7 +2929,7 @@ public static boolean depositLootingBag(){ sleepUntil(()-> Rs2Widget.getWidget(983046) != null, Rs2Random.between(2000,5000)); if(Rs2Widget.getWidget(983046) != null){ if(Rs2Widget.clickWidget(983046)){ - sleepUntil(()-> Rs2Widget.getWidget(15,11).getChildren()[0] == null, Rs2Random.between(500,1000)); + sleepUntil(Rs2Bank::isLootingBagViewEmpty, Rs2Random.between(500,1000)); if(Rs2Widget.clickWidget(983048)){ // close the bag return true; } @@ -2936,6 +2940,15 @@ public static boolean depositLootingBag(){ return false; } + static boolean isLootingBagViewEmpty() { + return Microbot.getClientThread().runOnClientThreadOptional(() -> { + Widget items = Microbot.getClient().getWidget(15, 11); + if (items == null) return true; + Widget[] children = items.getChildren(); + return children == null || children.length == 0 || children[0] == null; + }).orElse(true); + } + /** * Withdraw items from the lootTrackerPlugin @@ -3005,7 +3018,13 @@ private static Widget getBankSizeWidget() { public static int getBankItemCount() { Widget bank = getBankSizeWidget(); if (bank == null) return 0; - return Integer.parseInt(bank.getText()); + String text = bank.getText(); + if (text == null) return 0; + try { + return Integer.parseInt(text.trim()); + } catch (NumberFormatException e) { + return 0; + } } /** @@ -3328,7 +3347,7 @@ private static int calculateScrollYFromSlotId(int slotId) { Widget w = Microbot.getClient().getWidget(BANK_ITEM_CONTAINER); // Check the height of the bank window to adjust scrolling if necessary - assert w != null; + if (w == null) return 0; int bankHeight = w.getHeight() / (BANK_ITEM_HEIGHT + BANK_ITEM_Y_PADDING); // Calculate the minimum scrollY to ensure the item is visible at the top of the window @@ -3359,9 +3378,9 @@ private static int calculateScrollYFromSlotId(int slotId) { * @param slotId the slot ID of the item to scroll to */ public static boolean scrollBankToSlot(int slotId) { - int scrollY = calculateScrollYFromSlotId(slotId); Widget w = Microbot.getClient().getWidget(BANK_ITEM_CONTAINER); if (w == null) return false; + int scrollY = calculateScrollYFromSlotId(slotId); Microbot.getClientThread().invokeLater(() -> { Microbot.getClient().setVarcIntValue(VarClientInt.BANK_SCROLL, scrollY); 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 38c353ce5ac..a4f05fa641a 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,10 @@ public boolean hasRequirements() { case FARMING_GUILD: return Rs2Player.getSkillRequirement(Skill.FARMING, 45, true); case MINING_GUILD: - boolean inRegion = Rs2Player.getWorldLocation().getRegionID() == 12183 || Rs2Player.getWorldLocation().getRegionID() == 12184; + WorldPoint playerLocation = Rs2Player.getWorldLocation(); + if (playerLocation == null) return false; + int regionId = playerLocation.getRegionID(); + boolean inRegion = regionId == 12183 || regionId == 12184; return inRegion && Rs2Player.getSkillRequirement(Skill.MINING, 60, true); case FISHING_GUILD: return Rs2Player.getSkillRequirement(Skill.FISHING, 68, true); @@ -219,22 +222,6 @@ public boolean hasRequirements() { case VARLAMORE_WEST: // Requires Children of the Sun return Rs2Player.getQuestState(Quest.CHILDREN_OF_THE_SUN) == QuestState.FINISHED; - case LOVAKENGJ: - case HOSIDIUS: - case PISCARILIUS: - case ARCEUUS: - case WINTERTODT: - case SULPHUR_MINE: - case ZEAH_SAND_BANK: - case BLAST_MINE: - case SHAYZIEN_BANK: - case SHAYZIEN_CHEST: - case GREAT_KOUREND_CASTLE: - case HOSIDIUS_KITCHEN: - case VINERY_BANK: - case MOUNT_KARUULM: - // Requires Client of Kourend - return Rs2Player.getQuestState(Quest.CLIENT_OF_KOUREND) != QuestState.NOT_STARTED; case DWARF_MINE_BANK: case BLAST_FURNACE_BANK: // Requires The Giant Dwarf diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/camera/Rs2Camera.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/camera/Rs2Camera.java index 103f72d4385..bba45e62a2e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/camera/Rs2Camera.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/camera/Rs2Camera.java @@ -26,39 +26,29 @@ public class Rs2Camera { private static final int LEGACY_MAX_PITCH = 383; public static int angleToTile(Actor t) { - return Microbot.getClientThread().invoke(() -> { - var playerLocation = new Rs2PlayerModel().getWorldLocation(); - int angle = (int) Math.toDegrees(Math.atan2(t.getWorldLocation().getY() - playerLocation.getY(), - t.getWorldLocation().getX() - playerLocation.getX())); - return angle >= 0 ? angle : 360 + angle; - }); + return Microbot.getClientThread().invoke(() -> angleFromPlayer(t.getWorldLocation().getX(), t.getWorldLocation().getY())); } public static int angleToTile(TileObject t) { - return Microbot.getClientThread().invoke(() -> { - var playerLocation = new Rs2PlayerModel().getWorldLocation(); - int angle = (int) Math.toDegrees(Math.atan2(t.getWorldLocation().getY() - playerLocation.getY(), - t.getWorldLocation().getX() - playerLocation.getX())); - return angle >= 0 ? angle : 360 + angle; - }); + return Microbot.getClientThread().invoke(() -> angleFromPlayer(t.getWorldLocation().getX(), t.getWorldLocation().getY())); } public static int angleToTile(LocalPoint localPoint) { - return Microbot.getClientThread().invoke(() -> { - var playerLocation = new Rs2PlayerModel().getWorldLocation(); - int angle = (int) Math.toDegrees(Math.atan2(localPoint.getY() - playerLocation.getY(), - localPoint.getX() - playerLocation.getX())); - return angle >= 0 ? angle : 360 + angle; - }); + return Microbot.getClientThread().invoke(() -> angleFromPlayer(localPoint.getX(), localPoint.getY())); } public static int angleToTile(WorldPoint worldPoint) { - return Microbot.getClientThread().invoke(() -> { - var playerLocation = new Rs2PlayerModel().getWorldLocation(); - int angle = (int) Math.toDegrees(Math.atan2(worldPoint.getY() - playerLocation.getY(), - worldPoint.getX() - playerLocation.getX())); - return angle >= 0 ? angle : 360 + angle; - }); + return Microbot.getClientThread().invoke(() -> angleFromPlayer(worldPoint.getX(), worldPoint.getY())); + } + + private static int angleFromPlayer(int targetX, int targetY) { + var playerLocation = new Rs2PlayerModel().getWorldLocation(); + if (playerLocation == null) { + int facing = (int) (fromClientAngleUnits(readYawDirectly()) * (360.0 / LEGACY_ANGLE_UNITS)); + return (facing + 90) % 360; + } + int angle = (int) Math.toDegrees(Math.atan2(targetY - playerLocation.getY(), targetX - playerLocation.getX())); + return angle >= 0 ? angle : 360 + angle; } public static void turnTo(final Actor actor) { @@ -272,11 +262,14 @@ public static boolean isTrackingNpc() { } public static boolean isTileOnScreen(TileObject tileObject) { - int viewportHeight = Microbot.getClient().getViewportHeight(); - int viewportWidth = Microbot.getClient().getViewportWidth(); + Client client = Microbot.getClient(); + LocalPoint localPoint = tileObject.getLocalLocation(); + if (!isInWorldViewScene(client, localPoint)) return false; + int viewportHeight = client.getViewportHeight(); + int viewportWidth = client.getViewportWidth(); - Polygon poly = Perspective.getCanvasTilePoly(Microbot.getClient(), tileObject.getLocalLocation()); + Polygon poly = Perspective.getCanvasTilePoly(client, localPoint); if (poly == null) return false; @@ -285,6 +278,8 @@ public static boolean isTileOnScreen(TileObject tileObject) { public static boolean isTileOnScreen(LocalPoint localPoint) { Client client = Microbot.getClient(); + if (!isInWorldViewScene(client, localPoint)) return false; + int viewportHeight = client.getViewportHeight(); int viewportWidth = client.getViewportWidth(); @@ -300,6 +295,18 @@ public static boolean isTileOnScreen(LocalPoint localPoint) { return canvasPoint != null; } + private static boolean isInWorldViewScene(Client client, LocalPoint localPoint) { + if (localPoint == null) return false; + int sceneX = localPoint.getSceneX(); + int sceneY = localPoint.getSceneY(); + return Microbot.getClientThread().runOnClientThreadOptional(() -> { + WorldView worldView = client.getWorldView(localPoint.getWorldView()); + return worldView != null + && sceneX >= 0 && sceneY >= 0 + && sceneX < worldView.getSizeX() && sceneY < worldView.getSizeY(); + }).orElse(false); + } + // get the camera zoom public static int getZoom() { return Microbot.getClient().getVarcIntValue(VarClientInt.CAMERA_ZOOM_RESIZABLE_VIEWPORT); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/combat/Rs2Combat.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/combat/Rs2Combat.java index c833aeeaff3..d3422416cbe 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/combat/Rs2Combat.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/combat/Rs2Combat.java @@ -41,7 +41,8 @@ public static boolean setAttackStyle(WidgetInfo style) { // if (isSelected(widget.getId() + 1)) { // return true; // } - log("Setting attack style to " + Rs2Widget.getWidget(widget.getId() + 3).getText()); + Widget label = Rs2Widget.getWidget(widget.getId() + 3); + log("Setting attack style to " + (label != null ? label.getText() : style.name())); Rs2Widget.clickWidget(widget); return true; } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/coords/Rs2WorldPoint.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/coords/Rs2WorldPoint.java index 625742cc67b..756af0f6c08 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/coords/Rs2WorldPoint.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/coords/Rs2WorldPoint.java @@ -103,7 +103,7 @@ public int distanceToPath(WorldPoint other) return Integer.MAX_VALUE; } List checkpointWPs = this.pathTo(other); - if (checkpointWPs == null) + if (checkpointWPs == null || checkpointWPs.isEmpty()) { // No path found return Integer.MAX_VALUE; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/depositbox/DepositBoxLocation.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/depositbox/DepositBoxLocation.java index 4dfa8543c0d..7380a5b05ce 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/depositbox/DepositBoxLocation.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/depositbox/DepositBoxLocation.java @@ -208,9 +208,7 @@ public boolean hasRequirements() { case MOUNT_QUIDAMORTEM: case HOSIDIUS: case MOUNT_KARUULM: - if (!isMember()) return false; - // Requires Client of Kourend - return Rs2Player.getQuestState(Quest.CLIENT_OF_KOUREND) != QuestState.NOT_STARTED; + return isMember(); case SHILO_VILLAGE: if (!isMember()) return false; // Requires Shilo Village to enter the village & use the bank diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/discord/Rs2Discord.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/discord/Rs2Discord.java index 68c578b9618..87c4bc28040 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/discord/Rs2Discord.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/discord/Rs2Discord.java @@ -181,7 +181,7 @@ public static boolean sendCustomNotification(String title, String description, i embed.setDescription(description); embed.setColor(color); embed.addField(new DiscordEmbed.Field("Player", - playerName.isEmpty() ? "Unknown" : playerName, true)); + playerName == null || playerName.isEmpty() ? "Unknown" : playerName, true)); embed.addField(new DiscordEmbed.Field("Time", timestamp, true)); embed.addField(new DiscordEmbed.Field("Source", source, true)); embed.setFooter(new DiscordEmbed.Footer("Microbot", null)); @@ -281,6 +281,6 @@ public static DiscordEmbed.Field createTimestampField() { * creates a player field */ public static DiscordEmbed.Field createPlayerField(String playerName) { - return createField("Player", playerName.isEmpty() ? "Unknown" : playerName, true); + return createField("Player", playerName == null || playerName.isEmpty() ? "Unknown" : playerName, true); } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2Cannon.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2Cannon.java index fcaab7f99c7..6c31fdff4c0 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2Cannon.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2Cannon.java @@ -3,8 +3,8 @@ import net.runelite.api.ObjectID; import net.runelite.api.TileObject; import net.runelite.api.VarPlayer; -import net.runelite.api.coords.WorldArea; -import net.runelite.client.plugins.cannon.CannonPlugin; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.gameval.VarPlayerID; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; import net.runelite.client.plugins.microbot.util.math.Rs2Random; @@ -20,14 +20,7 @@ public static boolean repair() { if (brokenCannon == null) return false; - // Create centered WorldArea (3x3 area with cannon at center) - WorldArea cannonLocation = new WorldArea( - brokenCannon.getWorldLocation().getX() - 1, - brokenCannon.getWorldLocation().getY() - 1, - 3, 3, - brokenCannon.getWorldLocation().getPlane() - ); - if (!cannonLocation.toWorldPoint().equals(CannonPlugin.getCannonPosition().toWorldPoint())) return false; + if (!isOwnedCannon(brokenCannon)) return false; Microbot.status = "Repairing Cannon"; @@ -54,14 +47,7 @@ public static boolean refill(int cannonRefillAmount) { TileObject cannon = Rs2GameObject.findObject(new Integer[]{ObjectID.DWARF_MULTICANNON, ObjectID.DWARF_MULTICANNON_43027}); if (cannon == null) return false; - // Create centered WorldArea (3x3 area with cannon at center) - WorldArea cannonLocation = new WorldArea( - cannon.getWorldLocation().getX() - 1, - cannon.getWorldLocation().getY() - 1, - 3, 3, - cannon.getWorldLocation().getPlane() - ); - if (!cannonLocation.toWorldPoint().equals(CannonPlugin.getCannonPosition().toWorldPoint())) return false; + if (!isOwnedCannon(cannon)) return false; Microbot.pauseAllScripts.compareAndSet(false, true); Rs2GameObject.interact(cannon, "Fire"); Rs2Player.waitForWalking(); @@ -72,4 +58,12 @@ public static boolean refill(int cannonRefillAmount) { return true; } + private static boolean isOwnedCannon(TileObject cannon) { + int ownedCoord = Microbot.getVarbitPlayerValue(VarPlayerID.OWNEDMCANNON); + if (ownedCoord <= 0) return false; + WorldPoint location = cannon.getWorldLocation(); + WorldPoint southWest = new WorldPoint(location.getX() - 1, location.getY() - 1, location.getPlane()); + return southWest.equals(WorldPoint.fromCoord(ownedCoord)); + } + } 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 47b8e891598..fb5768b9e06 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 @@ -1562,10 +1562,11 @@ private static Stream getSceneObjects(Function 0 ? tiles[z][0].length : 0; - for (int x = 0; x < sceneSize; x++) { - for (int y = 0; y < sceneSize; y++) { + for (int x = 0; x < sizeX; x++) { + for (int y = 0; y < sizeY; y++) { for (int h = 0; h <= z; h++) { Tile tile = tiles[h][x][y]; if (tile == null) continue; @@ -1828,7 +1829,10 @@ public static boolean clickObject(TileObject object, String action) { int dy = playerLocal.getSceneY() - objectLocal.getSceneY(); tooFar = Math.max(Math.abs(dx), Math.abs(dy)) > 51; } else { - tooFar = Rs2Player.getWorldLocation().distanceTo(object.getWorldLocation()) > 51; + WorldPoint playerWorld = Rs2Player.getWorldLocation(); + WorldPoint objectWorld = object.getWorldLocation(); + if (playerWorld == null || objectWorld == null) return false; + tooFar = playerWorld.distanceTo(objectWorld) > 51; } if (tooFar) { Microbot.log("Object with id " + object.getId() + " is not close enough to interact with. Walking to the object...."); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grandexchange/GrandExchangeWidget.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grandexchange/GrandExchangeWidget.java index 1bea65ff752..fcae4e1a17a 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grandexchange/GrandExchangeWidget.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grandexchange/GrandExchangeWidget.java @@ -251,6 +251,10 @@ static Widget getChooseItem() static Widget getConfirm() { var parent = getOfferContainer(); + if (parent == null || parent.getDynamicChildren() == null) + { + return null; + } return Rs2Widget.findWidget("Confirm", Arrays.stream(parent.getDynamicChildren()).collect(Collectors.toList()), true); } @@ -276,6 +280,10 @@ static boolean hasOfferPriceChanged(long basePrice) */ static Widget getSlot(GrandExchangeSlots slot) { + if (slot == null) + { + return null; + } switch (slot) { case ONE: diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grandexchange/Rs2GrandExchange.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grandexchange/Rs2GrandExchange.java index a233384adb6..3b6aac530f8 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grandexchange/Rs2GrandExchange.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grandexchange/Rs2GrandExchange.java @@ -241,7 +241,7 @@ public static boolean processOffer(GrandExchangeRequest request) { //failed to set quantity return false; } - confirm(); + if (!confirm()) break; success = sleepUntil(() -> !isOfferSetupOpen()); break; @@ -267,7 +267,7 @@ public static boolean processOffer(GrandExchangeRequest request) { } } - confirm(); + if (!confirm()) break; success = sleepUntil(() -> !isOfferSetupOpen()); break; } @@ -580,12 +580,15 @@ public static boolean buyItem(String itemName, int price, int quantity) { * such as "Your offer is much higher". If such a prompt is detected, it will automatically * click "Yes" to proceed with the offer. */ - private static void confirm() { - Rs2Widget.clickWidget(GrandExchangeWidget.getConfirm()); + private static boolean confirm() { + if (!Rs2Widget.clickWidget(GrandExchangeWidget.getConfirm())) { + return false; + } sleepUntil(() -> Rs2Widget.hasWidget("Your offer is much"), 2000); if (Rs2Widget.hasWidget("Your offer is much")) { Rs2Widget.clickWidget("Yes"); } + return true; } /** 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 fde57b87b45..fa911af7ab2 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 @@ -1928,7 +1928,7 @@ private static void invokeMenu(Rs2ItemModel rs2Item, String action) { if (inventoryWidgets == null) return; if (!action.isEmpty()) { - var itemWidget = Arrays.stream(inventoryWidgets).filter(x -> x != null && x.getIndex() == rs2Item.getSlot()).findFirst().orElseGet(null); + var itemWidget = Arrays.stream(inventoryWidgets).filter(x -> x != null && x.getIndex() == rs2Item.getSlot()).findFirst().orElse(null); String[] actions = itemWidget != null && itemWidget.getActions() != null ? itemWidget.getActions() : diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcModel.java index 9f3a1cf7b9e..250371037ae 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcModel.java @@ -6,6 +6,8 @@ import net.runelite.api.NPC; import net.runelite.api.NPCComposition; import net.runelite.api.NpcOverrides; +import net.runelite.api.Player; +import net.runelite.api.coords.LocalPoint; import net.runelite.api.coords.WorldPoint; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.api.IEntity; @@ -88,8 +90,13 @@ public NPCComposition getComposition() */ public boolean isWithinDistanceFromPlayer(int maxDistance) { return Microbot.getClientThread().runOnClientThreadOptional(() -> { - return this.getLocalLocation().distanceTo( - Microbot.getClient().getLocalPlayer().getLocalLocation()) <= maxDistance; + Player localPlayer = Microbot.getClient().getLocalPlayer(); + LocalPoint playerLocation = localPlayer == null ? null : localPlayer.getLocalLocation(); + LocalPoint npcLocation = this.getLocalLocation(); + if (playerLocation == null || npcLocation == null) { + return false; + } + return npcLocation.distanceTo(playerLocation) <= maxDistance; }).orElse(false); } @@ -101,8 +108,13 @@ public boolean isWithinDistanceFromPlayer(int maxDistance) { */ public int getDistanceFromPlayer() { return Microbot.getClientThread().runOnClientThreadOptional(() -> { - return this.getLocalLocation().distanceTo( - Microbot.getClient().getLocalPlayer().getLocalLocation()); + Player localPlayer = Microbot.getClient().getLocalPlayer(); + LocalPoint playerLocation = localPlayer == null ? null : localPlayer.getLocalLocation(); + LocalPoint npcLocation = this.getLocalLocation(); + if (playerLocation == null || npcLocation == null) { + return Integer.MAX_VALUE; + } + return npcLocation.distanceTo(playerLocation); }).orElse(Integer.MAX_VALUE); } 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 19360a72493..7806e6559e3 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 @@ -1042,6 +1042,8 @@ public static WorldPoint getWorldLocation_Internal(){ if (player == null) return null; if (Microbot.getClient().getTopLevelWorldView().getScene().isInstance()) { LocalPoint l = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), player.getWorldLocation()); + if (l == null) l = player.getLocalLocation(); + if (l == null) return player.getWorldLocation(); return WorldPoint.fromLocalInstance(Microbot.getClient(), l); } return player.getWorldLocation(); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/shop/Rs2Shop.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/shop/Rs2Shop.java index bff1c462481..f1234a9721e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/shop/Rs2Shop.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/shop/Rs2Shop.java @@ -5,6 +5,7 @@ import net.runelite.api.NPCComposition; import net.runelite.api.events.ItemContainerChanged; import net.runelite.api.widgets.ComponentID; +import net.runelite.api.widgets.Widget; import net.runelite.api.widgets.WidgetInfo; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.util.Global; @@ -153,6 +154,7 @@ public static boolean buyItem(String itemName, String quantity) { invokeMenu(rs2Item, actionAndQuantity); } catch (Exception ex) { Microbot.logStackTrace("Rs2Shop", ex); + return false; } return true; } @@ -177,6 +179,7 @@ public static boolean buyItem(int itemId, String quantity) { invokeMenu(rs2Item, actionAndQuantity); } catch (Exception ex) { Microbot.logStackTrace("Rs2Shop", ex); + return false; } return true; } @@ -389,6 +392,7 @@ private static void invokeMenu(Rs2ItemModel rs2Item, String action) { } + Rectangle bounds = itemBounds(rs2Item); Microbot.doInvoke(new NewMenuEntry() .param0(param0) .param1(param1) @@ -397,7 +401,7 @@ private static void invokeMenu(Rs2ItemModel rs2Item, String action) { .itemId(rs2Item.getId()) .target(rs2Item.getName()) , - (itemBounds(rs2Item) == null) ? new Rectangle(1, 1) : itemBounds(rs2Item)); + bounds == null ? new Rectangle(1, 1) : bounds); //Rs2Reflection.invokeMenu(param0, param1, menuAction.getId(), identifier, rs2Item.id, action, target, -1, -1); } @@ -430,6 +434,11 @@ private static boolean hasShopChanged(List initialShopItems) { * @return Rectangle of the item */ private static Rectangle itemBounds(Rs2ItemModel rs2Item) { - return Rs2Widget.getWidget(19660816).getDynamicChildren()[rs2Item.getSlot()+1].getBounds(); + Widget items = Rs2Widget.getWidget(19660816); + if (items == null) return null; + Widget[] children = items.getDynamicChildren(); + int index = rs2Item.getSlot() + 1; + if (children == null || index < 0 || index >= children.length || children[index] == null) return null; + return children[index].getBounds(); } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/skills/slayer/Rs2Slayer.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/skills/slayer/Rs2Slayer.java index 0f154f58070..ca2dfdb0755 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/skills/slayer/Rs2Slayer.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/skills/slayer/Rs2Slayer.java @@ -81,9 +81,14 @@ public static String getSlayerTask() { { return null; } - taskDBRow = (Integer) Microbot.getDBTableField( + Object[] bossTask = Microbot.getDBTableField( bossRows.get(0), - DBTableID.SlayerTaskSublist.COL_TASK, 0)[0]; + DBTableID.SlayerTaskSublist.COL_TASK, 0); + if (bossTask == null || bossTask.length == 0) + { + return null; + } + taskDBRow = (Integer) bossTask[0]; } else { @@ -99,10 +104,15 @@ public static String getSlayerTask() { taskDBRow = taskRows.get(0); } - return (String) Microbot.getDBTableField( + Object[] taskName = Microbot.getDBTableField( taskDBRow, DBTableID.SlayerTask.COL_NAME_UPPERCASE, - 0)[0]; + 0); + if (taskName == null || taskName.length == 0) + { + return null; + } + return (String) taskName[0]; } /** 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 dae623eafd1..1b43f2c4679 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 @@ -457,6 +457,10 @@ static WalkerRouteState routeStateForTesting() { return routeState; } + static WalkerState processWalkForTesting(WorldPoint target, int distance) { + return processWalk(target, distance, 0); + } + private static void clearRecentTransportContext() { routeState.clearRecentTransportContext(); @@ -1757,6 +1761,19 @@ private static WalkerState processWalk(WorldPoint target, int distance) { return processWalk(target, distance, 0); } + private static WalkerState settleReachedPathEndpoint(WorldPoint target, WorldPoint endpoint, WorldPoint playerLoc, + int distance, int processWalkTail) { + if (walkCancelledDiag(target, "processWalk:at-path-endpoint", processWalkTail)) { + return WalkerState.EXIT; + } + boolean arrived = playerLoc.distanceTo(target) <= tightFinishThreshold(target, endpoint, distance); + if (!arrived) { + traceProcessWalkExit("path-endpoint-short-of-target", target, processWalkTail); + } + setTarget(null, "rs2walker:processWalk:reached-path-endpoint"); + return arrived ? WalkerState.ARRIVED : WalkerState.EXIT; + } + private static WalkerState processWalk(WorldPoint target, int distance, int partialRetries) { if (debug) { return WalkerState.EXIT; @@ -1948,7 +1965,7 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part // Partial paths end at an intermediate waypoint (dst still far from {@code target}); // clearing here would drop currentTarget before the partial-path retry/recalc branch. if (!partialPath && isNear(dst, walkLoop.playerLoc) && routeState.interimTargetWp == null) { - setTarget(null, "rs2walker:processWalk:reached-path-endpoint"); + return settleReachedPathEndpoint(target, dst, walkLoop.playerLoc, distance, processWalkTail); } boolean shouldIssueActiveRouteIdleNudge = shouldIssueActiveRouteIdleNudge(); diff --git a/runelite-client/src/test/java/net/runelite/client/config/ConfigManagerTest.java b/runelite-client/src/test/java/net/runelite/client/config/ConfigManagerTest.java index 61abc932ca9..7198b6ef0ff 100644 --- a/runelite-client/src/test/java/net/runelite/client/config/ConfigManagerTest.java +++ b/runelite-client/src/test/java/net/runelite/client/config/ConfigManagerTest.java @@ -24,6 +24,7 @@ */ package net.runelite.client.config; +import com.google.gson.Gson; import com.google.inject.Guice; import com.google.inject.testing.fieldbinder.Bind; import com.google.inject.testing.fieldbinder.BoundFieldModule; @@ -37,6 +38,7 @@ import net.runelite.client.RuneLite; import net.runelite.client.account.SessionManager; import net.runelite.client.eventbus.EventBus; +import net.runelite.http.api.RuneLiteAPI; import org.junit.Assert; import static org.junit.Assert.assertNotNull; import org.junit.Before; @@ -64,6 +66,9 @@ public class ConfigManagerTest @Bind RuneLiteConfig runeliteConfig; + @Bind + Gson gson = RuneLiteAPI.GSON; + @Bind @Named("sessionfile") File sessionfile = RuneLite.DEFAULT_SESSION_FILE; @@ -165,6 +170,33 @@ public void testResetNullDefaultConfig() Assert.assertNull(conf.nullDefaultKey()); } + @ConfigGroup("inventorySetupKeyTest") + public interface InventorySetupKeyConfig extends Config + { + @ConfigItem( + keyName = "useInventorySetup", + name = "Use Inventory Setup", + description = "value" + ) + default boolean useInventorySetup() + { + return false; + } + } + + @Test + public void testResetBooleanKeyNamedInventorySetup() + { + InventorySetupKeyConfig conf = manager.getConfig(InventorySetupKeyConfig.class); + manager.setConfiguration("inventorySetupKeyTest", "useInventorySetup", true); + Assert.assertTrue(conf.useInventorySetup()); + + manager.setDefaultConfiguration(conf, true); + + Assert.assertFalse(conf.useInventorySetup()); + Assert.assertEquals("false", manager.getConfiguration("inventorySetupKeyTest", "useInventorySetup")); + } + @Test public void testKeySplitter() { diff --git a/runelite-client/src/test/java/net/runelite/client/config/ProfileManagerLockTest.java b/runelite-client/src/test/java/net/runelite/client/config/ProfileManagerLockTest.java new file mode 100644 index 00000000000..5af6772efba --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/config/ProfileManagerLockTest.java @@ -0,0 +1,80 @@ +package net.runelite.client.config; + +import com.google.gson.Gson; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class ProfileManagerLockTest +{ + private ProfileManager profileManager; + private ExecutorService executor; + + @Before + public void setUp() + { + profileManager = new ProfileManager(new Gson()); + executor = Executors.newSingleThreadExecutor(); + } + + @After + public void tearDown() + { + executor.shutdownNow(); + } + + @Test + public void secondThreadWaitsForHeldLockInsteadOfThrowing() throws Exception + { + Future second; + try (ProfileManager.Lock ignored = profileManager.lock()) + { + second = executor.submit(() -> + { + try (ProfileManager.Lock lock = profileManager.lock()) + { + return lock.getProfiles() != null; + } + }); + + try + { + second.get(300, TimeUnit.MILLISECONDS); + fail("second lock acquired while the first was still held"); + } + catch (TimeoutException expected) + { + } + catch (ExecutionException e) + { + throw new AssertionError("second lock threw instead of waiting", e.getCause()); + } + assertFalse(second.isDone()); + } + + assertTrue(second.get(10, TimeUnit.SECONDS)); + } + + @Test + public void lockIsReacquirableAfterClose() + { + try (ProfileManager.Lock ignored = profileManager.lock()) + { + assertNotNull(ignored.getProfiles()); + } + try (ProfileManager.Lock lock = profileManager.lock()) + { + assertNotNull(lock.getProfiles()); + } + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/grounditems/GroundItemsPluginCollectedItemsTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/grounditems/GroundItemsPluginCollectedItemsTest.java new file mode 100644 index 00000000000..11e7b97bdb7 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/grounditems/GroundItemsPluginCollectedItemsTest.java @@ -0,0 +1,81 @@ +package net.runelite.client.plugins.grounditems; + +import com.google.common.collect.Table; +import java.lang.reflect.Field; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import net.runelite.api.coords.WorldPoint; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.assertTrue; + +public class GroundItemsPluginCollectedItemsTest +{ + private static final int ITEMS = 200; + + private Table collected; + private ExecutorService executor; + + @Before + @SuppressWarnings("unchecked") + public void setUp() throws Exception + { + Field field = GroundItemsPlugin.class.getDeclaredField("collectedGroundItems"); + field.setAccessible(true); + collected = (Table) field.get(null); + collected.clear(); + executor = Executors.newSingleThreadExecutor(); + } + + @After + public void tearDown() + { + executor.shutdownNow(); + collected.clear(); + } + + @Test + public void copyIsSafeWhileItemsSpawnAndDespawn() throws Exception + { + for (int i = 0; i < ITEMS; i++) + { + put(i); + } + + AtomicBoolean running = new AtomicBoolean(true); + Future writer = executor.submit(() -> + { + int i = ITEMS; + while (running.get()) + { + put(i); + collected.remove(new WorldPoint(3200 + (i - ITEMS) % ITEMS, 3200, 0), i - ITEMS); + i++; + } + }); + + try + { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2); + while (System.nanoTime() < deadline) + { + assertTrue(GroundItemsPlugin.getCollectedGroundItems().size() >= ITEMS); + } + } + finally + { + running.set(false); + } + writer.get(5, TimeUnit.SECONDS); + } + + private void put(int id) + { + WorldPoint point = new WorldPoint(3200 + id % ITEMS, 3200, 0); + collected.put(point, id, GroundItem.builder().id(id).location(point).build()); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/BlockingEventManagerLoggingTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/BlockingEventManagerLoggingTest.java new file mode 100644 index 00000000000..191411b4345 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/BlockingEventManagerLoggingTest.java @@ -0,0 +1,135 @@ +package net.runelite.client.plugins.microbot; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.IThrowableProxy; +import ch.qos.logback.core.read.ListAppender; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.LoggerFactory; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class BlockingEventManagerLoggingTest +{ + private Logger logger; + private Level previousLevel; + private CountDownLatch logged; + private ListAppender appender; + private BlockingEventManager manager; + + @Before + public void setUp() + { + logger = (Logger) LoggerFactory.getLogger(Microbot.class); + previousLevel = logger.getLevel(); + logger.setLevel(Level.DEBUG); + logged = new CountDownLatch(1); + appender = new ListAppender() + { + @Override + protected void append(ILoggingEvent event) + { + super.append(event); + logged.countDown(); + } + }; + appender.start(); + logger.addAppender(appender); + + manager = new BlockingEventManager(); + for (BlockingEvent event : new ArrayList<>(manager.getEvents())) + { + manager.remove(event); + } + } + + @After + public void tearDown() + { + manager.shutdown(); + logger.detachAppender(appender); + logger.setLevel(previousLevel); + } + + @Test + public void executeFailureIsLoggedWithThrowable() throws Exception + { + manager.add(new FailingEvent(false, new RuntimeException("Timed out waiting for client thread"))); + + validateAndEnqueue(); + assertTrue(manager.shouldBlockAndProcess()); + assertTrue(logged.await(5, TimeUnit.SECONDS)); + + ILoggingEvent event = appender.list.get(0); + assertEquals(Level.ERROR, event.getLevel()); + assertEquals("Error executing BlockingEvent (FailingEvent)", event.getFormattedMessage()); + IThrowableProxy throwable = event.getThrowableProxy(); + assertNotNull(throwable); + assertEquals(RuntimeException.class.getName(), throwable.getClassName()); + assertTrue(throwable.getStackTraceElementProxyArray().length > 0); + } + + @Test + public void validateFailureIsLoggedWithThrowable() throws Exception + { + manager.add(new FailingEvent(true, new IllegalStateException("boom"))); + + validateAndEnqueue(); + assertTrue(logged.await(5, TimeUnit.SECONDS)); + + ILoggingEvent event = appender.list.get(0); + assertEquals(Level.ERROR, event.getLevel()); + assertEquals("Error validating BlockingEvent (FailingEvent)", event.getFormattedMessage()); + assertNotNull(event.getThrowableProxy()); + assertEquals(IllegalStateException.class.getName(), event.getThrowableProxy().getClassName()); + } + + private void validateAndEnqueue() throws Exception + { + Method method = BlockingEventManager.class.getDeclaredMethod("validateAndEnqueueWithBackoff"); + method.setAccessible(true); + method.invoke(manager); + } + + private static final class FailingEvent implements BlockingEvent + { + private final boolean failValidate; + private final RuntimeException failure; + + private FailingEvent(boolean failValidate, RuntimeException failure) + { + this.failValidate = failValidate; + this.failure = failure; + } + + @Override + public boolean validate() + { + if (failValidate) + { + throw failure; + } + return true; + } + + @Override + public boolean execute() + { + throw failure; + } + + @Override + public BlockingEventPriority priority() + { + return BlockingEventPriority.NORMAL; + } + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/agentserver/handler/AgentHandlerBodyFieldTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/agentserver/handler/AgentHandlerBodyFieldTest.java new file mode 100644 index 00000000000..db02c7736c4 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/agentserver/handler/AgentHandlerBodyFieldTest.java @@ -0,0 +1,184 @@ +package net.runelite.client.plugins.microbot.agentserver.handler; + +import com.google.gson.Gson; +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class AgentHandlerBodyFieldTest { + + private static final class QuantityHandler extends AgentHandler { + QuantityHandler() { + super(new Gson()); + } + + @Override + public String getPath() { + return "/quantity"; + } + + @Override + protected void handleRequest(HttpExchange exchange) throws IOException { + Map body = readJsonBody(exchange); + Integer quantity = getIntField(body, "quantity"); + Map result = new LinkedHashMap<>(); + result.put("quantity", quantity); + sendJson(exchange, 200, result); + } + } + + private static final class RawCastHandler extends AgentHandler { + RawCastHandler() { + super(new Gson()); + } + + @Override + public String getPath() { + return "/raw"; + } + + @Override + protected void handleRequest(HttpExchange exchange) throws IOException { + Map body = readJsonBody(exchange); + Number quantity = (Number) body.get("quantity"); + sendJson(exchange, 200, Map.of("quantity", quantity.intValue())); + } + } + + private ByteArrayOutputStream responseBody; + + @Before + public void setUp() { + AgentHandler.setTokenSupplier(null); + responseBody = new ByteArrayOutputStream(); + } + + @After + public void tearDown() { + AgentHandler.setTokenSupplier(null); + } + + private HttpExchange exchangeWithBody(String body) { + return exchangeWithBody("/quantity", body); + } + + private HttpExchange exchangeWithBody(String path, String body) { + HttpExchange exchange = mock(HttpExchange.class); + Headers requestHeaders = new Headers(); + requestHeaders.add("Host", "127.0.0.1:8081"); + when(exchange.getRequestHeaders()).thenReturn(requestHeaders); + when(exchange.getResponseHeaders()).thenReturn(new Headers()); + when(exchange.getRequestURI()).thenReturn(URI.create(path)); + when(exchange.getRequestMethod()).thenReturn("POST"); + when(exchange.getRequestBody()).thenReturn(new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8))); + when(exchange.getResponseBody()).thenReturn(responseBody); + return exchange; + } + + private String response() { + return responseBody.toString(StandardCharsets.UTF_8); + } + + @Test + public void numericQuantityIsAccepted() throws Exception { + HttpExchange exchange = exchangeWithBody("{\"quantity\":28}"); + + new QuantityHandler().handle(exchange); + + verify(exchange).sendResponseHeaders(eq(200), anyLong()); + assertEquals("{\"quantity\":28}", response()); + } + + @Test + public void numericStringQuantityIsAccepted() throws Exception { + HttpExchange exchange = exchangeWithBody("{\"quantity\":\"28\"}"); + + new QuantityHandler().handle(exchange); + + verify(exchange).sendResponseHeaders(eq(200), anyLong()); + assertEquals("{\"quantity\":28}", response()); + } + + @Test + public void nonNumericQuantityReturnsBadRequest() throws Exception { + HttpExchange exchange = exchangeWithBody("{\"quantity\":\"all\"}"); + + new QuantityHandler().handle(exchange); + + verify(exchange).sendResponseHeaders(eq(400), anyLong()); + verify(exchange, never()).sendResponseHeaders(eq(500), anyLong()); + assertEquals("Field 'quantity' must be a number", new Gson().fromJson(response(), Map.class).get("error")); + } + + @Test + public void bankWithdrawWithNonNumericQuantityReturnsBadRequest() throws Exception { + HttpExchange exchange = exchangeWithBody("/bank/withdraw", "{\"quantity\":\"x\",\"name\":\"y\"}"); + + new BankHandler(new Gson()).handle(exchange); + + verify(exchange).sendResponseHeaders(eq(400), anyLong()); + verify(exchange, never()).sendResponseHeaders(eq(500), anyLong()); + assertEquals("Field 'quantity' must be a number", new Gson().fromJson(response(), Map.class).get("error")); + } + + @Test + public void internalClassCastExceptionStillReturnsServerError() throws Exception { + HttpExchange exchange = exchangeWithBody("{\"quantity\":\"28\"}"); + + new RawCastHandler().handle(exchange); + + verify(exchange).sendResponseHeaders(eq(500), anyLong()); + verify(exchange, never()).sendResponseHeaders(eq(400), anyLong()); + } + + @Test + public void accessorsReturnNullForMissingFields() { + Map body = new LinkedHashMap<>(); + + assertNull(AgentHandler.getIntField(body, "id")); + assertNull(AgentHandler.getStringField(body, "name")); + assertNull(AgentHandler.getBooleanField(body, "all")); + } + + @Test + public void stringAndBooleanAccessorsValidateType() { + Map body = new LinkedHashMap<>(); + body.put("name", 995.0); + body.put("all", "yes"); + body.put("wait", "true"); + + try { + AgentHandler.getStringField(body, "name"); + fail("expected BadRequestException"); + } catch (AgentHandler.BadRequestException e) { + assertEquals("Field 'name' must be a string", e.getMessage()); + } + try { + AgentHandler.getBooleanField(body, "all"); + fail("expected BadRequestException"); + } catch (AgentHandler.BadRequestException e) { + assertEquals("Field 'all' must be a boolean", e.getMessage()); + } + assertEquals(Boolean.TRUE, AgentHandler.getBooleanField(body, "wait")); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/agentserver/handler/AgentHandlerClientDisconnectTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/agentserver/handler/AgentHandlerClientDisconnectTest.java new file mode 100644 index 00000000000..106f449580b --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/agentserver/handler/AgentHandlerClientDisconnectTest.java @@ -0,0 +1,90 @@ +package net.runelite.client.plugins.microbot.agentserver.handler; + +import com.google.gson.Gson; +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import org.junit.After; +import org.junit.Test; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.util.Collections; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class AgentHandlerClientDisconnectTest { + + private static final String LOCALIZED_ABORT = "De software op uw hostcomputer heeft een verbinding verbroken"; + + private static final class PingHandler extends AgentHandler { + PingHandler() { + super(new Gson()); + } + + @Override + public String getPath() { + return "/ping"; + } + + @Override + protected void handleRequest(HttpExchange exchange) throws IOException { + sendJson(exchange, 200, Collections.singletonMap("ok", true)); + } + } + + @After + public void tearDown() { + AgentHandler.setTokenSupplier(null); + } + + @Test + public void bodyWriteFailureIsTreatedAsDisconnectWithoutErrorResponse() throws Exception { + AgentHandler.setTokenSupplier(null); + HttpExchange exchange = mock(HttpExchange.class); + Headers requestHeaders = new Headers(); + requestHeaders.add("Host", "127.0.0.1:8081"); + when(exchange.getRequestHeaders()).thenReturn(requestHeaders); + when(exchange.getResponseHeaders()).thenReturn(new Headers()); + when(exchange.getRequestURI()).thenReturn(URI.create("/ping")); + when(exchange.getResponseBody()).thenReturn(new OutputStream() { + @Override + public void write(int b) throws IOException { + throw new IOException(LOCALIZED_ABORT); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + throw new IOException(LOCALIZED_ABORT); + } + }); + + new PingHandler().handle(exchange); + + verify(exchange, times(1)).sendResponseHeaders(eq(200), anyLong()); + verify(exchange, never()).sendResponseHeaders(eq(500), anyLong()); + } + + @Test + public void recognisesWindowsDisconnectMessages() { + assertTrue(AgentHandler.isClientDisconnect(new IOException("An established connection was aborted by the software in your host machine"))); + assertTrue(AgentHandler.isClientDisconnect(new IOException("An existing connection was forcibly closed by the remote host"))); + assertTrue(AgentHandler.isClientDisconnect(new AgentHandler.ClientDisconnectException(new IOException(LOCALIZED_ABORT)))); + assertTrue(AgentHandler.isClientDisconnect(new RuntimeException(new AgentHandler.ClientDisconnectException(new IOException(LOCALIZED_ABORT))))); + } + + @Test + public void unrelatedIoFailureIsNotADisconnect() { + assertFalse(AgentHandler.isClientDisconnect(new IOException(LOCALIZED_ABORT))); + assertFalse(AgentHandler.isClientDisconnect(new IOException("No space left on device"))); + assertFalse(AgentHandler.isClientDisconnect(new IllegalStateException("broken pipe"))); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/agentserver/handler/AgentHandlerJsonBodyTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/agentserver/handler/AgentHandlerJsonBodyTest.java new file mode 100644 index 00000000000..804da5fda25 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/agentserver/handler/AgentHandlerJsonBodyTest.java @@ -0,0 +1,140 @@ +package net.runelite.client.plugins.microbot.agentserver.handler; + +import com.google.gson.Gson; +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class AgentHandlerJsonBodyTest { + + private static final class EchoHandler extends AgentHandler { + EchoHandler() { + super(new Gson()); + } + + @Override + public String getPath() { + return "/echo"; + } + + @Override + protected void handleRequest(HttpExchange exchange) throws IOException { + Map body = readJsonBody(exchange); + Map result = new LinkedHashMap<>(); + result.put("name", body.get("name")); + result.put("size", body.size()); + sendJson(exchange, 200, result); + } + } + + private ByteArrayOutputStream responseBody; + + @Before + public void setUp() { + AgentHandler.setTokenSupplier(null); + responseBody = new ByteArrayOutputStream(); + } + + @After + public void tearDown() { + AgentHandler.setTokenSupplier(null); + } + + private HttpExchange exchangeWithBody(String body) { + HttpExchange exchange = mock(HttpExchange.class); + Headers requestHeaders = new Headers(); + requestHeaders.add("Host", "127.0.0.1:8081"); + when(exchange.getRequestHeaders()).thenReturn(requestHeaders); + when(exchange.getResponseHeaders()).thenReturn(new Headers()); + when(exchange.getRequestURI()).thenReturn(URI.create("/echo")); + when(exchange.getRequestMethod()).thenReturn("POST"); + when(exchange.getRequestBody()).thenReturn(new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8))); + when(exchange.getResponseBody()).thenReturn(responseBody); + return exchange; + } + + private String response() { + return responseBody.toString(StandardCharsets.UTF_8); + } + + @Test + public void emptyBodyIsTreatedAsEmptyObject() throws Exception { + HttpExchange exchange = exchangeWithBody(""); + + new EchoHandler().handle(exchange); + + verify(exchange).sendResponseHeaders(eq(200), anyLong()); + assertTrue(response().contains("\"size\":0")); + } + + @Test + public void jsonNullBodyIsTreatedAsEmptyObject() throws Exception { + HttpExchange exchange = exchangeWithBody("null"); + + new EchoHandler().handle(exchange); + + verify(exchange).sendResponseHeaders(eq(200), anyLong()); + assertTrue(response().contains("\"size\":0")); + } + + @Test + public void validBodyIsParsed() throws Exception { + HttpExchange exchange = exchangeWithBody("{\"name\":\"Lobster\"}"); + + new EchoHandler().handle(exchange); + + verify(exchange).sendResponseHeaders(eq(200), anyLong()); + assertTrue(response().contains("\"name\":\"Lobster\"")); + } + + @Test + public void malformedJsonReturnsBadRequest() throws Exception { + HttpExchange exchange = exchangeWithBody("{\"path\":\"C:\\Users\\bot\\script.java\"}"); + + new EchoHandler().handle(exchange); + + verify(exchange).sendResponseHeaders(eq(400), anyLong()); + verify(exchange, never()).sendResponseHeaders(eq(500), anyLong()); + assertEquals("{\"error\":\"Invalid JSON body\"}", response()); + } + + @Test + public void nonObjectJsonReturnsBadRequest() throws Exception { + HttpExchange exchange = exchangeWithBody("[1,2,3]"); + + new EchoHandler().handle(exchange); + + verify(exchange).sendResponseHeaders(eq(400), anyLong()); + verify(exchange, never()).sendResponseHeaders(eq(500), anyLong()); + } + + @Test + public void oversizedBodyReturnsBadRequest() throws Exception { + HttpExchange exchange = exchangeWithBody("{}"); + exchange.getRequestHeaders().add("Content-Length", String.valueOf(1024 * 1024)); + + new EchoHandler().handle(exchange); + + verify(exchange).sendResponseHeaders(eq(400), anyLong()); + verify(exchange, never()).sendResponseHeaders(eq(500), anyLong()); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/agentserver/handler/QuestHelperHandlerStartTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/agentserver/handler/QuestHelperHandlerStartTest.java new file mode 100644 index 00000000000..c5b4da83308 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/agentserver/handler/QuestHelperHandlerStartTest.java @@ -0,0 +1,108 @@ +package net.runelite.client.plugins.microbot.agentserver.handler; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.plugins.PluginManager; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.questhelper.QuestHelperConfig; +import net.runelite.client.plugins.microbot.questhelper.QuestHelperPlugin; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +public class QuestHelperHandlerStartTest +{ + private static final Gson GSON = new GsonBuilder().create(); + + private MockedStatic microbot; + private QuestHelperPlugin plugin; + private PluginManager pluginManager; + private ConfigManager configManager; + private HttpExchange exchange; + private ByteArrayOutputStream responseBody; + + @Before + public void setUp() + { + plugin = mock(QuestHelperPlugin.class); + pluginManager = mock(PluginManager.class); + configManager = mock(ConfigManager.class); + + microbot = Mockito.mockStatic(Microbot.class); + microbot.when(() -> Microbot.getPlugin(QuestHelperPlugin.class)).thenReturn(plugin); + microbot.when(Microbot::getPluginManager).thenReturn(pluginManager); + microbot.when(Microbot::getConfigManager).thenReturn(configManager); + + responseBody = new ByteArrayOutputStream(); + exchange = mock(HttpExchange.class); + when(exchange.getRequestMethod()).thenReturn("POST"); + when(exchange.getRequestURI()).thenReturn(URI.create("/quest-helper/start")); + when(exchange.getRequestHeaders()).thenReturn(new Headers()); + when(exchange.getRequestBody()).thenReturn(new ByteArrayInputStream( + "{\"name\":\"A Taste of Hope\"}".getBytes(StandardCharsets.UTF_8))); + when(exchange.getResponseHeaders()).thenReturn(new Headers()); + when(exchange.getResponseBody()).thenReturn(responseBody); + } + + @After + public void tearDown() + { + microbot.close(); + } + + @Test + public void startReturns409WhenPluginIsNotActive() throws Exception + { + when(pluginManager.isActive(plugin)).thenReturn(false); + + new QuestHelperHandler(GSON).handleRequest(exchange); + + verify(plugin, never()).startQuestHelper(anyString()); + verifyNoInteractions(configManager); + verify(exchange).sendResponseHeaders(eq(409), anyLong()); + assertEquals("{\"error\":\"Quest Helper plugin is not active\"}", responseBody.toString(StandardCharsets.UTF_8)); + } + + @Test + public void startReturns404WhenHelperIsNotLoaded() throws Exception + { + when(pluginManager.isActive(plugin)).thenReturn(true); + when(plugin.startQuestHelper("A Taste of Hope")).thenReturn(false); + + new QuestHelperHandler(GSON).handleRequest(exchange); + + verifyNoInteractions(configManager); + verify(exchange).sendResponseHeaders(eq(404), anyLong()); + } + + @Test + public void startTurnsHelperOnWhenPluginIsActive() throws Exception + { + when(pluginManager.isActive(plugin)).thenReturn(true); + when(plugin.startQuestHelper("A Taste of Hope")).thenReturn(true); + + new QuestHelperHandler(GSON).handleRequest(exchange); + + verify(configManager).setRSProfileConfiguration(QuestHelperConfig.QUEST_HELPER_GROUP, "TurnOn", true); + verify(exchange).sendResponseHeaders(eq(200), anyLong()); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/agentserver/handler/QuestHelperHandlerStopTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/agentserver/handler/QuestHelperHandlerStopTest.java new file mode 100644 index 00000000000..9474424f269 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/agentserver/handler/QuestHelperHandlerStopTest.java @@ -0,0 +1,111 @@ +package net.runelite.client.plugins.microbot.agentserver.handler; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import java.io.ByteArrayOutputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.questhelper.QuestHelperPlugin; +import net.runelite.client.plugins.microbot.questhelper.managers.QuestManager; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class QuestHelperHandlerStopTest +{ + private static final Gson GSON = new GsonBuilder().create(); + + private MockedStatic microbot; + private ClientThread clientThread; + private QuestManager questManager; + private HttpExchange exchange; + private ByteArrayOutputStream responseBody; + private final AtomicBoolean onClientThread = new AtomicBoolean(); + + @Before + public void setUp() + { + questManager = mock(QuestManager.class); + QuestHelperPlugin plugin = mock(QuestHelperPlugin.class); + when(plugin.getQuestManager()).thenReturn(questManager); + clientThread = mock(ClientThread.class); + + microbot = Mockito.mockStatic(Microbot.class); + microbot.when(() -> Microbot.getPlugin(QuestHelperPlugin.class)).thenReturn(plugin); + microbot.when(Microbot::getClientThread).thenReturn(clientThread); + + responseBody = new ByteArrayOutputStream(); + exchange = mock(HttpExchange.class); + when(exchange.getRequestMethod()).thenReturn("POST"); + when(exchange.getRequestURI()).thenReturn(URI.create("/quest-helper/stop")); + when(exchange.getResponseHeaders()).thenReturn(new Headers()); + when(exchange.getResponseBody()).thenReturn(responseBody); + } + + @After + public void tearDown() + { + microbot.close(); + } + + @Test + public void stopShutsDownQuestOnClientThread() throws Exception + { + when(clientThread.runOnClientThreadOptional(any())).thenAnswer(inv -> + { + Callable callable = inv.getArgument(0); + onClientThread.set(true); + try + { + return Optional.ofNullable(callable.call()); + } + finally + { + onClientThread.set(false); + } + }); + doAnswer(inv -> + { + assertTrue("shutDownQuest must run on the client thread", onClientThread.get()); + return null; + }).when(questManager).shutDownQuest(true); + + new QuestHelperHandler(GSON).handleRequest(exchange); + + verify(questManager).shutDownQuest(true); + verify(exchange).sendResponseHeaders(eq(200), anyLong()); + assertTrue(responseBody.toString(StandardCharsets.UTF_8).contains("\"success\":true")); + } + + @Test + public void stopReturns500WhenClientThreadCallFails() throws Exception + { + when(clientThread.runOnClientThreadOptional(any())).thenReturn(Optional.empty()); + + new QuestHelperHandler(GSON).handleRequest(exchange); + + verify(questManager, never()).shutDownQuest(true); + verify(exchange).sendResponseHeaders(eq(500), anyLong()); + assertEquals("{\"error\":\"Failed to stop quest helper\"}", responseBody.toString(StandardCharsets.UTF_8)); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/api/npc/Rs2NpcCacheTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/api/npc/Rs2NpcCacheTest.java new file mode 100644 index 00000000000..3bbecbd881f --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/api/npc/Rs2NpcCacheTest.java @@ -0,0 +1,146 @@ +package net.runelite.client.plugins.microbot.api.npc; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import net.runelite.api.Client; +import net.runelite.api.IndexedObjectSet; +import net.runelite.api.NPC; +import net.runelite.api.WorldView; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +public class Rs2NpcCacheTest +{ + private static final int WORLD_VIEW_ID = 4244; + + private final AtomicBoolean onClientThread = new AtomicBoolean(); + private final AtomicInteger tick = new AtomicInteger(5); + private final AtomicInteger iterations = new AtomicInteger(); + + private Client client; + private ClientThread clientThread; + private NPC first; + private NPC second; + private Rs2NpcCache cache; + + @Before + public void setUp() + { + client = mock(Client.class); + clientThread = mock(ClientThread.class); + when(clientThread.runOnClientThreadOptional(any())).thenAnswer(invocation -> + { + Callable callable = invocation.getArgument(0); + onClientThread.set(true); + try + { + return Optional.ofNullable(callable.call()); + } + finally + { + onClientThread.set(false); + } + }); + + first = mock(NPC.class); + second = mock(NPC.class); + IndexedObjectSet npcs = new IndexedObjectSet<>() + { + @Override + public NPC byIndex(int index) + { + return null; + } + + @Override + public Iterator iterator() + { + iterations.incrementAndGet(); + Iterator delegate = Arrays.asList(first, null, second).iterator(); + return new Iterator<>() + { + @Override + public boolean hasNext() + { + return guarded(delegate.hasNext()); + } + + @Override + public NPC next() + { + return guarded(delegate.next()); + } + }; + } + }; + + WorldView worldView = mock(WorldView.class); + doReturn(npcs).when(worldView).npcs(); + when(client.getTickCount()).thenAnswer(invocation -> guarded(tick.get())); + when(client.getWorldView(WORLD_VIEW_ID)).thenAnswer(invocation -> guarded(worldView)); + + Microbot.getWorldViewIds().add(WORLD_VIEW_ID); + cache = new Rs2NpcCache(client, clientThread); + } + + @After + public void tearDown() + { + Microbot.getWorldViewIds().remove(WORLD_VIEW_ID); + } + + private T guarded(T value) + { + assertTrue("client state must only be read inside the client-thread task", onClientThread.get()); + return value; + } + + @Test + public void offClientThreadRefreshIteratesNpcsOnlyInsideClientThreadTask() + { + List npcs = cache.getStream().collect(Collectors.toList()); + + assertEquals(2, npcs.size()); + verify(clientThread, times(1)).runOnClientThreadOptional(any()); + } + + @Test + public void sameTickReusesSnapshotAndNextTickRescans() + { + assertEquals(2, cache.getStream().count()); + assertEquals(2, cache.getStream().count()); + assertEquals(1, iterations.get()); + + tick.incrementAndGet(); + assertEquals(2, cache.getStream().count()); + assertEquals(2, iterations.get()); + } + + @Test + public void clientThreadFailureReturnsEmptyStreamWithoutReadingClient() + { + doReturn(Optional.empty()).when(clientThread).runOnClientThreadOptional(any()); + + assertEquals(0, cache.getStream().count()); + verifyNoInteractions(client); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/api/npc/models/Rs2NpcModelDistanceFromPlayerTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/api/npc/models/Rs2NpcModelDistanceFromPlayerTest.java new file mode 100644 index 00000000000..51497265143 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/api/npc/models/Rs2NpcModelDistanceFromPlayerTest.java @@ -0,0 +1,120 @@ +package net.runelite.client.plugins.microbot.api.npc.models; + +import net.runelite.api.Client; +import net.runelite.api.NPC; +import net.runelite.api.Player; +import net.runelite.api.coords.LocalPoint; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.plugins.microbot.Microbot; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.function.Supplier; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class Rs2NpcModelDistanceFromPlayerTest { + + private MockedStatic microbot; + private Client client; + private NPC npc; + + @Before + public void setUp() { + ClientThread clientThread = mock(ClientThread.class); + when(clientThread.runOnClientThreadOptional(any())).thenAnswer(inv -> { + Callable callable = inv.getArgument(0); + return Optional.ofNullable(callable.call()); + }); + when(clientThread.invoke(any(Supplier.class))).thenAnswer(inv -> ((Supplier) inv.getArgument(0)).get()); + client = mock(Client.class); + npc = mock(NPC.class); + microbot = Mockito.mockStatic(Microbot.class); + microbot.when(Microbot::getClientThread).thenReturn(clientThread); + microbot.when(Microbot::getClient).thenReturn(client); + } + + @After + public void tearDown() { + microbot.close(); + } + + private void givenLocalPlayerAt(LocalPoint location) { + Player player = mock(Player.class); + when(player.getLocalLocation()).thenReturn(location); + when(client.getLocalPlayer()).thenReturn(player); + } + + @Test + public void nullLocalPlayerIsUnreachable() { + when(client.getLocalPlayer()).thenReturn(null); + when(npc.getLocalLocation()).thenReturn(new LocalPoint(128, 128, -1)); + + Rs2NpcModel model = new Rs2NpcModel(npc); + assertEquals(Integer.MAX_VALUE, model.getDistanceFromPlayer()); + assertFalse(model.isWithinDistanceFromPlayer(Integer.MAX_VALUE)); + } + + @Test + public void nullLocalPlayerLocationIsUnreachable() { + givenLocalPlayerAt(null); + when(npc.getLocalLocation()).thenReturn(new LocalPoint(128, 128, -1)); + + Rs2NpcModel model = new Rs2NpcModel(npc); + assertEquals(Integer.MAX_VALUE, model.getDistanceFromPlayer()); + assertFalse(model.isWithinDistanceFromPlayer(Integer.MAX_VALUE)); + } + + @Test + public void nullNpcLocationIsUnreachable() { + givenLocalPlayerAt(new LocalPoint(128, 128, -1)); + when(npc.getLocalLocation()).thenReturn(null); + + Rs2NpcModel model = new Rs2NpcModel(npc); + assertEquals(Integer.MAX_VALUE, model.getDistanceFromPlayer()); + assertFalse(model.isWithinDistanceFromPlayer(Integer.MAX_VALUE)); + } + + @Test + public void measuresDistanceWhenBothLocationsPresent() { + givenLocalPlayerAt(new LocalPoint(128, 128, -1)); + when(npc.getLocalLocation()).thenReturn(new LocalPoint(128, 128 + 3 * 128, -1)); + + Rs2NpcModel model = new Rs2NpcModel(npc); + assertEquals(3 * 128, model.getDistanceFromPlayer()); + assertTrue(model.isWithinDistanceFromPlayer(3 * 128)); + assertFalse(model.isWithinDistanceFromPlayer(3 * 128 - 1)); + } + + @Test + public void legacyModelNullLocalPlayerIsUnreachable() { + when(client.getLocalPlayer()).thenReturn(null); + when(npc.getLocalLocation()).thenReturn(new LocalPoint(128, 128, -1)); + + net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel model = + new net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel(npc); + assertEquals(Integer.MAX_VALUE, model.getDistanceFromPlayer()); + assertFalse(model.isWithinDistanceFromPlayer(Integer.MAX_VALUE)); + } + + @Test + public void legacyModelNullNpcLocationIsUnreachable() { + givenLocalPlayerAt(new LocalPoint(128, 128, -1)); + when(npc.getLocalLocation()).thenReturn(null); + + net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel model = + new net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel(npc); + assertEquals(Integer.MAX_VALUE, model.getDistanceFromPlayer()); + assertFalse(model.isWithinDistanceFromPlayer(Integer.MAX_VALUE)); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/api/npc/models/Rs2NpcModelGetIdThreadTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/api/npc/models/Rs2NpcModelGetIdThreadTest.java new file mode 100644 index 00000000000..f7910c454cf --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/api/npc/models/Rs2NpcModelGetIdThreadTest.java @@ -0,0 +1,69 @@ +package net.runelite.client.plugins.microbot.api.npc.models; + +import net.runelite.api.NPC; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.plugins.microbot.Microbot; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class Rs2NpcModelGetIdThreadTest { + + private MockedStatic microbot; + private final AtomicBoolean onClientThread = new AtomicBoolean(false); + private ClientThread clientThread; + + @Before + public void setUp() { + clientThread = mock(ClientThread.class); + when(clientThread.runOnClientThreadOptional(any())).thenAnswer(inv -> { + Callable callable = inv.getArgument(0); + onClientThread.set(true); + try { + return Optional.ofNullable(callable.call()); + } finally { + onClientThread.set(false); + } + }); + microbot = Mockito.mockStatic(Microbot.class); + microbot.when(Microbot::getClientThread).thenReturn(clientThread); + } + + @After + public void tearDown() { + microbot.close(); + } + + @Test + public void readsNpcIdOnClientThread() { + NPC npc = mock(NPC.class); + when(npc.getId()).thenAnswer(inv -> { + if (!onClientThread.get()) { + throw new NullPointerException("NPC.getId called off the client thread"); + } + return 12345; + }); + + assertEquals(12345, new Rs2NpcModel(npc).getId()); + } + + @Test + public void returnsMinusOneWhenClientThreadReturnsNothing() { + doReturn(Optional.empty()).when(clientThread).runOnClientThreadOptional(any()); + NPC npc = mock(NPC.class); + + assertEquals(-1, new Rs2NpcModel(npc).getId()); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/api/player/Rs2PlayerCacheTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/api/player/Rs2PlayerCacheTest.java new file mode 100644 index 00000000000..6aff154cca3 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/api/player/Rs2PlayerCacheTest.java @@ -0,0 +1,146 @@ +package net.runelite.client.plugins.microbot.api.player; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import net.runelite.api.Client; +import net.runelite.api.IndexedObjectSet; +import net.runelite.api.Player; +import net.runelite.api.WorldView; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +public class Rs2PlayerCacheTest +{ + private static final int WORLD_VIEW_ID = 4245; + + private final AtomicBoolean onClientThread = new AtomicBoolean(); + private final AtomicInteger tick = new AtomicInteger(5); + private final AtomicInteger iterations = new AtomicInteger(); + + private Client client; + private ClientThread clientThread; + private Player first; + private Player second; + private Rs2PlayerCache cache; + + @Before + public void setUp() + { + client = mock(Client.class); + clientThread = mock(ClientThread.class); + when(clientThread.runOnClientThreadOptional(any())).thenAnswer(invocation -> + { + Callable callable = invocation.getArgument(0); + onClientThread.set(true); + try + { + return Optional.ofNullable(callable.call()); + } + finally + { + onClientThread.set(false); + } + }); + + first = mock(Player.class); + second = mock(Player.class); + IndexedObjectSet players = new IndexedObjectSet<>() + { + @Override + public Player byIndex(int index) + { + return null; + } + + @Override + public Iterator iterator() + { + iterations.incrementAndGet(); + Iterator delegate = Arrays.asList(first, null, second).iterator(); + return new Iterator<>() + { + @Override + public boolean hasNext() + { + return guarded(delegate.hasNext()); + } + + @Override + public Player next() + { + return guarded(delegate.next()); + } + }; + } + }; + + WorldView worldView = mock(WorldView.class); + doReturn(players).when(worldView).players(); + when(client.getTickCount()).thenAnswer(invocation -> guarded(tick.get())); + when(client.getWorldView(WORLD_VIEW_ID)).thenAnswer(invocation -> guarded(worldView)); + + Microbot.getWorldViewIds().add(WORLD_VIEW_ID); + cache = new Rs2PlayerCache(client, clientThread); + } + + @After + public void tearDown() + { + Microbot.getWorldViewIds().remove(WORLD_VIEW_ID); + } + + private T guarded(T value) + { + assertTrue("client state must only be read inside the client-thread task", onClientThread.get()); + return value; + } + + @Test + public void offClientThreadRefreshIteratesPlayersOnlyInsideClientThreadTask() + { + List players = cache.getStream().collect(Collectors.toList()); + + assertEquals(2, players.size()); + verify(clientThread, times(1)).runOnClientThreadOptional(any()); + } + + @Test + public void sameTickReusesSnapshotAndNextTickRescans() + { + assertEquals(2, cache.getStream().count()); + assertEquals(2, cache.getStream().count()); + assertEquals(1, iterations.get()); + + tick.incrementAndGet(); + assertEquals(2, cache.getStream().count()); + assertEquals(2, iterations.get()); + } + + @Test + public void clientThreadFailureReturnsEmptyStreamWithoutReadingClient() + { + doReturn(Optional.empty()).when(clientThread).runOnClientThreadOptional(any()); + + assertEquals(0, cache.getStream().count()); + verifyNoInteractions(client); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/api/tileobject/models/Rs2TileObjectModelCompositionTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/api/tileobject/models/Rs2TileObjectModelCompositionTest.java new file mode 100644 index 00000000000..8deb58f06e6 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/api/tileobject/models/Rs2TileObjectModelCompositionTest.java @@ -0,0 +1,92 @@ +package net.runelite.client.plugins.microbot.api.tileobject.models; + +import java.awt.Rectangle; +import java.util.function.Supplier; +import net.runelite.api.Client; +import net.runelite.api.ObjectComposition; +import net.runelite.api.TileObject; +import net.runelite.api.coords.LocalPoint; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.menu.NewMenuEntry; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.when; + +public class Rs2TileObjectModelCompositionTest { + + private static final int OBJECT_ID = 43700; + + private MockedStatic microbot; + private Client client; + private TileObject tileObject; + + @Before + public void setUp() { + client = mock(Client.class); + ClientThread clientThread = mock(ClientThread.class); + when(clientThread.invoke(any(Supplier.class))).thenAnswer(inv -> ((Supplier) inv.getArgument(0)).get()); + microbot = Mockito.mockStatic(Microbot.class); + microbot.when(Microbot::getClientThread).thenReturn(clientThread); + microbot.when(Microbot::getClient).thenReturn(client); + + tileObject = mock(TileObject.class); + when(tileObject.getId()).thenReturn(OBJECT_ID); + when(tileObject.getLocalLocation()).thenReturn(new LocalPoint(6400, 6400, -1)); + } + + @After + public void tearDown() { + microbot.close(); + } + + @Test + public void unresolvedImpostorFallsBackToBaseComposition() { + ObjectComposition base = mock(ObjectComposition.class); + when(base.getImpostorIds()).thenReturn(new int[]{1, 2}); + when(base.getImpostor()).thenReturn(null); + when(client.getObjectDefinition(OBJECT_ID)).thenReturn(base); + + assertSame(base, new Rs2TileObjectModel(tileObject).getObjectComposition()); + } + + @Test + public void resolvedImpostorIsReturned() { + ObjectComposition base = mock(ObjectComposition.class); + ObjectComposition impostor = mock(ObjectComposition.class); + when(base.getImpostorIds()).thenReturn(new int[]{1, 2}); + when(base.getImpostor()).thenReturn(impostor); + when(client.getObjectDefinition(OBJECT_ID)).thenReturn(base); + + assertSame(impostor, new Rs2TileObjectModel(tileObject).getObjectComposition()); + } + + @Test + public void clickReturnsFalseWithoutInvokingWhenImpostorIsUnresolved() { + ObjectComposition base = mock(ObjectComposition.class); + when(base.getImpostorIds()).thenReturn(new int[]{1, 2}); + when(base.getImpostor()).thenReturn(null); + when(base.getActions()).thenReturn(new String[5]); + when(base.getName()).thenReturn("null"); + when(client.getObjectDefinition(OBJECT_ID)).thenReturn(base); + + assertFalse(new Rs2TileObjectModel(tileObject).click("Enter")); + microbot.verify(() -> Microbot.doInvoke(any(NewMenuEntry.class), any(Rectangle.class)), never()); + } + + @Test + public void clickReturnsFalseWhenCompositionIsMissing() { + when(client.getObjectDefinition(OBJECT_ID)).thenReturn(null); + + assertFalse(new Rs2TileObjectModel(tileObject).click("Enter")); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/questhelper/QuestHelperPluginStartQuestHelperTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/questhelper/QuestHelperPluginStartQuestHelperTest.java new file mode 100644 index 00000000000..a8dbe9769a2 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/questhelper/QuestHelperPluginStartQuestHelperTest.java @@ -0,0 +1,82 @@ +package net.runelite.client.plugins.microbot.questhelper; + +import net.runelite.client.plugins.microbot.questhelper.managers.QuestManager; +import net.runelite.client.plugins.microbot.questhelper.questhelpers.QuestHelper; +import net.runelite.client.plugins.microbot.questhelper.questinfo.QuestHelperQuest; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +public class QuestHelperPluginStartQuestHelperTest +{ + @Mock private QuestHelperConfig config; + @Mock private QuestManager questManager; + @InjectMocks private QuestHelperPlugin plugin; + + private QuestHelper helper; + private QuestHelperQuest originalQuest; + private QuestHelperConfig originalConfig; + + @Before + public void setUp() + { + MockitoAnnotations.initMocks(this); + helper = QuestHelperQuest.COOKS_ASSISTANT.getQuestHelper(); + originalQuest = helper.getQuest(); + originalConfig = helper.getConfig(); + } + + @After + public void tearDown() + { + helper.setQuest(originalQuest); + helper.setConfig(originalConfig); + } + + @Test + public void refusesHelperThatWasNeverInstantiated() + { + helper.setQuest(null); + helper.setConfig(null); + + assertFalse(plugin.startQuestHelper(QuestHelperQuest.COOKS_ASSISTANT.getName())); + verify(questManager, never()).startUpQuest(any(), anyBoolean()); + } + + @Test + public void refusesHelperWithoutConfig() + { + helper.setQuest(QuestHelperQuest.COOKS_ASSISTANT); + helper.setConfig(null); + + assertFalse(plugin.startQuestHelper(QuestHelperQuest.COOKS_ASSISTANT.getName())); + verify(questManager, never()).startUpQuest(any(), anyBoolean()); + } + + @Test + public void startsInstantiatedHelper() + { + helper.setQuest(QuestHelperQuest.COOKS_ASSISTANT); + helper.setConfig(config); + + assertTrue(plugin.startQuestHelper(QuestHelperQuest.COOKS_ASSISTANT.getName())); + verify(questManager).startUpQuest(helper, true); + } + + @Test + public void refusesUnknownQuestName() + { + assertFalse(plugin.startQuestHelper("Not A Real Quest")); + verify(questManager, never()).startUpQuest(any(), anyBoolean()); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/questhelper/helpers/quests/shadowofthestorm/IncantationStepTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/questhelper/helpers/quests/shadowofthestorm/IncantationStepTest.java new file mode 100644 index 00000000000..50d6789a468 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/questhelper/helpers/quests/shadowofthestorm/IncantationStepTest.java @@ -0,0 +1,112 @@ +package net.runelite.client.plugins.microbot.questhelper.helpers.quests.shadowofthestorm; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import net.runelite.api.Client; +import net.runelite.api.MenuEntry; +import net.runelite.api.events.MenuOptionClicked; +import net.runelite.api.gameval.InterfaceID; +import net.runelite.api.gameval.VarbitID; +import net.runelite.api.widgets.Widget; +import net.runelite.client.plugins.microbot.questhelper.questhelpers.QuestHelper; +import net.runelite.client.plugins.microbot.questhelper.steps.QuestStep; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class IncantationStepTest +{ + private Client client; + private IncantationStep step; + + @Before + public void setUp() throws Exception + { + client = mock(Client.class); + step = new IncantationStep(mock(QuestHelper.class), false); + Field clientField = QuestStep.class.getDeclaredField("client"); + clientField.setAccessible(true); + clientField.set(step, client); + } + + private void givenIncantationVarbits() + { + when(client.getVarbitValue(VarbitID.AGRITH_INCANTATION_1)).thenReturn(0); + when(client.getVarbitValue(VarbitID.AGRITH_INCANTATION_2)).thenReturn(1); + when(client.getVarbitValue(VarbitID.AGRITH_INCANTATION_3)).thenReturn(2); + when(client.getVarbitValue(VarbitID.AGRITH_INCANTATION_4)).thenReturn(3); + when(client.getVarbitValue(VarbitID.AGRITH_INCANTATION_5)).thenReturn(4); + } + + private void givenIncantationChatMenu() + { + Widget option = mock(Widget.class); + when(option.getText()).thenReturn("Nahudu"); + Widget menu = mock(Widget.class); + when(menu.getChildren()).thenReturn(new Widget[]{mock(Widget.class), mock(Widget.class), option}); + when(menu.getChild(2)).thenReturn(option); + when(client.getWidget(InterfaceID.CHATMENU, 1)).thenReturn(menu); + } + + private void updateChoice() throws Exception + { + Method method = IncantationStep.class.getDeclaredMethod("updateChoiceIfRequired"); + method.setAccessible(true); + method.invoke(step); + } + + private int position() throws Exception + { + Field field = IncantationStep.class.getDeclaredField("incantationPosition"); + field.setAccessible(true); + return field.getInt(step); + } + + @Test + public void chantOnInventorySigilResetsPosition() throws Exception + { + givenIncantationVarbits(); + givenIncantationChatMenu(); + step.updateHints(); + updateChoice(); + updateChoice(); + + Widget sigil = mock(Widget.class); + when(sigil.getId()).thenReturn(InterfaceID.Inventory.ITEMS); + MenuEntry entry = mock(MenuEntry.class); + when(entry.getWidget()).thenReturn(sigil); + when(entry.getOption()).thenReturn("Chant"); + step.onMenuOptionClicked(new MenuOptionClicked(entry)); + + assertEquals(0, position()); + } + + @Test + public void repeatedChantMenusDoNotRunPastIncantation() throws Exception + { + givenIncantationVarbits(); + givenIncantationChatMenu(); + step.updateHints(); + + for (int i = 0; i < 6; i++) + { + updateChoice(); + } + + assertEquals(1, position()); + } + + @Test + public void chantMenuBeforeOrderIsKnownIsIgnored() throws Exception + { + givenIncantationChatMenu(); + step.updateHints(); + + updateChoice(); + + assertEquals(0, position()); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/questhelper/managers/QuestManagerItemRequirementsTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/questhelper/managers/QuestManagerItemRequirementsTest.java new file mode 100644 index 00000000000..5d5d0ae0ffb --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/questhelper/managers/QuestManagerItemRequirementsTest.java @@ -0,0 +1,44 @@ +package net.runelite.client.plugins.microbot.questhelper.managers; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.plugins.microbot.questhelper.helpers.miniquests.alfredgrimhandsbarcrawl.AlfredGrimhandsBarcrawl; +import net.runelite.client.plugins.microbot.questhelper.questhelpers.QuestHelper; +import net.runelite.client.plugins.microbot.questhelper.questinfo.QuestHelperQuest; +import net.runelite.client.plugins.microbot.questhelper.requirements.item.ItemRequirement; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; + +public class QuestManagerItemRequirementsTest +{ + private AlfredGrimhandsBarcrawl helper; + + @Before + public void setUp() throws Exception + { + helper = new AlfredGrimhandsBarcrawl(); + helper.setQuest(QuestHelperQuest.ALFRED_GRIMHANDS_BARCRAWL); + Field field = QuestHelper.class.getDeclaredField("configManager"); + field.setAccessible(true); + field.set(helper, mock(ConfigManager.class)); + } + + @Test + public void initializesRequirementsBeforeReadingThem() + { + SortedMap> reqs = new TreeMap<>(); + SortedMap> recommended = new TreeMap<>(); + + QuestManager.collectItemRequirements(List.of(helper), reqs, recommended); + + assertEquals(1, reqs.get(QuestHelperQuest.ALFRED_GRIMHANDS_BARCRAWL).size()); + assertNotNull(recommended.get(QuestHelperQuest.ALFRED_GRIMHANDS_BARCRAWL)); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/questhelper/panel/QuestOverviewPanelTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/questhelper/panel/QuestOverviewPanelTest.java new file mode 100644 index 00000000000..e80fe6da27d --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/questhelper/panel/QuestOverviewPanelTest.java @@ -0,0 +1,30 @@ +package net.runelite.client.plugins.microbot.questhelper.panel; + +import java.util.Collections; +import net.runelite.client.plugins.microbot.questhelper.QuestHelperPlugin; +import net.runelite.client.plugins.microbot.questhelper.managers.QuestManager; +import net.runelite.client.plugins.microbot.questhelper.questhelpers.QuestHelper; +import org.junit.Test; + +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class QuestOverviewPanelTest +{ + @Test + public void addQuestToleratesCurrentStepClearedBeforeEdtRuns() + { + QuestOverviewPanel panel = new QuestOverviewPanel(mock(QuestHelperPlugin.class), mock(QuestManager.class)); + QuestHelper quest = mock(QuestHelper.class); + when(quest.getPanels()).thenReturn(Collections.emptyList()); + when(quest.getCurrentStep()).thenReturn(null); + + panel.addQuest(quest, true); + + assertSame(quest, panel.currentQuest); + verify(quest, never()).getQuest(); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/questhelper/questhelpers/QuestHelperClientMeetsRequirementsTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/questhelper/questhelpers/QuestHelperClientMeetsRequirementsTest.java new file mode 100644 index 00000000000..d373f288160 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/questhelper/questhelpers/QuestHelperClientMeetsRequirementsTest.java @@ -0,0 +1,40 @@ +package net.runelite.client.plugins.microbot.questhelper.questhelpers; + +import java.lang.reflect.Field; +import net.runelite.api.Client; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.plugins.microbot.questhelper.helpers.quests.currentaffairs.CurrentAffairs; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class QuestHelperClientMeetsRequirementsTest +{ + private CurrentAffairs helper; + + @Before + public void setUp() throws Exception + { + helper = new CurrentAffairs(); + Client client = mock(Client.class); + when(client.getIntStack()).thenReturn(new int[]{1}); + inject("client", client); + inject("configManager", mock(ConfigManager.class)); + } + + private void inject(String name, Object value) throws Exception + { + Field field = QuestHelper.class.getDeclaredField(name); + field.setAccessible(true); + field.set(helper, value); + } + + @Test + public void initializesRequirementsBeforeReadingThem() + { + assertFalse(helper.clientMeetsRequirements()); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPanelClueLocationTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPanelClueLocationTest.java new file mode 100644 index 00000000000..d0830e521fe --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPanelClueLocationTest.java @@ -0,0 +1,122 @@ +package net.runelite.client.plugins.microbot.shortestpath; + +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.plugins.PluginManager; +import net.runelite.client.plugins.cluescrolls.ClueScrollPlugin; +import net.runelite.client.plugins.cluescrolls.clues.ClueScroll; +import net.runelite.client.plugins.cluescrolls.clues.LocationClueScroll; +import net.runelite.client.plugins.cluescrolls.clues.LocationsClueScroll; +import net.runelite.client.plugins.microbot.Microbot; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +public class ShortestPathPanelClueLocationTest +{ + private static final WorldPoint LOCATION = new WorldPoint(2440, 5170, 0); + + private final AtomicBoolean onClientThread = new AtomicBoolean(false); + private MockedStatic microbot; + private ClientThread clientThread; + private ClueScrollPlugin cluePlugin; + private ShortestPathPanel panel; + + @Before + public void setUp() + { + clientThread = mock(ClientThread.class); + when(clientThread.runOnClientThreadOptional(any())).thenAnswer(inv -> { + Callable callable = inv.getArgument(0); + onClientThread.set(true); + try + { + return Optional.ofNullable(callable.call()); + } + finally + { + onClientThread.set(false); + } + }); + + cluePlugin = mock(ClueScrollPlugin.class); + PluginManager pluginManager = mock(PluginManager.class); + when(pluginManager.getPlugins()).thenReturn(Collections.singletonList(cluePlugin)); + + microbot = Mockito.mockStatic(Microbot.class); + microbot.when(Microbot::getClientThread).thenReturn(clientThread); + microbot.when(Microbot::getPluginManager).thenReturn(pluginManager); + + panel = mock(ShortestPathPanel.class, Mockito.CALLS_REAL_METHODS); + } + + @After + public void tearDown() + { + microbot.close(); + } + + @Test + public void resolvesSingleLocationOnClientThread() + { + ClueScroll clue = mock(ClueScroll.class, withSettings().extraInterfaces(LocationClueScroll.class)); + when(((LocationClueScroll) clue).getLocation(cluePlugin)).thenAnswer(inv -> { + if (!onClientThread.get()) + { + throw new IllegalStateException("must be called on client thread"); + } + return LOCATION; + }); + when(cluePlugin.getClue()).thenReturn(clue); + + assertEquals(LOCATION, panel.getCurrentClueLocation()); + } + + @Test + public void resolvesMultipleLocationsOnClientThread() + { + ClueScroll clue = mock(ClueScroll.class, withSettings().extraInterfaces(LocationsClueScroll.class)); + when(((LocationsClueScroll) clue).getLocations(cluePlugin)).thenAnswer(inv -> { + if (!onClientThread.get()) + { + throw new IllegalStateException("must be called on client thread"); + } + return new WorldPoint[]{LOCATION}; + }); + when(cluePlugin.getClue()).thenReturn(clue); + + assertEquals(LOCATION, panel.getCurrentClueLocation()); + } + + @Test + public void returnsNullWhenClientThreadReturnsNothing() + { + ClueScroll clue = mock(ClueScroll.class, withSettings().extraInterfaces(LocationClueScroll.class)); + when(cluePlugin.getClue()).thenReturn(clue); + doReturn(Optional.empty()).when(clientThread).runOnClientThreadOptional(any()); + + assertNull(panel.getCurrentClueLocation()); + } + + @Test + public void returnsNullWithoutActiveClue() + { + when(cluePlugin.getClue()).thenReturn(null); + + assertNull(panel.getCurrentClueLocation()); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPanelStartWalkingTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPanelStartWalkingTest.java new file mode 100644 index 00000000000..29afa9d730c --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPanelStartWalkingTest.java @@ -0,0 +1,84 @@ +package net.runelite.client.plugins.microbot.shortestpath; + +import java.lang.reflect.Field; +import javax.swing.JComboBox; +import javax.swing.JTextField; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.util.walker.enums.SlayerMasters; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ShortestPathPanelStartWalkingTest +{ + private ShortestPathPanel panel; + private ShortestPathScript script; + + @Before + public void setUp() throws Exception + { + panel = mock(ShortestPathPanel.class, Mockito.CALLS_REAL_METHODS); + ShortestPathPlugin plugin = mock(ShortestPathPlugin.class); + script = mock(ShortestPathScript.class); + when(plugin.getShortestPathScript()).thenReturn(script); + setField("plugin", plugin); + } + + @Test + public void startWalkingIgnoresNullTarget() + { + panel.startWalking(null); + + verify(script, never()).setTriggerWalker(any()); + } + + @Test + public void startWalkingTriggersWalkerForTarget() + { + WorldPoint target = new WorldPoint(3200, 3200, 0); + + panel.startWalking(target); + + verify(script).setTriggerWalker(target); + } + + @Test + public void slayerMasterWithoutLocationDoesNotTriggerWalker() throws Exception + { + JComboBox comboBox = new JComboBox<>(SlayerMasters.values()); + comboBox.setSelectedItem(SlayerMasters.NONE); + setField("slayerMasterComboBox", comboBox); + + assertNull(panel.getSlayerMasterTarget()); + panel.startWalking(panel.getSlayerMasterTarget()); + + verify(script, never()).setTriggerWalker(any()); + } + + @Test + public void invalidCustomLocationDoesNotTriggerWalker() throws Exception + { + setField("xField", new JTextField("abc")); + setField("yField", new JTextField("3200")); + setField("zField", new JTextField("0")); + + assertNull(panel.getCustomLocation()); + panel.startWalking(panel.getCustomLocation()); + + verify(script, never()).setTriggerWalker(any()); + } + + private void setField(String name, Object value) throws Exception + { + Field field = ShortestPathPanel.class.getDeclaredField(name); + field.setAccessible(true); + field.set(panel, value); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/ui/MicrobotConfigPanelInventorySetupsTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/ui/MicrobotConfigPanelInventorySetupsTest.java new file mode 100644 index 00000000000..ee90c049335 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/ui/MicrobotConfigPanelInventorySetupsTest.java @@ -0,0 +1,76 @@ +package net.runelite.client.plugins.microbot.ui; + +import net.runelite.client.config.ConfigManager; +import net.runelite.client.plugins.microbot.inventorysetups.InventorySetup; +import org.junit.Test; + +import javax.swing.JComboBox; +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class MicrobotConfigPanelInventorySetupsTest { + private static final String GROUP = "group"; + private static final String KEY = "setup"; + + private final ConfigManager configManager = mock(ConfigManager.class); + + @Test + public void nullSetupsReturnsEmptyBoxWithoutTouchingConfig() { + JComboBox box = MicrobotConfigPanel.createInventorySetupsComboBox(configManager, GROUP, KEY, null); + + assertEquals(0, box.getItemCount()); + verify(configManager, never()).setConfiguration(anyString(), anyString(), anyString()); + verify(configManager, never()).setConfiguration(anyString(), anyString(), any(Object.class)); + verify(configManager, never()).sendConfig(); + } + + @Test + public void emptySetupsClearsConfigWithoutSyncSave() { + JComboBox box = MicrobotConfigPanel.createInventorySetupsComboBox(configManager, GROUP, KEY, Collections.emptyList()); + + assertEquals(0, box.getItemCount()); + verify(configManager).setConfiguration(GROUP, KEY, ""); + verify(configManager, never()).sendConfig(); + } + + @Test + public void missingStoredSetupDefaultsToFirstWithoutSyncSave() { + InventorySetup first = setup("first"); + InventorySetup second = setup("second"); + + JComboBox box = MicrobotConfigPanel.createInventorySetupsComboBox(configManager, GROUP, KEY, Arrays.asList(first, second)); + + assertEquals(2, box.getItemCount()); + assertSame(first, box.getSelectedItem()); + verify(configManager).setConfiguration(GROUP, KEY, first); + verify(configManager, never()).sendConfig(); + } + + @Test + public void storedSetupIsSelectedByName() { + InventorySetup first = setup("first"); + InventorySetup second = setup("second"); + InventorySetup stored = setup("second"); + when(configManager.getConfiguration(GROUP, KEY, InventorySetup.class)).thenReturn(stored); + + JComboBox box = MicrobotConfigPanel.createInventorySetupsComboBox(configManager, GROUP, KEY, Arrays.asList(first, second)); + + assertSame(second, box.getSelectedItem()); + verify(configManager, never()).sendConfig(); + } + + private static InventorySetup setup(String name) { + InventorySetup setup = mock(InventorySetup.class); + when(setup.getName()).thenReturn(name); + return setup; + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginListPanelOpenConfigTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginListPanelOpenConfigTest.java new file mode 100644 index 00000000000..95892b73212 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginListPanelOpenConfigTest.java @@ -0,0 +1,32 @@ +package net.runelite.client.plugins.microbot.ui; + +import javax.inject.Provider; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.EventBus; +import net.runelite.client.externalplugins.ExternalPluginManager; +import net.runelite.client.plugins.PluginManager; +import net.runelite.client.plugins.microbot.externalplugins.MicrobotPluginManager; +import org.junit.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; + +public class MicrobotPluginListPanelOpenConfigTest { + @Test + @SuppressWarnings("unchecked") + public void openingPluginWithoutConfigDescriptorDoesNotBuildConfigPanel() { + Provider configPanelProvider = mock(Provider.class); + MicrobotPluginListPanel panel = new MicrobotPluginListPanel( + mock(ConfigManager.class), + mock(PluginManager.class), + mock(MicrobotPluginManager.class), + mock(ExternalPluginManager.class), + mock(EventBus.class), + configPanelProvider); + + panel.openConfigurationPanel(new MicrobotPluginConfigurationDescriptor( + "No Config", "Plugin without config", new String[0], null, null)); + + verifyNoInteractions(configPanelProvider); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/bank/BankLocationKourendRequirementsTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/bank/BankLocationKourendRequirementsTest.java new file mode 100644 index 00000000000..03b8b83a34c --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/bank/BankLocationKourendRequirementsTest.java @@ -0,0 +1,67 @@ +package net.runelite.client.plugins.microbot.util.bank; + +import net.runelite.api.Quest; +import net.runelite.api.QuestState; +import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; + +public class BankLocationKourendRequirementsTest { + + private static final BankLocation[] KOUREND_BANKS = { + BankLocation.LOVAKENGJ, + BankLocation.HOSIDIUS, + BankLocation.PISCARILIUS, + BankLocation.ARCEUUS, + BankLocation.WINTERTODT, + BankLocation.SULPHUR_MINE, + BankLocation.ZEAH_SAND_BANK, + BankLocation.BLAST_MINE, + BankLocation.SHAYZIEN_BANK, + BankLocation.SHAYZIEN_CHEST, + BankLocation.GREAT_KOUREND_CASTLE, + BankLocation.HOSIDIUS_KITCHEN, + BankLocation.VINERY_BANK, + BankLocation.MOUNT_KARUULM + }; + + private MockedStatic player; + + @Before + public void setUp() { + player = Mockito.mockStatic(Rs2Player.class); + player.when(() -> Rs2Player.getQuestState(any(Quest.class))).thenReturn(QuestState.NOT_STARTED); + } + + @After + public void tearDown() { + player.close(); + } + + @Test + public void kourendBanksAreUsableWithoutClientOfKourend() { + player.when(Rs2Player::isMember).thenReturn(true); + player.when(Rs2Player::isInMemberWorld).thenReturn(true); + for (BankLocation bank : KOUREND_BANKS) { + assertTrue(bank.name(), bank.hasRequirements()); + } + assertFalse(BankLocation.CANIFIS.hasRequirements()); + } + + @Test + public void kourendBanksStillRequireMembership() { + player.when(Rs2Player::isMember).thenReturn(false); + player.when(Rs2Player::isInMemberWorld).thenReturn(false); + for (BankLocation bank : KOUREND_BANKS) { + assertFalse(bank.name(), bank.hasRequirements()); + } + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/bank/Rs2BankLootingBagViewTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/bank/Rs2BankLootingBagViewTest.java new file mode 100644 index 00000000000..e86e1aaf7c3 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/bank/Rs2BankLootingBagViewTest.java @@ -0,0 +1,72 @@ +package net.runelite.client.plugins.microbot.util.bank; + +import net.runelite.api.Client; +import net.runelite.api.widgets.Widget; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.plugins.microbot.Microbot; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.util.Optional; +import java.util.concurrent.Callable; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class Rs2BankLootingBagViewTest { + + private MockedStatic microbot; + private Client client; + + @Before + public void setUp() { + client = mock(Client.class); + ClientThread clientThread = mock(ClientThread.class); + when(clientThread.runOnClientThreadOptional(any())).thenAnswer(inv -> { + Callable callable = inv.getArgument(0); + return Optional.ofNullable(callable.call()); + }); + microbot = Mockito.mockStatic(Microbot.class); + microbot.when(Microbot::getClient).thenReturn(client); + microbot.when(Microbot::getClientThread).thenReturn(clientThread); + } + + @After + public void tearDown() { + microbot.close(); + } + + private void lootingBagItems(Widget[] children) { + Widget items = mock(Widget.class); + when(items.getChildren()).thenReturn(children); + when(client.getWidget(15, 11)).thenReturn(items); + } + + @Test + public void emptyWhenWidgetIsNotLoaded() { + when(client.getWidget(15, 11)).thenReturn(null); + assertTrue(Rs2Bank.isLootingBagViewEmpty()); + } + + @Test + public void emptyWhenChildrenAreNullOrEmpty() { + lootingBagItems(null); + assertTrue(Rs2Bank.isLootingBagViewEmpty()); + lootingBagItems(new Widget[0]); + assertTrue(Rs2Bank.isLootingBagViewEmpty()); + lootingBagItems(new Widget[]{null}); + assertTrue(Rs2Bank.isLootingBagViewEmpty()); + } + + @Test + public void notEmptyWhileFirstChildIsPresent() { + lootingBagItems(new Widget[]{mock(Widget.class)}); + assertFalse(Rs2Bank.isLootingBagViewEmpty()); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/bank/Rs2BankNullPlayerLocationTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/bank/Rs2BankNullPlayerLocationTest.java index a3fabadabf1..b070019b75d 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/bank/Rs2BankNullPlayerLocationTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/bank/Rs2BankNullPlayerLocationTest.java @@ -45,6 +45,20 @@ public void isNearBankIsFalseWithoutPlayerLocation() { assertFalse(Rs2Bank.isNearBank(BankLocation.GRAND_EXCHANGE, 10)); } + @Test + public void walkToBankIsFalseWithoutBankLocation() { + assertFalse(Rs2Bank.walkToBank()); + assertFalse(Rs2Bank.walkToBank(null)); + assertFalse(Rs2Bank.walkToBank(null, false)); + } + + @Test + public void miningGuildRequirementsAreUnmetWithoutPlayerLocation() { + player.when(Rs2Player::isMember).thenReturn(true); + player.when(Rs2Player::isInMemberWorld).thenReturn(true); + assertFalse(BankLocation.MINING_GUILD.hasRequirements()); + } + @Test public void distanceBetweenIsUnreachableForNullEndpoint() { WorldPoint point = new WorldPoint(3164, 3487, 0); diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/bank/Rs2BankUnloadedWidgetsTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/bank/Rs2BankUnloadedWidgetsTest.java new file mode 100644 index 00000000000..f1c91772717 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/bank/Rs2BankUnloadedWidgetsTest.java @@ -0,0 +1,88 @@ +package net.runelite.client.plugins.microbot.util.bank; + +import net.runelite.api.Client; +import net.runelite.api.widgets.ComponentID; +import net.runelite.api.widgets.Widget; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.plugins.microbot.Microbot; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.util.Optional; +import java.util.concurrent.Callable; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class Rs2BankUnloadedWidgetsTest { + + private MockedStatic microbot; + private Client client; + private ClientThread clientThread; + + @Before + public void setUp() { + client = mock(Client.class); + clientThread = mock(ClientThread.class); + when(clientThread.runOnClientThreadOptional(any())).thenAnswer(inv -> { + Callable callable = inv.getArgument(0); + return Optional.ofNullable(callable.call()); + }); + microbot = Mockito.mockStatic(Microbot.class); + microbot.when(Microbot::getClient).thenReturn(client); + microbot.when(Microbot::getClientThread).thenReturn(clientThread); + } + + @After + public void tearDown() { + microbot.close(); + } + + private void bankSizeText(String text) { + Widget size = mock(Widget.class); + when(size.getText()).thenReturn(text); + when(client.getWidget(ComponentID.BANK_ITEM_COUNT_TOP)).thenReturn(size); + } + + @Test + public void bankItemCountIsZeroWhenSizeTextIsBlank() { + bankSizeText(""); + assertEquals(0, Rs2Bank.getBankItemCount()); + bankSizeText(null); + assertEquals(0, Rs2Bank.getBankItemCount()); + bankSizeText("abc"); + assertEquals(0, Rs2Bank.getBankItemCount()); + } + + @Test + public void bankItemCountParsesPopulatedText() { + bankSizeText(" 412 "); + assertEquals(412, Rs2Bank.getBankItemCount()); + } + + @Test + public void scrollBankToSlotIsFalseWithoutItemContainer() { + when(client.getWidget(Rs2Bank.BANK_ITEM_CONTAINER)).thenReturn(null); + assertFalse(Rs2Bank.scrollBankToSlot(10)); + verify(clientThread, never()).invokeLater(any(Runnable.class)); + } + + @Test + public void scrollBankToSlotSurvivesContainerDisappearingMidCalculation() { + Widget container = mock(Widget.class); + when(client.getWidget(Rs2Bank.BANK_ITEM_CONTAINER)).thenReturn(container, (Widget) null); + assertTrue(Rs2Bank.scrollBankToSlot(10)); + verify(container).setScrollY(0); + verify(container, never()).getHeight(); + verify(clientThread).invokeLater(any(Runnable.class)); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/camera/Rs2CameraAngleToTileTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/camera/Rs2CameraAngleToTileTest.java new file mode 100644 index 00000000000..bc0ca4e2dbe --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/camera/Rs2CameraAngleToTileTest.java @@ -0,0 +1,85 @@ +package net.runelite.client.plugins.microbot.util.camera; + +import net.runelite.api.Client; +import net.runelite.api.Player; +import net.runelite.api.WorldView; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.plugins.microbot.Microbot; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.function.Supplier; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class Rs2CameraAngleToTileTest { + + private static final int WEST_CLIENT_YAW = 512 * 8; + + private MockedStatic microbot; + private Client client; + + @Before + public void setUp() { + client = mock(Client.class); + when(client.isClientThread()).thenReturn(true); + when(client.getCameraYaw()).thenReturn(WEST_CLIENT_YAW); + + ClientThread clientThread = mock(ClientThread.class); + when(clientThread.invoke(any(Supplier.class))).thenAnswer(inv -> ((Supplier) inv.getArgument(0)).get()); + when(clientThread.runOnClientThreadOptional(any())).thenAnswer(inv -> { + Callable callable = inv.getArgument(0); + return Optional.ofNullable(callable.call()); + }); + + microbot = Mockito.mockStatic(Microbot.class); + microbot.when(Microbot::getClient).thenReturn(client); + microbot.when(Microbot::getClientThread).thenReturn(clientThread); + } + + @After + public void tearDown() { + microbot.close(); + } + + @Test + public void worldPointWithoutLocalPlayerKeepsCurrentFacing() { + when(client.getLocalPlayer()).thenReturn(null); + + int angle = Rs2Camera.angleToTile(new WorldPoint(3200, 3210, 0)); + + assertEquals(180, angle); + assertEquals(0, Rs2Camera.getAngleTo(angle - 90)); + } + + @Test + public void localPointWithoutLocalPlayerKeepsCurrentFacing() { + when(client.getLocalPlayer()).thenReturn(null); + + assertEquals(180, Rs2Camera.angleToTile(new LocalPoint(6400, 6400))); + } + + @Test + public void worldPointWithLocalPlayerUsesBearingFromPlayer() { + WorldView worldView = mock(WorldView.class); + when(worldView.isTopLevel()).thenReturn(true); + Player player = mock(Player.class); + when(player.getWorldView()).thenReturn(worldView); + when(player.getWorldLocation()).thenReturn(new WorldPoint(3200, 3200, 0)); + when(client.getLocalPlayer()).thenReturn(player); + + assertEquals(90, Rs2Camera.angleToTile(new WorldPoint(3200, 3210, 0))); + assertEquals(0, Rs2Camera.angleToTile(new WorldPoint(3210, 3200, 0))); + assertEquals(270, Rs2Camera.angleToTile(new WorldPoint(3200, 3190, 0))); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/camera/Rs2CameraTileOnScreenTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/camera/Rs2CameraTileOnScreenTest.java new file mode 100644 index 00000000000..cd3d49cf9c0 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/camera/Rs2CameraTileOnScreenTest.java @@ -0,0 +1,114 @@ +package net.runelite.client.plugins.microbot.util.camera; + +import net.runelite.api.Client; +import net.runelite.api.Scene; +import net.runelite.api.TileObject; +import net.runelite.api.WorldView; +import net.runelite.api.coords.LocalPoint; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.plugins.microbot.Microbot; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.util.Optional; +import java.util.concurrent.Callable; + +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class Rs2CameraTileOnScreenTest { + + private static final int WORLD_VIEW_ID = WorldView.TOPLEVEL; + private static final int SCENE_SIZE = 104; + + private MockedStatic microbot; + private Client client; + private WorldView worldView; + + @Before + public void setUp() { + client = mock(Client.class); + worldView = mock(WorldView.class); + when(worldView.getId()).thenReturn(WORLD_VIEW_ID); + when(worldView.isTopLevel()).thenReturn(true); + when(worldView.getSizeX()).thenReturn(SCENE_SIZE); + when(worldView.getSizeY()).thenReturn(SCENE_SIZE); + when(worldView.getTileHeight(anyInt(), anyInt(), anyInt())).thenThrow(new ArrayIndexOutOfBoundsException("Index 111 out of bounds for length 105")); + when(client.getWorldView(WORLD_VIEW_ID)).thenReturn(worldView); + when(client.getViewportWidth()).thenReturn(765); + when(client.getViewportHeight()).thenReturn(503); + + ClientThread clientThread = mock(ClientThread.class); + when(clientThread.runOnClientThreadOptional(any())).thenAnswer(inv -> { + Callable callable = inv.getArgument(0); + return Optional.ofNullable(callable.call()); + }); + + microbot = Mockito.mockStatic(Microbot.class); + microbot.when(Microbot::getClient).thenReturn(client); + microbot.when(Microbot::getClientThread).thenReturn(clientThread); + } + + @After + public void tearDown() { + microbot.close(); + } + + @Test + public void localPointPastSceneEdgeIsNotOnScreen() { + assertFalse(Rs2Camera.isTileOnScreen(sceneTile(111, 50))); + assertFalse(Rs2Camera.isTileOnScreen(sceneTile(50, 126))); + verify(worldView, never()).getTileHeight(anyInt(), anyInt(), anyInt()); + } + + @Test + public void localPointBeforeSceneOriginIsNotOnScreen() { + assertFalse(Rs2Camera.isTileOnScreen(sceneTile(-5, 50))); + assertFalse(Rs2Camera.isTileOnScreen(sceneTile(50, -5))); + verify(worldView, never()).getTileHeight(anyInt(), anyInt(), anyInt()); + } + + @Test + public void localPointInUnloadedWorldViewIsNotOnScreen() { + LocalPoint localPoint = new LocalPoint(50 * 128 + 64, 50 * 128 + 64, 7); + assertFalse(Rs2Camera.isTileOnScreen(localPoint)); + verify(worldView, never()).getTileHeight(anyInt(), anyInt(), anyInt()); + } + + @Test + public void nullLocalPointIsNotOnScreen() { + assertFalse(Rs2Camera.isTileOnScreen((LocalPoint) null)); + } + + @Test + public void tileObjectPastSceneEdgeIsNotOnScreen() { + TileObject tileObject = mock(TileObject.class); + when(tileObject.getLocalLocation()).thenReturn(sceneTile(109, 50)); + assertFalse(Rs2Camera.isTileOnScreen(tileObject)); + verify(worldView, never()).getTileHeight(anyInt(), anyInt(), anyInt()); + } + + @Test + public void localPointInsideSceneStillReachesPerspective() { + Scene scene = mock(Scene.class); + when(scene.getExtendedTileSettings()).thenReturn(new byte[4][184][184]); + when(worldView.getScene()).thenReturn(scene); + doReturn(0).when(worldView).getTileHeight(anyInt(), anyInt(), anyInt()); + Rs2Camera.isTileOnScreen(sceneTile(103, 103)); + verify(worldView, atLeastOnce()).getTileHeight(anyInt(), anyInt(), anyInt()); + } + + private static LocalPoint sceneTile(int sceneX, int sceneY) { + return new LocalPoint(sceneX * 128 + 64, sceneY * 128 + 64, WORLD_VIEW_ID); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/combat/Rs2CombatSetAttackStyleTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/combat/Rs2CombatSetAttackStyleTest.java new file mode 100644 index 00000000000..d33b4e4e114 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/combat/Rs2CombatSetAttackStyleTest.java @@ -0,0 +1,79 @@ +package net.runelite.client.plugins.microbot.util.combat; + +import net.runelite.api.Client; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class Rs2CombatSetAttackStyleTest { + + private static final int STYLE_WIDGET_ID = 38862852; + + private MockedStatic microbot; + private MockedStatic widgets; + private Client client; + private Widget styleWidget; + + @Before + public void setUp() { + client = mock(Client.class); + styleWidget = mock(Widget.class); + when(styleWidget.getId()).thenReturn(STYLE_WIDGET_ID); + + microbot = Mockito.mockStatic(Microbot.class); + microbot.when(Microbot::getClient).thenReturn(client); + widgets = Mockito.mockStatic(Rs2Widget.class); + } + + @After + public void tearDown() { + widgets.close(); + microbot.close(); + } + + @Test + public void clicksStyleWhenLabelWidgetIsMissing() { + when(client.getWidget(WidgetInfo.COMBAT_STYLE_ONE)).thenReturn(styleWidget); + widgets.when(() -> Rs2Widget.getWidget(STYLE_WIDGET_ID + 3)).thenReturn(null); + + assertTrue(Rs2Combat.setAttackStyle(WidgetInfo.COMBAT_STYLE_ONE)); + + microbot.verify(() -> Microbot.log("Setting attack style to COMBAT_STYLE_ONE")); + widgets.verify(() -> Rs2Widget.clickWidget(styleWidget)); + } + + @Test + public void logsLabelTextWhenLabelWidgetIsPresent() { + Widget label = mock(Widget.class); + when(label.getText()).thenReturn("Accurate"); + when(client.getWidget(WidgetInfo.COMBAT_STYLE_ONE)).thenReturn(styleWidget); + widgets.when(() -> Rs2Widget.getWidget(STYLE_WIDGET_ID + 3)).thenReturn(label); + + assertTrue(Rs2Combat.setAttackStyle(WidgetInfo.COMBAT_STYLE_ONE)); + + microbot.verify(() -> Microbot.log("Setting attack style to Accurate")); + widgets.verify(() -> Rs2Widget.clickWidget(styleWidget)); + } + + @Test + public void returnsFalseWithoutClickingWhenStyleWidgetIsMissing() { + when(client.getWidget(WidgetInfo.COMBAT_STYLE_ONE)).thenReturn(null); + + assertFalse(Rs2Combat.setAttackStyle(WidgetInfo.COMBAT_STYLE_ONE)); + + microbot.verify(() -> Microbot.log(anyString()), Mockito.never()); + widgets.verify(() -> Rs2Widget.clickWidget(styleWidget), Mockito.never()); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/coords/Rs2WorldPointEmptyPathTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/coords/Rs2WorldPointEmptyPathTest.java new file mode 100644 index 00000000000..15e5cdc755b --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/coords/Rs2WorldPointEmptyPathTest.java @@ -0,0 +1,44 @@ +package net.runelite.client.plugins.microbot.util.coords; + +import java.util.Collections; +import java.util.List; +import net.runelite.api.coords.WorldPoint; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class Rs2WorldPointEmptyPathTest +{ + private static final WorldPoint SOURCE = new WorldPoint(3200, 3200, 0); + private static final WorldPoint TARGET = new WorldPoint(3205, 3200, 0); + + @Test + public void distanceToPathIsUnreachableWhenPathIsEmpty() + { + Rs2WorldPoint point = new Rs2WorldPoint(SOURCE) + { + @Override + public List pathTo(WorldPoint other) + { + return Collections.emptyList(); + } + }; + + assertEquals(Integer.MAX_VALUE, point.distanceToPath(TARGET)); + } + + @Test + public void distanceToPathSumsCheckpointsWhenPathReachesTarget() + { + Rs2WorldPoint point = new Rs2WorldPoint(SOURCE) + { + @Override + public List pathTo(WorldPoint other) + { + return List.of(new WorldPoint(3203, 3200, 0), TARGET); + } + }; + + assertEquals(5, point.distanceToPath(TARGET)); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/depositbox/DepositBoxLocationKourendRequirementsTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/depositbox/DepositBoxLocationKourendRequirementsTest.java new file mode 100644 index 00000000000..fd8377e58f1 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/depositbox/DepositBoxLocationKourendRequirementsTest.java @@ -0,0 +1,71 @@ +package net.runelite.client.plugins.microbot.util.depositbox; + +import java.util.Optional; +import net.runelite.api.Quest; +import net.runelite.api.QuestState; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DepositBoxLocationKourendRequirementsTest { + + private static final DepositBoxLocation[] KOUREND_DEPOSIT_BOXES = { + DepositBoxLocation.ARCEUUS, + DepositBoxLocation.LOVAKENGJ, + DepositBoxLocation.SHAYZIEN, + DepositBoxLocation.KOUREND_CASTLE, + DepositBoxLocation.PORT_PISCARILIUS, + DepositBoxLocation.WINTERTODT_CAMP, + DepositBoxLocation.MOUNT_QUIDAMORTEM, + DepositBoxLocation.HOSIDIUS, + DepositBoxLocation.MOUNT_KARUULM + }; + + private MockedStatic microbot; + private MockedStatic player; + + @Before + public void setUp() { + ClientThread clientThread = mock(ClientThread.class); + when(clientThread.runOnClientThreadOptional(any())).thenReturn(Optional.of(false)); + microbot = Mockito.mockStatic(Microbot.class); + microbot.when(Microbot::getClientThread).thenReturn(clientThread); + player = Mockito.mockStatic(Rs2Player.class); + player.when(() -> Rs2Player.getQuestState(any(Quest.class))).thenReturn(QuestState.NOT_STARTED); + } + + @After + public void tearDown() { + player.close(); + microbot.close(); + } + + @Test + public void kourendDepositBoxesAreUsableWithoutClientOfKourend() { + player.when(Rs2Player::isMember).thenReturn(true); + player.when(Rs2Player::isInMemberWorld).thenReturn(true); + for (DepositBoxLocation box : KOUREND_DEPOSIT_BOXES) { + assertTrue(box.name(), box.hasRequirements()); + } + } + + @Test + public void kourendDepositBoxesStillRequireMembership() { + player.when(Rs2Player::isMember).thenReturn(false); + player.when(Rs2Player::isInMemberWorld).thenReturn(false); + for (DepositBoxLocation box : KOUREND_DEPOSIT_BOXES) { + assertFalse(box.name(), box.hasRequirements()); + } + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/discord/Rs2DiscordPlayerFieldTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/discord/Rs2DiscordPlayerFieldTest.java new file mode 100644 index 00000000000..fb6d67fcb93 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/discord/Rs2DiscordPlayerFieldTest.java @@ -0,0 +1,26 @@ +package net.runelite.client.plugins.microbot.util.discord; + +import net.runelite.client.plugins.microbot.util.discord.models.DiscordEmbed; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class Rs2DiscordPlayerFieldTest { + + @Test + public void nullPlayerNameFallsBackToUnknown() { + DiscordEmbed.Field field = Rs2Discord.createPlayerField(null); + assertEquals("Player", field.getName()); + assertEquals("Unknown", field.getValue()); + } + + @Test + public void emptyPlayerNameFallsBackToUnknown() { + assertEquals("Unknown", Rs2Discord.createPlayerField("").getValue()); + } + + @Test + public void playerNameIsUsedWhenPresent() { + assertEquals("Zezima", Rs2Discord.createPlayerField("Zezima").getValue()); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2CannonOwnershipTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2CannonOwnershipTest.java new file mode 100644 index 00000000000..ca42bdd8133 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2CannonOwnershipTest.java @@ -0,0 +1,100 @@ +package net.runelite.client.plugins.microbot.util.gameobject; + +import net.runelite.api.GameObject; +import net.runelite.api.TileObject; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.gameval.VarPlayerID; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.util.Optional; +import java.util.concurrent.Callable; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.when; + +public class Rs2CannonOwnershipTest { + + private static final WorldPoint CANNON_CENTER = new WorldPoint(3200, 3200, 0); + + private MockedStatic microbot; + private MockedStatic inventory; + private MockedStatic gameObject; + private GameObject cannon; + + @Before + public void setUp() { + ClientThread clientThread = mock(ClientThread.class); + when(clientThread.runOnClientThreadOptional(any(Callable.class))).thenReturn(Optional.of(0)); + microbot = Mockito.mockStatic(Microbot.class); + microbot.when(Microbot::getClientThread).thenReturn(clientThread); + + inventory = Mockito.mockStatic(Rs2Inventory.class); + inventory.when(() -> Rs2Inventory.hasItemAmount(eq("cannonball"), anyInt(), anyBoolean())).thenReturn(true); + + cannon = mock(GameObject.class); + when(cannon.getWorldLocation()).thenReturn(CANNON_CENTER); + gameObject = Mockito.mockStatic(Rs2GameObject.class); + gameObject.when(() -> Rs2GameObject.findObject(any(Integer[].class))).thenReturn(cannon); + } + + @After + public void tearDown() { + gameObject.close(); + inventory.close(); + microbot.close(); + Microbot.pauseAllScripts.set(false); + } + + private void ownCannonAt(int coord) { + microbot.when(() -> Microbot.getVarbitPlayerValue(VarPlayerID.OWNEDMCANNON)).thenReturn(coord); + } + + private static int coordOf(int x, int y, int plane) { + return (plane << 28) | (x << 14) | y; + } + + @Test + public void refillReturnsFalseWhenNoCannonOwned() { + ownCannonAt(0); + assertFalse(Rs2Cannon.refill(15)); + assertFalse(Microbot.pauseAllScripts.get()); + gameObject.verify(() -> Rs2GameObject.interact(any(TileObject.class), anyString()), never()); + } + + @Test + public void refillReturnsFalseWhenOwnedCannonIsElsewhere() { + ownCannonAt(coordOf(3300, 3300, 0)); + assertFalse(Rs2Cannon.refill(15)); + assertFalse(Microbot.pauseAllScripts.get()); + gameObject.verify(() -> Rs2GameObject.interact(any(TileObject.class), anyString()), never()); + } + + @Test + public void repairReturnsFalseWhenNoCannonOwned() { + ownCannonAt(-1); + assertFalse(Rs2Cannon.repair()); + gameObject.verify(() -> Rs2GameObject.interact(any(TileObject.class), anyString()), never()); + } + + @Test + public void repairInteractsWithOwnedCannonWithoutCannonPlugin() { + ownCannonAt(coordOf(CANNON_CENTER.getX() - 1, CANNON_CENTER.getY() - 1, CANNON_CENTER.getPlane())); + assertTrue(Rs2Cannon.repair()); + gameObject.verify(() -> Rs2GameObject.interact((TileObject) cannon, "Repair")); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObjectClickObjectNullLocationTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObjectClickObjectNullLocationTest.java new file mode 100644 index 00000000000..aaea7b50956 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObjectClickObjectNullLocationTest.java @@ -0,0 +1,66 @@ +package net.runelite.client.plugins.microbot.util.gameobject; + +import net.runelite.api.Client; +import net.runelite.api.TileObject; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class Rs2GameObjectClickObjectNullLocationTest { + + private MockedStatic microbot; + private MockedStatic player; + private MockedStatic walker; + private boolean savedCantReachTarget; + + @Before + public void setUp() { + savedCantReachTarget = Microbot.cantReachTarget; + Microbot.cantReachTarget = false; + Client client = mock(Client.class); + when(client.getLocalPlayer()).thenReturn(null); + microbot = Mockito.mockStatic(Microbot.class); + microbot.when(Microbot::getClient).thenReturn(client); + player = Mockito.mockStatic(Rs2Player.class); + walker = Mockito.mockStatic(Rs2Walker.class); + } + + @After + public void tearDown() { + walker.close(); + player.close(); + microbot.close(); + Microbot.cantReachTarget = savedCantReachTarget; + } + + @Test + public void returnsFalseWhenPlayerLocationUnknown() { + player.when(Rs2Player::getWorldLocation).thenReturn(null); + TileObject object = mock(TileObject.class); + when(object.getWorldLocation()).thenReturn(new WorldPoint(3200, 3200, 0)); + + assertFalse(Rs2GameObject.clickObject(object, "Bank")); + walker.verify(() -> Rs2Walker.walkTo(any(WorldPoint.class)), Mockito.never()); + } + + @Test + public void returnsFalseWhenObjectLocationUnknown() { + player.when(Rs2Player::getWorldLocation).thenReturn(new WorldPoint(3200, 3200, 0)); + TileObject object = mock(TileObject.class); + when(object.getWorldLocation()).thenReturn(null); + + assertFalse(Rs2GameObject.clickObject(object, "Bank")); + walker.verify(() -> Rs2Walker.walkTo(any(WorldPoint.class)), Mockito.never()); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObjectSmallWorldViewTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObjectSmallWorldViewTest.java new file mode 100644 index 00000000000..264d9fe405a --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObjectSmallWorldViewTest.java @@ -0,0 +1,77 @@ +package net.runelite.client.plugins.microbot.util.gameobject; + +import net.runelite.api.Client; +import net.runelite.api.Player; +import net.runelite.api.Scene; +import net.runelite.api.Tile; +import net.runelite.api.WallObject; +import net.runelite.api.WorldView; +import net.runelite.api.coords.LocalPoint; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.plugins.microbot.Microbot; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.util.List; +import java.util.function.Supplier; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class Rs2GameObjectSmallWorldViewTest { + + private static final int BOAT_SIZE = 8; + + private MockedStatic microbot; + private Tile[][][] tiles; + + @Before + public void setUp() { + tiles = new Tile[4][BOAT_SIZE][BOAT_SIZE]; + + Scene scene = mock(Scene.class); + when(scene.getTiles()).thenReturn(tiles); + WorldView worldView = mock(WorldView.class); + when(worldView.getScene()).thenReturn(scene); + when(worldView.getPlane()).thenReturn(0); + Player player = mock(Player.class); + when(player.getWorldView()).thenReturn(worldView); + Client client = mock(Client.class); + when(client.getLocalPlayer()).thenReturn(player); + + ClientThread clientThread = mock(ClientThread.class); + when(clientThread.invoke(any(Supplier.class))).thenAnswer(inv -> ((Supplier) inv.getArgument(0)).get()); + + microbot = Mockito.mockStatic(Microbot.class); + microbot.when(Microbot::getClient).thenReturn(client); + microbot.when(Microbot::getClientThread).thenReturn(clientThread); + } + + @After + public void tearDown() { + microbot.close(); + } + + @Test + public void wallObjectQueryOnBoatWorldViewDoesNotOverflowTileArray() { + int last = BOAT_SIZE - 1; + LocalPoint location = new LocalPoint(last * 128 + 64, last * 128 + 64); + WallObject wall = mock(WallObject.class); + when(wall.getLocalLocation()).thenReturn(location); + Tile tile = mock(Tile.class); + when(tile.getWallObject()).thenReturn(wall); + when(tile.getLocalLocation()).thenReturn(location); + tiles[0][last][last] = tile; + + List walls = Rs2GameObject.getWallObjects(o -> true, location, 10 * 128); + + assertEquals(1, walls.size()); + assertSame(wall, walls.get(0)); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/grandexchange/GrandExchangeWidgetNullSlotTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/grandexchange/GrandExchangeWidgetNullSlotTest.java new file mode 100644 index 00000000000..73e9faaad38 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/grandexchange/GrandExchangeWidgetNullSlotTest.java @@ -0,0 +1,63 @@ +package net.runelite.client.plugins.microbot.util.grandexchange; + +import net.runelite.api.gameval.InterfaceID; +import net.runelite.api.widgets.Widget; +import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class GrandExchangeWidgetNullSlotTest { + @Test + public void getSlotReturnsNullForNullSlot() { + assertNull(GrandExchangeWidget.getSlot(null)); + } + + @Test + public void getOfferBuyButtonReturnsNullForNullSlot() { + assertNull(GrandExchangeWidget.getOfferBuyButton(null)); + } + + @Test + public void getOfferSellButtonReturnsNullForNullSlot() { + assertNull(GrandExchangeWidget.getOfferSellButton(null)); + } + + @Test + public void getConfirmReturnsNullWhenOfferContainerMissing() { + try (MockedStatic widgets = Mockito.mockStatic(Rs2Widget.class)) { + widgets.when(() -> Rs2Widget.getWidget(InterfaceID.GE_OFFERS, 26)).thenReturn(null); + assertNull(GrandExchangeWidget.getConfirm()); + } + } + + @Test + public void getConfirmReturnsNullWhenOfferContainerHasNoDynamicChildren() { + Widget container = mock(Widget.class); + when(container.getDynamicChildren()).thenReturn(null); + try (MockedStatic widgets = Mockito.mockStatic(Rs2Widget.class)) { + widgets.when(() -> Rs2Widget.getWidget(InterfaceID.GE_OFFERS, 26)).thenReturn(container); + assertNull(GrandExchangeWidget.getConfirm()); + } + } + + @Test + public void getConfirmFindsButtonInOfferContainer() { + Widget container = mock(Widget.class); + Widget button = mock(Widget.class); + when(container.getDynamicChildren()).thenReturn(new Widget[]{button}); + try (MockedStatic widgets = Mockito.mockStatic(Rs2Widget.class)) { + widgets.when(() -> Rs2Widget.getWidget(InterfaceID.GE_OFFERS, 26)).thenReturn(container); + widgets.when(() -> Rs2Widget.findWidget(eq("Confirm"), anyList(), anyBoolean())).thenReturn(button); + assertSame(button, GrandExchangeWidget.getConfirm()); + } + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/grandexchange/Rs2GrandExchangeConfirmTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/grandexchange/Rs2GrandExchangeConfirmTest.java new file mode 100644 index 00000000000..f9941baccce --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/grandexchange/Rs2GrandExchangeConfirmTest.java @@ -0,0 +1,37 @@ +package net.runelite.client.plugins.microbot.util.grandexchange; + +import net.runelite.api.gameval.InterfaceID; +import net.runelite.client.plugins.microbot.util.Global; +import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; +import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; +import org.junit.Test; +import org.mockito.Answers; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.anyString; + +public class Rs2GrandExchangeConfirmTest { + @Test + public void sellItemReportsFailureWhenOfferContainerClosesBeforeConfirm() { + try (MockedStatic ge = Mockito.mockStatic(Rs2GrandExchange.class, Answers.CALLS_REAL_METHODS); + MockedStatic inventory = Mockito.mockStatic(Rs2Inventory.class); + MockedStatic widgets = Mockito.mockStatic(Rs2Widget.class); + MockedStatic global = Mockito.mockStatic(Global.class, invocation -> + invocation.getMethod().getReturnType() == boolean.class ? true : Answers.RETURNS_DEFAULTS.answer(invocation))) + { + ge.when(Rs2GrandExchange::isOpen).thenReturn(true); + ge.when(Rs2GrandExchange::getAvailableSlots).thenReturn(new GrandExchangeSlots[]{GrandExchangeSlots.ONE}); + ge.when(Rs2GrandExchange::getOfferPrice).thenReturn(100L); + ge.when(Rs2GrandExchange::getOfferQuantity).thenReturn(5); + ge.when(Rs2GrandExchange::isOfferSetupOpen).thenReturn(false); + inventory.when(() -> Rs2Inventory.hasItem("Oak plank", false)).thenReturn(true); + inventory.when(() -> Rs2Inventory.interact("Oak plank", "Offer", false)).thenReturn(true); + widgets.when(() -> Rs2Widget.getWidget(InterfaceID.GE_OFFERS, 26)).thenReturn(null); + + assertFalse(Rs2GrandExchange.sellItem("Oak plank", 5, 100)); + widgets.verify(() -> Rs2Widget.clickWidget(anyString()), Mockito.never()); + } + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/inventory/Rs2InventoryInvokeMenuTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/inventory/Rs2InventoryInvokeMenuTest.java new file mode 100644 index 00000000000..dd7ab082136 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/inventory/Rs2InventoryInvokeMenuTest.java @@ -0,0 +1,69 @@ +package net.runelite.client.plugins.microbot.util.inventory; + +import net.runelite.api.widgets.Widget; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; +import net.runelite.client.plugins.microbot.util.grandexchange.Rs2GrandExchange; +import net.runelite.client.plugins.microbot.util.shop.Rs2Shop; +import net.runelite.client.plugins.microbot.util.tabs.Rs2Tab; +import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; +import org.junit.Test; +import org.mockito.Answers; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.util.Optional; +import java.util.concurrent.Callable; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class Rs2InventoryInvokeMenuTest { + + private static final int SHOP_INVENTORY_ITEM_CONTAINER = 19726336; + + @Test + public void sellItemFallsBackToItemActionsWhenNoContainerChildMatchesSlot() { + ClientThread clientThread = mock(ClientThread.class); + when(clientThread.runOnClientThreadOptional(any())).thenAnswer(invocation -> + Optional.ofNullable(((Callable) invocation.getArgument(0)).call())); + + Widget otherSlot = mock(Widget.class); + when(otherSlot.getIndex()).thenReturn(0); + Widget container = mock(Widget.class); + when(container.getChildren()).thenReturn(new Widget[]{otherSlot, null}); + + Rs2ItemModel item = mock(Rs2ItemModel.class); + when(item.getName()).thenReturn("Pot"); + when(item.getId()).thenReturn(1931); + when(item.getSlot()).thenReturn(5); + when(item.getInventoryActions()).thenReturn(new String[]{"Value", "Sell 1", "Sell 5", "Sell 10", "Sell 50"}); + + try (MockedStatic inventory = Mockito.mockStatic(Rs2Inventory.class, Answers.CALLS_REAL_METHODS); + MockedStatic microbot = Mockito.mockStatic(Microbot.class); + MockedStatic widgets = Mockito.mockStatic(Rs2Widget.class); + MockedStatic tabs = Mockito.mockStatic(Rs2Tab.class); + MockedStatic bank = Mockito.mockStatic(Rs2Bank.class); + MockedStatic ge = Mockito.mockStatic(Rs2GrandExchange.class); + MockedStatic shop = Mockito.mockStatic(Rs2Shop.class)) + { + microbot.when(Microbot::getClientThread).thenReturn(clientThread); + shop.when(Rs2Shop::isOpen).thenReturn(true); + widgets.when(() -> Rs2Widget.getWidget(SHOP_INVENTORY_ITEM_CONTAINER)).thenReturn(container); + inventory.when(() -> Rs2Inventory.get("Pot", true)).thenReturn(item); + inventory.when(Rs2Inventory::isItemSelected).thenReturn(false); + inventory.when(() -> Rs2Inventory.itemBounds(item)).thenReturn(null); + + assertTrue(Rs2Inventory.sellItem("Pot", "1")); + + microbot.verify(() -> Microbot.doInvoke(argThat(entry -> + entry.getIdentifier() == 2 + && entry.getParam0() == 5 + && entry.getParam1() == SHOP_INVENTORY_ITEM_CONTAINER), any())); + } + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerInstanceWorldLocationTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerInstanceWorldLocationTest.java new file mode 100644 index 00000000000..f0c83c7a1ac --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerInstanceWorldLocationTest.java @@ -0,0 +1,79 @@ +package net.runelite.client.plugins.microbot.util.player; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Callable; +import net.runelite.api.Client; +import net.runelite.api.Player; +import net.runelite.api.Scene; +import net.runelite.api.WorldView; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.plugins.microbot.Microbot; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +public class Rs2PlayerInstanceWorldLocationTest { + private final Map original = new HashMap<>(); + private Client client; + private WorldView worldView; + private Player player; + + @Before + public void setUp() throws Exception { + client = mock(Client.class); + ClientThread clientThread = mock(ClientThread.class); + worldView = mock(WorldView.class); + Scene scene = mock(Scene.class); + player = mock(Player.class); + replace("client", client); + replace("clientThread", clientThread); + when(clientThread.runOnClientThreadOptional(any())).thenAnswer(invocation -> + Optional.ofNullable(((Callable) invocation.getArgument(0)).call())); + when(client.getLocalPlayer()).thenReturn(player); + when(client.getTopLevelWorldView()).thenReturn(worldView); + when(client.getWorldView(anyInt())).thenReturn(worldView); + when(worldView.getScene()).thenReturn(scene); + when(scene.isInstance()).thenReturn(true); + when(worldView.getPlane()).thenReturn(1); + when(player.getWorldLocation()).thenReturn(new WorldPoint(3200, 3200, 0)); + } + + private void replace(String name, Object value) throws Exception { + Field field = Microbot.class.getDeclaredField(name); + field.setAccessible(true); + original.put(name, field.get(null)); + field.set(null, value); + } + + @After + public void tearDown() throws Exception { + for (Map.Entry entry : original.entrySet()) { + Field field = Microbot.class.getDeclaredField(entry.getKey()); + field.setAccessible(true); + field.set(null, entry.getValue()); + } + } + + @Test + public void fallsBackToWorldLocationWhenNoLocalPointResolves() { + when(player.getLocalLocation()).thenReturn(null); + assertEquals(new WorldPoint(3200, 3200, 0), Rs2Player.getWorldLocation_Internal()); + } + + @Test + public void fallsBackToPlayerLocalLocationWhenWorldPointIsNotInTopLevelScene() { + when(worldView.isInstance()).thenReturn(false); + when(worldView.getBaseX()).thenReturn(3000); + when(worldView.getBaseY()).thenReturn(3100); + when(player.getLocalLocation()).thenReturn(LocalPoint.fromScene(10, 20)); + assertEquals(new WorldPoint(3010, 3120, 1), Rs2Player.getWorldLocation_Internal()); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/shop/Rs2ShopItemBoundsTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/shop/Rs2ShopItemBoundsTest.java new file mode 100644 index 00000000000..83d0ae6099b --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/shop/Rs2ShopItemBoundsTest.java @@ -0,0 +1,94 @@ +package net.runelite.client.plugins.microbot.util.shop; + +import net.runelite.api.widgets.Widget; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; +import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class Rs2ShopItemBoundsTest { + + private static final int SHOP_ITEMS_WIDGET = 19660816; + private static final int ITEM_ID = 1931; + + private MockedStatic widgets; + private MockedStatic microbot; + private List previousShopItems; + + @Before + public void setUp() { + widgets = Mockito.mockStatic(Rs2Widget.class); + microbot = Mockito.mockStatic(Microbot.class); + previousShopItems = Rs2Shop.shopItems; + Rs2ItemModel item = mock(Rs2ItemModel.class); + when(item.getId()).thenReturn(ITEM_ID); + when(item.getName()).thenReturn("Pot"); + when(item.getQuantity()).thenReturn(5); + when(item.getSlot()).thenReturn(11); + Rs2Shop.shopItems = new ArrayList<>(Collections.singletonList(item)); + } + + @After + public void tearDown() { + Rs2Shop.shopItems = previousShopItems; + microbot.close(); + widgets.close(); + } + + private void assertInvokedWith(Rectangle expected) { + assertTrue(Rs2Shop.buyItem(ITEM_ID, "1")); + microbot.verify(() -> Microbot.doInvoke(any(), eq(expected))); + microbot.verify(() -> Microbot.logStackTrace(anyString(), any(Exception.class)), Mockito.never()); + } + + @Test + public void buyFallsBackToDefaultBoundsWhenShopWidgetMissing() { + widgets.when(() -> Rs2Widget.getWidget(SHOP_ITEMS_WIDGET)).thenReturn(null); + assertInvokedWith(new Rectangle(1, 1)); + } + + @Test + public void buyFallsBackToDefaultBoundsWhenShopChildrenNotPopulated() { + Widget container = mock(Widget.class); + when(container.getDynamicChildren()).thenReturn(new Widget[0]); + widgets.when(() -> Rs2Widget.getWidget(SHOP_ITEMS_WIDGET)).thenReturn(container); + assertInvokedWith(new Rectangle(1, 1)); + } + + @Test + public void buyFallsBackToDefaultBoundsWhenShopChildrenNull() { + Widget container = mock(Widget.class); + when(container.getDynamicChildren()).thenReturn(null); + widgets.when(() -> Rs2Widget.getWidget(SHOP_ITEMS_WIDGET)).thenReturn(container); + assertInvokedWith(new Rectangle(1, 1)); + } + + @Test + public void buyUsesSlotBoundsWhenShopChildrenPopulated() { + Rectangle slotBounds = new Rectangle(40, 60, 32, 32); + Widget slot = mock(Widget.class); + when(slot.getBounds()).thenReturn(slotBounds); + Widget[] children = new Widget[13]; + children[12] = slot; + Widget container = mock(Widget.class); + when(container.getDynamicChildren()).thenReturn(children); + widgets.when(() -> Rs2Widget.getWidget(SHOP_ITEMS_WIDGET)).thenReturn(container); + assertInvokedWith(slotBounds); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/skills/slayer/Rs2SlayerGetSlayerTaskTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/skills/slayer/Rs2SlayerGetSlayerTaskTest.java new file mode 100644 index 00000000000..071d686423c --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/skills/slayer/Rs2SlayerGetSlayerTaskTest.java @@ -0,0 +1,95 @@ +package net.runelite.client.plugins.microbot.util.skills.slayer; + +import java.util.Collections; +import net.runelite.api.gameval.DBTableID; +import net.runelite.api.gameval.VarPlayerID; +import net.runelite.client.plugins.microbot.Microbot; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class Rs2SlayerGetSlayerTaskTest { + private static final int BOSS_TASK_ID = 98; + private static final int REGULAR_TASK_ID = 7; + private static final int SUBLIST_ROW = 4000; + private static final int TASK_ROW = 5000; + + private MockedStatic microbot; + + @Before + public void setUp() { + microbot = Mockito.mockStatic(Microbot.class); + } + + @After + public void tearDown() { + microbot.close(); + } + + private void taskId(int id) { + microbot.when(() -> Microbot.getVarbitPlayerValue(VarPlayerID.SLAYER_TARGET)).thenReturn(id); + } + + @Test + public void regularTaskWithEmptyNameFieldReturnsNull() { + taskId(REGULAR_TASK_ID); + microbot.when(() -> Microbot.getDBRowsByValue(DBTableID.SlayerTask.ID, DBTableID.SlayerTask.COL_ID, 0, REGULAR_TASK_ID)) + .thenReturn(Collections.singletonList(TASK_ROW)); + microbot.when(() -> Microbot.getDBTableField(TASK_ROW, DBTableID.SlayerTask.COL_NAME_UPPERCASE, 0)) + .thenReturn(new Object[]{}); + + assertNull(Rs2Slayer.getSlayerTask()); + } + + @Test + public void bossTaskWithEmptySublistFieldReturnsNull() { + taskId(BOSS_TASK_ID); + microbot.when(() -> Microbot.getDBRowsByValue(DBTableID.SlayerTaskSublist.ID, DBTableID.SlayerTaskSublist.COL_SUBTABLE_ID, 0, BOSS_TASK_ID)) + .thenReturn(Collections.singletonList(SUBLIST_ROW)); + microbot.when(() -> Microbot.getDBTableField(SUBLIST_ROW, DBTableID.SlayerTaskSublist.COL_TASK, 0)) + .thenReturn(new Object[]{}); + + assertNull(Rs2Slayer.getSlayerTask()); + } + + @Test + public void bossTaskWithEmptyNameFieldReturnsNull() { + taskId(BOSS_TASK_ID); + microbot.when(() -> Microbot.getDBRowsByValue(DBTableID.SlayerTaskSublist.ID, DBTableID.SlayerTaskSublist.COL_SUBTABLE_ID, 0, BOSS_TASK_ID)) + .thenReturn(Collections.singletonList(SUBLIST_ROW)); + microbot.when(() -> Microbot.getDBTableField(SUBLIST_ROW, DBTableID.SlayerTaskSublist.COL_TASK, 0)) + .thenReturn(new Object[]{TASK_ROW}); + microbot.when(() -> Microbot.getDBTableField(TASK_ROW, DBTableID.SlayerTask.COL_NAME_UPPERCASE, 0)) + .thenReturn(new Object[]{}); + + assertNull(Rs2Slayer.getSlayerTask()); + } + + @Test + public void regularTaskReturnsName() { + taskId(REGULAR_TASK_ID); + microbot.when(() -> Microbot.getDBRowsByValue(DBTableID.SlayerTask.ID, DBTableID.SlayerTask.COL_ID, 0, REGULAR_TASK_ID)) + .thenReturn(Collections.singletonList(TASK_ROW)); + microbot.when(() -> Microbot.getDBTableField(TASK_ROW, DBTableID.SlayerTask.COL_NAME_UPPERCASE, 0)) + .thenReturn(new Object[]{"Goblins"}); + + assertEquals("Goblins", Rs2Slayer.getSlayerTask()); + } + + @Test + public void bossTaskReturnsName() { + taskId(BOSS_TASK_ID); + microbot.when(() -> Microbot.getDBRowsByValue(DBTableID.SlayerTaskSublist.ID, DBTableID.SlayerTaskSublist.COL_SUBTABLE_ID, 0, BOSS_TASK_ID)) + .thenReturn(Collections.singletonList(SUBLIST_ROW)); + microbot.when(() -> Microbot.getDBTableField(SUBLIST_ROW, DBTableID.SlayerTaskSublist.COL_TASK, 0)) + .thenReturn(new Object[]{TASK_ROW}); + microbot.when(() -> Microbot.getDBTableField(TASK_ROW, DBTableID.SlayerTask.COL_NAME_UPPERCASE, 0)) + .thenReturn(new Object[]{"Cerberus"}); + + assertEquals("Cerberus", Rs2Slayer.getSlayerTask()); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerPathEndpointArrivalTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerPathEndpointArrivalTest.java new file mode 100644 index 00000000000..8a868fb3f7c --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerPathEndpointArrivalTest.java @@ -0,0 +1,139 @@ +package net.runelite.client.plugins.microbot.util.walker; + +import net.runelite.api.Client; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.shortestpath.ShortestPathConfig; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.tile.Rs2Tile; +import net.runelite.client.plugins.microbot.util.walker.state.WalkerRouteState; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.util.HashMap; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Callable; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class Rs2WalkerPathEndpointArrivalTest +{ + private static final WorldPoint TARGET = new WorldPoint(3227, 3214, 0); + private static final List PATH = List.of( + new WorldPoint(3224, 3214, 0), + new WorldPoint(3225, 3214, 0), + new WorldPoint(3226, 3214, 0), + TARGET); + private static final String REACHED_ENDPOINT = "rs2walker:processWalk:reached-path-endpoint"; + + private MockedStatic microbot; + private MockedStatic player; + private MockedStatic tile; + private MockedStatic pathApi; + private ShortestPathConfig originalConfig; + private WorldPoint originalTarget; + private Rs2ActiveRouteStatus status; + + @Before + public void setUp() + { + originalConfig = Rs2Walker.config; + originalTarget = Rs2Walker.currentTarget; + Rs2Walker.config = mock(ShortestPathConfig.class); + Rs2Walker.clearWalkerDedupeForTesting(); + WalkerRouteState routeState = Rs2Walker.routeStateForTesting(); + routeState.lastRouteClearReason = ""; + routeState.interimTargetWp = null; + routeState.interimTargetIdx = -1; + routeState.stuckCount = 0; + routeState.lastMovedTimeMs = System.currentTimeMillis(); + + microbot = Mockito.mockStatic(Microbot.class); + microbot.when(Microbot::getClient).thenReturn(mock(Client.class)); + microbot.when(Microbot::isLoggedIn).thenReturn(true); + ClientThread clientThread = mock(ClientThread.class); + when(clientThread.runOnClientThreadOptional(any())).thenAnswer(invocation -> { + Callable callable = invocation.getArgument(0); + return Optional.ofNullable(callable.call()); + }); + microbot.when(Microbot::getClientThread).thenReturn(clientThread); + + player = Mockito.mockStatic(Rs2Player.class); + player.when(Rs2Player::getWorldLocation).thenReturn(TARGET); + + tile = Mockito.mockStatic(Rs2Tile.class); + tile.when(() -> Rs2Tile.getReachableTilesFromTile(any(WorldPoint.class), anyInt())).thenReturn(new HashMap<>()); + + status = mock(Rs2ActiveRouteStatus.class); + when(status.isPresent()).thenReturn(true); + when(status.isReady()).thenReturn(true); + when(status.getRawPath()).thenReturn(PATH); + when(status.getWalkablePath()).thenReturn(PATH); + when(status.getMetrics()).thenReturn(Optional.empty()); + pathApi = Mockito.mockStatic(Rs2PathApi.class); + pathApi.when(Rs2PathApi::getActiveRouteStatus).thenReturn(status); + pathApi.when(() -> Rs2PathApi.hasWalkableTileWithin(any(WorldPoint.class), anyInt())).thenReturn(true); + + Rs2Walker.currentTarget = TARGET; + } + + @After + public void tearDown() + { + pathApi.close(); + tile.close(); + player.close(); + microbot.close(); + Rs2Walker.currentTarget = originalTarget; + Rs2Walker.config = originalConfig; + Rs2Walker.clearWalkerDedupeForTesting(); + } + + @Test + public void standingOnTheEndpointThatIsTheTargetArrives() + { + WalkerState state = Rs2Walker.processWalkForTesting(TARGET, 0); + + assertEquals(WalkerState.ARRIVED, state); + assertEquals(REACHED_ENDPOINT, Rs2Walker.getLastRouteClearReason()); + assertNull(Rs2Walker.currentTarget); + } + + @Test + public void externalCancelBeforeTheEndpointCheckStillExits() + { + player.when(Rs2Player::isInteracting).thenAnswer(invocation -> { + Rs2Walker.currentTarget = null; + return false; + }); + + WalkerState state = Rs2Walker.processWalkForTesting(TARGET, 0); + + assertEquals(WalkerState.EXIT, state); + assertNotEquals(REACHED_ENDPOINT, Rs2Walker.getLastRouteClearReason()); + } + + @Test + public void endpointShortOfTheTightFinishThresholdDoesNotArrive() + { + WorldPoint farTarget = new WorldPoint(3230, 3214, 0); + Rs2Walker.currentTarget = farTarget; + + WalkerState state = Rs2Walker.processWalkForTesting(farTarget, 5); + + assertEquals(WalkerState.EXIT, state); + assertEquals(REACHED_ENDPOINT, Rs2Walker.getLastRouteClearReason()); + assertNull(Rs2Walker.currentTarget); + } +} diff --git a/runelite-client/src/test/resources/threadsafety/client-thread-guardrail-baseline.txt b/runelite-client/src/test/resources/threadsafety/client-thread-guardrail-baseline.txt index 3054b91a799..80f1e2a97d4 100644 --- a/runelite-client/src/test/resources/threadsafety/client-thread-guardrail-baseline.txt +++ b/runelite-client/src/test/resources/threadsafety/client-thread-guardrail-baseline.txt @@ -25,19 +25,11 @@ net.runelite.client.plugins.microbot.api.boat.Rs2BoatCache#getLocalBoat(): Rs2Bo net.runelite.client.plugins.microbot.api.boat.models.Rs2BoatModel#clickSailButton(): boolean -> net.runelite.api.widgets.Widget#getDynamicChildren(): Widget[] net.runelite.client.plugins.microbot.api.boat.models.Rs2BoatModel#setHeading(Heading): void -> net.runelite.api.Client#getTopLevelWorldView(): WorldView net.runelite.client.plugins.microbot.api.boat.models.Rs2BoatModel#setHeading(Heading): void -> net.runelite.api.WorldView#getId(): int -net.runelite.client.plugins.microbot.api.npc.Rs2NpcCache#getStream(): Stream -> net.runelite.api.Client#getTickCount(): int -net.runelite.client.plugins.microbot.api.npc.Rs2NpcCache#getStream(): Stream -> net.runelite.api.Client#getWorldView(int): WorldView -net.runelite.client.plugins.microbot.api.npc.Rs2NpcCache#getStream(): Stream -> net.runelite.api.IndexedObjectSet#stream(): Stream -net.runelite.client.plugins.microbot.api.npc.Rs2NpcCache#getStream(): Stream -> net.runelite.api.WorldView#npcs(): IndexedObjectSet net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel#click(String): boolean -> net.runelite.api.NPCComposition#getActions(): String[] -net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel#getId(): int -> net.runelite.api.NPC#getId(): int net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel#getIndex(): int -> net.runelite.api.NPC#getIndex(): int net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel#getMenuAction(int): MenuAction -> net.runelite.api.Client#isWidgetSelected(): boolean net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel#hasLineOfSight(): boolean -> net.runelite.api.Client#getTopLevelWorldView(): WorldView net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel#hasLineOfSight(): boolean -> net.runelite.api.coords.WorldArea#hasLineOfSightTo(WorldView, WorldPoint): boolean -net.runelite.client.plugins.microbot.api.player.Rs2PlayerCache#getStream(): Stream -> net.runelite.api.Client#getTickCount(): int -net.runelite.client.plugins.microbot.api.player.Rs2PlayerCache#getStream(): Stream -> net.runelite.api.Client#getWorldView(int): WorldView -net.runelite.client.plugins.microbot.api.player.Rs2PlayerCache#getStream(): Stream -> net.runelite.api.IndexedObjectSet#stream(): Stream net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel#(): void -> net.runelite.api.Client#getLocalPlayer(): Player net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel#getId(): int -> net.runelite.api.Player#getId(): int net.runelite.client.plugins.microbot.api.playerstate.Rs2PlayerStateCache#getQuestState(Quest): QuestState -> net.runelite.api.Quest#getId(): int @@ -60,8 +52,6 @@ net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel#cl net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel#click(String): boolean -> net.runelite.api.GameObject#sizeX(): int net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel#click(String): boolean -> net.runelite.api.GameObject#sizeY(): int net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel#click(String): boolean -> net.runelite.api.ObjectComposition#getActions(): String[] -net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel#click(String): boolean -> net.runelite.api.ObjectComposition#getImpostor(): ObjectComposition -net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel#click(String): boolean -> net.runelite.api.ObjectComposition#getImpostorIds(): int[] net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel#click(String): boolean -> net.runelite.api.ObjectComposition#getName(): String net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel#click(String): boolean -> net.runelite.api.WorldView#getId(): int net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel#getClickbox(): Shape -> net.runelite.api.TileObject#getClickbox(): Shape @@ -99,7 +89,6 @@ net.runelite.client.plugins.microbot.util.bank.Rs2Bank#isLockedSlot(int): boolea net.runelite.client.plugins.microbot.util.bank.Rs2Bank#isLockedSlot(int): boolean -> net.runelite.api.widgets.Widget#getChildren(): Widget[] net.runelite.client.plugins.microbot.util.bank.Rs2Bank#isWidgetLocked(Widget): boolean -> net.runelite.api.widgets.Widget#getActions(): String[] net.runelite.client.plugins.microbot.util.bank.Rs2Bank#itemBounds(Rs2ItemModel): Rectangle -> net.runelite.api.widgets.Widget#getBounds(): Rectangle -net.runelite.client.plugins.microbot.util.bank.Rs2Bank#lambda$depositLootingBag$49(): boolean -> net.runelite.api.widgets.Widget#getChildren(): Widget[] net.runelite.client.plugins.microbot.util.bank.Rs2Bank#lambda$getPathAndBankToNearestBank$39(TileObject, BankLocation): int -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint net.runelite.client.plugins.microbot.util.bank.Rs2Bank#lambda$getPathAndBankToNearestBank$40(Set, TileObject): AbstractMap$SimpleEntry -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint net.runelite.client.plugins.microbot.util.bank.Rs2Bank#lambda$handleAmount$17(): boolean -> net.runelite.api.widgets.Widget#getText(): String @@ -167,8 +156,7 @@ net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment#storeEquipmentI net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment#storeEquipmentItemsInMemory(ItemContainerChanged): void -> net.runelite.api.ItemContainer#getItems(): Item[] net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment#storeEquipmentItemsInMemory(ItemContainerChanged): void -> net.runelite.api.events.ItemContainerChanged#getContainerId(): int net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment#storeEquipmentItemsInMemory(ItemContainerChanged): void -> net.runelite.api.events.ItemContainerChanged#getItemContainer(): ItemContainer -net.runelite.client.plugins.microbot.util.gameobject.Rs2Cannon#refill(int): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint -net.runelite.client.plugins.microbot.util.gameobject.Rs2Cannon#repair(): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint +net.runelite.client.plugins.microbot.util.gameobject.Rs2Cannon#isOwnedCannon(TileObject): boolean -> net.runelite.api.TileObject#getWorldLocation(): WorldPoint net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#canWalkTo(TileObject, int): boolean -> net.runelite.api.Client#isInInstancedRegion(): boolean net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#canWalkTo(TileObject, int): boolean -> net.runelite.api.GameObject#getSceneMinLocation(): Point net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject#canWalkTo(TileObject, int): boolean -> net.runelite.api.GameObject#sizeX(): int @@ -559,6 +547,7 @@ net.runelite.client.plugins.microbot.util.player.Rs2Player#getPlayerEquipmentIds net.runelite.client.plugins.microbot.util.player.Rs2Player#getPlayerEquipmentIds(Rs2PlayerModel): Map -> net.runelite.api.kit.KitType#values(): KitType[] net.runelite.client.plugins.microbot.util.player.Rs2Player#getPlayers(Predicate, boolean): Stream -> net.runelite.api.Client#getTopLevelWorldView(): WorldView net.runelite.client.plugins.microbot.util.player.Rs2Player#getPlayers(Predicate, boolean): Stream -> net.runelite.api.IndexedObjectSet#stream(): Stream +net.runelite.client.plugins.microbot.util.player.Rs2Player#getPlayers(Predicate, boolean): Stream -> net.runelite.api.WorldView#players(): IndexedObjectSet net.runelite.client.plugins.microbot.util.player.Rs2Player#getWorld(): int -> net.runelite.api.Client#getWorld(): int net.runelite.client.plugins.microbot.util.player.Rs2Player#handleAnimationChanged(AnimationChanged): void -> net.runelite.api.Client#getLocalPlayer(): Player net.runelite.client.plugins.microbot.util.player.Rs2Player#handleAnimationChanged(AnimationChanged): void -> net.runelite.api.Player#getAnimation(): int