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.
match: tableThe match's state: the engine's, match.map, match.gametype and match.number, and each match_state.
server
Section titled “server”server: tableThe server's state: its settings, as server.hostname, and each server_state.
everyone
Section titled “everyone”everyone: GroupEvery player.
owner: GroupThe owner of what it is given to: the player it is about, alone.
team: GroupThe players on the owner's team, the owner too; no one when they are on none.
spectators
Section titled “spectators”spectators: GroupThe players watching the owner through their eyes while dead.
admins
Section titled “admins”admins: GroupThe players the admins setting lists.
on(name, fn, { after? }?)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.
| Name | Type | What it is |
|---|---|---|
name | string | Needed. |
fn | (e: {[string]: any}) -> any | Needed. |
options | table? | A table of the options below. It, and each option, may be left out. |
after | booleandefault false | Runs 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.
| Name | Type | What it is |
|---|---|---|
name | string | Needed. |
fn | (e: {[string]: any}) -> () | Needed. |
- Fails
- a value of the wrong type
To greet each player who joins
Section titled “To greet each player who joins”on("joined", function(e) say(`Welcome, {e.player.name}`)end)To halve fall damage
Section titled “To halve fall damage”on("fell", function(e) e.amount /= 2end)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 falseend, { after = true })To count the frames drawn
Section titled “To count the frames drawn”local frames = 0on("frame", function(e) frames += 1end)To count this player’s deaths
Section titled “To count this player’s deaths”local deaths = 0on("killed", function(e) if e.victim == me then deaths += 1 endend)request
Section titled “request”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.
| Name | Type | What it is |
|---|---|---|
name | string | Needed. |
payload | ({[string]: any})? | May be left out. |
- Fails
- a value of the wrong type
To buy armor at a click
Section titled “To buy armor at a click”mount(function() return Box { "Buy armor", onClick = function() request("buy_armor") end, }end)require
Section titled “require”require(path) → anyrequire(path) → anyRuns 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.
| Name | Type | What it is |
|---|---|---|
path | string | Needed. |
- 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.
| Name | Type | What it is |
|---|---|---|
path | string | Needed. |
- Returns
any- Fails
- a value of the wrong type
group(picks) → GroupMakes 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.
| Name | Type | What it is |
|---|---|---|
picks | (owner: Player?, viewer: Player) -> boolean | Needed. |
- 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 > 0end)local flag = model("rocket", { read_by = living + admins })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.
| Name | Type | What it is |
|---|---|---|
name | string | Needed. |
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_request
Section titled “on_request”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.
| Name | Type | What it is |
|---|---|---|
name | string | Needed. |
schema | Type | Needed. |
fn | (e: {player: Player, [string]: any}) -> () | Needed. |
- Fails
- a value of the wrong type
To sell armor to a player who can pay
Section titled “To sell armor to a player who can pay”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) endend)match_state
Section titled “match_state”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.
| Name | Type | What it is |
|---|---|---|
name | string | Needed. |
type | Type | Needed. |
default | any | Needed. |
options | table? | 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. |
help | string? | 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 += 1end)server_state
Section titled “server_state”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.
| Name | Type | What it is |
|---|---|---|
name | string | Needed. |
type | Type | Needed. |
default | any | Needed. |
options | table? | 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. |
help | string? | 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 })player_state
Section titled “player_state”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.
| Name | Type | What it is |
|---|---|---|
name | string | Needed. |
type | Type | Needed. |
default | any | Needed. |
options | table? | 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. |
help | string? | 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 endend)entity_kind
Section titled “entity_kind”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.
| Name | Type | What it is |
|---|---|---|
name | string | Needed. |
base | "prop" | "body" | "sound" | "effect" | Needed. |
- Fails
- a value of the wrong type
To give crates coins a player takes
Section titled “To give crates coins a player takes”entity_kind("crate", "prop")entity_state("crate", "coins", t.int { min = 0 }, 10)
on("start", function() local box = model("rocket", { kind = "crate" }) box.coins -= 1end)entity_state
Section titled “entity_state”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.
| Name | Type | What it is |
|---|---|---|
kind | string | Needed. |
name | string | Needed. |
type | Type | Needed. |
default | any | Needed. |
options | table? | 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. |
help | string? | 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 })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.
| Name | Type | What it is |
|---|---|---|
name | string | Needed. |
schema | Type | Needed. |
options | table? | 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("start")
Section titled “on("start")”on("start", fn)The match started: the first moment a module may give orders, before any other event. Heard once a match.
on("ending")
Section titled “on("ending")”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("stopping")
Section titled “on("stopping")”on("stopping", fn)The server is to stop, which waits until every handler ends and every call is answered.