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

player_spawner

Legacy v6 default-identity avatar setup. v7 scenes place players through the PlayerSpawn / PlayerPrototype flow; this module stands down (`M.ensure` returns early) for any scene carrying a string `playerIntent`.

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

player_spawner

Legacy v6 default-identity avatar setup. v7 scenes place players through the PlayerSpawn / PlayerPrototype flow; this module stands down (M.ensure returns early) for any scene carrying a string playerIntent.

Hooks layers.onLoad: on every non-additive v6 scene load it locates the ensure_default_player-spawned identity entity and binds its avatar:

  • world.avatar_default_<mode> (per-mode world default). The avatar bundle combines visual + controller in one unit (mesh + skeleton + Locomotion + MovementState + CharacterController).
  • Per-scene settings.player.avatar_<mode> overrides the world default when set; "" is the explicit opt-out (the scene builds its own body in entrypoint.luau::onLocalReady).

It spawns a fresh body entity from the bundle, then binds it by assigning the identity's avatar field, which marks the body synced + PlayerOwned and attaches its PlayerAvatar link.

Idempotent per layer: reload doesn't re-instantiate; onUnload clears the per-layer "already applied" flag.

Surface

SymbolNotes
M.install()Idempotent. Registers layers.onLoad / onUnload hooks. Called once from the engine prelude.
M.ensure(sceneProxy) -> entity_id | nilApply the avatar bundle to the identity entity. Returns the identity id, or nil when none exists yet (or the scene is v7).

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 player_spawner deprecated v7 scenes place players through the PlayerSpawn / PlayerPrototype flow. This engine-default-Player avatar path serves legacy v6 scenes only; M.ensure stands down (returns early) for any scene carrying a string `playerIntent`. Binds the per-mode avatar bundle to the engine-default identity entity on non-additive `layers.onLoad`, for legacy v6 scenes. The avatar ref resolves as: per-scene `settings.player.avatar_<mode>` when set, else the world default `world.avatar_default_<mode>`. Missing-but-required is `log.error` + skip — no hardcoded fallback bundle. A single `avatar` slot combines visual + controller in one bundle. It spawns a fresh body entity from that bundle, then binds it by assigning the identity's `avatar` field, which marks the body synced + PlayerOwned and attaches its `PlayerAvatar` link. Scene-level `avatar_<mode> = ""` is the explicit opt-out — the scene builds its own body in `entrypoint.luau::onLocalReady`.

AssetUserIdentity

resolveWorldDefault( ) → void

Resolve the avatar ref for the current mode. Reads `world.avatar_default_<mode>` from the world-defaults metatable, which proxies into `.world_settings`. Returns `(ref, optOut)`: ref = AssetRef envelope, optOut = false → use this ref ref = nil, optOut = true → explicit "" in TOML ref = nil, optOut = false → unset key (log + skip)

resolveAvatar(sceneCfg: ?) → void

Per-scene override beats world default. Scene-level field name is `avatar_<mode>`. Three states: AssetRef envelope → use this ref "" (empty string) → explicit opt-out; scripts own the avatar slot nil → inherit world default

argtypedescription
sceneCfg?

refToIdentity(ref: ?) → void

Asset ref normaliser. `bundle.instantiate` (and the avatar setter) wants an AssetRef envelope; we may receive either an envelope or a bare identity / path string from world defaults or scene overrides.

argtypedescription
ref?

asAssetRef(maybeRef: ?) → void

argtypedescription
maybeRef?

spawnBodyFromRef(ref: ?) → void

Spawn a live body entity from an avatar / bundle ref, ready to bind. An `avatar` asset instantiates its composed body + movement kit; a bare `bundle` gets a generic `Asset` component that rebuilds it locally on every peer. The body is a fresh per-session instance (temporary) and session-scoped so the relay despawns it with its owning peer rather than keeping a ghost. Binding it via `identity.avatar = bodyId` marks it synced + PlayerOwned and attaches the PlayerAvatar link. Returns the body entity id, or nil on failure.

argtypedescription
ref?

localPlayerEntityId(sceneProxy: ?) → void

Resolve THIS client's local-player entity id via the canonical API on the active layer's players proxy. The proxy's `localPlayerEntityId` (defined on modules.api.engine.players via __index) runs the same chain (world arg → world.connectedUsers.localUser → findAll+isLocal → single-player fallback). When called from M.ensure(sceneProxy) prefer the passed proxy; when called from M.despawnLocal() (no proxy in scope) fall back to layers.active. The two-source lookup means we never reach for `require("...api.engine.players")` directly.

argtypedescription
sceneProxy?

ensure(sceneProxy: ?) → void

