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

camera_spawner

Generic primary-camera spawner. § 17 step 10 of `docs/plans/2026-05-01-player-camera-unification.md`.

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

camera_spawner

Generic primary-camera spawner. § 17 step 10 of docs/plans/2026-05-01-player-camera-unification.md.

Hooks layers.onLoad: on every non-additive scene load it spawns a Camera-bearing entity with behavior resolved through:

  1. sceneProxy.camera_default — per-scene override (future field)
  2. world.camera_default — world-level default
  3. Neither set: log.error + skip the spawn (no hardcoded fallback).

The spawned entity is temporary so layers.active:save() doesn't bake it. follow is set to the first Player entity in the scene at spawn time; later scripts can re-target by writing entity(camId).component.get("Camera").follow = someId.

Additive overlays do NOT spawn their own camera — they ride on the parent non-additive scene's camera.

Surface

SymbolNotes
M.install()Idempotent. Registers the layers.onLoad / onUnload hooks. Called once from the engine prelude.
M.ensure(sceneProxy) -> entity_id | nilManually spawn the primary camera for sceneProxy. Use from scene entrypoints that want to control spawn ordering. Returns nil when the camera was already in place.

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 camera_spawner deprecated v7 scenes author their own Camera entity. This procedural primary-camera spawner serves legacy v6 scenes only; M.ensure stands down (returns early) for any scene carrying a string `playerIntent`. Spawns the scene's primary Camera entity on non-additive `layers.onLoad`, for legacy v6 scenes. The per-mode world default (`world.camera_default_<mode>`) is the fallback; scene-level overrides via `sceneProxy.settings.camera` win when present. Missing refs → log.error + skip (no hardcoded fallback).

Camera

resolveBehaviorRef(sceneProxy: ?) → void

Resolve the behavior the scene's camera should use. Priority: 1. Per-scene override: `sceneProxy.settings.camera["behavior_<mode>"]` 2. World default: `world.camera_default_<mode>` 3. nil → log.error + caller skips the spawn (no hardcoded fallback). `engine.mode` drives the mode suffix ("edit" / "play"). Mode is engine state set at boot via the profile + flipped via `engine.mode = ...`. Returns `(behaviorRef, optOut)`. `optOut == true` means the scene OR world explicitly chose "no behavior" (empty string); the camera entity should still spawn, just without an automatic controller attached. `optOut == false` + `nil` means a misconfiguration (world default unset and no scene override); the spawn should be skipped and a diagnostic logged.

argtypedescription
sceneProxy?

ensure(sceneProxy: ?) → void

Spawn a Camera entity on the active scene. Returns the entity id of the spawned camera, or nil when one was already in place or when the scene opts out / ref is unresolved.

argtypedescription
sceneProxy?

resolveFollow( ) → void

Set follow to the avatar when available; otherwise fall back to the identity entity transiently. The avatar body may not be bound yet when M.ensure runs (it lands a few frames later), so the reconciler below rewrites follow once it does. Camera.follow is local-only; rewriting it doesn't replicate to peers. Resolve the follow target through the players surface, never by reaching into the identity component: `players.localPlayer` is the public handle and `.avatar` is the body's entity ref, nil until a live body is bound. Reading it there also dodges the "camera follows a dead entity" bug — the surface returns nil for an avatar id that a sync catch-up transiently projected but whose entity is already gone, so a stale id is never committed.

despawnLocal(layerName: ?) → void

Despawn the local `__primary_camera` entity owned by `layerName` (or all layers when layerName is nil), if it still exists. Primary cameras carry `temporary = true` and survive `__layers.clear()`; a non-additive scene load preserves the stale camera across the swap which is wrong (camera belonged to the previous scene context). Single-camera-per-client design: only the local camera is despawned. Other clients' cameras are not affected (each client owns its own primary camera; there are no "remote cameras" to worry about).

argtypedescription
layerName?

install( ) → void

Sub-parts

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

