updating serialization tdd with latest changes
This commit is contained in:
@@ -82,10 +82,10 @@ This is the classic **chicken-and-egg memory problem**:
|
||||
### 1.4 Architectural Objectives
|
||||
This specification establishes a robust in-place lifecycle pipeline that completely eliminates stack temporaries and dynamic heap allocations:
|
||||
1. **Direct In-Place Allocation:** Introduce `AllocateEntity(EntityManager& manager, Class* classPtr)` which allocates both the base `Entity` and the derived struct directly within their respective engine memory arenas.
|
||||
2. **Bidirectional Pointer Integrity:** Wire mutual pointers (`base->Derived` and `derived->Base`) at allocation time before any field deserialization begins.
|
||||
2. **Bidirectional Pointer Integrity:** Wire mutual pointers (`base->derived` and `derived->base`) at allocation time before any field deserialization begins.
|
||||
3. **In-Place Stream Deserialization:** Read class reflection metadata first, invoke `AllocateEntity`, and stream base and derived properties directly into arena-resident memory.
|
||||
4. **Isolated Entity Assets (`.jasset`):** Transition from a monolithic `world.bin` to a modular one-file-per-entity architecture (`Assets/Entities/{ID}.jasset`).
|
||||
5. **Dirty Tracking & Optimal Saves:** Introduce an `IsDirty` flag on `Entity` to avoid rewriting unchanged entity files, minimizing disk I/O and eliminating spurious Git repository modifications.
|
||||
5. **Dirty Tracking & Optimal Saves:** Introduce an `is_dirty` flag on `Entity` to avoid rewriting unchanged entity files, minimizing disk I/O and eliminating spurious Git repository modifications.
|
||||
6. **Robust Deletion Lifecycle:** Decouple in-memory removal (`RemoveAtFast` with pointer fixup) from disk synchronization using `World::PendingDeletions`.
|
||||
|
||||
---
|
||||
@@ -129,8 +129,8 @@ VectorArena<Entity, 100'000> Entities;
|
||||
```
|
||||
- **Capacity:** Fixed reserve of 100,000 entities allocated from the `WorldArena`.
|
||||
- **Memory Footprint:**
|
||||
$$\text{sizeof(Entity)} = 8\text{ (ID)} + 8\text{ (Kind)} + 8\text{ (Derived)} + 12\text{ (X, Y, Z)} + 1\text{ (IsDirty)} + 3\text{ (Padding)} = 40\text{ bytes}$$
|
||||
Total reserved space: $100{,}000 \times 40\text{ bytes} \approx 4.0\text{ MB}$.
|
||||
$$\text{sizeof(Entity)} = 8\text{ (ID)} + 8\text{ (derived\_kind)} + 8\text{ (derived)} + 16\text{ (position)} + 1\text{ (is\_dirty)} + 7\text{ (Padding)} = 48\text{ bytes}$$
|
||||
Total reserved space: $100{,}000 \times 48\text{ bytes} \approx 4.8\text{ MB}$.
|
||||
- **Access Speed:** O(1) random access by index; sequential streaming utilizes L1/L2 hardware prefetchers with zero cache line waste.
|
||||
|
||||
### 2.3 `manager.by_type[kind].arena`: Typed Component Arenas
|
||||
@@ -193,11 +193,11 @@ The canonical allocation function is defined in `EntityManager.h`:
|
||||
- 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.
|
||||
- `base->Derived` points to the derived struct.
|
||||
- `derived->Base` points to the base `Entity`.
|
||||
- `base->Kind` is assigned to `classPtr`.
|
||||
- `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->IsDirty` is initialized to `true`.
|
||||
- `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.
|
||||
|
||||
@@ -214,13 +214,11 @@ The implementation replaces the flawed `RegisterEntity` routine in [`Game/Entity
|
||||
|
||||
// 1. Allocate uninitialized Base Entity in the contiguous VectorArena
|
||||
Entity baseTemplate{};
|
||||
baseTemplate.ID = EntityManager::ID++;
|
||||
baseTemplate.DerivedKind = derivedClassPtr;
|
||||
baseTemplate.Derived = nullptr;
|
||||
baseTemplate.X = 0.0f;
|
||||
baseTemplate.Y = 0.0f;
|
||||
baseTemplate.Z = 0.0f;
|
||||
baseTemplate.IsDirty = true;
|
||||
baseTemplate.ID = EntityManager::ID++;
|
||||
baseTemplate.derived_kind = derivedClassPtr;
|
||||
baseTemplate.derived = nullptr;
|
||||
baseTemplate.position = {};
|
||||
baseTemplate.is_dirty = true;
|
||||
|
||||
manager.Entities.PushBack(baseTemplate);
|
||||
Entity* basePtr = manager.Entities.Back();
|
||||
@@ -343,16 +341,15 @@ Inert
|
||||
#### Important: No Header Structs for Derived Types
|
||||
- **Derived types NEVER require their own file header**: You do **not** write an `InertHeader`, `DoorHeader`, or `PlayerHeader`. Derived types only serialize their own member variables.
|
||||
- **Universal `; version` + optional `; class_version`**: Every `.jasset` file has a universal `; version` tag. For entity assets, `; version` governs base entity properties (`kEntityBaseVersion`), while an optional `; class_version` governs derived class properties (`Class::Version`). Non-entity assets like `WorldSettings.jasset` only have `; version`.
|
||||
- **No binary `EntityFileHeader` struct is needed**: Under the `; variable_name\nvalues` text format, there is no packed binary C-struct header at all. The common properties (`; id`, `; class`, `; version`, `; class_version`, `; position`) are standard text Key-Value nodes read by the exact same `archive` parser.
|
||||
- **No binary `EntityFileHeader` struct is needed**: Under the `; variable_name\nvalues` text format, there is no packed binary C-struct header at all. The common properties (`; id`, `; class`, `; version`, `; class_version`, `; position`) are standard text Key-Value nodes read by the exact same `Archive` parser.
|
||||
|
||||
### 4.2 Eliminating Intermediate Stack Allocations
|
||||
Under the new pipeline:
|
||||
1. The `.jasset` text file is read into memory onto a `TempArena` via `LoadFile`.
|
||||
2. The lines are tokenized into a `TextPropertyNode` scratch table.
|
||||
3. The engine reads `; class` (e.g. `"Inert"`) and resolves `Class* classPtr = FindClassByName(className)`.
|
||||
4. `AllocateEntity(manager, classPtr)` is called immediately. Memory for both base and derived components is allocated in-place in their permanent engine arenas.
|
||||
5. Base properties (`id`, `position`, etc.) are read directly into `*basePtr` via `SerializeEntityBase`.
|
||||
6. `classPtr->serialize_fct(ar, basePtr->Derived, classVersion)` is called. Derived fields stream **directly into the typed arena** without temporary staging buffers or stack copies.
|
||||
2. The property nodes are parsed into a `ParsedArchive` via `tokenize_archive(tempArena.Arena, fileBuffer, &ar.base)`.
|
||||
3. The engine reads `; class` (e.g. `"Inert"`) and resolves `Class* class_ptr = find_class_by_name(class_name)`.
|
||||
4. `AllocateEntity(manager, class_ptr)` is called immediately. Memory for both base and derived components is allocated in-place in their permanent engine arenas.
|
||||
5. `serialize(ar, NonNullPtr<Entity>(base_ptr))` is called. Base (`Entity::kind`) and derived (`base_ptr->derived_kind`) fields stream **directly into their permanent memory arenas** without temporary staging buffers or stack copies.
|
||||
|
||||
### 4.3 Runtime Class Resolution
|
||||
To ensure fast and safe type lookup during file deserialization:
|
||||
@@ -387,73 +384,58 @@ To ensure fast and safe type lookup during file deserialization:
|
||||
| In-Place Deserialization Flowchart |
|
||||
+---------------------------------------------------------------------------------------+
|
||||
| |
|
||||
| 1. Read Header from IOStream/ByteBuffer |
|
||||
| 1. LoadFile(scratch.Arena, filepath) into ByteBuffer |
|
||||
| | |
|
||||
| v |
|
||||
| 2. Validate Magic ('JAST') and Version (1) |
|
||||
| 2. tokenize_archive(scratch.Arena, file_buffer, &ar.base) |
|
||||
| | |
|
||||
| v |
|
||||
| 3. Resolve Class* from header.Kind & header.ClassCRC |
|
||||
| 3. Read "; class" & Resolve Class* via find_class_by_name(class_name) |
|
||||
| | |
|
||||
| v |
|
||||
| 4. basePtr = AllocateEntity(manager, classPtr) |
|
||||
| 4. base_ptr = AllocateEntity(manager, class_ptr) |
|
||||
| | |
|
||||
| +--> [manager.Entities]: Allocates base Entity |
|
||||
| +--> [manager.by_type[kind].arena]: Allocates derived struct |
|
||||
| +--> Mutual Back-Pointers Wired In-Place |
|
||||
| | |
|
||||
| v |
|
||||
| 5. Direct Copy Base Properties (ID, X, Y, Z) |
|
||||
| 5. serialize(ar, NonNullPtr<Entity>(base_ptr)) |
|
||||
| | |
|
||||
| +--> Streams Base Entity (Entity::kind, version, ID, position) |
|
||||
| +--> Streams Derived Component (base_ptr->derived_kind, class_version) |
|
||||
| | |
|
||||
| v |
|
||||
| 6. Does classPtr->serialize_fct exist? |
|
||||
| | | |
|
||||
| Yes No |
|
||||
| | | |
|
||||
| v | |
|
||||
| Invoke: | |
|
||||
| serialize_fct(&ar, | |
|
||||
| Derived) | |
|
||||
| | | |
|
||||
| +---------------------+ |
|
||||
| | |
|
||||
| v |
|
||||
| 7. Clear Dirty Flag: basePtr->IsDirty = false |
|
||||
| 6. Clear Dirty Flag: base_ptr->is_dirty = false |
|
||||
| |
|
||||
+---------------------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
```cpp
|
||||
[[nodiscard]] Entity* DeserializeEntityInPlace(EntityManager& manager, archive& ar)
|
||||
[[nodiscard]] Entity* deserialize_entity_in_place(EntityManager& manager, Archive& ar)
|
||||
{
|
||||
Assert(ar.loading);
|
||||
|
||||
// 1. Read class name and resolve Class*
|
||||
String className = {};
|
||||
SerializeProp(ar, "class", className, ar.arena);
|
||||
Class* classPtr = FindClassByName(className);
|
||||
if (!classPtr)
|
||||
String class_name = {};
|
||||
SERIALIZE(ar, class, class_name);
|
||||
Class* class_ptr = find_class_by_name(class_name);
|
||||
if (!class_ptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// 2. Allocate persistent memory for base and derived components in their respective arenas
|
||||
Entity* basePtr = AllocateEntity(manager, classPtr);
|
||||
Assert(basePtr != nullptr);
|
||||
Entity* base_ptr = AllocateEntity(manager, class_ptr);
|
||||
Assert(base_ptr != nullptr);
|
||||
|
||||
// 3. Serialize Base Entity in-place using Entity::Kind
|
||||
SerializeClassInstance(ar, Entity::Kind, basePtr);
|
||||
|
||||
// 4. Stream derived properties in-place using basePtr->DerivedKind
|
||||
if (basePtr->DerivedKind != nullptr && basePtr->Derived != nullptr)
|
||||
{
|
||||
SerializeClassInstance(ar, basePtr->DerivedKind, basePtr->Derived);
|
||||
}
|
||||
// 3. Serialize Base Entity and Derived in-place
|
||||
serialize(ar, NonNullPtr<Entity>(base_ptr));
|
||||
|
||||
// Freshly loaded entity matches disk state exactly
|
||||
basePtr->IsDirty = false;
|
||||
base_ptr->is_dirty = false;
|
||||
|
||||
return basePtr;
|
||||
return base_ptr;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -653,26 +635,24 @@ In a level containing $10{,}000$ entities:
|
||||
- **Monolithic `world.bin` Save:** Modifying a single entity's $X$ coordinate requires re-serializing all $10{,}000$ entities and overwriting a multi-megabyte binary file. This introduces a huge Git diff and constant merge conflicts.
|
||||
- **Full-Directory `.jasset` Save:** Iterating through all $10{,}000$ entities and unconditionally writing $10{,}000$ `.jasset` files incurs massive OS file-system overhead (directory table locks, I/O bandwidth) and changes the file timestamps of every asset. Git reports thousands of modified files even when only one entity changed!
|
||||
|
||||
### 6.2 The `IsDirty` Flag on `Entity`
|
||||
### 6.2 The `is_dirty` Flag on `Entity`
|
||||
To solve this, `Entity` in [`Game/Entity/Entity.h`](file:///w:/Classified/Juliet/Game/Entity/Entity.h#L28-L36) is augmented with an explicit dirty flag:
|
||||
|
||||
```cpp
|
||||
struct Entity final
|
||||
{
|
||||
DECLARE_ENTITY() // static Class* Kind; (Entity's own Class descriptor)
|
||||
DECLARE_CLASS() // static Class* kind; (Entity's own Class descriptor)
|
||||
|
||||
EntityID ID = 0;
|
||||
Class* DerivedKind = nullptr; // Pointer to derived class descriptor (e.g. Inert::Kind)
|
||||
DerivedType Derived = nullptr; // Pointer to derived component memory
|
||||
float X = 0.0f;
|
||||
float Y = 0.0f;
|
||||
float Z = 0.0f;
|
||||
bool IsDirty = false;
|
||||
EntityID ID = 0;
|
||||
Class* derived_kind = nullptr; // Pointer to derived class descriptor (e.g. Inert::kind)
|
||||
DerivedType derived = nullptr; // Pointer to derived component memory
|
||||
Vector4 position = {};
|
||||
bool is_dirty = false;
|
||||
};
|
||||
```
|
||||
|
||||
### 6.3 Granular State Transitions
|
||||
The `IsDirty` flag obeys a strict lifecycle state machine:
|
||||
The `is_dirty` flag obeys a strict lifecycle state machine:
|
||||
|
||||
```
|
||||
+-----------------------------------+
|
||||
@@ -682,25 +662,25 @@ The `IsDirty` flag obeys a strict lifecycle state machine:
|
||||
|
|
||||
v
|
||||
+---------------+
|
||||
+------->| IsDirty: TRUE |<-------+
|
||||
+------->|is_dirty: TRUE |<-------+
|
||||
| +-------+-------+ |
|
||||
| | |
|
||||
Entity Mutated | SaveWorld
|
||||
(Position, Component) | Completed
|
||||
| v |
|
||||
| +---------------+ |
|
||||
+--------+ IsDirty: FALSE+--------+
|
||||
+--------+is_dirty: FALSE+--------+
|
||||
+-------+-------+
|
||||
^
|
||||
|
|
||||
Deserialization
|
||||
Deserialization
|
||||
(LoadWorld / Asset)
|
||||
```
|
||||
|
||||
1. **Entity Creation:** Newly spawned entities in the editor have `IsDirty = true`.
|
||||
2. **Property Mutation:** Any modification to `X, Y, Z` or derived component payload sets `entity->IsDirty = true`.
|
||||
3. **Successful Deserialization:** Entities loaded from disk initialize with `IsDirty = false`.
|
||||
4. **Successful Save:** Upon successfully writing an entity to its `.jasset` file, the engine resets `entity->IsDirty = false`.
|
||||
1. **Entity Creation:** Newly spawned entities in the editor have `is_dirty = true`.
|
||||
2. **Property Mutation:** Any modification to `position` or derived component payload sets `entity->is_dirty = true`.
|
||||
3. **Successful Deserialization:** Entities loaded from disk initialize with `is_dirty = false`.
|
||||
4. **Successful Save:** Upon successfully writing an entity to its `.jasset` file, the engine resets `entity->is_dirty = false`.
|
||||
|
||||
### 6.4 Version Control Benefits (Git Friendly Assets)
|
||||
By coupling the one-file-per-entity `.jasset` format with dirty tracking:
|
||||
@@ -712,13 +692,13 @@ By coupling the one-file-per-entity `.jasset` format with dirty tracking:
|
||||
In [`Game/Data/World.cpp`](file:///w:/Classified/Juliet/Game/Data/World.cpp#L291-L311), editor UI widgets automatically set the dirty flag upon receiving user input:
|
||||
|
||||
```cpp
|
||||
float pos[3] = { ent.X, ent.Y, ent.Z };
|
||||
float pos[4] = { ent.position.x, ent.position.y, ent.position.z, ent.position.w };
|
||||
if (ImGui::DragFloat3("Position", pos, 0.1f))
|
||||
{
|
||||
ent.X = pos[0];
|
||||
ent.Y = pos[1];
|
||||
ent.Z = pos[2];
|
||||
ent.IsDirty = true; // Mark dirty for persistence
|
||||
ent.position.x = pos[0];
|
||||
ent.position.y = pos[1];
|
||||
ent.position.z = pos[2];
|
||||
ent.is_dirty = true; // Mark dirty for persistence
|
||||
UpdateWorld(world);
|
||||
}
|
||||
```
|
||||
@@ -729,13 +709,12 @@ if (ImGui::DragFloat3("Position", pos, 0.1f))
|
||||
|
||||
### Phase 1: Data Structures & Header Definitions
|
||||
1. **Update `Entity.h`:**
|
||||
- Add `bool IsDirty = false;` to `struct Entity`.
|
||||
- Add `bool is_dirty = false;` to `struct Entity`.
|
||||
- Update `MakeEntity<EntityType>` to delegate to `AllocateEntity`.
|
||||
- Define `EntityFileHeader`, `kEntityAssetMagic`, and `kEntityAssetVersion`.
|
||||
2. **Update `EntityManager.h`:**
|
||||
- Declare `[[nodiscard]] Entity* AllocateEntity(EntityManager& manager, Class* classPtr);`.
|
||||
- Declare `[[nodiscard]] Entity* AllocateEntity(EntityManager& manager, Class* class_ptr);`.
|
||||
- Declare `void DestroyEntity(EntityManager& manager, EntityID id);`.
|
||||
- Declare `void RemoveDerivedComponent(EntityManager& manager, Class* classPtr, DerivedType derivedPtr);`.
|
||||
- Declare `void RemoveDerivedComponent(EntityManager& manager, Class* class_ptr, DerivedType derived_ptr);`.
|
||||
3. **Update `World.h`:**
|
||||
- Add `VectorArena<EntityID, 1024> PendingDeletions;` to `struct World`.
|
||||
- Update `SaveWorld` and `LoadWorld` signatures to take directory paths.
|
||||
@@ -745,7 +724,7 @@ if (ImGui::DragFloat3("Position", pos, 0.1f))
|
||||
- Enforce parameter assertions.
|
||||
- Push to `manager.Entities`.
|
||||
- Allocate zeroed block in `manager.by_type[kind].arena`.
|
||||
- Wire mutual pointers (`base->Derived` and `derived->Base`).
|
||||
- Wire mutual pointers (`base->derived` and `derived->base`).
|
||||
- Increment `typedArray.count` and initialize `typedArray.array`.
|
||||
2. Implement `DestroyEntity` & `RemoveDerivedComponent`:
|
||||
- Implement swap-and-pop for derived components with base pointer update.
|
||||
@@ -753,27 +732,25 @@ if (ImGui::DragFloat3("Position", pos, 0.1f))
|
||||
|
||||
### Phase 3: In-Place Deserialization & Serialization Pipeline
|
||||
1. In `Entity.cpp`:
|
||||
- Implement `EntitySerialize(archive& ar, Entity* entity)`.
|
||||
- Fix the existing reversed `if (ar.loading)` branch.
|
||||
- `serialize(Archive& ar, NonNullPtr<Entity> entity)` handles both base and derived class serialization.
|
||||
2. In `World.cpp`:
|
||||
- Implement `SerializeEntityAsset(archive& ar, Entity* entity, String filepath)`.
|
||||
- Implement `DeserializeEntityAsset(EntityManager& manager, archive& ar, String filepath)`.
|
||||
- Stream derived properties directly using `classPtr->serialize_fct(&ar, entity->Derived)`.
|
||||
- Implement `serialize_entity_asset(Archive& ar, NonNullPtr<Entity> entity, String filepath)`.
|
||||
- Implement `deserialize_entity_asset(EntityManager& manager, Archive& ar, String filepath)`.
|
||||
|
||||
### Phase 4: World Save/Load Pipeline & Disk Deletion
|
||||
1. In `World.cpp`:
|
||||
- Implement `ProcessPendingDeletions(World& world, NonNullPtr<Arena> scratchArena)`.
|
||||
- Implement `SaveWorld(World& world, String worldDirectory)`:
|
||||
- Process pending deletions.
|
||||
- Iterate `manager.Entities`, skipping entities where `!entity.IsDirty`.
|
||||
- Write dirty entities to `.jasset` files and clear `IsDirty`.
|
||||
- Iterate `manager.Entities`, skipping entities where `!entity.is_dirty`.
|
||||
- Write dirty entities to `.jasset` files and clear `is_dirty`.
|
||||
- Implement `LoadWorld(World& world, String worldDirectory)`:
|
||||
- Enumerate `.jasset` files in directory.
|
||||
- Call `DeserializeEntityAsset` for each file.
|
||||
- Call `deserialize_entity_asset` for each file.
|
||||
|
||||
### Phase 5: Editor Integration
|
||||
1. In `RenderWorldEditorUI`:
|
||||
- Hook `ImGui::DragFloat3` and property inspectors to set `IsDirty = true`.
|
||||
- Hook `ImGui::DragFloat3` and property inspectors to set `is_dirty = true`.
|
||||
- Hook "Add Entity" button to call `MakeEntity<Inert>(*world.EntityManager, 0.0f, 0.0f, 0.0f)`.
|
||||
- Hook "Delete Entity" button to call `RemoveWorldEntity(world, selectedEntityId)`.
|
||||
|
||||
@@ -824,26 +801,26 @@ namespace UnitTest
|
||||
EntityManager& manager = *testWorld.EntityManager;
|
||||
|
||||
// 1. Allocate Inert Entity via AllocateEntity
|
||||
Entity* baseEntity = AllocateEntity(manager, Inert::Kind);
|
||||
Assert(baseEntity != nullptr);
|
||||
Assert(baseEntity->ID > 0);
|
||||
Assert(baseEntity->Kind == Inert::Kind);
|
||||
Assert(baseEntity->Derived != nullptr);
|
||||
Assert(baseEntity->IsDirty == true);
|
||||
Entity* base_entity = AllocateEntity(manager, Inert::kind);
|
||||
Assert(base_entity != nullptr);
|
||||
Assert(base_entity->ID > 0);
|
||||
Assert(base_entity->derived_kind == Inert::kind);
|
||||
Assert(base_entity->derived != nullptr);
|
||||
Assert(base_entity->is_dirty == true);
|
||||
|
||||
// 2. Validate mutual back-pointer wiring
|
||||
auto* derived = reinterpret_cast<entity_template*>(baseEntity->Derived);
|
||||
Assert(derived->Base == baseEntity);
|
||||
auto* derived = reinterpret_cast<entity_template*>(base_entity->derived);
|
||||
Assert(derived->base == base_entity);
|
||||
|
||||
// 3. DownCast verification
|
||||
Inert* inert = DownCast<Inert>(baseEntity);
|
||||
Inert* inert = DownCast<Inert>(base_entity);
|
||||
Assert(inert != nullptr);
|
||||
Assert(inert->Base == baseEntity);
|
||||
Assert(inert->base == base_entity);
|
||||
|
||||
// 4. Validate typed array tracking
|
||||
typed_entity_array& inertArray = manager.by_type[ENTITY(Inert)];
|
||||
Assert(inertArray.count == 1);
|
||||
Assert(inertArray.array == derived);
|
||||
typed_entity_array& inert_array = manager.by_type[ENTITY(Inert)];
|
||||
Assert(inert_array.count == 1);
|
||||
Assert(inert_array.array == derived);
|
||||
|
||||
ShutdownEntityManager();
|
||||
ShutdownWorld(&testWorld);
|
||||
@@ -864,50 +841,39 @@ namespace UnitTest
|
||||
EntityManager& manager = *testWorld.EntityManager;
|
||||
|
||||
// 1. Create and populate entity
|
||||
Inert* createdInert = MakeEntity<Inert>(manager, 12.5f, -44.0f, 108.2f);
|
||||
Assert(createdInert != nullptr);
|
||||
createdInert->MeshInstance = 42;
|
||||
Inert* created_inert = MakeEntity<Inert>(manager, 12.5f, -44.0f, 108.2f);
|
||||
Assert(created_inert != nullptr);
|
||||
created_inert->MeshInstance = 42;
|
||||
|
||||
Entity* originalBase = createdInert->Base;
|
||||
EntityID originalID = originalBase->ID;
|
||||
Entity* original_base = created_inert->base;
|
||||
EntityID original_id = original_base->ID;
|
||||
|
||||
// 2. Serialize to memory archive
|
||||
archive saveAr{ .arena = tempArena.Arena, .base_ptr = nullptr, .offset = 0, .loading = false };
|
||||
saveAr.base_ptr = ArenaPushArray<uint8>(tempArena.Arena, Kilobytes(16));
|
||||
|
||||
EntityFileHeader header{
|
||||
.Magic = kEntityAssetMagic,
|
||||
.Version = kEntityAssetVersion,
|
||||
.ClassCRC = originalBase->Kind->CRC,
|
||||
.Kind = originalBase->Kind->kind,
|
||||
.EntityID = originalBase->ID,
|
||||
.PositionX = originalBase->X,
|
||||
.PositionY = originalBase->Y,
|
||||
.PositionZ = originalBase->Z,
|
||||
.PayloadSize = sizeof(index_t)
|
||||
};
|
||||
serialize_elem(saveAr, header);
|
||||
serialize_elem(saveAr, createdInert->MeshInstance);
|
||||
// 2. Serialize to text archive memory stream
|
||||
MemoryStream mem_stream = MakeMemoryStream(tempArena.Arena);
|
||||
Archive save_ar{ .arena = tempArena.Arena, .loading = false, .stream = &mem_stream };
|
||||
serialize(save_ar, NonNullPtr<Entity>(original_base));
|
||||
|
||||
// 3. Clear manager to simulate fresh load
|
||||
ShutdownEntityManager();
|
||||
InitEntityManager(&testWorld);
|
||||
EntityManager& freshManager = *testWorld.EntityManager;
|
||||
EntityManager& fresh_manager = *testWorld.EntityManager;
|
||||
|
||||
// 4. Deserialize in-place
|
||||
archive loadAr{ .arena = tempArena.Arena, .base_ptr = saveAr.base_ptr, .offset = 0, .loading = true };
|
||||
Entity* loadedBase = DeserializeEntityInPlace(freshManager, loadAr);
|
||||
// 4. Tokenize and deserialize in-place
|
||||
Archive load_ar{ .arena = tempArena.Arena, .loading = true };
|
||||
tokenize_archive(tempArena.Arena, mem_stream.buffer, &load_ar.base);
|
||||
Entity* loaded_base = deserialize_entity_in_place(fresh_manager, load_ar);
|
||||
|
||||
Assert(loadedBase != nullptr);
|
||||
Assert(loadedBase->ID == originalID);
|
||||
Assert(loadedBase->X == 12.5f);
|
||||
Assert(loadedBase->Y == -44.0f);
|
||||
Assert(loadedBase->Z == 108.2f);
|
||||
Assert(loadedBase->IsDirty == false);
|
||||
Assert(loaded_base != nullptr);
|
||||
Assert(loaded_base->ID == original_id);
|
||||
Assert(loaded_base->position.x == 12.5f);
|
||||
Assert(loaded_base->position.y == -44.0f);
|
||||
Assert(loaded_base->position.z == 108.2f);
|
||||
Assert(loaded_base->is_dirty == false);
|
||||
|
||||
Inert* loadedInert = DownCast<Inert>(loadedBase);
|
||||
Assert(loadedInert != nullptr);
|
||||
Assert(loadedInert->Base == loadedBase);
|
||||
Inert* loaded_inert = DownCast<Inert>(loaded_base);
|
||||
Assert(loaded_inert != nullptr);
|
||||
Assert(loaded_inert->base == loaded_base);
|
||||
Assert(loaded_inert->MeshInstance == 42);
|
||||
|
||||
ShutdownEntityManager();
|
||||
ShutdownWorld(&testWorld);
|
||||
@@ -932,9 +898,9 @@ namespace UnitTest
|
||||
Inert* e1 = MakeEntity<Inert>(manager, 2.0f, 0.0f, 0.0f);
|
||||
Inert* e2 = MakeEntity<Inert>(manager, 3.0f, 0.0f, 0.0f);
|
||||
|
||||
EntityID id0 = e0->Base->ID;
|
||||
EntityID id1 = e1->Base->ID;
|
||||
EntityID id2 = e2->Base->ID;
|
||||
EntityID id0 = e0->base->ID;
|
||||
EntityID id1 = e1->base->ID;
|
||||
EntityID id2 = e2->base->ID;
|
||||
|
||||
Assert(manager.Entities.Size() == 3);
|
||||
|
||||
@@ -950,11 +916,11 @@ namespace UnitTest
|
||||
Assert(remaining0->ID == id0);
|
||||
Assert(remaining1->ID == id2);
|
||||
|
||||
auto* derived0 = reinterpret_cast<entity_template*>(remaining0->Derived);
|
||||
auto* derived1 = reinterpret_cast<entity_template*>(remaining1->Derived);
|
||||
auto* derived0 = reinterpret_cast<entity_template*>(remaining0->derived);
|
||||
auto* derived1 = reinterpret_cast<entity_template*>(remaining1->derived);
|
||||
|
||||
Assert(derived0->Base == remaining0);
|
||||
Assert(derived1->Base == remaining1);
|
||||
Assert(derived0->base == remaining0);
|
||||
Assert(derived1->base == remaining1);
|
||||
|
||||
ShutdownEntityManager();
|
||||
ShutdownWorld(&testWorld);
|
||||
@@ -974,17 +940,17 @@ namespace UnitTest
|
||||
InitEntityManager(&testWorld);
|
||||
EntityManager& manager = *testWorld.EntityManager;
|
||||
|
||||
Entity* entity = AllocateEntity(manager, Inert::Kind);
|
||||
Assert(entity->IsDirty == true);
|
||||
Entity* entity = AllocateEntity(manager, Inert::kind);
|
||||
Assert(entity->is_dirty == true);
|
||||
|
||||
// Simulate save
|
||||
entity->IsDirty = false;
|
||||
Assert(entity->IsDirty == false);
|
||||
entity->is_dirty = false;
|
||||
Assert(entity->is_dirty == false);
|
||||
|
||||
// Simulate mutation
|
||||
entity->X += 1.0f;
|
||||
entity->IsDirty = true;
|
||||
Assert(entity->IsDirty == true);
|
||||
entity->position.x += 1.0f;
|
||||
entity->is_dirty = true;
|
||||
Assert(entity->is_dirty == true);
|
||||
|
||||
ShutdownEntityManager();
|
||||
ShutdownWorld(&testWorld);
|
||||
|
||||
Reference in New Issue
Block a user