Skip to content

Codegen: collapse the six-touchpoint ECS component registration via OloHeaderTool #380

Closed

Description

@drsnuggles8

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:

  1. Scene/Components.h — the struct and the AllComponents tuple. (guarded by ComponentTupleCoverageTest)
  2. Scene/Scene.cpp — OnComponentAdded<T> specialization and OLO_ON_COMPONENT_REMOVED_NOOP(T). (link error if missing — not silent)
  3. Scene/SceneSerializer.cpp — SerializeEntity (write) + DeserializeEntities (read). (not guarded)
  4. SaveGame — 3 edits across 2 files: SaveGameComponentSerializer.{h,cpp} (decl + def + RegisterAll()), and the SAVE_COMPONENT(...) + TRY_LOAD_COMPONENT(...) enumeration lists in SaveGameSerializer.cpp. (silently dropped on save/load if missed)
  5. Scripting/C#/Generated/ — driven by OLO_PROPERTY annotations via OloHeaderTool. (generated)
  6. 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_COMPONENT enumeration lists + the SaveGameComponentSerializer decl/def/RegisterAll() registration.
  • The SceneSerializer SerializeEntity / DeserializeEntities field read/writes (where field types are serializable per their annotation).
  • Optionally the AllComponents tuple entry and the OnComponentAdded/OnComponentRemoved no-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_PROPERTY field 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

  1. coderabbitai commented on Jun 23, 2026

    @coderabbitai
    Contributor
    🔗 Related PRs

    #226 - Expose SoundGraphHandle for scripting [merged]
    #330 - Fix/savegame component coverage [merged]
    #340 - feat(physics): add CollideConnected toggle for joints (#308 item 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:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

  2. drsnuggles8 commented on Jun 24, 2026

    @drsnuggles8
    OwnerAuthor

    First slice landed: the AllComponents tuple is now generated

    Branch 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

    • OloHeaderTool now collects every struct *Component definition under OloEngine/src (definition-aware + comment-aware, so forward declarations and commented-out mentions are skipped) and emits the type list to a new generated file OloEngine/src/OloEngine/Scene/Generated/AllComponents.Generated.inl.
    • Scene/Components.h drops its ~100-line hand-maintained using AllComponents = ComponentGroup<…>; block and #includes the generated file instead.
    • New 4th CLI arg / OHT_SCENE_OUT cache var wires the Scene-domain output into the existing GenerateBindings target (kept separate from Scripting/C#/Generated/ so the include path is clean).
    • The runtime-only exclusion set (IDComponent, TagComponent, UIResolvedRectComponent, *StateComponent) is reproduced in the generator (kComponentsNotInTuple) — kept in sync with ComponentTupleCoverageTest::kNotInTuple.

    Verification

    • Generated tuple is byte-for-byte the same 96-component set as the old hand-written tuple (102 definitions − 6 runtime-only). Scripting bindings emitted unchanged (no regression).
    • ComponentTupleCoverageTest updated 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

    • AllComponents tuple (Scene/Components.h) — retired into codegen
    • SaveGame SAVE_COMPONENT / TRY_LOAD_COMPONENT twin 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-sensitive Serialize overloads
    • SaveGameComponentSerializer.{h,cpp} ar << field boilerplate
    • SceneSerializer.cpp per-component YAML — hardest (asset handles, Ref<T>, glm, nested structs); field-type serialization deliberately kept out of early slices
    • OnComponentAdded/Removed<T> bodies — domain-specific, not auto-generatable
    • LuaScriptGlue.cpp::RegisterAllTypes — intentionally not completeness-guarded

    Leaving this epic open for the remaining slices.

  3. drsnuggles8 commented on Jun 26, 2026

    @drsnuggles8
    OwnerAuthor

    Second slice done: the SaveGame SAVE_COMPONENT / TRY_LOAD_COMPONENT enumeration 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:

    • The tool emits SaveGame/Generated/SaveGameComponent{Capture,Restore}.Generated.inl from the same struct *Component scan as the tuple, minus a save-game-specific exclusion set kComponentsNotInSaveGame (components with no SaveGameComponentSerializer::Serialize() overload). That set deliberately differs from kComponentsNotInTuple: save-games keep IDComponent/TagComponent but drop the runtime *StateComponent/UIResolvedRectComponent plus AudioSoundGraphComponent/LocalizedTextComponent (tuple members with no save serializer yet).
    • SaveGameSerializer.cpp #includes the generated lists in place of the two ~97-entry hand lists.
    • SaveGameComponentSerializerCoverageTest now parses the generated .inl files (the documented "touch-point moved into a generated file → repoint the grep" gotcha).
    • Verified the generated lists are byte-for-byte identical in coverage to the prior hand lists (97 components; capture == restore == registered); full build + coverage + save→load round-trip tests pass.

    Net effect: adding a saveable component now needs only the Serialize() overload + REGISTER_SAVE_COMPONENT — the capture/restore lists update automatically on the next GenerateBindings run.

    Still open (later, riskier slices — leaving this issue open):

    • SceneSerializer.cpp per-field SerializeEntity / DeserializeEntities blocks.
    • The per-component SaveGameComponentSerializer::Serialize() overloads (need field-type introspection).
    • Possibly folding RegisterAll() in once the overloads are generated.
  4. added a commit that references this issue on Jun 27, 2026
  5. drsnuggles8 commented on Jun 27, 2026

    @drsnuggles8
    OwnerAuthor

    The second slice documented in the comment above (generated SaveGame SAVE_COMPONENT / TRY_LOAD_COMPONENT lists) landed as #405 (merged 2026-06-27); worktree removed.

    Remaining slices still open: SceneSerializer.cpp per-field SerializeEntity/DeserializeEntities blocks, and the per-component SaveGameComponentSerializer::Serialize() overloads (need field-type introspection). Leaving this umbrella issue open.

  6. added a commit that references this issue on Jun 27, 2026
  7. drsnuggles8 commented on Jun 27, 2026

    @drsnuggles8
    OwnerAuthor

    Third codegen slice landed: #413 (merged 2026-06-27) generates the Scene::OnComponentAdded<T> / OnComponentRemoved<T> no-op specializations from the struct *Component scan (into Scene/Generated/OnComponent{Added,Removed}.Generated.inl). With the earlier slices — the AllComponents tuple (#386) and the SaveGame SAVE_COMPONENT/TRY_LOAD_COMPONENT capture/restore lists (#405) — three of the list-shaped touch-points are now codegen'd.

    Still hand-maintained (issue stays open): SceneSerializer.cpp SerializeEntity/DeserializeEntities field read/writes, the SaveGameComponentSerializer.{h,cpp} decl/def/RegisterAll() registration, and the Lua RegisterAllTypes() usertype (intentionally unguarded). Flagging the advance from the merged worktree cleanup; leaving open for the remaining touch-points.

  8. drsnuggles8 commented on Jun 28, 2026

    @drsnuggles8
    OwnerAuthor

    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.

    • New full data-member scan (CollectComponentFields), distinct from the OLO_PROPERTY scan — the serializer persists every field, not just script-exposed ones.
    • Emits Scene/Generated/Scene{Serialize,Deserialize}Components.Generated.inl, #included by SceneSerializer.cpp; the 26 migrated hand-written blocks removed.
    • Auto-generates the 13 components whose every member is a primitive / 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 spans cpp ∪ 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-only SceneCamera, custom-validation trivial components) remains, plus the SaveGame Serialize()/RegisterAll() body codegen. Tracked in the follow-up issue below.

  9. added a commit that references this issue on Jun 29, 2026
  10. drsnuggles8 commented on Jun 29, 2026

    @drsnuggles8
    OwnerAuthor

    Cross-link: #451's first slice (AssetHandle/UUID scene-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.

  11. added
    toolingTier 1: MCP, dev tooling, codegen, CI, test infra. Freeze-exempt; start-work Tier-1 slot
    on Jul 4, 2026
  12. drsnuggles8 commented on Jul 6, 2026

    @drsnuggles8
    OwnerAuthor

    All four of this issue's original hand-maintained ECS touch-points are now codegen-collapsed: the AllComponents tuple, the SaveGame SAVE_COMPONENT/TRY_LOAD_COMPONENT lists, the OnComponentAdded<T>/OnComponentRemoved<T> no-op specializations, and the SceneSerializer trivial-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.

  13. drsnuggles8 commented on Jul 7, 2026

    @drsnuggles8
    OwnerAuthor

    Closing per the 2026-07-06 status: all 4 of this issue's original hand-maintained ECS touch-points are codegen-collapsed —

    • AllComponents tuple
    • SaveGame SAVE_COMPONENT/TRY_LOAD_COMPONENT lists
    • OnComponentAdded<T>/OnComponentRemoved<T> no-op specializations
    • SceneSerializer trivial-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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew feature or requesttoolingTier 1: MCP, dev tooling, codegen, CI, test infra. Freeze-exempt; start-work Tier-1 slot

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions