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
}
onStopruns on both a clean stop and a crash, release resources here.onPaintdraws the overlay HUD; its widgets are in Paint.- Event callbacks (
this.on) fire mid-frame: set flags orlog, and keep the work inloop().
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.