Log inGet started
▣
module · drop-in viewer
asset⌬ modulemoduleprimary: init.luau·originates fromworld 07158574-5…

players

The curated player surface. Wraps the raw `UserIdentity` component proxy in a view that exposes only the safe authoring API: `.isLocal` / `.avatar` / `.ready` / `.userId` / `.identity` / `.displayName`.

byzero-proxy @ DESKTOP-DB3UJOJ·posted 2mo ago
What it does

modules.api.engine.players

The curated player surface. Wraps the raw UserIdentity component proxy in a view that exposes only the safe authoring API: .isLocal / .avatar / .ready / .userId / .identity / .displayName.

.avatar is the visible body's entity REF (a live proxy), or nil until a body is bound — act on it directly (player.avatar.position = { x, y, z }). Want the id? player.avatar.id. Assign a live entity ref to bind it as the body — player.avatar = entityRef (assign nil to clear) is the one path that binds a body to the player.

.entity / .entityId / .id / .component are intentionally REFUSED. The identity entity is a internal anchor with no world presence — reach the visible body via player.avatar. Position writes always target the avatar.

Used by every layers.active.players.localPlayer / onLocalReady / playerJoined / playerLeft call site so user scripts can't reach past the curated wrapper into the underlying component proxy.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

module players Per-scene players registry. Tied to a non-additive Scene proxy. Reached as `layers.active.players` or `layers.find(name).players`. Returns curated player handles — never entity or component proxies — so the caller reads/writes player data (`.userId`, `.displayName`) and reaches the body via `player.avatar` (an entity ref) uniformly across all consumers. Both colon (`p:onJoin(cb)`) and dot (`p.onJoin(cb)`) call styles are supported on every method; the registry is a namespace surface, not an object, so neither style is canonical. Additive layers do NOT carry a players surface today (multiplayer support for players living inside additive overlays is a follow-up — see the layers.module additive-scene comment). `layers.find(<additive>).players`

localPlayerEntityId( ) → void

Resolve THIS client's local identity entity id from the `player_entity_id` world arg, which the engine publishes as soon as the local identity is spawned — available in the bootstrap window before the UserIdentity component has attached. Identities are internal (no 3D world presence) and self-register into the scene registry, so this reads the engine-published arg and never searches the world; it returns nil until the arg lands. This resolver is ROOM-scoped ("which identity entity is mine in this session") and deliberately does NOT consult `world.connectedUsers` — that is the WORLD-level identity surface, and its `user.entity` resolves an avatar by walking BACK through this registry, so reading it here would be a cycle. This chain is the canonical answer to "what's MY identity entity?" across every caller — never duplicate it in caller code.

resolvePlayerComponent(eid: ?) → void

argtypedescription
eid?

avatarRef(avatarId: ?) → void

Resolve an avatar entity-id string to its entity REF (proxy), or nil when no avatar is bound or the entity isn't live. `player.avatar` hands back this ref — never a bare id — so callers act on the body directly (`player.avatar.position = ...`) the way every other entity-returning API does (refs, not strings).

argtypedescription
avatarId?

liveIdentityEntities( ) → void

entitiesRevision( ) → number

The engine's monotonic entity-snapshot counter, bumped on every spawn, despawn, and component change. Reading it costs a fraction of a microsecond against several for an enumeration, so it gates the reconcile below down to one integer compare while the world holds still. nil when the counter is unavailable, which makes the reconcile run on each read.

wrapPlayer(comp: ?) → void

argtypedescription
comp?

isLocal( ) → void

__index(_: ?, k: ?) → void

argtypedescription
_?
k?

__newindex(_: ?, k: ?, v: ?) → void

argtypedescription
_?
k?
v?

new(sceneProxy: ?) → void

Per-instance state and callback registries. One block per non-additive Scene proxy, built by `Players.new(sceneProxy)` and stored on the proxy by layers.module.

argtypedescription
sceneProxy?

pushCb(reg: ?, fn: ?) → void

argtypedescription
reg?
fn?

removeCb(reg: ?, h: ?) → void

argtypedescription
reg?
h?

fireCb(reg: ?, ...: ?) → void

Fire every subscriber under pcall so one buggy listener can't block the rest. Errors land in the log as warnings, keyed by subscriber handle for traceability.

argtypedescription
reg?
...?

listRegistered( ) → void

