diff --git a/runelite-client/src/main/java/net/runelite/client/callback/ClientThread.java b/runelite-client/src/main/java/net/runelite/client/callback/ClientThread.java index 5ff96890ef2..4fbc990384c 100644 --- a/runelite-client/src/main/java/net/runelite/client/callback/ClientThread.java +++ b/runelite-client/src/main/java/net/runelite/client/callback/ClientThread.java @@ -35,6 +35,7 @@ import java.util.Optional; import java.util.concurrent.*; import java.util.function.BooleanSupplier; +import java.util.function.Supplier; @Singleton @Slf4j @@ -122,6 +123,40 @@ public void invoke(BooleanSupplier r) invokeLater(r); } + public T invoke(Supplier supplier) { + if (client.isClientThread()) { + return supplier.get(); + } + + CompletableFuture f = new CompletableFuture<>(); + invoke(() -> { + try { + f.complete(supplier.get()); + } catch (Throwable t) { + f.completeExceptionally(t); + } + }); + + try { + return f.get(10, TimeUnit.SECONDS); // configurable timeout + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted waiting for client thread", ie); + } catch (TimeoutException te) { + throw new RuntimeException("Timed out waiting for client thread", te); + } catch (ExecutionException ee) { + // unwrap original cause + Throwable cause = ee.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } + if (cause instanceof Error) { + throw (Error) cause; + } + throw new RuntimeException(cause); + } + } + /** * Will run r on the game thread after this method returns * If r returns false, r will be ran again, at a later point diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigProfile.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigProfile.java index f4e7058bff3..d6444535d56 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/ConfigProfile.java +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigProfile.java @@ -36,14 +36,14 @@ public class ConfigProfile @Getter private final long id; @Getter - @Setter(AccessLevel.PACKAGE) - private String name; + @Setter + public String name; @Getter - @Setter(AccessLevel.PACKAGE) - private String password; + @Setter + public String password; @Getter - @Setter(AccessLevel.PACKAGE) - private String bankPin; + @Setter + public String bankPin; @Getter @Setter private long memberExpireDaysTimeStemp; @@ -51,10 +51,10 @@ public class ConfigProfile @Setter private long memberExpireDays; @Getter - @Setter(AccessLevel.PACKAGE) - private boolean isMember; + @Setter + public boolean isMember; @Getter - @Setter(AccessLevel.PACKAGE) + @Setter private String discordWebhookUrl; @Getter @Setter diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/example/ExamplePlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/example/ExamplePlugin.java new file mode 100644 index 00000000000..e6b21e5b329 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/example/ExamplePlugin.java @@ -0,0 +1,40 @@ +package net.runelite.client.plugins.microbot.example; + +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.ConfigChanged; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; + +import javax.inject.Inject; +import java.awt.*; + +@PluginDescriptor( + name = PluginDescriptor.Default + "Example Plugin", + description = "Performance test for GameObject composition retrieval", + tags = {"performance", "microbot", "test", "gameobject"}, + enabledByDefault = false +) +@Slf4j +public class ExamplePlugin extends Plugin { + @Inject + ExampleScript exampleScript; + + + @Override + protected void startUp() throws AWTException { + + exampleScript.run(); + } + + + + protected void shutDown() { + exampleScript.shutdown(); + } + + // on settings change + @Subscribe + public void onConfigChanged(final ConfigChanged event) { + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/example/ExampleScript.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/example/ExampleScript.java new file mode 100644 index 00000000000..ca0e5d2ff74 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/example/ExampleScript.java @@ -0,0 +1,84 @@ +package net.runelite.client.plugins.microbot.example; + +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.Script; +import net.runelite.client.plugins.microbot.util.tileobject.Rs2TileObjectApi; +import net.runelite.client.plugins.microbot.util.tileobject.Rs2TileObjectModel; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +/** + * Performance test script for measuring GameObject composition retrieval speed. + * + * This script runs every 5 seconds and performs the following: + * - Gets all GameObjects in the scene + * - Retrieves the ObjectComposition for each GameObject + * - Measures and logs the total time taken + * - Reports average time per object + * + * Useful for performance profiling and optimization testing. + */ +@Slf4j +public class ExampleScript extends Script { + + /** + * Main entry point for the performance test script. + */ + public boolean run() { + mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { + try { + if (!Microbot.isLoggedIn()) return; + + // Performance test: Loop over game objects and get compositions + long startTime = System.currentTimeMillis(); + AtomicLong endTime = new AtomicLong(); + + var tileObjects = Microbot.getClientThread().invoke(() -> { + // List _tileObjects = Rs2TileObjectApi.getObjectsStream().filter(x -> x.getName() != null && !x.getName().isEmpty() && x.getName() != "null").collect(Collectors.toList()); + Rs2TileObjectModel test = Rs2TileObjectApi.getNearest(tile -> tile.getName() != null && tile.getName().toLowerCase().contains("tree")); + endTime.set(System.currentTimeMillis()); + System.out.println("Retrieved " + test.getName() + " game objects in " + (endTime.get() - startTime) + " ms"); + + /*for (Rs2TileObjectModel rs2TileObjectModel: _tileObjects) { + var name = rs2TileObjectModel.getName(); // Access name to simulate some processing + System.out.println("Object Name: " + name); + } +*/ + return Rs2TileObjectApi.getObjectsStream().collect(Collectors.toList()); + }); + + + int compositionCount = 0; + + /*for (Rs2TileObjectModel tileObject : tileObjects) { + var name = tileObject.getName(); // Access name to simulate some processing + if (name != null) { + compositionCount++; + System.out.println("composition " + compositionCount + ": " + name); + } + }*/ + + endTime.set(System.currentTimeMillis()); + long durationMs = (endTime.get() - startTime); + +/* + log.info("Performance Test Results:"); + log.info(" Total GameObjects: {}", tileObjects.size()); + log.info(" Compositions retrieved: {}", compositionCount); + log.info(" Time taken: {} ms", durationMs); + log.info(" Average time per object: {} μs", + tileObjects.size() > 0 ? (endTime.get() - startTime) / 1000 / tileObjects.size() : 0); +*/ + + } catch (Exception ex) { + log.error("Error in performance test loop", ex); + } + }, 0, 5000, TimeUnit.MILLISECONDS); + + return true; + } +} 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 e52b6d97981..1d4b4dee52a 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 @@ -1902,7 +1902,9 @@ public static ObjectComposition getObjectComposition(int id) { ObjectComposition objectComposition = Microbot.getClientThread().runOnClientThreadOptional(() -> Microbot.getClient().getObjectDefinition(id)) .orElse(null); if (objectComposition == null) return null; - return objectComposition.getImpostorIds() == null ? objectComposition : objectComposition.getImpostor(); + return objectComposition.getImpostorIds() == null ? + objectComposition : + Microbot.getClientThread().runOnClientThreadOptional((objectComposition::getImpostor)).orElse(null); } public static boolean canWalkTo(TileObject tileObject, int distance) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/security/LoginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/security/LoginManager.java index 1fb5e1598bc..865e1247d1c 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/security/LoginManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/security/LoginManager.java @@ -121,8 +121,7 @@ public static boolean login() { log.warn("Cannot login - client is not initialised"); return false; } - int currentWorld = client.getWorld(); - int targetWorld = currentWorld > 300 ? currentWorld : getRandomWorld(getActiveProfile().isMember()); + int targetWorld = getRandomWorld(getActiveProfile().isMember()); return login(getActiveProfile().getName(), getActiveProfile().getPassword(), targetWorld); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectApi.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectApi.java new file mode 100644 index 00000000000..be23e4a5b4c --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectApi.java @@ -0,0 +1,102 @@ +package net.runelite.client.plugins.microbot.util.tileobject; + +import net.runelite.api.GameObject; +import net.runelite.api.Player; +import net.runelite.api.Tile; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Stream; + +/** + * API for interacting with tile objects in the game world. + */ +public class Rs2TileObjectApi { + + private static int lastUpdateObjects = 0; + private static List tileObjects = new ArrayList<>(); + + /** + * Get all tile objects in the current scene + * @return Stream of Rs2TileObjectModel + */ + public static Stream getObjectsStream() { + + if (lastUpdateObjects >= Microbot.getClient().getTickCount()) { + return tileObjects.stream(); + } + + Player player = Microbot.getClient().getLocalPlayer(); + if (player == null) return Stream.empty(); + + List result = new ArrayList<>(); + + var tileValues = Microbot.getClient().getTopLevelWorldView().getScene().getTiles()[Microbot.getClient().getTopLevelWorldView().getPlane()]; + + for (Tile[] tileValue : tileValues) { + for (Tile tile : tileValue) { + if (tile == null) continue; + + if (tile.getGameObjects() != null) { + for (GameObject gameObject : tile.getGameObjects()) { + if (gameObject == null) continue; + if (gameObject.getSceneMinLocation().equals(tile.getSceneLocation())) { + result.add(new Rs2TileObjectModel(gameObject)); + } + } + } + if (tile.getGroundObject() != null) { + result.add(new Rs2TileObjectModel(tile.getGroundObject())); + } + if (tile.getWallObject() != null) { + result.add(new Rs2TileObjectModel(tile.getWallObject())); + } + if (tile.getDecorativeObject() != null) { + result.add(new Rs2TileObjectModel(tile.getDecorativeObject())); + } + } + } + + tileObjects = result; + lastUpdateObjects = Microbot.getClient().getTickCount(); + return result.stream(); + } + + /** + * Returns the nearest tile object matching the supplied predicate, or null if none match. + * Distance is based on straight-line world tile distance from the local player. + * + * @return nearest matching Rs2TileObjectModel or null + */ + public static Rs2TileObjectModel getNearest() { + return getNearest(null); + } + + /** + * Returns the nearest tile object matching the supplied predicate, or null if none match. + * Distance is based on straight-line world tile distance from the local player. + * + * @param filter predicate to test objects + * @return nearest matching Rs2TileObjectModel or null + */ + public static Rs2TileObjectModel getNearest(Predicate filter) { + Player player = Microbot.getClient().getLocalPlayer(); + if (player == null) return null; + + WorldPoint playerLoc = player.getWorldLocation(); + + Stream stream = getObjectsStream(); + if (filter != null) { + stream = stream.filter(filter); + } + + return stream + .min(Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(playerLoc))) + .orElse(null); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java new file mode 100644 index 00000000000..f67e90dc374 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java @@ -0,0 +1,265 @@ +package net.runelite.client.plugins.microbot.util.tileobject; + +import lombok.Getter; +import net.runelite.api.*; +import net.runelite.api.Point; +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.util.camera.Rs2Camera; +import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; +import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; +import net.runelite.client.plugins.microbot.util.menu.NewMenuEntry; +import net.runelite.client.plugins.microbot.util.misc.Rs2UiHelper; +import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; + +import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; + + +public class Rs2TileObjectModel implements TileObject { + + public Rs2TileObjectModel(GameObject gameObject) { + this.tileObject = gameObject; + this.tileObjectType = TileObjectType.GAME; + } + + public Rs2TileObjectModel(DecorativeObject tileObject) { + this.tileObject = tileObject; + this.tileObjectType = TileObjectType.DECORATIVE; + } + + public Rs2TileObjectModel(WallObject tileObject) { + this.tileObject = tileObject; + this.tileObjectType = TileObjectType.WALL; + } + + public Rs2TileObjectModel(GroundObject tileObject) { + this.tileObject = tileObject; + this.tileObjectType = TileObjectType.GROUND; + } + + public Rs2TileObjectModel(TileObject tileObject) { + this.tileObject = tileObject; + this.tileObjectType = TileObjectType.GENERIC; + } + + @Getter + private final TileObjectType tileObjectType; + private final TileObject tileObject; + private String[] actions; + + + @Override + public long getHash() { + return tileObject.getHash(); + } + + @Override + public int getX() { + return tileObject.getX(); + } + + @Override + public int getY() { + return tileObject.getY(); + } + + @Override + public int getZ() { + return tileObject.getZ(); + } + + @Override + public int getPlane() { + return tileObject.getPlane(); + } + + @Override + public WorldView getWorldView() { + return tileObject.getWorldView(); + } + + public int getId() { + return tileObject.getId(); + } + + @Override + public @NotNull WorldPoint getWorldLocation() { + return tileObject.getWorldLocation(); + } + + public String getName() { + return Microbot.getClientThread().invoke(() -> { + ObjectComposition composition = Microbot.getClient().getObjectDefinition(tileObject.getId()); + if(composition.getImpostorIds() != null) + { + composition = composition.getImpostor(); + } + if(composition == null) + return null; + return Rs2UiHelper.stripColTags(composition.getName()); + }); + } + + @Override + public @NotNull LocalPoint getLocalLocation() { + return tileObject.getLocalLocation(); + } + + @Override + public @Nullable Point getCanvasLocation() { + return tileObject.getCanvasLocation(); + } + + @Override + public @Nullable Point getCanvasLocation(int zOffset) { + return tileObject.getCanvasLocation(); + } + + @Override + public @Nullable Polygon getCanvasTilePoly() { + return tileObject.getCanvasTilePoly(); + } + + @Override + public @Nullable Point getCanvasTextLocation(Graphics2D graphics, String text, int zOffset) { + return tileObject.getCanvasTextLocation(graphics, text, zOffset); + } + + @Override + public @Nullable Point getMinimapLocation() { + return tileObject.getMinimapLocation(); + } + + @Override + public @Nullable Shape getClickbox() { + return tileObject.getClickbox(); + } + + public ObjectComposition getObjectComposition() { + return Microbot.getClientThread().invoke(() -> { + ObjectComposition composition = Microbot.getClient().getObjectDefinition(tileObject.getId()); + if(composition.getImpostorIds() != null) + { + composition = composition.getImpostor(); + } + return composition; + }); + } + + /** + * Clicks on the specified tile object with no specific action. + * Delegates to Rs2GameObject.clickObject. + * + * @param action the action to perform (e.g., "Open", "Climb") + * @return true if the interaction was successful, false otherwise + */ + public boolean click(String action) { + if (Microbot.getClient().getLocalPlayer().getWorldLocation().distanceTo(getWorldLocation()) > 51) { + Microbot.log("Object with id " + getId() + " is not close enough to interact with. Walking to the object...."); + Rs2Walker.walkTo(getWorldLocation()); + return false; + } + + try { + + int param0; + int param1; + MenuAction menuAction = MenuAction.WALK; + + + Microbot.status = action + " " + getName(); + + if (getTileObjectType() == TileObjectType.GAME) { + GameObject obj = (GameObject) tileObject; + if (obj.sizeX() > 1) { + param0 = obj.getLocalLocation().getSceneX() - obj.sizeX() / 2; + } else { + param0 = obj.getLocalLocation().getSceneX(); + } + + if (obj.sizeY() > 1) { + param1 = obj.getLocalLocation().getSceneY() - obj.sizeY() / 2; + } else { + param1 = obj.getLocalLocation().getSceneY(); + } + } else { + // Default objects like walls, groundobjects, decorationobjects etc... + param0 = getLocalLocation().getSceneX(); + param1 = getLocalLocation().getSceneY(); + } + + + int index = 0; + 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(); + } + + for (int i = 0; i < actions.length; i++) { + if (actions[i] == null) continue; + if (action.equalsIgnoreCase(Rs2UiHelper.stripColTags(actions[i]))) { + index = i; + break; + } + } + + if (index == actions.length) + index = 0; + + objName = objComp.getName(); + + // both hands must be free before using MINECART + if (objComp.getName().toLowerCase().contains("train cart")) { + Rs2Equipment.unEquip(EquipmentInventorySlot.WEAPON); + Rs2Equipment.unEquip(EquipmentInventorySlot.SHIELD); + sleepUntil(() -> Rs2Equipment.get(EquipmentInventorySlot.WEAPON) == null && Rs2Equipment.get(EquipmentInventorySlot.SHIELD) == null); + } + } + + if (index == -1) { + Microbot.log("Failed to interact with object " + getId() + " " + action); + } + + + if (Microbot.getClient().isWidgetSelected()) { + menuAction = MenuAction.WIDGET_TARGET_ON_GAME_OBJECT; + } else if (index == 0) { + menuAction = MenuAction.GAME_OBJECT_FIRST_OPTION; + } else if (index == 1) { + menuAction = MenuAction.GAME_OBJECT_SECOND_OPTION; + } else if (index == 2) { + menuAction = MenuAction.GAME_OBJECT_THIRD_OPTION; + } else if (index == 3) { + menuAction = MenuAction.GAME_OBJECT_FOURTH_OPTION; + } else if (index == 4) { + menuAction = MenuAction.GAME_OBJECT_FIFTH_OPTION; + } + + if (!Rs2Camera.isTileOnScreen(getLocalLocation())) { + Rs2Camera.turnTo(tileObject); + } + + + Microbot.doInvoke(new NewMenuEntry(param0, param1, menuAction.getId(), getId(), -1, action, objName, tileObject), Rs2UiHelper.getObjectClickbox(tileObject)); +// MenuEntryImpl(getOption=Use, getTarget=Barrier, getIdentifier=43700, getType=GAME_OBJECT_THIRD_OPTION, getParam0=53, getParam1=51, getItemId=-1, isForceLeftClick=true, getWorldViewId=-1, isDeprioritized=false) + //Rs2Reflection.invokeMenu(param0, param1, menuAction.getId(), object.getId(),-1, "", "", -1, -1); + + } catch (Exception ex) { + Microbot.log("Failed to interact with object " + ex.getMessage()); + } + + return true; + } + +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/TileObjectType.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/TileObjectType.java new file mode 100644 index 00000000000..a42306c061a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/TileObjectType.java @@ -0,0 +1,9 @@ +package net.runelite.client.plugins.microbot.util.tileobject; + +public enum TileObjectType { + GAME, + WALL, + DECORATIVE, + GROUND, + GENERIC +}