Skip to content

State and events

How modules keep data and talk. A module declares its state at its top, typed: a value of the match, of the server, of each player, or of each entity of a kind. It declares its events the same way. Modules share nothing else but the files they require.

Each declaration says who is sent it with read_by: a value or an event without one stays on the server. One with a group reaches the players it picks, and their client scripts read it as match.round or me.kills. See who sees what.

Handlers hear events, the engine’s and the modules’, with on, given at a module’s top too. Requests go the other way: a client script asks the server with request, and the server’s handler checks what it is asked.

On both sides match: table

The match's state: the engine's, match.map, match.gametype and match.number, and each match_state.

On both sides server: table

The server's state: its settings, as server.hostname, and each server_state.

On the server everyone: Group

Every player.

On the server owner: Group

The owner of what it is given to: the player it is about, alone.

On the server team: Group

The players on the owner's team, the owner too; no one when they are on none.

On the server spectators: Group

The players watching the owner through their eyes while dead.

On the server admins: Group

The players the admins setting lists.

On the server on(name, fn, { after? }?)
On a client on(name, fn)

Hears an event, the engine's or a module's, by its name or as <module>.<name>: fn is given its fields.

On the server

A handler is given at a module’s top, as it loads, and a module has one of a name in each of its files: a second replaces the first. Handlers run in the order the modules loaded, those after once the rest have. Each runs as a coroutine: before its first wait it may change the event’s fields, which those after it and the clients get, or return false, which stops the event there, and the clients hear nothing. After a wait, what it held may be gone: check exists. A handler’s error is said once and undoes what it did since it started or last waited, its changes to the event among them; the other handlers run on. A name no module declares and the engine has not fails the load.

NameTypeWhat it is
namestringNeeded.
fn(e: {[string]: any}) -> anyNeeded.
optionstable?A table of the options below. It, and each option, may be left out.
afterbooleandefault falseRuns once the handlers without it have, as a module's default does after a gametype's own.
Fails
  • a value of the wrong type
  • an option it does not have

On a client

Hears an event, the engine’s or one of the modules’ the server sends this player, by its name or as <module>.<name>: fn is given its fields, frozen.

A handler is given at a script’s top, as it loads, one a name in each file of the script, its own and each it requires: a second replaces the first. Several scripts may hear one name, in the order they listened. Each frame, the players who joined and left come first, then what the triggers heard of the player, then the events in order, then frame. A module’s event with no read_by, which stays on the server, fails, as does a request. A value an event reads may come after it: check for nil where that matters. A client has no wait: a handler that yields fails, but where open waits for a dialog’s answer.

NameTypeWhat it is
namestringNeeded.
fn(e: {[string]: any}) -> ()Needed.
Fails
  • a value of the wrong type
on("joined", function(e)
say(`Welcome, {e.player.name}`)
end)
on("fell", function(e)
e.amount /= 2
end)

To keep a kill quiet once every other module has heard of it

Section titled “To keep a kill quiet once every other module has heard of it”
on("killed", function(e)
return false
end, { after = true })
local frames = 0
on("frame", function(e)
frames += 1
end)
local deaths = 0
on("killed", function(e)
if e.victim == me then
deaths += 1
end
end)
On a client request(name, payload?)

Sends the server one of its requests, by its name or as <module>.<name>, its payload checked against the declaration here and again there, as the frame ends.

The payload may be left out where none of its fields is needed. While the match loads, requests wait until it plays, 64 at most; those past it are dropped. A request has no reply: what it does comes back as state or an event.

NameTypeWhat it is
namestringNeeded.
payload({[string]: any})?May be left out.
Fails
  • a value of the wrong type
mount(function()
return Box {
"Buy armor",
onClick = function()
request("buy_armor")
end,
}
end)
On the server require(path) → any
On a client require(path) → any

Runs another of the mod's server or shared files, by its path in its layer and its side without .luau, and gives what it returns.

On the server

A file runs once a load, the first time it is required, with main.server.luau‘s globals, and every module requiring it gets the one value it gave: keep what changes in declared state, not in it. A path is lowercase letters, digits and _ between slashes, as modules/teams/teams_lib.server or modules/ranks/ranks.shared. A client file is the clients’ alone and a weapon’s the engine’s, so neither is required here, and a file requiring itself fails. A mod of one file has no require.

NameTypeWhat it is
pathstringNeeded.
Returns
any
Fails
  • a value of the wrong type

On a client

Runs another of the mod’s client or shared files, by its path in its layer and its side without .luau, and gives what it returns.

A path is a .client file (modules/ui/button.client), a .shared one the server runs too (modules/ranks/ranks.shared) or another script (modules/teams/teams.client). A server file, even by name, or a file the server did not name fails, as does a file requiring itself. Each script runs a file once a load, in an environment of its own, and keeps what it gave: two scripts requiring one component get one each, each listening and drawing for itself. Its time is the script’s that required it.

NameTypeWhat it is
pathstringNeeded.
Returns
any
Fails
  • a value of the wrong type
On the server group(picks) → Group

