---
title: "multiplayer"
description: "The multiplayer namespace — the engine's Luau API reference for multiplayer."
section: "API Reference"
slug: "api-multiplayer"
canonical: "https://origozero.ai/docs/api-multiplayer"
updated: "2026-09-05T23:13:46.875339397+00:00"
tags: ["api", "reference"]
---

# multiplayer

The `multiplayer` namespace — 106 functions.

## globals/multiplayer/beginOperation {#globals-multiplayer-beginoperation}

```lua
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.

```lua
multiplayer.beginOperation("move cube")
```

## globals/multiplayer/canRedo {#globals-multiplayer-canredo}

```lua
multiplayer.canRedo() -> boolean
```

Check if this client has any redoable operations.

**Returns** `boolean` — True if redo is available.

## globals/multiplayer/canUndo {#globals-multiplayer-canundo}

```lua
multiplayer.canUndo() -> boolean
```

Check if this client has any undoable operations.

**Returns** `boolean` — True if undo is available.

## globals/multiplayer/cancelOperation {#globals-multiplayer-canceloperation}

```lua
multiplayer.cancelOperation()
```

Cancel the current operation and restore all properties to
their values at begin time.

```lua
multiplayer.cancelOperation()
```

## globals/multiplayer/claimOwnership {#globals-multiplayer-claimownership}

```lua
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 {#globals-multiplayer-clearhistory}

```lua
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).

```lua
multiplayer.clearHistory()
```

## globals/multiplayer/commitOperation {#globals-multiplayer-commitoperation}

```lua
multiplayer.commitOperation()
```

Finalize the current operation and push it onto the undo
stack. Only changes that actually differ from the start state
are recorded.

```lua
multiplayer.commitOperation()
```

## globals/multiplayer/connect {#globals-multiplayer-connect}

```lua
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.

```lua
multiplayer.connect("https://relay.example.com")
```

## globals/multiplayer/disconnect {#globals-multiplayer-disconnect}

```lua
multiplayer.disconnect()
```

Disconnect from the multiplayer relay server.

```lua
multiplayer.disconnect()
```

## globals/multiplayer/explain {#globals-multiplayer-explain}

```lua
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** `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.

```lua
local v = multiplayer.explain(player, "Health", "hp")
if not v.arriving then print(v.reason) end
```

## globals/multiplayer/getDiagnostics {#globals-multiplayer-getdiagnostics}

```lua
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.

```lua
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 {#globals-multiplayer-getpeerid}

```lua
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 {#globals-multiplayer-getpeers}

```lua
multiplayer.getPeers() -> { PeerInfo }
```

Get a list of all connected peers in the current session.

**Returns** `{ PeerInfo }` — Array of peer info tables.

```lua
for _, p in ipairs(multiplayer.getPeers()) do print(p.name) end
```

## globals/multiplayer/getRoomPeers {#globals-multiplayer-getroompeers}

```lua
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.

```lua
for _, p in ipairs(multiplayer.getRoomPeers(key)) do print(p.id) end
```

## globals/multiplayer/getRooms {#globals-multiplayer-getrooms}

```lua
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.

```lua
for _, key in ipairs(multiplayer.getRooms()) do print(key) end
```

## globals/multiplayer/getTickRate {#globals-multiplayer-gettickrate}

```lua
multiplayer.getTickRate() -> number
```

Get the current sync tick rate (network updates per second).

**Returns** `number` — Sync ticks per second (default 20).

## globals/multiplayer/heldMessages {#globals-multiplayer-heldmessages}

```lua
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.

```lua
for _, m in ipairs(multiplayer.heldMessages()) do print(m.kind, m.ageMs) end
```

## globals/multiplayer/isConnected {#globals-multiplayer-isconnected}

```lua
multiplayer.isConnected() -> boolean
```

Check if a multiplayer session is active and connected to a
relay.

**Returns** `boolean` — True if connected.

## globals/multiplayer/isHost {#globals-multiplayer-ishost}

```lua
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.

```lua
if multiplayer.isHost() then enemy = entity.spawnSynced("enemy") end
```

## globals/multiplayer/isOwner {#globals-multiplayer-isowner}

```lua
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 {#globals-multiplayer-isroomcreator}

```lua
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).

```lua
if multiplayer.isRoomCreator(key) ~= false then layers.load(scene) end
```

## globals/multiplayer/joinRoom {#globals-multiplayer-joinroom}

```lua
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.

```lua
multiplayer.joinRoom(worldGuid .. "/" .. engine.profile .. "/play/" .. sceneGuid)
```

## globals/multiplayer/leaveRoom {#globals-multiplayer-leaveroom}

```lua
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 {#globals-multiplayer-loopback}

```lua
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 {#globals-multiplayer-observe}

```lua
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.

```lua
local o = multiplayer.observe()
print(#o.entities .. " entities, " .. o.totals.properties .. " synced properties")
```

## globals/multiplayer/observeComponent {#globals-multiplayer-observecomponent}

```lua
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.

```lua
local c = multiplayer.observeComponent(player, "Health")
print(#c.properties .. " declared properties")
```

## globals/multiplayer/observeEntity {#globals-multiplayer-observeentity}

```lua
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.

```lua
local r = multiplayer.observeEntity(e)
for _, c in ipairs(r.components) do print(c.componentType) end
```

## globals/multiplayer/on {#globals-multiplayer-on}

```lua
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 {#globals-multiplayer-recordspawn}

```lua
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.

```lua
multiplayer.recordSpawn(id)
```

## globals/multiplayer/redo {#globals-multiplayer-redo}

```lua
multiplayer.redo() -> boolean
```

Redo this client's last undone operation.

**Returns** `boolean` — True if an operation was redone.

## globals/multiplayer/releaseOwnership {#globals-multiplayer-releaseownership}

```lua
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 {#globals-multiplayer-roomfor}

```lua
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.

```lua
local key = multiplayer.roomFor(player)
if key then multiplayer.joinRoom(key) end
```

## globals/multiplayer/send {#globals-multiplayer-send}

```lua
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 {#globals-multiplayer-synctotals}

```lua
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.

```lua
print(multiplayer.syncTotals().properties .. " synced properties registered")
```

## globals/multiplayer/undo {#globals-multiplayer-undo}

```lua
multiplayer.undo() -> boolean
```

Undo this client's last edit-mode operation.

**Returns** `boolean` — True if an operation was undone.

## modules/multiplayer/README {#modules-multiplayer-readme}

```lua
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 {#modules-multiplayer-beginoperation}

```lua
beginOperation(description: string)
```

Begin recording an undoable operation. All mutations until
`commitOperation()` are grouped into one undo entry.

**Parameters**

- `description` `string` — Human-readable label.

```lua
multiplayer.beginOperation("move cube")
```

## modules/multiplayer/canRedo {#modules-multiplayer-canredo}

```lua
canRedo(): boolean
```

Check if this client has any redoable operations.

## modules/multiplayer/canUndo {#modules-multiplayer-canundo}

```lua
canUndo(): boolean
```

Check if this client has any undoable operations.

## modules/multiplayer/cancelOperation {#modules-multiplayer-canceloperation}

```lua
cancelOperation()
```

Cancel the current operation and restore all properties to
their values at begin time.

```lua
multiplayer.cancelOperation()
```

## modules/multiplayer/claimOwnership {#modules-multiplayer-claimownership}

```lua
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 {#modules-multiplayer-clearhistory}

```lua
clearHistory()
```

Drop this client's whole undo/redo history — for boundaries where
old edits stop being meaningful (a scene load, a test rig reset).

```lua
multiplayer.clearHistory()
```

## modules/multiplayer/commitOperation {#modules-multiplayer-commitoperation}

```lua
commitOperation()
```

Finalize the current operation and push it onto the undo
stack. Only changes that actually differ from the start state
are recorded.

```lua
multiplayer.commitOperation()
```

## modules/multiplayer/connect {#modules-multiplayer-connect}

```lua
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.

```lua
multiplayer.connect("https://relay.example.com")
```

## modules/multiplayer/disconnect {#modules-multiplayer-disconnect}

```lua
disconnect()
```

Disconnect from the multiplayer relay server.

```lua
multiplayer.disconnect()
```

## modules/multiplayer/explain {#modules-multiplayer-explain}

```lua
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.

```lua
local v = multiplayer.explain(player, "Health", "hp")
if not v.arriving then print(v.reason) end
```

## modules/multiplayer/getDiagnostics {#modules-multiplayer-getdiagnostics}

```lua
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.

```lua
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 {#modules-multiplayer-getpeerid}

```lua
getPeerId(): number?
```

Get this client's peer ID in the current session.

## modules/multiplayer/getPeers {#modules-multiplayer-getpeers}

```lua
getPeers(): { PeerInfo }
```

Get a list of all connected peers in the current session.

```lua
for _, p in ipairs(multiplayer.getPeers()) do print(p.name) end
```

## modules/multiplayer/getRoomPeers {#modules-multiplayer-getroompeers}

```lua
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}`).

```lua
for _, p in ipairs(multiplayer.getRoomPeers(key)) do print(p.id) end
```

## modules/multiplayer/getRooms {#modules-multiplayer-getrooms}

```lua
getRooms(): { string }
```

The relay rooms this client has joined, sorted. A broadcast reaches
only the peers that share one of these.

```lua
for _, key in ipairs(multiplayer.getRooms()) do print(key) end
```

## modules/multiplayer/getTickRate {#modules-multiplayer-gettickrate}

```lua
getTickRate(): number
```

Get the current sync tick rate (network updates per second).

## modules/multiplayer/heldMessages {#modules-multiplayer-heldmessages}

```lua
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.

```lua
for _, m in ipairs(multiplayer.heldMessages()) do print(m.kind, m.ageMs) end
```

## modules/multiplayer/isConnected {#modules-multiplayer-isconnected}

```lua
isConnected(): boolean
```

Check if a multiplayer session is active and connected to a
relay.

## modules/multiplayer/isHost {#modules-multiplayer-ishost}

```lua
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'.

```lua
if multiplayer.isHost() then enemy = entity.spawnSynced("enemy") end
```

## modules/multiplayer/isOwner {#modules-multiplayer-isowner}

```lua
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 {#modules-multiplayer-isroomcreator}

```lua
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}`).

```lua
if multiplayer.isRoomCreator(key) ~= false then layers.load(scene) end
```

## modules/multiplayer/joinRoom {#modules-multiplayer-joinroom}

```lua
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.

```lua
multiplayer.joinRoom(worldGuid .. "/" .. engine.profile .. "/play/" .. sceneGuid)
```

## modules/multiplayer/leaveRoom {#modules-multiplayer-leaveroom}

```lua
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 {#modules-multiplayer-loopback}

```lua
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 {#modules-multiplayer-observe}

```lua
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.

```lua
local o = multiplayer.observe()
print(#o.entities .. " entities, " .. o.totals.properties .. " synced properties")
```

## modules/multiplayer/observeComponent {#modules-multiplayer-observecomponent}

```lua
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.

```lua
local c = multiplayer.observeComponent(player, "Health")
print(#c.properties .. " declared properties")
```

## modules/multiplayer/observeEntity {#modules-multiplayer-observeentity}

```lua
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.

```lua
local r = multiplayer.observeEntity(e)
for _, c in ipairs(r.components) do print(c.componentType) end
```

## modules/multiplayer/on {#modules-multiplayer-on}

```lua
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 {#modules-multiplayer-recordspawn}

```lua
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.

```lua
multiplayer.recordSpawn(id)
```

## modules/multiplayer/redo {#modules-multiplayer-redo}

```lua
redo(): boolean
```

Redo this client's last undone operation.

## modules/multiplayer/releaseOwnership {#modules-multiplayer-releaseownership}

```lua
releaseOwnership(entityId: (string | entityRef)?): boolean
```

Release ownership of an entity.

**Parameters**

- `entityId` `(string | entityRef)?` _(optional)_ — Entity id or proxy to release.

## modules/multiplayer/roomFor {#modules-multiplayer-roomfor}

```lua
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.

```lua
local key = multiplayer.roomFor(player)
if key then multiplayer.joinRoom(key) end
```

## modules/multiplayer/send {#modules-multiplayer-send}

```lua
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 {#modules-multiplayer-synctotals}

```lua
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.

```lua
print(multiplayer.syncTotals().properties .. " synced properties registered")
```

## modules/multiplayer/undo {#modules-multiplayer-undo}

```lua
undo(): boolean
```

Undo this client's last edit-mode operation.

## typed/builtin//modules/api/engine/multiplayer/multiplayer/beginOperation {#typed-builtin-modules-api-engine-multiplayer-multiplayer-beginoperation}

```lua
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.

```lua
multiplayer.beginOperation("move cube")
```

## typed/builtin//modules/api/engine/multiplayer/multiplayer/canRedo {#typed-builtin-modules-api-engine-multiplayer-multiplayer-canredo}

```lua
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 {#typed-builtin-modules-api-engine-multiplayer-multiplayer-canundo}

```lua
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 {#typed-builtin-modules-api-engine-multiplayer-multiplayer-canceloperation}

```lua
multiplayer.cancelOperation()
```

Cancel the current operation and restore all properties to
their values at begin time.

```lua
multiplayer.cancelOperation()
```

## typed/builtin//modules/api/engine/multiplayer/multiplayer/claimOwnership {#typed-builtin-modules-api-engine-multiplayer-multiplayer-claimownership}

```lua
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 {#typed-builtin-modules-api-engine-multiplayer-multiplayer-clearhistory}

```lua
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).

```lua
multiplayer.clearHistory()
```

## typed/builtin//modules/api/engine/multiplayer/multiplayer/commitOperation {#typed-builtin-modules-api-engine-multiplayer-multiplayer-commitoperation}

```lua
multiplayer.commitOperation()
```

Finalize the current operation and push it onto the undo
stack. Only changes that actually differ from the start state
are recorded.

```lua
multiplayer.commitOperation()
```

## typed/builtin//modules/api/engine/multiplayer/multiplayer/connect {#typed-builtin-modules-api-engine-multiplayer-multiplayer-connect}

```lua
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.

```lua
multiplayer.connect("https://relay.example.com")
```

## typed/builtin//modules/api/engine/multiplayer/multiplayer/disconnect {#typed-builtin-modules-api-engine-multiplayer-multiplayer-disconnect}

```lua
multiplayer.disconnect()
```

Disconnect from the multiplayer relay server.

```lua
multiplayer.disconnect()
```

## typed/builtin//modules/api/engine/multiplayer/multiplayer/explain {#typed-builtin-modules-api-engine-multiplayer-multiplayer-explain}

```lua
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** `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.

```lua
local v = multiplayer.explain(player, "Health", "hp")
if not v.arriving then print(v.reason) end
```

## typed/builtin//modules/api/engine/multiplayer/multiplayer/getDiagnostics {#typed-builtin-modules-api-engine-multiplayer-multiplayer-getdiagnostics}

```lua
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.

```lua
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 {#typed-builtin-modules-api-engine-multiplayer-multiplayer-getpeerid}

```lua
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 {#typed-builtin-modules-api-engine-multiplayer-multiplayer-getpeers}

```lua
multiplayer.getPeers() -> { PeerInfo }
```

Get a list of all connected peers in the current session.

**Returns** `{ PeerInfo }` — Array of peer info tables.

```lua
for _, p in ipairs(multiplayer.getPeers()) do print(p.name) end
```

## typed/builtin//modules/api/engine/multiplayer/multiplayer/getRoomPeers {#typed-builtin-modules-api-engine-multiplayer-multiplayer-getroompeers}

```lua
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.

```lua
for _, p in ipairs(multiplayer.getRoomPeers(key)) do print(p.id) end
```

## typed/builtin//modules/api/engine/multiplayer/multiplayer/getRooms {#typed-builtin-modules-api-engine-multiplayer-multiplayer-getrooms}

```lua
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.

```lua
for _, key in ipairs(multiplayer.getRooms()) do print(key) end
```

## typed/builtin//modules/api/engine/multiplayer/multiplayer/getTickRate {#typed-builtin-modules-api-engine-multiplayer-multiplayer-gettickrate}

```lua
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 {#typed-builtin-modules-api-engine-multiplayer-multiplayer-heldmessages}

```lua
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.

```lua
for _, m in ipairs(multiplayer.heldMessages()) do print(m.kind, m.ageMs) end
```

## typed/builtin//modules/api/engine/multiplayer/multiplayer/isConnected {#typed-builtin-modules-api-engine-multiplayer-multiplayer-isconnected}

```lua
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 {#typed-builtin-modules-api-engine-multiplayer-multiplayer-ishost}

```lua
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.

```lua
if multiplayer.isHost() then enemy = entity.spawnSynced("enemy") end
```

## typed/builtin//modules/api/engine/multiplayer/multiplayer/isOwner {#typed-builtin-modules-api-engine-multiplayer-multiplayer-isowner}

```lua
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 {#typed-builtin-modules-api-engine-multiplayer-multiplayer-isroomcreator}

```lua
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).

```lua
if multiplayer.isRoomCreator(key) ~= false then layers.load(scene) end
```

## typed/builtin//modules/api/engine/multiplayer/multiplayer/joinRoom {#typed-builtin-modules-api-engine-multiplayer-multiplayer-joinroom}

```lua
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.

```lua
multiplayer.joinRoom(worldGuid .. "/" .. engine.profile .. "/play/" .. sceneGuid)
```

## typed/builtin//modules/api/engine/multiplayer/multiplayer/leaveRoom {#typed-builtin-modules-api-engine-multiplayer-multiplayer-leaveroom}

```lua
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 {#typed-builtin-modules-api-engine-multiplayer-multiplayer-loopback}

```lua
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 {#typed-builtin-modules-api-engine-multiplayer-multiplayer-observe}

```lua
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.

```lua
local o = multiplayer.observe()
print(#o.entities .. " entities, " .. o.totals.properties .. " synced properties")
```

## typed/builtin//modules/api/engine/multiplayer/multiplayer/observeComponent {#typed-builtin-modules-api-engine-multiplayer-multiplayer-observecomponent}

```lua
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.

```lua
local c = multiplayer.observeComponent(player, "Health")
print(#c.properties .. " declared properties")
```

## typed/builtin//modules/api/engine/multiplayer/multiplayer/observeEntity {#typed-builtin-modules-api-engine-multiplayer-multiplayer-observeentity}

```lua
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.

```lua
local r = multiplayer.observeEntity(e)
for _, c in ipairs(r.components) do print(c.componentType) end
```

## typed/builtin//modules/api/engine/multiplayer/multiplayer/on {#typed-builtin-modules-api-engine-multiplayer-multiplayer-on}

```lua
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 {#typed-builtin-modules-api-engine-multiplayer-multiplayer-recordspawn}

```lua
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.

```lua
multiplayer.recordSpawn(id)
```

## typed/builtin//modules/api/engine/multiplayer/multiplayer/redo {#typed-builtin-modules-api-engine-multiplayer-multiplayer-redo}

```lua
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 {#typed-builtin-modules-api-engine-multiplayer-multiplayer-releaseownership}

```lua
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 {#typed-builtin-modules-api-engine-multiplayer-multiplayer-roomfor}

```lua
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.

```lua
local key = multiplayer.roomFor(player)
if key then multiplayer.joinRoom(key) end
```

## typed/builtin//modules/api/engine/multiplayer/multiplayer/send {#typed-builtin-modules-api-engine-multiplayer-multiplayer-send}

```lua
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 {#typed-builtin-modules-api-engine-multiplayer-multiplayer-synctotals}

```lua
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.

```lua
print(multiplayer.syncTotals().properties .. " synced properties registered")
```

## typed/builtin//modules/api/engine/multiplayer/multiplayer/undo {#typed-builtin-modules-api-engine-multiplayer-multiplayer-undo}

```lua
multiplayer.undo() -> boolean
```

Undo this client's last edit-mode operation.

**Returns** `boolean` — True if an operation was undone.
