Lifecycle, pacing and settings

Lua · Updated 09 November 2026

on_loop returns server ticks, not milliseconds. Settings fields and stopping.

The lifecycle

  1. on_config_update(ctx, config) runs with the initial settings, if you define it.
  2. on_start(ctx) runs once.
  3. on_loop(ctx) runs repeatedly.
  4. on_stop(ctx) runs once at the end.

Only on_loop is required. If on_config_update raises, neither on_start nor on_stop runs.

on_loop returns server ticks

This is the biggest difference from Python and Java. In Lua, on_loop returns a delay in server ticks (about 0.6 seconds each), not milliseconds. A negative value stops the script.

Language return 5 from on_loop means
Lua wait 5 server ticks, about 3 seconds
Python wait 5 milliseconds
Java wait 5 milliseconds

Porting a Python or Java script? Divide its millisecond delays by 600.

The API gives you ctx:sleep_ticks(n) rather than a millisecond timer, on purpose. server_tick is the only clock to pace on. game_cycle and publish_seq are exposed but are not timers, and pacing off them makes a script run about 30 times too fast.

ctx:sleep_ticks(n) returns true when the ticks have passed and false when the script is stopping. If the server tick stalls (in the lobby, or with a frozen client) it keeps waiting.

Settings

Declare settings as config_fields. The GUI shows them as a form:

local config = require("botwithus.config")

script.config_fields = {
  config.int_field("logs", "Logs per trip", 28, { min = 1, max = 28 }),
  config.choice_field("tree", "Tree", { "Tree", "Oak", "Willow" }, "Oak"),
  config.bool_field("bank", "Bank the logs", true),
  config.string_field("note", "Note", "", { max_len = 64 }),
  config.item_id_field("food", "Food"),
}

function script.on_config_update(ctx, cfg)
end
  • cfg holds every field, for example { logs = 28, tree = "Oak", bank = true, note = "", food = -1 }.
  • on_config_update runs once before on_start, and again between loops whenever the user changes a setting. ctx.config always holds the settings in force.
  • item_id_field defaults to -1, meaning "no item".
  • A script is refused before it runs if a choice default is not one of its options, a choice has no options, an item_id default is below -1, or a string default is over 256 bytes without a larger max_len.
  • On the command line the script gets the defaults.

Stopping

When the GUI stops a run:

  • ctx:stopping() turns true;
  • ctx:sleep_ticks(n) returns false at once;
  • no further on_loop runs;
  • on_stop runs.

A script stuck in a busy loop of its own does not see the stop, and after 10 seconds the GUI ends the process. So wait with ctx:sleep_ticks, and check ctx:stopping() inside any long loop. A blocking walk (ctx.game:walk(...)) also ends early on a stop, with its normal "cancelled" result.

When the game client goes away, the run ends on its own: no further on_loop, then on_stop. In on_stop the game reads still return the last state, so you can log where the character was, but actions no longer reach the client.

Next: Modules, require and walking.