Makes a group of a function, which + and - combine with the others: it picks the players it returns true for.

A read_by takes the function as it is too. The same function makes the same group. It is given each player’s object as viewer, and picks them for any value but nil and false; it may only read. It is asked again at once when a declared value it read changes, or a player’s name or team, or someone joins or leaves; else once a second.

NameTypeWhat it is
picks(owner: Player?, viewer: Player) -> booleanNeeded.
Returns
Group
Fails
  • a value of the wrong type

To show a flag to the living and the admins

Section titled “To show a flag to the living and the admins”
local living = group(function(_, viewer)
return viewer.health > 0
end)
local flag = model("rocket", { read_by = living + admins })
On the server emit(name, payload?) → ({[any]: any})?

Sends a module's event, its payload checked against its declaration: to its handlers here, then to the players its read_by picks.

The name may be another module’s as module.name. The payload may be left out where none of its fields is needed. It gives the event as its handlers left it, or nil if one stopped it. An event naming an entity, or an NPC as its player, goes only to those shown it, and to no one once its handlers removed it. Its read_by is asked again at each emit. A request is not emitted: clients send it.

NameTypeWhat it is
namestringNeeded.
payload({[string]: any})?May be left out.
Returns
({[any]: any})?
Fails
  • a value of the wrong type

To tell everyone a round was won, and say so here

Section titled “To tell everyone a round was won, and say so here”
event("round_won", t.record { team = t.int }, { read_by = everyone })
on("round_won", function(e)
say(`Team {e.team} wins the round`)
end)
on("start", function()
emit("round_won", { team = 1 })
end)
On the server on_request(name, schema, fn)

Declares a request clients send, its payload a t.record, and its one handler, given its sender as player.

It is declared at a module’s top, as it loads, and named as an event is; a second handler of a name fails the load. A client script sends it with request, a player with /request or a key bound to it. It arrives checked against its declaration, the players it names here and the entities alive and shown to the sender, at most 10 a second after a burst of 5; anything else is dropped. Any player may send any request at any time, so its handler checks what it is asked and never takes a price from the client. It has no reply: the result goes back as state or an event. Its handler runs on the sender’s time, 2 ms a tick: one past it is dropped, and a player past it 20 ticks in a row is kicked. 16 handlers of one player’s requests wait at once at most; the rest are dropped.

NameTypeWhat it is
namestringNeeded.
schemaTypeNeeded.
fn(e: {player: Player, [string]: any}) -> ()Needed.
Fails
  • a value of the wrong type
player_state("coins", t.int { min = 0 }, 0, { read_by = owner })
on_request("buy_armor", t.record {}, function(e)
if e.player.coins >= 100 then
e.player.coins -= 100
e.player:set_max_health(150)
end
end)
On the server match_state(name, type, default, { read_by?, help? }?)

Declares a value of the match's, which match.<name> reads and writes, from default again each match.

A declaration is made at a module’s top, as it loads. Its name is up to 32 lowercase letters, digits and _, not a Luau word nor one of the engine’s (map, gametype, number), and is the module’s alone: another module’s of the name fails the load, naming both. It is read as match["<module>.<name>"] too, main. for main.server.luau’s and engine. for the engine’s (match["engine.number"]). A write is checked against its type at its line, and a value outside its limits is refused, never clamped. A list, a record or a map read is frozen: change it by writing it whole. Without a read_by it is the server’s alone; one picking by an owner (owner, team, spectators) fails, as the match has none.

NameTypeWhat it is
namestringNeeded.
typeTypeNeeded.
defaultanyNeeded.
optionstable?A table of the options below. It, and each option, may be left out.
read_by(Group | ((Player?, Player) -> boolean))?The players sent it, whose client scripts read it; the server's alone without.
helpstring?What it is, in a line of up to 128 bytes, which the editor shows on hover.
Fails
  • a value of the wrong type
  • an option it does not have

To count the rounds, for everyone’s screen

Section titled “To count the rounds, for everyone’s screen”
match_state("round", t.int { min = 0 }, 0, { read_by = everyone, help = "The round being played" })
on("start", function()
match.round += 1
end)
On the server server_state(name, type, default, { read_by?, set_by?, help? }?)

Declares a value of the server's, which server.<name> reads and writes, kept from match to match until the server stops; with set_by, a setting.

It is checked as match_state’s is. A setting has its value as soon as it is declared, so the lines after it read it as configured: its default, under the server’s file, under the environment (SET_SCORE_LIMIT, or SET_TDM__SCORE_LIMIT for module tdm’s), under --set, under /set or a write of the mod’s. A setting is not named as one of the engine’s commands or the client’s settings. One the server’s --lock fixes refuses a write, failing at its line (server.score_limit is locked at 30), and at a module’s top failing the load. A secret, t.secret(t.string), is set_by = config and has no read_by.

