# Prefabs

| Field | Value |
|-------|-------|
| **Status** | Nested instances + variants; Shinra `.shinprefab` cook; the runtime loads cooked prefabs only. |
| **Audience** | Level designers, game developers, engine contributors |
| **Related** | [Scenes and gameplay](/docs/systems/scenes-and-gameplay.md), [Assets and Shinra](/docs/systems/assets-and-shinra.md#prefab-documents), [Gameplay API](/docs/guides/gameplay-api.md) |

## Summary

A **prefab** is a reusable actor subtree asset. A scene **references** an instance and records what differs from the template; the engine expands it at load (`prefab/hydrate.zig`). Expanded actors exist in memory only — each carries a `prefab` link stamp for identity, and every consumer downstream (editor hierarchy, physics, selection, undo, MCP) sees the same flat actor list it always did.

Scenes resolve their prefab templates when opened. Changes made outside the editor appear on the next scene open; an already-open scene uses the explicit refresh paths described below.

**Overrides are per field**, addressed as `render.material`, `light.intensity`, `transform.position`, `components.<name>`. Overriding a material therefore leaves `casts_shadow` tracking the template — the failure mode of block-granular overrides, where touching one field silently freezes every field beside it.

## Create and reuse a prefab

These steps use the editor; no code or JSON editing is required. Start in **Edit** mode with a saved scene.

1. Select the root of the object you want to reuse in the Hierarchy. Its children become part of the prefab too.
2. Open the root's context menu and choose **Make Prefab…**. Give it a unique name. The editor creates the asset and links the selected instance to it.
3. In the Asset Browser, select the prefab and use **Place Prefab** to add another instance. Move, rotate, or scale its root to position the whole object.
4. Change a property on one instance. That change becomes an **override**: other instances continue to use the template's value.
5. To change the template itself, choose **Open Prefab**, edit it, and choose **Save Prefab**. Use **Close Prefab** to return to your scene, then save the scene.

| Action on an instance | Result |
|---|---|
| **Apply** | Writes that instance's changes into the shared prefab asset and refreshes other open instances while preserving their overrides |
| **Revert** | Restores template values on the instance while keeping its root placement |
| **Unpack** | Keeps the objects but removes their prefab link |
| **Create Variant…** | Creates another prefab that inherits from this one and stores its own changes |

**Apply and Make Prefab write asset files that scene Undo cannot restore.** Save or version your work before changing a shared template. Revert and Unpack are undoable scene edits. Base-owned actors in a variant appear locked; edit the base prefab to change them.

The remaining sections document file formats, expansion, and runtime behavior for programmers and engine contributors.

## Authoring format

```json
{
  "kind": "com.hikari.prefab",
  "version": 1,
  "id": "crate_stack",
  "name": "Crate Stack",
  "root": "root",
  "actors": [{ "id": "root", "archetype": "empty" }]
}
```

- On disk: `assets/prefabs/<stem>.prefab.json` (authoring).
- Cooked: `.shinprefab` (the document as a schema-neutral value tree in the cooked container, like `.shinscene`) at the same relative path as its source under cook/work — never next to authoring JSON. Shinra finds prefabs anywhere under `--input` by suffix or `kind` tag, like every other authored document; Kaji stages the cooked file to the same relative path under `data/`.
- Runtime load (`World.retainPrefab` / AssetStore): prefer cooked `.shinprefab` (no JSON parse). Authoring `.prefab.json` only in Debug (mirror scene policy).
- A scene references instances in a top-level `instances` array (never expanded actors):

```json
{
  "instances": [{
    "id": "lamp_blue",
    "prefab": "prefabs/street_lamp",
    "transform": { "position": [-7, 0, 0] },
    "overrides": [
      { "local_id": "bulb", "path": "render.material", "value": "asset://./materials/blue" }
    ],
    "removed": ["banner"],
    "added": []
  }]
}
```

Members mint `{id}__{local_id}` at expansion, so anything referencing a member by scene id (joints, scripts) keeps working. The per-actor `prefab` stamp still appears **inside a `.prefab.json`**, where it marks a nested instance member — `scene_json_validator.Schema` is what keeps the two documents apart while they share one decoder.

### Variants (`base`)

Optional on version 1:

```json
{
  "kind": "com.hikari.prefab",
  "version": 1,
  "id": "enemy_red",
  "name": "Enemy Red",
  "base": "prefabs/enemy",
  "root": "root",
  "actors": [{ "id": "root", "archetype": "empty" }]
}
```

- `base`: extension-free stem or `asset://./…` of the parent prefab.
- Variant `actors` may be a full copy or sparse (only overridden locals + structure adds).
- Resolution (`scene/prefab/resolve.zig`):
  1. Load/resolve `base` recursively (cycle detect + depth limit, default 8).
  2. Start from base’s fully resolved actor list.
  3. Matching local ids: merge authoring fields from variant onto base (Apply-style via `diff.zig`).
  4. Actors only in variant: append (structure add).
  5. Actors only in base: keep.
  6. Result is a fully resolved `PrefabAsset` with variant id/name/root and `base == null`.
- Missing base → **hard error** on resolve.
- Base chain cycle → `PrefabCycle`.
- Editor Open Prefab edits the variant document as-is; `base` is preserved on save and editable via inspector **Base prefab** (`set_prefab_base`).
- Make Prefab writes no `base` by default. **Create Variant…** writes a new prefab with `base` set (optional sparse overrides from a linked instance).

## Shared expand

Module: `src/hikari/src/scene/prefab/`

- `expandToSceneActors(allocator, prefab, options) ![]SceneActorDesc`
- Options: instance `root_id`, optional root `parent_id`, root transform override, `asset_uri` for links, optional `reserved` host ids, optional **`id_probe`**, optional **`loader`** for nested re-hydrate, `max_nest_depth` (default 8).
- Child scene ids: `{root_id}__{local_id}` (collision sanitize with `-N` suffix). Ids over `expand.id_buf_len` (512) fail with `PrefabIdTooLong`.
- Root placement (parent **and** transform) is applied on the instance root whether it expands as a plain actor or as a re-hydrated nested root.
- `reserved` is for small caller-owned sets (an open document). A host with a large live index passes `id_probe` instead — `World.spawnPrefab` does, so a spawn costs one lookup per prefab actor rather than a copy of the whole scene-id index per instance.
- Pure logic — used by editor place/make and `World.spawnPrefab`.

### Nested prefabs

1. A prefab asset may contain actors that are themselves **prefab instance members** (`ScenePrefabLink` pointing at another prefab).
2. Validate allows nested links. Members sharing `prefab.root_id` must share the same `prefab.asset` (`NestedGroupInconsistent` otherwise). Parent-id cycles still rejected.
3. **Make Prefab** allows selection that includes nested instances; nested links are stored (with `root_id` remapped when the nested root is in the subtree). Links for the **selection instance** (same `prefab.root_id` as the selection root) are cleared so co-members become outer-owned locals — only true nested groups (different `root_id` still in the subtree) keep leaf stamps.
4. On **expand** of an outer prefab (when `loader` is set):
   - Remap all scene ids as usual.
   - For each distinct nested instance group (`asset` + template `prefab.root_id`):
     - Load nested asset via loader, **resolve** variants, expand under the remapped nested-root scene id.
     - Apply **overrides** from the outer template’s stored members (`mergeRefresh` vs nested source) onto re-expanded actors.
     - Nested members keep **leaf** prefab links (not the outer asset); `root_id` is the new nested root scene id.
   - Soft path if load fails: keep stored expanded members, remap ids/`root_id`, warn once — content is not hard-broken offline.
5. Cycle detection walks the prefab **asset stem** graph during nested load; depth limit stops runaway nests.
6. Apply / Revert / Unpack still work per **leaf** link on the instance. Unpack clears members matching that instance `root_id` (nested leaves unpack separately).

### Loader

```zig
pub const PrefabLoader = *const fn (ctx: *anyopaque, allocator: Allocator, stem: []const u8) anyerror!PrefabAsset;
// free via prefab.free after use

pub const Loader = struct { ctx: *anyopaque, load: PrefabLoader };

pub const ExpandOptions = struct {
    // …
    loader: ?Loader = null,
    max_nest_depth: u8 = 8,
};
```

World `retainPrefab` / `spawnPrefab` and editor place/make inject a loader that reads cooked `.shinprefab` (AssetStore) first; authoring `.prefab.json` only in Debug. Pure tests supply fixtures without World.

`retainPrefab` resolves variants before caching; expand re-hydrates nested instances via the same loader.

## Hydrate / dehydrate

| Direction | Module | When |
|---|---|---|
| `instances` → actors | `scene/prefab/hydrate.zig` | Every scene load, both runtime and editor |
| actors → `instances` | `editor/scene_document/dehydrate.zig` | Document save |

Hydrate runs **inside the scene decoder**, not in `world.loadScene`: `scene_loader/preload.zig` walks `asset.actors` to retain materials and meshes *before* the world loads, so a member appearing any later would render without its assets resident. Order per instance is fixed — resolve variants → expand (nested instances re-hydrate through the same loader) → drop `removed` → apply overrides → append `added`. Templates are decoded once per stem per load, so twelve lamps cost one decode. A broken instance warns and contributes nothing rather than failing the scene.

Dehydrate needs **no bookkeeping during editing**, which is why `scene_document/{commands,edits_*,apply}.zig` and undo were untouched by this model. Hydrate stamps each actor with its outer authored-instance ownership while retaining the leaf prefab link used by nested prefab operations. Save groups by that ownership stamp, treats an unstamped actor under a member as an `added`, treats an expected template local with no actor as a `removed`, and derives overrides from the diff.

The prefab loader is injected (`prefab.Loader`): runtime passes a store-backed one, the editor a path-backed one, both from `scene/prefab_source.zig`. A scene declaring instances with **no** loader fails with `error.PrefabLoaderRequired` — dropping them silently would open a scene that looks fine and is missing half its content.

## Override model

Module: `src/hikari/src/scene/prefab/patch.zig` (pure; testable without editor UI).

| Concept | Rule |
|---------|------|
| Baseline | Prefab asset actors (local ids) |
| Current | Expanded scene actor with matching `prefab.local_id` |
| Override | One differing **leaf**, addressed by path |
| Identity | `id`, `parent_id`, `prefab` link are never addressable |
| Root TRS | Instance-owned placement, recorded on the instance, never an override |

**Paths are derived from the struct definitions, not from a table.** `classify` treats an optional struct as a component block (`block.field`), special-cases `transform` and `components`, skips identity, and calls everything else a top-level leaf — so a field added to `SceneRenderDesc` is overridable the day it lands. Values are JSON text encoded/decoded by `std.json` over the typed field, so the value format follows the schema for free too. A path that no longer resolves (the template dropped the field) warns and is skipped; it never half-applies.

**Game and plugin components get the same granularity**, addressed as `components.<name>.<key>`. The engine has no schema for those payloads and must not grow one, so the split happens over the JSON object itself — each top-level key of the payload is a leaf, merged back in on apply. A component overriding `components.cube_logic.health` therefore keeps following the template's `max_health`, exactly like `render.material` and `render.casts_shadow`. Three cases stay whole-payload because no key path can express them: the component being added, removed, or holding a non-object payload, and a key the instance *dropped* (JSON `null` is a legal value, so it cannot double as "absent").

`diff.zig` remains for actor-granular work — variant resolution (`mergeApplyOwned`), Apply, and the inspector's override queries.

### Apply

1. Resolve instance root from selection (member or root → `prefab.root_id`).
2. Load current prefab authoring file from disk.
3. Merge non-root-transform instance fields into template locals.
4. **Write asset** (non-undoable file write); preserves variant `base` when present.
5. Plane `refresh_prefab` (or internal refresh from `apply_prefab`) updates **other** instances of the same stem using old→new delta (undoable document txn). Source instance already holds applied values.

Editor entry: `applyPrefabInstance(app, root_or_member_id)`.

### Revert

1. Load prefab asset.
2. For each linked member: `mergeRevertOwned` (root keeps placement).
3. `SceneDocument.replaceActor` per member in one undo transaction.
4. Mutation sink despawn/spawn updates the live edit world.

Editor entry: `revertPrefabInstance(app, id)`.

### Refresh (live document only)

Since every load expands from the template, refresh exists purely to update the *already-open* document, whose actors were expanded before the template changed. Closing and reopening the scene would achieve the same thing.

Plane op `refresh_prefab` (`asset_stem` + `old_utf8` + `new_utf8` + optional `skip_root_id`), used after Prefab Save on the stacked parent scene and by Apply. Internally:

- For each open-document instance of `asset_stem` (optionally skip the Apply source root):
  - For each member: if a field equaled the **old** template, copy from **new**; true overrides stay.
  - Instance root transform always preserved.

### Unpack

Clear `prefab` on every actor in the instance (`root_id` match). Data kept. Undoable via `replaceActor`.

Editor entry: `unpackPrefabInstance(app, id)`.

## Editor UX

All prefab mutations enter the shared plane `editor/authoring/scene_mutation.zig` (bodies in `editor/authoring/prefab_ops.zig`). Chrome and MCP never re-implement document/file logic outside that path.

### Plane ops (MCP `scene_edit` wire names)

| Op | Args | Notes |
|----|------|-------|
| `make_prefab` | `id` (root), `name` | Write `assets/prefabs/<stem>.prefab.json` (non-undoable) + replace subtree with linked expand (undoable). Scene mode only. **Refuses an existing stem** (`DuplicateName`) — the write is not undoable, and `sanitizeStem` collapses "Crate Stack" / "crate stack" onto one file. |
| `place_prefab` | `asset`, optional `parent_id` / `position` | Resolve + expand + insert tree. Scene mode only. |
| `apply_prefab` | `id` (root or member) | Merge instance → asset file (non-undoable); refresh sibling instances (undoable). |
| `revert_prefab` | `id` | Template → instance members (root TRS kept). Undoable. |
| `unpack_prefab` | `id` | Clear prefab links on instance. Undoable. |
| `create_variant` | `base_asset`, `name`, optional `from_instance_id` | Write variant with `base`; optional sparse overrides from instance. File only (non-undoable); refuses an existing stem like `make_prefab`. |
| `set_prefab_base` | `base` string or null | Prefab document mode only; updates `prefab_edit_base` + dirty. |

### Chrome

- **Make Prefab…** (hierarchy context): when authored and not in prefab document mode → name prompt → plane.
- **Create Variant…**: when actor is prefab-linked (base = link stem) or while editing a prefab document (base = open stem) → name prompt → plane → open new file.
- **Place Prefab**: asset browser kind `prefab` → plane `place_prefab`. Disabled while editing a prefab document.
- **Open Prefab**: asset browser double-click; hierarchy **Open Prefab** on a linked instance; inspector **Open Prefab** on the instance card; File → **Close Prefab** to return. Open is chrome document-stack (not a scene mutation).
- **Apply / Revert / Unpack**: hierarchy context menu + inspector prefab card → plane.
- Hierarchy context menu filters rows by actor (not hard-coded indices): Always Focus / Add Child / Rename / Copy / Paste / Duplicate / Delete; Make Prefab only in scene mode; Create Variant when linked or prefab doc; Open/Apply/Revert/Unpack only when `actor.prefab != null`.
- Hierarchy badges: `prefab` = top-level instance root; `nested` = instance root under a different prefab instance; `member` = non-root linked member; layers icon for all linked.
- Inspector override accents (rebuild only, selected actor): section title accent when a whole builtin/open component block differs from the resolved template; Active checkbox accent when differs; Transform section accent only for **non-root** members (root TRS is instance-owned).
- Prefab document: inspector **Base prefab** text field + Clear → plane `set_prefab_base`. Shown only on the prefab's **root** actor (`PrefabDocOpts.root_id`) or on an empty selection — it is a document-level control, and offering "Clear base" beside a banner reads as a property of that banner when it re-parents the entire template.
- Reparent of a linked **member** out of its instance root is rejected.

### Open Prefab document mode

Single `SceneDocument` chrome with a one-deep **document stack** (not a parallel document system):

1. `push` — parent scene `SceneDocument` is stacked; main document is materialised from the prefab asset (local actor ids, path = authoring `.prefab.json`). Nested member links stay for authoring fidelity.
2. Hierarchy/inspector edit the prefab actors as a mini-scene (live preview reloads session from scene-shaped UTF-8 of those actors).
3. **Save** writes prefab format via `prefab.writeUtf8` (not scene JSON), preserving `base` when the opened asset was a variant (or after `set_prefab_base`). On success, parent stack document instances of that stem are refreshed with old→new template delta (same as Apply sibling refresh).
4. **Close Prefab** restores the stacked scene document and reloads the live world from it.

Make / Place remain disabled in prefab mode.

**Two save modes.** `scene_document/io.zig: SaveOptions.prefab_members` picks whether members fold into `instances` (`.fold`, the default, for persistence) or stay ordinary actors (`.expand`). The prefab document's live-preview payload takes `.expand`, because folding would ask the save path to resolve the very templates the document is authoring, which a prefab document has no assets root for.

#### Editing a variant: the base chain is visible

A variant file stores only what it changes, so on its own `street_lamp_festive` opens as three actors floating where a lamp should be — useless for authoring, because inheritance is exactly the thing you need to see. Opening a prefab with a `base` therefore resolves the whole chain, and the two consumers get **different views of it**:

| | holds | why |
|---|---|---|
| **Document** | this file's actors *unmerged*, plus base-only actors appended and **locked** | so save writes back exactly what this file owns |
| **Live preview** | the fully **merged** chain | so the viewport shows the whole lamp |

The split is load-bearing, not tidiness. `resolve.mergeVariantActors` merges **fields**: a variant actor that overrides `light` and inherits `render` comes back from the resolver carrying the base's render block. Put that in the document and the next save writes an inheritance the author never touched into the variant file — silently flattening the chain one actor at a time. So an id the variant owns is taken from the variant, verbatim; only ids it does *not* own come from the resolved chain, and those are locked.

`documentFromPrefabAssetWithContext` builds it; a chain that will not load warns and degrades to the raw file rather than refusing to open.

**The chain, in the hierarchy.** A variant is additive, so a flat merged list cannot answer *who injects what*. `SceneDocument.origin_levels` records the chain — furthest ancestor first, this file last, `own_origin` indexing this file — and `actor_origin` credits each actor to the **deepest** file that mentions it. Deepest, not first: if the base declares `bulb` and the variant overrides it, the variant is where you edit it, so that is where it belongs. That rule is what makes "which section is it in" and "can I edit it" the same question.

`model.appendChainSections` splits the prefab's layer into one hierarchy section per file, top to bottom in composition order, badged `base` / `base 2` / … / `editing`. A chain file contributing nothing visible still gets a header row. `prefab_document.walkChain` builds the provenance by walking `base` a second time, because `resolvePrefab` returns only the merged result.

**Locked actors** are context: visible, selectable, inspectable, never edited, never saved. Locked is *derived* (`origin != own_origin`), not stored, so editability and the chain sections cannot drift apart.

- `scene_mutation.applyOne` rejects any operation whose `Operation.actorId()` is locked with `ErrorCode.read_only`. It gates the **subject**, not the parent: `create` / `insert` / `insert_tree` under a locked actor stay legal, because adding a child adds to the variant.
- `prefabAssetFromDocument` skips locked actors and sizes its output to the owned count (`prefab.free` releases `asset.actors` as a slice).
- The gizmo never offers a locked actor: `selection_flow.refreshOverlay` clears the target for a locked primary and `snapshotDragGroup` skips locked ids in a multi-select, so a drag cannot open a transaction the plane rejects every frame.
- Chrome hides destructive context-menu rows, the inspector builds `read_only` (the same mode Play uses), and the hierarchy row dims and takes prefab amber on its glyph and `base` badge.

#### Prefab mode chrome

`editor_app/prefab_chrome.zig` states the mode three ways: a 2pt amber frame inside the viewport rect, a band between the viewport sub-toolbar and the 3D view naming the template and the scene it returns to (with **Close Prefab**), and the toolbar Save button re-labelled **Save Prefab**. The hue is `asset_browser.kindColor(.prefab)`. Save changes its word only, never its variant (the accent already means "unsaved authoring"). The chrome is derived from `document_kind` every frame, not latched by the open/close paths, so a discarded dirty prompt, a rolled-back open, or a project close all update it, and it re-bakes after a live theme switch.

## Runtime SDK

```zig
const ref = hi.PrefabRef.must("prefabs/crate_stack");
const handle = try hi.world().retainPrefab(ref);
defer hi.world().releasePrefab(handle);
const root = try hi.world().spawnPrefab(handle, .{
    .position = .{ 0, 1, 0 },
    .rotation_euler = .{ 0, 45, 0 },
    .scale = .{ 1, 1, 1 },
    .parent = .invalid,
    .active = true,
});
// Member lookup uses expand mint: "{root_scene_id}__{local_id}"
const lid = hi.world().findPrefabMember(root, "lid");
try hi.world().despawnPrefabInstance(root);
```

- `retainPrefab` loads/decodes once (refcounted cache on `World`); resolves variants; nested links stay until expand. Cooked `.shinprefab` first; authoring JSON only in Debug.
- `spawnPrefab` expands with unique root id + nested loader, batch-friendly spawn, parents attached after members exist.
- Soft content refs on leaves match scene load (error mesh / `_missing` archetype).

### Bulk spawn job

World-owned, multi-frame, one job at a time. Template is re-retained for the job duration so the game may release its handle early.

```zig
const job = try hi.world().beginPrefabSpawnJob(handle, .{ .budget_ns = 2_000_000 });
try hi.world().prefabSpawnEnqueue(job, .{ .position = p0 });
try hi.world().prefabSpawnEnqueue(job, .{ .position = p1 });
// each frame:
_ = try hi.world().pumpPrefabSpawnJob(job); // true when complete
const roots = hi.world().prefabSpawnResults(job); // []ActorRef so far
// when done:
hi.world().endPrefabSpawnJob(job);
```

- Each pump spawns full instances via existing `spawnPrefab` (one expand + batch entity allocate per instance) until `budget_ns` is exhausted (`0` = one instance per pump).
- Results accumulate until `endPrefabSpawnJob`. Result storage is sized at `prefabSpawnEnqueue` (it can never exceed the queue), so a slice from `prefabSpawnResults` stays valid across any number of pumps — a later enqueue or `endPrefabSpawnJob` invalidates it.

### findPrefabMember

Runtime entities do **not** store `ScenePrefabLink`. Lookup uses the expand mint convention:

- Root: ActorRef returned by `spawnPrefab` / job results.
- Child: `findPrefabMember(root, local_id)` → `findBySceneId("{root_scene_id}__{local_id}")`.
- Collision suffixes (`-N`) are not searched.
- Nested member locals are relative to the **nested** expand root; deep nested mints follow the same `{parent_root}__{local}` pattern.

Cook does not emit dependency edges for `base` or nested `prefab.asset`, so re-cooking a base does not invalidate variants. Product staging (`StageCookedPrefabs`) ships the whole `prefabs/` tree.

## Sample

`hikari-games/example/scenes/prefab_showcase.json` — a street of lamps and crates
where every prop is an instance: plain instances, component and transform
overrides, a variant (`street_lamp_festive`), and a nested instance
(`lamp_corner` contains a `street_lamp`). Templates live in
`hikari-games/example/assets/prefabs/`. The scene file stores **unexpanded**
`instances` + overrides; the engine hydrates at load. Generator:
`hikari-games/example/tools/gen_prefab_showcase.py`. The scene README walks through
Open Prefab / Apply / Revert / Unpack / Create Variant on it.

## Invariants

- One expand + resolve + diff core under `scene/prefab/` — never expand/diff-only logic solely under `editor/`.
- Flat actor lists + `parent_id` only.
- Nested expand is O(n) with maps; recursive with depth limit + asset-stem cycle detection; no per-frame work.
- `validateAlloc` is O(n) (three-colour parent walk over an index table). No per-actor hashmap, and expand validates once per level — never twice at the same depth.
- Prefab writer changes are guarded by comptime field-count tripwires in `scene/prefab/asset.zig`, the same contract `editor/scene_document/serialization.zig` carries. A new `SceneActorDesc` / render / physics / render-params field must be emitted there or the build fails.
- Plane op bodies are `!void` with ordinary `errdefer`; only the thin public wrapper maps to `plane.ErrorCode`.
- No per-frame prefab work; spawn / apply / revert are explicit.
- Authoring structural/field edits go through document commands (`replaceActor`, mutation plane) so live sink + undo stay consistent.
- Game modules see only `PrefabRef` / `PrefabHandle` / `ActorRef` via HostApi (no new SDK surface required for nested/variants).
- Apply file write is intentionally non-undoable; document-side refresh/revert/unpack are undoable.
