Docs · The language

The language

The parts of a .bot script that are not commands — the events, the ways to make decisions and loops, the kinds of name, states, and waiting. Commands (the verbs like interact and nearestNpc) each have their own page in the reference on the left; this page is the grammar that holds them together.

Events — when your code runs

A script does not run top to bottom. You hang code off events, and the engine calls them at the right moment. A script has either one on loop or some states — never both — plus any of the others.

on start

Runs once, before anything else. Set up variables, ask each x() guard once, print a hello.

on loop

The heartbeat. Runs, ends, and runs again — it does not loop by itself. End it with return <ms> to say how long to wait before the next pass; return 0 comes straight back.

on stop

Runs once when the script is stopped. Print a total, tidy up. It does not run when you close the client window — that shuts the process down without unloading scripts, so press stop first if the script has something to say.

on message(line, from)

Fires the instant a game line arrives, between passes, so a flag it sets is already true on the very next on loop. from is the sender for a private message, "" for a public or system line. One per script.

on menu(kind, action, target)

Fires for every menu action your client sends — attacking, taking, chopping, casting — whether your script asked for it or you clicked it yourself. Delivered between passes like on message. RSCFalador only: elsewhere it never runs. One per script.

on render — drawing the overlay

A draw-only event, pumped about 30 times a second while the loop idles, where a script paints a HUD over the game. It is big enough to have its own page: Drawing & paint.

Reacting to what the game says
var resting = false

# runs the moment a line arrives, between passes
on message(line, from) {
    if contains(lower(line), "you are tired") { resting = true }
    if contains(lower(line), ", well done")   { resting = false }
}

on loop {
    if resting { return 1000 }
    # ... do the work ...
    return 600
}

Fold the line with lower() and test it with contains() — a message is rarely the exact string you would compare with ==. If you would rather ask than react, lastServerMessage() reads the most recent line and is legal inside a wait until.

Watching what your client actually does
var taken = 0

# one right-click option or left-click default = one action
on menu(kind, action, target) {
    print(action, "->", target)
    if kind == "take_item" { taken = taken + 1 }
}

Fires whether your script asked for the action or you clicked it yourself. The whole story — all fifty kinds, every reader, and what each one gives back — is Menu actions, next.

Keys and buttons — driving a running script

A script does not have to decide everything by itself. Bind a key, or put a button on its tab, and you can flip it into banking, dump the inventory, print a total or stop it dead — while it runs, without editing anything. Both take the name of a zero-argument fun, written as a literal string so the checker can prove that fun exists before the script ever starts.

hotkey("F1", "onBank")

Binds a key in the game window. While it is bound the client never sees that key; every other key reaches the game untouched.

uiButton("Drop all", "onDrop")

Puts a button on the script's own tab. Buttons upsert by their label, so calling it again with the same label changes nothing.

A key and a button that do the same job
var banking = false

on start {
    uiTab("Miner")
    hotkey("F1", "toggleBanking")
    uiButton("Bank now", "toggleBanking")
}

fun toggleBanking() {
    banking = not banking
    print("banking is now", banking)
}

on loop {
    if banking { return 1200 }
    return 600
}

Neither runs where you pressed it. A key press arrives on the launcher's thread and a click on the UI thread; both are queued and your fun runs on the script thread, between passes, exactly like on message and on menu. So it never interrupts an on loop half way through, and it is safe to touch the same variables from both.

Naming a key

Keys are named the way the Java keyboard names them and compared without case, so "f1" and "F1" are the same key.

Function keys"F1" … "F12"
Letters and digits"A" … "Z", "0" … "9"
Named keys"Enter", "Escape", "Space", "Tab", "Backspace", "Insert", "Delete", "Home", "End"
Arrows"Up", "Down", "Left", "Right"
Pages"Page Up", "Page Down"

The key itself, not a chord. There are no modifiers — hotkey("G", …) fires on G whether or not Shift is down, and there is no way to ask for Ctrl+G. Pick keys the game does not need: the function keys are the safe ones, letters are not, because the client uses them for chat. The launcher's own shortcuts (Ctrl+Tab, Ctrl+Shift and an arrow, Ctrl+Shift+1…9) are taken and a script cannot claim them.

The rules

Bind in on start

That is where it belongs — once, before anything runs. Binding in on loop works but re-registers the key every pass for nothing.

Rebinding replaces

Call hotkey again with the same key and it runs the new fun instead. The key is claimed once, however many times you bind it.

Dropped when the script stops

Every binding goes when the script does, and the key reaches the game again. There is nothing to unbind by hand.

Per client, on the focused one

With several clients in the window, a key fires for the client that has the keyboard. A key bound only by a script on another client is neither fired nor swallowed.

Only over the game

Keys are claimed only while the game area has focus. Typing in the editor, the console or a settings box is never intercepted.

Two scripts may share a key

Both funs run. The key stays claimed until the last of those scripts stops.

Examples

A pause key, and a panic key
var paused = false

on start {
    uiTab("Fighter")
    hotkey("F2", "togglePause")
    hotkey("F4", "panic")
    uiButton("Pause", "togglePause")
}

fun togglePause() {
    paused = not paused
    print(paused, "<- paused")
}

fun panic() {
    print("stopping on request")
    stop
}

on loop {
    if paused { return 1000 }
    # ... the actual work ...
    return 600
}

stop inside a bound fun ends the script the same way it does anywhere else, and on stop still runs — so a total printed there is printed.

Cycling a mode with one key
enum Mode { FIGHT, LOOT, BANK }

var mode = Mode.FIGHT

