Skip to content
Project X

Developer docs

Reliability

The worst thing a script can do is carry on in a broken state: clicking the same thing forever, standing still for hours, or wandering off. reliability() notices when a script is failing or stuck, tries to put it right, and if it cannot, stops it and tells the player why. It never changes how your script plays. Needs plugin API 2.6.31 or later (2.7.0 if you use the simple API names, as these examples do).

The idea in one example

public boolean run(MyCoolConfig config) {
    reliability()
            .named("My Cool Script")                         // the name the player sees if it stops
            .trackProgress(() -> Inventory.count("Logs", true))
            .onRecover(ctx -> {
                Checkpoint cp = ctx.getCheckpoint();
                return cp != null && cp.getLocation() != null && Walker.walkTo(cp.getLocation());
            });

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

            if (Inventory.isFull()) {
                ActionResult open = reliability().step("Open bank")
                        .action(Bank::walkToBankAndUseBank)
                        .verify(Bank::isOpen, 5_000)
                        .run();
                if (!open.isSuccess()) return;           // already retried, recovered or stopped

                ActionResult deposit = reliability().step("Deposit logs")
                        .require(Preconditions.bankOpen())
                        .action(() -> Bank.depositAll("Logs", true))
                        .verify(() -> !Inventory.hasItem("Logs", true), 3_000)
                        .run();
                if (deposit.isSuccess()) reliability().checkpointAtPlayer("Banked");
            }
        } catch (Exception e) {
            log.error("MyCool loop failed", e);
        }
    }, 0, 600, TimeUnit.MILLISECONDS);
    return true;
}

The classes are in net.runelite.client.plugins.projectx.util.reliability. Every Script has its own service from reliability(); nothing changes for a script that never calls it. Script.shutdown() resets its failures and checkpoint, and keeps the recovery handler and progress signal, so set those once in run().

Guarded steps

A step describes one action before it runs: what must be true first, what to do, how to tell it worked, and how hard to try. Nothing happens until run().

CallMeaning
.require(Precondition...)Checks that must all hold before every attempt.
.action(BooleanSupplier)The action. Return false when it could not even be carried out (nothing to click); that attempt fails without waiting.
.perform(Runnable)The action, for calls that return nothing.
.verify(condition, timeoutMs)How to tell it worked, polled until the timeout. Without it, an action returning true is success.
.pollEvery(ms)How often verify is checked. Default 100 ms.
.maxRetries(n)Fewer retries for this step. It can lower the player's setting, never raise it.
.backoff(baseMs, maxMs)Wait between attempts, doubling each time. Default 600 ms (a tick) up to 4,800 ms.
.classifier(throwable -> Outcome)Decides whether an exception from the action is worth retrying.
.run()Runs it on the calling thread and returns an ActionResult.

An ActionResult tells you what happened: isSuccess(), getOutcome() (SUCCESS, RECOVERABLE or UNRECOVERABLE), getAttempts(), getReason(), getError() and isScriptStopping(). When a step did not succeed, return from the loop: the service has already retried, recovered or stopped the script, and the next pass starts fresh.

Preconditions

Ready-made checks in Preconditions, all safe to call from the script thread:

PreconditionHolds when
loggedIn()Logged in.
hasItem(name) / hasItem(itemId)The inventory has it. The name form matches part of the name.
hasItemAmount(name, n) / hasItemAmount(itemId, n)The inventory has at least n.
freeSlots(n) / inventoryNotFull()There is room.
bankOpen() / bankClosed()The bank interface is or is not open.
npcPresent(name) / objectPresent(name)One is loaded nearby.
reachable(tile)The tile can be walked to without doors or obstacles.
playerNear(centre, radius)The player is within radius tiles.
check(description, supplier)Any check of your own.

An unmet precondition fails the step without running the action, as recoverable. Add .fatal() to one (for example Preconditions.hasItem("Hammer").fatal()) to stop the script instead. The description appears in the log, and in the message to the player if the script stops.

Retries, recovery and safe stops

  1. A failed attempt is retried after the back-off wait, up to the player's “Retries per action”.
  2. A step that still fails is counted and handed to your recovery handler, with a RecoveryContext: getTrigger() (STEP_FAILED or NO_PROGRESS), getStep(), getReason(), getConsecutiveFailures(), getStepFailures() and getCheckpoint().
  3. When “Failures before stopping” is reached, either that many failed steps in a row or the same step failing that many times without once succeeding, the script is stopped safely.

The recovery handler runs on the script thread, so it may walk, bank and use guarded steps (those never start a second, nested recovery). Return true when the game is back in a known state. To give up, throw UnrecoverableActionException: from an action, a precondition, a verify check or the handler, it stops the script at once with its message as the reason.

A safe stop logs the reason, shows the player a message (“My Cool Script stopped itself so it would not carry on in a broken state. Reason: ... Last good step: ...”) and calls shutdown(). Stop the script yourself the same way with reliability().stop("Out of runes.").

Progress and checkpoints

Call reliability().tick() once per pass, after super.run(), and return when it is false. It counts time without progress. When the player's “No-progress timeout” passes, the recovery handler is called once; if a second timeout passes with still no progress, the script is stopped (straight away if there is no handler). Time logged out, paused, on a break, or with the loop not ticking for over 30 seconds does not count.

CallCounts as progress when
trackProgress(supplier)The value changes: an item count, total XP, a quest step. With a signal set, only a change or a checkpoint counts, so a script that “succeeds” at opening and closing the bank forever is still caught.
checkpoint(name) / checkpoint(name, location)Always. Also records the last known-good step, which recovery reads.
checkpointAtPlayer(name)Always; a checkpoint at the player's position.
progress()Always; no checkpoint recorded.
a successful stepOnly when no progress signal is set.

The player's settings

Players tune this under Project X settings > Script reliability. A running script picks up a change on its next step.

SettingDefaultRange
Enable script reliabilityOnOn or off
Retries per action30 to 10
Failures before stopping51 to 50
No-progress timeout120 seconds15 to 1,800 seconds
Detailed diagnostic loggingOffWrites every attempt, retry and checkpoint to the session log

With reliability switched off, steps still run once and still return their result, but nothing is retried, recovered or stopped automatically. stop(reason) works either way.

Guarded steps wait between attempts, so they refuse to run on the client thread: called from an overlay or an event handler, a step returns UNRECOVERABLE without doing anything.