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 Geometry → 838548383, OnCaptured → 2099208964, CaptureRadius → 1043301209, 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 tree — InvokeBlueprintSpawnFromClone 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.
Design doc:
docs/ebx-editing.md(branchdocs/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) anddocs/bake-pipeline.md.The doc separates verified claims (read out of code, the VU
fbreference, 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
WorldPartData(),ReferenceObjectData()etc. already used inLevelInjectorarray:add(v)/array:clear()/arr[i] = v/#arrclear()+ re-add()SetEBXFieldCommandThree findings that shape the design
1. "Add a component" is three different operations.
GameEntityData.components/ComponentData.componentsare the Unity analogue;PrefabBlueprint.objectsis the prefab's contents; andObjectBlueprint.objectis singular — it has no array at all. 7 998 of the 70 093 indexed partitions have anObjectBlueprintprimary 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 positioninobjects— 2 861/2 861 in a 4 000-file sampleindexInBlueprint == 65535is a "not in objects" sentinel — 9 252 filesPhysicsPartInfo.partComponentIndexis a positional index intocomponents[]— 6 577 filesruntimeComponentCountis not#components— 366 vanilla counterexamples, so it must not be auto-derivedA 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 forEventConnection(47 538) andLinkConnection(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
objectsarray 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
WebUI/public/data.zipcontainsEventHashes.jsonwith 19 704 entries;FrostbiteDataManagerloads it intothis.eventHashesand_HandleFile's switch has nocasefor it, so it is never read. It resolves every id quoted in the wiring doc. Separately, Rime'sfb::hashQuick(FNV-1 variant, basis0x1505, prime0x21, reinterpreted as int32) was checked and exactly reproducesGeometry→838548383,OnCaptured→2099208964,CaptureRadius→1043301209,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.vueis 90 % of the reference picker — a searchable, icon-decorated, type-labelled virtualized list overfbdMan.partitions. It needs a modal host and a type filter, not a rewrite.Also worth recording
docs/entity-wiring-and-networking.mdlists G2 and G3 as open; both are fixed in the current tree —InvokeBlueprintSpawnFromClonenow honoursneedNetworkId(GameObjectManager.lua:1164) andGameObject:Destroydestroys onlyisEditorSpawnedentities (GameObject.lua:242-248). That doc should be updated.Proposed phasing (≈29 d total; see §11)
EventHashes.json, emit concrete endpoint types, shiphashQuick, ship the type graph. Cannot break a level; worth doing regardless of the rest.excluded = true, which is a scalar edit that is undoable, bakeable, and Apply-replayable today. Covers 126 of the 475GameObjectDatatypes.bake-pipeline.md§5 flags it as unverified).Open questions before implementation
Cheap, no game needed:
indexInBlueprintthe array position, level-global, or position-plus-base? The corpus measurement (2 861/2 861 positional) andentity-wiring-and-networking.md§1.5 (disjoint ranges 148–469 forBuildings) disagree, and they cannot both hold. ~30 min of grepping.Need the running game (§10 Q1–Q9), most importantly:
array["1"],EBXManager.lua:49, nevertonumber'd). If VEXT does not coerce, array editing is already broken today and nobody has noticed — there is no e2e coverage of it.clear()+ re-add()safe on aMakeWritable'd loaded array? VU changeset 18675 records fixing memory corruption / UB in "frostbite type array operations".SerializeCloneSubtreecapture 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.