Log inGet started

multiplayer

Updated 5 September 2026

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

  • description string — 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

  • relayUrl string — 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.
  • componentType string — Component type name, e.g. "Health".
  • property string — Property name as written in the component's sync {} block.

Returns DeliveryVerdictarriving 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

  • roomKey string — 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 to self.entityId in 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

  • roomKey string — 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

  • roomKey string — 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

  • roomKey string — 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.
  • componentType string — 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

  • channel string — 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

  • entityId string — 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

  • channel string — Channel name listeners subscribe to via multiplayer.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

  • description string — 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

  • relayUrl string — 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

  • roomKey string — 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 to self.entityId in 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

  • roomKey string — 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

  • roomKey string — 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

  • roomKey string — 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

  • channel string — 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

  • entityId string — 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

  • channel string — Channel name listeners subscribe to via multiplayer.on.
  • args any (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

  • description string — 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

  • relayUrl string — 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.
  • componentType string — Component type name, e.g. "Health".
  • property string — Property name as written in the component's sync {} block.

Returns DeliveryVerdictarriving 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

  • roomKey string — 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 to self.entityId in 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

  • roomKey string — 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

  • roomKey string — 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

  • roomKey string — 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.
  • componentType string — 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

  • entityId string — 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

  • channel string — Channel name listeners subscribe to via multiplayer.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.

  • api
  • reference