Skip to content
Project X

Developer docs

Getting started

A Project X script is a RuneLite plugin compiled against the Project X client. You write it in Java, build one jar, and sell it on the store. These pages cover everything from your first build to the traps that catch experienced authors.
Script lifecycleThe plugin, the script, the loop, and stopping cleanly.API cookbookShort, working snippets for inventory, bank, walking, NPCs, widgets, magic, prayer and the GE.Stats panelThe shared on-screen panel: ScriptPanel, ScriptPanelOverlay and SkillTracker.ReliabilityGuarded steps, retries, recovery, checkpoints and safe stops.Threading and performanceWhich thread runs what, and the rules that keep the game smooth.VersioningminClientVersion, the plugin API level, and what an older client does.PublishingGetting a build onto the store, and where the details live.Known trapsThe mistakes that break scripts silently, and how to avoid each one.

What you need

  • A JDK 11, for example Temurin 11. The build uses it whatever your default Java is, so newer JDKs can stay installed alongside it.
  • Git, and an editor that understands Gradle projects (IntelliJ IDEA works well).
  • The Project X launcher, to run the client. It also saves you a download: the build uses the client jar the launcher already has.

Clone and build the template

The script template is a complete, small script: it works on an object until the inventory is full, banks, walks back and carries on. It already uses the stats panel, guarded steps and an inventory setup, so you start from the shape every store script should have.

git clone https://github.com/iEasyScript/xclient-script-template.git
cd xclient-script-template
./gradlew build

That produces two files, and both are uploaded when you submit:

FileWhat it is
build/libs/TemplatePlugin.jarThe script.
build/distributions/TemplatePlugin-source.zipIts source, which a reviewer reads. Players only ever get the jar.

The build compiles against the client jar given by -PprojectxClientPath=..., or the launcher's copy at ~/.projectx/projectx-<version>.jar, or failing both, the one on the client's GitHub release. <version> is projectxClientVersion in gradle.properties; keep it on the current release (1.0.79). Why it matters.

What is in a script

ClassJob
TemplatePluginLifecycle and wiring. Its @PluginDescriptor is the script's identity: name, version, minClientVersion.
TemplateScriptThe loop, on a background thread. All clicking, walking and waiting happens here.
TemplateConfigThe settings the player can change, shown in the Project X config panel.
TemplateOverlayThe stats panel drawn over the game.

To make it yours:

  1. Rename the package and the four classes, e.g. MyCoolPlugin, MyCoolScript.
  2. Set pluginName in gradle.properties to the plugin class's simple name. The jar is named after it, the client finds your script by it, and it is the internal name of your store listing, which can never change.
  3. Change TemplateConfig.GROUP to something unique: it is the key the player's settings are saved under.
  4. Check the name is free in the live catalogue. A plugin class with the same name as a published script is treated as a stale copy of it and replaced.

Your first script

A script is two classes: a plugin (it shows up in the plugin list and starts and stops the script) and a script (the loop, which runs off the game's thread). This copper miner is the whole idea in about 30 lines. The template has the same shape with a bank trip, settings and a stats panel added.

CopperMinerPlugin.java
@PluginDescriptor(
        name = PluginDescriptor.Default + "Copper Miner",
        description = "Mines copper and drops the ore",
        tags = {"mining", "copper"},
        minClientVersion = "2.7.0",      // the simple API needs plugin API 2.7.0
        isExternal = true,
        enabledByDefault = false
)
public class CopperMinerPlugin extends Plugin {
    @Inject private CopperMinerScript script;

    @Override protected void startUp()  { script.run(); }
    @Override protected void shutDown() { script.shutdown(); }
}
CopperMinerScript.java
import net.runelite.client.plugins.projectx.ProjectX;
import net.runelite.client.plugins.projectx.Script;
import net.runelite.client.plugins.projectx.bot.*;
import java.util.concurrent.TimeUnit;

public class CopperMinerScript extends Script {

    public boolean run() {
        mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> {
            try {
                if (!ProjectX.isLoggedIn() || !super.run()) return;

                if (Inventory.isFull()) {
                    Inventory.dropAll("Copper ore");
                    return;
                }
                if (Player.isAnimating()) return;            // still mining

                var rock = Objects.query().nameEquals("Copper rocks").nearestReachable();
                if (rock == null) return;                     // nothing nearby right now

                if (rock.interact("Mine")) {
                    sleepUntil(Player::isAnimating, 3_000);   // wait for the swing to start
                }
            } catch (Exception e) {
                ProjectX.log("Copper miner: " + e.getMessage());
            }
        }, 0, 600, TimeUnit.MILLISECONDS);
        return true;
    }
}

