Developer docs
Script lifecycle
Plugin that the client switches on and off, and a Script that runs the loop on its own thread. This page follows one run from start to stop.From switch-on to switch-off
- The player switches the plugin on. The client calls your plugin's
startUp()on the Swing UI thread. startUp()adds the overlay and callsscript.run(config), which schedules the loop and returns straight away.- The loop runs on the script's own background thread, once per game tick, until it is cancelled.
- The player switches the plugin off. The client calls
shutDown(), which callsscript.shutdown()and removes the overlay.
The plugin
@PluginDescriptor(
name = "My Cool Script",
description = "One line about what it does",
tags = {"woodcutting", "skilling"},
version = MyCoolPlugin.version,
minClientVersion = "2.7.0", // the plugin API level you need, see Versioning
enabledByDefault = false,
isExternal = true // required for anything not built into the client
)
public class MyCoolPlugin extends Plugin {
public static final String version = "1.0.0";
@Inject private MyCoolScript script;
@Inject private MyCoolConfig config;
@Inject private OverlayManager overlayManager;
@Inject private MyCoolOverlay overlay;
@Provides
MyCoolConfig provideConfig(ConfigManager configManager) {
return configManager.getConfig(MyCoolConfig.class);
}
@Override
protected void startUp() {
overlayManager.add(overlay);
script.run(config); // schedule the loop and return
}
@Override
protected void shutDown() {
script.shutdown();
overlayManager.remove(overlay);
}
}The plugin class's simple name is the script's identity. The jar is named after it, and your store listing's internal name must match it exactly. Do not rename it after publishing.
The Script base class
Extend net.runelite.client.plugins.projectx.Script. It gives you:
| Member | What it is for |
|---|---|
scheduledExecutorService | The thread pool your loop runs on. Never the client thread. |
mainScheduledFuture | Assign your loop's schedule to this. shutdown() cancels it, and isRunning() checks it. |
super.run() | The pre-loop check. Call it first in every pass and do nothing when it returns false. |
reliability() | Guarded steps, recovery and safe stops. See Reliability. |
shutdown() | Cancels the loop and resets the shared helpers. Override it to clear your own state, and call super.shutdown() first. |
isRunning() | True while the main loop is scheduled. Handy in the overlay. |
Script also inherits the waiting helpers: sleepUntil(condition, timeoutMs), sleep(min, max), sleepGaussian(mean, stddev), sleepUntilNextTick() and sleepTicks(ticks). All of them return at once, without waiting, if called on the client thread.
super.run() returns false while a blocking event is being handled (the login screen, a level-up message, a random event), while scripts are paused, and once the thread has been interrupted. It also turns run on and drinks stamina potions according to the player's Project X settings.
The loop
public class MyCoolScript extends Script {
private volatile long startedAt;
public boolean run(MyCoolConfig config) {
startedAt = System.currentTimeMillis();
mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> {
try {
if (!super.run() || !ProjectX.isLoggedIn()) return;
// One state's worth of work, then return. The next pass carries on.
if (Inventory.isFull()) {
bank();
} else {
work(config);
}
} catch (Exception e) {
// Anything that escapes cancels the schedule silently.
log.error("MyCool loop failed", e);
}
}, 0, 600, TimeUnit.MILLISECONDS); // 600 ms is one game tick
return true;
}
@Override
public void shutdown() {
super.shutdown();
// clear your own fields here, so the next start is clean
}
}Keep each pass short and decide what to do from the game, not from what the last pass assumed. A script that asks “is the inventory full? is the bank open? am I at the spot?” every pass can be started anywhere and recovers from anything. For scripts with many phases, a small enum of states and one method per state keeps this readable; the template shows the pattern. The client also has an opt-in StateMachineScript<S> base class with declared transitions, if you prefer that style.
Stopping cleanly
When the script cannot carry on (out of supplies, goal reached, a setting missing), stop it and tell the player why:
if (!Bank.hasItem("Shark", true)) {
reliability().stop("Out of sharks in the bank."); // shows the message, then calls shutdown()
return;
}reliability().stop(reason) works whether or not the player has reliability switched on. To do it by hand, call ProjectX.showMessage(reason) and then shutdown(), and return from the loop straight away: shutdown() interrupts the thread you are on.
ProjectX.showMessage(String) opens a dialog and returns immediately; it never waits for the player to click. ProjectX.showMessage(String, int disposeTimeMs) closes itself after that long. Empty messages are ignored.
Status text
ProjectX.status is a single shared string, and many helpers write to it themselves: opening the bank sets it to “Opening bank”, clicking an object sets it to the action and the name. For the status line in your stats panel, keep a volatile String status field of your own and show that.