updating serialization tdd with latest changes

This commit is contained in:
2026-09-07 20:05:17 -04:00
parent b1de6ccc49
commit 3123d2f2e6
4 changed files with 454 additions and 1378 deletions
+120 -154
View File
@@ -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);