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
+12 -4
View File
@@ -7,10 +7,18 @@ use static_cast or reinterpret_cast but not parenthesis for casting.
No exceptions No exceptions
Use [[nodiscard]] when risk of memory leak (anything returning pointer) Use [[nodiscard]] when risk of memory leak (anything returning pointer)
auto is allowed but when its a pointer add the * and when reference adds the & auto is allowed but when its a pointer add the * and when reference adds the &
Member variable are CamelCase
Types are CamelCase
Functions are CamelCase.
Add Assert to make sure all assumptions are good. Parameters of functions for example should be verified with Assert. Add Assert to make sure all assumptions are good. Parameters of functions for example should be verified with Assert.
Code should be self commented using proper variable names, types and functions. No need to add comments most of the time, unless the algorithm is very complex and hard to read. Code should be self commented using proper variable names, types and functions. No need to add comments most of the time, unless the algorithm is very complex and hard to read.
When creating a new system framework, make a unit test. To make the unit test we should not modify the framework code for special unit test case. When creating a new system framework, make a unit test. To make the unit test we should not modify the framework code for special unit test case.
Always put braces for if,else,for,while etc. Always put braces for if,else,for,while etc.
Coding Style:
Note: The code base do not fully use those, if in doubt, use this style and not legacy style.
Types (Structs/Enums/Unions) : PascalCase. Exemple : Arena, String8, DbgTarget, EvalValue
Primitive Types : lower_snake_case. Exemple: uint8, uint16, uint32, uint64, size_t, index_t, int32...
Function Names: lower_snake_case. Exemple: str16_from_8.
Namespace: do not use c++ namespace, add PascalCase prefix to function names. Exemple: W32_do_something
Local Variables and parameters: lower_snake_case. Exemple: arena, str, data.
Struct members: lower_snake_case. Exemple: data, node, next.
global/static variables: lower_snake_case prefixed. Exemple: g_my_global.
const variables: lower_snake_case prefixed. Exemple: k_my_const_var.
Defines and Macros: ALL_CAPS_SNAKE. Exemple: MY_MACRO
-2
View File
@@ -1,6 +1,4 @@
#pragma once #pragma once
#include <Core/Common/CoreTypes.h>
constexpr index_t kPlayCamera = 0; constexpr index_t kPlayCamera = 0;
constexpr index_t kDebugCamera = 1; constexpr index_t kDebugCamera = 1;
+35 -31
View File
@@ -24,14 +24,14 @@ void ShutdownWorld(NonNullPtr<World> world)
world->WorldArena = nullptr; world->WorldArena = nullptr;
} }
void AddToWorld(NonNullPtr<World> world, NonNullPtr<Entity> entity) void AddToWorld(NonNullPtr<World> /*world*/, NonNullPtr<Entity> /*entity*/)
{ {
// RegisterEntity(*world->EntityManager, entity.Get()); // 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)); Assert(IsValid(filename));
@@ -41,28 +41,33 @@ void Serialize(archive& ar, World& world, String filename)
{ {
// Load // Load
ByteBuffer fileBuffer = LoadFile(ar.arena, filename); ByteBuffer fileBuffer = LoadFile(ar.arena, filename);
if (fileBuffer.Size >= sizeof(WorldFileHeader))
{
ar.base_ptr = fileBuffer.Data;
WorldFileHeader header; // TEST SERIALIZATION
serialize_elem(ar, header); ParsedArchive archive = tokenize_archive(ar.arena, fileBuffer);
Assert(header.Magic == kWorldMagic); // ArchivePropertyNode* property = find_property(&archive, "Position"_crc32);
Assert(header.Version == kWorldVersion); audit_unconsumed_properties(&archive, ConstString("World"));
for (typed_entity_array& type : entityManager.by_type) // if (fileBuffer.Size >= sizeof(WorldFileHeader))
{ // {
serialize_elem(ar, type.count); // ar.base_ptr = fileBuffer.Data;
//
if (type.count > 0) // WorldFileHeader header;
{ // serialize_elem(ar, header);
// Assert(header.Magic == kWorldMagic);
// Unserialize the base entity to get informations //
Entity entity; // for (typed_entity_array& type : entityManager.by_type)
serialize(ar, &entity); // {
} // serialize_elem(ar, type.count);
} //
} // if (type.count > 0)
// {
//
// // Unserialize the base entity to get informations
// Entity entity;
// // serialize(ar, &entity);
// }
// }
// }
} }
else else
{ {
@@ -72,9 +77,8 @@ void Serialize(archive& ar, World& world, String filename)
index_t beginPos = ArenaPos(ar.arena); index_t beginPos = ArenaPos(ar.arena);
// Headers // Headers
auto* header = ArenaPushStruct<WorldFileHeader>(ar.arena); auto* header = ArenaPushStruct<WorldFileHeader>(ar.arena);
header->Magic = kWorldMagic; header->Magic = kWorldMagic;
header->Version = kWorldVersion;
ar.base_ptr = header; ar.base_ptr = header;
@@ -84,12 +88,12 @@ void Serialize(archive& ar, World& world, String filename)
serialize_elem(ar, type.count); serialize_elem(ar, type.count);
auto* element = type.array; auto* element = type.array;
uint8* rawElement = reinterpret_cast<uint8*>(element); 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) for (index_t idx = 0; idx < type.count; ++idx)
{ {
// Todo : utils // Todo : utils
// Get base entity from type // 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); serialize(ar, entity);
} }
} }
@@ -164,7 +168,7 @@ void Serialize(archive& ar, World& world, String filename)
// return true; // return true;
} }
[[nodiscard]] bool LoadWorld(World& world, String filename) [[nodiscard]] bool LoadWorld(World& /*world*/, String filename)
{ {
Assert(IsValid(filename)); Assert(IsValid(filename));
// Assert(world.WorldArena != nullptr); // Assert(world.WorldArena != nullptr);
@@ -236,13 +240,13 @@ void RenderWorldEditorUI(World& world)
if (ImGui::Button("Save World")) if (ImGui::Button("Save World"))
{ {
archive data{ .arena = temp.Arena, .loading = false }; Archive data = { .arena = temp.Arena, .loading = false };
Serialize(data, world, path); Serialize(data, world, path);
} }
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button("Load World")) if (ImGui::Button("Load World"))
{ {
archive data{ .arena = temp.Arena, .loading = true }; Archive data{ .arena = temp.Arena, .loading = true };
Serialize(data, world, path); Serialize(data, world, path);
} }
+3 -5
View File
@@ -1,20 +1,18 @@
#pragma once #pragma once
#include <Core/Common/CoreTypes.h>
#include <Core/Common/NonNullPtr.h> #include <Core/Common/NonNullPtr.h>
#include <Core/Common/String.h> #include <Core/Common/String.h>
#include <Core/Container/Vector.h> #include <Core/Container/Vector.h>
#include <Core/Memory/MemoryArena.h> #include <Core/Memory/MemoryArena.h>
#include <Entity/Entity.h> #include <Entity/Entity.h>
struct archive; struct Archive;
struct EntityManager; struct EntityManager;
#pragma pack(push, 1) #pragma pack(push, 1)
struct WorldFileHeader struct WorldFileHeader
{ {
uint32 Magic = 0x444C574A; // 'JWLD' in little-endian uint32 Magic = 0x444C574A; // 'JWLD' in little-endian
uint32 Version = 1;
}; };
#pragma pack(pop) #pragma pack(pop)
@@ -33,7 +31,7 @@ void ShutdownWorld(NonNullPtr<World> world);
void AddToWorld(NonNullPtr<World> world, NonNullPtr<Entity> entity); void AddToWorld(NonNullPtr<World> world, NonNullPtr<Entity> entity);
void RemoveWorldEntity(World& world, size_t index); 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 #if JULIET_EDITOR
void RenderWorldEditorUI(World& world); void RenderWorldEditorUI(World& world);
+13 -16
View File
@@ -2,24 +2,21 @@
#include <Core/Common/serialization.h> #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(ar, id, entity->ID);
{ SERIALIZE(ar, position, entity->position);
serialize_elem(ar, entity->Kind->kind);
}
else
{
uint8 kind;
serialize_elem(ar, kind);
entity->Kind = kEntity_type_class_ptr[kind];
}
serialize_elem(ar, entity->X); // Derived fields
serialize_elem(ar, entity->Y); if (entity->derived_kind != nullptr && entity->derived != nullptr)
serialize_elem(ar, entity->Z); {
serialize(ar, entity->derived_kind, entity->derived);
}
} }
+24 -28
View File
@@ -2,24 +2,19 @@
#include <Core/Common/CoreUtils.h> #include <Core/Common/CoreUtils.h>
#include <Core/Common/EnumUtils.h> #include <Core/Common/EnumUtils.h>
#include <Core/Math/Vector.h>
#include <Core/Memory/Allocator.h> #include <Core/Memory/Allocator.h>
#include <Core/Memory/MemoryArena.h> #include <Core/Memory/MemoryArena.h>
#include <Engine/Class.h> #include <Engine/Class.h>
#define DECLARE_ENTITY() \ #define DECLARE_ENTITY() \
Entity* Base; \ Entity* base; \
static Class* Kind; static Class* kind;
// Will register the class globally at launch #define DEFINE_ENTITY_VERSIONED(entity, version, serialize_fct) \
#define DEFINE_ENTITY(entity) \ constexpr Class entityKind##entity = MakeClass(ConstString(#entity), (uint8)Entity_Type::entity, (version), \
Class entityKind##entity = \ &classKindEntity, sizeof(entity), alignof(entity), (serialize_fct)); \
MakeClass(ConstString(#entity), (uint8)Entity_Type::entity, sizeof(entity), alignof(entity), nullptr); \ Class* entity::kind = const_cast<Class*>(&entityKind##entity);
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;
struct EntityManager; struct EntityManager;
using DerivedType = void*; using DerivedType = void*;
@@ -27,12 +22,12 @@ using EntityID = uint64_t;
struct Entity final struct Entity final
{ {
EntityID ID = 0; static Class* kind;
Class* Kind = nullptr;
DerivedType Derived = nullptr; EntityID ID = 0;
float X = 0.0f; Class* derived_kind = nullptr;
float Y = 0.0f; DerivedType derived = nullptr;
float Z = 0.0f; Vector4 position = {};
}; };
struct Inert struct Inert
@@ -60,7 +55,7 @@ inline const char* kEntity_type_names[] = {
}; };
#undef AS_STR #undef AS_STR
#define AS_CLASS(name) name::Kind #define AS_CLASS(name) name::kind
inline Class* kEntity_type_class_ptr[] inline Class* kEntity_type_class_ptr[]
{ {
ENTITY_TYPE_LIST(AS_CLASS) ENTITY_TYPE_LIST(AS_CLASS)
@@ -80,8 +75,8 @@ struct entity_template
// //
template <typename EntityType> template <typename EntityType>
concept EntityConcept = requires(EntityType entity) { concept EntityConcept = requires(EntityType entity) {
{ EntityType::Kind } -> std::convertible_to<const Class*>; { EntityType::kind } -> std::convertible_to<const Class*>;
requires std::same_as<decltype(entity.Base), Entity*>; requires std::same_as<decltype(entity.base), Entity*>;
}; };
template <typename EntityType> template <typename EntityType>
@@ -89,7 +84,7 @@ template <typename EntityType>
[[nodiscard]] bool IsA(const Entity* entity) [[nodiscard]] bool IsA(const Entity* entity)
{ {
Assert(entity != nullptr); Assert(entity != nullptr);
return entity->Kind == EntityType::Kind; return entity->derived_kind == EntityType::kind;
} }
template <typename EntityType> template <typename EntityType>
@@ -98,10 +93,11 @@ template <typename EntityType>
{ {
EntityType result; EntityType result;
Entity base; Entity base;
base.X = x; base.position.x = x;
base.Y = y; base.position.y = y;
base.Z = z; base.position.z = z;
base.Kind = EntityType::Kind; base.position.w = 1.0f;
base.derived_kind = EntityType::kind;
return (EntityType*)RegisterEntity(manager, &base, &result); return (EntityType*)RegisterEntity(manager, &base, &result);
} }
@@ -112,7 +108,7 @@ template <typename EntityType>
{ {
Assert(entity != nullptr); Assert(entity != nullptr);
Assert(IsA<EntityType>(entity)); 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) entity_template* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity)
{ {
base->ID = EntityManager::ID++; base->ID = EntityManager::ID++;
base->Derived = entity; base->derived = entity;
manager.Entities.PushBack(*base); manager.Entities.PushBack(*base);
auto* ptr = (entity_template*)ArenaPushSize(manager.by_type[base->Kind->kind].arena, base->Kind->size_of, base->Kind->alignment, auto* ptr = (entity_template*)ArenaPushSize(manager.by_type[base->derived_kind->kind].arena,
false JULIET_DEBUG_PARAM(kEntity_type_names[base->Kind->kind])); base->derived_kind->size_of, base->derived_kind->alignment,
MemCopy(ptr, entity, base->Kind->size_of); false JULIET_DEBUG_PARAM(kEntity_type_names[base->derived_kind->kind]));
manager.by_type[base->Kind->kind].count += 1; 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; return ptr;
} }
void RegisterBaseEntity(EntityManager& manager, Entity&& base)
{
GetEntityManager().Entities.PushBack(std::move(base));
}
void UpdateEntityManager(EntityManager& manager) void UpdateEntityManager(EntityManager& manager)
{ {
// Todo : inert by definition dont move, but this is for test // 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; Inert* inert = reinterpret_cast<Inert*>(by_type.array) + i;
if (inert->MeshInstance != indexMax) 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 #pragma once
#include <Core/Common/CoreTypes.h>
#include <Core/Container/Vector.h> #include <Core/Container/Vector.h>
#include <Entity/Entity.h> #include <Entity/Entity.h>
@@ -28,5 +27,4 @@ void InitEntityManager(NonNullPtr<World> world);
void ShutdownEntityManager(); void ShutdownEntityManager();
EntityManager& GetEntityManager(); EntityManager& GetEntityManager();
entity_template* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity); entity_template* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity);
void RegisterBaseEntity(EntityManager& manager, Entity&& base);
void UpdateEntityManager(EntityManager& manager); 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. - **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. - **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. - **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`. - **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 ; AssetType
Entity Entity
; BaseVersion ; Version
1 1
; EntityID ; EntityID
@@ -712,10 +712,15 @@ In entity-component systems or object-oriented engine hierarchies, entities cons
#### The Fragility of Monolithic Versioning #### 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. 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 #### The Two-Tier Solution: Universal `; Version` + Optional `; ClassVersion`
Juliet decouples versioning into two independent tiers: To keep `.jasset` files clean and unified across all asset types:
- **Tier 1: Base Engine Version (`kEntityBaseVersion`)**: Declared centrally in `Entity.h`. Governs `Entity` base fields. - **All `.jasset` files declare a universal `; Version` tag**:
- **Tier 2: Derived Class Version (`Class::Version`)**: Declared per-entity class in `Class.h` and initialized in `DEFINE_ENTITY_VERSIONED`. - 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 ; AssetType
Entity Entity
; BaseVersion ------> Governed by kEntityBaseVersion in Entity.h ; Version ------> Universal asset version (kEntityBaseVersion for Entity)
1 1
; EntityID ; EntityID
0x0000000000000001 0x0000000000000001
@@ -732,32 +737,30 @@ Juliet decouples versioning into two independent tiers:
; Position ; Position
0.0 0.0 0.0 0.0 0.0 0.0
------------------------------------------------------------------------ ------------------------------------------------------------------------
; ClassVersion ------> Governed by Class::Version in Class.h ; ClassVersion ------> Optional derived version (Class::Version in Class.h)
2 2
; MeshInstance ; MeshInstance
42 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 ```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 struct Class
{ {
uint32 CRC; uint32 CRC;
uint8 kind; uint8 kind;
uint16 Version; // Added derived class version uint16 Version; // Struct schema version
serialize_fct_type serialize_fct; 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 size_of;
size_t alignment; size_t alignment;
@@ -766,12 +769,14 @@ struct Class
#endif #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 = {}; Class cls = {};
cls.CRC = crc32(name.Str, name.Size); cls.CRC = crc32(name.Str, name.Size);
cls.kind = kind; cls.kind = kind;
cls.Version = version; cls.Version = version;
cls.BaseClass = baseClass;
cls.size_of = size; cls.size_of = size;
cls.alignment = align; cls.alignment = align;
cls.serialize_fct = fct; 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 ```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) \ #define DEFINE_ENTITY_VERSIONED(entity, version, serialize_fct) \
Class entityKind##entity = \ 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; 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 ```cpp
void Serialize(archive& ar, NonNullPtr<Entity> entity) void Serialize(archive& ar, NonNullPtr<Entity> entity)
{ {
Assert(entity.Get() != nullptr); Assert(entity.Get() != nullptr);
// --- Tier 1: Base Entity Serialization --- // 1. Serialize Base Entity using Entity's own Class descriptor (Entity::Kind)
if (ar.IsSaving()) 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; SerializeClassInstance(ar, entity->DerivedKind, entity->Derived);
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);
} }
} }
``` ```
@@ -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. Retaining deprecated members in active C++ structs creates code clutter, wastes memory, and invites bugs.
### 7.2 Deprecation Primitives (`ReadDeprecated*`) ### 7.2 Stack-Allocated Migration via Standard Serialization
Deprecation primitives allow serializers to ingest obsolete properties exclusively during loading without polluting modern structs or writing deprecated keys back to disk during saving. 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 When an asset property is deprecated, restructured, or renamed:
bool ReadDeprecated(archive& ar, const char* keyName, uint32 keyCRC, float& outVal) 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.
Assert(keyName != nullptr); 3. A local variable of the legacy type is declared on the stack.
if (ar.IsSaving()) 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.
return false; // Deprecated fields are never saved 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.
}
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;
}
```
### 7.3 In-Place Migration Pattern ### 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 ```cpp
struct Projectile struct Projectile
@@ -976,39 +919,40 @@ struct Projectile
float Damage = 50.0f; float Damage = 50.0f;
}; };
void SerializeProjectile(archive* arPtr, void* payload) void SerializeProjectile(Archive* arPtr, void* payload)
{ {
Assert(arPtr != nullptr); Assert(arPtr != nullptr);
Assert(payload != nullptr); Assert(payload != nullptr);
auto& ar = *arPtr; auto& ar = *arPtr;
auto* projectile = static_cast<Projectile*>(payload); auto* projectile = static_cast<Projectile*>(payload);
if (ar.IsSaving()) if (ar.loading)
{ {
SERIALIZE_PROP(ar, projectile->VelocityX); SERIALIZE(ar, Damage, projectile->Damage);
SERIALIZE_PROP(ar, projectile->VelocityY);
SERIALIZE_PROP(ar, projectile->Damage);
}
else
{
SERIALIZE_PROP(ar, projectile->Damage);
if (ar.ClassVersion < 2) if (ar.class_version < 2)
{ {
// Migration from v1: scalar 'Speed' converted to 'VelocityX' // Migration from v1: scalar 'Speed' converted to 'VelocityX' on the stack
float legacySpeed = 0.0f; float deprecated_speed = 0.0f;
if (ReadDeprecated(ar, "Speed", "Speed"_crc32, legacySpeed)) if (SERIALIZE(ar, Speed, deprecated_speed))
{ {
projectile->VelocityX = legacySpeed; projectile->VelocityX = deprecated_speed;
projectile->VelocityY = 0.0f; projectile->VelocityY = 0.0f;
} }
} }
else else
{ {
SERIALIZE_PROP(ar, projectile->VelocityX); SERIALIZE(ar, VelocityX, projectile->VelocityX);
SERIALIZE_PROP(ar, projectile->VelocityY); 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 v
+-------------------------------------------------------------------------+ +-------------------------------------------------------------------------+
| Phase 3: Archive Struct & SerializeProp Helpers | | Phase 3: Archive Struct & Serialize Helpers |
| - Upgrade archive in serialization.h with ArchiveMode | | - Upgrade Archive in serialization.h with loading / version state |
| - Implement primitive, vector, and string SerializeProp helpers | | - Implement primitive, vector, and string serialize helpers |
| - Implement ReadDeprecated* primitives | | - Support stack-allocated schema migration |
+-------------------------------------------------------------------------+ +-------------------------------------------------------------------------+
| |
v v
@@ -1068,12 +1012,11 @@ void SerializeProjectile(archive* arPtr, void* payload)
- Implement `TokenizeTextArchive(NonNullPtr<Arena> arena, ByteBuffer buffer)`. - Implement `TokenizeTextArchive(NonNullPtr<Arena> arena, ByteBuffer buffer)`.
- Implement `FindProperty` and `AuditUnconsumedProperties`. - Implement `FindProperty` and `AuditUnconsumedProperties`.
#### Phase 3: Archive Struct & `SerializeProp` Helpers #### Phase 3: Archive Struct & `serialize` Helpers
1. **Target**: `Juliet/include/Core/Common/serialization.h` 1. **Target**: `Juliet/include/Core/Common/serialization.h`
- Introduce `enum class ArchiveMode : uint8`. - Upgrade `struct Archive` with stream pointer, property array, `loading` flag, and versions (`base_version`, `class_version`).
- Upgrade `struct archive` with stream pointer, property array, mode, and versions. - Implement overloaded `serialize`, `read_prop`, and `write` for `float`, `int32`, `uint64`, `bool`, vectors, and `String`.
- Implement overloaded `SerializeProp` 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)`.
- Implement `ReadDeprecated`, `ReadDeprecatedVec2`, `ReadDeprecatedString`.
#### Phase 4: Entity & World Integration #### Phase 4: Entity & World Integration
1. **Target**: `Game/Entity/Entity.h` and `Game/Entity/Entity.cpp` 1. **Target**: `Game/Entity/Entity.h` and `Game/Entity/Entity.cpp`
@@ -1240,35 +1183,35 @@ namespace UnitTest
float VelocityY = 0.0f; float VelocityY = 0.0f;
}; };
void SerializeLegacyWeapon(archive* arPtr, void* payload) void SerializeLegacyWeapon(Archive* arPtr, void* payload)
{ {
Assert(arPtr != nullptr); Assert(arPtr != nullptr);
Assert(payload != nullptr); Assert(payload != nullptr);
auto& ar = *arPtr; auto& ar = *arPtr;
auto* weapon = static_cast<LegacyWeapon*>(payload); auto* weapon = static_cast<LegacyWeapon*>(payload);
if (ar.IsSaving()) if (ar.loading)
{ {
SERIALIZE_PROP(ar, weapon->VelocityX); if (ar.class_version < 2)
SERIALIZE_PROP(ar, weapon->VelocityY);
}
else
{
if (ar.ClassVersion < 2)
{ {
float oldSpeed = 0.0f; float deprecated_speed = 0.0f;
if (ReadDeprecated(ar, "Speed", "Speed"_crc32, oldSpeed)) if (SERIALIZE(ar, Speed, deprecated_speed))
{ {
weapon->VelocityX = oldSpeed; weapon->VelocityX = deprecated_speed;
weapon->VelocityY = 0.0f; weapon->VelocityY = 0.0f;
} }
} }
else else
{ {
SERIALIZE_PROP(ar, weapon->VelocityX); SERIALIZE(ar, VelocityX, weapon->VelocityX);
SERIALIZE_PROP(ar, weapon->VelocityY); SERIALIZE(ar, VelocityY, weapon->VelocityY);
} }
} }
else
{
SERIALIZE(ar, VelocityX, weapon->VelocityX);
SERIALIZE(ar, VelocityY, weapon->VelocityY);
}
} }
DEFINE_ENTITY_VERSIONED(LegacyWeapon, 2, SerializeLegacyWeapon); DEFINE_ENTITY_VERSIONED(LegacyWeapon, 2, SerializeLegacyWeapon);
@@ -1279,7 +1222,7 @@ namespace UnitTest
// Simulated v1 file containing obsolete 'Speed' // Simulated v1 file containing obsolete 'Speed'
const char* v1Content = const char* v1Content =
"; ClassVersion\n" "; class_version\n"
"1\n" "1\n"
"; Speed\n" "; Speed\n"
"75.500000\n"; "75.500000\n";
@@ -1289,14 +1232,13 @@ namespace UnitTest
.Size = strlen(v1Content) .Size = strlen(v1Content)
}; };
ParsedTextArchive parsed = TokenizeTextArchive(temp.Arena, buffer); ParsedArchive parsed = tokenize_archive(temp.Arena, buffer);
archive ar = {}; Archive ar = {};
ar.ArenaInstance = temp.Arena; ar.arena = temp.Arena;
ar.Mode = ArchiveMode::LoadingText; ar.loading = true;
ar.Properties = parsed.Nodes; ar.base = parsed;
ar.PropertyCount = parsed.PropertyCount; ar.class_version = 1;
ar.ClassVersion = 1;
LegacyWeapon weapon; LegacyWeapon weapon;
SerializeLegacyWeapon(&ar, &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): The implementation replaces the flawed `RegisterEntity` routine in [`Game/Entity/EntityManager.cpp`](file:///w:/Classified/Juliet/Game/Entity/EntityManager.cpp):
```cpp ```cpp
[[nodiscard]] Entity* AllocateEntity(EntityManager& manager, Class* classPtr) [[nodiscard]] Entity* AllocateEntity(EntityManager& manager, Class* derivedClassPtr)
{ {
Assert(classPtr != nullptr); Assert(derivedClassPtr != nullptr);
Assert(classPtr->kind < ENTITY(Count)); Assert(derivedClassPtr->kind < ENTITY(Count));
Assert(classPtr->size_of >= sizeof(entity_template)); Assert(derivedClassPtr->size_of >= sizeof(entity_template));
Assert(classPtr->alignment > 0); Assert(derivedClassPtr->alignment > 0);
// 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.Kind = classPtr; baseTemplate.DerivedKind = derivedClassPtr;
baseTemplate.Derived = nullptr; baseTemplate.Derived = nullptr;
baseTemplate.X = 0.0f; baseTemplate.X = 0.0f;
baseTemplate.Y = 0.0f; baseTemplate.Y = 0.0f;
baseTemplate.Z = 0.0f; baseTemplate.Z = 0.0f;
baseTemplate.IsDirty = true; baseTemplate.IsDirty = true;
manager.Entities.PushBack(baseTemplate); manager.Entities.PushBack(baseTemplate);
Entity* basePtr = manager.Entities.Back(); Entity* basePtr = manager.Entities.Back();
Assert(basePtr != nullptr); Assert(basePtr != nullptr);
// 2. Allocate zeroed derived component memory in the typed arena // 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); Assert(typedArray.arena != nullptr);
void* rawMemory = ArenaPushSize( void* rawMemory = ArenaPushSize(
typedArray.arena, typedArray.arena,
classPtr->size_of, derivedClassPtr->size_of,
classPtr->alignment, derivedClassPtr->alignment,
true JULIET_DEBUG_PARAM(kEntity_type_names[classPtr->kind])); true JULIET_DEBUG_PARAM(kEntity_type_names[derivedClassPtr->kind]));
Assert(rawMemory != nullptr); Assert(rawMemory != nullptr);
auto* derivedTemplate = reinterpret_cast<entity_template*>(rawMemory); auto* derivedTemplate = reinterpret_cast<entity_template*>(rawMemory);
@@ -330,9 +330,9 @@ entity_instance
0x0100000000000042 0x0100000000000042
; class ; class
Inert Inert
; base_version ; version
1 1
; derived_version ; class_version
1 1
; position ; position
0.43 0.32 1.56 0.43 0.32 1.56
@@ -342,7 +342,8 @@ 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.
- **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 ### 4.2 Eliminating Intermediate Stack Allocations
Under the new pipeline: 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)`. 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. 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`. 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 ### 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:
@@ -440,18 +441,13 @@ To ensure fast and safe type lookup during file deserialization:
Entity* basePtr = AllocateEntity(manager, classPtr); Entity* basePtr = AllocateEntity(manager, classPtr);
Assert(basePtr != nullptr); Assert(basePtr != nullptr);
// 3. Read base versions and properties in-place // 3. Serialize Base Entity in-place using Entity::Kind
uint16 baseVersion = 1; SerializeClassInstance(ar, Entity::Kind, basePtr);
uint16 derivedVersion = 1;
SerializeProp(ar, "base_version", baseVersion);
SerializeProp(ar, "derived_version", derivedVersion);
SerializeEntityBase(ar, *basePtr, baseVersion); // 4. Stream derived properties in-place using basePtr->DerivedKind
if (basePtr->DerivedKind != nullptr && basePtr->Derived != nullptr)
// 4. Stream derived properties in-place directly into the typed arena
if (classPtr->serialize_fct != nullptr)
{ {
classPtr->serialize_fct(ar, basePtr->Derived, derivedVersion); SerializeClassInstance(ar, basePtr->DerivedKind, basePtr->Derived);
} }
// Freshly loaded entity matches disk state exactly // 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 ```cpp
struct Entity final struct Entity final
{ {
EntityID ID = 0; DECLARE_ENTITY() // static Class* Kind; (Entity's own Class descriptor)
Class* Kind = nullptr;
DerivedType Derived = nullptr; EntityID ID = 0;
float X = 0.0f; Class* DerivedKind = nullptr; // Pointer to derived class descriptor (e.g. Inert::Kind)
float Y = 0.0f; DerivedType Derived = nullptr; // Pointer to derived component memory
float Z = 0.0f; float X = 0.0f;
bool IsDirty = false; 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 ### 4.2 `WorldSettings.jasset` Format Specification
Global world settings are stored in `WorldSettings.jasset`. 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 ```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 struct WorldEnvironmentSettings
{ {
// Directional Sun & Ambient Lighting // Directional Sun & Ambient Lighting
Vector3 SunDirection = { 0.577f, -0.577f, -0.577f }; Vector3 SunDirection = { 0.577f, -0.577f, -0.577f };
float _Pad0 = 0.0f;
Vector3 SunColor = { 1.0f, 0.95f, 0.8f }; Vector3 SunColor = { 1.0f, 0.95f, 0.8f };
float SunIntensity = 1.0f; float SunIntensity = 1.0f;
Vector3 AmbientColor = { 0.2f, 0.25f, 0.35f }; Vector3 AmbientColor = { 0.2f, 0.25f, 0.35f };
@@ -391,9 +404,9 @@ entity_instance
0x0100000000000042 0x0100000000000042
; class ; class
Inert Inert
; base_version ; version
1 1
; derived_version ; class_version
1 1
; position ; position
0.43 0.32 1.56 0.43 0.32 1.56
@@ -408,8 +421,8 @@ Inert
- `; class`: The runtime `Class` name (e.g. `Inert`, `Door`). - `; class`: The runtime `Class` name (e.g. `Inert`, `Door`).
- `; template`: Optional relative path to archetype template (e.g. `Assets/Templates/Door_Wood.jasset`). - `; template`: Optional relative path to archetype template (e.g. `Assets/Templates/Door_Wood.jasset`).
2. **Version Directives**: 2. **Version Directives**:
- `; base_version`: Engine-wide base entity version (`kEntityBaseVersion`). - `; version`: Universal asset version (`kEntityBaseVersion` for entities, or asset schema version for non-entity files like `WorldSettings.jasset`).
- `; derived_version`: Class-specific gameplay version (`Class::Version`). - `; 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**: 3. **Base Entity Properties**:
- Position (`position\n0.43 0.32 1.56`), Rotation, Scale. - Position (`position\n0.43 0.32 1.56`), Rotation, Scale.
4. **Derived Entity Properties**: 4. **Derived Entity Properties**:
+1 -1
View File
@@ -21,7 +21,7 @@
// namespace // namespace
// { // {
// void serialize_test(archive* ar, void* payload) // void serialize_test(Archive* ar, void* payload)
// { // {
// SerializedEntityTest* test = reinterpret_cast<SerializedEntityTest*>(payload); // SerializedEntityTest* test = reinterpret_cast<SerializedEntityTest*>(payload);
// serialize_elem(ar, test->A); // serialize_elem(ar, test->A);
@@ -1,7 +1,6 @@
#pragma once #pragma once
#include <Core/Application/IApplication.h> #include <Core/Application/IApplication.h>
#include <Core/Common/CoreTypes.h>
#include <Juliet.h> #include <Juliet.h>
enum class JulietInit_Flags : uint8; enum class JulietInit_Flags : uint8;
+18
View File
@@ -43,5 +43,23 @@ constexpr int64 int64Max = MaxValueOf<int64>();
constexpr index_t indexMax = MaxValueOf<index_t>(); constexpr index_t indexMax = MaxValueOf<index_t>();
template <typename Type>
consteval Type MinValueOf()
{
return std::numeric_limits<Type>::lowest();
}
constexpr uint8 uint8Min = MinValueOf<uint8>();
constexpr uint16 uint16Min = MinValueOf<uint16>();
constexpr uint32 uint32Min = MinValueOf<uint32>();
constexpr uint64 uint64Min = MinValueOf<uint64>();
constexpr int8 int8Min = MinValueOf<int8>();
constexpr int16 int16Min = MinValueOf<int16>();
constexpr int32 int32Min = MinValueOf<int32>();
constexpr int64 int64Min = MinValueOf<int64>();
constexpr index_t indexMin = MinValueOf<index_t>();
#define Kilobytes(value) value * 1024 #define Kilobytes(value) value * 1024
#define Megabytes(value) Kilobytes(value) * 1024 #define Megabytes(value) Kilobytes(value) * 1024
+26 -28
View File
@@ -1,9 +1,7 @@
#pragma once #pragma once
#include <Core/Common/CoreTypes.h>
#include <Juliet.h> #include <Juliet.h>
#define global static #define global static
// 1. Stringify helpers // 1. Stringify helpers
@@ -50,40 +48,40 @@
#if JULIET_DEBUG #if JULIET_DEBUG
#define JULIET_ASSERT_INTERNAL(expression, message) \ #define JULIET_ASSERT_INTERNAL(expression, message) \
JULIET_WARNING_PUSH \ JULIET_WARNING_PUSH \
JULIET_SUPPRESS_CLANG("-Wextra-semi-stmt") \ JULIET_SUPPRESS_CLANG("-Wextra-semi-stmt") \
JULIET_SUPPRESS_MSVC(4127) \ JULIET_SUPPRESS_MSVC(4127) \
JULIET_SUPPRESS_MSVC(4548) \ JULIET_SUPPRESS_MSVC(4548) \
{ \ { \
if (!(expression)) [[unlikely]] \ if (!(expression)) [[unlikely]] \
{ \ { \
JulietAssert(#expression, message); \ JulietAssert(#expression, message); \
} \ } \
} \ } \
JULIET_WARNING_POP \ JULIET_WARNING_POP \
static_assert(true, "") static_assert(true, "")
#define AssertHR(hr_expression, message) \ #define AssertHR(hr_expression, message) \
do \ do \
{ \ { \
long hr_val = (hr_expression); \ long hr_val = (hr_expression); \
if (hr_val < 0) \ if (hr_val < 0) \
{ \ { \
JulietAssert(#hr_expression, message, std::source_location::current(), hr_val); \ JulietAssert(#hr_expression, message, std::source_location::current(), hr_val); \
} \ } \
} \ } \
while (0) while (0)
#define GET_ASSERT_MACRO(_1, _2, NAME, ...) NAME #define GET_ASSERT_MACRO(_1, _2, NAME, ...) NAME
#define Assert(...) GET_ASSERT_MACRO(__VA_ARGS__, JULIET_ASSERT_INTERNAL, JULIET_ASSERT_NO_MSG)(__VA_ARGS__) #define Assert(...) GET_ASSERT_MACRO(__VA_ARGS__, JULIET_ASSERT_INTERNAL, JULIET_ASSERT_NO_MSG)(__VA_ARGS__)
#define JULIET_ASSERT_NO_MSG(expression) JULIET_ASSERT_INTERNAL(expression, "No additional information provided.") #define JULIET_ASSERT_NO_MSG(expression) JULIET_ASSERT_INTERNAL(expression, "No additional information provided.")
#define Unimplemented() \ #define Unimplemented() \
do \ do \
{ \ { \
JulietAssert("UNIMPLEMENTED", "This code path is not yet functional."); \ JulietAssert("UNIMPLEMENTED", "This code path is not yet functional."); \
} \ } \
while (0) while (0)
#else #else
#define Assert(...) ((void)0) #define Assert(...) ((void)0)
+5 -3
View File
@@ -19,9 +19,9 @@ struct Arena;
#define ConstString(str) { const_cast<char*>((str)), sizeof(str) - 1 } #define ConstString(str) { const_cast<char*>((str)), sizeof(str) - 1 }
#define CStr(str) ((str).Str) #define CStr(str) ((str).Str)
#define InplaceString(name, size) \ #define InplaceString(name, size) \
char name##_[size]; \ char name##_[size]; \
MemSet(name##_, 0, sizeof(uint32)); \ MemSet(name##_, 0, sizeof(uint32)); \
String name = { name##_, 0 } String name = { name##_, 0 }
// Everything is Little Endian // Everything is Little Endian
enum class StringEncoding : uint8 enum class StringEncoding : uint8
@@ -164,6 +164,8 @@ extern JULIET_API bool ConvertString(String from, String to, String src, StringB
JULIET_API String StringCopy(NonNullPtr<Arena> arena, String str); JULIET_API String StringCopy(NonNullPtr<Arena> arena, String str);
JULIET_API String16 str16_from_8(NonNullPtr<Arena> arena, String8 str); JULIET_API String16 str16_from_8(NonNullPtr<Arena> arena, String8 str);
String trim_whitespace(String str);
template <typename... Args> template <typename... Args>
String Format(NonNullPtr<Arena> arena, const char* formatStr, Args&&... args) String Format(NonNullPtr<Arena> arena, const char* formatStr, Args&&... args)
{ {
+116 -6
View File
@@ -2,16 +2,126 @@
#include <Juliet.h> #include <Juliet.h>
#include <Core/Common/String.h>
#include <Core/HAL/IO/IOStream.h>
// .jasset
// AssetFile ::= { CommentLine | EmptyLine | PropertyDeclaration } ;
// CommentLine ::= ( "#" | "//" ) { Character } LineEnding ;
// EmptyLine ::= { Whitespace } LineEnding ;
// PropertyDeclaration ::= KeyHeader LineEnding ValueBlock ;
// KeyHeader ::= ";" { Whitespace } Identifier ;
// ValueBlock ::= { ValueLine LineEnding } ;
// ValueLine ::= { Whitespace } ValueString { Whitespace } ;
// LineEnding ::= "\r\n" | "\n" ;
// Identifier ::= [a-zA-Z_][a-zA-Z0-9_]* ;
struct Vector4;
struct IOStream;
struct Arena; struct Arena;
struct archive struct ArchivePropertyNode
{ {
Arena* arena; String key;
void* base_ptr; String value;
index_t offset; uint32 key_crc;
bool loading; bool consumed;
}; };
JULIET_API void serialize(archive& ar, void* data, size_t size); struct ParsedArchive
{
ArchivePropertyNode* nodes = nullptr;
uint32 property_count = 0;
};
struct Archive
{
Arena* arena;
bool loading;
ParsedArchive base = {};
IOStream* stream = nullptr;
uint32 base_version = 0;
uint16 class_version = 0;
// to remove
void* base_ptr = nullptr;
index_t offset = 0;
};
JULIET_API void serialize(Archive& ar, void* data, size_t size);
#define serialize_elem(ar, val) serialize((ar), &(val), sizeof(val)) #define serialize_elem(ar, val) serialize((ar), &(val), sizeof(val))
JULIET_API ParsedArchive tokenize_archive(NonNullPtr<Arena> arena, ByteBuffer file_buffer);
JULIET_API ArchivePropertyNode* find_property(NonNullPtr<ParsedArchive> archive, uint32 property_crc);
JULIET_API void write_property_header(Archive& archive, String property_name);
JULIET_API bool read_prop(Archive& ar, String value_raw, String& value);
JULIET_API void write(NonNullPtr<IOStream> stream, String value);
JULIET_API bool read(const char* buffer, float& value, const char** end = nullptr);
JULIET_API void write(NonNullPtr<IOStream> stream, float value);
JULIET_API bool read(const char* buffer, int8& value);
JULIET_API void write(NonNullPtr<IOStream> stream, int8 value);
JULIET_API bool read(const char* buffer, int16& value);
JULIET_API void write(NonNullPtr<IOStream> stream, int16 value);
JULIET_API bool read(const char* buffer, int32& value);
JULIET_API void write(NonNullPtr<IOStream> stream, int32 value);
JULIET_API bool read(const char* buffer, int64& value);
JULIET_API void write(NonNullPtr<IOStream> stream, int64 value);
JULIET_API bool read(const char* buffer, uint8& value);
JULIET_API void write(NonNullPtr<IOStream> stream, uint8 value);
JULIET_API bool read(const char* buffer, uint16& value);
JULIET_API void write(NonNullPtr<IOStream> stream, uint16 value);
JULIET_API bool read(const char* buffer, uint32& value);
JULIET_API void write(NonNullPtr<IOStream> stream, uint32 value);
JULIET_API bool read(const char* buffer, uint64& value);
JULIET_API void write(NonNullPtr<IOStream> stream, uint64 value);
JULIET_API bool read(const char* buffer, bool& value);
JULIET_API void write(NonNullPtr<IOStream> stream, bool value);
JULIET_API bool read(const char* buffer, Vector4& value);
JULIET_API void write(NonNullPtr<IOStream> stream, Vector4 value);
// For primitives not needing archive nor allocation
template <typename Type>
bool read_prop(Archive& /*ar*/, String value_raw, Type& value)
{
char buffer[64];
size_t cpy_size = Min(value_raw.Size, sizeof(buffer) - 1);
MemCopy(buffer, value_raw.Str, cpy_size);
buffer[cpy_size] = '\0';
return read(buffer, value);
}
#define SERIALIZE(ar, name, var) serialize((ar), ConstString(#name), #name##_crc32, (var))
template <typename Type>
bool serialize(Archive& ar, String property_name, uint32 property_crc, Type& value)
{
Assert(IsValid(property_name));
bool result = false;
if (ar.loading)
{
if (auto* prop = find_property(&ar.base, property_crc))
{
if (read_prop(ar, prop->value, value))
{
result = true;
}
}
}
else
{
// Save
write_property_header(ar, property_name);
write(ar.stream, value);
result = true;
}
return result;
}
#if JULIET_DEBUG
JULIET_API void audit_unconsumed_properties(NonNullPtr<ParsedArchive> archive, String context_name);
#endif
+1 -3
View File
@@ -1,9 +1,7 @@
#pragma once #pragma once
#include <Core/Common/CoreTypes.h>
#include <Core/Common/NonNullPtr.h> #include <Core/Common/NonNullPtr.h>
#include <Core/Common/String.h> #include <Core/Common/String.h>
#include <Juliet.h>
struct Window; struct Window;
@@ -15,4 +13,4 @@ extern JULIET_API void ShowWindow(NonNullPtr<Window> window);
extern JULIET_API void HideWindow(NonNullPtr<Window> window); extern JULIET_API void HideWindow(NonNullPtr<Window> window);
extern JULIET_API WindowID GetWindowID(NonNullPtr<Window> window); extern JULIET_API WindowID GetWindowID(NonNullPtr<Window> window);
extern JULIET_API void SetWindowTitle(NonNullPtr<Window> window, String title); extern JULIET_API void SetWindowTitle(NonNullPtr<Window> window, String title);
@@ -1,7 +1,5 @@
#pragma once #pragma once
#include <Core/Common/CoreTypes.h>
// Represents a Virtual Key corresponding to the Physical key, but localized using the keyboard layout // Represents a Virtual Key corresponding to the Physical key, but localized using the keyboard layout
// ScanCode reprensent US ASCII Keyboard // ScanCode reprensent US ASCII Keyboard
// WASD Scan codes are ZQSD in KeyCode for French keyboard // WASD Scan codes are ZQSD in KeyCode for French keyboard
-3
View File
@@ -1,8 +1,5 @@
#pragma once #pragma once
#include <Core/Common/CoreTypes.h>
#include <Juliet.h>
namespace Memory namespace Memory
{ {
Byte* OS_Reserve(size_t size); Byte* OS_Reserve(size_t size);
-1
View File
@@ -1,6 +1,5 @@
#pragma once #pragma once
#include <Core/Common/CoreTypes.h>
#include <Core/Common/NonNullPtr.h> #include <Core/Common/NonNullPtr.h>
#ifdef JULIET_ENABLE_IMGUI #ifdef JULIET_ENABLE_IMGUI
-2
View File
@@ -1,7 +1,5 @@
#pragma once #pragma once
#include <Core/Common/CoreTypes.h>
enum class JulietInit_Flags : uint8 enum class JulietInit_Flags : uint8
{ {
None = 0, None = 0,
-2
View File
@@ -1,9 +1,7 @@
#pragma once #pragma once
#include <Core/Common/CoreTypes.h>
#include <Core/Common/NonNullPtr.h> #include <Core/Common/NonNullPtr.h>
#include <Core/Memory/MemoryArena.h> #include <Core/Memory/MemoryArena.h>
#include <Juliet.h>
// TODO : Juliet strings // TODO : Juliet strings
// TODO Juliet Containers + Allocators... // TODO Juliet Containers + Allocators...
-3
View File
@@ -1,8 +1,5 @@
#pragma once #pragma once
#include <Core/Common/CoreTypes.h>
#include <Juliet.h>
extern JULIET_API float RoundF(float value); extern JULIET_API float RoundF(float value);
inline int32 LRoundF(float value) inline int32 LRoundF(float value)
+8 -6
View File
@@ -1,9 +1,5 @@
#pragma once #pragma once
#include <Core/Common/CoreTypes.h>
#include <Juliet.h>
#include <math.h>
struct Vector3 struct Vector3
{ {
float x, y, z; float x, y, z;
@@ -15,7 +11,10 @@ struct Vector3
struct Vector4 struct Vector4
{ {
float x, y, z, w; float x = 0.f;
float y = 0.f;
float z = 0.f;
float w = 0.f;
}; };
inline Vector3 Normalize(const Vector3& v) inline Vector3 Normalize(const Vector3& v)
@@ -33,4 +32,7 @@ inline Vector3 Cross(const Vector3& a, const Vector3& b)
return { a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x }; return { a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x };
} }
inline float Dot(const Vector3& a, const Vector3& b) { return a.x * b.x + a.y * b.y + a.z * b.z; } inline float Dot(const Vector3& a, const Vector3& b)
{
return a.x * b.x + a.y * b.y + a.z * b.z;
}
+1 -4
View File
@@ -1,10 +1,8 @@
#pragma once #pragma once
#include <Core/Common/CoreTypes.h>
#include <Core/Common/CoreUtils.h> #include <Core/Common/CoreUtils.h>
#include <Core/Common/NonNullPtr.h> #include <Core/Common/NonNullPtr.h>
#include <Core/Common/String.h> #include <Core/Common/String.h>
#include <Juliet.h>
#if JULIET_DEBUG #if JULIET_DEBUG
#include <Core/Memory/MemoryArenaDebug.h> #include <Core/Memory/MemoryArenaDebug.h>
@@ -79,8 +77,7 @@ JULIET_API void ArenaClear(NonNullPtr<Arena> arena);
template <typename FirstDebugArg, typename... DebugArgs> template <typename FirstDebugArg, typename... DebugArgs>
#endif #endif
[[nodiscard]] inline void* ArenaPushSize(NonNullPtr<Arena> arena, size_t size, size_t align, [[nodiscard]] inline void* ArenaPushSize(NonNullPtr<Arena> arena, size_t size, size_t align,
bool shouldBeZeroed JULIET_DEBUG_PARAM(FirstDebugArg&& firstDebugArg, bool shouldBeZeroed JULIET_DEBUG_PARAM(FirstDebugArg&& firstDebugArg, DebugArgs&&... debugArgs))
DebugArgs&&... debugArgs))
{ {
return ArenaPush(arena, size, align, return ArenaPush(arena, size, align,
shouldBeZeroed JULIET_DEBUG_PARAM( shouldBeZeroed JULIET_DEBUG_PARAM(
@@ -1,9 +1,7 @@
#pragma once #pragma once
#include <Core/Common/CoreTypes.h>
#include <Core/Common/NonNullPtr.h> #include <Core/Common/NonNullPtr.h>
#include <Core/Common/String.h> #include <Core/Common/String.h>
#include <Juliet.h>
#if JULIET_DEBUG #if JULIET_DEBUG
@@ -45,5 +43,4 @@ void DebugArenaRemoveLastAllocation(MemoryBlock* blk);
JULIET_API Arena* GetDebugInfoArena(); JULIET_API Arena* GetDebugInfoArena();
#endif #endif
+7 -9
View File
@@ -1,7 +1,5 @@
#pragma once #pragma once
#include <Core/Common/CoreTypes.h>
#define ArraySize(array) (sizeof(array) / sizeof(array[0])) #define ArraySize(array) (sizeof(array) / sizeof(array[0]))
inline int32 MemCompare(const void* leftValue, const void* rightValue, size_t size) inline int32 MemCompare(const void* leftValue, const void* rightValue, size_t size)
@@ -60,13 +58,13 @@ struct QueueNode
}; };
#define DECLARE_QUEUE(type) \ #define DECLARE_QUEUE(type) \
struct type##Queue \ struct type##Queue \
{ \ { \
type* First; \ type* First; \
type* Last; \ type* Last; \
size_t Nodecount; \ size_t Nodecount; \
size_t Size; \ size_t Size; \
}; };
// TODO: homemade versions // TODO: homemade versions
#define MemSet memset #define MemSet memset
@@ -1,7 +1,5 @@
#pragma once #pragma once
#include <Core/Common/CoreTypes.h>
// TODO : Do something better. // TODO : Do something better.
constexpr uint32 kLocalhost = (127 << 3) | (0 << 2) | (0 << 1) | 1; constexpr uint32 kLocalhost = (127 << 3) | (0 << 2) | (0 << 1) | 1;
constexpr uint32 kAnyIp = 0; constexpr uint32 kAnyIp = 0;
@@ -1,6 +1,5 @@
#pragma once #pragma once
#include <Core/Common/CoreTypes.h>
#include <Core/Container/Vector.h> #include <Core/Container/Vector.h>
class NetworkPacket class NetworkPacket
+2
View File
@@ -29,4 +29,6 @@
#include <type_traits> #include <type_traits>
#include <vector> #include <vector>
#include <Juliet.h>
#include <Core/Common/CoreTypes.h> #include <Core/Common/CoreTypes.h>
@@ -1,6 +1,5 @@
#pragma once #pragma once
#include <Core/Common/CoreTypes.h>
#include <Core/Memory/MemoryArena.h> #include <Core/Memory/MemoryArena.h>
struct thread_context struct thread_context
+22 -10
View File
@@ -1,19 +1,20 @@
#pragma once #pragma once
#include <Juliet.h> #include <Juliet.h>
#include <Core/Common/CRC32.h> #include <Core/Common/CRC32.h>
#include <Core/Common/String.h> #include <Core/Common/String.h>
struct archive; struct Archive;
using serialize_fct_type = void (*)(archive*, void* payload); using serialize_fct_type = void (*)(Archive&, uint16 version, void* payload);
struct Class struct Class
{ {
uint32 CRC; uint32 CRC;
uint8 kind; uint8 kind;
uint16 version;
const Class* base_class;
serialize_fct_type serialize_fct; serialize_fct_type serialize_fct;
size_t size_of; size_t size_of;
size_t alignment; size_t alignment;
@@ -23,25 +24,36 @@ struct Class
#endif #endif
}; };
consteval Class MakeClass(String name, uint8 kind, size_t size, size_t align, serialize_fct_type fct) #define DEFINE_CLASS_VERSIONED(cls, version, base_class, serialize_fct) \
constexpr Class classKind##cls = \
MakeClass(ConstString(#cls), 0, (version), (base_class), sizeof(cls), alignof(cls), (serialize_fct)); \
Class* cls::kind = const_cast<Class*>(&classKind##cls);
consteval Class MakeClass(String name, uint8 kind, uint16 version, const Class* base_class, size_t size, size_t align,
serialize_fct_type fct)
{ {
Class cls = {}; Class cls = {};
cls.CRC = crc32(name.Str, name.Size); cls.CRC = crc32(name.Str, name.Size);
cls.kind = kind; cls.kind = kind;
cls.version = version;
cls.base_class = base_class;
cls.size_of = size; cls.size_of = size;
cls.alignment = align; cls.alignment = align;
cls.serialize_fct = fct; cls.serialize_fct = fct;
#if JULIET_DEBUG #if JULIET_DEBUG
// TODO: string struct may be
cls.Name = name; cls.Name = name;
#endif #endif
return cls; return cls;
} }
bool IsA(const Class& query, const Class* target);
template <typename type> template <typename type>
bool IsA(Class& cls) bool IsA(const Class& cls)
{ {
return cls.CRC == type::StaticClass->CRC; return IsA(cls, type::StaticClass);
} }
JULIET_API void serialize(Archive& ar, NonNullPtr<Class> cls, void* instance);
-4
View File
@@ -1,9 +1,5 @@
#pragma once #pragma once
#include <Juliet.h>
#include <Core/Common/CoreTypes.h>
#if JULIET_DEBUG #if JULIET_DEBUG
#define ALLOW_SHADER_HOT_RELOAD 1 #define ALLOW_SHADER_HOT_RELOAD 1
#else #else
+3 -5
View File
@@ -1,18 +1,16 @@
#pragma once #pragma once
#include <Core/Common/CoreTypes.h>
#include <Core/Common/NonNullPtr.h> #include <Core/Common/NonNullPtr.h>
#include <Core/Common/String.h> #include <Core/Common/String.h>
#include <Core/Math/Matrix.h> #include <Core/Math/Matrix.h>
#include <Core/Math/Vector.h> #include <Core/Math/Vector.h>
#include <Juliet.h>
struct Arena; struct Arena;
struct Vertex; struct Vertex;
using MeshAssetID = index_t; using MeshAssetID = index_t;
using MaterialAssetID = index_t; using MaterialAssetID = index_t;
using MeshInstanceID = index_t; using MeshInstanceID = index_t;
struct MeshAsset struct MeshAsset
{ {
@@ -26,7 +24,7 @@ struct MeshAsset
struct MaterialAsset struct MaterialAsset
{ {
Vector4 AlbedoColor = {1.0f, 1.0f, 1.0f, 1.0f}; Vector4 AlbedoColor = { 1.0f, 1.0f, 1.0f, 1.0f };
}; };
struct MeshInstance struct MeshInstance
+19 -3
View File
@@ -351,8 +351,7 @@ bool ConvertString(StringEncoding from, StringEncoding to, String src, StringBuf
{ {
character = kUnknown_UNICODE; character = kUnknown_UNICODE;
} }
if ((character >= 0xD800 && character <= 0xDFFF) || (character == 0xFFFE || character == 0xFFFF) || if ((character >= 0xD800 && character <= 0xDFFF) || (character == 0xFFFE || character == 0xFFFF) || character > 0x10FFFF)
character > 0x10FFFF)
{ {
character = kUnknown_UNICODE; character = kUnknown_UNICODE;
} }
@@ -593,7 +592,7 @@ String StringCopy(NonNullPtr<Arena> arena, String str)
{ {
String result; String result;
result.Size = str.Size; result.Size = str.Size;
result.Str = static_cast<char*>(ArenaPush(arena, str.Size + 1, alignof(char), true JULIET_DEBUG_PARAM("String"))); result.Str = static_cast<char*>(ArenaPush(arena, str.Size + 1, alignof(char), true JULIET_DEBUG_PARAM("String")));
MemCopy(result.Str, str.Str, str.Size); MemCopy(result.Str, str.Str, str.Size);
result.Str[result.Size] = 0; result.Str[result.Size] = 0;
return result; return result;
@@ -623,3 +622,20 @@ String16 str16_from_8(NonNullPtr<Arena> arena, String8 in)
return result; return result;
} }
String trim_whitespace(String str)
{
// Handles windos and unix kind of whitespaces
// Remove any whitespace at the begin of the string
while (str.Size > 0 && (*str.Str == ' ' || *str.Str == '\t' || *str.Str == '\r' || *str.Str == '\n'))
{
str.Str++;
str.Size--;
}
// Remove any whitespace at the end of the string.
while (str.Size > 0 && (str.Str[str.Size - 1] == ' ' || str.Str[str.Size - 1] == '\t' ||
str.Str[str.Size - 1] == '\r' || str.Str[str.Size - 1] == '\n'))
{
str.Size--;
}
return str;
}
+362 -1
View File
@@ -1,10 +1,15 @@
#include <Core/Common/serialization.h> #include <Core/Common/serialization.h>
#include <Core/Common/CoreUtils.h> #include <Core/Common/CoreUtils.h>
#include <Core/Common/CRC32.h>
#include <Core/HAL/IO/IOStream.h>
#include <Core/Logging/LogManager.h>
#include <Core/Logging/LogTypes.h>
#include <Core/Math/Vector.h>
#include <Core/Memory/MemoryArena.h> #include <Core/Memory/MemoryArena.h>
#include <Core/Memory/Utils.h> #include <Core/Memory/Utils.h>
void serialize(archive& ar, void* data, size_t size) void serialize(Archive& ar, void* data, size_t size)
{ {
if (ar.loading) if (ar.loading)
{ {
@@ -19,3 +24,359 @@ void serialize(archive& ar, void* data, size_t size)
ar.offset += size; ar.offset += size;
} }
} }
ParsedArchive tokenize_archive(NonNullPtr<Arena> arena, ByteBuffer file_buffer)
{
Assert(file_buffer.Data);
ParsedArchive archive = {};
uint8* cursor = (uint8*)file_buffer.Data;
uint8* end = cursor + file_buffer.Size;
// Pass 1: count properties
uint32 property_count = 0;
uint8* scan = cursor;
while (scan < end)
{
if (*scan == ';')
{
if (scan == cursor || *(scan - 1) == '\n')
{
property_count++;
}
}
scan++;
}
if (property_count > 0)
{
// Pass 2: extract properties
NonNullPtr nodes =
ArenaPushArray<ArchivePropertyNode>(arena, property_count JULIET_DEBUG_PARAM("tokenizer nodes"));
uint32 node_index = 0;
scan = cursor;
while (scan < end && node_index < property_count)
{
// Skip comments
if (*scan == '#' || (*scan == '/' && scan + 1 < end && *(scan + 1) == '/'))
{
while (scan < end && *scan != '\n')
{
++scan;
}
++scan; // skipping trailing \n
continue;
}
if (*scan == ';')
{
++scan; // skipping the ;
// extract property name
uint8* property_name_cursor = scan;
while (scan < end && *scan != '\r' && *scan != '\n')
{
++scan;
}
String raw_property_name = { .Str = (char*)property_name_cursor, .Size = (size_t)(scan - property_name_cursor) };
String property_name = trim_whitespace(raw_property_name);
Assert(property_name.Str && property_name.Size > 0);
// skip \r\n
while (scan < end && (*scan == '\r' || *scan == '\n'))
{
++scan;
}
// Value block is everything until comment or new line
uint8* value_start = scan;
uint8* value_end = scan;
while (scan < end)
{
if (*scan == '\r' || *scan == '\n')
{
break;
}
if (*scan == '#' || (*scan == '/' && scan + 1 < end && *(scan + 1) == '/'))
{
break;
}
++scan;
value_end = scan;
}
String raw_value = { .Str = (char*)value_start, .Size = static_cast<size_t>(value_end - value_start) };
String value = trim_whitespace(raw_value);
nodes[node_index].key_crc = crc32(property_name);
nodes[node_index].key = StringCopy(arena, property_name);
nodes[node_index].value = value;
nodes[node_index].consumed = false;
node_index++;
}
else
{
++scan;
}
}
archive.nodes = nodes.Get();
archive.property_count = node_index;
}
return archive;
}
ArchivePropertyNode* find_property(NonNullPtr<ParsedArchive> archive, uint32 property_crc)
{
Assert(archive->nodes && archive->property_count > 0);
ArchivePropertyNode* result = nullptr;
ArchivePropertyNode* nodes = archive->nodes;
for (index_t idx = 0; idx < archive->property_count; ++idx)
{
if (nodes[idx].key_crc == property_crc)
{
nodes[idx].consumed = true;
result = &nodes[idx];
break;
}
}
return result;
}
void write_property_header(Archive& ar, String property_name)
{
Assert(!ar.loading);
Assert(ar.stream);
Assert(IsValid(property_name));
IOPrintf(ar.stream, "; %s\n", CStr(property_name));
}
bool read_prop(Archive& ar, String value_raw, String& value)
{
String parsed = value_raw;
// Ignoring eventual "
if (parsed.Size >= 2 && parsed.Str[0] == '"' && parsed.Str[parsed.Size - 1] == '"')
{
parsed.Str++;
parsed.Size -= 2;
}
Assert(ar.arena);
value = StringCopy(ar.arena, parsed);
return true;
}
void write(NonNullPtr<IOStream> stream, String value)
{
if (ContainsChar(value, ' ')) // Add " " around string with spaces
{
IOPrintf(stream, "\"%.*s\"\n", (int32)value.Size, value.Str);
}
else
{
IOPrintf(stream, "%.*s\n", (int32)value.Size, value.Str);
}
}
bool read(const char* buffer, float& value, const char** next /* = nullptr */)
{
bool result = false;
char* end = nullptr;
float parsed = strtof(buffer, &end);
if (end != buffer)
{
value = parsed;
if (next)
{
*next = end;
}
result = true;
}
return result;
}
void write(NonNullPtr<IOStream> stream, float value)
{
IOPrintf(stream, "%.9g\n", value);
}
bool read(const char* buffer, int8& value)
{
bool result = false;
char* end = nullptr;
int32 parsed = strtol(buffer, &end, 10);
if (end != buffer && parsed >= int8Min && parsed <= int8Max)
{
value = (int8)parsed;
result = true;
}
return result;
}
void write(NonNullPtr<IOStream> stream, int8 value)
{
IOPrintf(stream, "%d\n", (int32)value);
}
bool read(const char* buffer, int16& value)
{
bool result = false;
char* end = nullptr;
int32 parsed = strtol(buffer, &end, 10);
if (end != buffer && parsed >= int16Min && parsed <= int16Max)
{
value = (int16)parsed;
result = true;
}
return result;
}
void write(NonNullPtr<IOStream> stream, int16 value)
{
IOPrintf(stream, "%d\n", (int32)value);
}
bool read(const char* buffer, int32& value)
{
char* end = nullptr;
value = strtol(buffer, &end, 10);
return end != buffer;
}
void write(NonNullPtr<IOStream> stream, int32 value)
{
IOPrintf(stream, "%d\n", value);
}
bool read(const char* buffer, int64& value)
{
char* end = nullptr;
value = strtoll(buffer, &end, 0);
return end != buffer;
}
void write(NonNullPtr<IOStream> stream, int64 value)
{
IOPrintf(stream, "%lld\n", value);
}
bool read(const char* buffer, uint8& value)
{
bool result = false;
char* end = nullptr;
uint32 parsed = strtoul(buffer, &end, 10);
if (end != buffer && parsed >= uint8Min && parsed <= uint8Max)
{
value = (uint8)parsed;
result = true;
}
return result;
}
void write(NonNullPtr<IOStream> stream, uint8 value)
{
IOPrintf(stream, "%u\n", (uint32)value);
}
bool read(const char* buffer, uint16& value)
{
bool result = false;
char* end = nullptr;
uint32 parsed = strtoul(buffer, &end, 10);
if (end != buffer && parsed >= uint16Min && parsed <= uint16Max)
{
value = (uint16)parsed;
result = true;
}
return result;
}
void write(NonNullPtr<IOStream> stream, uint16 value)
{
IOPrintf(stream, "%u\n", (uint32)value);
}
bool read(const char* buffer, uint32& value)
{
char* end = nullptr;
value = strtoul(buffer, &end, 10);
return end != buffer;
}
void write(NonNullPtr<IOStream> stream, uint32 value)
{
IOPrintf(stream, "%u\n", value);
}
bool read(const char* buffer, uint64& value)
{
char* end = nullptr;
value = strtoull(buffer, &end, 10);
return end != buffer;
}
void write(NonNullPtr<IOStream> stream, uint64 value)
{
IOPrintf(stream, "%llu\n", value);
}
bool read(const char* buffer, bool& value)
{
bool result = false;
value = buffer[0] == '1';
if (value || buffer[0] == '0')
{
result = true;
}
return result;
}
void write(NonNullPtr<IOStream> stream, bool value)
{
IOPrintf(stream, "%u\n", value);
}
bool read(const char* buffer, Vector4& value)
{
bool result = read(buffer, value.x, &buffer);
result &= read(buffer, value.y, &buffer);
result &= read(buffer, value.z, &buffer);
result &= read(buffer, value.w, &buffer);
return result;
}
void write(NonNullPtr<IOStream> stream, Vector4 value)
{
IOPrintf(stream, "%.9g %.9g %.9g %.9g\n", value.x, value.y, value.z, value.w);
}
#if JULIET_DEBUG
void audit_unconsumed_properties(NonNullPtr<ParsedArchive> archive, String context_name)
{
Assert(archive->nodes && archive->property_count > 0);
auto* nodes = archive->nodes;
for (index_t idx = 0; idx < archive->property_count; ++idx)
{
if (!nodes[idx].consumed)
{
LogWarning(LogCategory::Core, "[%s] Unconsumed or obsolete property detected: [%s] - CRC 0x%08X (Value: '%.*s')",
CStr(context_name), CStr(nodes[idx].key), nodes[idx].key_crc,
static_cast<int>(nodes[idx].value.Size), nodes[idx].value.Str);
}
}
}
#endif
+1 -2
View File
@@ -1,5 +1,4 @@
#include <Core/Common/CoreTypes.h> #include <Core/HAL/Display/Display_cpp.h>
#include <Core/HAL/Display/Display_cpp.h>
#include <Core/HAL/Display/DisplayDevice.h> #include <Core/HAL/Display/DisplayDevice.h>
#include <Core/Memory/Allocator.h> #include <Core/Memory/Allocator.h>
#include <Core/Memory/MemoryArena.h> #include <Core/Memory/MemoryArena.h>
@@ -1,5 +1,4 @@
#include <Core/Common/CoreTypes.h> #include <Core/Common/CoreUtils.h>
#include <Core/Common/CoreUtils.h>
#include <Core/Common/String.h> #include <Core/Common/String.h>
#include <Core/HAL/Filesystem/Filesystem.h> #include <Core/HAL/Filesystem/Filesystem.h>
#include <Core/HAL/Filesystem/Filesystem_Platform.h> #include <Core/HAL/Filesystem/Filesystem_Platform.h>
+1 -2
View File
@@ -1,5 +1,4 @@
#include <Core/Common/CoreTypes.h> #include <Core/Common/CoreUtils.h>
#include <Core/Common/CoreUtils.h>
#include <Core/HAL/DynLib/DynamicLibrary.h> #include <Core/HAL/DynLib/DynamicLibrary.h>
#include <Core/HAL/OS/OS.h> #include <Core/HAL/OS/OS.h>
#include <Core/HAL/OS/OS_Private.h> #include <Core/HAL/OS/OS_Private.h>
+1 -2
View File
@@ -1,5 +1,4 @@
#include <Core/Common/CoreTypes.h> #include <Core/Math/Math_Private.h>
#include <Core/Math/Math_Private.h>
// From MUSL lib https://github.com/rofl0r/musl // From MUSL lib https://github.com/rofl0r/musl
namespace namespace
+1 -2
View File
@@ -1,5 +1,4 @@
#include <Core/Common/CoreTypes.h> #include <Core/Common/CoreUtils.h>
#include <Core/Common/CoreUtils.h>
#include <Core/Memory/Allocator.h> #include <Core/Memory/Allocator.h>
#include <Core/Memory/MemoryArena.h> #include <Core/Memory/MemoryArena.h>
+37
View File
@@ -1 +1,38 @@
#include <Core/Common/serialization.h>
#include <Engine/Class.h> #include <Engine/Class.h>
bool IsA(const Class& query, const Class* target)
{
bool result = false;
const Class* current = &query;
while (current != nullptr)
{
if (current == target)
{
result = true;
break;
}
current = current->base_class;
}
return result;
}
void serialize(Archive& ar, NonNullPtr<Class> cls, void* instance)
{
Assert(instance != nullptr);
uint16 version = cls->version;
if (cls->base_class)
{
serialize(ar, ConstString("class_version"), "class_version"_crc32, version);
}
else
{
serialize(ar, ConstString("version"), "version"_crc32, version);
}
if (cls->serialize_fct)
{
cls->serialize_fct(ar, version, instance);
}
}