Three habits to start with:

  • Wait with sleepUntil(condition, timeoutMs) when you are waiting for the game to change, not a fixed sleep.
  • Check for null. A search that finds nothing gives null. Or skip the check: Npcs.query("Cow").nearest().interact("Attack") on one line is the short way when you know a cow is there.
  • Search again every loop. The scene changes each tick, so do not keep a rock or an NPC from last time. Names are the in-game ones: the rock is “Copper rocks”.

The full API guide is docs/SCRIPTING.md in the client repo. The cookbook has a snippet for nearly everything else.

Old name -> new name

Scripts used to be written against Rs2* classes. Those names still work and are marked deprecated, so older scripts keep running, but new code should use the short names. The methods are the same; it is a rename.

OldNew
Rs2Inventory, Rs2Bank, Rs2EquipmentInventory, Bank, Equipment
Rs2Player, Rs2Walker, Rs2CameraPlayer, Walker, Camera
Rs2Widget, Rs2Dialogue, Rs2TabWidgets, Dialogue, Tabs
Rs2Magic, Rs2Combat, Rs2KeyboardMagic, Combat, Keyboard
Rs2Prayer, Rs2PrayerEnum.PIETYPrayers, Prayers.PIETY
Rs2GrandExchange, Rs2Shop, Rs2DepositBoxGrandExchange, Shop, DepositBox
Rs2InventorySetupLoadout
Rs2RandomRand
ProjectX.getRs2ItemManager()Items
ProjectX.getRs2TileObjectCache().query()Objects.query()
ProjectX.getRs2NpcCache().query()Npcs.query()
ProjectX.getRs2TileItemCache().query()GroundItems.query()
ProjectX.getRs2PlayerCache().query()Players.query()
Rs2NpcModel, Rs2TileObjectModel, Rs2TileItemModel, Rs2PlayerModelNpc, SceneObject, FloorItem, PlayerModel
query().withName("X")query("X") or query().nameEquals("X")
.click(action), .toList().interact(action), .list()

Two names can clash with other imports. Player is also RuneLite's own type: if a file needs both, write RuneLite's out in full (net.runelite.api.Player). Objects is also java.util.Objects: do not import that one, bot.Objects has the same methods. If a file has import net.runelite.api.*; or import java.util.*;, import the bot class by name.

Run your own scripts locally (dev folder)

You do not need to publish to try a script. Put your jar in %USERPROFILE%\.runelite\projectx-plugins\dev\ and restart the client. It loads from there and stays loaded, with no store check. Needs client 1.0.77 or later.

  • Restart the client after copying a new build over the old one; jars are read when it starts.
  • A jar that reuses a store script's name, package or hash is refused, so a local copy can never stand in for a listed script. Give your own script its own plugin class name.
  • Since 1.0.78 the client's My Scripts window has a Store tab and a Local tab, so you can see and switch on what you have in the dev folder.
Do not put test jars straight into ~/.runelite/projectx-plugins/. The client keeps only scripts the store knows about there and switches the rest off shortly after start-up. The dev folder is the place for your own work.

Testing tips

  • Compile against the live client. Calls that no longer exist, or have changed, fail the build instead of failing in front of a player.
  • Keep decisions in one place. A decide() method that works out the state from the game, as the template has, is easy to read, review and reason about.
  • Log state changes. Each launch writes a session log to ~/.runelite/logs/projectx/.
  • Submit it when it works. Every build is reviewed before it goes live. After the first approval, each new version is a new upload on the same listing; see Publishing.