Serialization : started to unify how to allocate entity whether its from MakeEntity or during deserialization.

This commit is contained in:
2026-09-08 23:24:48 -04:00
parent 3123d2f2e6
commit 615d36b09c
14 changed files with 307 additions and 194 deletions
+1 -1
View File
@@ -93,7 +93,7 @@ void Serialize(Archive& ar, World& /*world*/, String filename)
{
// Todo : utils
// Get base entity from type
Entity* entity = reinterpret_cast<entity_template*>(rawElement + (idx * stride))->base;
Entity* entity = reinterpret_cast<EntityTemplate*>(rawElement + (idx * stride))->base;
serialize(ar, entity);
}
}
+8 -16
View File
@@ -2,22 +2,6 @@
#include <Core/Common/serialization.h>
namespace
{
void serialize_entity(Archive& ar, uint16 /*version*/, void* payload)
{
Assert(payload);
Entity* entity = static_cast<Entity*>(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)
{
// Entity fields
@@ -29,3 +13,11 @@ void serialize(Archive& ar, NonNullPtr<Entity> 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);
}
+13 -61
View File
@@ -1,24 +1,10 @@
#pragma once
#include <Core/Common/CoreUtils.h>
#include <Core/Common/EnumUtils.h>
#include <Core/Math/Vector.h>
#include <Core/Memory/Allocator.h>
#include <Core/Memory/MemoryArena.h>
#include <Engine/Class.h>
#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<Class*>(&entityKind##entity);
struct EntityManager;
using DerivedType = void*;
using EntityID = uint64_t;
#include <Entity/entity_common.h>
#include <Entity/EntityManager.h>
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 <typename EntityType>
requires EntityConcept<EntityType>
[[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 <typename EntityType>
+41 -4
View File
@@ -3,6 +3,7 @@
#include <Core/Common/EnumUtils.h>
#include <Data/World.h>
#include <Entity/Entity.h>
#include <Entity/entity_types.h>
#include <game.h>
#include <Graphics/MeshRenderer.h>
@@ -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<Class> 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
+13 -10
View File
@@ -1,16 +1,18 @@
#pragma once
#include <Core/Container/Vector.h>
#include <Entity/Entity.h>
#include <Entity/entity_common.h>
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> world);
void ShutdownEntityManager();
EntityManager& GetEntityManager();
entity_template* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity);
void UpdateEntityManager(EntityManager& manager);
void InitEntityManager(NonNullPtr<World> world);
void ShutdownEntityManager();
EntityManager& GetEntityManager();
EntityTemplate* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity);
[[nodiscard]] Entity* allocate_entity(EntityManager& manager, NonNullPtr<Class> derived_type_class);
void UpdateEntityManager(EntityManager& manager);
+21
View File
@@ -0,0 +1,21 @@
#include <Entity/entity_common.h>
#include <Entity/Entity.h>
#include <Entity/entity_types.h>
// 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
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include <Core/Common/EnumUtils.h>
#include <Engine/Class.h>
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<entity>), (&serialize_thunk<entity>)); \
Class* entity::kind = const_cast<Class*>(&entityKind##entity);
// Note: Needed by every classes see define above
extern const Class class_kind_Entity;
+4
View File
@@ -0,0 +1,4 @@
#include <Entity/entity_types.h>
DEFINE_ENTITY_VERSIONED(Inert, 1)
internal void serialize(Archive& /*ar*/, uint16 /*version*/, Inert& /*inert*/) {}
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <Entity/entity_common.h>
struct Inert
{
DECLARE_ENTITY()
index_t MeshInstance = indexMax;
};
+16 -10
View File
@@ -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<cls*>(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<Class*>(&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<entity*>(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<Class*>(&entityKind##entity);
```
@@ -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<entity_template*>(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<entity*>(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<Class*>(&entityKind##entity);
```
No C++ placement `new`, `<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<entity*>(payload) = entity{}` is pure aggregate value-assignment without `<new>` 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<EntityType>` 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 <typename EntityType>
requires EntityConcept<EntityType>
[[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<EntityType*>(basePtr->Derived);
ConstructDerivedDefaults<EntityType>(derivedPtr);
// Re-establish Base pointer after placement-new
derivedPtr->Base = basePtr;
return derivedPtr;
return static_cast<EntityType*>(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<Entity>(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<EntityType>` to delegate to `AllocateEntity`.
- Update `MakeEntity<EntityType>` 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> 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);
+1
View File
@@ -9,6 +9,7 @@
#include <Core/Memory/MemoryArena.h>
#include <Data/World.h>
#include <Entity/Entity.h>
#include <Entity/entity_types.h>
#include <Entity/EntityManager.h>
#include <Graphics/Camera.h>
#include <Graphics/MeshRenderer.h>