updating serialization tdd with latest changes
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -82,10 +82,10 @@ This is the classic **chicken-and-egg memory problem**:
|
|||||||
### 1.4 Architectural Objectives
|
### 1.4 Architectural Objectives
|
||||||
This specification establishes a robust in-place lifecycle pipeline that completely eliminates stack temporaries and dynamic heap allocations:
|
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.
|
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.
|
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`).
|
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`.
|
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`.
|
- **Capacity:** Fixed reserve of 100,000 entities allocated from the `WorldArena`.
|
||||||
- **Memory Footprint:**
|
- **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}$$
|
$$\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 40\text{ bytes} \approx 4.0\text{ MB}$.
|
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.
|
- **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
|
### 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 `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`.
|
- 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 zeroed.
|
||||||
- `base->Derived` points to the derived struct.
|
- `base->derived` points to the derived struct.
|
||||||
- `derived->Base` points to the base `Entity`.
|
- `derived->base` points to the base `Entity`.
|
||||||
- `base->Kind` is assigned to `classPtr`.
|
- `base->derived_kind` is assigned to `class_ptr`.
|
||||||
- `base->ID` is assigned the next unique `EntityManager::ID`.
|
- `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::count` is incremented.
|
||||||
- `typed_entity_array::array` is initialized if this is the first entity of this type.
|
- `typed_entity_array::array` is initialized if this is the first entity of this type.
|
||||||
|
|
||||||
@@ -215,12 +215,10 @@ The implementation replaces the flawed `RegisterEntity` routine in [`Game/Entity
|
|||||||
// 1. Allocate uninitialized Base Entity in the contiguous VectorArena
|
// 1. Allocate uninitialized Base Entity in the contiguous VectorArena
|
||||||
Entity baseTemplate{};
|
Entity baseTemplate{};
|
||||||
baseTemplate.ID = EntityManager::ID++;
|
baseTemplate.ID = EntityManager::ID++;
|
||||||
baseTemplate.DerivedKind = derivedClassPtr;
|
baseTemplate.derived_kind = derivedClassPtr;
|
||||||
baseTemplate.Derived = nullptr;
|
baseTemplate.derived = nullptr;
|
||||||
baseTemplate.X = 0.0f;
|
baseTemplate.position = {};
|
||||||
baseTemplate.Y = 0.0f;
|
baseTemplate.is_dirty = true;
|
||||||
baseTemplate.Z = 0.0f;
|
|
||||||
baseTemplate.IsDirty = true;
|
|
||||||
|
|
||||||
manager.Entities.PushBack(baseTemplate);
|
manager.Entities.PushBack(baseTemplate);
|
||||||
Entity* basePtr = manager.Entities.Back();
|
Entity* basePtr = manager.Entities.Back();
|
||||||
@@ -343,16 +341,15 @@ Inert
|
|||||||
#### Important: No Header Structs for Derived Types
|
#### 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.
|
- **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`.
|
- **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
|
### 4.2 Eliminating Intermediate Stack Allocations
|
||||||
Under the new pipeline:
|
Under the new pipeline:
|
||||||
1. The `.jasset` text file is read into memory onto a `TempArena` via `LoadFile`.
|
1. The `.jasset` text file is read into memory onto a `TempArena` via `LoadFile`.
|
||||||
2. The lines are tokenized into a `TextPropertyNode` scratch table.
|
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* classPtr = FindClassByName(className)`.
|
3. The engine reads `; class` (e.g. `"Inert"`) and resolves `Class* class_ptr = find_class_by_name(class_name)`.
|
||||||
4. `AllocateEntity(manager, classPtr)` is called immediately. Memory for both base and derived components is allocated in-place in their permanent engine arenas.
|
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. Base properties (`id`, `position`, etc.) are read directly into `*basePtr` via `SerializeEntityBase`.
|
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.
|
||||||
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.
|
|
||||||
|
|
||||||
### 4.3 Runtime Class Resolution
|
### 4.3 Runtime Class Resolution
|
||||||
To ensure fast and safe type lookup during file deserialization:
|
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 |
|
| In-Place Deserialization Flowchart |
|
||||||
+---------------------------------------------------------------------------------------+
|
+---------------------------------------------------------------------------------------+
|
||||||
| |
|
| |
|
||||||
| 1. Read Header from IOStream/ByteBuffer |
|
| 1. LoadFile(scratch.Arena, filepath) into ByteBuffer |
|
||||||
| | |
|
| | |
|
||||||
| v |
|
| v |
|
||||||
| 2. Validate Magic ('JAST') and Version (1) |
|
| 2. tokenize_archive(scratch.Arena, file_buffer, &ar.base) |
|
||||||
| | |
|
| | |
|
||||||
| v |
|
| v |
|
||||||
| 3. Resolve Class* from header.Kind & header.ClassCRC |
|
| 3. Read "; class" & Resolve Class* via find_class_by_name(class_name) |
|
||||||
| | |
|
| | |
|
||||||
| v |
|
| v |
|
||||||
| 4. basePtr = AllocateEntity(manager, classPtr) |
|
| 4. base_ptr = AllocateEntity(manager, class_ptr) |
|
||||||
| | |
|
| | |
|
||||||
| +--> [manager.Entities]: Allocates base Entity |
|
| +--> [manager.Entities]: Allocates base Entity |
|
||||||
| +--> [manager.by_type[kind].arena]: Allocates derived struct |
|
| +--> [manager.by_type[kind].arena]: Allocates derived struct |
|
||||||
| +--> Mutual Back-Pointers Wired In-Place |
|
| +--> Mutual Back-Pointers Wired In-Place |
|
||||||
| | |
|
| | |
|
||||||
| v |
|
| 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 |
|
| v |
|
||||||
| 6. Does classPtr->serialize_fct exist? |
|
| 6. Clear Dirty Flag: base_ptr->is_dirty = false |
|
||||||
| | | |
|
|
||||||
| Yes No |
|
|
||||||
| | | |
|
|
||||||
| v | |
|
|
||||||
| Invoke: | |
|
|
||||||
| serialize_fct(&ar, | |
|
|
||||||
| Derived) | |
|
|
||||||
| | | |
|
|
||||||
| +---------------------+ |
|
|
||||||
| | |
|
|
||||||
| v |
|
|
||||||
| 7. Clear Dirty Flag: basePtr->IsDirty = false |
|
|
||||||
| |
|
| |
|
||||||
+---------------------------------------------------------------------------------------+
|
+---------------------------------------------------------------------------------------+
|
||||||
```
|
```
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
[[nodiscard]] Entity* DeserializeEntityInPlace(EntityManager& manager, archive& ar)
|
[[nodiscard]] Entity* deserialize_entity_in_place(EntityManager& manager, Archive& ar)
|
||||||
{
|
{
|
||||||
Assert(ar.loading);
|
Assert(ar.loading);
|
||||||
|
|
||||||
// 1. Read class name and resolve Class*
|
// 1. Read class name and resolve Class*
|
||||||
String className = {};
|
String class_name = {};
|
||||||
SerializeProp(ar, "class", className, ar.arena);
|
SERIALIZE(ar, class, class_name);
|
||||||
Class* classPtr = FindClassByName(className);
|
Class* class_ptr = find_class_by_name(class_name);
|
||||||
if (!classPtr)
|
if (!class_ptr)
|
||||||
{
|
{
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Allocate persistent memory for base and derived components in their respective arenas
|
// 2. Allocate persistent memory for base and derived components in their respective arenas
|
||||||
Entity* basePtr = AllocateEntity(manager, classPtr);
|
Entity* base_ptr = AllocateEntity(manager, class_ptr);
|
||||||
Assert(basePtr != nullptr);
|
Assert(base_ptr != nullptr);
|
||||||
|
|
||||||
// 3. Serialize Base Entity in-place using Entity::Kind
|
// 3. Serialize Base Entity and Derived in-place
|
||||||
SerializeClassInstance(ar, Entity::Kind, basePtr);
|
serialize(ar, NonNullPtr<Entity>(base_ptr));
|
||||||
|
|
||||||
// 4. Stream derived properties in-place using basePtr->DerivedKind
|
|
||||||
if (basePtr->DerivedKind != nullptr && basePtr->Derived != nullptr)
|
|
||||||
{
|
|
||||||
SerializeClassInstance(ar, basePtr->DerivedKind, basePtr->Derived);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Freshly loaded entity matches disk state exactly
|
// 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.
|
- **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!
|
- **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:
|
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
|
```cpp
|
||||||
struct Entity final
|
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;
|
EntityID ID = 0;
|
||||||
Class* DerivedKind = nullptr; // Pointer to derived class descriptor (e.g. Inert::Kind)
|
Class* derived_kind = nullptr; // Pointer to derived class descriptor (e.g. Inert::kind)
|
||||||
DerivedType Derived = nullptr; // Pointer to derived component memory
|
DerivedType derived = nullptr; // Pointer to derived component memory
|
||||||
float X = 0.0f;
|
Vector4 position = {};
|
||||||
float Y = 0.0f;
|
bool is_dirty = false;
|
||||||
float Z = 0.0f;
|
|
||||||
bool IsDirty = false;
|
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6.3 Granular State Transitions
|
### 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,14 +662,14 @@ The `IsDirty` flag obeys a strict lifecycle state machine:
|
|||||||
|
|
|
|
||||||
v
|
v
|
||||||
+---------------+
|
+---------------+
|
||||||
+------->| IsDirty: TRUE |<-------+
|
+------->|is_dirty: TRUE |<-------+
|
||||||
| +-------+-------+ |
|
| +-------+-------+ |
|
||||||
| | |
|
| | |
|
||||||
Entity Mutated | SaveWorld
|
Entity Mutated | SaveWorld
|
||||||
(Position, Component) | Completed
|
(Position, Component) | Completed
|
||||||
| v |
|
| v |
|
||||||
| +---------------+ |
|
| +---------------+ |
|
||||||
+--------+ IsDirty: FALSE+--------+
|
+--------+is_dirty: FALSE+--------+
|
||||||
+-------+-------+
|
+-------+-------+
|
||||||
^
|
^
|
||||||
|
|
|
|
||||||
@@ -697,10 +677,10 @@ The `IsDirty` flag obeys a strict lifecycle state machine:
|
|||||||
(LoadWorld / Asset)
|
(LoadWorld / Asset)
|
||||||
```
|
```
|
||||||
|
|
||||||
1. **Entity Creation:** Newly spawned entities in the editor have `IsDirty = true`.
|
1. **Entity Creation:** Newly spawned entities in the editor have `is_dirty = true`.
|
||||||
2. **Property Mutation:** Any modification to `X, Y, Z` or derived component payload sets `entity->IsDirty = 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 `IsDirty = false`.
|
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->IsDirty = 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)
|
### 6.4 Version Control Benefits (Git Friendly Assets)
|
||||||
By coupling the one-file-per-entity `.jasset` format with dirty tracking:
|
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:
|
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
|
```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))
|
if (ImGui::DragFloat3("Position", pos, 0.1f))
|
||||||
{
|
{
|
||||||
ent.X = pos[0];
|
ent.position.x = pos[0];
|
||||||
ent.Y = pos[1];
|
ent.position.y = pos[1];
|
||||||
ent.Z = pos[2];
|
ent.position.z = pos[2];
|
||||||
ent.IsDirty = true; // Mark dirty for persistence
|
ent.is_dirty = true; // Mark dirty for persistence
|
||||||
UpdateWorld(world);
|
UpdateWorld(world);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -729,13 +709,12 @@ if (ImGui::DragFloat3("Position", pos, 0.1f))
|
|||||||
|
|
||||||
### Phase 1: Data Structures & Header Definitions
|
### Phase 1: Data Structures & Header Definitions
|
||||||
1. **Update `Entity.h`:**
|
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`.
|
- Update `MakeEntity<EntityType>` to delegate to `AllocateEntity`.
|
||||||
- Define `EntityFileHeader`, `kEntityAssetMagic`, and `kEntityAssetVersion`.
|
|
||||||
2. **Update `EntityManager.h`:**
|
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 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`:**
|
3. **Update `World.h`:**
|
||||||
- Add `VectorArena<EntityID, 1024> PendingDeletions;` to `struct World`.
|
- Add `VectorArena<EntityID, 1024> PendingDeletions;` to `struct World`.
|
||||||
- Update `SaveWorld` and `LoadWorld` signatures to take directory paths.
|
- Update `SaveWorld` and `LoadWorld` signatures to take directory paths.
|
||||||
@@ -745,7 +724,7 @@ if (ImGui::DragFloat3("Position", pos, 0.1f))
|
|||||||
- Enforce parameter assertions.
|
- Enforce parameter assertions.
|
||||||
- Push to `manager.Entities`.
|
- Push to `manager.Entities`.
|
||||||
- Allocate zeroed block in `manager.by_type[kind].arena`.
|
- 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`.
|
- Increment `typedArray.count` and initialize `typedArray.array`.
|
||||||
2. Implement `DestroyEntity` & `RemoveDerivedComponent`:
|
2. Implement `DestroyEntity` & `RemoveDerivedComponent`:
|
||||||
- Implement swap-and-pop for derived components with base pointer update.
|
- 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
|
### Phase 3: In-Place Deserialization & Serialization Pipeline
|
||||||
1. In `Entity.cpp`:
|
1. In `Entity.cpp`:
|
||||||
- Implement `EntitySerialize(archive& ar, Entity* entity)`.
|
- `serialize(Archive& ar, NonNullPtr<Entity> entity)` handles both base and derived class serialization.
|
||||||
- Fix the existing reversed `if (ar.loading)` branch.
|
|
||||||
2. In `World.cpp`:
|
2. In `World.cpp`:
|
||||||
- Implement `SerializeEntityAsset(archive& ar, Entity* entity, String filepath)`.
|
- Implement `serialize_entity_asset(Archive& ar, NonNullPtr<Entity> entity, String filepath)`.
|
||||||
- Implement `DeserializeEntityAsset(EntityManager& manager, archive& ar, String filepath)`.
|
- Implement `deserialize_entity_asset(EntityManager& manager, Archive& ar, String filepath)`.
|
||||||
- Stream derived properties directly using `classPtr->serialize_fct(&ar, entity->Derived)`.
|
|
||||||
|
|
||||||
### Phase 4: World Save/Load Pipeline & Disk Deletion
|
### Phase 4: World Save/Load Pipeline & Disk Deletion
|
||||||
1. In `World.cpp`:
|
1. In `World.cpp`:
|
||||||
- Implement `ProcessPendingDeletions(World& world, NonNullPtr<Arena> scratchArena)`.
|
- Implement `ProcessPendingDeletions(World& world, NonNullPtr<Arena> scratchArena)`.
|
||||||
- Implement `SaveWorld(World& world, String worldDirectory)`:
|
- Implement `SaveWorld(World& world, String worldDirectory)`:
|
||||||
- Process pending deletions.
|
- Process pending deletions.
|
||||||
- Iterate `manager.Entities`, skipping entities where `!entity.IsDirty`.
|
- Iterate `manager.Entities`, skipping entities where `!entity.is_dirty`.
|
||||||
- Write dirty entities to `.jasset` files and clear `IsDirty`.
|
- Write dirty entities to `.jasset` files and clear `is_dirty`.
|
||||||
- Implement `LoadWorld(World& world, String worldDirectory)`:
|
- Implement `LoadWorld(World& world, String worldDirectory)`:
|
||||||
- Enumerate `.jasset` files in directory.
|
- Enumerate `.jasset` files in directory.
|
||||||
- Call `DeserializeEntityAsset` for each file.
|
- Call `deserialize_entity_asset` for each file.
|
||||||
|
|
||||||
### Phase 5: Editor Integration
|
### Phase 5: Editor Integration
|
||||||
1. In `RenderWorldEditorUI`:
|
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 "Add Entity" button to call `MakeEntity<Inert>(*world.EntityManager, 0.0f, 0.0f, 0.0f)`.
|
||||||
- Hook "Delete Entity" button to call `RemoveWorldEntity(world, selectedEntityId)`.
|
- Hook "Delete Entity" button to call `RemoveWorldEntity(world, selectedEntityId)`.
|
||||||
|
|
||||||
@@ -824,26 +801,26 @@ namespace UnitTest
|
|||||||
EntityManager& manager = *testWorld.EntityManager;
|
EntityManager& manager = *testWorld.EntityManager;
|
||||||
|
|
||||||
// 1. Allocate Inert Entity via AllocateEntity
|
// 1. Allocate Inert Entity via AllocateEntity
|
||||||
Entity* baseEntity = AllocateEntity(manager, Inert::Kind);
|
Entity* base_entity = AllocateEntity(manager, Inert::kind);
|
||||||
Assert(baseEntity != nullptr);
|
Assert(base_entity != nullptr);
|
||||||
Assert(baseEntity->ID > 0);
|
Assert(base_entity->ID > 0);
|
||||||
Assert(baseEntity->Kind == Inert::Kind);
|
Assert(base_entity->derived_kind == Inert::kind);
|
||||||
Assert(baseEntity->Derived != nullptr);
|
Assert(base_entity->derived != nullptr);
|
||||||
Assert(baseEntity->IsDirty == true);
|
Assert(base_entity->is_dirty == true);
|
||||||
|
|
||||||
// 2. Validate mutual back-pointer wiring
|
// 2. Validate mutual back-pointer wiring
|
||||||
auto* derived = reinterpret_cast<entity_template*>(baseEntity->Derived);
|
auto* derived = reinterpret_cast<entity_template*>(base_entity->derived);
|
||||||
Assert(derived->Base == baseEntity);
|
Assert(derived->base == base_entity);
|
||||||
|
|
||||||
// 3. DownCast verification
|
// 3. DownCast verification
|
||||||
Inert* inert = DownCast<Inert>(baseEntity);
|
Inert* inert = DownCast<Inert>(base_entity);
|
||||||
Assert(inert != nullptr);
|
Assert(inert != nullptr);
|
||||||
Assert(inert->Base == baseEntity);
|
Assert(inert->base == base_entity);
|
||||||
|
|
||||||
// 4. Validate typed array tracking
|
// 4. Validate typed array tracking
|
||||||
typed_entity_array& inertArray = manager.by_type[ENTITY(Inert)];
|
typed_entity_array& inert_array = manager.by_type[ENTITY(Inert)];
|
||||||
Assert(inertArray.count == 1);
|
Assert(inert_array.count == 1);
|
||||||
Assert(inertArray.array == derived);
|
Assert(inert_array.array == derived);
|
||||||
|
|
||||||
ShutdownEntityManager();
|
ShutdownEntityManager();
|
||||||
ShutdownWorld(&testWorld);
|
ShutdownWorld(&testWorld);
|
||||||
@@ -864,50 +841,39 @@ namespace UnitTest
|
|||||||
EntityManager& manager = *testWorld.EntityManager;
|
EntityManager& manager = *testWorld.EntityManager;
|
||||||
|
|
||||||
// 1. Create and populate entity
|
// 1. Create and populate entity
|
||||||
Inert* createdInert = MakeEntity<Inert>(manager, 12.5f, -44.0f, 108.2f);
|
Inert* created_inert = MakeEntity<Inert>(manager, 12.5f, -44.0f, 108.2f);
|
||||||
Assert(createdInert != nullptr);
|
Assert(created_inert != nullptr);
|
||||||
createdInert->MeshInstance = 42;
|
created_inert->MeshInstance = 42;
|
||||||
|
|
||||||
Entity* originalBase = createdInert->Base;
|
Entity* original_base = created_inert->base;
|
||||||
EntityID originalID = originalBase->ID;
|
EntityID original_id = original_base->ID;
|
||||||
|
|
||||||
// 2. Serialize to memory archive
|
// 2. Serialize to text archive memory stream
|
||||||
archive saveAr{ .arena = tempArena.Arena, .base_ptr = nullptr, .offset = 0, .loading = false };
|
MemoryStream mem_stream = MakeMemoryStream(tempArena.Arena);
|
||||||
saveAr.base_ptr = ArenaPushArray<uint8>(tempArena.Arena, Kilobytes(16));
|
Archive save_ar{ .arena = tempArena.Arena, .loading = false, .stream = &mem_stream };
|
||||||
|
serialize(save_ar, NonNullPtr<Entity>(original_base));
|
||||||
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);
|
|
||||||
|
|
||||||
// 3. Clear manager to simulate fresh load
|
// 3. Clear manager to simulate fresh load
|
||||||
ShutdownEntityManager();
|
ShutdownEntityManager();
|
||||||
InitEntityManager(&testWorld);
|
InitEntityManager(&testWorld);
|
||||||
EntityManager& freshManager = *testWorld.EntityManager;
|
EntityManager& fresh_manager = *testWorld.EntityManager;
|
||||||
|
|
||||||
// 4. Deserialize in-place
|
// 4. Tokenize and deserialize in-place
|
||||||
archive loadAr{ .arena = tempArena.Arena, .base_ptr = saveAr.base_ptr, .offset = 0, .loading = true };
|
Archive load_ar{ .arena = tempArena.Arena, .loading = true };
|
||||||
Entity* loadedBase = DeserializeEntityInPlace(freshManager, loadAr);
|
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(loaded_base != nullptr);
|
||||||
Assert(loadedBase->ID == originalID);
|
Assert(loaded_base->ID == original_id);
|
||||||
Assert(loadedBase->X == 12.5f);
|
Assert(loaded_base->position.x == 12.5f);
|
||||||
Assert(loadedBase->Y == -44.0f);
|
Assert(loaded_base->position.y == -44.0f);
|
||||||
Assert(loadedBase->Z == 108.2f);
|
Assert(loaded_base->position.z == 108.2f);
|
||||||
Assert(loadedBase->IsDirty == false);
|
Assert(loaded_base->is_dirty == false);
|
||||||
|
|
||||||
Inert* loadedInert = DownCast<Inert>(loadedBase);
|
Inert* loaded_inert = DownCast<Inert>(loaded_base);
|
||||||
Assert(loadedInert != nullptr);
|
Assert(loaded_inert != nullptr);
|
||||||
Assert(loadedInert->Base == loadedBase);
|
Assert(loaded_inert->base == loaded_base);
|
||||||
|
Assert(loaded_inert->MeshInstance == 42);
|
||||||
|
|
||||||
ShutdownEntityManager();
|
ShutdownEntityManager();
|
||||||
ShutdownWorld(&testWorld);
|
ShutdownWorld(&testWorld);
|
||||||
@@ -932,9 +898,9 @@ namespace UnitTest
|
|||||||
Inert* e1 = MakeEntity<Inert>(manager, 2.0f, 0.0f, 0.0f);
|
Inert* e1 = MakeEntity<Inert>(manager, 2.0f, 0.0f, 0.0f);
|
||||||
Inert* e2 = MakeEntity<Inert>(manager, 3.0f, 0.0f, 0.0f);
|
Inert* e2 = MakeEntity<Inert>(manager, 3.0f, 0.0f, 0.0f);
|
||||||
|
|
||||||
EntityID id0 = e0->Base->ID;
|
EntityID id0 = e0->base->ID;
|
||||||
EntityID id1 = e1->Base->ID;
|
EntityID id1 = e1->base->ID;
|
||||||
EntityID id2 = e2->Base->ID;
|
EntityID id2 = e2->base->ID;
|
||||||
|
|
||||||
Assert(manager.Entities.Size() == 3);
|
Assert(manager.Entities.Size() == 3);
|
||||||
|
|
||||||
@@ -950,11 +916,11 @@ namespace UnitTest
|
|||||||
Assert(remaining0->ID == id0);
|
Assert(remaining0->ID == id0);
|
||||||
Assert(remaining1->ID == id2);
|
Assert(remaining1->ID == id2);
|
||||||
|
|
||||||
auto* derived0 = reinterpret_cast<entity_template*>(remaining0->Derived);
|
auto* derived0 = reinterpret_cast<entity_template*>(remaining0->derived);
|
||||||
auto* derived1 = reinterpret_cast<entity_template*>(remaining1->Derived);
|
auto* derived1 = reinterpret_cast<entity_template*>(remaining1->derived);
|
||||||
|
|
||||||
Assert(derived0->Base == remaining0);
|
Assert(derived0->base == remaining0);
|
||||||
Assert(derived1->Base == remaining1);
|
Assert(derived1->base == remaining1);
|
||||||
|
|
||||||
ShutdownEntityManager();
|
ShutdownEntityManager();
|
||||||
ShutdownWorld(&testWorld);
|
ShutdownWorld(&testWorld);
|
||||||
@@ -974,17 +940,17 @@ namespace UnitTest
|
|||||||
InitEntityManager(&testWorld);
|
InitEntityManager(&testWorld);
|
||||||
EntityManager& manager = *testWorld.EntityManager;
|
EntityManager& manager = *testWorld.EntityManager;
|
||||||
|
|
||||||
Entity* entity = AllocateEntity(manager, Inert::Kind);
|
Entity* entity = AllocateEntity(manager, Inert::kind);
|
||||||
Assert(entity->IsDirty == true);
|
Assert(entity->is_dirty == true);
|
||||||
|
|
||||||
// Simulate save
|
// Simulate save
|
||||||
entity->IsDirty = false;
|
entity->is_dirty = false;
|
||||||
Assert(entity->IsDirty == false);
|
Assert(entity->is_dirty == false);
|
||||||
|
|
||||||
// Simulate mutation
|
// Simulate mutation
|
||||||
entity->X += 1.0f;
|
entity->position.x += 1.0f;
|
||||||
entity->IsDirty = true;
|
entity->is_dirty = true;
|
||||||
Assert(entity->IsDirty == true);
|
Assert(entity->is_dirty == true);
|
||||||
|
|
||||||
ShutdownEntityManager();
|
ShutdownEntityManager();
|
||||||
ShutdownWorld(&testWorld);
|
ShutdownWorld(&testWorld);
|
||||||
|
|||||||
@@ -654,10 +654,8 @@ To guarantee robust loading regardless of traversal order:
|
|||||||
#### Phase 1: Allocation, Deserialization, and ID Registration
|
#### Phase 1: Allocation, Deserialization, and ID Registration
|
||||||
1. For each `.jasset` file found in `Entities/`:
|
1. For each `.jasset` file found in `Entities/`:
|
||||||
- Read the file buffer via `LoadFile`.
|
- Read the file buffer via `LoadFile`.
|
||||||
- Validate `EntityFileHeader` (Magic `kEntityMagic`, Version `kEntityVersion`).
|
- Tokenize text properties via `tokenize_archive`.
|
||||||
- Push an `Entity` record into `EntityManager::Entities`.
|
- Call `deserialize_entity_in_place(entityManager, ar)` which reads `; class`, invokes `AllocateEntity`, and streams base and derived fields in-place.
|
||||||
- Allocate the type-specific memory block in `EntityManager::by_type[Kind].arena`.
|
|
||||||
- Copy base transform values (`X, Y, Z`) and invoke `Class::serialize_fct`.
|
|
||||||
- All entity references are left as raw `EntityID` values (or embedded within components).
|
- All entity references are left as raw `EntityID` values (or embedded within components).
|
||||||
- Register the entity into a temporary lookup table: `EntityID -> Entity*`.
|
- Register the entity into a temporary lookup table: `EntityID -> Entity*`.
|
||||||
- Call `ObserveEntityIDForCounterContinuity` to maintain the high-water mark.
|
- Call `ObserveEntityIDForCounterContinuity` to maintain the high-water mark.
|
||||||
@@ -699,72 +697,38 @@ void EntityFileDiscoveryCallback(String filename, String fullPath, bool isDirect
|
|||||||
TempArena scratch = scratch_begin(0, 0);
|
TempArena scratch = scratch_begin(0, 0);
|
||||||
|
|
||||||
ByteBuffer fileBuffer = LoadFile(scratch.Arena, fullPath);
|
ByteBuffer fileBuffer = LoadFile(scratch.Arena, fullPath);
|
||||||
if (fileBuffer.Size < sizeof(EntityFileHeader))
|
if (fileBuffer.Size == 0)
|
||||||
{
|
{
|
||||||
Log(LogLevel::Error, LogCategory::Game, "LoadWorld: Entity file '%s' corrupted (too small)", CStr(filename));
|
Log(LogLevel::Error, LogCategory::Game, "LoadWorld: Entity file '%s' is empty or missing", CStr(filename));
|
||||||
scratch_end(scratch);
|
scratch_end(scratch);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto* header = reinterpret_cast<const EntityFileHeader*>(fileBuffer.Data);
|
Archive ar = {
|
||||||
if (header->Magic != 0x544E454A || header->Version != 1)
|
|
||||||
{
|
|
||||||
Log(LogLevel::Error, LogCategory::Game, "LoadWorld: File '%s' has invalid header", CStr(filename));
|
|
||||||
scratch_end(scratch);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 1: Allocate & deserialize
|
|
||||||
Entity baseEntity = {};
|
|
||||||
baseEntity.ID = header->ID;
|
|
||||||
baseEntity.Kind = kEntity_type_class_ptr[header->Kind];
|
|
||||||
Assert(baseEntity.Kind != nullptr);
|
|
||||||
|
|
||||||
archive ar = {
|
|
||||||
.arena = scratch.Arena,
|
.arena = scratch.Arena,
|
||||||
.base_ptr = fileBuffer.Data + sizeof(EntityFileHeader),
|
|
||||||
.offset = 0,
|
|
||||||
.loading = true
|
.loading = true
|
||||||
};
|
};
|
||||||
|
if (!tokenize_archive(scratch.Arena, fileBuffer, &ar.base))
|
||||||
// Deserialize base entity properties
|
|
||||||
serialize_elem(ar, baseEntity.X);
|
|
||||||
serialize_elem(ar, baseEntity.Y);
|
|
||||||
serialize_elem(ar, baseEntity.Z);
|
|
||||||
|
|
||||||
// Register into EntityManager
|
|
||||||
entityManager.Entities.PushBack(baseEntity);
|
|
||||||
Entity* registeredBase = entityManager.Entities.Back();
|
|
||||||
|
|
||||||
// Allocate derived type payload
|
|
||||||
auto* derivedPtr = static_cast<entity_template*>(ArenaPushSize(
|
|
||||||
entityManager.ByType[header->Kind].arena,
|
|
||||||
baseEntity.Kind->size_of,
|
|
||||||
baseEntity.Kind->alignment,
|
|
||||||
false JULIET_DEBUG_PARAM(kEntity_type_names[header->Kind])
|
|
||||||
));
|
|
||||||
Assert(derivedPtr != nullptr);
|
|
||||||
|
|
||||||
// Deserialize derived data if class registered a serialize function
|
|
||||||
if (baseEntity.Kind->serialize_fct != nullptr)
|
|
||||||
{
|
{
|
||||||
baseEntity.Kind->serialize_fct(ar, derivedPtr);
|
Log(LogLevel::Error, LogCategory::Game, "LoadWorld: Entity file '%s' failed to tokenize", CStr(filename));
|
||||||
|
scratch_end(scratch);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
derivedPtr->Base = registeredBase;
|
// Phase 1: Allocate & deserialize in-place
|
||||||
registeredBase->Derived = derivedPtr;
|
Entity* registeredBase = deserialize_entity_in_place(entityManager, ar);
|
||||||
|
if (registeredBase == nullptr)
|
||||||
if (entityManager.ByType[header->Kind].array == nullptr)
|
|
||||||
{
|
{
|
||||||
entityManager.ByType[header->Kind].array = derivedPtr;
|
Log(LogLevel::Error, LogCategory::Game, "LoadWorld: Entity file '%s' failed to deserialize", CStr(filename));
|
||||||
|
scratch_end(scratch);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
entityManager.ByType[header->Kind].count += 1;
|
|
||||||
|
|
||||||
// Track in temporary lookup table for Phase 2
|
// Track in temporary lookup table for Phase 2
|
||||||
context->LookupTable.PushBack({ .ID = header->ID, .EntityPtr = registeredBase });
|
context->LookupTable.PushBack({ .ID = registeredBase->ID, .EntityPtr = registeredBase });
|
||||||
|
|
||||||
// Update session counter continuity
|
// Update session counter continuity
|
||||||
ObserveEntityIDForCounterContinuity(entityManager, header->ID);
|
ObserveEntityIDForCounterContinuity(entityManager, registeredBase->ID);
|
||||||
|
|
||||||
scratch_end(scratch);
|
scratch_end(scratch);
|
||||||
}
|
}
|
||||||
@@ -838,15 +802,15 @@ void PostLoadWorld(World& world, const WorldLoadContext& context)
|
|||||||
String settingsPath = { settingsPathBuf, settingsPathLen - 1 };
|
String settingsPath = { settingsPathBuf, settingsPathLen - 1 };
|
||||||
|
|
||||||
ByteBuffer settingsBuffer = LoadFile(scratch.Arena, settingsPath);
|
ByteBuffer settingsBuffer = LoadFile(scratch.Arena, settingsPath);
|
||||||
if (settingsBuffer.Size >= sizeof(WorldSettingsFileHeader))
|
if (settingsBuffer.Size > 0)
|
||||||
{
|
{
|
||||||
const auto* header = reinterpret_cast<const WorldSettingsFileHeader*>(settingsBuffer.Data);
|
Archive settingsAr = {
|
||||||
if (header->Magic == 0x5453574A && header->Version == 1)
|
.arena = scratch.Arena,
|
||||||
|
.loading = true
|
||||||
|
};
|
||||||
|
if (tokenize_archive(scratch.Arena, settingsBuffer, &settingsAr.base))
|
||||||
{
|
{
|
||||||
const auto* settings = reinterpret_cast<const WorldEnvironmentSettings*>(
|
serialize(settingsAr, world.Environment);
|
||||||
settingsBuffer.Data + sizeof(WorldSettingsFileHeader)
|
|
||||||
);
|
|
||||||
world.Environment = *settings;
|
|
||||||
Log(LogLevel::Message, LogCategory::Game, "LoadWorld: Loaded WorldSettings from %s", CStr(settingsPath));
|
Log(LogLevel::Message, LogCategory::Game, "LoadWorld: Loaded WorldSettings from %s", CStr(settingsPath));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -912,9 +876,8 @@ graph TD
|
|||||||
- `[NEW]` [EntityFileIO.h](file:///w:/Classified/Juliet/Game/Entity/EntityFileIO.h)
|
- `[NEW]` [EntityFileIO.h](file:///w:/Classified/Juliet/Game/Entity/EntityFileIO.h)
|
||||||
- `[NEW]` [EntityFileIO.cpp](file:///w:/Classified/Juliet/Game/Entity/EntityFileIO.cpp)
|
- `[NEW]` [EntityFileIO.cpp](file:///w:/Classified/Juliet/Game/Entity/EntityFileIO.cpp)
|
||||||
- **Deliverables**:
|
- **Deliverables**:
|
||||||
- Define `WorldSettingsFileHeader` ('JWST') and `EntityFileHeader` ('JENT').
|
|
||||||
- Implement `AtomicWriteEntityFile` using `.tmp` and Win32 `MoveFileExA`.
|
- Implement `AtomicWriteEntityFile` using `.tmp` and Win32 `MoveFileExA`.
|
||||||
- Implement serialization routines for `WorldEnvironmentSettings`.
|
- Implement text serialization routines for `WorldEnvironmentSettings` and entity `.jasset` files.
|
||||||
|
|
||||||
### Phase 4: Two-Phase Load Pipeline & EntityManager Refactoring
|
### Phase 4: Two-Phase Load Pipeline & EntityManager Refactoring
|
||||||
- **Files**:
|
- **Files**:
|
||||||
|
|||||||
@@ -206,13 +206,13 @@ When `GetOrLoadTemplate` is invoked with a relative path:
|
|||||||
6. **Default Base Setup**: Initialize a local `Entity defaultBase`:
|
6. **Default Base Setup**: Initialize a local `Entity defaultBase`:
|
||||||
```cpp
|
```cpp
|
||||||
Entity defaultBase = {};
|
Entity defaultBase = {};
|
||||||
defaultBase.Kind = entityClass;
|
defaultBase.derived_kind = entityClass;
|
||||||
defaultBase.Derived = archetypeMem;
|
defaultBase.derived = archetypeMem;
|
||||||
```
|
```
|
||||||
7. **Back-Pointer Linking**: Set `entity_template::Base` in the archetype memory:
|
7. **Back-Pointer Linking**: Set `entity_template::base` in the archetype memory:
|
||||||
```cpp
|
```cpp
|
||||||
auto* archetypeTemplate = reinterpret_cast<entity_template*>(archetypeMem);
|
auto* archetypeTemplate = reinterpret_cast<entity_template*>(archetypeMem);
|
||||||
archetypeTemplate->Base = &cachedEntry->DefaultBase;
|
archetypeTemplate->base = &cachedEntry->DefaultBase;
|
||||||
```
|
```
|
||||||
8. **KV Property Parsing**: Parse all key-value pairs in the template `.jasset` file and write their deserialized values directly into `archetypeMem` and `defaultBase`.
|
8. **KV Property Parsing**: Parse all key-value pairs in the template `.jasset` file and write their deserialized values directly into `archetypeMem` and `defaultBase`.
|
||||||
9. **Cache Insertion**: Store the fully baked `CachedTemplate` record in `cache->Templates`.
|
9. **Cache Insertion**: Store the fully baked `CachedTemplate` record in `cache->Templates`.
|
||||||
@@ -228,21 +228,22 @@ In Juliet, an entity is split into two tightly coupled structures:
|
|||||||
```cpp
|
```cpp
|
||||||
struct Entity final
|
struct Entity final
|
||||||
{
|
{
|
||||||
|
DECLARE_CLASS()
|
||||||
|
|
||||||
EntityID ID = 0;
|
EntityID ID = 0;
|
||||||
Class* Kind = nullptr;
|
Class* derived_kind = nullptr;
|
||||||
DerivedType Derived = nullptr; // Points to the specialized struct
|
DerivedType derived = nullptr; // Points to the specialized struct
|
||||||
float X = 0.0f;
|
Vector4 position = {};
|
||||||
float Y = 0.0f;
|
bool is_dirty = false;
|
||||||
float Z = 0.0f;
|
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
2. **`Derived` (Specialized Type)**: e.g., `Inert`, `Collectible`, `Player`. The first member is always `DECLARE_ENTITY()`, which expands to:
|
2. **`Derived` (Specialized Type)**: e.g., `Inert`, `Collectible`, `Player`. The first member is always `DECLARE_ENTITY()`, which expands to:
|
||||||
```cpp
|
```cpp
|
||||||
Entity* Base; // Back-pointer to the base Entity
|
Entity* base; // Back-pointer to the base Entity
|
||||||
static Class* Kind;
|
DECLARE_CLASS() // static Class* kind;
|
||||||
```
|
```
|
||||||
|
|
||||||
Because `DerivedType` stores a back-pointer (`Base`) to `Entity`, **a shallow memory copy of an archetype invalidates this pointer**! The loading pipeline must explicitly restore this invariant.
|
Because `DerivedType` stores a back-pointer (`base`) to `Entity`, **a shallow memory copy of an archetype invalidates this pointer**! The loading pipeline must explicitly restore this invariant.
|
||||||
|
|
||||||
### 4.2 The 5-Step Instantiation Pipeline
|
### 4.2 The 5-Step Instantiation Pipeline
|
||||||
When an entity instance is spawned or deserialized from a `.jasset` file, the engine executes this strict 5-step sequence:
|
When an entity instance is spawned or deserialized from a `.jasset` file, the engine executes this strict 5-step sequence:
|
||||||
@@ -400,12 +401,12 @@ void ApplyKvDeltaOverrides(Entity* base, void* derived, Class* cls, String kvCon
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check base properties first
|
// Check base properties first
|
||||||
if (StringCompare(pair.Key, ConstString("Position")) == 0)
|
if (StringCompare(pair.Key, ConstString("Position")) == 0 || StringCompare(pair.Key, ConstString("position")) == 0)
|
||||||
{
|
{
|
||||||
Vector3 pos = ParseVector3(pair.Value);
|
Vector3 pos = ParseVector3(pair.Value);
|
||||||
base->X = pos.X;
|
base->position.x = pos.x;
|
||||||
base->Y = pos.Y;
|
base->position.y = pos.y;
|
||||||
base->Z = pos.Z;
|
base->position.z = pos.z;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,8 +484,8 @@ bool CreateTemplateFromEntity(World& world,
|
|||||||
Assert(entityIndex < manager.Entities.Size());
|
Assert(entityIndex < manager.Entities.Size());
|
||||||
|
|
||||||
Entity* sourceEntity = &manager.Entities[entityIndex];
|
Entity* sourceEntity = &manager.Entities[entityIndex];
|
||||||
Class* entityKind = sourceEntity->Kind;
|
Class* entityKind = sourceEntity->derived_kind;
|
||||||
void* derivedMem = sourceEntity->Derived;
|
void* derivedMem = sourceEntity->derived;
|
||||||
|
|
||||||
// Format destination template path
|
// Format destination template path
|
||||||
String templatePath = Format(scratchArena, "Assets/Templates/{}.jasset", CStr(templateName));
|
String templatePath = Format(scratchArena, "Assets/Templates/{}.jasset", CStr(templateName));
|
||||||
@@ -561,11 +562,11 @@ void RenderEntityPropertyInspector(Entity* entity, CachedTemplate* archetype)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Iterate through properties
|
// Iterate through properties
|
||||||
Class* cls = entity->Kind;
|
Class* cls = entity->derived_kind;
|
||||||
for (size_t i = 0; i < cls->PropertyCount; ++i)
|
for (size_t i = 0; i < cls->PropertyCount; ++i)
|
||||||
{
|
{
|
||||||
const PropertyDescriptor& prop = cls->Properties[i];
|
const PropertyDescriptor& prop = cls->Properties[i];
|
||||||
void* instanceField = static_cast<uint8*>(entity->Derived) + prop.Offset;
|
void* instanceField = static_cast<uint8*>(entity->derived) + prop.Offset;
|
||||||
void* templateField = isTemplated ? (static_cast<uint8*>(archetype->DefaultDerivedMemory) + prop.Offset) : nullptr;
|
void* templateField = isTemplated ? (static_cast<uint8*>(archetype->DefaultDerivedMemory) + prop.Offset) : nullptr;
|
||||||
|
|
||||||
const bool isOverridden = isTemplated && (MemCompare(instanceField, templateField, GetPropertySize(prop.Type)) != 0);
|
const bool isOverridden = isTemplated && (MemCompare(instanceField, templateField, GetPropertySize(prop.Type)) != 0);
|
||||||
@@ -605,10 +606,10 @@ Reverting an entire entity to its template archetype restores all properties whi
|
|||||||
```cpp
|
```cpp
|
||||||
void RevertEntityToTemplate(NonNullPtr<Entity> instance, NonNullPtr<const CachedTemplate> archetype)
|
void RevertEntityToTemplate(NonNullPtr<Entity> instance, NonNullPtr<const CachedTemplate> archetype)
|
||||||
{
|
{
|
||||||
Assert(instance->Kind == archetype->EntityKind);
|
Assert(instance->derived_kind == archetype->EntityKind);
|
||||||
Assert(archetype->DefaultDerivedMemory != nullptr);
|
Assert(archetype->DefaultDerivedMemory != nullptr);
|
||||||
|
|
||||||
void* derivedMem = instance->Derived;
|
void* derivedMem = instance->derived;
|
||||||
const size_t derivedSize = archetype->EntityKind->size_of;
|
const size_t derivedSize = archetype->EntityKind->size_of;
|
||||||
|
|
||||||
// Preserve the current Base pointer
|
// Preserve the current Base pointer
|
||||||
@@ -619,16 +620,16 @@ void RevertEntityToTemplate(NonNullPtr<Entity> instance, NonNullPtr<const Cached
|
|||||||
|
|
||||||
// 2. Re-establish the Base back-pointer
|
// 2. Re-establish the Base back-pointer
|
||||||
auto* templateDerived = reinterpret_cast<entity_template*>(derivedMem);
|
auto* templateDerived = reinterpret_cast<entity_template*>(derivedMem);
|
||||||
templateDerived->Base = basePtr;
|
templateDerived->base = basePtr;
|
||||||
|
|
||||||
// 3. Mark visual / physics state as updated
|
// 3. Mark visual / physics state as updated
|
||||||
if (instance->Kind->kind == ENTITY(Inert))
|
if (instance->derived_kind->kind == ENTITY(Inert))
|
||||||
{
|
{
|
||||||
auto* inert = reinterpret_cast<Inert*>(derivedMem);
|
auto* inert = reinterpret_cast<Inert*>(derivedMem);
|
||||||
if (inert->MeshInstance != indexMax)
|
if (inert->MeshInstance != indexMax)
|
||||||
{
|
{
|
||||||
SetMeshInstanceTransform(inert->MeshInstance,
|
SetMeshInstanceTransform(inert->MeshInstance,
|
||||||
MatrixTranslation(basePtr->X, basePtr->Y, basePtr->Z));
|
MatrixTranslation(basePtr->position.x, basePtr->position.y, basePtr->position.z));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -844,17 +845,17 @@ namespace UnitTest
|
|||||||
Assert(instance != nullptr);
|
Assert(instance != nullptr);
|
||||||
|
|
||||||
// Verify Step 1 & 2: Base spatial delta applied, non-overridden derived property retained
|
// Verify Step 1 & 2: Base spatial delta applied, non-overridden derived property retained
|
||||||
Assert(instance->X == 100.0f);
|
Assert(instance->position.x == 100.0f);
|
||||||
Assert(instance->Y == 200.0f);
|
Assert(instance->position.y == 200.0f);
|
||||||
Assert(instance->Z == 300.0f);
|
Assert(instance->position.z == 300.0f);
|
||||||
|
|
||||||
auto* inertDerived = DownCast<Inert>(instance);
|
auto* inertDerived = DownCast<Inert>(instance);
|
||||||
Assert(inertDerived != nullptr);
|
Assert(inertDerived != nullptr);
|
||||||
Assert(inertDerived->MeshInstance == 42); // Retained from template!
|
Assert(inertDerived->MeshInstance == 42); // Retained from template!
|
||||||
|
|
||||||
// Verify Step 3: CRITICAL back-pointer fixup check
|
// Verify Step 3: CRITICAL back-pointer fixup check
|
||||||
Assert(inertDerived->Base == instance);
|
Assert(inertDerived->base == instance);
|
||||||
Assert(instance->Derived == inertDerived);
|
Assert(instance->derived == inertDerived);
|
||||||
|
|
||||||
ShutdownEntityManager();
|
ShutdownEntityManager();
|
||||||
ShutdownWorld(&world);
|
ShutdownWorld(&world);
|
||||||
@@ -900,12 +901,12 @@ namespace UnitTest
|
|||||||
Assert(inertDerived->MeshInstance == 100);
|
Assert(inertDerived->MeshInstance == 100);
|
||||||
|
|
||||||
// Verify world position is preserved across revert
|
// Verify world position is preserved across revert
|
||||||
Assert(instance->X == 5.0f);
|
Assert(instance->position.x == 5.0f);
|
||||||
Assert(instance->Y == 5.0f);
|
Assert(instance->position.y == 5.0f);
|
||||||
Assert(instance->Z == 5.0f);
|
Assert(instance->position.z == 5.0f);
|
||||||
|
|
||||||
// Verify back-pointer invariant preserved after revert
|
// Verify back-pointer invariant preserved after revert
|
||||||
Assert(inertDerived->Base == instance);
|
Assert(inertDerived->base == instance);
|
||||||
|
|
||||||
ShutdownEntityManager();
|
ShutdownEntityManager();
|
||||||
ShutdownWorld(&world);
|
ShutdownWorld(&world);
|
||||||
|
|||||||
Reference in New Issue
Block a user