Networking Functions¶
These functions let your game host or join an online multiplayer session and share data between
players in real time. They all live in the global net table.
Sessions are created through the platform’s own dialogs: net.host() and net.join()
open them for the player. The platform handles the session title, visibility (public or
invite-code), invite codes, and session browsing – your game only decides the player capacity,
an optional default title, and when to host, join, or leave. Under the hood, players connect
peer-to-peer with an automatic relay fallback; your code never has to care which one is in use.
Note
Except for net.host(), net.join(), and net.leave(), every net function
requires an active session and raises the error net: no active session otherwise. See
Multiplayer for the concepts behind the API.
net.host¶
- net.host(config, callback)¶
Ask the player to create (host) a new session by opening the platform’s host dialog.
- Parameters:
config (table) – Optional session settings (see below).
callback (function) – Optional function called once, with no arguments, when the session has been created.
Both arguments are optional, and
net.host(callback)is also accepted. Theconfigtable supports two keys; any other key is ignored:Key
Type
Meaning
max_playersnumber
Session capacity, including the host. Defaults to
2. The player cannot change it – capacity belongs to the game.titlestring
Default session title offered in the dialog. The player can edit it.
The callback fires only when the session is actually created. If the player cancels the dialog, the callback is never called and no session starts – design your game so it simply stays in its menu state in that case.
Raises
net: already in a session; call net.leave() firstif a session is active. A second call made while a host/join dialog is still open is silently ignored (a no-op), not an error.Warning
While the dialog is open a repeat call is ignored, but once the session is created an unconditional
net.hostfrom_update()raisesnet: already in a sessionon the very next frame. Guard it so it runs once, for example by switching to a"waiting"state before the call.net.host({ max_players = 4, title = "Tag arena" }, function() state = "playing" print("session created, id " .. net.id()) end)
net.join¶
- net.join(callback)¶
Ask the player to join an existing session by opening the platform’s join dialog, where they can enter an invite code or browse public sessions.
- Parameters:
callback (function) – Optional function called once, with no arguments, after the session has been joined.
As with
net.host(), the callback fires only on success: if the player cancels the dialog, nothing happens. Likenet.host(), it raisesnet: already in a session; call net.leave() firstwhen a session is already active, and silently ignores a repeat call made while a dialog is still open.net.join(function() state = "playing" print("joined as player " .. net.id()) end)
net.leave¶
- net.leave()¶
Leave the current session and tear it down. Safe to call when there is no session (it does nothing). After leaving, the game may host or join again.
if game_over then net.leave() state = "menu" end
net.id¶
net.state¶
- net.state¶
A table shared by every player in the session. Writes replicate to all peers automatically; reads always return the latest replicated value. A player who joins mid-game receives the whole current contents.
net.statebehaves like a nested Lua table:net.state.score = 0 -- write a value net.state.ball = { x = 160, y = 90, dx = 2 } -- assign a whole table at once net.state.ball.x = 42 -- update a nested value local s = net.state.score -- read (nil if absent) net.state.bonus = nil -- delete a key (or a whole subtree)
Rules and behavior:
Stored values are numbers, strings, and booleans. Nested tables are supported; storing a function raises
net: cannot store a function in net.state.Assigning a table replaces that subtree: existing keys under the path are deleted first, then the table’s contents are written.
Assigning
nildeletes the key, including everything nested below it.Reading an absent key returns
nil; reading a branch (likenet.state.ball) returns a table-like view you can index further.A branch exists only while at least one value lives under it. Assigning an empty table stores nothing, so the branch still reads back as
nil– create nested data by assigning a non-empty table, then update its fields.A slot may also hold a lock or queue object (see
net.lock()andnet.queue()). Its internal state is stored and replicated alongsidenet.statebut is hidden from plain reads: the slot reads back as the object’s handle, not as data, and it does not appear as ordinary keys.#net.state.listcounts consecutive integer keys starting at1, like a regular Lua sequence.pairs(net.state.players)iterates the branch’s direct children. Keys come back as strings (numeric ids included – usetonumberif you need the number back).
Because of the empty-branch rule, adding an entry to a shared collection uses this idiom (the first writer creates the branch, later writers go through it):
local entry = { x = 24, y = 40 } if net.state.players then net.state.players[playerId] = entry else net.state.players = { [playerId] = entry } end
Warning
There is no built-in write protection: any peer can write any key. Keep your game consistent by convention – each player writes only its own keys, and the host owns everything global. See Multiplayer.
net.emit¶
- net.emit(name, payload)¶
Send a one-shot custom event to the other players in the session.
- Parameters:
name (string) – Event name; receivers subscribe with
net.on("event:" .. name, ...).payload – Optional value to send along – a number, string, boolean, or (nested) table. Functions cannot be sent. Omitted, the receivers get
nil.
The sender does not receive its own event: if the sender needs the same effect, apply it locally after emitting.
net.emit("serve", { direction = -1 })
net.on¶
- net.on(pattern, callback)¶
Register a callback for a network event. What arrives depends on the pattern:
Pattern
Callback arguments
Fires when
"peer.joined"(playerId)Another player joins the session
"peer.left"(playerId)Another player leaves or disconnects
"ended"(none)
The session ends – in particular when the host leaves
"event:<name>"(from, payload)A peer calls
net.emit("<name>", payload);fromis its player id"error"(path, reason)The host rejects (and rolls back) a client write to a permission-protected path
anything else (a state path)
(path, newValue)A matching
net.statekey changesState patterns match dotted key paths and support two wildcards:
*matches exactly one path segment,**matches any number of segments. Change callbacks fire only when the value actually changes (rewriting an identical value is silent), and they fire for your own writes too. The names above are reserved:net.on("error", ...)always registers the rejection listener, so it never observes anet.statekey literally namederror. Listening for"error"is the intended way to detect a write the host rolled back; the Permissions – locking down net.state tutorial shows that rejected-write behavior in action, there observed via the protected value flipping back.net.on("score", function(path, value) print("score is now " .. value) end) net.on("players.*.x", function(path, value) -- fires for players.7.x but not for players.7.inventory.gold end) net.on("players.**", function(path, value) -- fires for any change anywhere under players end) net.on("event:serve", function(from, payload) serve_ball(payload.direction) end) net.on("peer.left", function(playerId) net.state.players[playerId] = nil end)
Note
An error raised inside a
net.oncallback (or a lock/queue callback) is printed to the output panel but does not stop the game.
net.lock¶
- net.lock()¶
Create a mutual-exclusion lock. Locks let players compete for something safely – only one peer at a time can hold a given lock, no matter how simultaneously they ask.
- Returns:
A lock handle – a table with an
acquireand anis_lockedfunction.
Put the lock in
net.stateto share it. A lock becomes shared and host-serialized only once it is assigned intonet.state; its net.state path is its identity, and you use it straight from there. Store it right next to what it protects:net.state.score_lock = net.lock() -- create the shared lock (host does this once) net.state.score_lock.acquire(function(release) -- exclusive section: no other peer holds this lock right now, so this -- read-modify-write cannot lose an increment to a simultaneous one net.state.score = (net.state.score or 0) + 1 release() -- always release when done end) if net.state.score_lock.is_locked() then ... end
acquire(fn)requests the lock. When granted,fnis called with a single argument: areleasefunction that frees the lock. If the lock is busy, the request waits in a first-come, first-served queue andfnruns later, when the current holder releases.is_locked()returnstruewhile any peer holds the lock.
Requests are ordered by the session host, so two peers acquiring “at the same time” are serialized – one runs, then the other. If a peer disconnects while holding locks, its locks are released automatically and its pending requests are dropped. A lock stored under a host-only path (see Permissions – locking down net.state) can only be acquired by the host, just like a write to that path.
Note
A lock you never assign into
net.stateis a local lock: usable without a session, but private to this player and uncontended (it grants immediately). It exists so the same code runs in single-player and multiplayer.Warning
Nothing prevents a peer from writing a lock-protected
net.statekey without acquiring the lock first. A lock only protects against peers that also use it.
net.queue¶
- net.queue()¶
Create a shared FIFO queue. All players see the same queue and pops are ordered by the session host, so an item is delivered to exactly one popper.
- Returns:
A queue handle – a table with
push,pop,lengthandpeek.
Like a lock, a queue becomes shared only once it is assigned into
net.state; its net.state path is its identity, and you use it from there:net.state.respawns = net.queue() -- create the shared queue (host does this once) net.state.respawns.push({ coin = 3 }) -- append a value net.state.respawns.pop(function(value) -- remove the head; nil when empty if value then respawn_coin(value.coin) end end) local n = net.state.respawns.length() -- current number of items local head = net.state.respawns.peek() -- read the head without removing it (nil if empty)
push(value)appends any serializable value (numbers, strings, booleans, nested tables). Pushing a function raisesnet: cannot queue a function.pop(callback)removes the head and delivers it tocallback(value). On an empty queue the callback receivesnil. The callback form exists because the head may live on another peer; treat the value as arriving “soon” rather than instantly.length()andpeek()read the local replica synchronously.
Note
A queue you never assign into
net.stateis a local queue: usable without a session, but private to this player.