Skip to content
Project X

Developer docs

Scripting API

Scripts talk to the game through one package with short, plain names. Available from client plugin API 2.7.0.

One import

import net.runelite.client.plugins.projectx.bot.*;

SceneObject rock = Objects.query("Copper rocks").nearestReachable();
if (rock != null) {
    rock.interact("Mine");
}

Npcs.query("Cow").interact("Attack");

if (Inventory.isFull()) {
    Bank.depositAll();
}

Rules worth knowing from day one:

  • Wait with sleepUntil(condition, timeoutMs), not a fixed sleep, when you are waiting for the game to change.
  • Check for null: a search that finds nothing returns null from nearest(). Npcs.query("Cow").interact("Attack") searches and clicks in one go and returns false when there is no cow.
  • Don't keep entities between loops. Search again each time; the scene changes every tick.
  • Object names are the in-game names: the rock is Copper rocks, not Copper rock.

Entry points

All in net.runelite.client.plugins.projectx.bot.

ClassWhat it is for
ObjectsRocks, trees, doors, banks, ladders: Objects.query(...)
NpcsNPCs: Npcs.query(...)
GroundItemsItems on the ground: GroundItems.query(...)
PlayersOther players (and you): Players.query(...), Players.local()
InventoryThe items you carry
Bank, DepositBoxBanking
EquipmentWhat you wear
PlayerYour own character: health, run energy, animation, eating
Walker, PathApi, MinimapWalking and route finding
Widgets, Dialogue, TabsInterfaces, chat dialogues, side-panel tabs
PrayersPrayers, plus the prayer constants (Prayers.PIETY)
Magic, Spells, CombatSpells, Spellbooks, Staffs, Tomes, ThrallsMagic
Combat, Pvp, Slayer, Cannon, NpcDataCombat
GrandExchange, Shop, ItemsTrading and item lookups (ids, prices)
Keyboard, CameraInput and camera
RandRandom numbers and human-like delays (Rand.between(600, 900))
Antiban, AntibanSettingsThe built-in antiban behaviour
LoadoutLoad a saved inventory setup: new Loadout("Mining", mainScheduledFuture)
Foods, Potions, EnsouledHeadsItem data
GemBag, LogBasket, RunePouch, ExplorersRingContainers and special items
Tiles, LocalPoints, Worlds, Boats, BoatNavigatorTiles, coordinates, worlds, sailing

Searching: query()

Every search has three parts: where to look, filters, and a terminal that gives the answer.

SceneObject tree = Objects.query("Oak tree").nearestReachable();         // by exact name
Npc banker       = Npcs.query().nameEquals("Banker").nearest();          // same, long form
FloorItem coins  = GroundItems.query().withId(ItemID.COINS).within(10).first();
List<Npc> goblins = Npcs.query().withNameContains("goblin").list();
int nearby       = Players.query().within(15).count();
boolean clicked  = Objects.query().withIds(ROCK_IDS).interact("Mine");  // search + click

Filters (chain as many as you like): nameEquals(name), withName(name), withNames(a, b), withNameContains(text), withId(id), withIds(ids...), within(tiles), within(point, tiles), where(entity -> ...), fromWorldView(). Names compare ignoring case.

Terminals (end the search): nearest(), nearest(maxTiles), nearestReachable(), first(), firstReachable(), list(), count(), interact(action).

Name searches off the game thread. A name filter reads each entity's name from the game, one hop per entity. If you search by name a lot, the ...OnClientThread() terminals (nearestOnClientThread(), firstOnClientThread(), toListOnClientThread()) do the whole search in one hop and are much cheaper.

Npcs.stream(), Objects.stream(), GroundItems.stream() and Players.stream() give everything in the scene this tick as a Java Stream.

What a search returns

SearchReturnsUseful methods
Objects.query()SceneObjectinteract(action), getId(), getName(), getWorldLocation(), isReachable()
Npcs.query()Npcinteract(action), getHealthPercentage(), isInteractingWithPlayer(), isMoving()
GroundItems.query()FloorItempickup(), interact("Take"), getQuantity(), getTotalGeValue(), isOwned()
Players.query()PlayerModelgetName(), getCombatLevel(), isFriend(), getWorldLocation()

interact(action) is the same as click(action), which also still works. It returns true when the click was sent. The names avoid clashing with RuneLite's own types: SceneObject (not GameObject), Npc (RuneLite has NPC), FloorItem, PlayerModel (RuneLite has Player).

Constants: prayers, spells, food

Prayers.toggle(Prayers.PROTECT_MELEE, true);
Magic.cast(Spells.HIGH_LEVEL_ALCHEMY);
for (Rs2Food food : Foods.values()) { ... }

Prayers.PIETY is Rs2PrayerEnum.PIETY: a variable that holds one still has the old enum type (Rs2PrayerEnum, Rs2Spells, Rs2Food, ...).

Name clashes to know about

  • Player is the helper for your character. RuneLite also has net.runelite.api.Player; if a file needs both, write RuneLite's out in full.
  • Objects shares its name with java.util.Objects. Don't import java.util.Objects; bot.Objects has the same equals, hash, requireNonNull, ... methods.
  • With import net.runelite.api.*; or import java.util.*; in the same file, import Player or Objects by name so Java knows which one you mean.

Old names

Every old Rs2* name keeps working: scripts compiled against them run unchanged. They are marked deprecated so new code picks the short names. To move code over, rename; the methods are the same.

Old nameNew name
Rs2InventoryInventory
Rs2PlayerPlayer
Rs2BankBank
Rs2WalkerWalker
Rs2WidgetWidgets
Rs2DialogueDialogue
Rs2EquipmentEquipment
Rs2RandomRand
Rs2GrandExchangeGrandExchange
Rs2KeyboardKeyboard
Rs2MagicMagic
Rs2CombatCombat
Rs2CameraCamera
Rs2TabTabs
Rs2ShopShop
Rs2DepositBoxDepositBox
Rs2PotionPotions
Rs2PrayerPrayers
Rs2GameObjectObjects
Rs2NpcNpcs
Rs2GroundItemGroundItems
Rs2InventorySetupLoadout
Rs2ItemManagerItems
Rs2TileTiles
Rs2NpcModel (api.npc.models)Npc
Rs2TileObjectModel (api.tileobject.models)SceneObject
Rs2TileItemModelFloorItem
Rs2PlayerModel (api.player.models)PlayerModel

Searches move from the cache getters to the entry points: ProjectX.getRs2NpcCache().query() becomes Npcs.query(), ....query().withName("X") becomes ....query("X"), model.click(action) becomes model.interact(action), and query.toList() becomes query.list().