rs2b0t docs

Manual › Scripting API › Bots

Bots

Bot base classes #

All bots extend AbstractBot (usually via LoopingBot, TaskBot, or TreeBot).

Lifecycle hooks #

abstract class AbstractBot {
    loopDelay: number;                 // legacy pacing; 600 = one server tick (see loopCadence)
    loopCadence: LoopCadence | null;   // optional explicit frame | server-tick | time
    readonly settings: SettingsBag;    // resolved run parameters

    onStart?(): void | Promise<void>;  // before the first loop
    onStop?(): void;                   // cleanup after stop or crash
    onPause?(): void;
    onResume?(): void;
    onPaint?(ctx: CanvasRenderingContext2D): void; // overlay HUD, every redraw

    recoveryAnchor?(): Tile | null;    // watchdog recovery walks back here when the bot is 8+ tiles away
    grindTargets(): string[];          // NPCs this bot fights; the random-event detector ignores them
    ignoredRandoms(): string[];        // ignored random events, checked on each detection

    log(msg: string): void;
    on<K>(event, cb): void;            // event subscription, auto-removed on stop; public so a task can subscribe for its bot
}
  • onStop runs on both a clean stop and a crash, release resources here.
  • onPaint draws the overlay HUD; its widgets are in Paint.
  • Event callbacks (this.on) fire mid-frame: set flags or log, and keep the work in loop().

LoopingBot #

The common case: implement loop(). Return a number to override pacing for the next iteration (0 = next frame, 600 = next server tick, other = wall-clock ms). Set loopCadence on the bot for an explicit { kind: 'frame' | 'server-tick' | 'time' } policy (see #427).

abstract class LoopingBot extends AbstractBot {
    abstract loop(): number | void | Promise<number | void>;
}

TaskBot #

A priority list of tasks. Each loop, the first task whose validate() returns true has its execute() run.

interface Task {
    validate(): boolean | Promise<boolean>;
    execute(): void | Promise<void>;
}
abstract class TaskBot extends LoopingBot {
    protected add(...tasks: Task[]): void; // usually in onStart, highest priority first
}
class Fighter extends TaskBot {
    override onStart() {
        this.add(
            { validate: () => Game.energy() < 20, execute: async () => { /* rest */ } },
            { validate: () => !Game.inCombat(),    execute: async () => { /* attack */ } },
        );
    }
}

TreeBot #

A behaviour tree. Walk BranchTask.validate() from root() until a LeafTask, then run it, once per loop.

abstract class BranchTask { validate(): boolean; success(): TreeNode; failure(): TreeNode; }
abstract class LeafTask   { execute(): void | Promise<void>; }
type TreeNode = BranchTask | LeafTask;
abstract class TreeBot extends LoopingBot { abstract root(): TreeNode; }

Execution #

The only legal way to sleep. Awaiting anything else escapes the runtime. Stop can't unwind it and the watchdog warns.

Execution.delay(ms: number): Promise<void>          // wall-clock
Execution.delayTicks(n: number): Promise<void>      // n server ticks (~600ms each)
Execution.delayUntil(cond: () => boolean, timeoutMs = 6000): Promise<boolean>
Execution.delayUntilTicks(cond: () => boolean, maxTicks: number): Promise<boolean>   // the same, bounded in server ticks

delayUntil resolves true when cond() holds (checked once per frame), false on timeout. Use it to confirm an action landed:

const before = Inventory.used();
await item.interact('Bury');
const ok = await Execution.delayUntil(() => Inventory.used() < before, 3000);
Execution.noteProgress(): void   // work the watchdog cannot see

The watchdog reads progress from tile movement and xp, so a script that trades or reads chat from one tile looks wedged after ten minutes and is walked home or restarted. noteProgress reports work it cannot see. Call it only after that work was observed, as MarketMaker does behind onStation() && !tradeStalled(). An unconditional call per loop turns wedge detection off.


Registering a bot #

interface BotManifestInput {
    name: string;
    description?: string;
    version?: string;
    category?: string;      // filter chip in the library (e.g. "Mining")
    tags?: string[];        // free-form search labels
    settingsSchema?: SettingsSchema;
    create(): AbstractBot;
}

function defineBot(manifest: BotManifestInput): BotManifest;   // default-export this
function registerScript(manifest: BotManifestInput, origin?: string): void; // imperative

Default-export defineBot({...}) from your entry module. The URL loader calls registerScript for you; in-tree scripts are registered from src/bot/scripts/index.ts.


See also #

Docs built from rs2b0t fe14990b 2026-09-18. Non-affiliation disclaimer. Built on the Lost City project.