25items
·
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
◇
component · born here
❒asset
# Camera Manages viewport priority, render-to-texture, and capture. State is stored in the native `Camera` ECS component; the Rust camera system handles render scheduling and render targets. Public fields: `fov`, `near`, `far`, `priority`, `textureHandle` (the guid of the texture the camera renders into; empty = main viewport), `renderLayers` (which render layers this camera draws — a space-separated spec of names, e.g. `"all"`, `"all !ui"`, or `"default sky"`; `ui`/`sky`/`debug`/`EditorUI` are built-in layers), `postProcessing` (whether this camera runs the post-process chain), `debugChannel`, plus the behavior slot below. Methods: `:lookAt(target)` (entity id string OR `{x, y, z}` table), `:render()`, `:capture()`, `:setTargetTexture(tex?)` (a `renderer.texture.create` handle to render into, or nil for the viewport). ```luau entity(id).component.add("Camera", { fov = 90, priority = 10 }) entity(id).component.get("Camera"):capture() ``` The scene's play-mode camera is reached as `layers.active.camera`, a handle that reads and writes the fields above on whichever entity currently carries them. `layers.active.camera.entity` is the entity ref for that camera and `layers.active.camera.entityId` its id string, so a script that needs to attach something to the camera — an `AudioListener`, a child entity — goes through the ref: ```luau local cam = layers.active.camera cam.fov = 70 -- the camera's settings cam.entity.component.add("AudioListener") -- the entity carrying them ``` ## How the camera moves: `behavior` and `follow` A `Camera` does not move itself. `behavior` names a component that does, and setting it attaches that component to this entity. Clearing it detaches whatever was attached. ```luau local cam = entity(id).component.get("Camera") cam.behavior = asset.ref("@builtin::controller.orbital_follow", "component") cam.follow = playerBody ``` `follow` is the standard slot every shipped behavior reads. Set the follow target on the **Camera**, not on the behavior, so swapping behaviors keeps it. A behavior that finds its own `follow` field empty falls back to this one, which is what lets a rig keep tracking the player across a behavior swap. `followResolves` answers whether that slot names an entity that is live — `true` while it names a live one or names nothing at all, `false` once the target is despawned or the id names no entity. A rig whose target does not resolve holds its last pose, and this is the field that tells it from a rig posed correctly on a subject that has not moved. `camera.get` carries the same value beside `follow`, and a write naming an id with no entity behind it draws a warning where it lands. ```luau cam.followResolves -- false once the followed entity is gone tools.use("camera", "get", id).followResolves -- the same answer off the tool ``` The shipped behaviors live under `@builtin::controller.*`: `orbital_follow`, `third_person_follow`, `first_person`, `free`, `orbit`, `chase`, `isometric`, `rts`, `birds_eye`, `side_scroller`, `cinematic`, `menu`. ## Writing your own Any component can be a camera behavior. Write one that moves its own entity and attach it the same way: ```luau cam.behavior = asset.ref("MyChaseCam", "component") ``` `follow` lives on the Camera, so a behavior gets no property notification of its own when the target changes. Declare `onFollowChanged(newFollow, oldFollow)` to be told the moment it does — the Camera calls it on the component it attached, which is what lets a rig re-pose on the new subject at once. A behavior that reads `Camera.follow` on its own schedule declares nothing and is attached the same way. ```luau typed function public:onFollowChanged(newFollow: any, oldFollow: any) -- pose this entity against the new target end ``` To make it appear in the discovery catalog alongside the shipped ones, declare the `cameraBehavior` tag in the component's `.metadata`: ```json { "tags": ["cameraBehavior"] } ``` The tag governs **discovery**, not attachment. A tagged component is listed by `layers.active.camera.behaviors` and is what tooling offers when something asks "which camera behaviors exist"; an untagged component attaches just as well and simply stays out of that list. Tag the ones you want other people (and agents) to find. ```luau for name, ref in pairs(layers.active.camera.behaviors) do print(name, ref.identity) end ``` ## Texture colour space `textureColorSpace = "display"` applies the display transform when rendering into a texture. `"linear"` writes scene-linear values instead. `postProcessing` independently controls the effects chain in either mode. Floating-point targets retain values above one; normalized targets clamp to their representable range. The main viewport uses display encoding.
▲ 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
·
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
▣
module · born here
❒asset
# environment Module Environment / reflection capture — bake the scene into reflection-probe cube slots from world positions, persist them as `faces6` `.texture` assets, set per-probe blend data so surfaces reflect the probes covering them, and capture the sky into its own slot as the fallback under them. Public Luau surface over the `__environment` Internal FFI namespace, auto-injected as `_G.environment` via the prelude. ## Purpose The generic "render the scene into a cubemap from a point" capability the reflection-probe system is built on. Captures are queued for the render system (which owns the live scene); `captureSlotToAsset` additionally yields a few frames while the GPU readback completes. Persisted cubes are `faces6` `.texture` assets (px/nx/py/ny/pz/nz PNGs + a `cube.yaml` sidecar — see `docs/specs/cubemap-textures.md` §4 for the face convention). For probe authoring use the higher-level `reflectionProbe` module; reach for `environment` when you need the raw per-slot primitives. ## Usage ```luau -- Register probe blend data: index i maps to cube slot i. environment.setProbes({ { x = 0, y = 2, z = 0, radius = 12 } }) -- Bake slot 0 from a point (queued, next frame). environment.captureSlot(0, 0, 2, 0) -- Bake + persist to /source/probe_lobby.texture/ (yields; call from a -- task/coroutine/execute context). local path, err = environment.captureSlotToAsset("probe_lobby", 0, 0, 2, 0) -- Restore a persisted cube into a slot WITHOUT re-rendering. environment.loadSlotFromAsset("probe_lobby", 0) -- Capture the sky alone into the fallback slot: a surface no probe covers -- reflects the sky rather than black. environment.captureSky() ``` ## Exports - `environment.setProbes(probes) -> boolean` — set active probes' blend data; array of `{ x, y, z, radius, priority? }`, index i → cube slot i, gathered highest `priority` first - `environment.captureSky(x?, y?, z?) -> boolean` — render the sky alone into the fallback slot and arm it (queued) - `environment.setSkyFallback(active) -> boolean` — arm/disarm the fallback against the sky already captured (arming is refused while the slot holds none) - `environment.captureSlot(slot, x, y, z) -> boolean` — bake the scene into a slot from a point (queued) - `environment.captureSlotToAsset(name, slot, x, y, z, timeoutFrames?) -> (string?, string?)` — bake + persist as a `faces6` `.texture`; yields - `environment.loadSlotFromAsset(name, slot) -> (boolean, string?)` — upload a persisted cube into a slot without re-rendering Back-compat single-global-reflection helpers (slot 0 + one full-coverage probe): - `environment.capture(x, y, z) -> boolean` - `environment.captureToAsset(name, x, y, z) -> (string?, string?)` - `environment.loadFromAsset(name) -> (boolean, string?)`
▲ 0↑ born
▣
module · born here
❒asset
# transform Math helpers for positions, rotations, and directions on transforms. Exposed as the global `Transform` table via `--!global Transform` — no explicit require needed in user code. Functions that take an entity accept either an entity ID string or an entity proxy table from `entity("id")`. ## Exports Look-at and entity-aware helpers: - `Transform.lookAtQuat(fx, fy, fz, tx, ty, tz) -> (qx?, qy?, qz?, qw?)` — quaternion from origin toward target. Nil when degenerate. - `Transform.lookAt(entity, txOrTarget, ty?, tz?) -> (boolean, string?)` — make an entity face a world position or another entity. Both slots read world space: the subject and an entity target are read as `entity(id).position` and the aim is written as `entity(id).rotation`, so a parent under either one still leaves the aim on the point named. Returns whether the rotation was written, and the reason when it was not. - `Transform.distance(x1, y1, z1, x2, y2, z2) -> number` — Euclidean distance between two points. - `Transform.distanceBetween(entityA, entityB) -> number?` — distance between two entities' world positions. Nil when either is unresolvable. - `Transform.direction(fromX, fromY, fromZ, toX, toY, toZ) -> (dx, dy, dz)` — unit direction vector. - `Transform.directionBetween(entityA, entityB) -> (dx, dy, dz)` — unit world-space direction between two entities' world positions. Rotation shapes: A quaternion **constructor** here returns the four components as four separate values, so a caller either names them or braces the call to make one table: ```lua local qx, qy, qz, qw = Transform.quatFromAxisAngle(0, 1, 0, math.rad(90)) entity("cam").localRotation = { Transform.quatFromAxisAngle(0, 1, 0, math.rad(90)) } ``` A rotation-taking **surface** reads that table through `Transform.toQuaternion`, which also takes euler DEGREES — so `{ qx, qy, qz, qw }`, `{ x =, y =, z =, w = }`, `{ pitch, yaw, roll }` and `{ pitch =, yaw =, roll = }` all mean the same thing wherever a rotation is assigned: `entity(id).rotation` / `.localRotation`, `entityOps.spawn`, `entityOps.transform`, and the capture viewpoints. - `Transform.toQuaternion(rotation, label?) -> { qx, qy, qz, qw }` — the shared reading of a rotation a caller wrote. Raises when the value matches no form, naming what arrived; a value that is one of the shapes a quaternion helper returns is named as such along with the packing it goes in as. - `Transform.tryQuaternion(rotation, label?) -> ({ qx, qy, qz, qw } | nil, message?)` — the same reading without raising, for a surface that wants to raise the message at its own caller's line. - `Transform.readVec3(value, label?) -> { x, y, z }` — the same for a vector. - `Transform.snapVec3(v, step) -> { x, y, z }` — quantize a vector to a step grid. Quaternion construction / conversion: - `Transform.quatFromYaw(yaw)`, `Transform.quatFromYawPitch(yaw, pitch)`, `Transform.quatFromAxisAngle(ax, ay, az, angle)` — quaternion constructors. - `Transform.quatIdentity()` — identity quaternion. - `Transform.euler(qx, qy, qz, qw) -> (yaw, pitch, roll)` and the named alias `Transform.quatToEuler`. - `Transform.eulerToQuat(yaw, pitch?, roll?)` — euler-to-quaternion in YXZ order. Lerps and interpolation: - `Transform.lerp(ax, ay, az, bx, by, bz, t) -> (x, y, z)` — vec3 lerp. - `Transform.lerp1(a, b, t) -> number` — scalar lerp. - `Transform.normalizeAngle(a) -> number` — wrap angle into `[-pi, pi]`. - `Transform.lerpAngle(a, b, t) -> number` — shortest-arc angle lerp. - `Transform.slerp(ax, ay, az, aw, bx, by, bz, bw, t) -> (qx, qy, qz, qw)` — quaternion slerp with shortest-path and near-parallel fallback. Quaternion operations: - `Transform.quatMul(...) -> (qx, qy, qz, qw)` — `qa * qb` composition. - `Transform.quatInverse(qx, qy, qz, qw) -> (qx, qy, qz, qw)` — inverse (= conjugate for unit quats). - `Transform.quatRotateVec(qx, qy, qz, qw, vx, vy, vz) -> (x, y, z)` — rotate a vec3 by a quaternion. Pose helpers: - `Transform.orbit(centerX, centerY, centerZ, radius, height, angle) -> (x, y, z, qx, qy, qz, qw)` — orbital pose facing the center. - `Transform.worldToLocal(...)` / `Transform.localToWorld(...)` — pose-space conversions. Nested `Transform.vec.*` namespace (component-wise vec3): - `Transform.vec.add`, `sub`, `scale`, `dot`, `cross`, `length`, `normalize`. Types: - `Vec3 = { x: number, y: number, z: number }` - `EntityRef = string | { entityId: string }` ## Usage ```luau -- Look-at by coordinates or by target entity: Transform.lookAt("cam", 0, 1, 0) -- an entity target resolves to that entity's world position local aimed, why = Transform.lookAt("cam", "box") -- Orbit pose around a point: local x, y, z, qx, qy, qz, qw = Transform.orbit(0, 1, 0, 5, 2, t) entity.find("cam").localPosition = { x, y, z } entity.find("cam").localRotation = { qx, qy, qz, qw } -- Quaternion math: local qx, qy, qz, qw = Transform.quatFromYawPitch(math.pi / 4, 0) local sx, sy, sz, sw = Transform.slerp(0, 0, 0, 1, qx, qy, qz, qw, 0.5) -- Component-wise vec3 helpers: local nx, ny, nz = Transform.vec.normalize(1, 1, 0) ``` ## Notes - The `--!global Transform` directive promotes the module's typed functions onto the runtime universe's globals bucket, so `Transform.*` is available without any per-source `require`. - Entity-aware functions (`lookAt`, `distanceBetween`, `directionBetween`) report a missing entity or a missing transform in their return value rather than raising: `lookAt` answers `false, "unresolved"` / `"no-transform"` / `"incomplete-target"` / `"degenerate"`, `distanceBetween` answers `nil`, and `directionBetween` answers zeros. - Quaternion APIs operate on raw `(qx, qy, qz, qw)` tuples for parity with the entity proxy's `localRotation.get`/`set`. Use `Transform.quatIdentity()` rather than hand-rolling `(0, 0, 0, 1)`. - `Transform.slerp` flips the second quaternion if `dot < 0` to take the shortest path, and falls back to lerp+normalize when the inputs are within `dot > 0.9995` to avoid `1/0` near-parallel issues.
▲ 0↑ born
·
other · born here
▤file
▲ 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.