Apply the avatar bundle to the active scene's identity entity. Returns the identity entity id or nil when no identity entity is available.

argtypedescription
sceneProxy?

despawnLocal( ) → void

Despawn THIS client's local identity entity, if it exists. The UserIdentity component's `onDestroy` handler tears down the avatar (despawning the spawned tree) before the entity itself is removed; sync propagates the despawn to peers.

install( ) → void

activeRootGuid( ) → void

localReady( ) → void

Wait for the fresh local player to self-register into the root scene registry before re-binding M.ensure — identities are internal and never appear in a 3D-world query; the registry is the only source.

Sub-parts

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

49items
·
other · born here
▤file
▲ 0↑ born
▣
module · born here
❒asset
# settings World-settings reader and writer for `/zero/source/.world_settings`. The single canonical surface for any script that needs to read or mutate engine settings (renderer culling, physics gravity, LSP strictness, startup scene, etc.). Auto-injected as the global `settings` by the prelude — user code never needs to `require` this module. Settings live in a TOML file inside the world's manifest. Reads always re-parse the file so callers see the live state (no stale cache); writes go through `vfs.write`, so play mode locks settings the same as any other source file. Call `wld.edit()` first to unlock for mid-play writes. ## Exports - `settings.get(key: string) -> any` — raw value at a dotted key, or `nil`. - `settings.getString(key: string, default?: string) -> string` — type-narrowed string accessor. - `settings.getNumber(key: string, default?: number) -> number` — type-narrowed number accessor. - `settings.getBool(key: string, default?: boolean) -> boolean` — type-narrowed boolean accessor. - `settings.set(key: string, value: any)` — set + write. - `settings.setMany(updates: { [string]: any })` — batched set + single write. - `settings.all() -> { [string]: any }` — snapshot of the full settings document. ## Usage ```luau -- Read local mode = settings.getString("render.culling_mode", "gpu") local gravity = settings.getNumber("physics.gravity", -9.81) if settings.getBool("render.shadows", true) then ... end -- Write settings.set("render.culling_mode", "cpu") settings.setMany({ ["render.culling_mode"] = "cpu", ["physics.gravity"] = -3.7, }) -- Inspect everything for section, keys in pairs(settings.all()) do print("[" .. section .. "]") for k, v in pairs(keys) do print(" " .. k .. " =", v) end end ``` ## Notes - The typed accessors (`getString` / `getNumber` / `getBool`) fall back to the documented default (`""` / `0` / `false`) on type mismatch — they never coerce. - Modifying the snapshot returned by `settings.all()` does NOT propagate. Persist changes with `set` or `setMany`. - Each `get*` re-reads the file. Settings access is infrequent enough that the parse cost is negligible; the trade-off is no stale-cache class of bug from foreign writes.
▲ 0↑ born
▣
module · born here
❒asset
# mode_flip_guard Two tiny cross-module transient signals about the edit↔play mode flip: - **owned** — "the `layers` module currently owns the mode-flip reset, so the `player_spawner` / `camera_spawner` `onModeChange` watchers should stand down." - **in flight** — "a mode-flip transition is materialising the scene right now, so the live entities are a partial rebuild of it." Formerly `_G.__zero_layers_owns_mode_flip`. Moved off `_G` ahead of the read-only `_G` seal — the flags are runtime writes (set when `layers` drives a mode flip), which would break under a sealed `_G`. `require()` is cached per VM, so the module-local upvalues are shared state across every requirer within a VM — exactly the cross-module reach the old `_G` key provided. - **Setter**: `layers.module` claims ownership before any flip, and raises the in-flight signal for the span of the transition it runs. - **Readers**: `player_spawner.module`, `camera_spawner.module` stand down while owned so the player/camera respawn happens exactly once via the `layers` transition's reload fan-out (not a second time from their own `onModeChange` watchers); `playerSetupValidation.module` judges the authored scene once the transition has settled. ## Surface | Symbol | Notes | |---|---| | `M.setOwned(v: boolean)` | Set whether the layers module owns the current mode-flip reset. | | `M.isOwned() -> boolean` | True while the layers transition owns the flip; spawners stand down. | | `M.setInFlight(v: boolean)` | Set whether a mode-flip transition is materialising the scene. | | `M.isInFlight() -> boolean` | True for the span of the transition; the live entities are a partial rebuild of the scene. |
▲ 0↑ born
◇
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
◇
component · born here
❒asset
# Asset Promote the bundle's spawned children from temporary to permanent scene state and remove the Asset component, "baking" the bundle's contents directly into the owning scene. Subsequent saves persist the baked entities verbatim and stop replaying the bundle template. Location: `src/lua/lib/components/Asset.component`
▲ 0↑ born
·
other · born here
▤file
▲ 0↑ born
·
other · born here
▤file
▲ 0↑ born
·
other · born here
▤file
▲ 0↑ born
▣
module · born here
❒asset
# toml Pure-Luau TOML parser and emitter. Use whenever a script needs to read or write a TOML file — settings, importer rules, tool configs, or any other authored config the user touches by hand. Auto-injected as the global `toml` by the prelude — no `require` in user code. `vfs.read` returns bytes. JSON has built-in parsing via `json`, but TOML — the format used for `.world_settings`, `pyproject`-style configs, and any human-friendly key=value file — needs a parser. `toml` provides one with no native dependency, so it works the same on native and WASM. ## Exports - `toml.parse(src: string) -> { [string]: any }` — TOML bytes to nested Luau table. Throws with the line number on syntax errors. - `toml.encode(root: { [string]: any }) -> string` — Luau table to canonical TOML bytes (alphabetical section/key order; deterministic output). ## Usage ```luau -- Parse local body = vfs.read("/zero/source/myconfig.toml") local config = toml.parse(body) print(config.render.culling_mode) -- Mutate + write back config.render.culling_mode = "cpu" vfs.write("/zero/source/myconfig.toml", toml.encode(config)) ``` ## Supported TOML - Sections, including dotted (`[a.b.c]`) - Key/value pairs with dotted keys (`a.b.c = 1`) - Strings: `"..."` (escaped) and `'...'` (literal); triple-quoted variants for multi-line bodies - Integers (with `_` digit separators) and floats (incl. `inf`, `-inf`, `nan`, exponents) - Booleans (`true` / `false`) - Inline arrays (`[1, 2, 3]`) - Inline tables (`{ a = 1, b = 2 }`) - `#` comments ## Not implemented These are rare in settings/config files; add when a real call site needs them rather than carrying dead code. - Array-of-tables (`[[name]]`) - Hex / octal / binary integer literals (`0xff`, `0o77`, `0b1010`) - Date / time literals ## Notes - `--!global toml` directive promotes the module's typed functions onto the runtime universe's globals bucket, so `toml.parse` / `toml.encode` are available without any per-source `require`. - The encoder is fully deterministic: sections and keys are sorted alphabetically, integer-shaped numbers are emitted without a decimal point (`2` not `2.0`), and nested tables become dotted section headers (`[a.b]`). - Parser errors carry the line number for fast diagnosis.
▲ 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
▣
module · born here
❒asset
# component_snapshot Serialized snapshots of script-component public data. A component's `public` table is a live proxy — its fields live behind `__iter` / `__index` metamethods, so copying or JSON-encoding the proxy directly yields an empty table. This module materializes the plain, serializable form. It is the script-component parallel to `ecs.snapshot` (native components). ```lua local componentSnapshot = require("modules.component_snapshot") -- one component: live proxy -> plain table local data = componentSnapshot.snapshot(entity(id).component.get("Camera")) -- whole entity: { [type] = data }, named/multi instances nested as -- { [type] = { [instanceName or "__default"] = data } } local all = componentSnapshot.snapshotEntity(entity(id)) ``` `componentSnapshot.plainCopy(value)` materializes ONE live value the same way — a table behind a proxy is walked into a plain table, every other value passes through — for callers reading a single field rather than a whole component. Serialized form: entity refs become id strings, asset refs become `{ __ref, name, type }` envelopes, vectors and colors their plain-table forms; framework functions are omitted. `snapshot` returns nil for a proxy with no serializable fields. Persistence (scene serializer, bundle capture, prototype spawn) and inspection tooling (`debug.inspect`) read component data through this module. Live gameplay reads and writes stay on the proxy itself via `entity(id).component.get` / `getAll`.
▲ 0↑ born
▣
module · born here
❒asset
# json JSON encode/decode library for Luau. Encodes Lua values to JSON strings and decodes JSON strings back to Lua values. Used for communication with the Rust side of the engine, the VFS read/write bridge, and any wire-format that needs JSON. Pure Luau, no engine dependencies. Compact and pretty-printed encoders, plus a hand-rolled decoder that streams the input by position so it works under WASM as well as native. ## Exports - `Json.encode(value: any, indent?: string, currentIndent?: string) -> string` — compact encode. Functions / unknown types and NaN/Inf encode as `null`. - `Json.encodePretty(value: any, indentStr?: string) -> string` — pretty-printed encode with sorted object keys (diff-friendly). - `Json.encodeArgs(...: any) -> string` — encode varargs as a JSON array. - `Json.decode(str: string) -> any` — decode a JSON string. Returns the decoded value, or `nil` + error message on failure. ## Usage ```luau local Json = require("@builtin::modules.json") local widget = { type = "button", text = "Click Me" } local compact = Json.encode(widget) -- '{"text":"Click Me","type":"button"}' local pretty = Json.encodePretty(widget, " ") local decoded = Json.decode(compact) local v, err = Json.decode("oops") -- v = nil, err = error message ``` ## Notes - Object keys are sorted alphabetically in both encoders for consistent output across runs. - Numeric keys on objects are stringified at encode time (JSON has no numeric keys). Pure-integer key sets get detected as arrays via `isArray` and encoded with brackets. - NaN, +Inf, -Inf encode as `null` — JSON has no representation. Round trips through `decode` recover `null` (Lua `nil`), so they don't preserve. - Unicode `\uXXXX` escapes decode to UTF-8 by hand to stay WASM-safe. Only the BMP is covered; supplementary planes via surrogate pairs are not. - Functions encode as `null`. - Decode is character-streamed — no regex, no `string.match` patterns on the whole input — so the line-and-column information needs to be reconstructed from the position offset.
▲ 0↑ born
▣
module · born here
❒asset
# asset_instance_inspector The `Asset` component's custom entity-inspector view: an instance of an asset placed in the scene, read against the asset it came from. The generic field grid shows the component's `source`, `idMap` and `diff` — a reference and two opaque tables. This view shows what they mean instead: - a summary of how the instance stands — in sync with its asset, or how many parts are edited, added or removed in the scene; - the `source` field, with the reference chip's reveal / open / replace; - **Overrides** — every part that differs from the asset, with what changed (`position`, `name`, `Model tintR`, …) and a Revert (or Remove, for an entity added under the instance); - **Linked parts** — every part still as the asset has it; - Revert all to asset, and Unpack into scene. It reads the instance through the component's own `overrides()` and writes through `revert(part?)` and `bakeIntoScene()`, so the view holds no knowledge of how the diff is measured. `M.sections(entityId, proxy)` answers the view as plain data; `M.build(ctx)` renders it into the entity inspector's card with the Context's widgets, the `source` field going through `ctx.setSettings` as one undoable edit. ```luau local Inspector = require("@builtin::modules.editor.asset_instance_inspector") local sections = Inspector.sections(entityId, entity(entityId).component.get("Asset")) ``` The `Asset` component's `inspector.luau` hands `build` to the entity inspector as the card's `settings` section, for every entity carrying the component.
▲ 0↑ born
·
other · born here
▤file
▲ 0↑ born
·
other · born here
▤file
▲ 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
▣
module · born here
❒asset
# editorSelection Named selection scopes for the editor. A scope is an independently tracked, ordered set of typed refs plus a primary (the last ref added or clicked). Distinct scopes never clobber each other, so viewport, outliner, inspector and asset browser share one answer to "what is selected" per kind. A ref is `{ kind: string, id: string }` — a stable id, never a display string, so rename/move never invalidates a selection. ## Exports - `M.scope(name) -> Scope` — get/create a named scope handle. - `M.set(scope, refs)` — replace refs in order; primary becomes the last. - `M.get(scope) -> { Ref }` — refs in click order (fresh array). - `M.primary(scope) -> Ref?` — last-clicked ref, or nil. - `M.clear(scope)` — empty a scope. - `M.toggle(scope, ref)` — add if absent, remove if present. - `M.add(scope, refs)` — add each ref not already present; primary becomes the last added. - `M.remove(scope, refs)` — remove each of `refs`; primary becomes the last remaining or nil. - `M.contains(scope, ref) -> boolean` — membership by `(kind, id)`. - `M.count(scope) -> number` — the scope's selection size. - `M.subscribe(scope, fn) -> handle` / `M.unsubscribe(scope, handle) -> boolean`. - `M.context() -> { scope, refs, primary }` — last-focused scope, for command ctx. ## Usage ```luau local Selection = require("@builtin::modules.api.editor.selection") local scope = Selection.scope("entity") Selection.set(scope, { { kind = "entity", id = "player_1" } }) Selection.subscribe(scope, function() refreshInspector() end) ``` ## Notes - State lives in a fixed `_G` slot, seeded pre-seal by the boot chain (`prelude.luau`); it survives hot-reload and edit↔play flips. Mutations after boot write into nested tables only. - `set` / `toggle` / `clear` mark their scope as last-focused, which drives `context()` and therefore which scope commands act on. - The service holds the editor's selection state in Luau. Refs handed back by `get` / `primary` / `context` are copies, so a caller can hold or mutate them without touching internal state.
▲ 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
backing path · modules/api/engine/player_spawner.module

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.