Blacksite/docs/adr/0027-stable-prefab-ownership-and-variants.md
Rbanh 0553a85220
Some checks are pending
CI / Format, lint, test, build (push) Waiting to run
Build production-ready editor authoring workflows
2026-07-11 12:41:04 -04:00

90 lines
6.2 KiB
Markdown

# ADR 0027: Stable Prefab Ownership, Overrides, And Variants
## Status
Accepted
## Context
ADR 0005 established stable asset-registry identity and linked `PrefabInstance` roots, but the
first structural boundary treated every descendant of an instance as generated source content.
That prevented an authored scene or prefab from adding local children, nesting another linked
instance, or expressing a variant without flattening the source prefab.
Bevy allocates `Entity` identifiers at runtime, and display names can change. Neither is a durable
key for an override, especially when multiple nested prefab layers contain the same source
`ActorId`. Overrides must also produce the same result in a packaged game that does not include the
editor. Finally, publishing an override to a source file crosses the in-memory undo boundary and
must not overwrite unrelated external edits.
## Decision
- Keep `PrefabInstance { asset_id, source_path, overrides_ron }` as the authored, serialized link.
Its source path remains project-relative and under `assets/`.
- Tag entities spawned from that link with runtime-only `HydratedPrefabMember { instance_root }`.
Tagged source members are excluded from scene, recovery, and prefab serialization. The linked
root, authored local descendants, and nested `PrefabInstance` actors remain editable and
serializable.
- Parse `PrefabInstance` components through `SceneDocument` and validate the dependency graph
recursively. Reject empty asset IDs, unsafe or non-`.scn.ron` paths, missing sources, dependency
cycles, authored actors without `ActorId`, and duplicate `ActorId` values within one document.
Editor load, prefab save, project inspection, and level validation use this structured path.
- Store overrides in the versioned, shared `PrefabOverrides` contract. A `PrefabActorPath` combines
an `instance_chain` of nested link-anchor `ActorId` values with the target actor's `ActorId`, so
repeated IDs in different nested documents remain unambiguous. Runtime override application
lives in `shared` hydration rather than the editor.
- Property overrides carry a reflected component type, property path, base component value, and
replacement component value.
- Component overrides add, replace, or remove a reflected component and retain its base value.
- Structural overrides remove or reparent a source actor. The schema reserves local-child
attachment, but the editor does not expose attachment directly to a generated member until its
reload and ownership lifecycle is safe.
- Retain the v1 root transform, material, and child-visibility fields for wire compatibility. New
authoring uses stable property/component/structural operations; legacy child visibility remains
keyed by `ActorId`, with name keys read only as a compatibility fallback.
- Resolve a composed result in increasing specificity: source document, each nested link's override
layer, the containing variant document and its authored local structure, then the placed
instance's overrides. A containing layer may target a nested source actor through its stable
`instance_chain` without flattening that source.
- Distinguish **instance override** from **Apply to source**. Editing a linked member records an
undoable operation in the containing `PrefabInstance`. Applying a direct target to source patches
that source document. Applying a nested target transfers the operation into the immediate nested
link stored by the containing source; it does not skip the composition layer and mutate a leaf
prefab unexpectedly.
- Validate the complete graph and write source changes atomically. The source-apply history command
stores the exact source bytes before and after the write plus the instance state. Undo and redo
proceed only when the current file exactly matches the expected side of that command; an external
change blocks the history operation instead of being overwritten.
- Record a transitive deterministic source-graph revision and the base values needed for three-way
conflict checks. Health distinguishes loading, ready, untracked, changed-base, semantic/stale
conflict, broken-source, and malformed-override states. Conflict recovery can rebase while keeping
overrides, take the current source for conflicting operations, remove legacy stale targets,
retry a repaired source, or relink to another validated prefab.
- Make **Unpack Layer** an undoable outer-layer conversion: remove the selected instance link and
retain its hydrated members as authored local actors, while preserving nested `PrefabInstance`
links. Make **Convert to Local** the explicit recursive conversion: remove all nested links in the
selected instance result, assign fresh local `ActorId` values, and store snapshots so redo does not
depend on the source files.
- Define a variant as an ordinary prefab document whose selected root is itself a linked
`PrefabInstance`. Saving the selection serializes that link, its override layer, and authored local
descendants/nested instances while excluding hydrated source members. No separate variant asset
type is introduced.
## Consequences
- Nested prefab graphs and variants preserve their source links instead of duplicating generated
content into every owner file. The same shared override data is applied by editor and game
hydration.
- Renaming a source actor does not break current overrides while its `ActorId` and nested anchor
chain remain stable. Moving an actor between documents or replacing an anchor is an identity
change and can surface a recoverable conflict.
- Instance-local edits and Revert operations remain ordinary editor history. **Apply to source** is an
explicit asset mutation with stricter exact-file undo/redo preconditions.
- Generated members may be removed or reparented inside the same prefab layer through structural
overrides. Cross-layer parenting, generated sibling-order overrides, and direct authored-local
attachment to generated members remain intentionally blocked until their persistence semantics
are defined.
- **Unpack Layer** preserves reusable nested links; **Convert to Local** intentionally severs all
links and creates a source-independent authored snapshot. The destructive scope is visible in the
command name rather than inferred.