Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ When you finish a task that touched code or assets:
3. **Cross-binding check** — the pre-commit hook does **not** catch this. If you added or changed an ECS component, you must update the remaining hand-maintained touch-points or scripting / scene saves / save-games / runtime copies will silently drop it (and the build will fail to link):
- `Scene/Components.h` — declare the component struct itself. The `AllComponents` tuple is **no longer hand-edited**: OloHeaderTool now generates it from every `struct *Component` *definition* under `OloEngine/src` into `Scene/Generated/AllComponents.Generated.inl`, which `Components.h` `#include`s at the bottom — declare the struct and the next build registers it for scene copy / prefab / `HasComponent<T>()` automatically. The exceptions are **runtime-only / hand-copied** components (entity-identity `IDComponent` / `TagComponent`, and per-tick derived `*StateComponent` / `UIResolvedRectComponent`): those must be excluded in **both** the generator's `kComponentsNotInTuple` set (`tools/OloHeaderTool/main.cpp`) **and** `ComponentTupleCoverageTest`'s `kNotInTuple` set — keep the two in sync. Still **guarded** by `OloEngine/tests/ComponentTupleCoverageTest.cpp` (both directions, now reading the generated file): it catches a stale generated tuple (rebuild `GenerateBindings`) and drift between the generator's recursive scan and the test's curated header-root list (a component in a header the test doesn't list surfaces in the inverse test → add the header to `ComponentHeaderRoots()`).
- `Scene/Scene.cpp` — the `Scene::OnComponentAdded<T>` / `OnComponentRemoved<T>` **no-op specializations are no longer hand-edited** (issue #380, third slice): OloHeaderTool generates them from the same `struct *Component` scan into `Scene/Generated/OnComponent{Added,Removed}.Generated.inl`, which `Scene.cpp` `#include`s inside the `OLO_ON_COMPONENT_{ADDED,REMOVED}_NOOP` macros — so a new component that needs **no** init/teardown is registered automatically and you touch nothing here. You only edit this file when the component needs a **real body**: hand-write the `template<>` specialization (alongside the existing custom ones — `CameraComponent`, the physics group, audio/video, etc.) **and** add the component to the matching exclusion set in `tools/OloHeaderTool/main.cpp` (`kComponentsCustomOnAdd` and/or `kComponentsCustomOnRemove` — **two deliberately different sets**: e.g. `CameraComponent`/`CinematicComponent`/`LocalizedTextComponent` do real work on add but no-op on remove, while `Rigidbody2DComponent`/`SpringBoneComponent`/`NoiseAnimationComponent` are the reverse). Forgetting the exclusion-set edit ⇒ the generated no-op collides with your body (**duplicate-definition** compile error); forgetting the body while leaving it in the set ⇒ a **link error** (engine for add via `AddComponent<T>`, OloEditor for remove via the remove-component button). Both are loud, not silent. The non-`struct *Component` `Skeleton` is the one type the scan can't see, so its no-ops stay hand-written. Guarded by `OloEngine/tests/ComponentHandlerCoverageTest.cpp` (verifies each generated list == every component minus its custom set; mirrors both `kComponentsCustomOn{Add,Remove}` sets — keep them in sync, same contract as `ComponentTupleCoverageTest`).
- `Scene/SceneSerializer.cpp` — `SerializeEntity` (writes) and `DeserializeEntityComponents` (reads). Validate every float with `std::isfinite`. **Trivial components are no longer hand-written** (issue #380, fourth slice): OloHeaderTool runs a full data-member scan (`CollectComponentFields` — *not* the `OLO_PROPERTY` scan, since the serializer persists every field, not just script-exposed ones) and, for every `struct *Component` whose **every** data member is a primitive / `glm::vec*` / `std::string`, generates a serialize block + a deserialize block into `Scene/Generated/Scene{Serialize,Deserialize}Components.Generated.inl`, which `SceneSerializer.cpp` `#include`s at the end of `SerializeEntity` / `DeserializeEntityComponents`. So a new **all-trivial** component round-trips through scene YAML with **zero** edits here. You only hand-write a block when the component has a **non-trivial field** (enum, `AssetHandle`, `Ref<T>`, `std::vector`, nested struct, `glm::quat`/`mat`/`ivec`, …) — the parser classifies it non-trivial and skips it automatically — **or** when an all-trivial component needs custom deserialize logic the plain round-trip would drop (range clamps / `Sanitize*` / a runtime-only field the serializer omits / a non-`m_`-stripped key); in that case keep the hand-written block **and** add the component to `kComponentsCustomSerialize` (`tools/OloHeaderTool/main.cpp`) so the generator skips it. Forgetting that exclusion ⇒ the component is emitted **and** hand-written ⇒ a **double-emit** (duplicate YAML key on write, `AddComponent<T>` twice on read) — caught loudly by `ComponentSerializerCoverageTest`'s `NoComponentIsBothHandWrittenAndGenerated` disjointness test, never silent. The same test's existence check now reads `SceneSerializer.cpp` **plus** the two generated `.inl` as one corpus, so a component handled by either path counts as plumbed-through. `kComponentsCustomSerialize` is a **fourth** distinct exclusion set (unrelated to the tuple / save-game / on-add / on-remove sets). The generated deserialize validates floats via `TryReadFiniteF32` (scalars) / the glm `Decode` helpers (vectors); a missing key keeps the constructor default.
- `Scene/SceneSerializer.cpp` — `SerializeEntity` (writes) and `DeserializeEntityComponents` (reads). Validate every float with `std::isfinite`. **Trivial components are no longer hand-written** (issue #380, fourth slice): OloHeaderTool runs a full data-member scan (`CollectComponentFields` — *not* the `OLO_PROPERTY` scan, since the serializer persists every field, not just script-exposed ones) and, for every `struct *Component` whose **every** data member is a primitive / `glm::vec*` / `std::string` / `AssetHandle` / `UUID`, generates a serialize block + a deserialize block into `Scene/Generated/Scene{Serialize,Deserialize}Components.Generated.inl`, which `SceneSerializer.cpp` `#include`s at the end of `SerializeEntity` / `DeserializeEntityComponents`. `AssetHandle` / `UUID` (a `u64` wrapper) joined the trivial set in **issue #451's first slice** — they round-trip as a `u64` (`static_cast<u64>` on write, `.as<u64>` + the implicit `UUID(u64)` ctor on read; matches every existing hand-written handle block). So a new **all-trivial** component round-trips through scene YAML with **zero** edits here. You only hand-write a block when the component has a **still-unhandled non-trivial field** (enum, `Ref<T>`, `std::vector`, map, nested struct, `glm::quat`/`mat`/`ivec`, … — the remaining #451 follow-up slices) — the parser classifies it non-trivial and skips it automatically — **or** when an all-trivial component needs custom deserialize logic the plain round-trip would drop (range clamps / `Sanitize*` / a runtime-only field the serializer omits / a non-`m_`-stripped key); in that case keep the hand-written block **and** add the component to `kComponentsCustomSerialize` (`tools/OloHeaderTool/main.cpp`) so the generator skips it. Forgetting that exclusion ⇒ the component is emitted **and** hand-written ⇒ a **double-emit** (duplicate YAML key on write, `AddComponent<T>` twice on read) — caught loudly by `ComponentSerializerCoverageTest`'s `NoComponentIsBothHandWrittenAndGenerated` disjointness test, never silent. **One blind spot the tests do *not* cover:** a component that is entity-identity or runtime-only and is persisted some *other* way than a sub-map (e.g. `IDComponent` — its `UUID` is the top-level `Entity: <uuid>` line, re-applied via `CreateEntityWithUUID`) is invisible to both the existence check (it's in the test's `kRuntimeOnly` set) and the disjointness check (it has no hand-written `out << YAML::Key` block). If a classifier-widening makes such a component all-trivial it gets auto-generated into a bogus sub-map + a double-`AddComponent` on load with **no** test failure — so it **must** be added to `kComponentsCustomSerialize` by hand (as `IDComponent` now is). When you widen the classifier, rebuild `GenerateBindings` and diff the generated `.inl` to catch every newly-flipped component. The same test's existence check now reads `SceneSerializer.cpp` **plus** the two generated `.inl` as one corpus, so a component handled by either path counts as plumbed-through. `kComponentsCustomSerialize` is a **fourth** distinct exclusion set (unrelated to the tuple / save-game / on-add / on-remove sets). The generated deserialize validates floats via `TryReadFiniteF32` (scalars) / the glm `Decode` helpers (vectors); a missing key keeps the constructor default.
- **SaveGame — two edits in one file:** (a) `SaveGame/SaveGameComponentSerializer.h` — forward decl + `Serialize()` overload declaration; (b) `SaveGame/SaveGameComponentSerializer.cpp` — `Serialize()` overload definition + `RegisterAll()` registration. The `SAVE_COMPONENT(...)` capture list and `TRY_LOAD_COMPONENT(...)` restore list in `SaveGame/SaveGameSerializer.cpp` are **no longer hand-edited** (issue #380, second slice): OloHeaderTool generates them from the same `struct *Component` scan as the `AllComponents` tuple, minus a save-game-specific exclusion set, into `SaveGame/Generated/SaveGameComponent{Capture,Restore}.Generated.inl`, which `SaveGameSerializer.cpp` `#include`s — so giving a component a `Serialize()` overload makes the next build capture/restore it automatically. The exclusion set is `kComponentsNotInSaveGame` (`tools/OloHeaderTool/main.cpp`) and is **deliberately different** from `kComponentsNotInTuple`: save-games *keep* `IDComponent`/`TagComponent` but *drop* every component lacking a `Serialize()` overload (the four per-tick `*StateComponent` / `UIResolvedRectComponent`, **plus** `AudioSoundGraphComponent` / `LocalizedTextComponent`, which are in the scene-copy tuple but have no save serializer yet). A component with a serializer but wrongly listed in that set — or vice versa — is caught by `SaveGameComponentSerializerCoverageTest` (which now parses the generated `.inl` files; rebuild `GenerateBindings` after editing `RegisterAll`). A component that needs saving but lacks a `Serialize()` overload is **still** silently dropped through save-games while round-tripping through scene YAML — so the `Serialize()` overload + `RegisterAll()` registration remain the two edits you must not forget.
- `Scripting/C#/Generated/` is auto-generated from `OLO_PROPERTY` annotations by OloHeaderTool (see below) — add the annotations on the component fields. Don't hand-edit the generated `.inl` / `.cs`.
- `Scripting/Lua/LuaScriptGlue.cpp::RegisterAllTypes()` — Sol2 usertype registration. (`OloEngine-LuaScriptCore/` is the Mono-equivalent project target but the actual bindings live in `Scripting/Lua/`.) **Intentionally NOT completeness-guarded** — many components legitimately aren't Lua-exposed, so only per-component *functional* round-trips exist (`tests/Lua/LuaBindingTest.cpp`); a completeness test here would be noise. This is the one cross-binding touch-point with no completeness guard, by design.
Expand Down Expand Up @@ -144,7 +144,7 @@ Cross-cutting patterns:
- the ECS `AllComponents` type list (from every `struct *Component` *definition*, minus a small runtime-only exclusion set) → `OloEngine/src/OloEngine/Scene/Generated/AllComponents.Generated.inl`, `#include`d by `Scene/Components.h`. This collapses one of the formerly-hand-maintained ECS component touch-points into codegen (issue #380, first slice).
- the SaveGame `SAVE_COMPONENT(...)` / `TRY_LOAD_COMPONENT(...)` enumeration lists (same `struct *Component` scan, minus a save-game-specific exclusion set `kComponentsNotInSaveGame` — components with no save serializer) → `OloEngine/src/OloEngine/SaveGame/Generated/SaveGameComponent{Capture,Restore}.Generated.inl`, `#include`d by `SaveGame/SaveGameSerializer.cpp`. This collapses the two most dangerous unguarded ECS touch-points — a component missing from either list was *silently* dropped from every save-game (issue #380, second slice).
- the Scene `OnComponentAdded<T>` / `OnComponentRemoved<T>` no-op specialization lists (same `struct *Component` scan, minus two *distinct* custom-handler exclusion sets `kComponentsCustomOnAdd` / `kComponentsCustomOnRemove` — the components whose add/remove callback is hand-written because it does real init/teardown) → `OloEngine/src/OloEngine/Scene/Generated/OnComponent{Added,Removed}.Generated.inl` (same `Scene/Generated` dir as the tuple, no extra CLI arg), `#include`d by `Scene/Scene.cpp` inside the `OLO_ON_COMPONENT_{ADDED,REMOVED}_NOOP` macros. This collapses the `OnComponentAdded`/`OnComponentRemoved` touch-point (issue #380, third slice); the declaration-only primary templates mean a mis-excluded handler is a loud build error (duplicate-definition or unresolved-symbol), never silent.
- the Scene serializer per-component serialize/deserialize blocks → `OloEngine/src/OloEngine/Scene/Generated/Scene{Serialize,Deserialize}Components.Generated.inl` (same `Scene/Generated` dir, no extra CLI arg), `#include`d by `Scene/SceneSerializer.cpp`. Driven by a **separate full data-member scan** (`CollectComponentFields`), **not** the `OLO_PROPERTY` scan — the serializer persists *every* field, not just script-exposed ones (e.g. `DirectionalLightComponent` serializes 9 fields but annotates only 3). A component is emitted iff **every** data member is a primitive / `glm::vec*` / `std::string` (`SceneSerType`) **and** it is not in `kComponentsCustomSerialize` (the fourth, distinct exclusion set — trivial components kept hand-written for range-clamp/`Sanitize`/runtime-field/non-`m_` reasons). Anything with a non-trivial field is classified non-trivial and skipped automatically (no exclusion entry needed). This collapses the last big *unguarded* ECS touch-point (issue #380, fourth slice) — a forgotten field was **silent scene-data loss**, which the old name-only coverage test couldn't catch. Guarded by `ComponentSerializerCoverageTest`: existence (every component handled by `SceneSerializer.cpp` ∪ the generated `.inl`) **and** disjointness (`NoComponentIsBothHandWrittenAndGenerated` — no double-emit).
- the Scene serializer per-component serialize/deserialize blocks → `OloEngine/src/OloEngine/Scene/Generated/Scene{Serialize,Deserialize}Components.Generated.inl` (same `Scene/Generated` dir, no extra CLI arg), `#include`d by `Scene/SceneSerializer.cpp`. Driven by a **separate full data-member scan** (`CollectComponentFields`), **not** the `OLO_PROPERTY` scan — the serializer persists *every* field, not just script-exposed ones (e.g. `DirectionalLightComponent` serializes 9 fields but annotates only 3). A component is emitted iff **every** data member is a primitive / `glm::vec*` / `std::string` / `AssetHandle` / `UUID` (`SceneSerType`; the last two `u64`-wrapper types added by issue #451's first slice — `static_cast<u64>` on write, `.as<u64>` + implicit `UUID(u64)` on read) **and** it is not in `kComponentsCustomSerialize` (the fourth, distinct exclusion set — trivial components kept hand-written for range-clamp/`Sanitize`/runtime-field/non-`m_`/entity-identity reasons; e.g. `IDComponent` is excluded because the test suite can't catch its mis-generation — see the cross-binding section). Anything with a still-unhandled non-trivial field (enum, `Ref<T>`, `std::vector`, nested struct, …) is classified non-trivial and skipped automatically (no exclusion entry needed). This collapses the last big *unguarded* ECS touch-point (issue #380, fourth slice) — a forgotten field was **silent scene-data loss**, which the old name-only coverage test couldn't catch. Guarded by `ComponentSerializerCoverageTest`: existence (every component handled by `SceneSerializer.cpp` ∪ the generated `.inl`) **and** disjointness (`NoComponentIsBothHandWrittenAndGenerated` — no double-emit).

Wired as the `GenerateBindings` custom target; `OloEngine` and `OloEngine-ScriptCore` depend on it, so it runs automatically on configure/build. If you change an annotated property, add/rename a component, and the generated `.inl` / `.cs` look stale, build `GenerateBindings` directly (then re-stage the regenerated files — they're tracked, not git-ignored). **Gotcha for future codegen slices:** the coverage tests that guard these touch-points parse generated/source files as *text* (e.g. `ComponentTupleCoverageTest::CollectTupleMembers` reads the `AllComponents = ComponentGroup<…>` marker) — when you move a touch-point from a hand-written location into a generated file, repoint the test's parser at the generated path or it fails on an empty parse.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,10 @@ if (auto node = entity["UIToggleComponent"]; node)
comp.m_KnobColor = node["KnobColor"].as<glm::vec4>(comp.m_KnobColor);
comp.m_Interactable = node["Interactable"].as<bool>(comp.m_Interactable);
}

if (auto node = entity["UIWorldAnchorComponent"]; node)
{
auto& comp = deserializedEntity.AddComponent<UIWorldAnchorComponent>();
comp.m_TargetEntity = node["TargetEntity"].as<u64>(static_cast<u64>(comp.m_TargetEntity));
comp.m_WorldOffset = node["WorldOffset"].as<glm::vec3>(comp.m_WorldOffset);
}
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,13 @@ if (entity.HasComponent<UIToggleComponent>())
out << YAML::Key << "Interactable" << YAML::Value << comp.m_Interactable;
out << YAML::EndMap; // UIToggleComponent
}

if (entity.HasComponent<UIWorldAnchorComponent>())
{
out << YAML::Key << "UIWorldAnchorComponent";
out << YAML::BeginMap; // UIWorldAnchorComponent
auto const& comp = entity.GetComponent<UIWorldAnchorComponent>();
out << YAML::Key << "TargetEntity" << YAML::Value << static_cast<u64>(comp.m_TargetEntity);
out << YAML::Key << "WorldOffset" << YAML::Value << comp.m_WorldOffset;
out << YAML::EndMap; // UIWorldAnchorComponent
}
22 changes: 5 additions & 17 deletions OloEngine/src/OloEngine/Scene/SceneSerializer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2761,12 +2761,9 @@ namespace OloEngine
TrySet(progress.m_FillColor, uiProgressBarComponent["FillColor"]);
}

if (auto uiWorldAnchorComponent = entity["UIWorldAnchorComponent"]; uiWorldAnchorComponent)
{
auto& anchor = deserializedEntity.AddComponent<UIWorldAnchorComponent>();
TrySet(anchor.m_TargetEntity, uiWorldAnchorComponent["TargetEntity"]);
TrySet(anchor.m_WorldOffset, uiWorldAnchorComponent["WorldOffset"]);
}
// UIWorldAnchorComponent — auto-generated (all-trivial: a UUID entity
// reference + a glm::vec3 offset). Its serialize/deserialize blocks live in
// Scene{Serialize,Deserialize}Components.Generated.inl (issue #451).

if (auto uiInputFieldComponent = entity["UIInputFieldComponent"]; uiInputFieldComponent)
{
Expand Down Expand Up @@ -4725,17 +4722,8 @@ namespace OloEngine
out << YAML::EndMap; // UIProgressBarComponent
}

if (entity.HasComponent<UIWorldAnchorComponent>())
{
out << YAML::Key << "UIWorldAnchorComponent";
out << YAML::BeginMap; // UIWorldAnchorComponent

auto const& anchor = entity.GetComponent<UIWorldAnchorComponent>();
out << YAML::Key << "TargetEntity" << YAML::Value << anchor.m_TargetEntity;
out << YAML::Key << "WorldOffset" << YAML::Value << anchor.m_WorldOffset;

out << YAML::EndMap; // UIWorldAnchorComponent
}
// UIWorldAnchorComponent — auto-generated (see the matching note in
// DeserializeEntityComponents); block lives in the generated .inl (issue #451).

if (entity.HasComponent<UIInputFieldComponent>())
{
Expand Down
Loading
Loading