Skip to content
Project X

Developer docs

API cookbook

Short snippets for the things nearly every script does. They all run on the script thread, inside your loop: that is where clicking, walking and waiting belong. Every call here exists with exactly this signature in the current client.
Most methods that take an item name match part of the name unless you pass 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.*;
ClassWhat it is for
Inventory, Bank, EquipmentWhat you carry, bank and wear
Walker, PlayerWalking, and your own character
Npcs, Objects, GroundItems, PlayersSearching the scene with query()
Npc, SceneObject, FloorItem, PlayerModelWhat a search gives back; each has interact(action)
Dialogue, Widgets, KeyboardChat dialogues, interfaces, key presses
Magic, Spells, PrayersCasting and praying, with the constants (Spells.X, Prayers.X)
GrandExchange, Shop, ItemsTrading and item lookups
LoadoutA 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 it

count 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 Npc

Prayer

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:

In your Config
@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
}
In your Script
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.