Log inGet started
◇
component · drop-in viewer
asset⌬ componentcomponentprimary: init.luau·originates fromworld 07158574-5…

UserIdentity

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.

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

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.

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.

Interface

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

conforms to

zero/source-extract/v2

UserIdentity The per-user identity anchor. A hidden, registry-only entity (named "user") with no world presence: never rendered, never parented to anything, never a parent, never surfaced by entity queries or the inspector's default view. It exists only so a connected user has a stable, synced handle in the room. It owns exactly two things: 1. `avatar` — a synced string entity id pointing at the user's BODY, a live world entity carrying the visual, controller, and transform. This is the identity → body pointer; the body carries the reverse link (`PlayerAvatar.owner` = this identity's userId). 2. `userId` / `displayName` — the connected user's account identity, synced so every peer resolves the real user behind the body. The body is a standalone entity at the scene root, connected to the identity only by its `PlayerAvatar` component. Everything spatial — movement, camera-follow, raycasts — targets the body, never the identity, which has no transform of its own. ONE path binds the body: assign a live entity to the player's avatar — `layers.active.players.localPlayer.avatar = entityRef` (a `.bundle` / `.avatar` asset is not a world entity, so it is never accepted here; spawn it into the world first, then bind the resulting entity). The bind runs in `onPropertyChanged` on the LOCAL identity: it marks the body synced + `PlayerOwned`, attaches `PlayerAvatar.owner`, and tears down the previously-bound body. A remote identity receives its `avatar` field and its body over sync, so its `onPropertyChanged` only drives the readiness latch — the local bind never runs for a body this peer does not own.

PlayerAvatar

writePlayerAvatar(avatarEid: ?, ownerIdentity: ?) → void

The avatar carries a first-class `PlayerAvatar` component whose `owner` field is the owning user identity id (the account id). It is the visible, inspector-facing link from the body back to who owns it — the identity is registry-only and is never a parent, so a proper component link (not a hierarchy walk, not a hidden attribute) is how scripts find the owner. They read `PlayerAvatar.owner` and resolve the live player via the registry.

argtypedescription
avatarEid?
ownerIdentity?

clearPlayerAvatar(avatarEid: ?) → void

argtypedescription
avatarEid?

despawnTree(eid: ?) → void

Walk an avatar's entity tree post-order and despawn from the leaves up. `entity.despawn` does not cascade — without this descendant entities become orphans with detached scripts still ticking.

argtypedescription
eid?

markPlayerOwned(eid: ?) → void

Attach the Rust `PlayerOwned` marker to the AVATAR (body) entity. The identity entity carries its own `PlayerOwned` marker (inserted in awake), but the body is a standalone entity at the scene root — not under the identity in the hierarchy — so the sync layer's `root_carries_player_owned()` parent-walk can never reach the body through the identity. The body must carry the marker itself, or the edit-mode relay gate (and the broadcast filter) skip it and the body never replicates. The slot setter/teardown own the body's copy.

argtypedescription
eid?

clearPlayerOwnedMarker(eid: ?) → void

Drop the avatar's `PlayerOwned` marker when the slot clears (counterpart to markPlayerOwned). Id-first form — the avatar is a different entity than the identity component's host, so the implicit-context remove would target the wrong entity.

argtypedescription
eid?

teardownAvatar(avatarId: ?) → void

Tear down the body currently bound to the avatar slot: drop its `PlayerAvatar` link + `PlayerOwned` marker, then despawn it. The identity owns its body's lifecycle — on rebind the previous body is despawned, and on the identity's onDestroy the current body is despawned, so a body is never left orphaned in the scene tree with no way to reach it through the registry.

argtypedescription
avatarId?

markSynced(eid: ?) → void

Flip a single entity to multiplayer-synced at the root. Idempotent. Used on the identity entity in awake and on a bound body. The body root replicates; its Asset-spawned visual (mesh, skeleton, bones) is a per-peer local reconstruction driven by the synced `Asset.source` ref, which each peer's `Asset.awake` rebuilds.

argtypedescription
eid?

markSessionScoped(eid: ?) → void

Flip an entity to session-scoped: it dies with its owning peer's relay session. The relay despawns session-scoped entities owned by a peer when that peer disconnects (broadcasting EntityDespawn to the remaining peers so they clean up too) and omits them from the snapshot sent to new joiners. User identity entities are pure per-session identity — without this they are non-session-scoped synced entities the relay keeps after their owner leaves and re-snapshots to every future joiner, which is exactly how ghost identities accumulate across reconnects. `entity(id):setSessionScoped(true)` is idempotent and also tags the entity SyncEntity, so it is safe alongside `markSynced`.

argtypedescription
eid?

awake( ) → void

onDestroy( ) → void

onPropertyChanged(key: ?, value: ?, oldValue: ?) → void

The one path that binds a body to this identity. Assigning the `avatar` field a live entity id runs here — via the public surface `layers.active.players.localPlayer.avatar = entityRef`, or the authored spawn adopting its clone body. On the LOCAL identity it despawns the previously- bound body (rebind or clear), then marks the new body synced + `PlayerOwned` and attaches its `PlayerAvatar.owner` back-link, and drives the per-scene `players:onLocalReady` latch — the first moment body-touching game code can run (awake is too early, no body is bound yet). A REMOTE identity receives its avatar field and body over sync, already marked by the owning peer, so it takes none of these actions.

argtypedescription
key?
value?
oldValue?

onAssetReload(_field: ?) → void

Asset-content reload (any VFS write under the body's directory). The slot's stored value is an entity id; the body's content is reloaded via the bundle.instantiate machinery elsewhere, and the slot value survives.

argtypedescription
_field?

Sub-parts

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

16items
·
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.