Snapshot the registered players (this room's connected players) into an array of curated player handles. Reads the IDENTITY REGISTRY, never a scene query — the identity entity is internal and unfindable by design. Stale entries (entity gone without a clean `playerLeft`, e.g. an abrupt relay despawn) are skipped AND pruned so the registry self-heals.

list( ) → void

get(key: ?) → void

`get(key)` resolves a player by EITHER its account userId (the natural, exposed key) OR its internal identity entity id (internal callers). Scans the identity registry — only registered (connected) players resolve.

argtypedescription
key?

ownerOf(avatar: ?) → void

`ownerOf(avatar)` — the connected player whose body is this avatar. Accepts the avatar entity REF (the normal `player.avatar` value) or its id string. Resolves through the synced `UserIdentity.avatar` tie: scan the registry for the player pointing at this avatar. Works on every peer because the identity entities replicate into the room. nil when the avatar belongs to no registered player.

argtypedescription
avatar?

onLocalReady(cb: ?) → void

argtypedescription
cb?

onPlayerJoined(cb: ?) → void

argtypedescription
cb?

onPlayerLeft(cb: ?) → void

argtypedescription
cb?

offLocalReady(h: ?) → void

argtypedescription
h?

offPlayerJoined(h: ?) → void

argtypedescription
h?

offPlayerLeft(h: ?) → void

argtypedescription
h?

count( ) → void

Short-form aliases. The getting-started doc + the test suite use the shorter names; the longer names stay for back-compat. Both shapes call the same registry, so subscribers added via either name fire together.

exists(id: ?) → void

argtypedescription
id?

onJoin(cb: ?) → void

argtypedescription
cb?

offJoin(h: ?) → void

argtypedescription
h?

onLeave(cb: ?) → void

argtypedescription
cb?

offLeave(h: ?) → void

argtypedescription
h?

_addPlayer(eid: ?) → void

argtypedescription
eid?

_removePlayer(eid: ?) → void

argtypedescription
eid?

reconcile( ) → void

fireLocalReadyOnce(eid: ?) → void

Latch-controlled local-ready fan-out. Routed through two distinct entry points so the trigger conditions are explicit: _markLocalAvatarOptOut(): scene declared `avatar_<mode> = ""`, so the player_spawner won't bind an avatar; fire onLocalReady as soon as the UserIdentity component is attached. _onLocalAvatarBound(eid): the player_spawner finished binding an avatar to the local identity; fire onLocalReady now. Both call this single fan-out which is idempotent — the latch protects against double-fires when an avatar rebind happens after the initial ready.

argtypedescription
eid?

_markLocalAvatarOptOut(eid: ?) → void

argtypedescription
eid?

_onLocalAvatarBound(eid: ?, avatarEid: ?) → void

argtypedescription
eid?
avatarEid?

_fireLocalReady(eid: ?) → void

Legacy callout retained because player_lifecycle still routes through it at the identity's awake time. The latch keeps the fire from happening too early (avatar not yet bound) — callers should prefer the explicit `_onLocalAvatarBound` / `_markLocalAvatarOptOut` triggers above. Kept as a no-op-when-not-ready stub for API compatibility.

argtypedescription
eid?

localRegisteredEntityId( ) → void

Resolve the local player's internal entity id through the IDENTITY REGISTRY. The local player is the identity THIS session OWNS — an `isLocal` identity — so ownership is the primary key: it uniquely names this session's identity even when several sessions share an account (identical `userId`), where a userId match alone is ambiguous and could return a remote peer's identity. The userId match is the fallback for the brief window before `isLocal` has resolved on a fresh join; the module-level `localPlayerEntityId` is the last resort before the registry entry has landed.

__index(_: ?, k: ?) → void

argtypedescription
_?
k?

__newindex(_: ?, k: ?, _: ?) → void

argtypedescription
_?
k?
_?

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

19items
◇
component · born here
❒asset
# Player Identity tag for a player entity. Attaches the Rust `PlayerOwned` marker via `__native` in `awake` and removes it in `onDestroy`. No public state, no update loop, no visuals. ```luau entity(id).component.add("UserIdentity") entity(id).component.has("UserIdentity") -- true ``` Future rules (not yet enforced): - **Non-serialising.** Scene and world saves omit this component. Players are attached at runtime per peer, not inherited from the save file. - **Non-removable.** Once an entity is a player, removing this component is a no-op. A player stays a player for its lifetime outside an explicit identity swap. Visuals, cameras, input, and movement live in separate components (e.g. `PlayerController`, `PlayerTemplate` bundle content) so adding or removing them never affects identity.
▲ 0↑ born
·
other · born here
▤file
▲ 0↑ born
◇
component · born here
❒asset
▲ 0↑ born
▣
module · born here
❒asset
# modules.api.engine.player_lifecycle Wires the UserIdentity component's avatar-bind events into the `localPlayerReady` lifecycle hook. Owns the local-avatar-bound latch (fires once after the avatar entity is bound + has settled past the bundle.instantiate deferred-mutation pipeline) and the opt-out path for legacy v6 scenes that declare the avatar slot as `""`. Exposes the `__layers_local_avatar_bound` + `__layers_local_avatar_opt_out` dispatch channels wired in `install()`; the UserIdentity component's lifecycle and the legacy v6 player_spawner route through them.
▲ 0↑ born
·
other · born here
▤file
▲ 0↑ born
◇
component · born here
❒asset
# PlayerNameLabel Drives this entity's `Text3D` with the owning player's name, read from the nearest ancestor `PlayerAvatar`'s synced `displayName` so every peer renders the same name without a local registry lookup. The label re-rasterises when the display name changes, and in play it hides itself on the local player's own avatar — name labels exist so other players are identifiable. An unowned avatar reads a placeholder. Location: `src/lua/lib/components/PlayerNameLabel.component`
▲ 0↑ born
◇
component · born here
❒asset
# Text3D Renders text in 3D world space. The quad auto-sizes to fit the text content and never clips. Positioning works like TextMeshPro: `offsetX/Y/Z` is a local offset from the entity, `pivotX/pivotY` is the anchor within the quad, and `billboard` makes it face the camera. With `billboard = false` the text reads from the entity's own forward, the local -Z that `transform.forward` reports and that `entity:lookAt` aims. Sizing — three fields, two jobs: - `worldHeight` — the quad's height in **world units** (default `1.0`). This is the one field that resizes a label, and it holds that height under every transform between the label and the world: a label hung off a shrunken detail box on a model comes out the size it asked for, and so does one whose own entity carries a `localScale`. Width follows the rasterised text's aspect. Measure the result with `getWorldSize()`, which reports the extent the quad is drawn at. - `fontSize` — the **raster resolution** in texels (default `32`). Higher values sharpen the texture and change wrapping against `maxWidth`; the world-space size stays `worldHeight`. - `scale` — rasterisation scale multiplier applied at raster time. Resolution only, like `fontSize`. `maxWidth` is the width the text wraps at, in pixels of the raster `fontSize` states; `0` keeps it on one line. A label already drawn is laid out again when the field is written, so a caller re-wraps one label to line after line rather than making a label per line. `alphaCutoff` decides whether the letters are a surface. At `0` (the default) the text is pure alpha blending: it draws over what is behind it and leaves the depth buffer alone, so a screen-space effect that reads scene depth — volumetric fog, screen-space shadows — integrates the whole distance behind the letters and the text sits inside it. Above `0`, coverage at or over the threshold is drawn opaque and written to depth, so those effects stop at the glyph shape instead. `0.5` reads well for most fonts; higher thins the letters, lower keeps more of the antialiased edge. Public fields: `content`, `fontSize`, `color`, `alignment`, `richText`, `maxWidth`, `outline`, `outlineColor`, `background`, `scale`, `worldHeight`, `alphaCutoff`, `shadowX/Y`, `fontFamily`, `weight`, `slant`, `offsetX/Y/Z`, `pivotX/Y`, `billboard`. `weight` and `slant` are strings (`"regular"` / `"bold"`, `"normal"` / `"italic"`). Methods: `setText(content)`, `setStyle(options)`, `getText()`, `getSize()`, `getWorldSize()`, `refresh()`. ```luau entity(id).component.add("Text3D", { content = "Hello World" }) entity(id).component.add("Text3D", { content = "HP: 100", worldHeight = 0.5, fontSize = 96, color = "red", offsetY = 2.0, pivotY = 0 }) -- A title that keeps its letters crisp through volumetric fog. entity(id).component.add("Text3D", { content = "RAISING", worldHeight = 3.0, alphaCutoff = 0.5 }) ``` A label that is not showing, or came out in a face you did not ask for, reads back out of the text system: `text.observe()` lists every live text object with the entity that owns it and the texture its raster is in, and `text.face(h)` names the font face the shaper actually used against the `fontFamily` that was requested. `topics/text` walks both.
▲ 0↑ born
·
other · born here
▤file
▲ 0↑ born

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported. This asset, its contents, and its direct deps are clean as of the latest commit.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.