Calls
The server’s calls to the outside: HTTP requests, SQL databases (SQLite, Postgres, MySQL) and WebSockets, and json to read and write what they carry. Only the server makes them.
A call never holds up the game. It gives a task at once, and a handler that needs the answer awaits it, going on at the first tick after it comes. A failed call never raises an error: its answer’s ok is false and its error says what went wrong. A call sent always finishes, whatever becomes of the match.
Calls are made in handlers, timers, commands, on_tick and on_event, never at a module’s top as it loads; only a pool may be made there. Each call waits 30 seconds for its answer unless its timeout says otherwise.
Keep passwords and keys out of the mod: declare them as secret settings, which the server owner gives, and pass them as a URL or a header’s value. See settings.
await(...tasks) → ...AnswerPauses the handler until each task is answered, and gives their answers in order.
It goes on at the first tick after the last answer came, when what it held may be gone: check a player’s or an
entity’s exists. A task already answered, as its done says, gives its answer at once. A handler, a timer or a
command awaits, never a module’s top, on_tick or on_event. A paused handler is undone, resumed and dropped as one
paused in wait is.
Every answer has ok, and error once it is false: no answer came, the call took longer than its timeout, or
the answers on their way together passed the memory the mod has left. A failed call never raises an error; one no
handler awaits is said once as the mod’s error.
| Name | Type | What it is |
|---|---|---|
...tasks | Task | Any number of them. |
- Returns
...Answer- Fails
- a value of the wrong type
To fetch two things at once
Section titled “To fetch two things at once”local rank, kills = await( http.request("GET", "https://stats.example/rank/" .. p.id), http.request("GET", "https://stats.example/kills/" .. p.id))if rank.ok and kills.ok and p.exists then print(`{p.name}: rank {json.decode(rank.body).rank}, {json.decode(kills.body).kills} kills`)endhttp.request
Section titled “http.request”http.request(method, url, options?) → TaskSends an HTTP or HTTPS request, and gives its task at once, which await answers.
Any status, 500 too, is an answer: ok, with status, headers and body. A header’s name is in lower case,
and a name sent several times has its values joined by , . No answer, as a refused connection or a timeout
passed, fails the call, its error saying why. Redirects are followed, never telling the next host where they came
from; a request carrying a secret, in its URL or a header, follows them only to the same scheme, host and port,
answering one elsewhere as it is. A call made at a module’s top, or in a group, fails.
| Name | Type | What it is |
|---|---|---|
method | string | Needed. |
url | string | Secret | Needed. |
options | table? | A table of the options below. It, and each option, may be left out. |
headers | ({[string]: string | Secret})? | Headers to send, by name; a value may be a secret. |
body | (string | buffer)? | The body, a string's or a buffer's bytes as they are. Not with json. |
json | any? | A value sent as the body, as json.encode writes it, with its content type unless headers names one. Not with body. |
timeout | numberdefault 30 | How long it may take, in seconds, before it fails. |
- Returns
Task- Fails
- a value of the wrong type
- an option it does not have
bodyandjsonboth
To fetch a player’s rank
Section titled “To fetch a player’s rank”local answer = await(http.request("GET", "https://stats.example/rank/" .. p.id))if answer.ok and answer.status == 200 then local rank = json.decode(answer.body).rank print(`{p.name} is ranked {rank}`)endTo post a kill to a stats site
Section titled “To post a kill to a stats site”local answer = await(http.request("POST", "https://stats.example/kills", { headers = { ["x-server"] = "eu-1" }, json = { player = p.name, weapon = "weapons/ak47" }, timeout = 5,}))if not answer.ok then print("the stats site is down: " .. answer.error)endjson.encode
Section titled “json.encode”json.encode(value) → stringWrites a value as JSON text.
A table of keys 1 to n is a list, an empty table too, and any other table an object of string keys, in sorted order; nil is null. A whole number is written without a decimal point. A table holding itself fails, as does a function, a key neither a string nor 1 to n, or text larger than the memory the mod has left.
| Name | Type | What it is |
|---|---|---|
value | any | Needed. |
- Returns
string- Fails
- a value of the wrong type
To save a player’s stats as text
Section titled “To save a player’s stats as text”local text = json.encode({ name = "ann", kills = 4, weapons = { "weapons/ak47", "weapons/usp" } })json.decode
Section titled “json.decode”json.decode(text) → (any, string?)Reads JSON text: gives its value, or nil and why it is not JSON.
An array is a table of keys 1 to n and an object a table of string keys; null is nil, leaving a hole in a list.
| Name | Type | What it is |
|---|---|---|
text | string | Needed. |
- Returns
(any, string?)- Fails
- a value of the wrong type
To read what a stats site answered
Section titled “To read what a stats site answered”local stats, why = json.decode('{"rank": 3, "best": null}')if stats then print(`rank {stats.rank}`)else print("not JSON: " .. why)endsql.pool
Section titled “sql.pool”sql.pool(url, options?) → PoolMakes a pool of connections to a SQLite, Postgres or MySQL database, which connects on its first call.
The URL starts sqlite:, postgres:// or mysql://, and may be a secret. A SQLite file is in the server’s
directory, made if missing, and its SQL opens no other file. The pool is made at once, so a module’s top may make
one: keep it in a global, as a reload in place runs a module’s top again. It closes once the mod no longer holds
it and its calls have finished.
| Name | Type | What it is |
|---|---|---|
url | string | Secret | Needed. |
options | table? | A table of the options below. It, and each option, may be left out. |
max_connections | number?1 to 4294967295 | The most connections it opens at once, 10 without. |
min_connections | numberdefault 0 | The connections it keeps open, even idle; kept within max_connections, refused above it when both are given. |
acquire_timeout | numberdefault 30 | How long a call waits for a free connection, in seconds, before it fails. |
idle_timeout | numberdefault 600 | How long a connection with no call on it stays open, in seconds. |
max_lifetime | numberdefault 1800 | How long a connection stays open at most, in seconds, before a new one takes its place. |
- Returns
Pool- Fails
- a value of the wrong type
- an option it does not have
- a value outside its range
To keep a SQLite file of stats
Section titled “To keep a SQLite file of stats”db = db or sql.pool("sqlite:stats.db")To share a Postgres database between servers
Section titled “To share a Postgres database between servers”db = db or sql.pool("postgres://game@db.example/stats", { max_connections = 4 })ws.connect
Section titled “ws.connect”ws.connect(url, options?) → TaskConnects a WebSocket, and gives its task at once, which answers socket once it is open.
The URL starts ws:// or wss://, and it and a header’s value may be secrets. message and closed run as
handlers of the module that connected, in the order the socket heard, never a player’s; what they hear has secrets
hidden. The messages on their way, either way, count against the memory the mod has left, the socket closing past
it. A socket closes once the mod no longer holds it, unless one of its own callbacks holds it, and with its match:
reconnecting is the mod’s. socket.open is false once either side closes it. A call made at a module’s top, or in
a group, fails.
| Name | Type | What it is |
|---|---|---|
url | string | Secret | Needed. |
options | table? | A table of the options below. It, and each option, may be left out. |
headers | ({[string]: string | Secret})? | Headers to send with the handshake, by name; a value may be a secret. |
protocols | {string}? | The subprotocols offered, of which the host picks one, socket.protocol. |
timeout | numberdefault 30 | How long it may take to connect, in seconds, before it fails. |
message | ((data: string | buffer) -> ())? | Runs for each message the socket hears: a string for text, a buffer for binary. |
closed | ((code: number, reason: string) -> ())? | Runs once as the socket closes, whichever side closed it, with the close's code and reason. |
- Returns
Task- Fails
- a value of the wrong type
- an option it does not have
To follow a chat relay
Section titled “To follow a chat relay”local answer = await(ws.connect("wss://chat.example/relay", { message = function(data) say(`[relay] {data}`) end, closed = function(code, reason) print(`the relay closed: {code} {reason}`) end,}))if answer.ok then relay = answer.socketendPool:query
Section titled “Pool:query”Pool:query(sql, ...params) → TaskRuns a query on one of the pool's connections, and gives its task at once, which answers rows.
The SQL is one statement in the database’s own SQL, each ? ($1 and on in Postgres) a parameter after it, the
options last. Each row is a table of its values by column name, a database’s null as nil and its bytes as a
buffer. A call made at a module’s top, or in a group, fails.
| Name | Type | What it is |
|---|---|---|
sql | string | Needed. |
...params | SqlParam | Any number of them. |
- Returns
Task- Fails
- a value of the wrong type
To list the best players
Section titled “To list the best players”db = db or sql.pool("sqlite:stats.db")local answer = await(db:query("SELECT name, kills FROM players WHERE kills > ? ORDER BY kills DESC", 5))if answer.ok then for i, row in answer.rows do print(`{i}. {row.name}, {row.kills} kills`) endendPool:execute
Section titled “Pool:execute”Pool:execute(sql, ...params) → TaskRuns a statement that changes the database on one of the pool's connections, and gives its task at once, which answers the rows it changed.
Its SQL and parameters are as Pool:query’s. A call made at a module’s top, or in a group, fails.
| Name | Type | What it is |
|---|---|---|
sql | string | Needed. |
...params | SqlParam | Any number of them. |
- Returns
Task- Fails
- a value of the wrong type
To count a player’s kill
Section titled “To count a player’s kill”db = db or sql.pool("sqlite:stats.db")local answer = await(db:execute("UPDATE players SET kills = kills + 1 WHERE name = ?", p.name, { timeout = 5 }))if not answer.ok then print("not saved: " .. answer.error)endPool:transaction
Section titled “Pool:transaction”Pool:transaction(fn, { timeout? }?) → TaskRuns a function's calls on one connection as a transaction, and gives its task at once, which answers once it is committed or rolled back.
The function runs at once, as a handler of its own given the transaction, tx, so it may await. Its tx:query
and tx:execute go to the database in order. It commits as the function returns; it is rolled back if the
function returns false or fails, a call on tx fails, or timeout passes. A call made at a module’s top, or in a
group, fails.
| Name | Type | What it is |
|---|---|---|
fn | (tx: Tx) -> ...any | Needed. |
options | table? | A table of the options below. It, and each option, may be left out. |
timeout | numberdefault 30 | How long it may take, in seconds, from its start to its commit, before it is rolled back. |
- Returns
Task- Fails
- a value of the wrong type
- an option it does not have
To move points from one wallet to another, or not at all
Section titled “To move points from one wallet to another, or not at all”db = db or sql.pool("sqlite:stats.db")local moved = await(db:transaction(function(tx) local from = await(tx:query("SELECT points FROM wallets WHERE name = ?", "ann")).rows[1] if not from or from.points < 100 then return false end tx:execute("UPDATE wallets SET points = points - 100 WHERE name = ?", "ann") tx:execute("UPDATE wallets SET points = points + 100 WHERE name = ?", "bob")end, { timeout = 10 }))print(moved.ok and "moved" or "not moved: " .. moved.error)Socket:send
Section titled “Socket:send”Socket:send(data, { timeout? }?) → TaskSends a message, after those sent before it, and gives its task at once, which answers once it is sent.
A string is sent as a text message and a buffer as a binary one. A message to a socket no longer open fails.
| Name | Type | What it is |
|---|---|---|
data | string | buffer | Needed. |
options | table? | A table of the options below. It, and each option, may be left out. |
timeout | numberdefault 30 | How long it may take, in seconds, before it fails. |
- Returns
Task- Fails
- a value of the wrong type
- an option it does not have
To tell a relay a player joined
Section titled “To tell a relay a player joined”local answer = await(ws.connect("wss://chat.example/relay"))if answer.ok then answer.socket:send(json.encode({ joined = p.name }), { timeout = 5 })endSocket:close
Section titled “Socket:close”Socket:close(code?, reason?) → TaskCloses the socket, after the messages sent before, and gives its task at once, which answers once the close is sent.
The code is 1000 without one, or 3000 to 4999 for the mod’s own, and the reason up to 123 bytes. The socket is no
longer open at once, and its closed runs once the host closes too.
| Name | Type | What it is |
|---|---|---|
code | number? | May be left out. |
reason | string? | May be left out. |
- Returns
Task- Fails
- a value of the wrong type
To close a relay as the match ends
Section titled “To close a relay as the match ends”local answer = await(ws.connect("wss://chat.example/relay"))if answer.ok then answer.socket:close(4000, "the match ended")endTx:query
Section titled “Tx:query”Tx:query(sql, ...params) → TaskRuns a query in the transaction, after the calls on it before, and gives its task at once, which answers rows.
Its SQL, parameters and rows are as Pool:query’s. One that fails rolls the transaction back, and those after it
fail. It fails once the transaction’s function has ended.
| Name | Type | What it is |
|---|---|---|
sql | string | Needed. |
...params | SqlParam | Any number of them. |
- Returns
Task- Fails
- a value of the wrong type
To read a wallet within a transaction
Section titled “To read a wallet within a transaction”db = db or sql.pool("sqlite:stats.db")await(db:transaction(function(tx) local wallet = await(tx:query("SELECT points FROM wallets WHERE name = ?", "ann")).rows[1] print(`ann has {wallet and wallet.points or 0} points`)end))Tx:execute
Section titled “Tx:execute”Tx:execute(sql, ...params) → TaskRuns a statement that changes the database in the transaction, after the calls on it before, and gives its task at once, which answers the rows it changed.
Its SQL and parameters are as Pool:query’s. One that fails rolls the transaction back, and those after it fail.
It fails once the transaction’s function has ended.
| Name | Type | What it is |
|---|---|---|
sql | string | Needed. |
...params | SqlParam | Any number of them. |
- Returns
Task- Fails
- a value of the wrong type
To save a round’s scores together
Section titled “To save a round’s scores together”db = db or sql.pool("sqlite:stats.db")await(db:transaction(function(tx) for _, p in players do tx:execute("UPDATE players SET kills = kills + ? WHERE name = ?", 1, p.name) endend))