ongoing refactor of serialization and various cleanup

This commit is contained in:
2026-09-07 16:58:57 -04:00
parent 1a045c3578
commit a462575af4
43 changed files with 952 additions and 483 deletions
-2
View File
@@ -1,6 +1,4 @@
#pragma once
#include <Core/Common/CoreTypes.h>
constexpr index_t kPlayCamera = 0;
constexpr index_t kDebugCamera = 1;
+35 -31
View File
@@ -24,14 +24,14 @@ void ShutdownWorld(NonNullPtr<World> world)
world->WorldArena = nullptr;
}
void AddToWorld(NonNullPtr<World> world, NonNullPtr<Entity> entity)
void AddToWorld(NonNullPtr<World> /*world*/, NonNullPtr<Entity> /*entity*/)
{
// RegisterEntity(*world->EntityManager, entity.Get());
}
void RemoveWorldEntity(World& world, size_t index) {}
void RemoveWorldEntity(World& /*world*/, size_t /*index*/) {}
void Serialize(archive& ar, World& world, String filename)
void Serialize(Archive& ar, World& /*world*/, String filename)
{
Assert(IsValid(filename));
@@ -41,28 +41,33 @@ void Serialize(archive& ar, World& world, String filename)
{
// Load
ByteBuffer fileBuffer = LoadFile(ar.arena, filename);
if (fileBuffer.Size >= sizeof(WorldFileHeader))
{
ar.base_ptr = fileBuffer.Data;
WorldFileHeader header;
serialize_elem(ar, header);
Assert(header.Magic == kWorldMagic);
Assert(header.Version == kWorldVersion);
// TEST SERIALIZATION
ParsedArchive archive = tokenize_archive(ar.arena, fileBuffer);
// ArchivePropertyNode* property = find_property(&archive, "Position"_crc32);
audit_unconsumed_properties(&archive, ConstString("World"));
for (typed_entity_array& type : entityManager.by_type)
{
serialize_elem(ar, type.count);
if (type.count > 0)
{
// Unserialize the base entity to get informations
Entity entity;
serialize(ar, &entity);
}
}
}
// if (fileBuffer.Size >= sizeof(WorldFileHeader))
// {
// ar.base_ptr = fileBuffer.Data;
//
// WorldFileHeader header;
// serialize_elem(ar, header);
// Assert(header.Magic == kWorldMagic);
//
// for (typed_entity_array& type : entityManager.by_type)
// {
// serialize_elem(ar, type.count);
//
// if (type.count > 0)
// {
//
// // Unserialize the base entity to get informations
// Entity entity;
// // serialize(ar, &entity);
// }
// }
// }
}
else
{
@@ -72,9 +77,8 @@ void Serialize(archive& ar, World& world, String filename)
index_t beginPos = ArenaPos(ar.arena);
// Headers
auto* header = ArenaPushStruct<WorldFileHeader>(ar.arena);
header->Magic = kWorldMagic;
header->Version = kWorldVersion;
auto* header = ArenaPushStruct<WorldFileHeader>(ar.arena);
header->Magic = kWorldMagic;
ar.base_ptr = header;
@@ -84,12 +88,12 @@ void Serialize(archive& ar, World& world, String filename)
serialize_elem(ar, type.count);
auto* element = type.array;
uint8* rawElement = reinterpret_cast<uint8*>(element);
size_t stride = element->Base->Kind->size_of;
size_t stride = element->base->derived_kind->size_of;
for (index_t idx = 0; idx < type.count; ++idx)
{
// Todo : utils
// Get base entity from type
Entity* entity = reinterpret_cast<entity_template*>(rawElement + (idx * stride))->Base;
Entity* entity = reinterpret_cast<entity_template*>(rawElement + (idx * stride))->base;
serialize(ar, entity);
}
}
@@ -164,7 +168,7 @@ void Serialize(archive& ar, World& world, String filename)
// return true;
}
[[nodiscard]] bool LoadWorld(World& world, String filename)
[[nodiscard]] bool LoadWorld(World& /*world*/, String filename)
{
Assert(IsValid(filename));
// Assert(world.WorldArena != nullptr);
@@ -236,13 +240,13 @@ void RenderWorldEditorUI(World& world)
if (ImGui::Button("Save World"))
{
archive data{ .arena = temp.Arena, .loading = false };
Archive data = { .arena = temp.Arena, .loading = false };
Serialize(data, world, path);
}
ImGui::SameLine();
if (ImGui::Button("Load World"))
{
archive data{ .arena = temp.Arena, .loading = true };
Archive data{ .arena = temp.Arena, .loading = true };
Serialize(data, world, path);
}
+3 -5
View File
@@ -1,20 +1,18 @@
#pragma once
#include <Core/Common/CoreTypes.h>
#include <Core/Common/NonNullPtr.h>
#include <Core/Common/String.h>
#include <Core/Container/Vector.h>
#include <Core/Memory/MemoryArena.h>
#include <Entity/Entity.h>
struct archive;
struct Archive;
struct EntityManager;
#pragma pack(push, 1)
struct WorldFileHeader
{
uint32 Magic = 0x444C574A; // 'JWLD' in little-endian
uint32 Version = 1;
uint32 Magic = 0x444C574A; // 'JWLD' in little-endian
};
#pragma pack(pop)
@@ -33,7 +31,7 @@ void ShutdownWorld(NonNullPtr<World> world);
void AddToWorld(NonNullPtr<World> world, NonNullPtr<Entity> entity);
void RemoveWorldEntity(World& world, size_t index);
void Serialize(archive& data, World& world, String filename);
void Serialize(Archive& data, World& world, String filename);
#if JULIET_EDITOR
void RenderWorldEditorUI(World& world);
+13 -16
View File
@@ -2,24 +2,21 @@
#include <Core/Common/serialization.h>
DEFINE_ENTITY(Inert);
DEFINE_CLASS_VERSIONED(Entity, 1, nullptr, nullptr)
void serialize(archive& ar, NonNullPtr<Entity> entity)
DEFINE_ENTITY_VERSIONED(Inert, 1, nullptr)
void serialize(Archive& ar, NonNullPtr<Entity> entity)
{
serialize_elem(ar, entity->ID);
// Entity fields
serialize(ar, Entity::kind, entity.Get());
if (ar.loading)
{
serialize_elem(ar, entity->Kind->kind);
}
else
{
uint8 kind;
serialize_elem(ar, kind);
entity->Kind = kEntity_type_class_ptr[kind];
}
SERIALIZE(ar, id, entity->ID);
SERIALIZE(ar, position, entity->position);
serialize_elem(ar, entity->X);
serialize_elem(ar, entity->Y);
serialize_elem(ar, entity->Z);
// Derived fields
if (entity->derived_kind != nullptr && entity->derived != nullptr)
{
serialize(ar, entity->derived_kind, entity->derived);
}
}
+24 -28
View File
@@ -2,24 +2,19 @@
#include <Core/Common/CoreUtils.h>
#include <Core/Common/EnumUtils.h>
#include <Core/Math/Vector.h>
#include <Core/Memory/Allocator.h>
#include <Core/Memory/MemoryArena.h>
#include <Engine/Class.h>
#define DECLARE_ENTITY() \
Entity* Base; \
static Class* Kind;
Entity* base; \
static Class* kind;
// Will register the class globally at launch
#define DEFINE_ENTITY(entity) \
Class entityKind##entity = \
MakeClass(ConstString(#entity), (uint8)Entity_Type::entity, sizeof(entity), alignof(entity), nullptr); \
Class* entity::Kind = &entityKind##entity;
#define DEFINE_ENTITY_SERIALIZED(entity, serialize_fct) \
Class entityKind##entity = \
MakeClass(ConstString(#entity), (uint8)Entity_Type::entity, sizeof(entity), alignof(entity), serialize_fct); \
Class* entity::Kind = &entityKind##entity;
#define DEFINE_ENTITY_VERSIONED(entity, version, serialize_fct) \
constexpr Class entityKind##entity = MakeClass(ConstString(#entity), (uint8)Entity_Type::entity, (version), \
&classKindEntity, sizeof(entity), alignof(entity), (serialize_fct)); \
Class* entity::kind = const_cast<Class*>(&entityKind##entity);
struct EntityManager;
using DerivedType = void*;
@@ -27,12 +22,12 @@ using EntityID = uint64_t;
struct Entity final
{
EntityID ID = 0;
Class* Kind = nullptr;
DerivedType Derived = nullptr;
float X = 0.0f;
float Y = 0.0f;
float Z = 0.0f;
static Class* kind;
EntityID ID = 0;
Class* derived_kind = nullptr;
DerivedType derived = nullptr;
Vector4 position = {};
};
struct Inert
@@ -60,7 +55,7 @@ inline const char* kEntity_type_names[] = {
};
#undef AS_STR
#define AS_CLASS(name) name::Kind
#define AS_CLASS(name) name::kind
inline Class* kEntity_type_class_ptr[]
{
ENTITY_TYPE_LIST(AS_CLASS)
@@ -80,8 +75,8 @@ struct entity_template
//
template <typename EntityType>
concept EntityConcept = requires(EntityType entity) {
{ EntityType::Kind } -> std::convertible_to<const Class*>;
requires std::same_as<decltype(entity.Base), Entity*>;
{ EntityType::kind } -> std::convertible_to<const Class*>;
requires std::same_as<decltype(entity.base), Entity*>;
};
template <typename EntityType>
@@ -89,7 +84,7 @@ template <typename EntityType>
[[nodiscard]] bool IsA(const Entity* entity)
{
Assert(entity != nullptr);
return entity->Kind == EntityType::Kind;
return entity->derived_kind == EntityType::kind;
}
template <typename EntityType>
@@ -98,10 +93,11 @@ template <typename EntityType>
{
EntityType result;
Entity base;
base.X = x;
base.Y = y;
base.Z = z;
base.Kind = EntityType::Kind;
base.position.x = x;
base.position.y = y;
base.position.z = z;
base.position.w = 1.0f;
base.derived_kind = EntityType::kind;
return (EntityType*)RegisterEntity(manager, &base, &result);
}
@@ -112,7 +108,7 @@ template <typename EntityType>
{
Assert(entity != nullptr);
Assert(IsA<EntityType>(entity));
return static_cast<EntityType*>(entity->Derived);
return static_cast<EntityType*>(entity->derived);
}
void serialize(archive& ar, NonNullPtr<Entity> entity);
void serialize(Archive& ar, NonNullPtr<Entity> entity);
+12 -14
View File
@@ -46,30 +46,26 @@ EntityManager& GetEntityManager()
entity_template* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity)
{
base->ID = EntityManager::ID++;
base->Derived = entity;
base->derived = entity;
manager.Entities.PushBack(*base);
auto* ptr = (entity_template*)ArenaPushSize(manager.by_type[base->Kind->kind].arena, base->Kind->size_of, base->Kind->alignment,
false JULIET_DEBUG_PARAM(kEntity_type_names[base->Kind->kind]));
MemCopy(ptr, entity, base->Kind->size_of);
manager.by_type[base->Kind->kind].count += 1;
auto* ptr = (entity_template*)ArenaPushSize(manager.by_type[base->derived_kind->kind].arena,
base->derived_kind->size_of, base->derived_kind->alignment,
false JULIET_DEBUG_PARAM(kEntity_type_names[base->derived_kind->kind]));
MemCopy(ptr, entity, base->derived_kind->size_of);
manager.by_type[base->derived_kind->kind].count += 1;
ptr->Base = manager.Entities.Back();
ptr->base = manager.Entities.Back();
if (manager.by_type[base->Kind->kind].array == nullptr)
if (manager.by_type[base->derived_kind->kind].array == nullptr)
{
manager.by_type[base->Kind->kind].array = ptr;
manager.by_type[base->derived_kind->kind].array = ptr;
}
return ptr;
}
void RegisterBaseEntity(EntityManager& manager, Entity&& base)
{
GetEntityManager().Entities.PushBack(std::move(base));
}
void UpdateEntityManager(EntityManager& manager)
{
// Todo : inert by definition dont move, but this is for test
@@ -79,7 +75,9 @@ void UpdateEntityManager(EntityManager& manager)
Inert* inert = reinterpret_cast<Inert*>(by_type.array) + i;
if (inert->MeshInstance != indexMax)
{
SetMeshInstanceTransform(inert->MeshInstance, MatrixTranslation(inert->Base->X, inert->Base->Y, inert->Base->Z));
SetMeshInstanceTransform(inert->MeshInstance,
MatrixTranslation(inert->base->position.x, inert->base->position.y,
inert->base->position.z));
}
}
}
-2
View File
@@ -1,6 +1,5 @@
#pragma once
#include <Core/Common/CoreTypes.h>
#include <Core/Container/Vector.h>
#include <Entity/Entity.h>
@@ -28,5 +27,4 @@ void InitEntityManager(NonNullPtr<World> world);
void ShutdownEntityManager();
EntityManager& GetEntityManager();
entity_template* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity);
void RegisterBaseEntity(EntityManager& manager, Entity&& base);
void UpdateEntityManager(EntityManager& manager);
+157 -215
View File
@@ -24,7 +24,7 @@ The `.jasset` text archive framework is engineered to replace legacy binary blob
- **Symmetric Single-Function Serialization**: A single `Serialize` implementation per entity or data structure handles both Save and Load paths, guaranteeing that write and read schemas never diverge.
- **Graceful Forward/Backward Compatibility**: Missing keys during load automatically preserve struct default values. Unknown keys present in newer asset files are safely ignored without parse failure.
- **Two-Tier Decoupled Versioning**: Core engine entity properties (`kEntityBaseVersion`) and derived gameplay class properties (`Class::Version`) are versioned independently. Engine-level updates never bump derived entity class versions.
- **Post-Serialization In-Place Migration**: Deprecated fields can be read through specialized primitives (`ReadDeprecated*`) to migrate legacy data structures in-place upon loading, producing cleaned, modern schemas on subsequent saves.
- **In-Place Schema Migration**: Deprecated properties no longer present in modern structs can be read during loading into local stack variables using standard serialization (`serialize` / `SERIALIZE`) guarded by version checks (`if (ar.loading && ar.class_version < N)`), seamlessly transforming legacy values without struct pollution or persisting obsolete keys on subsequent saves.
- **Zero Exceptions & Total Warning Cleanliness**: Fully conforming to Juliet coding guidelines: strict assertions (`Assert`), `[[nodiscard]]`, `auto*`/`auto&`, mandatory braces, and `static_cast`/`reinterpret_cast`.
---
@@ -133,7 +133,7 @@ Identifier ::= [a-zA-Z_][a-zA-Z0-9_]* ;
; AssetType
Entity
; BaseVersion
; Version
1
; EntityID
@@ -712,10 +712,15 @@ In entity-component systems or object-oriented engine hierarchies, entities cons
#### The Fragility of Monolithic Versioning
In naive serialization architectures, a single `uint32 Version` governs the entire file. When an engine programmer updates the base `Entity` struct (e.g. adding a `uint32 LayerMask`), bumping the global version invalidates or forces schema changes across every single derived entity type in the project.
#### The Two-Tier Solution
Juliet decouples versioning into two independent tiers:
- **Tier 1: Base Engine Version (`kEntityBaseVersion`)**: Declared centrally in `Entity.h`. Governs `Entity` base fields.
- **Tier 2: Derived Class Version (`Class::Version`)**: Declared per-entity class in `Class.h` and initialized in `DEFINE_ENTITY_VERSIONED`.
#### The Two-Tier Solution: Universal `; Version` + Optional `; ClassVersion`
To keep `.jasset` files clean and unified across all asset types:
- **All `.jasset` files declare a universal `; Version` tag**:
- In simple standalone assets (e.g., `WorldSettings.jasset`, `Material.jasset`), `; Version` is the single asset schema version.
- In entity assets, `; Version` maps to the Base Engine Version (`kEntityBaseVersion` in `Entity.h`), governing core engine fields (`Position`, `Rotation`, `Scale`, etc.).
- **Entity classes with derived versioning add an optional `; ClassVersion`**:
- Governed by `Class::Version` in `Class.h`.
- Only written/read for entity classes that define custom versions. If omitted in the file, it defaults to `1`.
- Non-entity assets never see or use `; ClassVersion`.
```
========================================================================
@@ -723,7 +728,7 @@ Juliet decouples versioning into two independent tiers:
========================================================================
; AssetType
Entity
; BaseVersion ------> Governed by kEntityBaseVersion in Entity.h
; Version ------> Universal asset version (kEntityBaseVersion for Entity)
1
; EntityID
0x0000000000000001
@@ -732,32 +737,30 @@ Juliet decouples versioning into two independent tiers:
; Position
0.0 0.0 0.0
------------------------------------------------------------------------
; ClassVersion ------> Governed by Class::Version in Class.h
; ClassVersion ------> Optional derived version (Class::Version in Class.h)
2
; MeshInstance
42
========================================================================
```
When an engine programmer bumps `kEntityBaseVersion` from `1` to `2` to add `LayerMask`, no derived game classes (`Inert`, `Monster`, `Vehicle`) need version increments or code modifications.
When an engine programmer bumps `kEntityBaseVersion` from `1` to `2` to add `LayerMask`, no derived game classes (`Inert`, `Monster`, `Vehicle`) need version increments or code modifications. Conversely, non-entity assets (like `WorldSettings.jasset`) simply use `; Version \n 1` without carrying entity-specific terminology.
### 6.2 Implementation Details
### 6.2 Implementation Details: The Generalized `Class` Model
#### Engine Base Version (`Game/Entity/Entity.h`)
In this architecture, **every serializable struct in Juliet has a `Class` descriptor**:
#### Generalized `Class` Struct (`Juliet/include/Engine/Class.h`)
```cpp
constexpr uint32 kEntityBaseVersion = 1;
using serialize_fct_type = void (*)(archive& ar, void* payload, uint16 version);
void SerializeEntityBase(archive& ar, NonNullPtr<Entity> entity);
```
#### Derived Class Version (`Juliet/include/Engine/Class.h`)
```cpp
struct Class
{
uint32 CRC;
uint8 kind;
uint16 Version; // Added derived class version
serialize_fct_type serialize_fct;
uint16 Version; // Struct schema version
Class* BaseClass; // Pointer to parent class (or nullptr if root)
serialize_fct_type serialize_fct; // Type-specific serialization callback
size_t size_of;
size_t alignment;
@@ -766,12 +769,14 @@ struct Class
#endif
};
consteval Class MakeClass(String name, uint8 kind, uint16 version, size_t size, size_t align, serialize_fct_type fct)
consteval Class MakeClass(String name, uint8 kind, uint16 version, Class* baseClass,
size_t size, size_t align, serialize_fct_type fct)
{
Class cls = {};
cls.CRC = crc32(name.Str, name.Size);
cls.kind = kind;
cls.Version = version;
cls.BaseClass = baseClass;
cls.size_of = size;
cls.alignment = align;
cls.serialize_fct = fct;
@@ -784,80 +789,95 @@ consteval Class MakeClass(String name, uint8 kind, uint16 version, size_t size,
}
```
#### Class Registration Macros (`Game/Entity/Entity.h`)
#### Registration Macros
```cpp
// General class registration (e.g. for Entity, WorldSettings, Materials)
#define DEFINE_CLASS_VERSIONED(cls, version, base_class, serialize_fct) \
Class classKind##cls = \
MakeClass(ConstString(#cls), 0, (version), (base_class), sizeof(cls), alignof(cls), (serialize_fct)); \
Class* cls::Kind = &classKind##cls;
// Entity derived class registration (automatically sets BaseClass = Entity::Kind)
#define DEFINE_ENTITY_VERSIONED(entity, version, serialize_fct) \
Class entityKind##entity = \
MakeClass(ConstString(#entity), (uint8)Entity_Type::entity, (version), sizeof(entity), alignof(entity), serialize_fct); \
MakeClass(ConstString(#entity), (uint8)Entity_Type::entity, (version), Entity::Kind, sizeof(entity), \
alignof(entity), (serialize_fct)); \
Class* entity::Kind = &entityKind##entity;
```
### 6.3 Execution Flow in `SerializeEntity`
#### Universal Class Instance Serializer
Because every type is a `Class`, serializing **any** struct in the engine is unified into a single function:
```cpp
void SerializeClassInstance(archive& ar, Class* cls, void* instance)
{
Assert(cls != nullptr);
Assert(instance != nullptr);
uint16 version = cls->Version;
if (ar.IsSaving())
{
// If this is a derived entity class, write as class_version; otherwise write universal version
if (cls->BaseClass != nullptr)
{
SerializeProp(ar, "class_version", "class_version"_crc32, version);
}
else
{
SerializeProp(ar, "version", "version"_crc32, version);
}
}
else
{
if (cls->BaseClass != nullptr)
{
(void)SerializeProp(ar, "class_version", "class_version"_crc32, version);
}
else
{
(void)SerializeProp(ar, "version", "version"_crc32, version);
}
}
if (cls->serialize_fct != nullptr)
{
cls->serialize_fct(ar, instance, version);
}
}
```
#### Hierarchical Type Queries via `BaseClass`
```cpp
[[nodiscard]] inline bool IsA(const Class* queryClass, const Class* targetClass)
{
const Class* current = queryClass;
while (current != nullptr)
{
if (current == targetClass)
{
return true;
}
current = current->BaseClass;
}
return false;
}
```
### 6.3 Entity Serialization with Generalized Classes
An entity instance is cleanly composed of its base `Entity` class and its derived `DerivedKind` class:
```cpp
void Serialize(archive& ar, NonNullPtr<Entity> entity)
{
Assert(entity.Get() != nullptr);
// --- Tier 1: Base Entity Serialization ---
if (ar.IsSaving())
// 1. Serialize Base Entity using Entity's own Class descriptor (Entity::Kind)
SerializeClassInstance(ar, Entity::Kind, entity.Get());
// 2. Serialize Derived Entity using its Class descriptor (entity->DerivedKind)
if (entity->DerivedKind != nullptr && entity->Derived != nullptr)
{
uint32 baseVer = kEntityBaseVersion;
SERIALIZE_PROP(ar, baseVer);
SERIALIZE_PROP(ar, entity->ID);
String kindStr = WrapString(kEntity_type_names[entity->Kind->kind]);
SerializeProp(ar, "Kind", "Kind"_crc32, kindStr);
SerializeProp(ar, "Position", "Position"_crc32, entity->X, entity->Y, entity->Z);
}
else
{
uint32 baseVer = 0;
if (!SerializeProp(ar, "BaseVersion", "BaseVersion"_crc32, baseVer))
{
baseVer = 1; // Default to initial schema if absent
}
ar.BaseVersion = baseVer;
SERIALIZE_PROP(ar, entity->ID);
String kindStr = {};
if (SerializeProp(ar, "Kind", "Kind"_crc32, kindStr))
{
// Resolve class pointer from name
for (uint8 i = 0; i < ToUnderlying(Entity_Type::Count); ++i)
{
if (StringCompare(kindStr, WrapString(kEntity_type_names[i])) == 0)
{
entity->Kind = kEntity_type_class_ptr[i];
break;
}
}
}
Assert(entity->Kind != nullptr);
SerializeProp(ar, "Position", "Position"_crc32, entity->X, entity->Y, entity->Z);
}
// --- Tier 2: Derived Entity Serialization ---
if (entity->Kind->serialize_fct != nullptr && entity->Derived != nullptr)
{
if (ar.IsSaving())
{
uint32 classVer = entity->Kind->Version;
SerializeProp(ar, "ClassVersion", "ClassVersion"_crc32, classVer);
}
else
{
uint32 classVer = 0;
if (!SerializeProp(ar, "ClassVersion", "ClassVersion"_crc32, classVer))
{
classVer = entity->Kind->Version;
}
ar.ClassVersion = static_cast<uint16>(classVer);
}
entity->Kind->serialize_fct(&ar, entity->Derived);
SerializeClassInstance(ar, entity->DerivedKind, entity->Derived);
}
}
```
@@ -874,96 +894,19 @@ Over the lifecycle of a game, gameplay mechanics evolve:
Retaining deprecated members in active C++ structs creates code clutter, wastes memory, and invites bugs.
### 7.2 Deprecation Primitives (`ReadDeprecated*`)
Deprecation primitives allow serializers to ingest obsolete properties exclusively during loading without polluting modern structs or writing deprecated keys back to disk during saving.
### 7.2 Stack-Allocated Migration via Standard Serialization
Rather than maintaining dedicated deprecation primitives or polluting C++ structs with obsolete members, deprecated properties are migrated using the standard `serialize` / `SERIALIZE` function with temporary local variables allocated on the stack.
```cpp
bool ReadDeprecated(archive& ar, const char* keyName, uint32 keyCRC, float& outVal)
{
Assert(keyName != nullptr);
if (ar.IsSaving())
{
return false; // Deprecated fields are never saved
}
auto* node = FindProperty(ar.Properties, ar.PropertyCount, keyCRC);
if (node == nullptr)
{
return false;
}
char buffer[64];
size_t copySize = Min(node->Value.Size, sizeof(buffer) - 1);
MemCopy(buffer, node->Value.Str, copySize);
buffer[copySize] = '\0';
char* endPtr = nullptr;
float parsed = strtof(buffer, &endPtr);
if (endPtr != buffer)
{
outVal = parsed;
return true;
}
return false;
}
bool ReadDeprecatedVec2(archive& ar, const char* keyName, uint32 keyCRC, float& outX, float& outY)
{
Assert(keyName != nullptr);
if (ar.IsSaving())
{
return false;
}
auto* node = FindProperty(ar.Properties, ar.PropertyCount, keyCRC);
if (node == nullptr)
{
return false;
}
char buffer[128];
size_t copySize = Min(node->Value.Size, sizeof(buffer) - 1);
MemCopy(buffer, node->Value.Str, copySize);
buffer[copySize] = '\0';
float x = 0.0f;
float y = 0.0f;
if (sscanf_s(buffer, "%f %f", &x, &y) == 2)
{
outX = x;
outY = y;
return true;
}
return false;
}
bool ReadDeprecatedString(archive& ar, const char* keyName, uint32 keyCRC, NonNullPtr<Arena> arena, String& outVal)
{
Assert(keyName != nullptr);
if (ar.IsSaving())
{
return false;
}
auto* node = FindProperty(ar.Properties, ar.PropertyCount, keyCRC);
if (node == nullptr)
{
return false;
}
String parsed = node->Value;
if (parsed.Size >= 2 && parsed.Str[0] == '"' && parsed.Str[parsed.Size - 1] == '"')
{
parsed.Str++;
parsed.Size -= 2;
}
outVal = StringCopy(arena, parsed);
return true;
}
```
When an asset property is deprecated, restructured, or renamed:
1. The obsolete field is completely removed from the modern C++ struct definition.
2. In the entity/component serializer, an `if (ar.loading && ar.class_version < N)` block is added.
3. A local variable of the legacy type is declared on the stack.
4. The standard `SERIALIZE(ar, OldPropName, deprecated_val)` (or `serialize(ar, ConstString("OldPropName"), "OldPropName"_crc32, deprecated_val)`) is called. If the property exists in the loaded asset, it parses into `deprecated_val` and returns `true`.
5. The serializer performs whatever mapping or transformation is required into the modern struct fields.
6. Because the migration block is strictly guarded by `ar.loading`, it never executes during save operations (`ar.loading == false`). The serializer writes only modern struct fields, automatically purging obsolete keys on subsequent saves without requiring dedicated cleanup routines.
### 7.3 In-Place Migration Pattern
When an asset file with an older `ClassVersion` is loaded, the derived serializer detects `ar.ClassVersion < N`, calls `ReadDeprecated*` to read obsolete fields, maps the legacy data into the modern struct, and continues. On the subsequent save, the asset file is emitted using the modern schema without deprecated keys.
When an asset file with an older `class_version` is loaded, the serializer detects `ar.class_version < N`, reads obsolete fields into stack variables using standard serialization, maps the legacy data into the modern struct, and completes loading. On the subsequent save, the asset file is emitted using the modern schema without deprecated keys.
```cpp
struct Projectile
@@ -976,39 +919,40 @@ struct Projectile
float Damage = 50.0f;
};
void SerializeProjectile(archive* arPtr, void* payload)
void SerializeProjectile(Archive* arPtr, void* payload)
{
Assert(arPtr != nullptr);
Assert(payload != nullptr);
auto& ar = *arPtr;
auto* projectile = static_cast<Projectile*>(payload);
if (ar.IsSaving())
if (ar.loading)
{
SERIALIZE_PROP(ar, projectile->VelocityX);
SERIALIZE_PROP(ar, projectile->VelocityY);
SERIALIZE_PROP(ar, projectile->Damage);
}
else
{
SERIALIZE_PROP(ar, projectile->Damage);
SERIALIZE(ar, Damage, projectile->Damage);
if (ar.ClassVersion < 2)
if (ar.class_version < 2)
{
// Migration from v1: scalar 'Speed' converted to 'VelocityX'
float legacySpeed = 0.0f;
if (ReadDeprecated(ar, "Speed", "Speed"_crc32, legacySpeed))
// Migration from v1: scalar 'Speed' converted to 'VelocityX' on the stack
float deprecated_speed = 0.0f;
if (SERIALIZE(ar, Speed, deprecated_speed))
{
projectile->VelocityX = legacySpeed;
projectile->VelocityX = deprecated_speed;
projectile->VelocityY = 0.0f;
}
}
else
{
SERIALIZE_PROP(ar, projectile->VelocityX);
SERIALIZE_PROP(ar, projectile->VelocityY);
SERIALIZE(ar, VelocityX, projectile->VelocityX);
SERIALIZE(ar, VelocityY, projectile->VelocityY);
}
}
else
{
// Save modern schema
SERIALIZE(ar, VelocityX, projectile->VelocityX);
SERIALIZE(ar, VelocityY, projectile->VelocityY);
SERIALIZE(ar, Damage, projectile->Damage);
}
}
```
@@ -1034,10 +978,10 @@ void SerializeProjectile(archive* arPtr, void* payload)
|
v
+-------------------------------------------------------------------------+
| Phase 3: Archive Struct & SerializeProp Helpers |
| - Upgrade archive in serialization.h with ArchiveMode |
| - Implement primitive, vector, and string SerializeProp helpers |
| - Implement ReadDeprecated* primitives |
| Phase 3: Archive Struct & Serialize Helpers |
| - Upgrade Archive in serialization.h with loading / version state |
| - Implement primitive, vector, and string serialize helpers |
| - Support stack-allocated schema migration |
+-------------------------------------------------------------------------+
|
v
@@ -1068,12 +1012,11 @@ void SerializeProjectile(archive* arPtr, void* payload)
- Implement `TokenizeTextArchive(NonNullPtr<Arena> arena, ByteBuffer buffer)`.
- Implement `FindProperty` and `AuditUnconsumedProperties`.
#### Phase 3: Archive Struct & `SerializeProp` Helpers
#### Phase 3: Archive Struct & `serialize` Helpers
1. **Target**: `Juliet/include/Core/Common/serialization.h`
- Introduce `enum class ArchiveMode : uint8`.
- Upgrade `struct archive` with stream pointer, property array, mode, and versions.
- Implement overloaded `SerializeProp` for `float`, `int32`, `uint64`, `bool`, vectors, and `String`.
- Implement `ReadDeprecated`, `ReadDeprecatedVec2`, `ReadDeprecatedString`.
- Upgrade `struct Archive` with stream pointer, property array, `loading` flag, and versions (`base_version`, `class_version`).
- Implement overloaded `serialize`, `read_prop`, and `write` for `float`, `int32`, `uint64`, `bool`, vectors, and `String`.
- Support deprecation migration using standard `serialize` / `SERIALIZE` with local stack variables under `if (ar.loading && ar.class_version < N)`.
#### Phase 4: Entity & World Integration
1. **Target**: `Game/Entity/Entity.h` and `Game/Entity/Entity.cpp`
@@ -1240,35 +1183,35 @@ namespace UnitTest
float VelocityY = 0.0f;
};
void SerializeLegacyWeapon(archive* arPtr, void* payload)
void SerializeLegacyWeapon(Archive* arPtr, void* payload)
{
Assert(arPtr != nullptr);
Assert(payload != nullptr);
auto& ar = *arPtr;
auto* weapon = static_cast<LegacyWeapon*>(payload);
if (ar.IsSaving())
if (ar.loading)
{
SERIALIZE_PROP(ar, weapon->VelocityX);
SERIALIZE_PROP(ar, weapon->VelocityY);
}
else
{
if (ar.ClassVersion < 2)
if (ar.class_version < 2)
{
float oldSpeed = 0.0f;
if (ReadDeprecated(ar, "Speed", "Speed"_crc32, oldSpeed))
float deprecated_speed = 0.0f;
if (SERIALIZE(ar, Speed, deprecated_speed))
{
weapon->VelocityX = oldSpeed;
weapon->VelocityX = deprecated_speed;
weapon->VelocityY = 0.0f;
}
}
else
{
SERIALIZE_PROP(ar, weapon->VelocityX);
SERIALIZE_PROP(ar, weapon->VelocityY);
SERIALIZE(ar, VelocityX, weapon->VelocityX);
SERIALIZE(ar, VelocityY, weapon->VelocityY);
}
}
else
{
SERIALIZE(ar, VelocityX, weapon->VelocityX);
SERIALIZE(ar, VelocityY, weapon->VelocityY);
}
}
DEFINE_ENTITY_VERSIONED(LegacyWeapon, 2, SerializeLegacyWeapon);
@@ -1279,7 +1222,7 @@ namespace UnitTest
// Simulated v1 file containing obsolete 'Speed'
const char* v1Content =
"; ClassVersion\n"
"; class_version\n"
"1\n"
"; Speed\n"
"75.500000\n";
@@ -1289,14 +1232,13 @@ namespace UnitTest
.Size = strlen(v1Content)
};
ParsedTextArchive parsed = TokenizeTextArchive(temp.Arena, buffer);
ParsedArchive parsed = tokenize_archive(temp.Arena, buffer);
archive ar = {};
ar.ArenaInstance = temp.Arena;
ar.Mode = ArchiveMode::LoadingText;
ar.Properties = parsed.Nodes;
ar.PropertyCount = parsed.PropertyCount;
ar.ClassVersion = 1;
Archive ar = {};
ar.arena = temp.Arena;
ar.loading = true;
ar.base = parsed;
ar.class_version = 1;
LegacyWeapon weapon;
SerializeLegacyWeapon(&ar, &weapon);
@@ -205,36 +205,36 @@ The canonical allocation function is defined in `EntityManager.h`:
The implementation replaces the flawed `RegisterEntity` routine in [`Game/Entity/EntityManager.cpp`](file:///w:/Classified/Juliet/Game/Entity/EntityManager.cpp):
```cpp
[[nodiscard]] Entity* AllocateEntity(EntityManager& manager, Class* classPtr)
[[nodiscard]] Entity* AllocateEntity(EntityManager& manager, Class* derivedClassPtr)
{
Assert(classPtr != nullptr);
Assert(classPtr->kind < ENTITY(Count));
Assert(classPtr->size_of >= sizeof(entity_template));
Assert(classPtr->alignment > 0);
Assert(derivedClassPtr != nullptr);
Assert(derivedClassPtr->kind < ENTITY(Count));
Assert(derivedClassPtr->size_of >= sizeof(entity_template));
Assert(derivedClassPtr->alignment > 0);
// 1. Allocate uninitialized Base Entity in the contiguous VectorArena
Entity baseTemplate{};
baseTemplate.ID = EntityManager::ID++;
baseTemplate.Kind = classPtr;
baseTemplate.Derived = nullptr;
baseTemplate.X = 0.0f;
baseTemplate.Y = 0.0f;
baseTemplate.Z = 0.0f;
baseTemplate.IsDirty = true;
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;
manager.Entities.PushBack(baseTemplate);
Entity* basePtr = manager.Entities.Back();
Assert(basePtr != nullptr);
// 2. Allocate zeroed derived component memory in the typed arena
typed_entity_array& typedArray = manager.by_type[classPtr->kind];
typed_entity_array& typedArray = manager.by_type[derivedClassPtr->kind];
Assert(typedArray.arena != nullptr);
void* rawMemory = ArenaPushSize(
typedArray.arena,
classPtr->size_of,
classPtr->alignment,
true JULIET_DEBUG_PARAM(kEntity_type_names[classPtr->kind]));
derivedClassPtr->size_of,
derivedClassPtr->alignment,
true JULIET_DEBUG_PARAM(kEntity_type_names[derivedClassPtr->kind]));
Assert(rawMemory != nullptr);
auto* derivedTemplate = reinterpret_cast<entity_template*>(rawMemory);
@@ -330,9 +330,9 @@ entity_instance
0x0100000000000042
; class
Inert
; base_version
; version
1
; derived_version
; class_version
1
; position
0.43 0.32 1.56
@@ -342,7 +342,8 @@ 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.
- **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`, `; base_version`, `; derived_version`, `; position`) are standard text Key-Value nodes read by the exact same `archive` parser.
- **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.
### 4.2 Eliminating Intermediate Stack Allocations
Under the new pipeline:
@@ -351,7 +352,7 @@ Under the new pipeline:
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, derivedVersion)` is called. Derived fields stream **directly into the typed arena** 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
To ensure fast and safe type lookup during file deserialization:
@@ -440,18 +441,13 @@ To ensure fast and safe type lookup during file deserialization:
Entity* basePtr = AllocateEntity(manager, classPtr);
Assert(basePtr != nullptr);
// 3. Read base versions and properties in-place
uint16 baseVersion = 1;
uint16 derivedVersion = 1;
SerializeProp(ar, "base_version", baseVersion);
SerializeProp(ar, "derived_version", derivedVersion);
// 3. Serialize Base Entity in-place using Entity::Kind
SerializeClassInstance(ar, Entity::Kind, basePtr);
SerializeEntityBase(ar, *basePtr, baseVersion);
// 4. Stream derived properties in-place directly into the typed arena
if (classPtr->serialize_fct != nullptr)
// 4. Stream derived properties in-place using basePtr->DerivedKind
if (basePtr->DerivedKind != nullptr && basePtr->Derived != nullptr)
{
classPtr->serialize_fct(ar, basePtr->Derived, derivedVersion);
SerializeClassInstance(ar, basePtr->DerivedKind, basePtr->Derived);
}
// Freshly loaded entity matches disk state exactly
@@ -663,13 +659,15 @@ To solve this, `Entity` in [`Game/Entity/Entity.h`](file:///w:/Classified/Juliet
```cpp
struct Entity final
{
EntityID ID = 0;
Class* Kind = nullptr;
DerivedType Derived = nullptr;
float X = 0.0f;
float Y = 0.0f;
float Z = 0.0f;
bool IsDirty = false;
DECLARE_ENTITY() // 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;
};
```
+28 -15
View File
@@ -353,23 +353,36 @@ Assets/Worlds/<WorldName>/
└── ...
```
### 4.2 `WorldSettings.jasset` Binary Layout
Global world settings are stored in `WorldSettings.jasset`.
### 4.2 `WorldSettings.jasset` Format Specification
Global world settings are stored in `WorldSettings.jasset` in the standard `.jasset` text format with universal `; version`:
```ini
; asset_type
world_settings
; version
1
; sun_direction
0.577 -0.577 -0.577
; sun_color
1.0 0.95 0.8
; sun_intensity
1.0
; ambient_color
0.2 0.25 0.35
; ambient_intensity
0.15
; gravity
0.0 0.0 -9.81
; kill_plane_z
-50.0
```
In memory, these map to standard C-style POD data:
```cpp
#pragma pack(push, 1)
struct WorldSettingsFileHeader
{
uint32 Magic = 0x5453574A; // 'JWST' (Juliet World SeTtings) in little-endian
uint32 Version = 1;
};
#pragma pack(pop)
struct WorldEnvironmentSettings
{
// Directional Sun & Ambient Lighting
Vector3 SunDirection = { 0.577f, -0.577f, -0.577f };
float _Pad0 = 0.0f;
Vector3 SunColor = { 1.0f, 0.95f, 0.8f };
float SunIntensity = 1.0f;
Vector3 AmbientColor = { 0.2f, 0.25f, 0.35f };
@@ -391,9 +404,9 @@ entity_instance
0x0100000000000042
; class
Inert
; base_version
; version
1
; derived_version
; class_version
1
; position
0.43 0.32 1.56
@@ -408,8 +421,8 @@ Inert
- `; class`: The runtime `Class` name (e.g. `Inert`, `Door`).
- `; template`: Optional relative path to archetype template (e.g. `Assets/Templates/Door_Wood.jasset`).
2. **Version Directives**:
- `; base_version`: Engine-wide base entity version (`kEntityBaseVersion`).
- `; derived_version`: Class-specific gameplay version (`Class::Version`).
- `; version`: Universal asset version (`kEntityBaseVersion` for entities, or asset schema version for non-entity files like `WorldSettings.jasset`).
- `; class_version`: Optional class-specific gameplay version (`Class::Version`), defaults to 1 if omitted. Non-entity assets do not use this.
3. **Base Entity Properties**:
- Position (`position\n0.43 0.32 1.56`), Rotation, Scale.
4. **Derived Entity Properties**:
+1 -1
View File
@@ -21,7 +21,7 @@
// namespace
// {
// void serialize_test(archive* ar, void* payload)
// void serialize_test(Archive* ar, void* payload)
// {
// SerializedEntityTest* test = reinterpret_cast<SerializedEntityTest*>(payload);
// serialize_elem(ar, test->A);