❒
package · drop-in viewer
What it does
No description provided yet.
Interface
What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.
This asset doesn't declare a schema or structured payload.
Sub-parts
Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.
Showing the 199 items nearest this part. It holds more than this page lists.
199+items
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
# bookshelf
Voxel template `bookshelf` (demo asset). Live instances spawned via `Voxel.fromTemplate("bookshelf")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# bot_seated
Voxel template `bot_seated` (demo asset). Live instances spawned via `Voxel.fromTemplate("bot_seated")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# bot_standing
Voxel template `bot_standing` (demo asset). Live instances spawned via `Voxel.fromTemplate("bot_standing")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# cabinet
Voxel template `cabinet` (demo asset). Live instances spawned via `Voxel.fromTemplate("cabinet")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# chair
Voxel template `chair` (demo asset). Live instances spawned via `Voxel.fromTemplate("chair")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# clock
Voxel template `clock` (demo asset). Live instances spawned via `Voxel.fromTemplate("clock")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# desk
Voxel template `desk` (demo asset). Live instances spawned via `Voxel.fromTemplate("desk")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# keyboard
Voxel template `keyboard` (demo asset). Live instances spawned via `Voxel.fromTemplate("keyboard")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# lamp
Voxel template `lamp` (demo asset). Live instances spawned via `Voxel.fromTemplate("lamp")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# linked_workstation_baked
Voxel template `linked_workstation_baked` (demo asset). Live instances spawned via `Voxel.fromTemplate("linked_workstation_baked")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# monitor
Voxel template `monitor` (demo asset). Live instances spawned via `Voxel.fromTemplate("monitor")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# mug
Voxel template `mug` (demo asset). Live instances spawned via `Voxel.fromTemplate("mug")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# plant
Voxel template `plant` (demo asset). Live instances spawned via `Voxel.fromTemplate("plant")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# printer
Voxel template `printer` (demo asset). Live instances spawned via `Voxel.fromTemplate("printer")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# trash
Voxel template `trash` (demo asset). Live instances spawned via `Voxel.fromTemplate("trash")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# whiteboard
Voxel template `whiteboard` (demo asset). Live instances spawned via `Voxel.fromTemplate("whiteboard")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# blade
Voxel template `blade` (demo asset). Live instances spawned via `Voxel.fromTemplate("blade")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# house
Voxel template `house` (demo asset). Live instances spawned via `Voxel.fromTemplate("house")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# rock
Voxel template `rock` (demo asset). Live instances spawned via `Voxel.fromTemplate("rock")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# tree
Voxel template `tree` (demo asset). Live instances spawned via `Voxel.fromTemplate("tree")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# windmill_base
Voxel template `windmill_base` (demo asset). Live instances spawned via `Voxel.fromTemplate("windmill_base")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# cabin
Voxel template `cabin` (demo asset). Live instances spawned via `Voxel.fromTemplate("cabin")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# chassis
Voxel template `chassis` (demo asset). Live instances spawned via `Voxel.fromTemplate("chassis")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# door
Voxel template `door` (demo asset). Live instances spawned via `Voxel.fromTemplate("door")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# front_bumper
Voxel template `front_bumper` (demo asset). Live instances spawned via `Voxel.fromTemplate("front_bumper")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# headlight
Voxel template `headlight` (demo asset). Live instances spawned via `Voxel.fromTemplate("headlight")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# hood
Voxel template `hood` (demo asset). Live instances spawned via `Voxel.fromTemplate("hood")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# mirror
Voxel template `mirror` (demo asset). Live instances spawned via `Voxel.fromTemplate("mirror")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# rear_bumper
Voxel template `rear_bumper` (demo asset). Live instances spawned via `Voxel.fromTemplate("rear_bumper")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# rear_window
Voxel template `rear_window` (demo asset). Live instances spawned via `Voxel.fromTemplate("rear_window")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# seat
Voxel template `seat` (demo asset). Live instances spawned via `Voxel.fromTemplate("seat")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# spoiler
Voxel template `spoiler` (demo asset). Live instances spawned via `Voxel.fromTemplate("spoiler")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# steering_wheel
Voxel template `steering_wheel` (demo asset). Live instances spawned via `Voxel.fromTemplate("steering_wheel")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# taillight
Voxel template `taillight` (demo asset). Live instances spawned via `Voxel.fromTemplate("taillight")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# trunk
Voxel template `trunk` (demo asset). Live instances spawned via `Voxel.fromTemplate("trunk")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# wheel
Voxel template `wheel` (demo asset). Live instances spawned via `Voxel.fromTemplate("wheel")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
# windshield
Voxel template `windshield` (demo asset). Live instances spawned via `Voxel.fromTemplate("windshield")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
# greedy_mesh
Turns a voxel chunk into a mesh, merging coplanar faces of one colour into the
largest rectangle that covers them so a solid wall costs two triangles instead
of two per voxel. One invocation per slice; the vertex and index counts are
claimed atomically, so the pass writes both buffers in a single dispatch.
## Bindings
| name | kind | what it is |
|------|------|------------|
| `params` | `buffer` (read, u32) | chunk size, padding and the origin the mesh is built around |
| `solid_padded` | `buffer` (read, u32) | the chunk's occupancy bitmask, one voxel of padding on every side so a face test at the edge reads a real neighbour |
| `colors` | `buffer` (read, u32) | one packed colour per voxel |
| `vertex_buf` | `buffer` (read_write, u32) | the vertices produced, in the engine's vertex layout |
| `index_buf` | `buffer` (read_write, u32) | the indices produced |
| `counters` | `buffer` (read_write, atomic u32) | how many vertices and indices have been claimed |
## Params
| name | type | what it is |
|------|------|------------|
| `vertStride` | `u32` | one vertex in u32 words, so the writes follow the engine's vertex layout |
▲ 0↑ born
# voxelConfig
Read and write the voxel engine's configuration — the path defaults and engine knobs the voxel system runs on (chunk size, default block size, default mesher, CPU fallback, play-mode write permission, and the path defaults).
Get a value by key, get the whole merged config, set a runtime override, or reset one or all overrides. Values resolve in order: a runtime override wins, then the world's voxel settings, then the built-in defaults.
▲ 0↑ born
# voxelEngine
Build and edit voxel worlds — spawn worlds and shapes, mutate blocks, query them, transform them, manage palettes, persist them, and kitbash composites.
Spawn worlds, shapes, and templates, clone handles, or scatter and stamp templates across regions, and destroy them. Mutate blocks — set, clear, fill boxes, spheres, cylinders, and lines, mirror, translate, and paste regions. Read the world back — a block, its neighbours, a region, counts by name, raycasts, sphere and line queries, region copies, and bounds. Apply entity-level transforms (position, rotation, scale, parent, snap-to-world) and read them. Manage the palette. Persist to voxbin and templates — save, bake, save and copy templates, and capture an area. Kitbash composite props from existing templates — new, add child, save baked or linked, explode, save as a bundle. And read or override the voxel config. A target — a handle, `"world"`, an entity id, or an entity name — is accepted uniformly across the toolbox.
▲ 0↑ born
# voxelEngine
Public API for the voxel system. Single entry point for creating and
editing voxel worlds and shapes. Tools and custom code call into here;
nobody touches the `VoxelWorld` / `VoxelShape` components directly.
Two top-level concepts:
- **World** — singleton entity carrying a chunked `VoxelWorld` grid.
Created lazily on the first `createWorld` call, found by name later
via `world()` / `hasWorld()`, destroyed with `destroyWorld()`.
- **Shapes** — per-entity dense or chunked `VoxelShape` grids. Spawned
freely; each carries its own voxbin path on disk under
`Config.get("paths.shapes")/<entityId>.voxbin`.
Templates are stored under `Config.get("paths.templates")/<name>.voxbin`
and instantiated via `fromTemplate`. Captures (snapshots of a world
region) land at `Config.get("paths.captures")/<name>.voxbin`.
## Exports
Module-level:
- `M.world() -> Handle?` — handle to the singleton VoxelWorld, or nil.
- `M.createWorld(opts: CreateWorldOpts?) -> Handle` — create or reuse the singleton.
- `M.destroyWorld()` — despawn the singleton world if it exists.
- `M.hasWorld() -> boolean` — true when the singleton exists.
- `M.shape(name_or_id) -> Handle?` — handle to an existing shape entity.
- `M.createShape(opts: CreateShapeOpts) -> Handle` — spawn a new shape entity.
- `M.destroyShape(name_or_id)` — despawn the shape entity if it exists.
- `M.fromTemplate(name: string, opts: FromTemplateOpts?) -> Handle` — instantiate from `paths.templates/<name>.voxbin`. Supports live-link, detached, and memory-only modes.
- `M.saveAsTemplate(shape: Handle, name: string, opts: SaveAsTemplateOpts?) -> string` — write a shape's grid as a named template.
- `M.listTemplates() -> { string }` — enumerate template names (no extension).
- `M.deleteTemplate(name: string)` — remove a template file.
- `M.copyTemplate(srcName: string, dstName: string) -> string` — duplicate a template file.
- `M.captureArea(opts: CaptureAreaOpts) -> any` — flatten a region of world + shapes into a dense voxbin record (and optionally save it).
- `M.scatter(templateName: string, opts: ScatterOpts) -> { Handle }` — bulk-spawn instances across a region.
- `M.stamp(templateName: string, opts: StampOpts) -> Handle` — single placement, either as a new entity (`mode = "instance"`, default) or rasterised into the world grid (`mode = "merge"`).
- `M.composite` — re-export of the composite-prop module.
- `M._resolvePivot(value) -> { number, number, number }?` — internal pivot helper, exposed for composite reuse.
Handle methods (returned by `createWorld` / `createShape` / `fromTemplate` / `world` / `shape` / `scatter` / `stamp`):
- `h:setBlock(x, y, z, name)` / `h:getBlock(x, y, z)`
- `h:fillBox(min, max, name)` / `h:fillSphere(center, radius, name)`
- `h:fillCylinder(p1, p2, radius, name)` / `h:fillLine(p1, p2, name)`
- `h:clear()` — clear every cell.
- `h:setPalette(palette)` / `h:addPaletteEntry(name, color)`
- `h:bounds() -> Bounds?` — inclusive bounds of every filled cell.
- `h:flush()` — flush pending dirty-chunk remeshes.
- `h:save()` — write the grid to the component's dataPath.
- `h:dirtyChunks() -> { any }`
- `h:isLive() -> boolean` / `h:templatePath() -> string?`
- `h:bake() -> Handle` — break the live-link, copy into a private dataPath. Shape-only.
- `h:saveAsTemplate(name, opts) -> string`
- `h:exists() -> boolean` / `h:destroy()`
- `h:setPosition(x, y, z) -> Handle` / `h:getPosition() -> Vec3Map?`
- `h:setRotation(r) -> Handle` / `h:getRotation() -> Vec3Map?` (Euler radians, ZYX)
- `h:setParent(parentEntityId) -> Handle`
- `h:copyRegion(min, max) -> any` / `h:pasteRegion(offset, regionData, opts)`
- `h:mirror(axis)` / `h:translate(dx, dy, dz)`
- `h:getNeighbors(x, y, z) -> any`
- `h:queryRegion(min, max, opts) -> { any }` / `h:countByName(min, max) -> { [string]: number }`
- `h:raycast(origin, dir, maxDist) -> any` / `h:sphereQuery(center, radius) -> { any }` / `h:lineQuery(p1, p2) -> { any }`
- `h:clone(opts) -> Handle` — shape-only. Detached duplicate.
- `h:snapToWorld(opts) -> Handle` — quantise transform or shift voxel data to the world grid.
Types:
- `Vec3Map = { x: number?, y: number?, z: number? }`
- `Vec3Array = { number }`
- `Vec3Like = Vec3Map | Vec3Array`
- `ColorMap = { r: number?, g: number?, b: number? }`
- `ColorArray = { number }`
- `ColorLike = ColorMap | ColorArray`
- `PaletteEntry = { name: string, color: ColorLike, material: any?, atlas: any?, faces: any? }`
- `Palette = { PaletteEntry } | { [string]: ColorLike }`
- `Bounds = { min: Vec3Map, max: Vec3Map }`
- `Handle = { entityId: any, componentName: string, [string]: any }`
- `CreateWorldOpts` / `CreateShapeOpts` / `FromTemplateOpts` / `SaveAsTemplateOpts`
- `CaptureAreaOpts` / `CaptureAreaBoxSelector` / `CaptureAreaSphereSelector`
- `CloneOpts` / `SnapToWorldOpts`
- `RegionSpec` / `ScatterOpts` / `StampOpts`
- `QueryRegionOpts` / `PasteRegionOpts`
## Usage
```luau
local Voxel = require("@builtin::systems.voxelEngine")
-- Create the singleton world, write a couple of cells.
local world = Voxel.createWorld({
size = { x = 64, y = 32, z = 64 },
palette = { stone = { r = 0.5, g = 0.5, b = 0.5 } },
})
world:setBlock(0, 0, 0, "stone")
world:fillBox({ 1, 0, 0 }, { 3, 0, 3 }, "stone")
-- Spawn a shape from a template, live-linked to the template file.
local tree = Voxel.fromTemplate("tree", {
position = { x = 10, y = 0, z = 5 },
})
-- Scatter 20 copies inside a region.
local hs = Voxel.scatter("tree", {
region = { box = { min = { 0, 0, 0 }, max = { 50, 0, 50 } } },
count = 20,
snap = true,
})
-- Capture a region of world + shapes into a dense voxbin record.
local rec = Voxel.captureArea({
box = { min = { 0, 0, 0 }, max = { 15, 15, 15 } },
})
```
## Notes
- Handles are thin wrappers over an `entityId` + a component name. Each
method re-resolves the underlying component on every call, so the
handle stays usable across hot-reload cycles.
- All `h:setBlock` / `h:fillBox` / … calls forward to the component via
colon-call (`c:method(...)`) — dot-call would drop `self` and the
strict typed-function checker would read args from the wrong slots.
- `fromTemplate` defaults to `live = true` + `persist = true` —
live-linked instance that auto-bakes private edits to disk. Pass
`live = false` for a detached copy with its own dataPath, or
`persist = false` for a memory-only instance.
- `Voxel.scatter` / `Voxel.stamp` (mode = "merge") obey the play-mode
write gate enforced by `assertModeAllowsWrite`. In play mode, callers
must toggle `Config.set("allowPlayWrites", true)` (or set the
`[voxel].allowPlayWrites` WORLD_SETTINGS override) to unlock voxbin
writes.
- `Voxel.stamp(mode = "merge")` only supports yaw rotations of
0 / 90 / 180 / 270 degrees — voxel grids require axis-aligned stamps.
Non-axis-aligned yaws raise.
- The `composite` sub-module is reachable as `Voxel.composite`. It
builds kitbashed `.voxcomp` props that re-resolve their children at
instantiation time; `fromTemplate` transparently dispatches to it
when the named template resolves to a `.voxcomp` manifest.
▲ 0↑ born
# Voxel Template (asset type)
A voxel template is a reusable, named voxel grid that live instances
link to. It is the source of truth for a family of `VoxelShape`
instances spawned via `Voxel.fromTemplate(<name>)` — every live
instance reloads its grid + rebuilds its mesh automatically when the
template's payload is rewritten, and a `Voxel.deleteTemplate(<name>)`
flips every live instance's `staleTemplate` flag.
## When to use one
- You want a reusable voxel prop (`tree`, `rock`, `barrel`) that many
entities share, with edits to the template propagating to every
live instance.
- You want a stable identity (a guid) for a voxel grid so the engine's
asset-reload machinery can drive the live-link instead of a
poll loop.
If you just need a one-off shape on a single entity, use
`Voxel.createShape` — that writes a private `dataPath` voxbin and never
becomes a template.
## Where it lives
- Source: `Config.get("paths.templates")/<Name>.voxelTemplate/`
- Identity: `<Name>` (the `.voxelTemplate` suffix strips).
- Folder shape:
- `template.voxbin` — the binary grid payload (size, blockSize,
palette, flat grid array). Written by `saveAsTemplate` /
`copyTemplate`. **Optional** in the structural spec only so the
freshly-scaffolded (empty) folder validates; every real template
carries it.
- `.metadata` — agent-editable tags + free-form fields. **Required.**
## How to create one
From a live shape you authored:
```luau
local Voxel = require("@builtin::systems.voxelEngine.voxelEngine")
local shape = Voxel.createShape({ size = { 8, 8, 8 } })
shape:fillBox({0,0,0}, {7,7,7}, "stone")
shape:saveAsTemplate("my_prop")
-- Writes: <paths.templates>/my_prop.voxelTemplate/
-- template.voxbin (the grid payload)
-- .metadata
```
Or scaffold an empty one to fill in later:
```luau
asset.create("voxelTemplate", "MyProp")
-- Creates: /zero/source/MyProp.voxelTemplate/
-- .metadata
```
## How it operates
1. **Live-link.** `Voxel.fromTemplate(<name>)` resolves the template to
an `AssetRef<voxelTemplate>` and stores it in
`VoxelShape.templateRef`. The engine indexes that subscription by
guid, so a write to the template's `template.voxbin` fires
`onAssetReload("templateRef")` on every live instance, which
re-reads the payload and rebuilds the mesh.
2. **Delete.** `Voxel.deleteTemplate(<name>)` removes the folder and
directly flips `staleTemplate = true` on every live instance —
deletes do not emit a content-change event, so the engine notifies
instances synchronously rather than waiting on a reload hook.
3. **Copy-on-write.** Editing a live instance (`setBlock`, `fillBox`,
…) auto-bakes a private `dataPath` copy and clears `templateRef`,
detaching the instance from the template. The original template is
untouched.
## In the Inspector
Selecting a voxel template opens its **Grid** section, decoded from `template.voxbin` once per version of the file: the template's size in voxels, its block size, whether it is chunked (and, when it is, its chunk size and chunk count), how many palette entries it holds, and whether it carries custom data.
▲ 0↑ born
# Package (asset type)
A package is an asset — a `<Name>.package/` folder that bundles
components, modules, scenes, presets, tools, and any other typed
sub-assets under one identity, installed into a world as one unit.
Packages are how authoring sets get distributed across worlds.
A **library** is the addressable root scope content resolves under
(`@builtin`, `@<name>`) — a whole world mounted into this one under a
namespace, via `zeromind.installLib` / `world.installLibrary`. A package is
an asset that lives inside such a root, and one library holds as many
packages as its author put there. A package is installed by guid the
way every asset is (`zeromind.installAsset`).
## When to use one
- You're authoring a coherent feature suite (character controller
stack, voxel engine, UI kit, animation library) and want to ship
it as one install target.
- You want a stable identity worlds can install and resolve sub-assets
against (`@<lib>::pkg.sub_module`).
- You want versioning + a per-package metadata layer that travels with
the contents.
If you're authoring inside a single world and don't need a separate
install lifecycle, you don't need a package — drop your `.component`
/ `.module` / etc. directly in `/zero/source/`. Packages are for
cross-world distribution.
## Where it lives
- Source: `/zero/source/.../<Name>.package/` (or, for legacy
packages, a folder containing `package.yaml` at its root).
- Identity: `<Name>` (the `.package` suffix collapses in identity —
mirrors `.module`). A file at `<Name>.package/<sub>/init.luau`
resolves under the same identity it would at `<Name>/<sub>/init.luau`.
- Folder shape (container-style — open child set):
- `package.yaml`: metadata (name, version, description) and the
packages it depends on (`dependencies`). **Required.**
- `README.md` — instance documentation. **Required.**
- `.metadata` — agent-editable tags + free-form fields. **Required.**
- Sub-asset folders: any combination of `.component/`,
`.module/`, `.scene/`, `.preset/`, `.tool/`, `.toolbox/`,
`.material/`, `.shader/`, etc.
The validator permits unlisted children (`allow_unlisted: true`) by
design — packages are open containers.
## How to create one
```luau
asset.create("package", "<Name>")
-- Creates: /zero/source/<Name>.package/
-- package.yaml (metadata scaffold)
-- README.md (instance README template)
-- `folder` places it in a subfolder of /source instead of the root:
asset.create("package", "<Name>", { folder = "systems" })
```
Then scaffold sub-assets inside it:
```luau
local pkg = asset.resolve("<Name>.package")
asset.create("component", "Spinner", { into = pkg })
asset.create("module", "util", { into = pkg })
```
## `package.yaml`
```yaml
name: inkBlocks # optional; the folder stem when absent
version: "1.0.3"
description: "Ink and paper style blocks"
dependencies:
- { name: starterBlocks, version: "1.0.8" }
- styleKit # a bare name states no version
```
`dependencies` lists the packages this package needs. Each entry's `name` is
a reference the package records, the same kind of reference a `require` or an
asset name in a data file makes:
- **It resolves in the author's world.** Install the dependency here first
(`zeromind.installAsset`); the entry then names the copy this world holds.
A push refuses a package whose dependency resolves to nothing, naming it.
- **An install brings it along.** Installing the package installs every
dependency it lists, at the version this world holds when the package is
pushed, under `/source/deps/<owner world>/`, deduplicated against what the
installing world already holds. `zeromind.preview` lists each one before
anything is written, and the install's `deps` lists what it brought.
Uninstalling the package leaves a dependency that another installed
package still uses.
- **`version` is checked, not chosen.** A stated `version` has to equal the
`version` the dependency's own `package.yaml` states in this world;
`asset.validate` (and so the push) refuses the package naming both when
they differ. To depend on another version, install that version here.
## How it operates
1. **Registration.** Writing `package.yaml` indexes the package with
the package registry.
2. **Install.** A package is read before it is installed: `zm ls-tree
@<guid>` in the shell lists what it contains, and `zm show
@<guid>:<path>` and `zm grep <pattern> @<guid>` read it, so candidates
are compared by what they do. The one that fits is installed by guid,
the way every asset is: `zeromind_install` over the engine's MCP, or
`tools.use("zeromind", "installAsset", <guid>)` inside the engine. The
install writes the package's files (and the deps its `.refs`
record, the packages its `package.yaml` lists under `dependencies`
included) into this world's source, so its sub-assets resolve here.
`tools.use("zeromind", "preview", <guid>)` reports what an install would
write before it writes anything.
3. **Sub-asset resolution.** Because `.package` collapses in
identity, sub-asset paths work whether the package folder is
suffixed (`<Name>.package/<sub>`) or not (`<Name>/<sub>`).
4. **Updates.** `tools.use("zeromind", "updates")` lists installed content
with upstream changes available; re-running the install pulls them.
5. **Metadata.** `package.yaml::name` overrides the folder stem if
present (rare); `version` + `description` are surfaced in the
package index.
6. **Hot reload.** Edits to sub-assets hot-reload as normal; edits to
`package.yaml` reload package metadata.
## Discovery
- `asset.list("package")` — every registered package.
- `asset.inspect("<name>")` — sub-asset catalog, source, this type
README.
- `ls /zero/source/<Name>.package` lists the files the folder holds.
- `library.list()` — the library assets registered in this world,
including the ones packages under an installed library contribute.
## Authoring conventions
- Name packages by their feature (`voxels`, `cameraRigs`,
`dialogSystem`), not by their owner — the host library is the
`@<lib>::` scope prefix.
- Cluster cohesive sub-assets together. A "character controller"
package should ship the controller component, its preset configs,
its supporting modules, and a demo scene — not just the component.
- Document the package's surface in `README.md` — what install
installs, what each sub-asset does, how they relate. Agents reading
`cat /zero/source/<Name>.package/README.md` get the full picture in one shot.
- Version conservatively — breaking a package means breaking every
world that installed it. Use semantic version bumps.
## Common pitfalls
- **Missing `package.yaml`.** Without it the folder isn't recognized
as a package even if it has a `.package/` suffix.
- **A dependency named only by a string the code builds.** A package
whose files reach another package only through a name assembled at
runtime records no reference to it, so an install arrives without it.
List that package under `dependencies` in `package.yaml`.
- **Identity collapse.** Sub-asset paths skip the `.package` suffix.
Don't include `.package` in `require` strings.
## Referencing inside a package: the `~` rule
`~` means **this package**, resolved structurally from the referring file's own
location. It is the only package-specific syntax. It is rename-safe (no package
name is ever written) and library-agnostic (it never names the host library, so
it survives `@builtin` being renamed or the package being mounted elsewhere).
`~.tail` maps to `<pkg>.package/tail` from anywhere inside the package.
| Referencing from | Use | Example |
|---|---|---|
| An asset/module **inside this package** | `~.tail` | `require("~.modules.myThing")`, `shader: "~.shaders.myThingBlend"` |
| A **different package** under the same root | root-relative identity | `require("@root::systems.otherPkg.modules.x")` |
| A **different library's** public API (a deliberate external dependency) | `@otherlib::…` | `require("@physics::modules.rigidbody")` |
**Never write `@builtin::` (or any host-library name) to reference your own
package** — that couples the package to the library it currently lives in and
breaks if the library is renamed or the files move. Use `~.`. It works from
Luau `require()` and from asset-reference strings in data files (a material's
`mat.yaml` `shader:` field, a component's default asset ref); the engine
resolves it against the referring file's own package. `asset.resolve` takes a
`~.` name the same way, against the package of the file that calls it:
`asset.resolve("~.materials.myThingStandard", "material")`.
`~.` is the package-relative self-reference; `@root::` is the root-relative one
(the library root, or the world source tree for non-library content). The
`core/requiring-modules` guide covers the root model in full.
A package that ships substantial functionality should include a `.guide` asset
(a `<name>.guide/guide.md` folder anywhere under the package); the `guides` tool
discovers it live, so collaborators find the system the way they find built-in
guides. A package's test suite reaches the shared `Test` framework through its
enclosing asset: `local Test = asset.containing(__FILE__).modules.shared`
(`asset.containing` returns the deepest enclosing typed asset, the `.testSuite`,
not the outer `.package`).
## Related types
- Any of `.component`, `.module`, `.scene`, `.preset`, `.tool`,
`.toolbox`, `.material`, `.shader`, `.style`, `.service`,
`.importer` — packages bundle these.
## In the Inspector
Package: the name, version and description package.yaml declares, how many things the package holds and what they are, and **Open** for package.yaml. A package.yaml that does not decode is listed under Problems with the decoder's error.
▲ 0↑ born
# Component (asset type)
A component is a reusable behavior + data unit attached to an entity.
The component file declares public fields, methods, and lifecycle
callbacks; the engine instantiates one per attachment and drives its
hooks every frame. Components are the primary unit of gameplay code —
movement, AI, animations, UI panels, network sync, and per-entity state
all live here.
## When to use one
- You need per-entity state that survives across frames.
- You need per-entity behavior driven by engine lifecycle hooks
(`start`, `update`, `onDestroy`).
- You want to expose configurable fields to designers / `entity.spawn`
callers / scenes without code changes.
If you need a stateless pure-function library, use a `.module`. If you
need to generate content declaratively, use a `.service`; compute work
belongs in a `.computeShader`.
## Where it lives
- Source: `/zero/source/.../<Name>.component/`
- Identity: `<Name>` (the `.component` suffix strips from the
identity, but the folder retains the suffix on disk).
- Folder shape:
- `init.luau` (or `init.lua`) — declares the component. **Required.**
- `README.md` — instance-level documentation. **Required.**
- `.metadata` — agent-editable tags + free-form fields. **Required.**
The structural contract is enforced by `asset.validate` against this
type's `type.yaml`.
## How to create one
Always start with the scaffold — it guarantees the folder shape
matches `type.yaml` from the moment it's on disk:
```luau
asset.create("component", "<Name>")
-- Creates: /zero/source/<Name>.component/
-- init.luau (canonical declare + public block + hooks stubbed)
-- README.md (instance README template)
```
After scaffolding, edit `init.luau` to fill in the actual fields and
behaviour. The component registers as soon as the file is written —
hot-reload is automatic; no engine restart required.
## How it operates
1. **Registration.** Writing `init.luau` into a `.component/` folder
indexes the asset, mints a guid (in the `.meta` sidecar), and
surfaces the type to the engine's component registry. Same-frame
visible to `entity.component.add`.
2. **Attachment.** `entity(id).component.add(name, data)` (or the
batch form `tools.use("entityOps", "addComponents", targets,
{ [name] = data })`) attaches an instance.
The `data` table's fields override the component's declared
defaults; omitted fields take the declared default. The `data`
values are copied into `public.*` BEFORE `awake()` runs.
3. **Lifecycle.** The hooks run in this order:
- `awake()` — same frame as attach, immediately after the data
copy. Initialise state, set up native bridges, register with
other systems. Other components on the same entity may not have
awoken yet.
- `start()` — the next frame. Every component on the entity has
completed `awake()` by then, so cross-component setup is safe
here.
- `update(dt)` — same frame as `start()`, then every frame after,
for as long as the gameplay clock is running. `fixedUpdate(dt)`
runs once per physics step, straight after it, with the step's
`dt`, so a frame can run it several times or not at all;
`editorUpdate(dt)` runs
while the clock is held. Which of them ticks is decided by
`engine.mode` AND `engine.paused` together — see **Per-frame
hooks and the two axes** below, and `man modes`.
- `onDestroy()` — when the component is removed via
`component.remove` or when the owning entity is despawned.
Release resources, drop native ECS components.
For the full list (`onEnable`, `onDisable`, `fixedUpdate`,
`editorUpdate`, `onPropertyChanged`, `onAssetReload`,
`onModuleReload`,
`onBeforeSave`, …) and the timing details, **read `man components`
— that is the canonical lifecycle reference. Generic component
patterns (declare blocks, public fields, sync, ECS bindings,
typed methods) all live there.**
4. **Public surface.** Fields declared in `public` are inspectable,
editable in the inspector, and persist with the entity. Methods are
declared on that same table with a colon —
`typed function public:method(args)` — and every one of them takes
the component ref as `self`, so a caller invokes it with a colon
too: `entity(id).component.get(name):method(args)`. That is the one
calling convention a component ref answers to: the reflection
methods `ref:fields()` / `ref:getField(name)` /
`ref:setField(name, value)` take the same spelling. A function the
chunk declares at file scope (`function helper()`) is the
component's own, reachable from its hooks under its bare name;
`public` is what the ref carries.
5. **Settings and the Inspector card.** `type.yaml` names the component's
settings schema as `settings: { schema: { instance: "init.luau",
export: "public" } }`: the `public` table IS the schema, and its values
live on each entity the component is on, so `asset.schema(componentRef)`
answers the Field descriptors and no values file exists. On an entity
the Inspector shows the component as a card whose generic `settings`
section edits those fields as typed controls, each edit checked against
the schema and written to the live component as one undoable operation.
`type.yaml` also names `inspector: { instance: "inspector.luau" }`: a
component that ships an `inspector.luau` beside its `init.luau` shapes
its card — the file returns `{ sections, replace, order }` (see
`assetTypes/README.md`, "`inspector:`"), each section's `build(ctx)`
reading `ctx.component` (the live component) and `ctx.selection.id` (the
entity). `SceneModule.component/inspector.luau` replaces `settings` with
the bound module's declared inputs.
6. **Events.** An `events` block declares typed moments the component
fires from inside — `events = { onHit = Event({ dmg =
Field.number(0, NoSync) }) }`. The owner fires with
`events.onHit:fire({ dmg = 10 })`; outside code subscribes through
the proxy's subscribe-only facade,
`entity(id).component.get(name).events.onHit:connect(fn)`
(`:once` / `:wait` too). Pass `Sync` as the event's second argument
to replicate a fire to other peers. Events let one component react
to another's moments without polling. Full reference: **`man
components`**, Events section.
7. **Hot reload.** Editing `init.luau` reloads the component
definition; live instances are reconciled with the new declaration
in-place, and each crosses one lifecycle boundary: `onDestroy()`
ends the running life, then `awake()` and `start()` construct the
one that replaces it, and `update()` resumes on the next frame.
`public` and `private` values carry across; module-level `local`s
belong to the chunk, so the new life starts with them at their
initial values and its `awake`/`start` are what fill them in.
Whatever the old life handed elsewhere — a subscription, a shared
registration, a spawned entity — is released in `onDestroy`, the
call that pairs with the construction about to run. An edit that
leaves the source byte-identical crosses no boundary at all.
A field carries its value across when something stated it: a key of
the `data` table the instance was attached with (a scene record's
data is that table), or a write made since. Every other field takes
the edited source's declared default, so changing a default reaches
every instance that left the field to it. An instance that states
its own value for a field whose default the edit changed keeps that
value, and the reload logs a `[HotReload]` warning naming the field,
the entity, the value kept and both defaults.
## Discovery
- `asset.list("component")` — every registered component (engine +
world + libraries).
- `asset.inspect("<name>")` — identity, scope, public fields,
lifecycle methods, source path, and this type README. `detail`
carries the component's declared `fields`, `methods`, and `events`,
parsed from its source — `tools.use("assets","describe", "<name>")`
renders it as markdown, so a component's events are visible without
opening its source.
- `ls /zero/source/<Name>.component` lists the files the folder holds.
- `entity(id).component.list()` — components attached to an entity.
## Per-frame hooks and the two axes
`engine.mode` (`"edit"` / `"play"`) and `engine.paused` (the gameplay
clock) are independent, and the per-frame hooks read them separately:
- `update(dt)` / `fixedUpdate(dt)` tick while **the gameplay clock is
running** — whichever mode the engine is in.
- `editorUpdate(dt)` ticks while **the clock is held** — because the
mode is `edit`, or because play is paused — and also alongside
`update` whenever edit is running.
Which gives four states, and this table is the contract:
| `engine.mode` | `engine.paused` | hooks that tick |
|---|---|---|
| `edit` | `true` | `editorUpdate` |
| `edit` | `false` | `update`, `fixedUpdate`, `editorUpdate` |
| `play` | `false` | `update`, `fixedUpdate` |
| `play` | `true` | `editorUpdate` |
Entering edit sets `paused = true` and entering play sets it back to
`false`, so a session that never writes `engine.paused` only ever sees
rows 1 and 3 — which is where the shorthand "`update` is the play tick,
`editorUpdate` is the edit tick" comes from. It is the two default rows
of a four-row table, not the rule.
**Row 2 is the one that surprises.** With `engine.paused = false` in
edit — set by hand, or by a tool that advances the gameplay clock to
sample it, such as a `capture.collage` given a `duration` — `update` and `editorUpdate`
both fire, once each, every frame. A component that declares both and
routes them to one body therefore runs that body **twice per frame**:
```luau
local function drive(dt) ... end
function update(dt) drive(dt) end
function editorUpdate(dt) drive(dt) end -- twice per frame in row 2
```
Nothing reports the doubling; it reads as an animation running at twice
its intended speed. To get exactly one tick per frame in every row, ask
which axis you meant and answer it in the body:
```luau
function update(dt) drive(dt) end
function editorUpdate(dt) if engine.paused then drive(dt) end end
```
A scene `entrypoint.luau` declaring the same two hook names is dispatched
by the scene loader, not by this table: there `update` needs play AND
running, `editorUpdate` needs edit, and play-paused ticks neither. Read
`man scenes` before carrying a component's rule to an entrypoint.
## Authoring conventions
- Annotate public fields with `--!desc` so the inspector + LSP show
documentation alongside the field name.
- Declare lifecycle hooks (`start`, `update`, `onDestroy`) explicitly
rather than relying on engine defaults — readers can see at a glance
what runs when.
- Keep `update(dt)` cheap — it runs every frame per instance. Move
heavy work to `start`, to a `.computeShader`, or behind a job (`man jobs`).
- For multi-entity batching, prefer
`tools.use("entityOps", "addComponents", targets, components)` over
a loop of `entity(id).component.add` — it takes many targets and
many components in one call.
## Common pitfalls
- **Naming.** The component identity is the folder stem with
`.component` stripped (e.g. `<Spinner>.component/` → identity
`Spinner`). Don't repeat the suffix in the identity passed to
`component.add`.
- **`init.lua` vs `init.luau`.** Either is accepted (it's a
`one_of_group: "entrypoint"`), but `.luau` is canonical. Mixing both
in one folder is invalid.
- **`update` in edit mode.** It runs whenever the gameplay clock is
running, and entering edit stops that clock — so `update` is idle in
a freshly-entered edit session and ticks again the moment anything
sets `engine.paused = false`. Use `tools.use("wld", "play")` to test
update logic under the mode it ships in; `tools.use("wld", "edit")`
to return. See **Per-frame hooks and the two axes** above.
- **Renaming.** Renaming the folder changes the identity. References
to the component are recorded against the component itself, so
renaming or moving it does not break them.
## Related types
- `.module` — pure-function library, no per-entity state.
- `.service`: a declarative content generator, no per-entity state.
- `.preset` — a captured component configuration, replayable into
`component.add` without re-typing fields.
- `.bundle` — entity hierarchies that may carry component attachments
as part of the bundle.
## In the Inspector
Fields: its execution order, hooks, and how many methods and events it declares, then the fields its `public` table declares as read-only typed controls holding their defaults, with **Check** and **Open** for its entry file. Instances: a link to every entity carrying it. What its card shows on an entity comes from the component's own `inspector.luau`.
▲ 0↑ born
# Scene (asset type)
A scene is a saved, loadable snapshot of an entity scenegraph inside a
world: the entity hierarchy, transforms, components, lighting, and a
declared `player` intent, plus an auto-discovered startup script. A scene
is the unit of "level" or "screen" — a title screen, a lobby, a gameplay
area, and an end-of-game summary each get their own scene.
Scenes load as **layers**. One root scene is active at a time; additional
scenes can load additively as overlays on top of it. Each scene owns its
own multiplayer relay room. Switching scenes is a local call: the client
that makes it leaves the old scene's room and joins the new scene's room.
## When to use one
- You want a savable, re-loadable snapshot of an entity scenegraph plus
its lighting and player setup.
- You want a runnable starting state for a level or screen.
- You want a scenes-as-data flow: `layers.load("lobby")`, edit live, then
`layers.active:save()` to publish the edits back to the scene.
For a reusable entity assembly (a prop, a vehicle, a UI panel) that many
scenes spawn into themselves, use a `.bundle`. A scene is a whole-world
snapshot; a bundle is one prefab that lives inside scenes.
## Folder shape
A scene asset is a folder ending in `.scene`. Its identity is the folder
name with the suffix stripped (`Lobby.scene` → `Lobby`).
- `scene.json` — the scenegraph: `entities[]`, lighting, and the
top-level `player` intent. **Required.** Written by the engine — a build,
a save, a human dragging something in. Not hand-edited.
- `build.luau` — what the scene is made of, written as code. Runs while you
author; saving bakes its result into `scene.json` (see Building a scene).
- `entrypoint.luau`: the startup script. **Required.** Auto-discovered as a
sibling of `scene.json`; define lifecycle callbacks here (see The entrypoint).
- `<name>.<type>/` — an asset the build makes, authored here by `build.asset`
(see The assets a build makes), alongside `.build.assets`, which records
which `build.luau` produced each of them.
- `prefabs/` — optional subfolder for scene-local bundles.
- `scripts/` — optional subfolder for scene-local Luau helpers, required
from `entrypoint.luau`.
- `README.md`: notes about THIS scene: what it shows or plays and what it
is for. **Required.** The general scene model lives here, in
`guides { path: "types/scene" }`.
- `.metadata`: **Required.**
`scene.json` is version 7. It carries `"format": "scene"`,
`"version": 7`, an `entities[]` array, and a top-level `"player"` string
intent.
## The `player` intent
Every scene declares how it handles players with a top-level `player`
field in `scene.json`:
- `"spawns"` — the scene carries an authored player setup, and the engine
spawns a player for each connecting user from it. This is the default
for a freshly created scene.
- `"none"` — the scene spawns no player and authors no camera. Author your
own Camera entity for the view (menus, UI-only screens, cinematics).
### How `"spawns"` works
A `"spawns"` scene authors two things in `scene.json`:
1. A **PlayerPrototype** — an entity carrying the `PlayerPrototype`
component, whose `body` field names a child entity (the body). The
body carries an avatar (an `Asset` pointing at an avatar, e.g.
`@builtin::avatars.humanoid`). A `CameraRig` child carries a `Camera`
with a camera behavior (e.g. `orbital_follow`). The prototype root is
authored `PrototypeOnly` so it exists as a clonable template.
2. A **PlayerSpawn** — an entity carrying the `PlayerSpawn` component,
whose `prototype` field names the PlayerPrototype to instantiate and
whose transform is where players appear.
The scaffolded scene groups these under two organizing entities,
`PlayerSetups` (holding the prototype) and `Spawns` (holding the spawn),
so the authoring tree stays legible.
On each user join — entering play, and each subsequent connect — the
engine runs the spawn for that user:
- It clones the PlayerPrototype subtree.
- The clone root becomes the user's **internal identity** — a registry-only
entity named `user` carrying the `UserIdentity` component. The identity
is the anchor for who the player is; it has no world presence.
- The body named by `PlayerPrototype.body` is adopted as the user's
**avatar** — their visible presence in the world — and placed at the
PlayerSpawn's world transform. Move the PlayerSpawn (or drag it in the
editor) to set where players appear.
- The `CameraRig`'s Camera follows the bound avatar.
The player prototype is live in edit mode so you can see and adjust it. In
play mode it is deactivated and marked internal — it stays a clonable source while
each user gets their own clone.
### Reaching the live player
The player surface lives on the root scene's registry,
`layers.active.players`:
- `players.localPlayer` — this client's player, a curated handle.
- `players.localReady` — `true` once the local player's avatar is bound
(or the scene opted its avatar out).
- `players:list()` / `players:count()` — the players connected in this
scene's room.
- `players:get(userId)` — a connected player by account id.
- `players:ownerOf(avatar)` — the player whose body is a given avatar.
A player handle is a data record about who the player is, plus a link to
their body:
- `player.avatar` — the body's entity ref. Act on the body through it
(`player.avatar.position`, raycasts, rig access).
- `player.isLocal` — whether this client owns the player.
- `player.ready` — whether a live avatar is bound.
- `player.userId` / `player.identity` / `player.displayName` — the
player's account identity.
- `player.avatar = ref` — bind a live body entity as the player's body
(assign nil to clear). To use a `.bundle` / `.avatar` asset, spawn it
into the world first, then bind the resulting entity.
The player's identity entity is the internal anchor; the handle exposes the
body (`player.avatar`) and the player's data, and reaching for
`.entity` / `.id` / `.component` raises with a redirect to `player.avatar`.
## Building a scene
`build.luau` declares what the scene holds. It is ordinary engine code —
`entity.spawn`, `component.add`, transform writes, `for` loops, a `require`
of a module that computes a layout — and what it creates is what ends up in
the scene.
```lua
function content()
for i = 0, 4 do
local crate = entity.spawn("crate")
crate.localPosition = { i * 2, 0.5, 0 }
crate.component.add("Model", { model = asset.resolve("cube", "mesh") })
end
end
```
The build runs while you author, in **edit** mode, and what it created lands in
the live scene. Saving the scene bakes it into `scene.json`, which is what makes
the entities durable content: a human can select and inspect them, they
replicate, and they are there when the world opens without `build.luau` running
at all. Play mode never runs it — in play the entities load from `scene.json`
like anything else the scene holds.
**Editing the file rebuilds the scene in place.** Writing `build.luau` is the
signal; there is nothing to reload, and the rebuild saves. An entity keeps the
id it had, so a reference to it — a spawn naming a prototype, another entity's
component naming this one — survives every rebuild. Add a crate to the loop and
the other four do not move.
The write returns as soon as the file is on disk and the rebuild finishes behind
it, so what it landed comes back as a notice:
```
[info] a write to build.luau rebuilt the scene { content="6", scene="scenes.main",
editorOnly="1" }
```
An edit saved while a rebuild is still running is an edit to code that rebuild
has already read, so it gets a rebuild of its own once that one finishes, and
says which it was:
```
[info] a write to build.luau arrived during a rebuild and was rebuilt after it
{ content="7", scene="scenes.main", editorOnly="1" }
```
Several edits saved during the same rebuild get the one run after it, which
reads the file as it stands then.
A scene that declares a build also runs it when it **loads** in edit, so the
scene shows what the code says now, including an edit made while the world was
closed. A load whose `.build.state` records the current script reads
`scene.json` as the build and runs nothing. When the load does run it, a
world's own scene saves the result, unless the scene holds unsaved edits: those
stay the author's to keep or discard, and the file write waits for their save.
A library scene (one under `/libs/`, the `@builtin` library included) is read
by its load: the build lands in the live scene and its files stay as the
library states them. Play reads `scene.json`, so it comes up without that build
until `:build()` saves it.
### The two surfaces
- `content()` — the scene's own entities. Present in play.
- `editorOnly()` — entities present while authoring and absent in play. They
carry the `EditorOnly` participation the runtime deactivates and hides when
play begins, and they are baked too, so a collaborator opening the world sees
the same ones. Anything an author needs to see and a player does not goes here
— an alignment guide, a spacing marker, a debug volume.
Both are optional. A scene with neither has no build.
### What a rebuild touches
Only the entities the build itself placed. Anything a human dragged in, or a
tool placed directly, is invisible to a rebuild and never moved or removed.
Within what it owns, the build is authoritative: an entity the code stopped
emitting is despawned, a component it stopped adding is removed.
The build writes its own name and each entity's place in the hierarchy onto the
entity, and `scene.json` records that pair beside the entity's name and
transform — which is why a rebuild after a reload, in a later session, lands on
exactly the entities it placed the first time without anything having been
remembered.
### When the file did not change
`:build()` runs the build against what it resolves right now — for the case
where something the builder *reads* changed and the file did not: a module it
requires, an asset it resolves. After a write it is unnecessary; the write has
already run it.
```lua
layers.active.asset:build() -- rebuild and save
layers.active.asset:build({ save = false })
```
It answers with how many entities each surface placed —
`{ content = 6, editorOnly = 1 }` — and with nil for a scene that has no
`build.luau`. Called while a build for that scene is already running, it returns
at once with `{ inFlight = true, scene, trigger, startedBy, message }` naming
that build instead of starting a second one; `inFlight` is what tells the two
answers apart.
An operation the bake cannot hold is **refused** rather than applied. A build
accepts what its records carry: entity lifecycle, transforms, hierarchy, names,
visibility, active state, participation mode, network scope, attributes, and
components with their public data. Anything else raises while the build runs,
naming the operation and the build it answered to, instead of leaving the scene
depending on something no reload brings back. When that happens the previous
build stays exactly as it was. A runtime resource a build needs — a mesh it
generates, a texture it writes — is made as an asset instead, below. Everything
else a refusal names goes outside the builder, or into a component the builder
attaches, which runs at play.
### The assets a build makes
`build.asset(type, name, produce)` makes an asset the build's content names — a
mesh a lathe produces, a texture a pattern writes. `produce` takes nothing and
returns the creation parameters `asset.create` takes for that type; the call
returns the asset's `AssetRef`, which is what a component field holds.
```lua
function content()
local tower = build.asset("mesh", "tower", function()
local positions, indices = lathe(profile, 16)
return { positions = positions, indices = indices }
end)
entity.spawn("tower").component.add("Model", { model = tower })
end
```
The asset is authored inside the scene's own folder — `Lobby.scene/tower.mesh`
— and the scene records which `build.luau` produced it in `.build.assets`. So
`produce` runs when the build script changed and never otherwise: a reload
re-runs the build and reuses the asset, while editing the profile re-authors
`tower.mesh` **in place**, keeping its path and its guid. That is what holds the
reference — `scene.json` names assets by guid.
For a part of a scene that several scenes place — or that one scene places many
times with different settings — see `guides { path: "types/sceneModule" }`,
which is the same idea with declared `inputs` a placement sets.
`guides { path: "core/scenes-as-code" }` covers both.
## The entrypoint
`entrypoint.luau` is the scene's **runtime** — what happens while the game is
being played. What the scene *is* comes from `build.luau` above; nothing here
has to construct it.
`entrypoint.luau` is auto-discovered as a sibling of `scene.json`. The
loader runs it in a fresh environment and folds its top-level
`function name(...)` declarations into the matching scene lifecycle event.
Define the callbacks the scene needs — the recognised set:
- `onLoad()` — the scene loaded in **play** mode. Gameplay-start hook,
runs on every connected client.
- `onHostLoad()` — the scene loaded in **play** mode, on the client that
owns the scene's synced content (the relay room creator; offline and
single-player count as host). Fires once. Entities spawned here — and
anywhere downstream — become synced automatically, so this is where
shared world content is spawned: it runs on exactly one peer, and every
other peer receives the entities from the relay snapshot.
- `onEditLoad()` — the scene loaded in **edit** mode. Custom per-scene
authoring tools. Writing this file while the scene is loaded in edit mode
reloads its layer, so the new `onEditLoad` runs after the previous load's
`onUnload`.
- `onUnload()` — before the scene's entities are torn down. Extra cleanup;
engine-managed teardown (entity despawn, resource release) runs
separately.
- `update(dt)` — per frame in play mode, while the gameplay clock
runs.
- `editorUpdate(dt)` — per frame in edit mode, running or paused.
Play-paused ticks neither hook. The scene loader partitions these
strictly on mode; a component's hooks of the same name read
`engine.paused` instead, so the two rules differ — `man components`
has that table.
- `localPlayerReady(player)` — once the local player's avatar is ready.
Gameplay wiring for the local player: attach state, bind UI, swap the
avatar. The avatar is already placed at the PlayerSpawn, so this is for
behavior, not positioning.
- `playerJoined(player)` — a remote player joined this scene's room. Use
for per-player UI (nameplates, greetings). The local player arrives
through `localPlayerReady`.
- `playerLeft(eid)` — a remote player left this scene's room. Receives the
departed entity's id (the entity is already despawned).
Each scene's entrypoint runs in its own environment, so two scenes can
declare the same top-level names without clashing. Keep each callback
short; push per-scene logic into `scripts/` modules and `require` them.
## Layers
Scenes load through the `layers` namespace.
- `layers.load(ref, opts?)` — load a scene. `ref` is a scene AssetRef or
identity string. Without `opts.additive`, this is a **root** load: it
replaces the current root scene. `opts`:
- `additive` — load as an overlay alongside the current root instead of
replacing it.
- `persistent` — keep an additive overlay mounted across mode flips and
root swaps.
- `origin` — a `{x, y, z}` world offset for the overlay's entities.
- `deferred` — spread the entity spawn across frames (for
thousand-entity scenes); the load signals complete only once the
scene has settled.
- `layers.active` — the current root scene proxy (nil before any load).
- `layers.list()` — every loaded layer (root + additive overlays).
- `layers.find(ref)` / `layers.is_loaded(ref)` — look up a loaded layer by
ref.
- `layers.onLoad(cb)` / `layers.onUnload(cb)` / `layers.onBeforeLoad(cb)` —
subscribe to every scene load / unload (distinct from a scene proxy's
own `:on_load` / `:on_unload`, which fire only for that scene).
A root scene carries the `players`, `camera`, and `settings` surfaces;
additive overlays inherit the root's players and lighting and do not carry
their own.
### The scene proxy
`layers.active` (and `layers.find` / `layers.list` entries) returns a
scene proxy:
- Fields: `.guid` (canonical identity), `.name`, `.path`, `.asset`,
`.additive`, `.visible`, `.state` (`"loading"` → `"loaded"` → `"ready"`
→ `"unloading"`), `.ready`, `.players`, `.camera`, `.settings`,
`.entrypoint`.
- `.camera` — a proxy over the scene's primary camera entity; read and
write the live Camera component through it, and
`.camera.behaviors._list()` enumerates the available camera behaviors.
- `:save(opts?)` — publish the live scenegraph to canonical `scene.json`
(see Saving). A convenience that forwards to the scene ASSET's `save`
(`self.asset:save(opts)`): the proxy is the runtime view and owns nothing
on disk, so persistence lives on the asset.
- `:reload()`: unload and reload the same scene (a runtime respawn). A scene
with a `build.luau` runs its build again and saves the scene, so `scene.json`
holds what the reload built and the next play loads it; a library scene's
rebuild lands in the live scene alone.
- `:clearDirty()` — discard unsaved edits and respawn from canonical.
Forwards to the scene asset's `discard` (`self.asset:discard(opts)`).
- `:sceneAsset()` — the durable `AssetRef<scene>` this live layer maps to.
- `:promoteDirty()` / `:writeDirty()` / `:hasDirty()` — manage the dirty
overlay directly.
- `:unload()` — despawn the layer and free its slot.
- `:onReady(cb)` — fire once the scene reaches `ready` (latched: a late
subscriber fires immediately).
## Multiplayer & relay rooms
Every scene has its own relay room, keyed by the world guid, the boot
profile, the mode, and the scene guid. Switching scenes with
`layers.load` runs a room transition — leave the old room, tear down,
join the destination scene's room — all on the client that made the call.
Teardown of the old scene never leaks to peers in the new one.
In play mode connected to a relay, the scene's entities are
host-authoritative: only the room creator instantiates them (marked
synced), and every joiner receives them from the relay snapshot rather
than spawning their own. This is why shared world content belongs in
`onHostLoad` — spawning it in `onLoad` would run on every peer and
duplicate it. Each player's own avatar spawns per-user from the
PlayerSpawn on every client.
Edit mode and offline boots spawn locally and never auto-sync.
## Loading and saving
**Loading** — `layers.load(ref)` for a root scene:
1. Despawns the current root scene's non-persistent entities.
2. Instantiates the entities from `scene.json` (including the player
prototype and spawn for a `"spawns"` scene).
3. Applies lighting and sky.
4. Runs `entrypoint.luau` and fires lifecycle callbacks as their events
occur.
**Saving** — persistence lives on the scene ASSET. `sceneRef:save()`
writes the scene's current state back to canonical `scene.json`; when the
scene is loaded it captures the live scenegraph, and when it is not loaded
it promotes the pending dirty overlay. `layers.active:save()` is the
convenience form — it forwards to the active layer's mapped asset
(`layers.active.asset:save()`). Writing canonical is the only path that
does so: edit ↔ play cycles never save, so hitting play to test never
overwrites the scene. Between saves, live edits accumulate in a per-scene
dirty overlay that the loader merges on top of canonical.
`sceneRef:discard()` throws the overlay away and restores the saved state
— deleting the overlay, and respawning the live layer when the scene is
loaded. It works whether or not the scene is loaded, because the overlay
belongs to the asset, not the runtime layer. `layers.active:clearDirty()`
forwards here. Saves capture entity data, not the entrypoint script —
edits to `entrypoint.luau` and `scripts/` are always preserved.
`opts.to` on `save`/`discard` targets a different scene ("save as"),
taking either a bare name (`"level_2"`) or a full VFS path.
**What a record states.** A component's record in `scene.json` holds the
fields that instance's configuration states: the keys of the table it was
added with (a `build.luau` call's own init table, or the record it was
loaded from) and every field written since, through code, a tool or the
inspector. A field nothing set stays out of the record and takes the
component's declared default each time the scene loads, so changing that
default reaches every entity that left the field to it. To have an entity
follow the default again, remove the field from its record. A field set on a
live entity after its record was made is a change the scene review names
(`sceneAuthoring.changes` lists it as `+ Type.field = value`), and accepting it
writes the field into the record. A stored
value cannot be told apart from one an author chose, so a stored value
loads and saves as it is, and a hot reload that changes a default one of
them shadows names it. A stored field the component no longer declares is named by a
`[component.add]` warning when the record loads and left out of the next
save.
## Lighting
Lights are ordinary entities: a directional light, an ambient light, and
a sky are entities carrying `Light` / `ProceduralSky` components in
`scene.json`. Author, move, and tune them like any other entity. If a
scene is missing any of {directional light, ambient light, sky}, the
loader adds a temporary default for the absent one so a scene never loads
dark or skyless; the defaults are never saved into the scene.
Scene-level lighting state that is not an entity — clear color and sky
descriptor — lives under `layers.active.settings.lighting` (`clear_color`,
`sky`); writing those fields updates the engine live.
## Modes
Scenes load in both edit and play mode (see `man modes`). The mode
governs behavior:
- An entrypoint's `update(dt)` runs in play while the gameplay clock
runs, and its `editorUpdate(dt)` runs in edit. A component's hooks of
the same name follow the clock rather than the mode — `man
components` has the four states that produces.
- Player prototypes are live in edit and deactivated + internal in play,
where each user instead gets a clone. `EditorOnly` entities follow the
same edit-live / play-internal toggle.
- `onLoad` / `onHostLoad` fire on a play-mode load; `onEditLoad` fires on
an edit-mode load.
## How to create one
```luau
asset.create("scene", "<Name>")
-- Creates /zero/source/<Name>.scene/ with:
-- build.luau where this scene's content is written. A new one starts
-- with the sun / ambient / sky lighting and the "spawns"
-- player setup, commented for editing — everything else the
-- scene comes to hold is written here too.
-- scene.json what that code produces, baked.
-- entrypoint.luau commented lifecycle-callback stubs.
```
A freshly created scene has a player with a following camera on the first press
of play. It carries no ground, so the first thing to author is what the player
stands on — a `content()` line away in `build.luau`.
## Discovery
- `asset.list("scene")` — every registered scene.
- `asset.inspect("<name>")` — entity count, lighting summary, source path,
and this type README.
## Authoring conventions
- Keep static layout in `scene.json` — geometry, lights, props, the player
prototype and spawn.
- Keep dynamic setup in `entrypoint.luau` lifecycle callbacks — event
listeners, gameplay state, services attached to spawned entities.
- Spawn shared, synced world content from `onHostLoad`; spawn each
player's own gameplay wiring from `localPlayerReady`.
- Set the world's startup scene in `/.world_settings` — that is the scene
the world boots into.
- Y is up; -Z is forward.
- Renaming the folder changes the scene's identity. References to the
scene are recorded against the scene itself, so renaming or moving it
does not break them.
## Related types
- `.bundle` — reusable entity hierarchies that scenes spawn into
themselves.
- `.service`: a declarative content generator a scene can draw on.
- `.world_settings` — picks the startup scene and world-level renderer /
physics options.
## In the Inspector
Selecting a scene opens its **Scene** section, read from `scene.json` without loading the scene: how many entities it holds, how many of them are roots (and their names), how players join and the file's format version when it states them, and whether the scene has edits not yet saved.
▲ 0↑ born
backing path · systems/voxelEngine.package
Problems
Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.
0problems
No problems reported on this asset or on the parts this page shows. Parts it leaves out are not counted here.
⌬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
+ 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.