multiplayer
The multiplayer namespace — 106 functions.
globals/multiplayer/beginOperation
multiplayer.beginOperation(description: string)
Begin recording an undoable operation. All mutations until
commitOperation() are grouped into one undo entry.
Parameters
descriptionstring— Human-readable label.
multiplayer.beginOperation("move cube")
globals/multiplayer/canRedo
multiplayer.canRedo() -> boolean
Check if this client has any redoable operations.
Returns boolean — True if redo is available.
globals/multiplayer/canUndo
multiplayer.canUndo() -> boolean
Check if this client has any undoable operations.
Returns boolean — True if undo is available.
globals/multiplayer/cancelOperation
multiplayer.cancelOperation()
Cancel the current operation and restore all properties to their values at begin time.
multiplayer.cancelOperation()
globals/multiplayer/claimOwnership
multiplayer.claimOwnership(entityId: (string | entityRef)?) -> boolean
Request ownership of an entity. Returns true if the claim was tentatively granted (relay confirmation pending).
Parameters
entityId(string | entityRef)(optional) — Entity id or proxy to claim.
Returns boolean — True if the claim was tentatively accepted.
globals/multiplayer/clearHistory
multiplayer.clearHistory()
Drop this client's whole undo/redo history — for boundaries where old edits stop being meaningful (a scene load, a test rig reset).
multiplayer.clearHistory()
globals/multiplayer/commitOperation
multiplayer.commitOperation()
Finalize the current operation and push it onto the undo stack. Only changes that actually differ from the start state are recorded.
multiplayer.commitOperation()
globals/multiplayer/connect
multiplayer.connect(relayUrl: string)
Connect to a multiplayer relay server for the current world.
Uses the loaded world's world_id as the room prefix for scene
isolation. A world must be loaded before connecting.
Parameters
relayUrlstring— Relay server URL.
multiplayer.connect("https://relay.example.com")
globals/multiplayer/disconnect
multiplayer.disconnect()
Disconnect from the multiplayer relay server.
multiplayer.disconnect()
globals/multiplayer/explain
multiplayer.explain(entityId: (string | entityRef), componentType: string, property: string) -> DeliveryVerdict
Why a synced property is not reaching the peers this client shares its entity's room with. Answers from the engine's own registry, so a name the component never registered is reported as such instead of inferred from a second client's silence.
Parameters
entityId(string | entityRef)— Entity id or proxy.componentTypestring— Component type name, e.g. "Health".propertystring— Property name as written in the component'ssync {}block.
Returns DeliveryVerdict — arriving true when the property is on its way; otherwise reason names the cause and property carries the registry's record of it when one exists.
local v = multiplayer.explain(player, "Health", "hp")
if not v.arriving then print(v.reason) end
globals/multiplayer/getDiagnostics
multiplayer.getDiagnostics() -> SyncDiagnostics
Get sync diagnostics — traffic counts, bandwidth, link quality,
peer count. The counts — bytesSent/Received,
datagramsSent/Received, rpcsSent/Received, ownershipChanges —
are running totals for the session, so a sparse event stays readable
long after it happened; subtract two samples for the rate over the
interval between them. bytesSentPerSec / bytesReceivedPerSec are
averages over the last completed ~1 second window.
rttMs is the smoothed round-trip time to the relay and
packetLoss the fraction (0..1) of packets lost over the last 5
seconds; both read 0 until the transport has sampled a live
connection. messagesAwaitingEntity counts the sync messages this
peer is holding for an entity it has not received yet — each waits
for the spawn that names it, applies the moment it arrives, and is
released once its wait runs out.
Returns SyncDiagnostics — Diagnostics table.
local d = multiplayer.getDiagnostics()
if d.packetLoss > 0.05 then warnThePlayer(d.rttMs) end
print(d.messagesAwaitingEntity .. " message(s) waiting for their entity")
globals/multiplayer/getPeerId
multiplayer.getPeerId() -> number?
Get this client's peer ID in the current session.
Returns number? — Local peer ID, or nil if not connected.
globals/multiplayer/getPeers
multiplayer.getPeers() -> { PeerInfo }
Get a list of all connected peers in the current session.
Returns { PeerInfo } — Array of peer info tables.
for _, p in ipairs(multiplayer.getPeers()) do print(p.name) end
globals/multiplayer/getRoomPeers
multiplayer.getRoomPeers(roomKey: string) -> { PeerInfo }
Get the peers this client shares the given room with, ordered by
peer id. getPeers answers for the whole session — the union of every
room this client is in — while this answers for one room, so a peer
that leaves this room while staying in another disappears from here
and remains in getPeers.
Parameters
roomKeystring— Fully-qualified room key ({worldGuid}/{profile}/{mode}/{sceneGuid}).
Returns { PeerInfo } — Array of peer info tables for that room.
for _, p in ipairs(multiplayer.getRoomPeers(key)) do print(p.id) end
globals/multiplayer/getRooms
multiplayer.getRooms() -> { string }
The relay rooms this client has joined, sorted. A broadcast reaches only the peers that share one of these.
Returns { string } — Room keys.
for _, key in ipairs(multiplayer.getRooms()) do print(key) end
globals/multiplayer/getTickRate
multiplayer.getTickRate() -> number
Get the current sync tick rate (network updates per second).
Returns number — Sync ticks per second (default 20).
globals/multiplayer/heldMessages
multiplayer.heldMessages() -> { HeldMessage }
The sync messages this peer is holding for entities it has not
received — what getDiagnostics().messagesAwaitingEntity counts, one
entry each, with the entity sync id it names, its age and the grace it
is held against.
Returns { HeldMessage } — Held messages.
for _, m in ipairs(multiplayer.heldMessages()) do print(m.kind, m.ageMs) end
globals/multiplayer/isConnected
multiplayer.isConnected() -> boolean
Check if a multiplayer session is active and connected to a relay.
Returns boolean — True if connected.
globals/multiplayer/isHost
multiplayer.isHost() -> boolean
Whether THIS client is the host (authoritative owner) of the
current scene's play room — the relay room CREATOR, or offline /
single-player. Host code spawns the shared synced world (via
entity.spawnSynced or a scene's onHostLoad) and runs authoritative
simulation; a non-host (JOINER) receives that content from the relay
snapshot and must NOT re-create it. Gate ANY code that spawns synced
entities or owns shared state with this so it runs on exactly one
client — running it on every peer is the double-spawn 'explosion'.
Returns boolean — True on the host / offline / single-player; false on a confirmed joiner. Defaults to true when the role isn't known yet (degrade to host so single-player and pre-join code still run) — pair with a scene's onHostLoad hook when exact one-shot timing matters.
if multiplayer.isHost() then enemy = entity.spawnSynced("enemy") end
globals/multiplayer/isOwner
multiplayer.isOwner(entityId: (string | entityRef)?) -> boolean
Check if the local client owns the given entity (or the current entity if called from a component). Only the owner can modify synced properties directly.
Parameters
entityId(string | entityRef)(optional) — Entity id or proxy to check (defaults toself.entityIdin component context).
Returns boolean — True if the local client is the owner.
globals/multiplayer/isRoomCreator
multiplayer.isRoomCreator(roomKey: string) -> boolean?
Whether this client created the given room — it was the FIRST peer to join it (race-free; the relay assigns it on join). In play mode the creator instantiates the scene's entities (synced) and every other joiner receives them from the relay snapshot, so the scene is never double-instantiated.
Parameters
roomKeystring— Fully-qualified room key ({worldGuid}/{profile}/{mode}/{sceneGuid}).
Returns boolean? — True if this client created the room, false if it joined an existing one, nil if the relay hasn't reported a role yet (offline / not joined).
if multiplayer.isRoomCreator(key) ~= false then layers.load(scene) end
globals/multiplayer/joinRoom
multiplayer.joinRoom(roomKey: string)
Join a relay room. Room keys are built as
{worldGuid}/{profile}/{mode}/{sceneGuid} — four segments, the
{profile} one keeping a runtime peer (published content) and an
editor peer (live content) in separate rooms even when both are in
play mode. Rooms partition the relay's fan-out: only peers in the
same room receive each other's broadcasts. getRooms() reports the
keys this client is already in and roomFor(entity) the one an
entity broadcasts into, so a key can be read rather than rebuilt.
No-op when not connected or already joined.
Parameters
roomKeystring— Fully-qualified room key.
multiplayer.joinRoom(worldGuid .. "/" .. engine.profile .. "/play/" .. sceneGuid)
globals/multiplayer/leaveRoom
multiplayer.leaveRoom(roomKey: string)
Leave a relay room. The key is reported under
observe().withdrawnRooms until joinRoom names it again. No-op
when not connected or not joined.
Parameters
roomKeystring— Fully-qualified room key.
globals/multiplayer/loopback
multiplayer.loopback() -> { [string]: any }
Loopback testing harness. Returns a table with enable(),
disable(), flush(), receive() methods for testing sync
without a relay server.
Returns { [string]: any } — Loopback API table.
globals/multiplayer/observe
multiplayer.observe() -> ReplicationObservation
Report what this peer is replicating and why a property is not
arriving. Carries the rooms this client joined, one record per entity
with a sync id — its owner, the room it broadcasts into, how many
other peers share that room, and every REGISTERED synced component
with its declared property names, wire indices, public/private table
and dirty bits — the messages held for entities that have not arrived,
and the registry's totals. Every property carries notArriving: one
name from reasons, or nil when it is on its way. Answers in edit
mode as well as play mode, for what the relay carries in each: in
edit mode scene content is left out of the sync-id pass, so its
changes travel to the other clients with the source they are
written into and it reads entityNotSynced here.
Returns ReplicationObservation — The engine's current replication observation.
local o = multiplayer.observe()
print(#o.entities .. " entities, " .. o.totals.properties .. " synced properties")
globals/multiplayer/observeComponent
multiplayer.observeComponent(entityId: (string | entityRef), componentType: string) -> SyncedComponent?
The registered synced component of the named type on an entity's
record. Matches a fully-qualified type (@builtin::components.Model)
and the leaf name it ends in (Model) alike.
Parameters
entityId(string | entityRef)— Entity id or proxy.componentTypestring— Component type name or its leaf.
Returns SyncedComponent? — The registered component instance, or nil when none of that type is registered on the entity.
local c = multiplayer.observeComponent(player, "Health")
print(#c.properties .. " declared properties")
globals/multiplayer/observeEntity
multiplayer.observeEntity(entityId: (string | entityRef)) -> EntityReplication?
The replication record for one entity — its sync id, owner, room, and the synced components registered on it.
Parameters
entityId(string | entityRef)— Entity id or proxy.
Returns EntityReplication? — The entity's record, or nil when the engine holds no sync record for it.
local r = multiplayer.observeEntity(e)
for _, c in ipairs(r.components) do print(c.componentType) end
globals/multiplayer/on
multiplayer.on(channel: string, callback: (number, ...any) -> ())
Subscribe to a custom message channel. The callback runs as
callback(fromPeerId, ...args) whenever another peer calls
multiplayer.send(channel, ...). Multiple callbacks per channel fire
in registration order.
Parameters
channelstring— Channel name to listen on.callback(number, ...any) -> ()—function(fromPeerId: number, ...)— the sender's peer id then the sent args.
globals/multiplayer/recordSpawn
multiplayer.recordSpawn(entityId: string)
Adopt an existing entity into the open operation as its spawn — for flows that create an entity before the operation opens (a drag preview adopted on drop). Undoing the operation despawns it.
Parameters
entityIdstring— Entity id to record as spawned by this operation.
multiplayer.recordSpawn(id)
globals/multiplayer/redo
multiplayer.redo() -> boolean
Redo this client's last undone operation.
Returns boolean — True if an operation was redone.
globals/multiplayer/releaseOwnership
multiplayer.releaseOwnership(entityId: (string | entityRef)?) -> boolean
Release ownership of an entity.
Parameters
entityId(string | entityRef)(optional) — Entity id or proxy to release.
Returns boolean — True if ownership was released.
globals/multiplayer/roomFor
multiplayer.roomFor(entityId: (string | entityRef)) -> string?
The room key an entity's spawns and property deltas broadcast into.
Parameters
entityId(string | entityRef)— Entity id or proxy.
Returns string? — The room key, or nil when the engine has established no scene context for the entity.
local key = multiplayer.roomFor(player)
if key then multiplayer.joinRoom(key) end
globals/multiplayer/send
multiplayer.send(channel: string, ...: any?)
Broadcast a message on a named channel to every OTHER peer in the
room. The relay forwards it transparently; peers receive it via
multiplayer.on. Arguments may be any synced value (nil, boolean,
number, string, Vec3, entity/component proxy, or table) and are
delivered to listeners in order. No-op when not connected.
Parameters
channelstring— Channel name listeners subscribe to viamultiplayer.on....any(optional) — Zero or more values delivered to each listener after the sender's peer id.
globals/multiplayer/syncTotals
multiplayer.syncTotals() -> SyncTotals
What the sync registry holds across every entity: entities with a registered synced component, component instances, declared properties, declared functions, and the component instances holding a dirty property this tick.
Returns SyncTotals — The registry totals.
print(multiplayer.syncTotals().properties .. " synced properties registered")
globals/multiplayer/undo
multiplayer.undo() -> boolean
Undo this client's last edit-mode operation.
Returns boolean — True if an operation was undone.
modules/multiplayer/README
require("@builtin/modules/api/engine/multiplayer") -- multiplayer (also available as global 'multiplayer')
Multiplayer sync state and operations — connection, peers, ownership, rooms, undo/redo. Public Luau surface over the __multiplayer Internal FFI namespace.
Usage: local multiplayer = require("@builtin/modules/api/engine/multiplayer") Also available as global: multiplayer
modules/multiplayer/beginOperation
beginOperation(description: string)
Begin recording an undoable operation. All mutations until
commitOperation() are grouped into one undo entry.
Parameters
descriptionstring— Human-readable label.
multiplayer.beginOperation("move cube")
modules/multiplayer/canRedo
canRedo(): boolean
Check if this client has any redoable operations.
modules/multiplayer/canUndo
canUndo(): boolean
Check if this client has any undoable operations.
modules/multiplayer/cancelOperation
cancelOperation()
Cancel the current operation and restore all properties to their values at begin time.
multiplayer.cancelOperation()
modules/multiplayer/claimOwnership
claimOwnership(entityId: (string | entityRef)?): boolean
Request ownership of an entity. Returns true if the claim was tentatively granted (relay confirmation pending).
Parameters
entityId(string | entityRef)?(optional) — Entity id or proxy to claim.
modules/multiplayer/clearHistory
clearHistory()
Drop this client's whole undo/redo history — for boundaries where old edits stop being meaningful (a scene load, a test rig reset).
multiplayer.clearHistory()
modules/multiplayer/commitOperation
commitOperation()
Finalize the current operation and push it onto the undo stack. Only changes that actually differ from the start state are recorded.
multiplayer.commitOperation()
modules/multiplayer/connect
connect(relayUrl: string)
Connect to a multiplayer relay server for the current world.
Uses the loaded world's world_id as the room prefix for scene
isolation. A world must be loaded before connecting.
Parameters
relayUrlstring— Relay server URL.
multiplayer.connect("https://relay.example.com")
modules/multiplayer/disconnect
disconnect()
Disconnect from the multiplayer relay server.
multiplayer.disconnect()
modules/multiplayer/explain
explain(
Why a synced property is not reaching the peers this client shares its entity's room with. Answers from the engine's own registry, so a name the component never registered is reported as such instead of inferred from a second client's silence.
local v = multiplayer.explain(player, "Health", "hp")
if not v.arriving then print(v.reason) end
modules/multiplayer/getDiagnostics
getDiagnostics(): SyncDiagnostics
Get sync diagnostics — traffic counts, bandwidth, link quality,
peer count. The counts — bytesSent/Received,
datagramsSent/Received, rpcsSent/Received, ownershipChanges —
are running totals for the session, so a sparse event stays readable
long after it happened; subtract two samples for the rate over the
interval between them. bytesSentPerSec / bytesReceivedPerSec are
averages over the last completed ~1 second window.
rttMs is the smoothed round-trip time to the relay and
packetLoss the fraction (0..1) of packets lost over the last 5
seconds; both read 0 until the transport has sampled a live
connection. messagesAwaitingEntity counts the sync messages this
peer is holding for an entity it has not received yet — each waits
for the spawn that names it, applies the moment it arrives, and is
released once its wait runs out.
local d = multiplayer.getDiagnostics()
if d.packetLoss > 0.05 then warnThePlayer(d.rttMs) end
print(d.messagesAwaitingEntity .. " message(s) waiting for their entity")
modules/multiplayer/getPeerId
getPeerId(): number?
Get this client's peer ID in the current session.
modules/multiplayer/getPeers
getPeers(): { PeerInfo }
Get a list of all connected peers in the current session.
for _, p in ipairs(multiplayer.getPeers()) do print(p.name) end
modules/multiplayer/getRoomPeers
getRoomPeers(roomKey: string): { PeerInfo }
Get the peers this client shares the given room with, ordered by
peer id. getPeers answers for the whole session — the union of every
room this client is in — while this answers for one room, so a peer
that leaves this room while staying in another disappears from here
and remains in getPeers.
Parameters
roomKeystring— Fully-qualified room key ({worldGuid}/{profile}/{mode}/{sceneGuid}).
for _, p in ipairs(multiplayer.getRoomPeers(key)) do print(p.id) end
modules/multiplayer/getRooms
getRooms(): { string }
The relay rooms this client has joined, sorted. A broadcast reaches only the peers that share one of these.
for _, key in ipairs(multiplayer.getRooms()) do print(key) end
modules/multiplayer/getTickRate
getTickRate(): number
Get the current sync tick rate (network updates per second).
modules/multiplayer/heldMessages
heldMessages(): { HeldMessage }
The sync messages this peer is holding for entities it has not
received — what getDiagnostics().messagesAwaitingEntity counts, one
entry each, with the entity sync id it names, its age and the grace it
is held against.
for _, m in ipairs(multiplayer.heldMessages()) do print(m.kind, m.ageMs) end
modules/multiplayer/isConnected
isConnected(): boolean
Check if a multiplayer session is active and connected to a relay.
modules/multiplayer/isHost
isHost(): boolean
Whether THIS client is the host (authoritative owner) of the
current scene's play room — the relay room CREATOR, or offline /
single-player. Host code spawns the shared synced world (via
entity.spawnSynced or a scene's onHostLoad) and runs authoritative
simulation; a non-host (JOINER) receives that content from the relay
snapshot and must NOT re-create it. Gate ANY code that spawns synced
entities or owns shared state with this so it runs on exactly one
client — running it on every peer is the double-spawn 'explosion'.
if multiplayer.isHost() then enemy = entity.spawnSynced("enemy") end
modules/multiplayer/isOwner
isOwner(entityId: (string | entityRef)?): boolean
Check if the local client owns the given entity (or the current entity if called from a component). Only the owner can modify synced properties directly.
Parameters
entityId(string | entityRef)?(optional) — Entity id or proxy to check (defaults toself.entityIdin component context).
modules/multiplayer/isRoomCreator
isRoomCreator(roomKey: string): boolean?
Whether this client created the given room — it was the FIRST peer to join it (race-free; the relay assigns it on join). In play mode the creator instantiates the scene's entities (synced) and every other joiner receives them from the relay snapshot, so the scene is never double-instantiated.
Parameters
roomKeystring— Fully-qualified room key ({worldGuid}/{profile}/{mode}/{sceneGuid}).
if multiplayer.isRoomCreator(key) ~= false then layers.load(scene) end
modules/multiplayer/joinRoom
joinRoom(roomKey: string)
Join a relay room. Room keys are built as
{worldGuid}/{profile}/{mode}/{sceneGuid} — four segments, the
{profile} one keeping a runtime peer (published content) and an
editor peer (live content) in separate rooms even when both are in
play mode. Rooms partition the relay's fan-out: only peers in the
same room receive each other's broadcasts. getRooms() reports the
keys this client is already in and roomFor(entity) the one an
entity broadcasts into, so a key can be read rather than rebuilt.
No-op when not connected or already joined.
Parameters
roomKeystring— Fully-qualified room key.
multiplayer.joinRoom(worldGuid .. "/" .. engine.profile .. "/play/" .. sceneGuid)
modules/multiplayer/leaveRoom
leaveRoom(roomKey: string)
Leave a relay room. The key is reported under
observe().withdrawnRooms until joinRoom names it again. No-op
when not connected or not joined.
Parameters
roomKeystring— Fully-qualified room key.
modules/multiplayer/loopback
loopback(): { [string]: any }
Loopback testing harness. Returns a table with enable(),
disable(), flush(), receive() methods for testing sync
without a relay server.
modules/multiplayer/observe
observe(): ReplicationObservation
Report what this peer is replicating and why a property is not
arriving. Carries the rooms this client joined, one record per entity
with a sync id — its owner, the room it broadcasts into, how many
other peers share that room, and every REGISTERED synced component
with its declared property names, wire indices, public/private table
and dirty bits — the messages held for entities that have not arrived,
and the registry's totals. Every property carries notArriving: one
name from reasons, or nil when it is on its way. Answers in edit
mode as well as play mode, for what the relay carries in each: in
edit mode scene content is left out of the sync-id pass, so its
changes travel to the other clients with the source they are
written into and it reads entityNotSynced here.
local o = multiplayer.observe()
print(#o.entities .. " entities, " .. o.totals.properties .. " synced properties")
modules/multiplayer/observeComponent
observeComponent(
The registered synced component of the named type on an entity's
record. Matches a fully-qualified type (@builtin::components.Model)
and the leaf name it ends in (Model) alike.
local c = multiplayer.observeComponent(player, "Health")
print(#c.properties .. " declared properties")
modules/multiplayer/observeEntity
observeEntity(entityId: (string | entityRef)): EntityReplication?
The replication record for one entity — its sync id, owner, room, and the synced components registered on it.
Parameters
entityId(string | entityRef)— Entity id or proxy.
local r = multiplayer.observeEntity(e)
for _, c in ipairs(r.components) do print(c.componentType) end
modules/multiplayer/on
on(channel: string, callback: (number, ...any) -> ())
Subscribe to a custom message channel. The callback runs as
callback(fromPeerId, ...args) whenever another peer calls
multiplayer.send(channel, ...). Multiple callbacks per channel fire
in registration order.
Parameters
channelstring— Channel name to listen on.callback(number, ...any) -> ()—function(fromPeerId: number, ...)— the sender's peer id then the sent args.
modules/multiplayer/recordSpawn
recordSpawn(entityId: string)
Adopt an existing entity into the open operation as its spawn — for flows that create an entity before the operation opens (a drag preview adopted on drop). Undoing the operation despawns it.
Parameters
entityIdstring— Entity id to record as spawned by this operation.
multiplayer.recordSpawn(id)
modules/multiplayer/redo
redo(): boolean
Redo this client's last undone operation.
modules/multiplayer/releaseOwnership
releaseOwnership(entityId: (string | entityRef)?): boolean
Release ownership of an entity.
Parameters
entityId(string | entityRef)?(optional) — Entity id or proxy to release.
modules/multiplayer/roomFor
roomFor(entityId: (string | entityRef)): string?
The room key an entity's spawns and property deltas broadcast into.
Parameters
entityId(string | entityRef)— Entity id or proxy.
local key = multiplayer.roomFor(player)
if key then multiplayer.joinRoom(key) end
modules/multiplayer/send
send(channel: string, ...: any)
Broadcast a message on a named channel to every OTHER peer in the
room. The relay forwards it transparently; peers receive it via
multiplayer.on. Arguments may be any synced value (nil, boolean,
number, string, Vec3, entity/component proxy, or table) and are
delivered to listeners in order. No-op when not connected.
Parameters
channelstring— Channel name listeners subscribe to viamultiplayer.on.argsany(optional)
modules/multiplayer/syncTotals
syncTotals(): SyncTotals
What the sync registry holds across every entity: entities with a registered synced component, component instances, declared properties, declared functions, and the component instances holding a dirty property this tick.
print(multiplayer.syncTotals().properties .. " synced properties registered")
modules/multiplayer/undo
undo(): boolean
Undo this client's last edit-mode operation.
typed/builtin//modules/api/engine/multiplayer/multiplayer/beginOperation
multiplayer.beginOperation(description: string)
Begin recording an undoable operation. All mutations until
commitOperation() are grouped into one undo entry.
Parameters
descriptionstring— Human-readable label.
multiplayer.beginOperation("move cube")
typed/builtin//modules/api/engine/multiplayer/multiplayer/canRedo
multiplayer.canRedo() -> boolean
Check if this client has any redoable operations.
Returns boolean — True if redo is available.
typed/builtin//modules/api/engine/multiplayer/multiplayer/canUndo
multiplayer.canUndo() -> boolean
Check if this client has any undoable operations.
Returns boolean — True if undo is available.
typed/builtin//modules/api/engine/multiplayer/multiplayer/cancelOperation
multiplayer.cancelOperation()
Cancel the current operation and restore all properties to their values at begin time.
multiplayer.cancelOperation()
typed/builtin//modules/api/engine/multiplayer/multiplayer/claimOwnership
multiplayer.claimOwnership(entityId: (string | entityRef)?) -> boolean
Request ownership of an entity. Returns true if the claim was tentatively granted (relay confirmation pending).
Parameters
entityId(string | entityRef)(optional) — Entity id or proxy to claim.
Returns boolean — True if the claim was tentatively accepted.
typed/builtin//modules/api/engine/multiplayer/multiplayer/clearHistory
multiplayer.clearHistory()
Drop this client's whole undo/redo history — for boundaries where old edits stop being meaningful (a scene load, a test rig reset).
multiplayer.clearHistory()
typed/builtin//modules/api/engine/multiplayer/multiplayer/commitOperation
multiplayer.commitOperation()
Finalize the current operation and push it onto the undo stack. Only changes that actually differ from the start state are recorded.
multiplayer.commitOperation()
typed/builtin//modules/api/engine/multiplayer/multiplayer/connect
multiplayer.connect(relayUrl: string)
Connect to a multiplayer relay server for the current world.
Uses the loaded world's world_id as the room prefix for scene
isolation. A world must be loaded before connecting.
Parameters
relayUrlstring— Relay server URL.
multiplayer.connect("https://relay.example.com")
typed/builtin//modules/api/engine/multiplayer/multiplayer/disconnect
multiplayer.disconnect()
Disconnect from the multiplayer relay server.
multiplayer.disconnect()
typed/builtin//modules/api/engine/multiplayer/multiplayer/explain
multiplayer.explain(entityId: (string | entityRef), componentType: string, property: string) -> DeliveryVerdict
Why a synced property is not reaching the peers this client shares its entity's room with. Answers from the engine's own registry, so a name the component never registered is reported as such instead of inferred from a second client's silence.
Parameters
entityId(string | entityRef)— Entity id or proxy.componentTypestring— Component type name, e.g. "Health".propertystring— Property name as written in the component'ssync {}block.
Returns DeliveryVerdict — arriving true when the property is on its way; otherwise reason names the cause and property carries the registry's record of it when one exists.
local v = multiplayer.explain(player, "Health", "hp")
if not v.arriving then print(v.reason) end
typed/builtin//modules/api/engine/multiplayer/multiplayer/getDiagnostics
multiplayer.getDiagnostics() -> SyncDiagnostics
Get sync diagnostics — traffic counts, bandwidth, link quality,
peer count. The counts — bytesSent/Received,
datagramsSent/Received, rpcsSent/Received, ownershipChanges —
are running totals for the session, so a sparse event stays readable
long after it happened; subtract two samples for the rate over the
interval between them. bytesSentPerSec / bytesReceivedPerSec are
averages over the last completed ~1 second window.
rttMs is the smoothed round-trip time to the relay and
packetLoss the fraction (0..1) of packets lost over the last 5
seconds; both read 0 until the transport has sampled a live
connection. messagesAwaitingEntity counts the sync messages this
peer is holding for an entity it has not received yet — each waits
for the spawn that names it, applies the moment it arrives, and is
released once its wait runs out.
Returns SyncDiagnostics — Diagnostics table.
local d = multiplayer.getDiagnostics()
if d.packetLoss > 0.05 then warnThePlayer(d.rttMs) end
print(d.messagesAwaitingEntity .. " message(s) waiting for their entity")
typed/builtin//modules/api/engine/multiplayer/multiplayer/getPeerId
multiplayer.getPeerId() -> number?
Get this client's peer ID in the current session.
Returns number? — Local peer ID, or nil if not connected.
typed/builtin//modules/api/engine/multiplayer/multiplayer/getPeers
multiplayer.getPeers() -> { PeerInfo }
Get a list of all connected peers in the current session.
Returns { PeerInfo } — Array of peer info tables.
for _, p in ipairs(multiplayer.getPeers()) do print(p.name) end
typed/builtin//modules/api/engine/multiplayer/multiplayer/getRoomPeers
multiplayer.getRoomPeers(roomKey: string) -> { PeerInfo }
Get the peers this client shares the given room with, ordered by
peer id. getPeers answers for the whole session — the union of every
room this client is in — while this answers for one room, so a peer
that leaves this room while staying in another disappears from here
and remains in getPeers.
Parameters
roomKeystring— Fully-qualified room key ({worldGuid}/{profile}/{mode}/{sceneGuid}).
Returns { PeerInfo } — Array of peer info tables for that room.
for _, p in ipairs(multiplayer.getRoomPeers(key)) do print(p.id) end
typed/builtin//modules/api/engine/multiplayer/multiplayer/getRooms
multiplayer.getRooms() -> { string }
The relay rooms this client has joined, sorted. A broadcast reaches only the peers that share one of these.
Returns { string } — Room keys.
for _, key in ipairs(multiplayer.getRooms()) do print(key) end
typed/builtin//modules/api/engine/multiplayer/multiplayer/getTickRate
multiplayer.getTickRate() -> number
Get the current sync tick rate (network updates per second).
Returns number — Sync ticks per second (default 20).
typed/builtin//modules/api/engine/multiplayer/multiplayer/heldMessages
multiplayer.heldMessages() -> { HeldMessage }
The sync messages this peer is holding for entities it has not
received — what getDiagnostics().messagesAwaitingEntity counts, one
entry each, with the entity sync id it names, its age and the grace it
is held against.
Returns { HeldMessage } — Held messages.
for _, m in ipairs(multiplayer.heldMessages()) do print(m.kind, m.ageMs) end
typed/builtin//modules/api/engine/multiplayer/multiplayer/isConnected
multiplayer.isConnected() -> boolean
Check if a multiplayer session is active and connected to a relay.
Returns boolean — True if connected.
typed/builtin//modules/api/engine/multiplayer/multiplayer/isHost
multiplayer.isHost() -> boolean
Whether THIS client is the host (authoritative owner) of the
current scene's play room — the relay room CREATOR, or offline /
single-player. Host code spawns the shared synced world (via
entity.spawnSynced or a scene's onHostLoad) and runs authoritative
simulation; a non-host (JOINER) receives that content from the relay
snapshot and must NOT re-create it. Gate ANY code that spawns synced
entities or owns shared state with this so it runs on exactly one
client — running it on every peer is the double-spawn 'explosion'.
Returns boolean — True on the host / offline / single-player; false on a confirmed joiner. Defaults to true when the role isn't known yet (degrade to host so single-player and pre-join code still run) — pair with a scene's onHostLoad hook when exact one-shot timing matters.
if multiplayer.isHost() then enemy = entity.spawnSynced("enemy") end
typed/builtin//modules/api/engine/multiplayer/multiplayer/isOwner
multiplayer.isOwner(entityId: (string | entityRef)?) -> boolean
Check if the local client owns the given entity (or the current entity if called from a component). Only the owner can modify synced properties directly.
Parameters
entityId(string | entityRef)(optional) — Entity id or proxy to check (defaults toself.entityIdin component context).
Returns boolean — True if the local client is the owner.
typed/builtin//modules/api/engine/multiplayer/multiplayer/isRoomCreator
multiplayer.isRoomCreator(roomKey: string) -> boolean?
Whether this client created the given room — it was the FIRST peer to join it (race-free; the relay assigns it on join). In play mode the creator instantiates the scene's entities (synced) and every other joiner receives them from the relay snapshot, so the scene is never double-instantiated.
Parameters
roomKeystring— Fully-qualified room key ({worldGuid}/{profile}/{mode}/{sceneGuid}).
Returns boolean? — True if this client created the room, false if it joined an existing one, nil if the relay hasn't reported a role yet (offline / not joined).
if multiplayer.isRoomCreator(key) ~= false then layers.load(scene) end
typed/builtin//modules/api/engine/multiplayer/multiplayer/joinRoom
multiplayer.joinRoom(roomKey: string)
Join a relay room. Room keys are built as
{worldGuid}/{profile}/{mode}/{sceneGuid} — four segments, the
{profile} one keeping a runtime peer (published content) and an
editor peer (live content) in separate rooms even when both are in
play mode. Rooms partition the relay's fan-out: only peers in the
same room receive each other's broadcasts. getRooms() reports the
keys this client is already in and roomFor(entity) the one an
entity broadcasts into, so a key can be read rather than rebuilt.
No-op when not connected or already joined.
Parameters
roomKeystring— Fully-qualified room key.
multiplayer.joinRoom(worldGuid .. "/" .. engine.profile .. "/play/" .. sceneGuid)
typed/builtin//modules/api/engine/multiplayer/multiplayer/leaveRoom
multiplayer.leaveRoom(roomKey: string)
Leave a relay room. The key is reported under
observe().withdrawnRooms until joinRoom names it again. No-op
when not connected or not joined.
Parameters
roomKeystring— Fully-qualified room key.
typed/builtin//modules/api/engine/multiplayer/multiplayer/loopback
multiplayer.loopback() -> { [string]: any }
Loopback testing harness. Returns a table with enable(),
disable(), flush(), receive() methods for testing sync
without a relay server.
Returns { [string]: any } — Loopback API table.
typed/builtin//modules/api/engine/multiplayer/multiplayer/observe
multiplayer.observe() -> ReplicationObservation
Report what this peer is replicating and why a property is not
arriving. Carries the rooms this client joined, one record per entity
with a sync id — its owner, the room it broadcasts into, how many
other peers share that room, and every REGISTERED synced component
with its declared property names, wire indices, public/private table
and dirty bits — the messages held for entities that have not arrived,
and the registry's totals. Every property carries notArriving: one
name from reasons, or nil when it is on its way. Answers in edit
mode as well as play mode, for what the relay carries in each: in
edit mode scene content is left out of the sync-id pass, so its
changes travel to the other clients with the source they are
written into and it reads entityNotSynced here.
Returns ReplicationObservation — The engine's current replication observation.
local o = multiplayer.observe()
print(#o.entities .. " entities, " .. o.totals.properties .. " synced properties")
typed/builtin//modules/api/engine/multiplayer/multiplayer/observeComponent
multiplayer.observeComponent(entityId: (string | entityRef), componentType: string) -> SyncedComponent?
The registered synced component of the named type on an entity's
record. Matches a fully-qualified type (@builtin::components.Model)
and the leaf name it ends in (Model) alike.
Parameters
entityId(string | entityRef)— Entity id or proxy.componentTypestring— Component type name or its leaf.
Returns SyncedComponent? — The registered component instance, or nil when none of that type is registered on the entity.
local c = multiplayer.observeComponent(player, "Health")
print(#c.properties .. " declared properties")
typed/builtin//modules/api/engine/multiplayer/multiplayer/observeEntity
multiplayer.observeEntity(entityId: (string | entityRef)) -> EntityReplication?
The replication record for one entity — its sync id, owner, room, and the synced components registered on it.
Parameters
entityId(string | entityRef)— Entity id or proxy.
Returns EntityReplication? — The entity's record, or nil when the engine holds no sync record for it.
local r = multiplayer.observeEntity(e)
for _, c in ipairs(r.components) do print(c.componentType) end
typed/builtin//modules/api/engine/multiplayer/multiplayer/on
multiplayer.on(channel: string, callback: (number, ...any) -> ())
Subscribe to a custom message channel. The callback runs as
callback(fromPeerId, ...args) whenever another peer calls
multiplayer.send(channel, ...). Multiple callbacks per channel fire
in registration order.
typed/builtin//modules/api/engine/multiplayer/multiplayer/recordSpawn
multiplayer.recordSpawn(entityId: string)
Adopt an existing entity into the open operation as its spawn — for flows that create an entity before the operation opens (a drag preview adopted on drop). Undoing the operation despawns it.
Parameters
entityIdstring— Entity id to record as spawned by this operation.
multiplayer.recordSpawn(id)
typed/builtin//modules/api/engine/multiplayer/multiplayer/redo
multiplayer.redo() -> boolean
Redo this client's last undone operation.
Returns boolean — True if an operation was redone.
typed/builtin//modules/api/engine/multiplayer/multiplayer/releaseOwnership
multiplayer.releaseOwnership(entityId: (string | entityRef)?) -> boolean
Release ownership of an entity.
Parameters
entityId(string | entityRef)(optional) — Entity id or proxy to release.
Returns boolean — True if ownership was released.
typed/builtin//modules/api/engine/multiplayer/multiplayer/roomFor
multiplayer.roomFor(entityId: (string | entityRef)) -> string?
The room key an entity's spawns and property deltas broadcast into.
Parameters
entityId(string | entityRef)— Entity id or proxy.
Returns string? — The room key, or nil when the engine has established no scene context for the entity.
local key = multiplayer.roomFor(player)
if key then multiplayer.joinRoom(key) end
typed/builtin//modules/api/engine/multiplayer/multiplayer/send
multiplayer.send(channel: string, ...: any?)
Broadcast a message on a named channel to every OTHER peer in the
room. The relay forwards it transparently; peers receive it via
multiplayer.on. Arguments may be any synced value (nil, boolean,
number, string, Vec3, entity/component proxy, or table) and are
delivered to listeners in order. No-op when not connected.
Parameters
channelstring— Channel name listeners subscribe to viamultiplayer.on....any(optional) — Zero or more values delivered to each listener after the sender's peer id.
typed/builtin//modules/api/engine/multiplayer/multiplayer/syncTotals
multiplayer.syncTotals() -> SyncTotals
What the sync registry holds across every entity: entities with a registered synced component, component instances, declared properties, declared functions, and the component instances holding a dirty property this tick.
Returns SyncTotals — The registry totals.
print(multiplayer.syncTotals().properties .. " synced properties registered")
typed/builtin//modules/api/engine/multiplayer/multiplayer/undo
multiplayer.undo() -> boolean
Undo this client's last edit-mode operation.
Returns boolean — True if an operation was undone.