Developer docs
API cookbook
exact = true, and a few match exactly by default. The snippets below pass true wherever there is a choice. See Known traps.Where things live
One import covers the whole API. Everything below is in net.runelite.client.plugins.projectx.bot, apart from ProjectX and Script themselves (net.runelite.client.plugins.projectx):
import net.runelite.client.plugins.projectx.ProjectX;
import net.runelite.client.plugins.projectx.Script;
import net.runelite.client.plugins.projectx.bot.*;| Class | What it is for |
|---|---|
Inventory, Bank, Equipment | What you carry, bank and wear |
Walker, Player | Walking, and your own character |
Npcs, Objects, GroundItems, Players | Searching the scene with query() |
Npc, SceneObject, FloorItem, PlayerModel | What a search gives back; each has interact(action) |
Dialogue, Widgets, Keyboard | Chat dialogues, interfaces, key presses |
Magic, Spells, Prayers | Casting and praying, with the constants (Spells.X, Prayers.X) |
GrandExchange, Shop, Items | Trading and item lookups |
Loadout | A saved inventory setup |
Item, object, NPC and widget ids are in RuneLite's net.runelite.api.gameval (ItemID, ObjectID, NpcID, InterfaceID). Use those names rather than raw numbers. Two names clash with other libraries: Player (RuneLite has its own) and Objects (Java has one). See the getting started page.
Inventory
if (Inventory.isFull()) { /* bank */ }
int free = Inventory.emptySlotCount();
boolean hasShark = Inventory.hasItem("Shark", true); // exact name
boolean hasAnyAxe = Inventory.hasItem("axe", false); // any name containing "axe"
int sharks = Inventory.count("Shark", true); // slots holding sharks
int coins = Inventory.itemQuantity("Coins"); // the stack size (exact name)
Inventory.interact("Shark", "Eat", true);
Inventory.combine("Knife", "Logs"); // use one on the other
Inventory.drop("Logs", true);
Inventory.waitForInventoryChanges(3_000); // after an action that changes itcount counts slots; use itemQuantity for anything stackable. Inventory.get(name, true) returns the item itself (store it with var) when you need its id, slot or quantity.
Bank
// Walks to the nearest bank if needed, then opens it. True once it is open.
if (!Bank.walkToBankAndUseBank()) return;
Bank.depositAllExcept(true, "Rune pickaxe"); // exact names to keep
Bank.depositAll("Logs", true);
Inventory.waitForInventoryChanges(3_000);
int inBank = Bank.count("Shark", true);
Bank.withdrawX("Shark", Math.min(10, inBank), true); // see the note below
Bank.withdrawOne("Knife", true);
Bank.withdrawAll("Coins", true);
Bank.closeBank();Bank.withdrawX(name, amount, exact) only matches a bank stack holding at least amount. Ask for 10 when the bank has 6 and nothing is withdrawn, and nothing says so. Clamp the amount with Bank.count first, as above.
Bank.openBank() opens the nearest visible booth, chest or banker without walking. Bank.isOpen() also types the bank PIN if the PIN screen is showing.
Walking
WorldPoint target = BankLocation.GRAND_EXCHANGE.getWorldPoint();
// Blocks until it arrives or gives up: paths, opens doors, takes stairs and transports.
boolean arrived = Walker.walkTo(target, 3); // within 3 tiles
// The same walk, telling you why it stopped.
WalkerState state = Walker.walkWithState(target, 3); // ARRIVED, MOVING, UNREACHABLE or EXIT
boolean near = Player.isInArea(target, 10);
int tiles = Player.distanceTo(target);walkTo closes the bank first if it is open. Because it blocks, call it from the loop, never from an overlay or an event handler.
NPCs, objects and ground items
Every search has three parts: where to look (Npcs, Objects, GroundItems, Players), some filters, and a terminal that gives the answer. Run it from the script thread.
Npcs.query("Cow").nearest().interact("Attack"); // search and click in one line
var rock = Objects.query().nameEquals("Copper rocks").nearestReachable();
if (rock != null && rock.interact("Mine")) {
sleepUntil(Player::isAnimating, 3_000);
}
var goblin = Npcs.query()
.nameEquals("Goblin") // exact, ignoring case
.where(npc -> !npc.isInteractingWithPlayer())
.within(10) // tiles from the player
.nearest();
if (goblin != null && goblin.interact("Attack")) {
sleepUntil(Player::isInCombat, 3_000);
}
var booth = Objects.query("Bank booth").nearest();
if (booth != null) booth.interact("Bank");
var bones = GroundItems.query("Bones").within(5).nearest();
if (bones != null) bones.pickup();
List<Npc> guards = Npcs.query().withNames("Guard", "Man").list();
int playersNearby = Players.query().within(15).count();Other filters: withId(id), withIds(...), withNameContains(text), within(anchor, distance) and where(predicate). Terminals: nearest(), nearestReachable(), first(), list(), count() and interact(action). A terminal returns null when nothing matches, so check before you click. Results are snapshots: search again each pass rather than keeping one. Npcs.stream() and the others give everything in view as a Java stream if you prefer that.
Dialogue and widgets
if (Dialogue.isInDialogue()) {
if (Dialogue.hasContinue()) {
Dialogue.clickContinue(); // presses space
} else if (Dialogue.hasSelectAnOption()) {
Dialogue.clickOption("Yes", true);
}
}
Dialogue.sleepUntilHasDialogueOption("Yes");
// Find a widget (on the client thread, done for you), then click it from here.
Widget make = Widgets.findWidget("Make", true);
if (make != null) Widgets.clickWidget(make);
if (Widgets.isProductionWidgetOpen()) {
Keyboard.keyPress(KeyEvent.VK_SPACE);
}
boolean bankShowing = Widgets.isWidgetVisible(InterfaceID.Bankmain.UNIVERSE);Prefer finding the widget and then calling clickWidget(widget), as above, over Widgets.clickWidget("text"): the text form clicks from inside a client-thread call, so it returns before the click has happened.
The player
WorldPoint here = Player.getWorldLocation();
boolean busy = Player.isAnimating() || Player.isMoving();
boolean fighting = Player.isInCombat();
Player.eatAt(50); // eats if health is at or below 50%
double hp = Player.getHealthPercentage();
int wc = Player.getRealSkillLevel(Skill.WOODCUTTING);
Player.waitForXpDrop(Skill.WOODCUTTING, 5_000);Magic
// MagicAction is net.runelite.client.plugins.skillcalculator.skills.MagicAction
if (Magic.canCast(MagicAction.VARROCK_TELEPORT)) {
Magic.cast(MagicAction.VARROCK_TELEPORT);
}
if (Magic.hasRequiredRunes(Spells.HIGH_LEVEL_ALCHEMY)) {
Magic.alch(Inventory.get("Rune platebody", true)); // High Alchemy from 55 Magic, Low below
}
Magic.castOn(MagicAction.WIND_STRIKE, goblin); // goblin: an NpcPrayer
Prayers.toggle(Prayers.PROTECT_MELEE, true);
boolean on = Prayers.isPrayerActive(Prayers.PIETY);
Prayers.toggleQuickPrayer(true);
int points = Prayers.getPrayerPoints();
Player.drinkPrayerPotionAt(20); // drinks if points are at or below 20
Prayers.disableAllPrayers();toggle(prayer, on) sets a prayer on or off. The one-argument toggle(prayer) flips it, which is rarely what you want in a loop.
For 1-tick flicking, call Prayers.flickQuickPrayer() or Prayers.flick(prayer) once per tick from a game-tick subscriber in your plugin, inside ProjectX.withDirectInput so both clicks land on the same tick (plugin API 2.6.29 and later):
@Subscribe
public void onGameTick(GameTick tick) {
if (config.flickPrayers()) {
ProjectX.withDirectInput(() -> Prayers.flickQuickPrayer());
}
}Direct input sends clicks without moving the mouse, which is easier for the game to notice, so offer it as a setting rather than always on.
Grand Exchange
GrandExchange.walkToGrandExchange();
if (!GrandExchange.openExchange()) return;
GrandExchange.buyItem("Shark", 900, 100); // name, price each, quantity
sleepUntil(GrandExchange::hasFinishedBuyingOffers, 30_000);
GrandExchange.collectAllToBank();
GrandExchange.sellItem("Yew logs", 500, 300); // name, QUANTITY, then price
GrandExchange.closeExchange();Note the argument order: buyItem(name, price, quantity) but sellItem(name, quantity, price). GrandExchange.getPrice(itemId) makes a network request, so call it from the loop, never from an overlay.
Inventory setups
Players save inventory setups in the client. Let them pick one in your settings with a config item of type InventorySetup (from projectx.inventorysetups), which is shown as a picker:
@ConfigItem(
keyName = "inventorySetup",
name = "Inventory setup",
description = "Loaded from the bank before starting",
position = 0
)
default InventorySetup inventorySetup() {
return null; // null until the player picks one
}InventorySetup chosen = config.inventorySetup();
if (chosen != null) {
Loadout setup = new Loadout(chosen, mainScheduledFuture);
if (!setup.doesEquipmentMatch() || !setup.doesInventoryMatch()) {
// Each opens the nearest bank itself, then withdraws and wears what is missing.
setup.loadEquipment();
setup.loadInventory();
}
// Later, when banking: keep everything the setup holds.
Bank.depositAllExcept(setup.itemsToNotDeposit());
}Passing mainScheduledFuture lets a long load stop part-way when the script is switched off. Never pass a null setup to the constructor: it shows an error and pauses every script. A setup can also be found by name with new Loadout("Barrows", mainScheduledFuture); if no setup has that name, it quietly falls back to one named “default”, so check Loadout.isInventorySetup(name) first.