6.2 KiB
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 underassets/. - 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 nestedPrefabInstanceactors remain editable and serializable. - Parse
PrefabInstancecomponents throughSceneDocumentand validate the dependency graph recursively. Reject empty asset IDs, unsafe or non-.scn.ronpaths, missing sources, dependency cycles, authored actors withoutActorId, and duplicateActorIdvalues within one document. Editor load, prefab save, project inspection, and level validation use this structured path. - Store overrides in the versioned, shared
PrefabOverridescontract. APrefabActorPathcombines aninstance_chainof nested link-anchorActorIdvalues with the target actor'sActorId, so repeated IDs in different nested documents remain unambiguous. Runtime override application lives insharedhydration 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_chainwithout 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
PrefabInstancelinks. Make Convert to Local the explicit recursive conversion: remove all nested links in the selected instance result, assign fresh localActorIdvalues, 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
ActorIdand 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.