Repository navigation
Description
Problem
Adding a single ECS component currently requires hand-editing ~6 touch-points across multiple files (documented in CLAUDE.md → "Cross-binding check"). Miss one and scripting / scene saves / save-games / runtime copies silently drop the component, or the build fails to link:
Scene/Components.h— the struct and theAllComponentstuple. (guarded byComponentTupleCoverageTest)Scene/Scene.cpp—OnComponentAdded<T>specialization andOLO_ON_COMPONENT_REMOVED_NOOP(T). (link error if missing — not silent)Scene/SceneSerializer.cpp—SerializeEntity(write) +DeserializeEntities(read). (not guarded)- SaveGame — 3 edits across 2 files:
SaveGameComponentSerializer.{h,cpp}(decl + def +RegisterAll()), and theSAVE_COMPONENT(...)+TRY_LOAD_COMPONENT(...)enumeration lists inSaveGameSerializer.cpp. (silently dropped on save/load if missed) Scripting/C#/Generated/— driven byOLO_PROPERTYannotations via OloHeaderTool. (generated)Scripting/Lua/LuaScriptGlue.cpp::RegisterAllTypes()— Sol2 usertype. (intentionally not completeness-guarded)
The coverage tests (ComponentTupleCoverageTest, ComponentSerializerCoverageTest) catch some misses after the fact, but several touch-points (SceneSerializer, SaveGame enumeration lists) are unguarded, and the whole thing is tedious and error-prone.
Proposal
Extend OloHeaderTool (or add a sibling generator) to emit the mechanical, list-shaped touch-points from the OLO_PROPERTY-annotated component struct rather than requiring hand edits — collapsing ~6 manual edits toward one annotated struct:
- The SaveGame
SAVE_COMPONENT/TRY_LOAD_COMPONENTenumeration lists + theSaveGameComponentSerializerdecl/def/RegisterAll()registration. - The
SceneSerializerSerializeEntity/DeserializeEntitiesfield read/writes (where field types are serializable per their annotation). - Optionally the
AllComponentstuple entry and theOnComponentAdded/OnComponentRemovedno-op specializations.
OloHeaderTool already scans OLO_PROPERTY and emits C#/C++ glue, so the annotation source of truth and the scanning infrastructure already exist.
Why codegen over the current "guard with tests" approach
The coverage tests detect a forgotten touch-point; codegen prevents it, and removes the manual labor entirely. Components added via annotation would round-trip through scene-YAML, save-games, copy/prefab, and scripting with no extra edits.
Scope / risks to evaluate when picking this up
- Field-type serialization coverage: which
OLO_PROPERTYfield types can be auto-serialized vs need custom handling (e.g. asset handles, nested structs,Ref<T>). - Components with non-trivial init/teardown still need a hand-written
OnComponentAdded<T>body — codegen should emit the no-op only when none exists. - Keep the existing coverage tests as a backstop against codegen gaps.
Filed as a follow-up to productivity work on generated-file merge conflicts and the test catalogue. Not urgent; to be picked up separately.
Score
capability: 5
craft: 1
stability: 8
decay: 3
effort: 3
confidence: 0.8
learning: 3
fun: 3
kano: table-stakes
blocked_by: []
blocks: []
Rated per issue-scoring · score = confidence × (capability + craft + stability + decay) / effort, derived by the picker.
Activity
coderabbitai commentedon Jun 23, 2026
🔗 Related PRs
#226 - Expose SoundGraphHandle for scripting [merged]
#330 - Fix/savegame component coverage [merged]
#340 - feat(physics): add CollideConnected toggle for joints (
#308item 1) [merged]#386 - Refactor ECS component management: automate AllComponents tuple gener… [merged]
#405 - Codegen: generate the SaveGame component capture/restore lists (
#380) [merged]🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
drsnuggles8 commentedon Jun 24, 2026
First slice landed: the
AllComponentstuple is now generatedBranch
feature/codegen-component-touchpoints(implemented + locally verified green; PR pending). This proves the codegen approach end-to-end on the most mechanical of the six touch-points.What changed
OloHeaderToolnow collects everystruct *Componentdefinition underOloEngine/src(definition-aware + comment-aware, so forward declarations and commented-out mentions are skipped) and emits the type list to a new generated fileOloEngine/src/OloEngine/Scene/Generated/AllComponents.Generated.inl.Scene/Components.hdrops its ~100-line hand-maintainedusing AllComponents = ComponentGroup<…>;block and#includes the generated file instead.OHT_SCENE_OUTcache var wires the Scene-domain output into the existingGenerateBindingstarget (kept separate fromScripting/C#/Generated/so the include path is clean).IDComponent,TagComponent,UIResolvedRectComponent,*StateComponent) is reproduced in the generator (kComponentsNotInTuple) — kept in sync withComponentTupleCoverageTest::kNotInTuple.Verification
ComponentTupleCoverageTestupdated to read the generated file and passes both directions; full suite 3433 passed, 0 failed (4 env-gated skips).Design note: the generator scans recursively (auto-discovers new components), while the coverage test keeps a curated header-root list — intentional tension, so a component added in a header the test doesn't yet list surfaces loudly via the test's inverse check rather than silently.
Touch-point status
AllComponentstuple (Scene/Components.h) — retired into codegenSAVE_COMPONENT/TRY_LOAD_COMPONENTtwin lists (SaveGameSerializer.cpp) — recommended next; this is the documented silent-drop bug Save-games silently drop 28 component types: registered in SaveGameComponentSerializer but missing from SAVE_COMPONENT/TRY_LOAD_COMPONENT lists #325, but depends on the field-order-sensitiveSerializeoverloadsSaveGameComponentSerializer.{h,cpp}ar << fieldboilerplateSceneSerializer.cppper-component YAML — hardest (asset handles,Ref<T>, glm, nested structs); field-type serialization deliberately kept out of early slicesOnComponentAdded/Removed<T>bodies — domain-specific, not auto-generatableLuaScriptGlue.cpp::RegisterAllTypes— intentionally not completeness-guardedLeaving this epic open for the remaining slices.
drsnuggles8 commentedon Jun 26, 2026
Second slice done: the SaveGame
SAVE_COMPONENT/TRY_LOAD_COMPONENTenumeration lists are now generated.Following the AllComponents-tuple slice (#386), OloHeaderTool now also generates the two per-entity save-game enumeration lists in
SaveGame/SaveGameSerializer.cpp— the most dangerous unguarded touch-points, where a component missing from either list was silently dropped from every save-game (it round-tripped fine through scene YAML, so no error/warning).What landed:
SaveGame/Generated/SaveGameComponent{Capture,Restore}.Generated.inlfrom the samestruct *Componentscan as the tuple, minus a save-game-specific exclusion setkComponentsNotInSaveGame(components with noSaveGameComponentSerializer::Serialize()overload). That set deliberately differs fromkComponentsNotInTuple: save-games keepIDComponent/TagComponentbut drop the runtime*StateComponent/UIResolvedRectComponentplusAudioSoundGraphComponent/LocalizedTextComponent(tuple members with no save serializer yet).SaveGameSerializer.cpp#includes the generated lists in place of the two ~97-entry hand lists.SaveGameComponentSerializerCoverageTestnow parses the generated.inlfiles (the documented "touch-point moved into a generated file → repoint the grep" gotcha).Net effect: adding a saveable component now needs only the
Serialize()overload +REGISTER_SAVE_COMPONENT— the capture/restore lists update automatically on the nextGenerateBindingsrun.Still open (later, riskier slices — leaving this issue open):
SceneSerializer.cppper-fieldSerializeEntity/DeserializeEntitiesblocks.SaveGameComponentSerializer::Serialize()overloads (need field-type introspection).RegisterAll()in once the overloads are generated.Merge pull request #405 from drsnuggles8/feature/savegame-component-l…
drsnuggles8 commentedon Jun 27, 2026
The second slice documented in the comment above (generated SaveGame
SAVE_COMPONENT/TRY_LOAD_COMPONENTlists) landed as #405 (merged 2026-06-27); worktree removed.Remaining slices still open:
SceneSerializer.cppper-fieldSerializeEntity/DeserializeEntitiesblocks, and the per-componentSaveGameComponentSerializer::Serialize()overloads (need field-type introspection). Leaving this umbrella issue open.Merge pull request #413 from drsnuggles8/feature/codegen-oncomponent-…
drsnuggles8 commentedon Jun 27, 2026
Third codegen slice landed: #413 (merged 2026-06-27) generates the
Scene::OnComponentAdded<T>/OnComponentRemoved<T>no-op specializations from thestruct *Componentscan (intoScene/Generated/OnComponent{Added,Removed}.Generated.inl). With the earlier slices — theAllComponentstuple (#386) and the SaveGameSAVE_COMPONENT/TRY_LOAD_COMPONENTcapture/restore lists (#405) — three of the list-shaped touch-points are now codegen'd.Still hand-maintained (issue stays open):
SceneSerializer.cppSerializeEntity/DeserializeEntitiesfield read/writes, theSaveGameComponentSerializer.{h,cpp}decl/def/RegisterAll()registration, and the LuaRegisterAllTypes()usertype (intentionally unguarded). Flagging the advance from the merged worktree cleanup; leaving open for the remaining touch-points.drsnuggles8 commentedon Jun 28, 2026
Fourth slice landed — #450 (
feat(codegen): generate SceneSerializer blocks for trivial components).OloHeaderTool now generates the per-component scene-YAML serialize/deserialize blocks, collapsing the last big unguarded hand-maintained touch-point (
Scene/SceneSerializer.cpp). A forgotten field was silent scene-data loss; codegen prevents the miss.CollectComponentFields), distinct from theOLO_PROPERTYscan — the serializer persists every field, not just script-exposed ones.Scene/Generated/Scene{Serialize,Deserialize}Components.Generated.inl,#included bySceneSerializer.cpp; the 26 migrated hand-written blocks removed.glm::vec*/std::string; anything with a non-trivial field is classified non-trivial and stays hand-written automatically.kComponentsCustomSerialize(4th distinct exclusion set) keeps the trivial-but-custom ones (range clamps /Sanitize/ runtime fields) hand-written.ComponentSerializerCoverageTest: existence corpus now spanscpp ∪ generated .inl, plus a new disjointness test (NoComponentIsBothHandWrittenAndGenerated) — every component handled by exactly one path, drop and double-emit both fail loudly.Not closing #380 — the harder ~40% of SceneSerializer codegen (asset handles,
Ref<T>, enums,std::vector, nested structs, setter-onlySceneCamera, custom-validation trivial components) remains, plus the SaveGameSerialize()/RegisterAll()body codegen. Tracked in the follow-up issue below.Merge pull request #450 from drsnuggles8/feature/codegen-scene-serial…
drsnuggles8 commentedon Jun 29, 2026
Cross-link: #451's first slice (
AssetHandle/UUIDscene-serializer codegen) shipped in #467 — the serializer per-component blocks now generate for components whose only non-primitive fields are asset handles / entity-reference UUIDs. Remaining complex field types tracked in #451.drsnuggles8 commentedon Jul 6, 2026
All four of this issue's original hand-maintained ECS touch-points are now codegen-collapsed: the
AllComponentstuple, the SaveGameSAVE_COMPONENT/TRY_LOAD_COMPONENTlists, theOnComponentAdded<T>/OnComponentRemoved<T>no-op specializations, and theSceneSerializertrivial-component serialize/deserialize blocks (all generated by OloHeaderTool, guarded by coverage tests). The only remaining cross-binding touch-point (LuaScriptGlue.cpp::RegisterAllTypes) is intentionally NOT completeness-guarded per CLAUDE.md, since many components legitimately aren't Lua-exposed. Remaining complex-field-shape work is already tracked under #451, which per a survey just posted there (#451 (comment)) is itself currently hypothetical with no blocked real component. Recommending this umbrella for closure — flagging for confirmation rather than closing outright.drsnuggles8 commentedon Jul 7, 2026
Closing per the 2026-07-06 status: all 4 of this issue's original hand-maintained ECS touch-points are codegen-collapsed —
AllComponentstupleSAVE_COMPONENT/TRY_LOAD_COMPONENTlistsOnComponentAdded<T>/OnComponentRemoved<T>no-op specializationsSceneSerializertrivial-component serialize/deserialize blocks— all generated by OloHeaderTool and guarded by coverage tests. The only remaining cross-binding touch-point (
LuaScriptGlue.cpp::RegisterAllTypes) is intentionally not completeness-guarded per CLAUDE.md.Remaining complex-field-shape work stays tracked under #451, which its own survey found to be currently hypothetical (no real component blocked by it) — rescored down accordingly so it doesn't outrank actionable work.