Skip to content
Project X

Developer docs

Stats panel

Every Project X script shows the same style of panel: a title bar with the version and run time, a status line, then sections of rows, bars and skill stats. Players learn to read one panel, and you write a few lines instead of a hand-drawn overlay. Needs plugin API 2.6.25 or later (2.7.0 if you use the simple API names, as these examples do).

The three classes

All three are in net.runelite.client.plugins.projectx.util.overlay.

ClassWhat it does
ScriptPanelOverlayBase class for your overlay. Handles position (top left by default), layering and drawing. You implement build().
ScriptPanelThe panel itself, built with chained calls. Made afresh every frame; it is just a list of lines.
SkillTrackerXP gained, XP an hour, levels gained and time to level, for every skill, since the run started.

A complete overlay

public class MyCoolOverlay extends ScriptPanelOverlay {
    private final MyCoolPlugin plugin;
    private final Client client;
    private final SkillTracker skills = new SkillTracker();   // one per overlay

    @Inject
    MyCoolOverlay(MyCoolPlugin plugin, Client client) {
        super(plugin);
        this.plugin = plugin;
        this.client = client;
    }

    @Override
    protected ScriptPanel build() {
        MyCoolScript script = plugin.getScript();
        long startedAt = script.getStartedAt();       // stored once, when the run started
        skills.observe(client, startedAt);

        return ScriptPanel.create("My Cool Script", MyCoolPlugin.version)
                .theme(ScriptPanel.Theme.GREEN)
                .startedAt(startedAt)
                .status(script.getStatus())
                .section("EXPERIENCE")
                .skill(skills, Skill.WOODCUTTING)
                .section("SESSION")
                .row("Logs", ScriptPanel.withRate(script.getLogs(), skills.elapsedMs()))
                .bar("Inventory", Inventory.fullSlotCount() / 28.0, Inventory.fullSlotCount() + "/28");
    }
}

Add it in startUp() with overlayManager.add(overlay) and remove it in shutDown(). If build() throws, the overlay draws a bare panel with the error as its status line instead of disappearing, and logs the error at most once a minute.

build() runs on the client thread, every frame. Read fields and return. Never wait, click, walk or make a network call from it.

ScriptPanel reference

CallShows
ScriptPanel.create(name, version)A panel titled “name V version”.
.theme(ScriptPanel.Theme.X)ORANGE, PURPLE, GREEN, BLUE, RED or GOLD. Same layout, different accent.
.startedAt(epochMillis)The run clock in the title bar. 0 shows 00:00:00.
.status(text)One line under the title. Null or blank shows “Idle”.
.icon(image)A small image in the title bar, such as the item being made.
.section(title)A heading in the accent colour.
.row(label, value)A label and value. A null value shows a muted dash.
.row(label, value, color)The same, coloured: ScriptPanel.WARN, GOOD or BAD.
.bar(label, fraction, text)A progress bar, fraction from 0 to 1, with text beside it.
.skill(tracker, skill)Two rows: XP gained and XP/hr, then levels gained and time to level.
.trainedSkills(tracker, limit)One row per skill trained this run, most XP first. For combat and other mixed scripts.
.width(px), .labelWidth(px)Panel width, and where values start. Widen the label column for long labels.

Static helpers for your own rows:

HelperExample
ScriptPanel.withRate(count, elapsedMs)1,204 (310/h)
ScriptPanel.perHour(count, elapsedMs)310
ScriptPanel.compact(n)950, 54.5k, 1.2M
ScriptPanel.clock(ms)24:04:09
ScriptPanel.duration(ms)6h 4m, or 4m 12s under an hour

SkillTracker

Call observe(client, startedAt) at the top of every build(), then read gained(skill), perHour(skill), level(skill), levelsGained(skill), msToLevel(skill) (-1 when not gaining or at 99), trained() and elapsedMs().

The tracker starts again from zero whenever the startedAt you pass changes. Store System.currentTimeMillis() once when the script starts and pass that field every frame. Passing something worked out each frame, such as now - runtime, resets every number on every frame.

With buttons

An overlay that also carries its own components, such as a Pause button, needs an OverlayPanel parent. Use ScriptPanelComponent: call setPanel(panel) each frame, add it before the other children, and give the parent no background or border so only the panel's frame is drawn. Keep scene drawing (highlighting tiles or NPCs) in a separate overlay.