Create: Computing AgesBrass Docs
Programming the world

Events

How a program waits for the world: the event queue, os.pull_event, timers, and the main loop that reacts to buttons, keys and messages at once.

A program that runs a contraption spends most of its life waiting: for a button, a key, a message from another computer, the end of a delay. In Brass, all of these arrive the same way, as events.

An event is a small table that describes something that just happened. It always has a name, and some events carry more fields:

Brass
local lever = {name = "redstone"}
local keypress = {name = "key", key = "enter"}
local order = {name = "message", sender = 12, channel = "door", data = "open", via = "cable", distance = 8.5}

This page explains where events come from, how to wait for them without missing any, and how to write the main loop that every serious program ends up with.

The event queue

Each computer keeps a queue of events. When something happens (a redstone input changes, a key is pressed, a message arrives, a timer rings), the computer adds an event at the end of the queue. os.pull_event takes the oldest event out of the queue and returns it. When the queue is empty, the program waits inside os.pull_event until an event arrives. Waiting costs nothing: the computer runs no instructions meanwhile.

Brass
os.queue_event("door", "open")
os.queue_event("lamp", 15)
local first = os.pull_event()
local second = os.pull_event()
print(first.name, first.data)
print(second.name, second.data)
Screen
door    open
lamp    15

os.queue_event adds an event of your own to the queue (its second argument becomes e.data). Events come out in the order they went in.

A few rules about the queue:

  • It holds 256 events at most. When it is full, the oldest event is dropped to make room for the new one. A program that stops reading events for a long time loses the oldest ones first.
  • Events keep arriving while the program is busy or asleep in sleep: they wait in the queue (see sleep and events).
  • The queue is emptied when the computer reboots or shuts down. A computer that is switched off receives nothing.
  • At the shell prompt, when no program runs, events other than typing are thrown away.

Waiting for one kind of event

os.pull_event takes an optional name. With it, the call returns only an event of that name:

Brass
os.pull_event("redstone")   -- waits until a redstone input changes
print("the button was pressed (or released)")

This is the easiest way to wait for one thing. But a filter has a price: every event that does not match is thrown away, not kept for later.

Brass
os.queue_event("click")
os.queue_event("redstone")
os.queue_event("message")
local e = os.pull_event("redstone")
print("got " .. e.name)
os.queue_event("check")
print("then " .. os.pull_event().name)
Screen
got redstone
then message

The click that was before the redstone event is gone. The message, which came after, is still in the queue. So a program that waits in a loop with os.pull_event("redstone") loses every click on its screen and every message that arrives in the meantime.

Watch out

net.receive is os.pull_event("message") in disguise: it also throws away every other event. A program that must react to messages and to something else needs the main loop below.

A filter is the right tool when the program really does one thing at a time: a door that waits for its button, then opens, then waits again. As soon as two things can happen, drop the filter.

The events of the game

