From 615d36b09ca848acb04901a0ca0c996a07aa0687 Mon Sep 17 00:00:00 2001 From: Patedam Date: Tue, 8 Sep 2026 23:24:48 -0400 Subject: [PATCH] Serialization : started to unify how to allocate entity whether its from MakeEntity or during deserialization. --- Game/Data/World.cpp | 2 +- Game/Entity/Entity.cpp | 24 ++-- Game/Entity/Entity.h | 74 ++-------- Game/Entity/EntityManager.cpp | 45 ++++++- Game/Entity/EntityManager.h | 23 ++-- Game/Entity/entity_common.cpp | 21 +++ Game/Entity/entity_common.h | 40 ++++++ Game/Entity/entity_types.cpp | 4 + Game/Entity/entity_types.h | 10 ++ .../01_Serialization_And_Text_Archive.md | 26 ++-- .../02_Entity_Allocation_And_Lifecycle.md | 127 +++++++++++------- Game/game.cpp | 1 + Juliet/include/Engine/Class.h | 50 ++++--- Juliet/src/UnitTest/serialization_test.cpp | 54 ++++---- 14 files changed, 307 insertions(+), 194 deletions(-) create mode 100644 Game/Entity/entity_common.cpp create mode 100644 Game/Entity/entity_common.h create mode 100644 Game/Entity/entity_types.cpp create mode 100644 Game/Entity/entity_types.h diff --git a/Game/Data/World.cpp b/Game/Data/World.cpp index b44ab72..1d19b54 100644 --- a/Game/Data/World.cpp +++ b/Game/Data/World.cpp @@ -93,7 +93,7 @@ void Serialize(Archive& ar, World& /*world*/, String filename) { // Todo : utils // Get base entity from type - Entity* entity = reinterpret_cast(rawElement + (idx * stride))->base; + Entity* entity = reinterpret_cast(rawElement + (idx * stride))->base; serialize(ar, entity); } } diff --git a/Game/Entity/Entity.cpp b/Game/Entity/Entity.cpp index 11e2c11..ab65f7f 100644 --- a/Game/Entity/Entity.cpp +++ b/Game/Entity/Entity.cpp @@ -2,22 +2,6 @@ #include -namespace -{ - void serialize_entity(Archive& ar, uint16 /*version*/, void* payload) - { - Assert(payload); - Entity* entity = static_cast(payload); - - SERIALIZE(ar, id, entity->ID); - SERIALIZE(ar, position, entity->position); - } -} // namespace - -DEFINE_CLASS_VERSIONED(Entity, 1, nullptr, serialize_entity) - -DEFINE_ENTITY_VERSIONED(Inert, 1, nullptr) - void serialize(Archive& ar, NonNullPtr entity) { // Entity fields @@ -29,3 +13,11 @@ void serialize(Archive& ar, NonNullPtr entity) serialize(ar, entity->derived_kind, entity->derived); } } + +DEFINE_CLASS_VERSIONED(Entity, 1, nullptr) + +internal void serialize(Archive& ar, uint16 /*version*/, Entity& entity) +{ + SERIALIZE(ar, id, entity.ID); + SERIALIZE(ar, position, entity.position); +} diff --git a/Game/Entity/Entity.h b/Game/Entity/Entity.h index 1d05a94..fbc81e4 100644 --- a/Game/Entity/Entity.h +++ b/Game/Entity/Entity.h @@ -1,24 +1,10 @@ #pragma once #include -#include #include -#include -#include #include - -#define DECLARE_ENTITY() \ - Entity* base; \ - DECLARE_CLASS() - -#define DEFINE_ENTITY_VERSIONED(entity, version, serialize_fct) \ - constexpr Class entityKind##entity = MakeClass(ConstString(#entity), (uint8)Entity_Type::entity, (version), \ - &classKindEntity, sizeof(entity), alignof(entity), (serialize_fct)); \ - Class* entity::kind = const_cast(&entityKind##entity); - -struct EntityManager; -using DerivedType = void*; -using EntityID = uint64_t; +#include +#include struct Entity final { @@ -30,44 +16,8 @@ struct Entity final Vector4 position = {}; }; -struct Inert -{ - DECLARE_ENTITY() - - index_t MeshInstance = indexMax; -}; - -// clang-format off -#define ENTITY_TYPE_LIST(X) X(Inert), - -#define AS_ENUM(name) name -enum class Entity_Type : uint8 -{ - ENTITY_TYPE_LIST(AS_ENUM) - Count -}; -#undef AS_ENUM - -#define AS_STR(name) #name -inline const char* kEntity_type_names[] = { - ENTITY_TYPE_LIST(AS_STR) - "Count" -}; -#undef AS_STR - -#define AS_CLASS(name) name::kind -inline Class* kEntity_type_class_ptr[] -{ - ENTITY_TYPE_LIST(AS_CLASS) - nullptr -}; -#undef AS_CLASS - -#define ENTITY(kind) ToUnderlying(Entity_Type::kind) -// clang-format on - // Can reinterpret cast to this to have the offset of Base and Kind for any entity -struct entity_template +struct EntityTemplate { DECLARE_ENTITY(); }; @@ -91,15 +41,17 @@ template requires EntityConcept [[nodiscard]] EntityType* MakeEntity(EntityManager& manager, float x, float y, float z) { - EntityType result; - Entity base; - base.position.x = x; - base.position.y = y; - base.position.z = z; - base.position.w = 1.0f; - base.derived_kind = EntityType::kind; + Entity* base_ptr = allocate_entity(manager, EntityType::kind); + Assert(base_ptr); - return (EntityType*)RegisterEntity(manager, &base, &result); + base_ptr->ID = EntityManager::ID++; + + base_ptr->position.x = x; + base_ptr->position.y = y; + base_ptr->position.z = z; + base_ptr->position.w = 1.0f; + + return (EntityType*)base_ptr->derived; } template diff --git a/Game/Entity/EntityManager.cpp b/Game/Entity/EntityManager.cpp index 83326fc..5047427 100644 --- a/Game/Entity/EntityManager.cpp +++ b/Game/Entity/EntityManager.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -43,16 +44,16 @@ EntityManager& GetEntityManager() return *entityManager; } -entity_template* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity) +EntityTemplate* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity) { base->ID = EntityManager::ID++; base->derived = entity; manager.Entities.PushBack(*base); - auto* ptr = (entity_template*)ArenaPushSize(manager.by_type[base->derived_kind->kind].arena, - base->derived_kind->size_of, base->derived_kind->alignment, - false JULIET_DEBUG_PARAM(kEntity_type_names[base->derived_kind->kind])); + auto* ptr = (EntityTemplate*)ArenaPushSize(manager.by_type[base->derived_kind->kind].arena, + base->derived_kind->size_of, base->derived_kind->alignment, + false JULIET_DEBUG_PARAM(kEntity_type_names[base->derived_kind->kind])); MemCopy(ptr, entity, base->derived_kind->size_of); manager.by_type[base->derived_kind->kind].count += 1; @@ -66,6 +67,42 @@ entity_template* RegisterEntity(EntityManager& manager, Entity* base, DerivedTyp return ptr; } +Entity* allocate_entity(EntityManager& manager, NonNullPtr derived_type_class) +{ + Assert(derived_type_class->kind < ENTITY(Count)); + Assert(derived_type_class->size_of >= sizeof(EntityTemplate)); + Assert(derived_type_class->alignment > 0); + Assert(derived_type_class->initialize_fct); + + Entity base_template = {}; + base_template.derived_kind = derived_type_class; + + manager.Entities.PushBack(base_template); + Entity* base_ptr = manager.Entities.Back(); + Assert(base_ptr); + + typed_entity_array& typed_array = manager.by_type[derived_type_class->kind]; + Assert(typed_array.arena); + + void* derived = ArenaPushSize(typed_array.arena, derived_type_class->size_of, derived_type_class->alignment, + false JULIET_DEBUG_PARAM(kEntity_type_names[derived_type_class->kind])); + Assert(derived); + derived_type_class->initialize_fct(derived); + + auto* derived_template = (EntityTemplate*)derived; + + base_ptr->derived = derived; + derived_template->base = base_ptr; + + if (typed_array.array == nullptr) + { + typed_array.array = derived_template; + } + typed_array.count += 1; + + return base_ptr; +} + void UpdateEntityManager(EntityManager& manager) { // Todo : inert by definition dont move, but this is for test diff --git a/Game/Entity/EntityManager.h b/Game/Entity/EntityManager.h index 2097535..e1439a9 100644 --- a/Game/Entity/EntityManager.h +++ b/Game/Entity/EntityManager.h @@ -1,16 +1,18 @@ #pragma once #include -#include +#include -struct Entity; struct World; +struct EntityTemplate; +struct Entity; +struct Class; struct typed_entity_array { - Arena* arena; - entity_template* array; - size_t count; + Arena* arena; + EntityTemplate* array; + size_t count; }; struct EntityManager @@ -23,8 +25,9 @@ struct EntityManager typed_entity_array by_type[ENTITY(Count)]; }; -void InitEntityManager(NonNullPtr world); -void ShutdownEntityManager(); -EntityManager& GetEntityManager(); -entity_template* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity); -void UpdateEntityManager(EntityManager& manager); +void InitEntityManager(NonNullPtr world); +void ShutdownEntityManager(); +EntityManager& GetEntityManager(); +EntityTemplate* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity); +[[nodiscard]] Entity* allocate_entity(EntityManager& manager, NonNullPtr derived_type_class); +void UpdateEntityManager(EntityManager& manager); diff --git a/Game/Entity/entity_common.cpp b/Game/Entity/entity_common.cpp new file mode 100644 index 0000000..55a5c9c --- /dev/null +++ b/Game/Entity/entity_common.cpp @@ -0,0 +1,21 @@ +#include + +#include +#include + +// clang-format off +#define AS_STR(name) #name + const char* kEntity_type_names[] = { + ENTITY_TYPE_LIST(AS_STR) + "Count" +}; +#undef AS_STR + +#define AS_CLASS(name) name::kind +const Class* kEntity_type_class_ptr[] +{ + ENTITY_TYPE_LIST(AS_CLASS) + nullptr +}; +#undef AS_CLASS +// clang-format on diff --git a/Game/Entity/entity_common.h b/Game/Entity/entity_common.h new file mode 100644 index 0000000..276c69a --- /dev/null +++ b/Game/Entity/entity_common.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +struct Class; +using DerivedType = void*; +using EntityID = uint64_t; + +// clang-format off +#define ENTITY_TYPE_LIST(X) X(Inert), + +#define AS_ENUM(name) name +enum class Entity_Type : uint8 +{ + ENTITY_TYPE_LIST(AS_ENUM) + Count +}; +#undef AS_ENUM + +extern const char* kEntity_type_names[]; +extern const Class* kEntity_type_class_ptr[]; + +#define ENTITY(kind) ToUnderlying(Entity_Type::kind) +// clang-format on + +struct Entity; + +#define DECLARE_ENTITY() \ + Entity* base; \ + DECLARE_CLASS() + +#define DEFINE_ENTITY_VERSIONED(entity, version) \ + constexpr Class entityKind##entity = \ + MakeClass(ConstString(#entity), (uint8)Entity_Type::entity, (version), &class_kind_Entity, sizeof(entity), \ + alignof(entity), (&initialize_thunk), (&serialize_thunk)); \ + Class* entity::kind = const_cast(&entityKind##entity); + +// Note: Needed by every classes see define above +extern const Class class_kind_Entity; diff --git a/Game/Entity/entity_types.cpp b/Game/Entity/entity_types.cpp new file mode 100644 index 0000000..fa11c72 --- /dev/null +++ b/Game/Entity/entity_types.cpp @@ -0,0 +1,4 @@ +#include + +DEFINE_ENTITY_VERSIONED(Inert, 1) +internal void serialize(Archive& /*ar*/, uint16 /*version*/, Inert& /*inert*/) {} diff --git a/Game/Entity/entity_types.h b/Game/Entity/entity_types.h new file mode 100644 index 0000000..97bc225 --- /dev/null +++ b/Game/Entity/entity_types.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +struct Inert +{ + DECLARE_ENTITY() + + index_t MeshInstance = indexMax; +}; diff --git a/Game/Plans/01_Serialization_And_Text_Archive.md b/Game/Plans/01_Serialization_And_Text_Archive.md index f611e05..11368aa 100644 --- a/Game/Plans/01_Serialization_And_Text_Archive.md +++ b/Game/Plans/01_Serialization_And_Text_Archive.md @@ -257,17 +257,19 @@ To prevent monolithic engine updates from forcing all gameplay assets to re-vers Every serializable entity or component is described by an immutable `Class` instance: ```cpp -using serialize_fct_type = void (*)(Archive& ar, uint16 version, void* payload); +using serialize_fct_type = void (*)(Archive& ar, uint16 version, void* payload); +using default_init_fct_type = void (*)(void* payload); struct Class { - uint32 CRC; - uint8 kind; - uint16 version; - const Class* base_class; - serialize_fct_type serialize_fct; - size_t size_of; - size_t alignment; + uint32 CRC; + uint8 kind; + uint16 version; + const Class* base_class; + serialize_fct_type serialize_fct; + default_init_fct_type default_init_fct; + size_t size_of; + size_t alignment; #if JULIET_DEBUG String Name; @@ -281,8 +283,10 @@ struct Class static Class* kind; #define DEFINE_CLASS_VERSIONED(cls, version, base_class, serialize_fct) \ + inline void default_init_##cls(void* payload) { *static_cast(payload) = cls{}; } \ constexpr Class classKind##cls = \ - MakeClass(ConstString(#cls), 0, (version), (base_class), sizeof(cls), alignof(cls), (serialize_fct)); \ + MakeClass(ConstString(#cls), 0, (version), (base_class), sizeof(cls), alignof(cls), \ + (serialize_fct), default_init_##cls); \ Class* cls::kind = const_cast(&classKind##cls); ``` @@ -293,8 +297,10 @@ For derived entity types, `DECLARE_ENTITY()` and `DEFINE_ENTITY_VERSIONED` compo DECLARE_CLASS() #define DEFINE_ENTITY_VERSIONED(entity, version, serialize_fct) \ + inline void default_init_##entity(void* payload) { *static_cast(payload) = entity{}; } \ constexpr Class entityKind##entity = MakeClass(ConstString(#entity), (uint8)Entity_Type::entity, (version), \ - &classKindEntity, sizeof(entity), alignof(entity), (serialize_fct)); \ + &classKindEntity, sizeof(entity), alignof(entity), \ + (serialize_fct), default_init_##entity); \ Class* entity::kind = const_cast(&entityKind##entity); ``` diff --git a/Game/Plans/02_Entity_Allocation_And_Lifecycle.md b/Game/Plans/02_Entity_Allocation_And_Lifecycle.md index e1f3b64..d844b3e 100644 --- a/Game/Plans/02_Entity_Allocation_And_Lifecycle.md +++ b/Game/Plans/02_Entity_Allocation_And_Lifecycle.md @@ -192,11 +192,11 @@ The canonical allocation function is defined in `EntityManager.h`: #### Postconditions: - A new `Entity` record is appended to `manager.Entities`. - A new typed block of `classPtr->size_of` bytes is allocated in `manager.by_type[classPtr->kind].arena`. -- The derived memory is zeroed. +- The derived memory is initialized with C++ struct defaults via `classPtr->default_init_fct` (or zeroed via `MemZero` if null). - `base->derived` points to the derived struct. - `derived->base` points to the base `Entity`. - `base->derived_kind` is assigned to `class_ptr`. -- `base->ID` is assigned the next unique `EntityManager::ID`. +- `base->ID` is initialized to `0` (unassigned; populated by `MakeEntity` or deserialization). - `base->is_dirty` is initialized to `true`. - `typed_entity_array::count` is incremented. - `typed_entity_array::array` is initialized if this is the first entity of this type. @@ -212,9 +212,9 @@ The implementation replaces the flawed `RegisterEntity` routine in [`Game/Entity Assert(derivedClassPtr->size_of >= sizeof(entity_template)); Assert(derivedClassPtr->alignment > 0); - // 1. Allocate uninitialized Base Entity in the contiguous VectorArena + // 1. Allocate Base Entity in the contiguous VectorArena (ID is 0 until assigned by MakeEntity or deserialization) Entity baseTemplate{}; - baseTemplate.ID = EntityManager::ID++; + baseTemplate.ID = 0; baseTemplate.derived_kind = derivedClassPtr; baseTemplate.derived = nullptr; baseTemplate.position = {}; @@ -224,7 +224,7 @@ The implementation replaces the flawed `RegisterEntity` routine in [`Game/Entity Entity* basePtr = manager.Entities.Back(); Assert(basePtr != nullptr); - // 2. Allocate zeroed derived component memory in the typed arena + // 2. Allocate derived component memory in the typed arena typed_entity_array& typedArray = manager.by_type[derivedClassPtr->kind]; Assert(typedArray.arena != nullptr); @@ -232,16 +232,26 @@ The implementation replaces the flawed `RegisterEntity` routine in [`Game/Entity typedArray.arena, derivedClassPtr->size_of, derivedClassPtr->alignment, - true JULIET_DEBUG_PARAM(kEntity_type_names[derivedClassPtr->kind])); + false JULIET_DEBUG_PARAM(kEntity_type_names[derivedClassPtr->kind])); Assert(rawMemory != nullptr); + // 3. Initialize derived component defaults via Class reflection stub + if (derivedClassPtr->default_init_fct != nullptr) + { + derivedClassPtr->default_init_fct(rawMemory); + } + else + { + MemZero(rawMemory, derivedClassPtr->size_of); + } + auto* derivedTemplate = reinterpret_cast(rawMemory); - // 3. Establish mutual back-pointers - basePtr->Derived = rawMemory; - derivedTemplate->Base = basePtr; + // 4. Establish mutual back-pointers + basePtr->derived = rawMemory; + derivedTemplate->base = basePtr; - // 4. Update typed array tracking + // 5. Update typed array tracking if (typedArray.array == nullptr) { typedArray.array = derivedTemplate; @@ -252,33 +262,49 @@ The implementation replaces the flawed `RegisterEntity` routine in [`Game/Entity } ``` -### 3.3 Pure C-Style Zeroing & Initialization -`ArenaPushSize` is called with `shouldBeZeroed = true` (or followed by `MemZero`). This guarantees that: -- All bytes are zeroed (`0x00`). -- Pointers inside derived structs default to `nullptr`. -- Numerical fields default to `0`. +### 3.3 Default Struct Initialization via `Class::default_init_fct` -For structs that require specific non-zero sentinel values (such as `Inert::MeshInstance = indexMax`), Juliet follows a pure C approach without C++ constructor or placement-new machinery: -1. **Direct assignment or C-style init function**: +#### The Problem with Zero-Only Initialization +If allocation only zeroes memory (`MemZero` / `0x00`), any C++ member variables with non-zero defaults (such as `index_t MeshInstance = indexMax;` or `float Density = 1.0f;`) are populated with `0`. During deserialization, if a `.jasset` file lacks that property (e.g. an older file version or an optional field), `SERIALIZE` leaves the field untouched, meaning it incorrectly remains `0` rather than its intended default sentinel value! + +#### The Solution: Compile-Time Default Stub in `DEFINE_ENTITY_VERSIONED` +Every entity descriptor `Class` includes a function pointer: ```cpp -inline void InitInert(Inert* inert) +using default_init_fct_type = void (*)(void* payload); + +struct Class { - Assert(inert != nullptr); - inert->MeshInstance = indexMax; -} + ... + default_init_fct_type default_init_fct = nullptr; +}; ``` -2. **Or simple struct literal assignment**: + +When registering an entity type with `DEFINE_ENTITY_VERSIONED`, the macro automatically defines a tiny type-safe stub that aggregate value-initializes the struct: ```cpp -*inert = Inert{ .MeshInstance = indexMax }; +#define DEFINE_ENTITY_VERSIONED(entity, version, serialize_fct) \ + inline void default_init_##entity(void* payload) \ + { \ + *static_cast(payload) = entity{}; \ + } \ + constexpr Class entityKind##entity = MakeClass(ConstString(#entity), (uint8)Entity_Type::entity, (version), \ + &classKindEntity, sizeof(entity), alignof(entity), \ + (serialize_fct), default_init_##entity); \ + Class* entity::kind = const_cast(&entityKind##entity); ``` -No C++ placement `new`, `` headers, or hidden constructor/destructor calls are ever used. Data structures remain pure C-style aggregates. + +#### Architectural Advantages: +1. **Zero Boilerplate**: Developers write member initializers once in the struct definition (`index_t MeshInstance = indexMax;`). +2. **Type-Agnostic Core**: `AllocateEntity` does not need to know any C++ struct types; it unconditionally calls `derivedClassPtr->default_init_fct(rawMemory)`. +3. **Robust Deserialization**: In `deserialize_entity_in_place`, newly allocated entities already hold their canonical C++ defaults. Any properties absent in the `.jasset` file naturally retain their correct initial values. +4. **No Dynamic Heap / Placement-New**: `*static_cast(payload) = entity{}` is pure aggregate value-assignment without `` headers or exceptions. ### 3.4 Bidirectional Pointer Wiring Notice the sequence: 1. `manager.Entities.PushBack(baseTemplate)` places the struct at its final, fixed arena address. -2. `basePtr = manager.Entities.Back()` retrieves the persistent memory pointer. -3. `derivedTemplate->Base = basePtr` wires the derived back-pointer directly to this permanent location. -4. `basePtr->Derived = rawMemory` wires the base forward-pointer to the arena-allocated derived struct. +2. `base_ptr = manager.Entities.Back()` retrieves the persistent memory pointer. +3. `derived_class_ptr->default_init_fct(raw_memory)` initializes canonical struct defaults. +4. `derived_template->base = base_ptr` wires the derived back-pointer directly to this permanent location. +5. `base_ptr->derived = raw_memory` wires the base forward-pointer to the arena-allocated derived struct. No stack copying occurs. Neither pointer is ever left dangling. @@ -287,30 +313,26 @@ No stack copying occurs. Neither pointer is ever left dangling. - `typedArray.count`: The exact count of active entities of this type. - `typedArray.array`: Pointer to the first element in the arena. -When the first entity of a given type is allocated, `typedArray.array` is set to `derivedTemplate`. Because the arena allocates sequentially, `typedArray.array[i]` can be indexed directly with stride `classPtr->size_of` as long as memory remains contiguous. +When the first entity of a given type is allocated, `typedArray.array` is set to `derived_template`. Because the arena allocates sequentially, `typedArray.array[i]` can be indexed directly with stride `class_ptr->size_of` as long as memory remains contiguous. ### 3.6 Refactoring `MakeEntity` Template -With `AllocateEntity` operational, `MakeEntity` in [`Game/Entity/Entity.h`](file:///w:/Classified/Juliet/Game/Entity/Entity.h#L95-L107) becomes clean, safe, and stack-free: +With `AllocateEntity` handling memory allocation, default member initialization, and mutual pointer wiring, `MakeEntity` in [`Game/Entity/Entity.h`](file:///w:/Classified/Juliet/Game/Entity/Entity.h) becomes clean, safe, and stack-free: ```cpp template requires EntityConcept [[nodiscard]] EntityType* MakeEntity(EntityManager& manager, float x, float y, float z) { - Entity* basePtr = AllocateEntity(manager, EntityType::Kind); - Assert(basePtr != nullptr); + Entity* base_ptr = AllocateEntity(manager, EntityType::kind); + Assert(base_ptr != nullptr); - basePtr->X = x; - basePtr->Y = y; - basePtr->Z = z; + base_ptr->ID = EntityManager::ID++; + base_ptr->position.x = x; + base_ptr->position.y = y; + base_ptr->position.z = z; + base_ptr->position.w = 1.0f; - auto* derivedPtr = static_cast(basePtr->Derived); - ConstructDerivedDefaults(derivedPtr); - - // Re-establish Base pointer after placement-new - derivedPtr->Base = basePtr; - - return derivedPtr; + return static_cast(base_ptr->derived); } ``` @@ -429,9 +451,15 @@ To ensure fast and safe type lookup during file deserialization: Entity* base_ptr = AllocateEntity(manager, class_ptr); Assert(base_ptr != nullptr); - // 3. Serialize Base Entity and Derived in-place + // 3. Serialize Base Entity and Derived in-place (loads ID and properties from disk) serialize(ar, NonNullPtr(base_ptr)); + // 4. Advance generator counter to avoid collisions with loaded IDs + if (base_ptr->ID >= EntityManager::ID) + { + EntityManager::ID = base_ptr->ID + 1; + } + // Freshly loaded entity matches disk state exactly base_ptr->is_dirty = false; @@ -708,9 +736,11 @@ if (ImGui::DragFloat3("Position", pos, 0.1f)) ## 7. Step-by-Step Implementation Roadmap ### Phase 1: Data Structures & Header Definitions -1. **Update `Entity.h`:** +1. **Update `Class.h` & `Entity.h`:** + - Add `using default_init_fct_type = void (*)(void* payload);` and `default_init_fct` to `struct Class` and `MakeClass`. + - Update `DEFINE_ENTITY_VERSIONED` and `DEFINE_CLASS_VERSIONED` to define `default_init_##entity` and pass it to `MakeClass`. - Add `bool is_dirty = false;` to `struct Entity`. - - Update `MakeEntity` to delegate to `AllocateEntity`. + - Update `MakeEntity` to assign `base_ptr->ID = EntityManager::ID++;` and delegate allocation and defaults cleanly to `AllocateEntity`. 2. **Update `EntityManager.h`:** - Declare `[[nodiscard]] Entity* AllocateEntity(EntityManager& manager, Class* class_ptr);`. - Declare `void DestroyEntity(EntityManager& manager, EntityID id);`. @@ -722,8 +752,9 @@ if (ImGui::DragFloat3("Position", pos, 0.1f)) ### Phase 2: Core Memory Allocation & Wiring in `EntityManager.cpp` 1. Implement `AllocateEntity`: - Enforce parameter assertions. - - Push to `manager.Entities`. - - Allocate zeroed block in `manager.by_type[kind].arena`. + - Push to `manager.Entities` with `baseTemplate.ID = 0` (unassigned). + - Allocate block in `manager.by_type[kind].arena`. + - Call `derivedClassPtr->default_init_fct(rawMemory)` (or `MemZero` if null) to initialize struct defaults. - Wire mutual pointers (`base->derived` and `derived->base`). - Increment `typedArray.count` and initialize `typedArray.array`. 2. Implement `DestroyEntity` & `RemoveDerivedComponent`: @@ -736,6 +767,7 @@ if (ImGui::DragFloat3("Position", pos, 0.1f)) 2. In `World.cpp`: - Implement `serialize_entity_asset(Archive& ar, NonNullPtr entity, String filepath)`. - Implement `deserialize_entity_asset(EntityManager& manager, Archive& ar, String filepath)`. + - In `deserialize_entity_in_place`, advance `EntityManager::ID` past `base_ptr->ID` to prevent ID collisions. ### Phase 4: World Save/Load Pipeline & Disk Deletion 1. In `World.cpp`: @@ -803,6 +835,8 @@ namespace UnitTest // 1. Allocate Inert Entity via AllocateEntity Entity* base_entity = AllocateEntity(manager, Inert::kind); Assert(base_entity != nullptr); + Assert(base_entity->ID == 0); // Pure memory allocator leaves ID unassigned (0) until MakeEntity or deserialization + base_entity->ID = EntityManager::ID++; Assert(base_entity->ID > 0); Assert(base_entity->derived_kind == Inert::kind); Assert(base_entity->derived != nullptr); @@ -865,6 +899,7 @@ namespace UnitTest Assert(loaded_base != nullptr); Assert(loaded_base->ID == original_id); + Assert(EntityManager::ID > original_id); // Counter was advanced past loaded ID to prevent collisions Assert(loaded_base->position.x == 12.5f); Assert(loaded_base->position.y == -44.0f); Assert(loaded_base->position.z == 108.2f); diff --git a/Game/game.cpp b/Game/game.cpp index a982855..31fb875 100644 --- a/Game/game.cpp +++ b/Game/game.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include diff --git a/Juliet/include/Engine/Class.h b/Juliet/include/Engine/Class.h index 22f9da7..36deb45 100644 --- a/Juliet/include/Engine/Class.h +++ b/Juliet/include/Engine/Class.h @@ -7,7 +7,25 @@ struct Archive; -using serialize_fct_type = void (*)(Archive&, uint16 version, void* payload); +using serialize_fct_type = void(Archive&, uint16 version, void* payload); +using serialize_fct_ptr = serialize_fct_type*; + +using initialize_fct_type = void(void* payload); +using initialize_fct_ptr = initialize_fct_type*; + +template +void serialize_thunk(Archive& ar, uint16 version, void* payload) +{ + Assert(payload); + serialize(ar, version, *((Type*)payload)); +} + +template +void initialize_thunk(void* payload) +{ + auto& typed_val = *(Type*)payload; + typed_val = {}; +} struct Class { @@ -15,7 +33,8 @@ struct Class uint8 kind; uint16 version; const Class* base_class; - serialize_fct_type serialize_fct; + initialize_fct_ptr initialize_fct; + serialize_fct_ptr serialize_fct; size_t size_of; size_t alignment; @@ -26,22 +45,23 @@ struct Class #define DECLARE_CLASS() static Class* kind; -#define DEFINE_CLASS_VERSIONED(cls, version, base_class, serialize_fct) \ - constexpr Class classKind##cls = \ - MakeClass(ConstString(#cls), 0, (version), (base_class), sizeof(cls), alignof(cls), (serialize_fct)); \ - Class* cls::kind = const_cast(&classKind##cls); +#define DEFINE_CLASS_VERSIONED(cls, version, base_class) \ + constexpr Class class_kind_##cls = MakeClass(ConstString(#cls), 0, (version), (base_class), sizeof(cls), \ + alignof(cls), (&initialize_thunk), (&serialize_thunk)); \ + Class* cls::kind = const_cast(&class_kind_##cls); consteval Class MakeClass(String name, uint8 kind, uint16 version, const Class* base_class, size_t size, size_t align, - serialize_fct_type fct) + initialize_fct_ptr init_fct, serialize_fct_ptr serde_fct) { - Class cls = {}; - cls.CRC = crc32(name.Str, name.Size); - cls.kind = kind; - cls.version = version; - cls.base_class = base_class; - cls.size_of = size; - cls.alignment = align; - cls.serialize_fct = fct; + Class cls = {}; + cls.CRC = crc32(name.Str, name.Size); + cls.kind = kind; + cls.version = version; + cls.base_class = base_class; + cls.size_of = size; + cls.alignment = align; + cls.initialize_fct = init_fct; + cls.serialize_fct = serde_fct; #if JULIET_DEBUG cls.Name = name; diff --git a/Juliet/src/UnitTest/serialization_test.cpp b/Juliet/src/UnitTest/serialization_test.cpp index c2e6688..9037d7e 100644 --- a/Juliet/src/UnitTest/serialization_test.cpp +++ b/Juliet/src/UnitTest/serialization_test.cpp @@ -63,20 +63,17 @@ namespace UnitTest float pos_x = 10.0f; }; - internal void serialize_dummy_vehicle(Archive& ar, uint16 /*version*/, void* payload) + DEFINE_CLASS_VERSIONED(DummyVehicle, 1, nullptr) + + internal void serialize(Archive& ar, uint16 /*version*/, DummyVehicle& vehicle) { - Assert(payload != nullptr); - auto* vehicle = static_cast(payload); - - SERIALIZE(ar, max_speed, vehicle->max_speed); - SERIALIZE(ar, gear_count, vehicle->gear_count); - SERIALIZE(ar, chassis_uuid, vehicle->chassis_uuid); - SERIALIZE(ar, turbo, vehicle->turbo); - SERIALIZE(ar, pos_x, vehicle->pos_x); + SERIALIZE(ar, max_speed, vehicle.max_speed); + SERIALIZE(ar, gear_count, vehicle.gear_count); + SERIALIZE(ar, chassis_uuid, vehicle.chassis_uuid); + SERIALIZE(ar, turbo, vehicle.turbo); + SERIALIZE(ar, pos_x, vehicle.pos_x); } - DEFINE_CLASS_VERSIONED(DummyVehicle, 1, nullptr, serialize_dummy_vehicle) - internal void test_default_value_retention() { TempArena temp = scratch_begin(nullptr, 0); @@ -116,14 +113,12 @@ namespace UnitTest float base_damage = 25.0f; }; - internal void serialize_dummy_weapon_base(Archive& ar, uint16 /*version*/, void* payload) - { - Assert(payload != nullptr); - auto* base_weapon = static_cast(payload); - SERIALIZE(ar, base_damage, base_weapon->base_damage); - } + DEFINE_CLASS_VERSIONED(DummyWeaponBase, 1, nullptr) - DEFINE_CLASS_VERSIONED(DummyWeaponBase, 1, nullptr, serialize_dummy_weapon_base) + internal void serialize(Archive& ar, uint16 /*version*/, DummyWeaponBase& base_weapon) + { + SERIALIZE(ar, base_damage, base_weapon.base_damage); + } // Derived test class for testing version migration (v1 -> v2) struct LegacyWeapon @@ -134,11 +129,11 @@ namespace UnitTest float velocity_y = 0.0f; }; - internal void serialize_legacy_weapon(Archive& ar, uint16 version, void* payload) - { - Assert(payload != nullptr); - auto* weapon = static_cast(payload); + // Inherits from DummyWeaponBase (base_class != nullptr) to verify derived class_version handling + DEFINE_CLASS_VERSIONED(LegacyWeapon, 2, &class_kind_DummyWeaponBase) + internal void serialize(Archive& ar, uint16 version, LegacyWeapon& weapon) + { if (ar.loading) { if (version < 2) @@ -147,26 +142,23 @@ namespace UnitTest float deprecated_speed = 0.0f; if (SERIALIZE(ar, speed, deprecated_speed)) { - weapon->velocity_x = deprecated_speed; - weapon->velocity_y = 0.0f; + weapon.velocity_x = deprecated_speed; + weapon.velocity_y = 0.0f; } } else { - SERIALIZE(ar, velocity_x, weapon->velocity_x); - SERIALIZE(ar, velocity_y, weapon->velocity_y); + SERIALIZE(ar, velocity_x, weapon.velocity_x); + SERIALIZE(ar, velocity_y, weapon.velocity_y); } } else { - SERIALIZE(ar, velocity_x, weapon->velocity_x); - SERIALIZE(ar, velocity_y, weapon->velocity_y); + SERIALIZE(ar, velocity_x, weapon.velocity_x); + SERIALIZE(ar, velocity_y, weapon.velocity_y); } } - // Inherits from DummyWeaponBase (base_class != nullptr) to verify derived class_version handling - DEFINE_CLASS_VERSIONED(LegacyWeapon, 2, &classKindDummyWeaponBase, serialize_legacy_weapon) - // Test 3: Deprecation migration from v1 to v2 via stack variable internal void test_deprecation_migration() {