NameTypeWhat it is
namestringNeeded.
typeTypeNeeded.
defaultanyNeeded.
optionstable?A table of the options below. It, and each option, may be left out.
read_by(Group | ((Player?, Player) -> boolean))?The players sent it, whose client scripts read it; the server's alone without.
set_by(Group | ((Player?, Player) -> boolean) | Config)?Who may change it with /set, which makes it a setting, or config for the server's configuration alone.
helpstring?What it is, in a line of up to 128 bytes, which /set and the editor show.
Fails
  • a value of the wrong type
  • an option it does not have

To keep the best round played since the server started

Section titled “To keep the best round played since the server started”
server_state("best_round", t.int { min = 0 }, 0, { read_by = everyone })

To let admins set the points that win a match

Section titled “To let admins set the points that win a match”
server_state("score_limit", t.int { min = 1, max = 999 }, 50, {
read_by = everyone,
set_by = admins,
help = "Team points that win the match",
})

To take a database’s address from the server’s configuration alone

Section titled “To take a database’s address from the server’s configuration alone”
server_state("stats_db", t.secret(t.string), "sqlite:stats.sqlite3", { set_by = config })
On the server player_state(name, type, default, { read_by?, help? }?)

Declares a value of each player's, the NPCs among them, which p.<name> reads and writes, from default as they join, until they leave or the match ends.

It is checked as match_state’s is, and is not named as a player’s own field or method (health, hurt). Its owner is the player, whom owner, team and spectators pick by. A person’s reaches the players its read_by picks wherever they are; an NPC’s rides with its body, so it reaches only those shown the NPC.

NameTypeWhat it is
namestringNeeded.
typeTypeNeeded.
defaultanyNeeded.
optionstable?A table of the options below. It, and each option, may be left out.
read_by(Group | ((Player?, Player) -> boolean))?The players sent it, whose client scripts read it; the server's alone without.
helpstring?What it is, in a line of up to 128 bytes, which the editor shows on hover.
Fails
  • a value of the wrong type
  • an option it does not have

To count each player’s kills, shown to everyone

Section titled “To count each player’s kills, shown to everyone”
player_state("kills", t.int { min = 0 }, 0, { read_by = everyone })
on("killed", function(e)
if e.killer then
e.killer.kills += 1
end
end)
On the server entity_kind(name, base)

Declares a kind of entity, on one of the engine's bases, which the calls making one take as their kind.

A "prop" is made by model, a "body" by spawn, a "sound" by play_sound and an "effect" by play_effect, each a looping one. Its entity_state is each such entity’s. A kind is named as state is, and is its module’s alone.

NameTypeWhat it is
namestringNeeded.
base"prop" | "body" | "sound" | "effect"Needed.
Fails
  • a value of the wrong type
entity_kind("crate", "prop")
entity_state("crate", "coins", t.int { min = 0 }, 10)
on("start", function()
local box = model("rocket", { kind = "crate" })
box.coins -= 1
end)
On the server entity_state(kind, name, type, default, { read_by?, help? }?)

Declares a value of each entity of a kind entity_kind declared, which e.<name> reads and writes, from default as it is made, until it is removed.

It is checked as match_state’s is, and is not named as an entity’s own field or method (position, move_to). Its owner is the entity’s, whom owner, team and spectators pick by. A client is sent it only while it is shown the entity.

NameTypeWhat it is
kindstringNeeded.
namestringNeeded.
typeTypeNeeded.
defaultanyNeeded.
optionstable?A table of the options below. It, and each option, may be left out.
read_by(Group | ((Player?, Player) -> boolean))?The players sent it, whose client scripts read it; the server's alone without.
helpstring?What it is, in a line of up to 128 bytes, which the editor shows on hover.
Fails
  • a value of the wrong type
  • an option it does not have

To show a turret’s shells to its owner alone

Section titled “To show a turret’s shells to its owner alone”
entity_kind("turret", "prop")
entity_state("turret", "shells", t.int { min = 0, max = 6 }, 6, { read_by = owner })
On the server event(name, schema, { read_by? }?)

Declares an event of the module's, its payload a t.record, which emit sends and on hears, on the server and on the clients its read_by picks.

It is named as state is, and not as one of the engine’s events. Its payload’s player, of t.player or t.optional(t.player), names the player it is about, else its entity an entity, whose owner owner, team and spectators pick by; one naming an entity reaches only those shown it. A read_by picking by an owner needs one of the two.

NameTypeWhat it is
namestringNeeded.
schemaTypeNeeded.
optionstable?A table of the options below. It, and each option, may be left out.
read_by(Group | ((Player?, Player) -> boolean))?The players sent it, whose client scripts hear it; the server's alone without.
Fails
  • a value of the wrong type
  • an option it does not have

To tell a player, and those watching them, that they bought something

Section titled “To tell a player, and those watching them, that they bought something”
event("bought", t.record { player = t.player, item = t.string }, { read_by = owner + spectators })
on("start", function()
emit("bought", { player = players[1], item = "armor" })
end)
On the server on("start", fn)

The match started: the first moment a module may give orders, before any other event. Heard once a match.

On the server on("ending", fn)

The match is to switch to the next, which waits until every handler ends: the time to save what a mod keeps.

On the server on("stopping", fn)

The server is to stop, which waits until every handler ends and every call is answered.