events#
Messages between scripts. This is how the switch by the door tells the door to open, and how a personal HUD talks to a shop.
Use it to
- let a switch tell a door to open
- let the scripts in your own build talk to each other
- let a menu talk to a shop somebody else built
local events = require("events")Two channels, and the difference is who can hear#
There are four verbs and they come in two pairs. Picking the wrong pair is the most common confusion on this page, so start here.
| Pair | Reaches | Use it for |
|---|---|---|
emit / listen | scripts of the same owner | your own build talking to itself |
send / open | any owner who opted in | your HUD talking to somebody else's shop |
emit is private to you. Another creator's scripts never hear it, ever, even if they guess the channel name. A switch and its door are yours, so this is the pair you want almost every time.
send crosses owners, and both sides must agree. It only reaches scripts that called open on that exact channel. Opening is the receiver's consent, nobody can push a message into a script that never asked for one.
emit#
events:emit(channel, data) → true, or nil, reason
Sends to every script of the same owner that is listening on that channel, including the one that sent it.
events:emit("door:open", { by = player.id })channel is any name from 1 to 64 characters. data is optional.
listen#
events:listen(channel, fn) → handle
Hears what emit sent.
events:listen("door:open", function(data) if type(data) ~= "table" then return end swing_open()end)Registering the same channel again replaces the previous handler.
A switch and a door#
The switch:
--!strictlocal self = require("self")local events = require("events") self:on_touch(function(player) events:emit("gate", { by = player.id })end)The door, which can be anywhere in the region:
--!strictlocal self = require("self")local events = require("events") local open = false events:listen("gate", function(data) open = not open self:tween({ rot = { x = 0, y = open and 90 or 0, z = 0 }, seconds = 0.6, ease = "in_out", })end)Two switches and five doors work the same way, with no extra code: everything that emits "gate" opens everything that listens for it.
send#
events:send(channel, data) → true, or nil, reason
Sends on a public channel, to every script in the region that has that channel open, whatever owner they belong to. It never comes back to the sender.
events:send("acme.shop", { want = "rope" })Careful
Channel names are global to the region. Two creators who both pick "shop" are on the same channel and will hear each other. Prefix yours with something nobody else will use: a brand, your name, the build's name: "acme.shop", "mira.lift".
open#
events:open(channel, fn) → handle
Declares that this script is willing to be reached by anyone's send.
events:open("acme.shop", function(data, from) if from.kind ~= "hud" then return end give(from.owner, data.want)end)The handler gets a second argument saying who sent it:
| Field | Meaning |
|---|---|
kind | "object" (a world object), "hud" (somebody's personal HUD) or "attachment" (a script on an item they are wearing) |
owner | the id of the account that owns the sending script |
object | the sending object's id, or nil when it came from a HUD |
Treat from as the only thing you can trust. data is whatever the sender chose to put in it; from is stamped by the region.
Registering the same channel again replaces the previous handler.
What can travel#
data may be:
- nothing at all
- a boolean, a number or a string
- a table with string keys
Tables are limited to 3 levels deep, 32 entries, and strings of 512 bytes. Anything else is refused with a reason rather than silently trimmed.
Numbers and text come out the other side as what they were, so you do not have to convert the way self.store makes you.
The ceilings#
| Limit | Value |
|---|---|
Sends per event (emit and send together) | 16 |
| Deliveries before a cascade is cut | 64 |
| Channel name | 1 to 64 characters |
The cascade ceiling is worth understanding. If A emits to B, and B emits to C, and C emits back to A, you have built a loop. The region cuts it at 64 deliveries rather than letting it run. If a message of yours mysteriously stops arriving, a loop is the first thing to look for.
Choosing between events and a shared object#
Events are for telling, not for storing. If two scripts need to agree on a number, such as a score or an amount of fuel, one of them should own it in self.store and announce changes, rather than both keeping a copy.
See also#
- self:on_hud is how buttons on a menu report back
- objects acts on another object directly
- Talking between scripts has worked examples
Updated 17 October 2026