namewhenfields
redstonea redstone input changes on one of the six facesnone: read the faces with rs.get
timera timer started by os.start_timer endsid
keya special key is pressed in the terminalkey: "enter", "backspace", "delete", "up", "down", "left", "right", "home", "end", "tab"
chara character is typed in the terminal (letters, digits, space...)char: the character
pasteCtrl+V in the terminaltext: the pasted text (4096 characters at most)
clicka click on the terminal screen (not on the Tube Computer's paper), or a right-click on a monitorx, y (character), px, py (gfx pixel), button, source
dragthe mouse moves with a button held, in the terminalthe same as click
messagea message from another computer (net)sender, channel, data, via, distance
linkthe strength received on a Redstone Link frequency the program used changes (link)a, b, power
diska storage medium is inserted or ejectedinserted: true or false
any nameos.queue_event(name, data)data

The Events page details every field. A few things worth knowing now:

  • The redstone event says that something changed, not what. Read the faces you care about with rs.get when it arrives (see Redstone and Create links).
  • Clicks count from 1, 1 at the top left. button is 1 for the left button, 2 for the right one, 3 for the middle one; a press on a monitor always has button 1. source is "terminal" or "monitor".
  • A right-click on a monitor reaches the program only once the program has waited for clicks: with os.pull_event() (no filter) or os.pull_event("click"). Otherwise the right-click opens the terminal, as usual. Sneaking always opens the terminal.
  • Ctrl+T (stop) and Ctrl+R (reboot) are not events: a program cannot catch them.

The main loop

Almost every program that controls something has the same heart: one os.pull_event without filter, in an endless loop, followed by a test on e.name:

Brass
while true do
  local e = os.pull_event()
  if e.name == "redstone" then
    -- a button, a lever, a detector
  elseif e.name == "click" then
    -- a press on the screen
  elseif e.name == "message" then
    -- another computer talks to us
  elseif e.name == "timer" then
    -- time to do the periodic work
  end
end

Nothing is lost, since every event comes out of the queue and goes through the tests. Events the program does not care about simply fall through.

Keep each branch short: while one branch runs, the other events wait. Put the state of the program (is the door open, which item is selected) in variables at the top of the file, so that every branch can read and change it.

When there are many kinds of events, a table of handlers reads better than a long if. Each handler is a function stored under the name of the event:

Brass
local handlers = {}
local crates = 0

function handlers.crate(e)
  crates = crates + e.data
end

function handlers.report(e)
  print("crates so far: " .. crates)
end

os.queue_event("crate", 3)
os.queue_event("noise")
os.queue_event("crate", 4)
os.queue_event("report")
for i = 1, 4 do
  local e = os.pull_event()
  local handler = handlers[e.name]
  if handler then
    handler(e)
  end
end
Screen
crates so far: 7

The noise event has no handler: it is ignored without any error. (A real program uses while true do instead of the for loop, which only stops this example after four events.)

Timers: periodic work without missing events

sleep(5) stops the program for 5 seconds: no event is handled during that time. To do something every few seconds and stay responsive, start a timer instead. os.start_timer(seconds) returns at once with a number, the id of the timer; when the time is up, a timer event with that id joins the queue.

A timer rings once. For periodic work, start a new one each time the previous one rings:

Brass
local refresh = os.start_timer(2)
while true do
  local e = os.pull_event()
  if e.name == "timer" and e.id == refresh then
    -- every 2 seconds: read the sensors, redraw the screen...
    refresh = os.start_timer(2)
  elseif e.name == "click" then
    -- clicks are handled at once, even between two refreshes
  end
end

Always compare e.id: as soon as a program uses two timers, a timer event alone does not say which one rang. A timer cannot be cancelled: when you no longer need it, forget its id, and its event will fall through the tests.

Since os.pull_event has no time limit, a timer also gives a timeout: wait for something, but not forever.

Brass
local function wait_for(name, seconds)
  local timeout = os.start_timer(seconds)
  while true do
    local e = os.pull_event()
    if e.name == name then
      return e
    elseif e.name == "timer" and e.id == timeout then
      return nil
    end
  end
end

local e = wait_for("redstone", 1)
if e == nil then
  print("nobody pressed the button")
end
Screen
nobody pressed the button

Like a filter, this function drops the other events it reads while it waits. Use it for short waits, or call the handlers of your main loop from inside it.

Time and timers explains timers, sleep and the clocks of the computer in more detail.

sleep and events

sleep does not take events out of the queue. Whatever happens while the program sleeps waits in the queue, and the next os.pull_event returns it:

Brass
os.start_timer(0.5)
sleep(2)
local e = os.pull_event()
print(e.name .. " rang during the sleep")
Screen
timer rang during the sleep

Nothing is lost (as long as fewer than 256 events pile up), but everything is late: a button pressed at the start of a sleep(10) is only seen 10 seconds later. In a program that must react, replace long sleeps with timers.

read() and events

read() waits for the player to type a line in the terminal. It only consumes key, char and paste events: the other events (redstone, timers, messages, clicks) stay in the queue, in their order, and the program finds them with os.pull_event after read() returns.

Brass
local timeout = os.start_timer(30)
write("Password: ")
local password = read("*")   -- the stars hide what is typed
-- a redstone change, a click or the timer during the typing are still in the queue here

While a program waits in os.pull_event, the keys typed in the terminal arrive as key and char events instead: nothing is echoed on the screen, the program decides what to do with each key.

How fast a program reacts

A computer runs once per tick (20 times a second) while it turns. An event queued during a tick is handled at the latest at the computer's next tick, so a waiting program reacts within a twentieth of a second.

When many events arrive together, the program can come back from os.pull_event (or any other wait) 16 times per tick at most, all within the instruction budget of that tick. A burst of 40 events is handled over three ticks: 16 in the first one, 16 in the second, the last 8 in the third.

Brass
for i = 1, 40 do
  os.queue_event("crate", i)
end
local start = os.time()
for i = 1, 40 do
  os.pull_event("crate")
end
print("done " .. os.time() - start .. " ticks after the start")
Screen
done 2 ticks after the start

A computer that does not turn (no rotation, or an overstressed network) is frozen: its events wait in the queue, and the program handles them as soon as the rotation comes back. The Speed, memory and limits guide explains the budget.

Example: a door with a button, a timer and the network

A door controller on a Minicomputer (or newer, for the network). A stone button is on the left face, an iron door stands on top of the computer. The door opens when the button is pressed, or when another computer sends "open" on the "door" channel, and closes by itself 5 seconds later. A "close" message closes it at once.

startup
local DOOR = "top"
local close_timer = nil   -- id of the pending timer, nil when the door is closed

local function open_door()
  rs.set(DOOR, true)
  close_timer = os.start_timer(5)   -- a new timer: the previous one will be ignored
end

local function close_door()
  rs.set(DOOR, false)
  close_timer = nil
end

while true do
  local e = os.pull_event()
  if e.name == "redstone" then
    if rs.get("left") > 0 then   -- only the press, not the release
      open_door()
    end
  elseif e.name == "message" and e.channel == "door" then
    if e.data == "open" then
      open_door()
    elseif e.data == "close" then
      close_door()
    end
  elseif e.name == "timer" and e.id == close_timer then
    close_door()
  end
end

Three sources of events, one loop, nothing missed. Pressing the button again while the door is open starts a new timer: the event of the old one no longer matches close_timer, so the door stays open 5 seconds after the last press. Another computer opens it with net.send(id, "open", "door").

A menu for a factory: the arrows move the selection, Enter runs the selected line, the number keys choose a line directly, and a click (in the terminal or on a monitor) works too. A Create Clutch is on the left face (a powered clutch stops the line), a lamp on top.

Brass
local items = {"Start the press line", "Stop the press line", "Toggle the lamp", "Quit"}
local selected = 1

local function draw()
  term.set_bg(term.colors.black)
  term.clear()
  term.set_cursor(3, 2)
  term.set_fg(term.colors.yellow)
  term.write("PRESS LINE CONTROL")
  for i, label in ipairs(items) do
    term.set_cursor(3, 3 + i)
    if i == selected then
      term.set_bg(term.colors.blue)
      term.set_fg(term.colors.white)
    else
      term.set_bg(term.colors.black)
      term.set_fg(term.colors.light_gray)
    end
    term.write(" " .. i .. ". " .. label .. " ")
  end
  term.set_bg(term.colors.black)
  term.set_fg(term.colors.gray)
  term.set_cursor(3, 10)
  term.write("Arrows + Enter, a number, or a click")
end

local function run(i)
  if i == 1 then
    rs.set("left", false)   -- clutch released: the line turns
  elseif i == 2 then
    rs.set("left", true)    -- clutch powered: the line stops
  elseif i == 3 then
    rs.set("top", rs.get_output("top") == 0)
  end
end

draw()
while true do
  local e = os.pull_event()
  local chosen = nil
  if e.name == "key" then
    if e.key == "up" and selected > 1 then
      selected = selected - 1
    elseif e.key == "down" and selected < #items then
      selected = selected + 1
    elseif e.key == "enter" then
      chosen = selected
    end
  elseif e.name == "char" then
    local n = tonumber(e.char)
    if n ~= nil and n >= 1 and n <= #items then
      chosen = n
    end
  elseif e.name == "click" and e.y >= 4 and e.y < 4 + #items then
    chosen = e.y - 3
  end
  if chosen ~= nil then
    selected = chosen
    if chosen == #items then
      break
    end
    run(chosen)
  end
  draw()
end
term.clear()
term.set_cursor(1, 1)
Screen
Screen

The loop reads every event without filter, so the same menu works with the keyboard, the mouse and a monitor. Note the test n ~= nil: tonumber returns nil for a letter. The Screens and monitors guide shows how to draw buttons and bigger interfaces, and buttons is a ready-made template.