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

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 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.