From 0785da7110c3972eeb66d5b2f82ee48e029338ac Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 27 Dec 2025 16:56:08 +0100 Subject: [PATCH 1/5] docs(agents): update Microbot Agent Guide with new structure and examples --- AGENTS.md | 133 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 95 insertions(+), 38 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bb5bcc12596..b3c50bf73d3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,38 +1,95 @@ -# Repository Guidelines - -## Project Structure & Module Organization -- The root `pom.xml` controls the multi-module Maven build for `cache`, `runelite-api`, `runelite-client`, `runelite-jshell`, and `runelite-maven-plugin`. -- Gameplay automation lives in `runelite-client/src/main/java/net/runelite/client/plugins/microbot`; keep new scripts and utilities inside this plugin. -- Shared helpers sit under `.../microbot/util`, while runnable examples live in `.../microbot/example`. -- Tests mirror sources in `runelite-client/src/test/java`, and project documentation and walkthroughs are kept in `docs/`. -- CI helpers and custom Maven settings are in `ci/`, and distributable jars land in `runelite-client/target/`. - -## Build, Test, and Development Commands -- `mvn -pl runelite-client -am package` builds the client and produces `target/microbot-.jar`. -- `./ci/build.sh` recreates the CI pipeline, fetching `glslangValidator` and running `mvn verify --settings ci/settings.xml`. -- `mvn -pl runelite-client test` runs the unit suite; add `-DskipTests` only when packaging binaries for distribution. -- `java -jar runelite-client/target/microbot-.jar` launches a locally built client for manual validation. - -## Coding Style & Naming Conventions -- Target Java 11 (`maven-compiler-plugin` uses `11`); rely on Lombok for boilerplate where already adopted. -- Keep indentation with tabs, follow the brace placement already in `MicrobotPlugin.java`, and prefer lines under 120 characters. -- Use `UpperCamelCase` for types, `lowerCamelCase` for members, and prefix configuration interfaces with the plugin name (e.g., `ExampleConfig`). -- Centralize shared logic in util classes rather than duplicating inside scripts; inject dependencies through RuneLite’s DI when needed. - -## Testing Guidelines -- Write JUnit 4 tests (`junit:4.12`) under matching package paths in `runelite-client/src/test/java`. -- Name test classes with the `*Test` suffix and break scenarios into focused `@Test` methods that assert observable client state. -- Use Mockito (`mockito-core:3.1.0`) for client services; rely on `guice-testlib` when event bus wiring is involved. -- Run `mvn test` (or `mvn verify` before release) locally before opening a pull request and attach logs when failures require review. - -## Commit & Pull Request Guidelines -- Follow the existing conventional commit style: `type(scope): summary` (e.g., `refactor(Rs2Walker): expand teleport keywords`). -- Squash noisy work-in-progress commits before pushing and keep summaries under 72 characters. -- PRs should explain the gameplay scenario, note affected plugins, link related issues or scripts, and include screenshots or clips when UI overlays change. -- Confirm tests/builds in the PR description and mention any follow-up tasks or config changes reviewers must perform. - -## Agent-Specific Instructions -- Register new automation under `net.runelite.client.plugins.microbot` and reuse the scheduler pattern shown in `ExampleScript`. -- Expose reusable behaviour through `microbot/util` packages so scripts stay thin and composable. -- When adding panel controls or overlays, update the Microbot navigation panel setup in `MicrobotPlugin` and provide default config values. -- Document new APIs in `docs/api/` and cross-link from `docs/development.md` so contributors can discover capabilities quickly. +# Microbot Agent Guide + +Guidance for AI agents building Microbot scripts with the new `microbot/api` queryable layer. + +## Scope & Paths +- Primary plugin code: `runelite-client/src/main/java/net/runelite/client/plugins/microbot`. +- Queryable API docs: `.../microbot/api/QUERYABLE_API.md`; quick read: `api/README.md`. +- Keep new scripts inside the microbot plugin; share helpers under `microbot/util`. + +## Build & Test +- Fast build: `mvn -pl runelite-client -am package` (jar in `runelite-client/target/`). +- Unit tests: `mvn -pl runelite-client test`. +- CI parity: `./ci/build.sh` (runs `mvn verify --settings ci/settings.xml`). + +## Style Rules +- Java 11 target, tabs for indentation, braces match `MicrobotPlugin.java`, prefer <120 chars/line. +- Name types in UpperCamelCase, members in lowerCamelCase; configs prefixed with plugin name (e.g., `ExampleConfig`). + +## Script Pattern +Pair a RuneLite `Plugin` with a `Script` that runs on a background thread; never sleep on the client thread. + +```java +@PluginDescriptor(name = "Gathering Demo") +public class GatheringPlugin extends Plugin { + @Inject private GatheringScript script; + @Override protected void startUp() { script.run(); } + @Override protected void shutDown() { script.shutdown(); } +} + +@Slf4j +public class GatheringScript extends Script { + @Override + public boolean run() { + mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { + try { + if (!Microbot.isLoggedIn() || !super.run()) return; + + Rs2TileObjectModel tree = new Rs2TileObjectQueryable() + .withName("Tree") + .where(obj -> !Rs2Player.isAnimating()) + .nearest(); + + if (tree != null) { + tree.click("Chop down"); + sleepUntil(() -> Rs2Player.isAnimating(), 3000); + } + } catch (Exception e) { + log.error("Loop error", e); + } + }, 0, 600, TimeUnit.MILLISECONDS); // ~1 tick + return true; + } +} +``` + +## Queryable API Cheatsheet +- **NPCs** + ```java + Rs2NpcModel banker = new Rs2NpcQueryable() + .withNames("Banker", "Bank clerk") + .where(npc -> !npc.isInteracting()) + .nearest(15); + if (banker != null) banker.click("Bank"); + ``` +- **Ground items** + ```java + Rs2TileItemModel loot = new Rs2TileItemQueryable() + .where(Rs2TileItemModel::isLootAble) + .where(item -> item.getTotalGeValue() >= 3000) + .nearest(10); + if (loot != null) loot.pickup(); + ``` +- **Tile objects** + ```java + Rs2TileObjectModel bankChest = new Rs2TileObjectQueryable() + .withNames("Bank chest", "Bank booth") + .nearest(20); + if (bankChest != null && !Rs2Bank.isOpen()) { + bankChest.click("Bank"); + sleepUntil(Rs2Bank::isOpen, 5000); + } + ``` +- **Players** + ```java + Rs2PlayerModel ally = new Rs2PlayerQueryable() + .where(Rs2PlayerModel::isFriend) + .within(20) + .nearest(); + ``` + +## Safety & Timing +- Always guard logic with `Microbot.isLoggedIn()` and `super.run()`; bail early when paused. +- Use `sleep`/`sleepUntil` only on script threads; wrap client access with `Microbot.getClientThread().runOnClientThread(...)` when needed. +- Wait for state changes after interactions (`Rs2Bank.isOpen()`, `Rs2Player.isAnimating()`, inventory/bank counts). +- Limit query radius with `.within(...)` to reduce overhead and cache results inside a loop when reused. From 0b655b30d1108a505919780bf248b27a1962c839 Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 27 Dec 2025 16:56:33 +0100 Subject: [PATCH 2/5] docs(microbot): add Microbot API Guide for automation scripts --- CLAUDE.MD | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 CLAUDE.MD diff --git a/CLAUDE.MD b/CLAUDE.MD new file mode 100644 index 00000000000..35bdf4c3252 --- /dev/null +++ b/CLAUDE.MD @@ -0,0 +1,96 @@ +# Microbot API Guide (for Claude) + +Short notes for writing automation scripts with the Microbot plugin inside RuneLite. + +## Paths & Builds +- Plugin sources live in `runelite-client/src/main/java/net/runelite/client/plugins/microbot`. +- The queryable API lives in `.../microbot/api`; full guide: `.../microbot/api/QUERYABLE_API.md`. +- Quick builds: `mvn -pl runelite-client -am package`; tests: `mvn -pl runelite-client test`. + +## Script Skeleton +Use a Plugin + Script pair. Keep sleeps off the client thread and always check login state. + +```java +@PluginDescriptor(name = "Goblin Demo", description = "Queryable API example") +public class GoblinDemoPlugin extends Plugin { + @Inject private GoblinDemoScript script; + + @Override protected void startUp() { script.run(); } + @Override protected void shutDown() { script.shutdown(); } +} + +@Slf4j +public class GoblinDemoScript extends Script { + @Override + public boolean run() { + mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { + try { + if (!Microbot.isLoggedIn() || !super.run()) return; + + Rs2NpcModel target = new Rs2NpcQueryable() + .withName("Goblin") + .where(npc -> !npc.isInteracting()) + .nearest(10); + + if (target != null && !Rs2Player.isInCombat()) { + target.click("Attack"); + sleepUntil(() -> Rs2Player.isInCombat(), 2000); + } + } catch (Exception e) { + log.error("Loop error", e); + } + }, 0, 600, TimeUnit.MILLISECONDS); // ~1 game tick + return true; + } +} +``` + +## Queryable API Quick Reference +Prefer the queryable API over legacy util calls. + +- **NPCs** + ```java + Rs2NpcModel banker = new Rs2NpcQueryable() + .withNames("Banker", "Bank clerk") + .where(npc -> !npc.isInteracting()) + .nearest(15); + if (banker != null) banker.click("Bank"); + ``` + +- **Ground items** + ```java + Rs2TileItemModel loot = new Rs2TileItemQueryable() + .where(Rs2TileItemModel::isLootAble) + .where(item -> item.getTotalGeValue() >= 5000) + .nearest(10); + if (loot != null) loot.pickup(); + ``` + +- **Tile objects** + ```java + Rs2TileObjectModel tree = new Rs2TileObjectQueryable() + .where(obj -> obj.getName() != null && obj.getName().contains("Tree")) + .nearest(); + if (tree != null && !Rs2Player.isAnimating()) { + tree.click("Chop down"); + sleepUntil(() -> Rs2Player.isAnimating(), 3000); + } + ``` + +- **Players** + ```java + Rs2PlayerModel teammate = new Rs2PlayerQueryable() + .where(Rs2PlayerModel::isFriend) + .within(20) + .nearest(); + ``` + +## Interaction & Timing Tips +- Never sleep on the RuneLite client thread; use the script thread with `sleep(...)` / `sleepUntil(...)`. +- After interactions, wait for state changes (e.g., `Rs2Bank.isOpen()`, `Rs2Player.isAnimating()`). +- Limit search radius with `.within(...)` to reduce overhead, and cache query results when reusing in a loop. + +## Helpful References +- Example templates: `runelite-client/src/main/java/net/runelite/client/plugins/microbot/example/`. +- API examples: `api/*/` directories contain `*ApiExample.java` files for NPCs, tile items, players, and objects. +- Core utilities (legacy but still useful): `microbot/util` (e.g., `Rs2Inventory`, `Rs2Bank`, `Rs2Walker`). From 0e641b76c3d2cf32c1f67fb535bd6b0c8c209c3d Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 27 Dec 2025 22:24:22 +0100 Subject: [PATCH 3/5] feat(microbot): add application configuration and debug task --- .run/Microbot.run.xml | 13 ++ runelite-client/build.gradle.kts | 21 ++++ .../externalplugins/MicrobotPluginClient.java | 111 ++++++++++-------- .../MicrobotPluginManager.java | 26 +++- 4 files changed, 113 insertions(+), 58 deletions(-) create mode 100644 .run/Microbot.run.xml diff --git a/.run/Microbot.run.xml b/.run/Microbot.run.xml new file mode 100644 index 00000000000..34e9c1bbce5 --- /dev/null +++ b/.run/Microbot.run.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/runelite-client/build.gradle.kts b/runelite-client/build.gradle.kts index 442fcf7383f..d4e5d6d85e5 100644 --- a/runelite-client/build.gradle.kts +++ b/runelite-client/build.gradle.kts @@ -49,6 +49,27 @@ plugins { id("net.runelite.runelite-gradle-plugin.assemble") id("net.runelite.runelite-gradle-plugin.index") id("net.runelite.runelite-gradle-plugin.jarsign") + + application // <-- add this +} + +application { + mainClass.set("net.runelite.client.RuneLite") +} + +tasks.register("runDebug") { + group = "application" + description = "Run RuneLite client with JDWP debug" + + classpath = sourceSets.main.get().runtimeClasspath + mainClass.set("net.runelite.client.RuneLite") + + // same JVM args you need normally + jvmArgs( + "-Dfile.encoding=UTF-8", + // JDWP agent for debugger + "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005" + ) } lombok.version = libs.versions.lombok.get() diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginClient.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginClient.java index dd5c0741ad7..cf8b4f90781 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginClient.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginClient.java @@ -55,7 +55,7 @@ public class MicrobotPluginClient { private static final HttpUrl MICROBOT_PLUGIN_HUB_URL = HttpUrl.parse("https://chsami.github.io/Microbot-Hub/"); private static final HttpUrl MICROBOT_PLUGIN_RELEASES_URL = HttpUrl.parse( - "https://github.com/chsami/Microbot-Hub/releases/download/" + "https://github.com/chsami/Microbot-Hub/releases/download/latest-release/" ); private static final HttpUrl MICROBOT_PLUGIN_RELEASES_API_URL = HttpUrl.parse( "https://api.github.com/repos/chsami/Microbot-Hub/releases" @@ -141,12 +141,8 @@ public HttpUrl getJarURL(MicrobotPluginManifest manifest, String versionOverride if (MICROBOT_PLUGIN_RELEASES_URL != null && !Strings.isNullOrEmpty(artifactId) && !Strings.isNullOrEmpty(version)) { - String releaseTag = !Strings.isNullOrEmpty(manifest.getReleaseTag()) - ? manifest.getReleaseTag() - : "v" + version; String fileName = buildAssetFileName(artifactId, version); return MICROBOT_PLUGIN_RELEASES_URL.newBuilder() - .addPathSegment(releaseTag) .addPathSegment(fileName) .build(); } @@ -167,17 +163,11 @@ public Map getPluginCounts() throws IOException /** * Fetches the list of published versions for the given plugin from GitHub releases. */ - public List fetchAvailableVersions(MicrobotPluginManifest manifest) throws IOException + public JsonArray fetchAllReleases() throws IOException { - if (manifest == null) + if (MICROBOT_PLUGIN_RELEASES_API_URL == null) { - return Collections.emptyList(); - } - - String artifactId = resolveArtifactId(manifest); - if (Strings.isNullOrEmpty(artifactId) || MICROBOT_PLUGIN_RELEASES_API_URL == null) - { - return Collections.emptyList(); + return new JsonArray(); } HttpUrl releasesUrl = MICROBOT_PLUGIN_RELEASES_API_URL.newBuilder() @@ -193,68 +183,85 @@ public List fetchAvailableVersions(MicrobotPluginManifest manifest) thro { if (res.body() == null || res.code() != 200) { - throw new IOException("Failed to fetch releases for " + artifactId + ": HTTP " + res.code()); + throw new IOException("Failed to fetch releases: HTTP " + res.code()); } JsonArray releases = gson.fromJson(res.body().string(), JsonArray.class); - if (releases == null || releases.size() == 0) + return releases != null ? releases : new JsonArray(); + } + catch (JsonSyntaxException ex) + { + throw new IOException("Unable to parse releases", ex); + } + } + + public List parseVersionsFromReleases(MicrobotPluginManifest manifest, JsonArray releases) throws IOException + { + if (manifest == null || releases == null || releases.size() == 0) + { + return Collections.emptyList(); + } + + String artifactId = resolveArtifactId(manifest); + if (Strings.isNullOrEmpty(artifactId)) + { + return Collections.emptyList(); + } + + Set versions = new LinkedHashSet<>(); + String normalizedArtifact = artifactId.toLowerCase(Locale.ROOT); + + for (JsonElement releaseElem : releases) + { + if (!releaseElem.isJsonObject()) { - return Collections.emptyList(); + continue; } - Set versions = new LinkedHashSet<>(); - String normalizedArtifact = artifactId.toLowerCase(Locale.ROOT); + JsonObject release = releaseElem.getAsJsonObject(); + String tagName = getString(release, "tag_name"); + JsonArray assets = release.getAsJsonArray("assets"); + if (assets == null) + { + continue; + } - for (JsonElement releaseElem : releases) + for (JsonElement assetElem : assets) { - if (!releaseElem.isJsonObject()) + if (!assetElem.isJsonObject()) { continue; } - JsonObject release = releaseElem.getAsJsonObject(); - String tagName = getString(release, "tag_name"); - JsonArray assets = release.getAsJsonArray("assets"); - if (assets == null) + JsonObject asset = assetElem.getAsJsonObject(); + String assetName = getString(asset, "name"); + if (Strings.isNullOrEmpty(assetName)) { continue; } - for (JsonElement assetElem : assets) + if (matchesArtifact(assetName, normalizedArtifact)) { - if (!assetElem.isJsonObject()) - { - continue; - } - - JsonObject asset = assetElem.getAsJsonObject(); - String assetName = getString(asset, "name"); - if (Strings.isNullOrEmpty(assetName)) + String version = extractVersionFromAsset(assetName, normalizedArtifact); + if (!Strings.isNullOrEmpty(version)) { - continue; + versions.add(version); } - - if (matchesArtifact(assetName, normalizedArtifact)) + else if (!Strings.isNullOrEmpty(tagName)) { - String version = extractVersionFromAsset(assetName, normalizedArtifact); - if (!Strings.isNullOrEmpty(version)) - { - versions.add(version); - } - else if (!Strings.isNullOrEmpty(tagName)) - { - versions.add(normalizeTag(tagName)); - } + versions.add(normalizeTag(tagName)); } } } - - return new ArrayList<>(versions); - } - catch (JsonSyntaxException ex) - { - throw new IOException("Unable to parse releases for " + artifactId, ex); } + + return new ArrayList<>(versions); + } + + public List fetchAvailableVersions(MicrobotPluginManifest manifest) throws IOException + { + JsonArray releases = fetchAllReleases(); + return parseVersionsFromReleases(manifest, releases); } private Request.Builder withGithubHeaders(Request.Builder builder) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java index 70411616d3b..37a3b4ec5be 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java @@ -143,14 +143,26 @@ private void loadManifest() { try { List manifests = microbotPluginClient.downloadManifest(); Map next = new HashMap<>(manifests.size()); + + com.google.gson.JsonArray allReleases = null; + try { + allReleases = microbotPluginClient.fetchAllReleases(); + log.debug("Fetched {} releases from GitHub", allReleases.size()); + } catch (IOException ex) { + log.warn("Failed to fetch GitHub releases: {}", ex.getMessage()); + log.debug("Releases fetch error", ex); + } + for (MicrobotPluginManifest m : manifests) { next.put(m.getInternalName(), m); - try { - List versions = microbotPluginClient.fetchAvailableVersions(m); - m.setAvailableVersions(versions); - } catch (IOException ex) { - log.warn("Failed to fetch available versions for {}: {}", m.getInternalName(), ex.getMessage()); - log.debug("Version fetch error", ex); + if (allReleases != null) { + try { + List versions = microbotPluginClient.parseVersionsFromReleases(m, allReleases); + m.setAvailableVersions(versions); + } catch (IOException ex) { + log.warn("Failed to parse available versions for {}: {}", m.getInternalName(), ex.getMessage()); + log.debug("Version parse error", ex); + } } } boolean changed = !next.keySet().equals(manifestMap.keySet()) @@ -934,6 +946,8 @@ private boolean downloadPlugin(String internalName, @Nullable String versionOver .url(jarUrl) .build(); + log.info("from url : " + jarUrl); + try (Response response = clientWithoutProxy.newCall(request).execute()) { if (!response.isSuccessful()) { log.error("Failed to download plugin {}: HTTP {}", internalName, response.code()); From db2c6a29681f09f214bae1d8142dfe8331ea9532 Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 27 Dec 2025 22:24:38 +0100 Subject: [PATCH 4/5] feat(microbot): add application configuration and debug task --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ca54ad5872c..cbef9075e53 100644 --- a/gradle.properties +++ b/gradle.properties @@ -31,7 +31,7 @@ project.build.group=net.runelite project.build.version=1.12.10 glslang.path= -microbot.version=2.1.0 +microbot.version=2.1.1 microbot.commit.sha=nogit microbot.repo.url=http://138.201.81.246:8081/repository/microbot-snapshot/ microbot.repo.username= From 3ddec8c57f4324264220600ae1786d825609d687 Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 27 Dec 2025 22:30:44 +0100 Subject: [PATCH 5/5] chore(build): comment out application block in build.gradle.kts --- runelite-client/build.gradle.kts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/runelite-client/build.gradle.kts b/runelite-client/build.gradle.kts index d4e5d6d85e5..fff3c9d7b72 100644 --- a/runelite-client/build.gradle.kts +++ b/runelite-client/build.gradle.kts @@ -50,10 +50,10 @@ plugins { id("net.runelite.runelite-gradle-plugin.index") id("net.runelite.runelite-gradle-plugin.jarsign") - application // <-- add this + // application // <-- add this } -application { +/*application { mainClass.set("net.runelite.client.RuneLite") } @@ -70,7 +70,7 @@ tasks.register("runDebug") { // JDWP agent for debugger "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005" ) -} +}*/ lombok.version = libs.versions.lombok.get()