on start {
    uiTab("Multi")
    hotkey("F3", "nextMode")
    uiButton("Next mode", "nextMode")
}

fun nextMode() {
    if mode == Mode.FIGHT {
        mode = Mode.LOOT
    } else if mode == Mode.LOOT {
        mode = Mode.BANK
    } else {
        mode = Mode.FIGHT
    }
    uiStat("mode", "Mode", str(mode), "")
}

on loop {
    if mode == Mode.FIGHT { return 600 }
    if mode == Mode.LOOT { return 800 }
    return 1500
}

The fun takes no arguments and returns nothing — that is the whole contract. Everything it needs comes from vars, and everything it changes is read by the loop on its next pass.

A key that does something to the game
on start {
    uiTab("Helper")
    hotkey("F5", "dropJunk")
    uiButton("Drop junk", "dropJunk")
}

fun dropJunk() {
    let gone = dropAll("Bones")
    print("dropped", gone, "bones")
    wait 600
}

on loop {
    return 600
}

Because it runs on the script thread, a bound fun may do anything a pass may do — act on the game, and even wait. The loop simply does not run while it is working, so keep it short: a fun that waits ten seconds is ten seconds the bot is not botting.

The rest of the panel

Buttons are one of a family that builds the script's own tab: uiTab, uiHeader, uiLabel, uiStat, uiProgress, uiNote, uiSeparator to show things; uiToggle/uiToggled, uiInput/uiText, uiNumber, uiChoice/uiChosen to ask for them; and uiHas, uiRemove, uiClear to manage the rows. Each has its own card in the ui group. A toggle you read on the next pass is often simpler than a button that sets a flag — reach for uiButton when you want something to happen, and for uiToggle when you want something to be.

Kinds of value

Every value has a kind and the engine never quietly converts. 151 and "151" are not the same — quotes mean a name, so bankWithdraw("500", 10) looks for an item named "500".

intwhole number — 151, 1_000
stringtext in quotes — "Rock"
booltrue / false
tilea spot on the map — tile(303, 553)
npc object item…a thing you found (read .name, .tile with a dot)
listseveral of a thing — what a for walks over
bagyour own record — named fields you put in and read back; see Bags & JSON
nonenothing was found. A type written with ? can be none, and the checker will not let you use it until you have tested it

Read a property with a dot and no brackets: rock.tile.x. Anything that needs a second value is a command instead: distance(a, b), not a.distanceTo(b). A handle's properties are the reading taken when you found it, not a fresh one — find things again inside the loop that uses them.

bag — the script's own record

The closest thing the language has to a class. newBag() makes one; bagPut(b, "hp", 30) gives it named fields (numbers, texts, flags, tiles, nested bags, and lists of any of those); typed reads take them back — bagNumber(b, "hp") is an int? (none when missing or a different type), bagNumberOr(b, "hp", 0) an int. Bags are mutable and compared by identity (== asks "same bag"), unlike lists, which stay immutable. A list<bag> is a table — all the usual list commands work on it, and bag can be written as a type: var rows = bagList(), fun f(bag b) -> int, list<bag> in a fun's signature. The whole story — tables, queries, saving, JSON — is on Bags & JSON.

Decisions & loops

if invIsFull() {
    become banking
} else if invCount() > 20 {
    print("nearly there")
}

for plant in objectsByDistance(name: "Flax", within: 6) {
    if interact(plant, "Pick") { return 900 }
}

if needs a yes/no answer — if invCount() > 0, not if invCount().

for x in <list> walks a list once — the items a query like objectsByDistance(...) returns, nearest first.

Inside a block, return <ms> ends the whole pass right there; stop ends the script.

Waiting

Two ways to pause. wait <ms> sleeps for a fixed time. wait until <condition> sleeps until something becomes true — and always needs a timeout, because an untimed one is the single loop that can never finish. Only reading commands may appear in the condition; an acting command (one marked does something) is refused there.

wait 600                                  # a fixed pause

wait until bankIsOpen() timeout 6000 else {
    print("the bank did not open")
    return 1000
}

Names — var, let, setting, fun

var and let

var survives between passes — trip counters, flags. let lives for one pass and is gone at its end. Nothing is created just by writing a name; declare it first.

var trips = 0
let rock = nearestObject(id: 102, within: 6)

setting

A dial the person running the bot can turn — it shows up on the settings panel and reading it in the script gives its value.

setting int rockId = 151 "Which rock"
setting bool bank = true "Bank the ore"

fun — your own commands

Give a piece of logic a name. A fun may take arguments and may return a value with -> type. Call it like any command.

fun bagIsFull() -> bool {
    return invFree() == 0
}

on loop {
    if bagIsFull() { become banking }
    return 600
}

States — two jobs, one bot

A mining bot mines, then banks. Give each job a name with state. The first state is where it starts; become <name> says which runs next. A script has either one on loop or some states, never both.

state mining {
    if invIsFull() { become banking }
    let rock = nearestObject(id: 102, within: 6)
    if rock == none { return 1000 }
    interact(rock, "Mine")
    return 900
}

state banking {
    if invCount() == 0 { become mining }
    # ... walk to the bank and deposit ...
    return 1000
}

Enums — your own set of names

A script can declare its own closed set of names and use it as a type. Each enum is a type of its own: a member never equals a string or a member of another enum, and the checker refuses the comparison. A setting with an enum type appears as a dropdown of the members.

enum Mode { POWER, BANK, SELL }

setting Mode mode = Mode.POWER "What to do with ores"

if mode == Mode.BANK { become banking }
print(mode)        # Mode.POWER
print(mode.name)   # POWER