Skip to content

Codegen: SceneSerializer blocks for complex-field components (#380 follow-up) #451

Closed

Description

@drsnuggles8

Follow-up to #380 / #450. The fourth slice (#450) generates SceneSerializer.cpp serialize/deserialize blocks for the all-trivial components (every member a primitive / glm::vec* / std::string). The harder cases were deliberately left hand-written and are tracked here.

Shipped

  • First slice — AssetHandle / UUID fields (feat(codegen): SceneSerializer blocks for AssetHandle/UUID-field components (#451) #467): the codegen now classifies AssetHandle/UUID (a u64 wrapper) as a trivial-ish field type and round-trips it (static_cast<u64> on write, .as<u64> + the implicit UUID(u64) ctor on read — format-identical to the hand-written handle blocks). A component whose only non-primitive fields are asset handles / entity-reference UUIDs is now fully generated; UIWorldAnchorComponent migrated off its hand-written block. Four components that newly classify as trivial are kept hand-written via kComponentsCustomSerialize: DialogueComponent / IKTargetComponent / PerceptionComponent (Sanitize/clamp deserialize the round-trip would drop) and IDComponent (entity identity — serialized as the top-level Entity: line; auto-gen would emit a bogus sub-map + double-AddComponent on load, and neither coverage test catches that, so it's a hard exclusion).
  • Second slice — enum / enum class fields (feat(codegen): SceneSerializer enum-field support (#451 slice) #477): the classifier now recognises an enum field by matching the member type's leaf name against a scan of every enum/enum class definition under OloEngine/src (CollectEnumTypes in main.cpp) and round-trips it as an int (static_cast<int> on write, .as<int> on read, cast back to the member's own type via static_cast<decltype(comp.member)> — so a nested enum like AnimationStateComponent::State needs no qualified spelling). Regenerating flipped several components to all-trivial that then needed kComponentsCustomSerialize exclusions for omitted runtime fields / clamps / non-m_ keys: Rigidbody3DComponent (enum keyed BodyType, runtime m_RuntimeBodyToken + omitted fields), StreamingVolumeComponent (runtime IsLoaded + radius clamps), FogVolumeComponent (Shape/Priority clamps), UIButtonComponent (runtime m_State), UISliderComponent (runtime m_IsDragging).
  • Fourth slice — per-field OLO_SERIALIZE(Skip) (codegen: OLO_SERIALIZE(Skip) field annotation for SceneSerializer (#451) #500): a new marker macro (Scene/ComponentReflection.h, alongside OLO_PROPERTY) that drops a member from the generated (de)serialize and stops it marking the component non-trivial — so an otherwise-all-trivial component whose only obstacle is a runtime-only field is now fully generated (the field reloads at its ctor default) instead of a whole-component kComponentsCustomSerialize exclusion. The field parser peels a leading OLO_SERIALIZE(...) (Skip/Skip = true drops the field; Skip = false or a future Clamp/Min/Max peels the marker and serializes normally); it composes with OLO_PROPERTY on the same field (e.g. a runtime field still script-exposed), and the OLO_PROPERTY scanner skips an OLO_SERIALIZE( line so order doesn't matter. Migrated UIButtonComponent (m_State) and UISliderComponent (m_IsDragging) off kComponentsCustomSerialize with byte-identical on-disk format. This is the "field-level annotation → per-field control" direction below (the Skip half; Clamp/Min/Max remain). Motivation: the vector<struct> / nested-struct classes have no clean beneficiary today — every candidate must stay hand-written for an independent reason (private members ColliderMaterial, deserialize sanitize NavMeshBoundsComponent, runtime-only DialogueStateComponent, flattened format LODGroup) — so widening the classifier there would emit zero live blocks; the annotation direction does have beneficiaries.
  • Third slice — glm-math / small-int / std::vector<primitive> fields (feat(codegen): SceneSerializer glm-math / small-int / std::vector<primitive> support (#451 slice) #482): the classifier now also handles glm::quat/mat3/mat4/ivec2/ivec3/ivec4 (via new Encode/Decode + Emitter<< helpers in Core/YAMLConverters.h; float components finiteness-validated), the small ints u8/u16/i8/i16 (widened to u32/i32 on emit — yaml-cpp's convert<unsigned char>::encode writes a raw char, not a number — and read via .as<decltype(member)>), and std::vector<E> where E is any trivial type (round-tripped as a YAML sequence). The field parser now also tracks access level so a non-public member marks the component non-trivial (fail-safe: stays hand-written, no compile error / silent drop — e.g. TransformComponent's private Rotation/euler-sync now needs no exclusion). Migrated off hand-written blocks: InstancePortalComponent (u8), QuestGiverComponent (vector<string>), RelationshipComponent (UUID + vector<UUID>). Three components newly flipped to all-trivial but kept hand-written via kComponentsCustomSerialize: LightProbeVolumeComponent (runtime m_Dirty/m_ShowDebugProbes + clamps), NavAgentComponent (runtime path/target state), PhysicsJoint3DComponent (runtime m_RuntimeConstraintToken + dozens of clamps).
  • Fifth slice — Ref<T> asset-handle fields (this PR): the classifier now recognises a Ref<T> member as serializer-trivial when T transitively derives from Asset (CollectAssetTypes in main.cpp, walking class X : public Y inheritance chains — one hop of indirection matters, since Material/Model/EnvironmentMap derive from the intermediate RendererResource : public Asset rather than Asset directly). Round-trips as a "<Key>Handle" u64 written only when the Ref is non-null and actually asset-manager-registered (GetHandle() != 0), resolved back via AssetManager::GetAsset<T> on read — format-identical to the hand-written MeshComponent::m_MeshSource -> "MeshSourceHandle" idiom. Regenerating flipped 16 components to all-trivial; every one stayed hand-written via kComponentsCustomSerialize, for one of three reasons (see the exclusion set's comment in main.cpp for the full per-component breakdown): on-disk FORMAT INCOMPATIBILITY (DecalComponent, SpriteRendererComponent, TextComponent, UIImageComponent, UIPanelComponent, UITextComponent, UIInputFieldComponent, UIDropdownComponent persist textures/fonts by file PATH, not handle), DROPPED CUSTOM LOAD LOGIC (ModelComponent Reload(), MeshComponent primitive-range validation + mesh-from-primitive fallback, SubmeshComponent's m_Mesh reconstructed from its parent at runtime, ProceduralSkyComponent/StarNestSkyComponent/ReflectionProbeComponent positivity/range guards), or RUNTIME-ONLY FIELDS auto-gen would newly persist (CinematicComponent's playback state, ReflectionProbeComponent's baked cubemap). One component migrated cleanly: EnvironmentMapComponent (its Ref<EnvironmentMap> cache is always a no-op — never asset-manager-registered — so the round-trip is behaviorally identical to the hand-written block; bonus fix: m_EnvironmentMapAsset, an AssetHandle SaveGame already persists, was being silently dropped from scene YAML and is now round-tripped too).

Out of scope of #450 (still hand-written in SceneSerializer.cpp)

A component stays hand-written today if it has any non-trivial field, or is in kComponentsCustomSerialize. The field types / patterns the generator does not yet handle:

  • AssetHandle / UUID fields — serialized as static_cast<u64>(...), deserialized with re-resolution. Pervasive (materials, meshes, colliders, dialogue, streaming, …). ✅ Done in feat(codegen): SceneSerializer blocks for AssetHandle/UUID-field components (#451) #467 (see Shipped above).
  • Ref<T> runtime handles (textures, meshes, fonts, environment maps, players) — usually paired with an AssetHandle that is the persisted form; the Ref is rebuilt on load. ✅ Done (see Shipped above).
  • enums — currently static_cast<Enum>(node.as<int>(...)); the generator classifies any enum field as non-trivial. (Needs an enum-detection or annotation mechanism.) ✅ Done in feat(codegen): SceneSerializer enum-field support (#451 slice) #477 (see Shipped above).
  • std::vector of a non-trivial element — std::vector<primitive> done in feat(codegen): SceneSerializer glm-math / small-int / std::vector<primitive> support (#451 slice) #482 (scalars / small-ints / glm-math / strings / handles / enums); std::vector<trivial struct> done in the nested-struct slice. What remains is a std::vector of a non-trivial struct.
  • std::unordered_map / std::unordered_set ✅ Done — unordered_map/set slice. A std::unordered_set<E> is trivial when E is a sortable trivial scalar (bool/int/u32/u64/small-int/AssetHandle/std::string/enum — not float or glm types, which have no meaningful operator<); a std::unordered_map<std::string, V> is trivial for any V the vector path accepts. Both round-trip through a sorted temporary, because unordered iteration order is implementation-defined and would otherwise break the serialize→deserialize→serialize identical-output guarantee. Migrated PrefabComponent fully off hand-written code.
  • nested structs ✅ Done — nested-struct slice. A leaf-name→body registry of every struct (CollectStructBodies) feeds a recursive ClassifyStruct/ParseComponentFields pair (with a visited cycle-guard): an all-trivial nested struct emits a sub-map, a std::vector of one emits a sequence of sub-maps, nesting to any depth. (glm::quat/mat/ivec ✅ done in feat(codegen): SceneSerializer glm-math / small-int / std::vector<primitive> support (#451 slice) #482.) The structs originally listed here stay hand-written for other, independent reasons — SceneCamera is setter-only, ColliderMaterial has private members (a non-public member now correctly marks a component non-trivial), LODGroup's hand-written form flattens to top-level keys.

Still genuinely unhandled by the classifier: std::array, a non-std::string-keyed std::unordered_map, a std::unordered_set of a non-sortable element, a std::vector of a non-trivial struct, Ref<T> of a non-Asset-derived type.

  • trivial-but-custom components in kComponentsCustomSerialize — BuoyancyComponent, NoiseAnimationComponent (vec3 range clamps) ✅ Done — vec3-Clamp slice (this PR): OLO_SERIALIZE(Clamp, Min=…, Max=…) now also accepts glm::vec3 fields (clamped per-component via glm::clamp/glm::max/glm::min, mirroring the hand-written SanitizeVec3Clamped idiom), and both components migrated fully off kComponentsCustomSerialize — see the code comment on kComponentsCustomSerialize in tools/OloHeaderTool/main.cpp for detail. Other trivial-but-custom components remain (range clamps / Sanitize* / helper delegation), e.g. SphereAreaLightComponent, VehicleComponent (runtime field omitted), ScriptComponent (external C# field map), TagComponent/PhaseComponent/UIResolvedRectComponent (identity / runtime-only), plus the four added by the feat(codegen): SceneSerializer blocks for AssetHandle/UUID-field components (#451) #467 slice — DialogueComponent/IKTargetComponent/PerceptionComponent (Sanitize/clamp) and IDComponent (entity identity), plus the sixteen added by the Ref<T> slice (see Shipped above — format-incompatible path-based asset resolution, dropped custom load logic, or runtime-only fields). Each would need an opt-in for codegen with custom validation/skip hooks.

Possible directions

  • Field-level annotation (OLO_SERIALIZE(Skip=..., Clamp=..., Min=..., Max=...)) so the generator can emit validation and skip runtime fields — turning kComponentsCustomSerialize from "all-or-nothing per component" into "per-field control". Skip done in codegen: OLO_SERIALIZE(Skip) field annotation for SceneSerializer (#451) #500; scalar Clamp/Min/Max done in Add clamping functionality to component serialization and deserializa… #536; vec3 Clamp ✅ done — this PR (see Shipped above) — that unblocked BuoyancyComponent/NoiseAnimationComponent. Remaining clamp-excluded components need a cross-field invariant beyond a per-field range clamp (LightProbeVolumeComponent, NavMeshBoundsComponent, StreamingVolumeComponent, PhysicsJoint3DComponent). Also a candidate for migrating some of the Ref<T>-slice's runtime-only exclusions (CinematicComponent, ReflectionProbeComponent) off kComponentsCustomSerialize via OLO_SERIALIZE(Skip) on their runtime fields.
  • Enum detection (scan enum class definitions) or an explicit enum-field marker. ✅ Done in feat(codegen): SceneSerializer enum-field support (#451 slice) #477 (CollectEnumTypes leaf-name scan).
  • A registry of Encode/Decode helpers for known nested structs / Ref<T>+handle pairs, keyed by type name. ✅ Ref<T> half done (see Shipped above); nested-struct helpers remain (see "nested structs" above).
  • A follow-up slice could migrate the 8 format-incompatible Ref<T>-slice exclusions (DecalComponent, SpriteRendererComponent, TextComponent, UIImageComponent, UIPanelComponent, UITextComponent, UIInputFieldComponent, UIDropdownComponent) off kComponentsCustomSerialize by first migrating their on-disk format from a "*Path" string key to a handle-based key — a deliberate, separate on-disk-format-change slice, not a codegen-only one.

Also tracked separately

SaveGame SaveGameComponentSerializer::Serialize() / RegisterAll() body codegen (the lists are already generated by #405; the per-component bodies are still hand-written) — noted in #380.


Audit note (2026-07-27). The two bullets above were stale: the
std::unordered_map/std::unordered_set and nested-struct slices had both shipped while this
list still described them as remaining (verified against tools/OloHeaderTool/main.cpp —
CollectStructBodies, ClassifyStruct, and the map/set parse are all present). Corrected above.

This issue is not exhausted. kComponentsCustomSerialize still holds 39 components, and
concrete slices remain — smallest first:

  1. CinematicComponent / ReflectionProbeComponent → migrate off the set with
    OLO_SERIALIZE(Skip) on their runtime-only fields (mechanical).
  2. The 8 format-incompatible Ref<T> exclusions (DecalComponent, SpriteRendererComponent,
    TextComponent, UIImageComponent, UIPanelComponent, UITextComponent,
    UIInputFieldComponent, UIDropdownComponent) → needs their on-disk format migrated from a
    "*Path" string key to a handle key; a deliberate format-change slice, not codegen-only.
  3. LightProbeVolumeComponent / NavMeshBoundsComponent / StreamingVolumeComponent /
    PhysicsJoint3DComponent → need a cross-field invariant (Min≤Max, hysteresis) that no
    per-field annotation can express.

Note SphereAreaLightComponent / ProceduralSkyComponent must stay hand-written regardless:
they reject an out-of-range value and keep the constructor default, which is a different
semantic from Clamp's clamp-to-bound — generating one for the other would silently change what
a corrupted save does to the field.

Score

capability: 1
craft: 1
stability: 2
decay: 1
effort: 3
confidence: 0.5
learning: 3
fun: 1
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 28, 2026

    @coderabbitai
    Contributor
    🔗 Related PRs

    #299 - Feature/reflective skybox [merged]
    #405 - Codegen: generate the SaveGame component capture/restore lists (#380) [merged]
    #467 - feat(codegen): SceneSerializer blocks for AssetHandle/UUID-field components (#451) [merged]
    #477 - feat(codegen): SceneSerializer enum-field support (#451 slice) [merged]
    #482 - feat(codegen): SceneSerializer glm-math / small-int / std::vector support (#451 slice) [merged]
    #511 - feat(codegen): nested-struct + vector SceneSerializer codegen (#451) [merged]
    #569 - feat(codegen): SceneSerializer unordered_set/unordered_map field support (#451 slice) [merged]
    #589 - perf(scene): full-coverage binary sidecar cache for scene loads (#525) [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 29, 2026

    @drsnuggles8
    OwnerAuthor

    First slice shipped in #467: the scene-serializer codegen now recognizes AssetHandle/UUID and round-trips it as a u64. UIWorldAnchorComponent is fully generated off its old hand-written block; DialogueComponent/IKTargetComponent/PerceptionComponent (Sanitize/clamp) and IDComponent (entity identity — the coverage tests can't catch its mis-generation) are kept hand-written via kComponentsCustomSerialize. Remaining field types (enums, vector/map/set, nested structs, Ref<T>, glm::quat/mat/ivec) tracked above as follow-up slices.

  3. added a commit that references this issue on Jun 29, 2026
  4. drsnuggles8 commented on Jun 29, 2026

    @drsnuggles8
    OwnerAuthor

    Advanced (not closed) by #467 (merged 2026-06-29); worktree removed. First slice landed: OloHeaderTool's SceneSerializer codegen now treats AssetHandle/UUID (u64 wrappers) as trivial-ish, so handle/UUID-only components auto-generate their serialize/deserialize blocks (UIWorldAnchorComponent now fully generated; IDComponent + the Sanitize-clamp components kept hand-written via kComponentsCustomSerialize). Remaining epic scope still open: enums, std::vector/map/set, nested structs, Ref, glm::quat/mat/ivec. Leaving open.

  5. drsnuggles8 commented on Jun 30, 2026

    @drsnuggles8
    OwnerAuthor

    Enum-field slice shipped in #477 (follows the AssetHandle/UUID first slice in #467).

    The scene-serializer codegen now recognizes enum / enum class members, so an otherwise-all-trivial component with enum fields round-trips through scene YAML with zero hand-written serializer code.

    How (tools/OloHeaderTool/main.cpp):

    • New CollectEnumTypes() scans every enum/enum class definition under OloEngine/src; ParseComponentFields classifies a member PropType::Enum when SceneSerType returns Unknown and the member type's leaf name is a known enum (handles unqualified BodyType3D and nested Component::State alike).
    • Serialize: static_cast<int>(comp.member). Deserialize: static_cast<decltype(comp.member)>(node["K"].as<int>(static_cast<int>(comp.member))). The decltype cast-back (vs naming the enum) keeps it robust for nested enums not in scope by their unqualified name at the SceneSerializer level; a missing key keeps the ctor default.

    Migrated to generated code (hand-written blocks removed, keys/fields matched): UICanvasComponent, UIProgressBarComponent, UIScrollViewComponent, UIGridLayoutComponent, NetworkIdentityComponent, NetworkLODComponent.

    Flipped but kept hand-written (added to kComponentsCustomSerialize, documented inline) because the plain round-trip would lose/change data:

    • Rigidbody3DComponent — enum keyed "BodyType" not "Type", runtime m_RuntimeBodyToken, several deliberately-omitted fields
    • StreamingVolumeComponent — runtime IsLoaded + radius clamps
    • FogVolumeComponent — Shape/Priority clamps + Sanitize*
    • UIButtonComponent — runtime m_State
    • UISliderComponent — runtime m_IsDragging

    Tests: extended ComponentRoundTrip.UICanvasComponentSurvivesYAMLRoundTrip to cover its two enums; ComponentSerializerCoverageTest (existence + disjointness) validates the rest. Verified 72 + 86 + 483 tests green.

    Leaving open — remaining field-type slices still hand-written: std::vector/map/set, nested structs, Ref<T>, glm::quat/mat/ivec.

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

    @drsnuggles8
    OwnerAuthor

    Enum / enum class SceneSerializer codegen slice delivered by #477 (merged 2026-06-30); worktree removed. Remaining complex-field slices still open: std::vector/map/set, nested structs, Ref<T>, glm::quat/mat/ivec. Keeping this umbrella issue open.

  8. drsnuggles8 commented on Jun 30, 2026

    @drsnuggles8
    OwnerAuthor

    Updated the description: the enum-field slice shipped in #477 (merged 2026-06-30) — moved it from "Out of scope" to a "Shipped" entry (struck the stale bullet + the now-done "Enum detection" direction). Remaining open slices: Ref<T>, std::vector/maps, nested structs (glm::quat/mat/ivec, etc.) — the latter is being picked up next on branch feature/codegen-serializer-glm-fields.

  9. drsnuggles8 commented on Jun 30, 2026

    @drsnuggles8
    OwnerAuthor

    Third slice shipped in #482 — glm-math (quat/mat3/mat4/ivec2-4), small ints (u8/u16/i8/i16), and std::vector<primitive>, plus an access-level fix in the field parser (non-public members now fail safe to non-trivial). Migrated InstancePortalComponent / QuestGiverComponent / RelationshipComponent; kept LightProbeVolumeComponent / NavAgentComponent / PhysicsJoint3DComponent hand-written (runtime fields / clamps).

    Leaving this open for the remaining categories — Ref<T> (rebuild-from-handle), nested structs / std::vector<struct> (recursive serialization), and std::unordered_map/set — which need the Encode/Decode registry under Possible directions.

  10. added a commit that references this issue on Jun 30, 2026
  11. drsnuggles8 commented on Jul 1, 2026

    @drsnuggles8
    OwnerAuthor

    Slice advanced by #482 (merged 2026-06-30); worktree removed. This slice widened the SceneSerializer codegen classifier to cover glm-math (quat/mat3/mat4/ivec2-4), small ints (u8/u16/i8/i16), and std::vector<primitive>, plus a parser access-level fix (private/protected members now correctly mark a component non-trivial). Still open per the PR: Ref<T>, nested structs / std::vector<struct>, and std::unordered_map/set need the recursive/registry design. Leaving this umbrella open.

  12. added a commit that references this issue on Jul 1, 2026
  13. drsnuggles8 commented on Jul 2, 2026

    @drsnuggles8
    OwnerAuthor

    Advanced by #500 (merged 2026-07-01): the OLO_SERIALIZE(Skip) per-field opt-out slice — lets SceneSerializer codegen auto-generate a component whose only obstacle is a runtime-only field (migrated UIButtonComponent/UISliderComponent off kComponentsCustomSerialize). Worktree removed. The remaining hand-written classes — vector<struct>, map/set, nested structs, and the Clamp/Min/Max annotation keys — stay open; leaving this open for them.

  14. drsnuggles8 commented on Jul 2, 2026

    @drsnuggles8
    OwnerAuthor

    Shipped — Nested-struct / std::vector<struct> slice (PR #511)

    The classifier now recursively handles an all-trivial nested struct and a std::vector<struct> of one (tools/OloHeaderTool/main.cpp):

    • Struct registry + recursion. CollectStructBodies builds a leaf-name→body map of every struct under OloEngine/src; ClassifyStruct (mutually recursive with ParseComponentFields, with a visited cycle-guard) classifies a member as PropType::Struct iff its struct's every member is itself serializer-trivial and public. A scalar nested struct emits a nested BeginMap…EndMap sub-map; a std::vector<struct> emits a BeginSeq of sub-maps. Emit is a recursive pair (EmitSerializeFields/EmitDeserializeFields) so nesting composes to any depth (e.g. LODGroup → Levels[] → each LODLevel).
    • Parser-robustness fix (prerequisite). The statement-splitter now terminates a statement at the brace-depth-0 }, not only at ;. An inline method body carries no trailing ;, so without this it merged forward and swallowed a following member and an intervening private: label — which would have mis-seen ColliderMaterial (friction getters then private: floats) as all-public-trivial and emitted private-member-access code that won't compile. With the fix the private members are seen correctly and the struct is rejected, so the six 3D colliders stay hand-written.

    Diff-the-.inl: 5 components flipped, all needed exclusion

    As predicted in the #500 note ("the vector / nested-struct classes have no clean beneficiary — every candidate must stay hand-written for an independent reason"), regenerating flipped 5 components and every one required a kComponentsCustomSerialize exclusion:

    • LODGroupComponent — hand-written flattens the LODGroup sub-struct to top-level Bias/Levels keys and omits runtime m_GeneratedLODHandles; a nested LODGroup: sub-map would change the on-disk format.
    • NavMeshBoundsComponent — deserialize SanitizeVec3s Min/Max, orders Min≤Max, drops non-finite links / clamps Radius.
    • DialogueStateComponent / NoiseAnimationStateComponent / SpringBoneStateComponent — per-tick runtime state that must not be serialized (the IDComponent blind spot: no hand-written block, so only the exclusion stops an unwanted auto-generation).

    Net generated engine code is byte-identical to before (only header comments changed), so the emit refactor is behavior-preserving. Added OloEngine/tests/Serialization/NestedStructSerializerCodegenTest.cpp — round-trips the real LODGroup/OffMeshLink structs through the generator's exact emit shape (the live path isn't compiled into the engine while all beneficiaries are excluded). All 14 serializer/coverage tests + 407 broader tests pass.

    Still remaining (parser skips them automatically)

    • Ref<T> runtime handles (rebuilt from a paired AssetHandle on load).
    • std::unordered_map / std::unordered_set.
    • std::array (e.g. SHCoefficients).
    • std::vector of a non-trivial struct.
    • Clamp/Min/Max per-field annotation (the OLO_SERIALIZE direction begun by codegen: OLO_SERIALIZE(Skip) field annotation for SceneSerializer (#451) #500's Skip) — the highest-leverage next slice, since it would unblock the many clamp-excluded components.

    Leaving this issue open for the follow-up slices.

  15. added a commit that references this issue on Jul 2, 2026
  16. drsnuggles8 commented on Jul 3, 2026

    @drsnuggles8
    OwnerAuthor

    Clamp slice delivered by #536 (merged 2026-07-03, worktree removed): added OLO_SERIALIZE(Clamp, Min=…, Max=…) and migrated SnowDeformerComponent/FogVolumeComponent/NavAgentComponent/SpringBoneComponent off hand-written serialize blocks. Still hand-written per CLAUDE.md: BuoyancyComponent/NoiseAnimationComponent (need vec3 range clamping), LightProbeVolumeComponent/NavMeshBoundsComponent/StreamingVolumeComponent (cross-field invariants), PhysicsJoint3DComponent (large/varied), SphereAreaLightComponent (reject-not-clamp semantics). Leaving open.

  17. drsnuggles8 commented on Jul 4, 2026

    @drsnuggles8
    OwnerAuthor

    Partial progress: #536 (merged 2026-07-03) shipped OLO_SERIALIZE(Clamp, Min=…, Max=…) — the field-level annotation this issue's "Possible directions" section proposed. Migrated FogVolumeComponent, SnowDeformerComponent, SpringBoneComponent, and NavAgentComponent (whose runtime fields also got Skip) off kComponentsCustomSerialize onto full codegen. Remaining scope is unchanged otherwise: std::vector<struct>, std::unordered_map/set, and the trivial-but-custom components still needing per-component opt-in (BuoyancyComponent, SphereAreaLightComponent, VehicleComponent, ScriptComponent, etc.). Leaving open.

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

    @drsnuggles8
    OwnerAuthor

    Sixth slice shipped in #569 — std::unordered_set/std::unordered_map field support.

    The scene-serializer codegen now classifies a std::unordered_set<E> (E a sortable trivial scalar — bool/int/u32/u64/small-int/AssetHandle/std::string/enum, excluding float/glm-vector/matrix/quat/struct since none of those define a meaningful operator<) and a std::string-keyed std::unordered_map<std::string, V> as serializer-trivial. Both round-trip through a sorted temporary copy before emit — unlike std::vector's already-deterministic declaration order, an unordered container's iteration order is implementation-defined and can vary run-to-run, which would otherwise break the "serialize → deserialize → serialize produces identical YAML" round-trip guarantee.

    Migrated: PrefabComponent (three std::unordered_set<std::string> override-tracking fields) fully off its hand-written block — it needed no kComponentsCustomSerialize entry either before or after, since it was hand-written solely because unordered_set wasn't a recognised type yet.

    Left hand-written: MorphTargetComponent::Weights (std::unordered_map<std::string, f32>) is now individually trivial too, but the component stays hand-written for two unrelated reasons: a Ref<MorphTargetSet> field where MorphTargetSet isn't Asset-derived, and a per-value [0, 1] clamp on load the plain round-trip doesn't express.

    No live component exercises an AssetHandle/enum-element set or the unordered_map path yet, so OloEngine/tests/Serialization/ContainerSerializerCodegenTest.cpp mirrors the generator's exact emitted shape for those cases (same pattern as NestedStructSerializerCodegenTest.cpp for the earlier nested-struct slice) to give them compile + runtime coverage ahead of a real beneficiary.

    Remaining open per the "Out of scope" section: std::array, a non-std::string-keyed map, std::vector of a non-trivial struct, and the handful of kComponentsCustomSerialize components needing per-component opt-in. Leaving this umbrella issue open.

  20. added a commit that references this issue on Jul 6, 2026
  21. drsnuggles8 commented on Jul 6, 2026

    @drsnuggles8
    OwnerAuthor

    Serializer-codegen slice completed by #569 (merged 2026-07-06): the field classifier now recognizes std::unordered_set<E> (sortable trivial scalar element) and std::string-keyed std::unordered_map<std::string, V>, both round-tripping through a sorted temporary to preserve YAML determinism. Migrated PrefabComponent's three override-tracking unordered_set<std::string> fields fully onto the generated path. Worktree removed. Leaving this umbrella open — remaining unhandled field shapes: Ref<T> of a non-Asset type, std::array, a non-std::string-keyed map, an unordered_set of a non-scalar element, and a std::vector of a non-trivial struct.

  22. drsnuggles8 commented on Jul 6, 2026

    @drsnuggles8
    OwnerAuthor

    Surveyed the remaining scope (std::array, non-std::string-keyed unordered_map, non-scalar unordered_set elements, Ref<T> of non-Asset types) against every component currently in kComponentsCustomSerialize plus a full std::array< grep across all component headers: no component is blocked by exactly one of these 4 shapes today — every hand-written exclusion is for a clamp/Sanitize/runtime-field-omission/entity-identity reason already handled by other mechanisms (Skip/Clamp/nested-struct/etc.), and std::array has zero real component users. The one non-string-keyed unordered_map in the engine (TerrainComponent::m_VoxelMeshes) is already correctly excluded for unrelated non-Asset Ref<T> reasons, so adding map support wouldn't flip it to auto-trivial anyway. This umbrella's remaining scope is currently hypothetical future-proofing, not blocking any real component — flagging so it doesn't keep floating to the top of the ranked backlog as if it were actionable.

  23. added a commit that references this issue on Jul 7, 2026
  24. drsnuggles8 commented on Jul 7, 2026

    @drsnuggles8
    OwnerAuthor

    Another slice shipped in #580 (merged 2026-07-07): OLO_SERIALIZE(Clamp, ...) now accepts glm::vec3 fields (per-component clamp via glm::clamp/glm::max/glm::min, mirroring the hand-written SanitizeVec3Clamped idiom). Migrated BuoyancyComponent and NoiseAnimationComponent fully off kComponentsCustomSerialize onto the generated path — both were previously flagged in CLAUDE.md as needing this exact follow-up. Worktree removed.

    Leaving this umbrella open — the still-open survey items (std::array, non-std::string-keyed map, non-scalar unordered_set element, Ref<T> of a non-Asset type) remain unblocked-by-any-real-component per the 2026-07-06 survey.

  25. drsnuggles8 commented on Jul 9, 2026

    @drsnuggles8
    OwnerAuthor

    Swept during /start-work triage (2026-07-09), confirming the 2026-07-06 survey comment: the remaining field-shape gaps (std::array, non-string-keyed unordered_map, non-scalar unordered_set element, Ref of a non-Asset type) were surveyed against every component in kComponentsCustomSerialize plus a full std::array<' grep across all component headers — none is currently blocked by exactly one of these 4 shapes; every real exclusion has an independent, already-handled reason (Skip/Clamp/nested-struct/format-incompatibility/entity-identity). This umbrella keeps floating to the top of the score-ranked backlog (score 0.83) as if actionable, but there is no real component to migrate. Recommend closing, or re-scoring effort/confidence down to reflect that remaining scope is speculative future-proofing rather than blocking work — reopen if a new component ever needs one of these shapes.

  26. drsnuggles8 commented on Oct 15, 2026

    @drsnuggles8
    OwnerAuthor

    Closed in favour of #1623, which keeps only the work that is left (three groups of kComponentsCustomSerialize exclusions). Every field-type slice tracked here has shipped.

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 requestrobustnessProduction hardening / shipping robustnesstoolingTier 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