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