diff --git a/.run/Microbot.run.xml b/.run/Microbot.run.xml
new file mode 100644
index 0000000000..34e9c1bbce
--- /dev/null
+++ b/.run/Microbot.run.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
index bb5bcc1259..b3c50bf73d 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.
diff --git a/CLAUDE.MD b/CLAUDE.MD
new file mode 100644
index 0000000000..35bdf4c325
--- /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`).
diff --git a/gradle.properties b/gradle.properties
index ca54ad5872..cbef9075e5 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=
diff --git a/runelite-client/build.gradle.kts b/runelite-client/build.gradle.kts
index 442fcf7383..fff3c9d7b7 100644
--- a/runelite-client/build.gradle.kts
+++ b/runelite-client/build.gradle.kts
@@ -49,8 +49,29 @@ 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()
java {
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 dd5c0741ad..cf8b4f9078 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 70411616d3..37a3b4ec5b 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());