Skip to content

EBX structural editing: add/remove components, generic array mutation, and a reference picker #397

Description

@Powback

Design doc: docs/ebx-editing.md (branch docs/ebx-editing, not merged).

Research + design for Unity-like structural editing in the inspector: add a light to a prefab, delete a component, append/remove/reorder array elements, and pick a reference target from a list instead of typing a guid. Companion to docs/entity-wiring-and-networking.md (G1–G11) and docs/bake-pipeline.md.

The doc separates verified claims (read out of code, the VU fb reference, Rime's generated types, or measured over the 73 475-file Venice-EBX corpus) from inferred ones, and lists what still needs the running game.

What the runtime allows

Construct any Frostbite container type at runtime ✅ verified — WorldPartData(), ReferenceObjectData() etc. already used in LevelInjector
array:add(v) / array:clear() / arr[i] = v / #arr ✅ verified in-repo
Remove one element / insert / reorder no API observed anywhere — must be clear() + re-add()
Express any structural edit through SetEBXFieldCommand ❌ the wire format is field-name recursion terminating in a scalar; it has no verb but assign

Three findings that shape the design

1. "Add a component" is three different operations. GameEntityData.components / ComponentData.components are the Unity analogue; PrefabBlueprint.objects is the prefab's contents; and ObjectBlueprint.object is singular — it has no array at all. 7 998 of the 70 093 indexed partitions have an ObjectBlueprint primary instance, so this is the common case, not an edge case.

2. Array position is load-bearing in four separate ways. Measured over Venice-EBX:

  • indexInBlueprint == array position in objects — 2 861/2 861 in a 4 000-file sample
  • indexInBlueprint == 65535 is a "not in objects" sentinel — 9 252 files
  • PhysicsPartInfo.partComponentIndex is a positional index into components[]6 577 files
  • runtimeComponentCount is not #components — 366 vanilla counterexamples, so it must not be auto-derived

A naive append/remove/reorder produces data that loads, renders, and is silently wrong.

3. Connection endpoints are mostly cross-partition. Classifying ~165 000 connections by endpoint locality, F→F (neither endpoint in the bus's own partition) is the largest bucket for EventConnection (47 538) and LinkConnection (32 327). So a same-partition endpoint picker is not sufficient, and deleting an object can break a bus in a partition the object has no back-reference to. (This corrected an assumption made earlier in the same investigation.)

Also: a blueprint's objects array never leaves its own partition — 0 of 195 440 members. "Add a component" therefore means copy an instance into this partition, never point at one elsewhere.

Two things already built and unused

  • G6 (field-id hashes shown raw) is already solved by shipped data. WebUI/public/data.zip contains EventHashes.json with 19 704 entries; FrostbiteDataManager loads it into this.eventHashes and _HandleFile's switch has no case for it, so it is never read. It resolves every id quoted in the wiring doc. Separately, Rime's fb::hashQuick (FNV-1 variant, basis 0x1505, prime 0x21, reinterpreted as int32) was checked and exactly reproduces Geometry838548383, OnCaptured2099208964, CaptureRadius1043301209, AlternativeSpawnPoints-2001390482. So authoring a connection from a field name needs no lookup table at all. G6 should be re-scored from "Med" to a ~half-day WebUI change.
  • ExplorerComponent.vue is 90 % of the reference picker — a searchable, icon-decorated, type-labelled virtualized list over fbdMan.partitions. It needs a modal host and a type filter, not a rewrite.

Also worth recording

docs/entity-wiring-and-networking.md lists G2 and G3 as open; both are fixed in the current treeInvokeBlueprintSpawnFromClone now honours needNetworkId (GameObjectManager.lua:1164) and GameObject:Destroy destroys only isEditorSpawned entities (GameObject.lua:242-248). That doc should be updated.

Proposed phasing (≈29 d total; see §11)

  • Phase 0 (≈1.5 d) — wire up EventHashes.json, emit concrete endpoint types, ship hashQuick, ship the type graph. Cannot break a level; worth doing regardless of the rest.
  • Phase 1 (≈4 d) — reference picker (offline + same-partition scope, no ext change), and "remove" implemented as excluded = true, which is a scalar edit that is undoable, bakeable, and Apply-replayable today. Covers 126 of the 475 GameObjectData types.
  • Phase 2 (≈8.5 d) — real array mutation, per-instance clones only, including the index-coupling fixups from finding 2.
  • Phase 3 (≈5 d) — add component.
  • Phase 4 (≈6 d) — connection authoring (G1/G5/G7), needing the cross-partition endpoint scan.
  • Phase 5 (≈4 d, gated) — blueprint-wide structural edits, blocked on partition shadowing being proven (bake-pipeline.md §5 flags it as unverified).

Open questions before implementation

Cheap, no game needed:

  • Q10 (blocking any reorder): is indexInBlueprint the array position, level-global, or position-plus-base? The corpus measurement (2 861/2 861 positional) and entity-wiring-and-networking.md §1.5 (disjoint ranges 148–469 for Buildings) disagree, and they cannot both hold. ~30 min of grepping.

Need the running game (§10 Q1–Q9), most importantly:

  • Q1: array element edits index with a string (array["1"], EBXManager.lua:49, never tonumber'd). If VEXT does not coerce, array editing is already broken today and nobody has noticed — there is no e2e coverage of it.
  • Q3: is clear() + re-add() safe on a MakeWritable'd loaded array? VU changeset 18675 records fixing memory corruption / UB in "frostbite type array operations".
  • Q4: does SerializeCloneSubtree capture a container appended to a clone after cloning? (Bake correctness.)

Not requesting a merge — the doc is for review and to make the plan arguable before any code is written.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions