finishing first step of serialization : the basics

This commit is contained in:
2026-09-07 19:42:15 -04:00
parent a462575af4
commit b1de6ccc49
8 changed files with 406 additions and 110 deletions
+13 -4
View File
@@ -2,7 +2,19 @@
#include <Core/Common/serialization.h> #include <Core/Common/serialization.h>
DEFINE_CLASS_VERSIONED(Entity, 1, nullptr, nullptr) namespace
{
void serialize_entity(Archive& ar, uint16 /*version*/, void* payload)
{
Assert(payload);
Entity* entity = static_cast<Entity*>(payload);
SERIALIZE(ar, id, entity->ID);
SERIALIZE(ar, position, entity->position);
}
} // namespace
DEFINE_CLASS_VERSIONED(Entity, 1, nullptr, serialize_entity)
DEFINE_ENTITY_VERSIONED(Inert, 1, nullptr) DEFINE_ENTITY_VERSIONED(Inert, 1, nullptr)
@@ -11,9 +23,6 @@ void serialize(Archive& ar, NonNullPtr<Entity> entity)
// Entity fields // Entity fields
serialize(ar, Entity::kind, entity.Get()); serialize(ar, Entity::kind, entity.Get());
SERIALIZE(ar, id, entity->ID);
SERIALIZE(ar, position, entity->position);
// Derived fields // Derived fields
if (entity->derived_kind != nullptr && entity->derived != nullptr) if (entity->derived_kind != nullptr && entity->derived != nullptr)
{ {
+3 -3
View File
@@ -8,8 +8,8 @@
#include <Engine/Class.h> #include <Engine/Class.h>
#define DECLARE_ENTITY() \ #define DECLARE_ENTITY() \
Entity* base; \ Entity* base; \
static Class* kind; DECLARE_CLASS()
#define DEFINE_ENTITY_VERSIONED(entity, version, serialize_fct) \ #define DEFINE_ENTITY_VERSIONED(entity, version, serialize_fct) \
constexpr Class entityKind##entity = MakeClass(ConstString(#entity), (uint8)Entity_Type::entity, (version), \ constexpr Class entityKind##entity = MakeClass(ConstString(#entity), (uint8)Entity_Type::entity, (version), \
@@ -22,7 +22,7 @@ using EntityID = uint64_t;
struct Entity final struct Entity final
{ {
static Class* kind; DECLARE_CLASS()
EntityID ID = 0; EntityID ID = 0;
Class* derived_kind = nullptr; Class* derived_kind = nullptr;
+121 -97
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.
- **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. - **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 the class serializer's version parameter (`if (ar.loading && 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`.
--- ---
@@ -899,14 +899,14 @@ Rather than maintaining dedicated deprecation primitives or polluting C++ struct
When an asset property is deprecated, restructured, or renamed: When an asset property is deprecated, restructured, or renamed:
1. The obsolete field is completely removed from the modern C++ struct definition. 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. 2. In the entity/component serializer `serialize_fct(Archive& ar, uint16 version, void* payload)`, an `if (ar.loading && version < N)` block is added.
3. A local variable of the legacy type is declared on the stack. 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`. 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. 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. 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 ### 7.3 In-Place Migration Pattern
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. When an asset file with an older schema is loaded, `serialize(ar, cls, instance)` parses the version from the file (e.g. `; version` or `; class_version`) and passes it directly into the callback `serialize_fct(ar, version, payload)`. The serializer detects `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
@@ -919,18 +919,16 @@ struct Projectile
float Damage = 50.0f; float Damage = 50.0f;
}; };
void SerializeProjectile(Archive* arPtr, void* payload) void SerializeProjectile(Archive& ar, uint16 version, void* payload)
{ {
Assert(arPtr != nullptr);
Assert(payload != nullptr); Assert(payload != nullptr);
auto& ar = *arPtr;
auto* projectile = static_cast<Projectile*>(payload); auto* projectile = static_cast<Projectile*>(payload);
if (ar.loading) if (ar.loading)
{ {
SERIALIZE(ar, Damage, projectile->Damage); SERIALIZE(ar, Damage, projectile->Damage);
if (ar.class_version < 2) if (version < 2)
{ {
// Migration from v1: scalar 'Speed' converted to 'VelocityX' on the stack // Migration from v1: scalar 'Speed' converted to 'VelocityX' on the stack
float deprecated_speed = 0.0f; float deprecated_speed = 0.0f;
@@ -1014,9 +1012,9 @@ void SerializeProjectile(Archive* arPtr, void* payload)
#### Phase 3: Archive Struct & `serialize` Helpers #### Phase 3: Archive Struct & `serialize` Helpers
1. **Target**: `Juliet/include/Core/Common/serialization.h` 1. **Target**: `Juliet/include/Core/Common/serialization.h`
- Upgrade `struct Archive` with stream pointer, property array, `loading` flag, and versions (`base_version`, `class_version`). - Upgrade `struct Archive` with stream pointer, property array, and `loading` flag (pure I/O container decoupled from domain versions).
- Implement overloaded `serialize`, `read_prop`, and `write` for `float`, `int32`, `uint64`, `bool`, vectors, and `String`. - 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)`. - Support deprecation migration using standard `serialize` / `SERIALIZE` with local stack variables under `if (ar.loading && version < N)` in `serialize_fct`.
#### 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`
@@ -1026,18 +1024,20 @@ void SerializeProjectile(Archive* arPtr, void* payload)
2. **Target**: `Game/Data/World.h` and `Game/Data/World.cpp` 2. **Target**: `Game/Data/World.h` and `Game/Data/World.cpp`
- Implement text-based world saving and loading using `.jasset` formatting. - Implement text-based world saving and loading using `.jasset` formatting.
#### Phase 5: Comprehensive Unit Testing #### Phase 5: Comprehensive Unit Testing (Juliet Engine Layer)
1. **Target**: `Game/UnitTest/SerializationUnitTest.h` and `Game/UnitTest/SerializationUnitTest.cpp` 1. **Target**: `Juliet/src/UnitTest/SerializationUnitTest.h` and `Juliet/src/UnitTest/SerializationUnitTest.cpp`
- Add unit tests verifying parsing, round-trip serialization, defaults preservation, versioning, and deprecation. - Add self-contained unit tests in Juliet verifying parsing, round-trip serialization, defaults preservation, versioning, and deprecation.
- Use `DECLARE_CLASS()` and `DEFINE_CLASS_VERSIONED` (no dependency on Game/Entity headers).
- Hook into `Juliet/src/UnitTest/RunUnitTests.cpp` via `UnitTest::SerializationUnitTest()`.
--- ---
### 8.2 Comprehensive Unit Testing Plan (`SerializationUnitTest.cpp`) ### 8.2 Comprehensive Unit Testing Plan (`SerializationUnitTest.cpp`)
The unit test suite validates all architectural requirements without modifying engine framework code for test-specific cases. The unit test suite validates all architectural requirements directly within the Juliet engine layer without modifying framework code or depending on Game-layer entity headers. It uses `DECLARE_CLASS()` and `DEFINE_CLASS_VERSIONED` directly to test base and derived class serialization, versioning, and deprecation.
```cpp ```cpp
// Game/UnitTest/SerializationUnitTest.h // Juliet/src/UnitTest/SerializationUnitTest.h
#pragma once #pragma once
#include <Juliet.h> #include <Juliet.h>
@@ -1045,13 +1045,13 @@ The unit test suite validates all architectural requirements without modifying e
#if JULIET_DEBUG #if JULIET_DEBUG
namespace UnitTest namespace UnitTest
{ {
void RunSerializationUnitTests(); void SerializationUnitTest();
} }
#endif #endif
``` ```
```cpp ```cpp
// Game/UnitTest/SerializationUnitTest.cpp // Juliet/src/UnitTest/SerializationUnitTest.cpp
#include <UnitTest/SerializationUnitTest.h> #include <UnitTest/SerializationUnitTest.h>
#if JULIET_DEBUG #if JULIET_DEBUG
@@ -1064,40 +1064,10 @@ namespace UnitTest
#include <Core/Logging/LogTypes.h> #include <Core/Logging/LogTypes.h>
#include <Core/Memory/MemoryArena.h> #include <Core/Memory/MemoryArena.h>
#include <Core/Thread/ThreadContext.h> #include <Core/Thread/ThreadContext.h>
#include <Entity/Entity.h> #include <Engine/Class.h>
namespace UnitTest namespace UnitTest
{ {
// Test Struct for Derived Entity Testing
struct DummyVehicle
{
DECLARE_ENTITY()
float MaxSpeed = 120.0f;
int32 GearCount = 6;
uint64 ChassisUUID = 0xABCDEF0123456789ULL;
bool Turbo = true;
float PosX = 10.0f;
float PosY = 20.0f;
float PosZ = 30.0f;
};
void SerializeDummyVehicle(archive* arPtr, void* payload)
{
Assert(arPtr != nullptr);
Assert(payload != nullptr);
auto& ar = *arPtr;
auto* vehicle = static_cast<DummyVehicle*>(payload);
SERIALIZE_PROP(ar, vehicle->MaxSpeed);
SERIALIZE_PROP(ar, vehicle->GearCount);
SERIALIZE_PROP(ar, vehicle->ChassisUUID);
SERIALIZE_PROP(ar, vehicle->Turbo);
SerializeProp(ar, "Position", "Position"_crc32, vehicle->PosX, vehicle->PosY, vehicle->PosZ);
}
DEFINE_ENTITY_VERSIONED(DummyVehicle, 1, SerializeDummyVehicle);
// Test 1: Parser tokenization with whitespace, comments, and mixed line endings // Test 1: Parser tokenization with whitespace, comments, and mixed line endings
static void TestParserTokenization() static void TestParserTokenization()
{ {
@@ -1121,110 +1091,156 @@ namespace UnitTest
.Size = strlen(testContent) .Size = strlen(testContent)
}; };
ParsedTextArchive parsed = TokenizeTextArchive(temp.Arena, buffer); ParsedArchive parsed = tokenize_archive(temp.Arena, buffer);
Assert(parsed.PropertyCount == 3); Assert(parsed.property_count == 3);
auto* healthNode = FindProperty(parsed.Nodes, parsed.PropertyCount, "Health"_crc32); auto* healthNode = find_property(&parsed, "Health"_crc32);
Assert(healthNode != nullptr); Assert(healthNode != nullptr);
Assert(StringCompare(healthNode->Value, WrapString("100.500000")) == 0); Assert(StringCompare(healthNode->value, WrapString("100.500000")) == 0);
auto* nameNode = FindProperty(parsed.Nodes, parsed.PropertyCount, "Name"_crc32); auto* nameNode = find_property(&parsed, "Name"_crc32);
Assert(nameNode != nullptr); Assert(nameNode != nullptr);
Assert(StringCompare(nameNode->Value, WrapString("\"Paladin Hero\"")) == 0); Assert(StringCompare(nameNode->value, WrapString("\"Paladin Hero\"")) == 0);
auto* posNode = FindProperty(parsed.Nodes, parsed.PropertyCount, "Position"_crc32); auto* posNode = find_property(&parsed, "Position"_crc32);
Assert(posNode != nullptr); Assert(posNode != nullptr);
Assert(StringCompare(posNode->Value, WrapString("1.0 2.0 3.0")) == 0); Assert(StringCompare(posNode->value, WrapString("1.0 2.0 3.0")) == 0);
scratch_end(temp); scratch_end(temp);
LogMessage(LogCategory::Core, "TestParserTokenization passed."); LogMessage(LogCategory::Core, "TestParserTokenization passed.");
} }
// Test Struct for Default Value Testing in Juliet
struct DummyVehicle
{
DECLARE_CLASS()
float max_speed = 120.0f;
int32 gear_count = 6;
uint64 chassis_uuid = 0xABCDEF0123456789ULL;
bool turbo = true;
float pos_x = 10.0f;
};
void serialize_dummy_vehicle(Archive& ar, uint16 /*version*/, void* payload)
{
Assert(payload != nullptr);
auto* vehicle = static_cast<DummyVehicle*>(payload);
SERIALIZE(ar, max_speed, vehicle->max_speed);
SERIALIZE(ar, gear_count, vehicle->gear_count);
SERIALIZE(ar, chassis_uuid, vehicle->chassis_uuid);
SERIALIZE(ar, turbo, vehicle->turbo);
SERIALIZE(ar, pos_x, vehicle->pos_x);
}
DEFINE_CLASS_VERSIONED(DummyVehicle, 1, nullptr, serialize_dummy_vehicle)
// Test 2: Missing properties retain default struct values // Test 2: Missing properties retain default struct values
static void TestDefaultValueRetention() static void TestDefaultValueRetention()
{ {
TempArena temp = scratch_begin(nullptr, 0); TempArena temp = scratch_begin(nullptr, 0);
const char* incompleteContent = const char* incompleteContent =
"; MaxSpeed\n" "; version\n"
"180.0\n"; "1\n"
"; max_speed\n"
"180.000000\n";
ByteBuffer buffer = { ByteBuffer buffer = {
.Data = reinterpret_cast<Byte*>(const_cast<char*>(incompleteContent)), .Data = reinterpret_cast<Byte*>(const_cast<char*>(incompleteContent)),
.Size = strlen(incompleteContent) .Size = strlen(incompleteContent)
}; };
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;
DummyVehicle vehicle; DummyVehicle vehicle;
// Defaults: MaxSpeed=120, GearCount=6, Turbo=true // Defaults: max_speed=120, gear_count=6, turbo=true, pos_x=10
SerializeDummyVehicle(&ar, &vehicle); serialize(ar, DummyVehicle::kind, &vehicle);
Assert(vehicle.MaxSpeed == 180.0f); // Overwritten by archive Assert(vehicle.max_speed == 180.0f); // Overwritten by archive
Assert(vehicle.GearCount == 6); // Preserved default Assert(vehicle.gear_count == 6); // Preserved default
Assert(vehicle.Turbo == true); // Preserved default Assert(vehicle.turbo == true); // Preserved default
Assert(vehicle.PosX == 10.0f); // Preserved default Assert(vehicle.pos_x == 10.0f); // Preserved default
scratch_end(temp); scratch_end(temp);
LogMessage(LogCategory::Core, "TestDefaultValueRetention passed."); LogMessage(LogCategory::Core, "TestDefaultValueRetention passed.");
} }
// Test 3: Deprecation migration from v1 to v2 // Base test class for testing class hierarchy
struct LegacyWeapon struct DummyWeaponBase
{ {
DECLARE_ENTITY() DECLARE_CLASS()
float VelocityX = 0.0f;
float VelocityY = 0.0f; float base_damage = 25.0f;
}; };
void SerializeLegacyWeapon(Archive* arPtr, void* payload) void serialize_dummy_weapon_base(Archive& ar, uint16 /*version*/, void* payload)
{
Assert(payload != nullptr);
auto* base_weapon = static_cast<DummyWeaponBase*>(payload);
SERIALIZE(ar, base_damage, base_weapon->base_damage);
}
DEFINE_CLASS_VERSIONED(DummyWeaponBase, 1, nullptr, serialize_dummy_weapon_base)
// Derived test class for testing version migration (v1 -> v2)
struct LegacyWeapon
{
DECLARE_CLASS()
float velocity_x = 0.0f;
float velocity_y = 0.0f;
};
void serialize_legacy_weapon(Archive& ar, uint16 version, void* payload)
{ {
Assert(arPtr != nullptr);
Assert(payload != nullptr); Assert(payload != nullptr);
auto& ar = *arPtr;
auto* weapon = static_cast<LegacyWeapon*>(payload); auto* weapon = static_cast<LegacyWeapon*>(payload);
if (ar.loading) if (ar.loading)
{ {
if (ar.class_version < 2) if (version < 2)
{ {
// Migration from v1: scalar 'speed' converted to 'velocity_x' on the stack
float deprecated_speed = 0.0f; float deprecated_speed = 0.0f;
if (SERIALIZE(ar, Speed, deprecated_speed)) if (SERIALIZE(ar, speed, deprecated_speed))
{ {
weapon->VelocityX = deprecated_speed; weapon->velocity_x = deprecated_speed;
weapon->VelocityY = 0.0f; weapon->velocity_y = 0.0f;
} }
} }
else else
{ {
SERIALIZE(ar, VelocityX, weapon->VelocityX); SERIALIZE(ar, velocity_x, weapon->velocity_x);
SERIALIZE(ar, VelocityY, weapon->VelocityY); SERIALIZE(ar, velocity_y, weapon->velocity_y);
} }
} }
else else
{ {
SERIALIZE(ar, VelocityX, weapon->VelocityX); SERIALIZE(ar, velocity_x, weapon->velocity_x);
SERIALIZE(ar, VelocityY, weapon->VelocityY); SERIALIZE(ar, velocity_y, weapon->velocity_y);
} }
} }
DEFINE_ENTITY_VERSIONED(LegacyWeapon, 2, SerializeLegacyWeapon); // Inherits from DummyWeaponBase (base_class != nullptr) to verify derived class_version handling
DEFINE_CLASS_VERSIONED(LegacyWeapon, 2, &classKindDummyWeaponBase, serialize_legacy_weapon)
// Test 3: Deprecation migration from v1 to v2 via stack variable
static void TestDeprecationMigration() static void TestDeprecationMigration()
{ {
TempArena temp = scratch_begin(nullptr, 0); TempArena temp = scratch_begin(nullptr, 0);
// Simulated v1 file containing obsolete 'Speed' // Simulated v1 file containing obsolete 'speed' and class_version 1
const char* v1Content = const char* v1Content =
"; class_version\n" "; class_version\n"
"1\n" "1\n"
"; Speed\n" "; speed\n"
"75.500000\n"; "75.500000\n";
ByteBuffer buffer = { ByteBuffer buffer = {
@@ -1235,27 +1251,35 @@ namespace UnitTest
ParsedArchive parsed = tokenize_archive(temp.Arena, buffer); ParsedArchive parsed = tokenize_archive(temp.Arena, buffer);
Archive ar = {}; Archive ar = {};
ar.arena = temp.Arena; ar.arena = temp.Arena;
ar.loading = true; ar.loading = true;
ar.base = parsed; ar.base = parsed;
ar.class_version = 1;
LegacyWeapon weapon; LegacyWeapon weapon;
SerializeLegacyWeapon(&ar, &weapon); serialize(ar, LegacyWeapon::kind, &weapon);
Assert(weapon.VelocityX == 75.5f); Assert(weapon.velocity_x == 75.5f);
Assert(weapon.VelocityY == 0.0f); Assert(weapon.velocity_y == 0.0f);
scratch_end(temp); scratch_end(temp);
LogMessage(LogCategory::Core, "TestDeprecationMigration passed."); LogMessage(LogCategory::Core, "TestDeprecationMigration passed.");
} }
void RunSerializationUnitTests() // Test 4: Hierarchical type query (IsA)
static void TestClassInheritance()
{ {
LogMessage(LogCategory::Core, "=== Running Serialization & .jasset Unit Tests ==="); Assert(IsA(*LegacyWeapon::kind, DummyWeaponBase::kind));
Assert(!IsA(*DummyWeaponBase::kind, LegacyWeapon::kind));
LogMessage(LogCategory::Core, "TestClassInheritance passed.");
}
void SerializationUnitTest()
{
LogMessage(LogCategory::Core, "=== Running Serialization & .jasset Unit Tests (Juliet) ===");
TestParserTokenization(); TestParserTokenization();
TestDefaultValueRetention(); TestDefaultValueRetention();
TestDeprecationMigration(); TestDeprecationMigration();
TestClassInheritance();
LogMessage(LogCategory::Core, "=== All Serialization Unit Tests Passed Successfully ==="); LogMessage(LogCategory::Core, "=== All Serialization Unit Tests Passed Successfully ===");
} }
} // namespace UnitTest } // namespace UnitTest
+1
View File
@@ -3,6 +3,7 @@
#include <Juliet.h> #include <Juliet.h>
#define global static #define global static
#define internal static
// 1. Stringify helpers // 1. Stringify helpers
#define JULIET_STR(x) #x #define JULIET_STR(x) #x
+3 -4
View File
@@ -38,10 +38,8 @@ struct Archive
{ {
Arena* arena; Arena* arena;
bool loading; bool loading;
ParsedArchive base = {}; ParsedArchive base = {};
IOStream* stream = nullptr; IOStream* stream = nullptr;
uint32 base_version = 0;
uint16 class_version = 0;
// to remove // to remove
void* base_ptr = nullptr; void* base_ptr = nullptr;
@@ -94,6 +92,7 @@ bool read_prop(Archive& /*ar*/, String value_raw, Type& value)
} }
#define SERIALIZE(ar, name, var) serialize((ar), ConstString(#name), #name##_crc32, (var)) #define SERIALIZE(ar, name, var) serialize((ar), ConstString(#name), #name##_crc32, (var))
#define SERIALIZE_SIMPLE(ar, var) SERIALIZE(ar, var, var)
template <typename Type> template <typename Type>
bool serialize(Archive& ar, String property_name, uint32 property_crc, Type& value) bool serialize(Archive& ar, String property_name, uint32 property_crc, Type& value)
+3 -1
View File
@@ -24,6 +24,8 @@ struct Class
#endif #endif
}; };
#define DECLARE_CLASS() static Class* kind;
#define DEFINE_CLASS_VERSIONED(cls, version, base_class, serialize_fct) \ #define DEFINE_CLASS_VERSIONED(cls, version, base_class, serialize_fct) \
constexpr Class classKind##cls = \ constexpr Class classKind##cls = \
MakeClass(ConstString(#cls), 0, (version), (base_class), sizeof(cls), alignof(cls), (serialize_fct)); \ MakeClass(ConstString(#cls), 0, (version), (base_class), sizeof(cls), alignof(cls), (serialize_fct)); \
@@ -53,7 +55,7 @@ bool IsA(const Class& query, const Class* target);
template <typename type> template <typename type>
bool IsA(const Class& cls) bool IsA(const Class& cls)
{ {
return IsA(cls, type::StaticClass); return IsA(cls, type::kind);
} }
JULIET_API void serialize(Archive& ar, NonNullPtr<Class> cls, void* instance); JULIET_API void serialize(Archive& ar, NonNullPtr<Class> cls, void* instance);
+3 -1
View File
@@ -1,4 +1,4 @@
#include <Juliet.h> #include <Juliet.h>
#if JULIET_DEBUG #if JULIET_DEBUG
@@ -10,6 +10,7 @@ namespace UnitTest
// Forward declare the VectorUnitTest function // Forward declare the VectorUnitTest function
void VectorUnitTest(); void VectorUnitTest();
void TestMemoryArena(); void TestMemoryArena();
void SerializationUnitTest();
void RunUnitTests() void RunUnitTests()
{ {
@@ -17,6 +18,7 @@ namespace UnitTest
TestMemoryArena(); TestMemoryArena();
VectorUnitTest(); VectorUnitTest();
SerializationUnitTest();
LogMessage(LogCategory::Core, "Unit Tests Completed Successfully."); LogMessage(LogCategory::Core, "Unit Tests Completed Successfully.");
} }
+259
View File
@@ -0,0 +1,259 @@
#if JULIET_DEBUG
#include <Core/Common/CoreUtils.h>
#include <Core/Common/CRC32.h>
#include <Core/Common/serialization.h>
#include <Core/Common/String.h>
#include <Core/Logging/LogManager.h>
#include <Core/Logging/LogTypes.h>
#include <Core/Math/Vector.h>
#include <Core/Memory/MemoryArena.h>
#include <Core/Thread/ThreadContext.h>
#include <Engine/Class.h>
namespace UnitTest
{
// Test 1: Parser tokenization with whitespace, comments (# and //), and CRLF / LF line endings
internal void test_parser_tokenization()
{
TempArena temp = scratch_begin(nullptr, 0);
String test_content = ConstString("# Header Comment\r\n"
"// Secondary comment\n"
"\n"
"; Health\r\n"
" 100.500000 \r\n"
"\n"
"; Name\n"
"\"Paladin Hero\"\n"
"\n"
"; Position\r\n"
"1.0 2.0 3.0 4.0\r\n");
ByteBuffer buffer = { .Data = (Byte*)test_content.Str, .Size = test_content.Size };
ParsedArchive parsed = tokenize_archive(temp.Arena, buffer);
Assert(parsed.property_count == 3);
auto* health_node = find_property(&parsed, "Health"_crc32);
Assert(health_node != nullptr);
Assert(StringCompare(health_node->value, WrapString("100.500000")) == 0);
auto* name_node = find_property(&parsed, "Name"_crc32);
Assert(name_node != nullptr);
Assert(StringCompare(name_node->value, WrapString("\"Paladin Hero\"")) == 0);
auto* pos_node = find_property(&parsed, "Position"_crc32);
Assert(pos_node != nullptr);
Assert(StringCompare(pos_node->value, WrapString("1.0 2.0 3.0 4.0")) == 0);
scratch_end(temp);
LogMessage(LogCategory::Core, "test_parser_tokenization passed.");
}
// Test 2: Missing properties retain default struct values
struct DummyVehicle
{
DECLARE_CLASS()
float max_speed = 120.0f;
int32 gear_count = 6;
uint64 chassis_uuid = 0xABCDEF0123456789ULL;
bool turbo = true;
float pos_x = 10.0f;
};
internal void serialize_dummy_vehicle(Archive& ar, uint16 /*version*/, void* payload)
{
Assert(payload != nullptr);
auto* vehicle = static_cast<DummyVehicle*>(payload);
SERIALIZE(ar, max_speed, vehicle->max_speed);
SERIALIZE(ar, gear_count, vehicle->gear_count);
SERIALIZE(ar, chassis_uuid, vehicle->chassis_uuid);
SERIALIZE(ar, turbo, vehicle->turbo);
SERIALIZE(ar, pos_x, vehicle->pos_x);
}
DEFINE_CLASS_VERSIONED(DummyVehicle, 1, nullptr, serialize_dummy_vehicle)
internal void test_default_value_retention()
{
TempArena temp = scratch_begin(nullptr, 0);
String incomplete_content = ConstString("; version\n"
"1\n"
"; max_speed\n"
"180.000000\n");
ByteBuffer buffer = { .Data = (Byte*)incomplete_content.Str, .Size = incomplete_content.Size };
ParsedArchive parsed = tokenize_archive(temp.Arena, buffer);
Archive ar = {};
ar.arena = temp.Arena;
ar.loading = true;
ar.base = parsed;
DummyVehicle vehicle;
// Defaults: max_speed=120, gear_count=6, turbo=true, pos_x=10
serialize(ar, DummyVehicle::kind, &vehicle);
Assert(vehicle.max_speed == 180.0f); // Overwritten by archive
Assert(vehicle.gear_count == 6); // Preserved default
Assert(vehicle.turbo == true); // Preserved default
Assert(vehicle.pos_x == 10.0f); // Preserved default
scratch_end(temp);
LogMessage(LogCategory::Core, "test_default_value_retention passed.");
}
// Base test class for testing hierarchy
struct DummyWeaponBase
{
DECLARE_CLASS()
float base_damage = 25.0f;
};
internal void serialize_dummy_weapon_base(Archive& ar, uint16 /*version*/, void* payload)
{
Assert(payload != nullptr);
auto* base_weapon = static_cast<DummyWeaponBase*>(payload);
SERIALIZE(ar, base_damage, base_weapon->base_damage);
}
DEFINE_CLASS_VERSIONED(DummyWeaponBase, 1, nullptr, serialize_dummy_weapon_base)
// Derived test class for testing version migration (v1 -> v2)
struct LegacyWeapon
{
DECLARE_CLASS()
float velocity_x = 0.0f;
float velocity_y = 0.0f;
};
internal void serialize_legacy_weapon(Archive& ar, uint16 version, void* payload)
{
Assert(payload != nullptr);
auto* weapon = static_cast<LegacyWeapon*>(payload);
if (ar.loading)
{
if (version < 2)
{
// Migration from v1: scalar 'speed' converted to 'velocity_x' on the stack
float deprecated_speed = 0.0f;
if (SERIALIZE(ar, speed, deprecated_speed))
{
weapon->velocity_x = deprecated_speed;
weapon->velocity_y = 0.0f;
}
}
else
{
SERIALIZE(ar, velocity_x, weapon->velocity_x);
SERIALIZE(ar, velocity_y, weapon->velocity_y);
}
}
else
{
SERIALIZE(ar, velocity_x, weapon->velocity_x);
SERIALIZE(ar, velocity_y, weapon->velocity_y);
}
}
// Inherits from DummyWeaponBase (base_class != nullptr) to verify derived class_version handling
DEFINE_CLASS_VERSIONED(LegacyWeapon, 2, &classKindDummyWeaponBase, serialize_legacy_weapon)
// Test 3: Deprecation migration from v1 to v2 via stack variable
internal void test_deprecation_migration()
{
TempArena temp = scratch_begin(nullptr, 0);
// Simulated v1 file containing obsolete 'speed' and class_version 1
String v1_content = ConstString("; class_version\n"
"1\n"
"; speed\n"
"75.500000\n");
ByteBuffer buffer = { .Data = (Byte*)v1_content.Str, .Size = v1_content.Size };
ParsedArchive parsed = tokenize_archive(temp.Arena, buffer);
Archive ar = {};
ar.arena = temp.Arena;
ar.loading = true;
ar.base = parsed;
LegacyWeapon weapon;
serialize(ar, LegacyWeapon::kind, &weapon);
Assert(weapon.velocity_x == 75.5f);
Assert(weapon.velocity_y == 0.0f);
scratch_end(temp);
LogMessage(LogCategory::Core, "test_deprecation_migration passed.");
}
// Test 4: Hierarchical type query (IsA)
internal void test_class_inheritance()
{
Assert(IsA<DummyWeaponBase>(*LegacyWeapon::kind));
Assert(!IsA<LegacyWeapon>(*DummyWeaponBase::kind));
Assert(!IsA<DummyWeaponBase>(*DummyVehicle::kind));
LogMessage(LogCategory::Core, "test_class_inheritance passed.");
}
// Test 5: String and Vector4 read_prop serialization
internal void test_string_and_vector4()
{
TempArena temp = scratch_begin(nullptr, 0);
String content = ConstString("; title\n"
"\"Sword of Destiny\"\n"
"; position\n"
"10.0 20.0 30.0 1.0\n");
ByteBuffer buffer = { .Data = (Byte*)content.Str, .Size = content.Size };
ParsedArchive parsed = tokenize_archive(temp.Arena, buffer);
Archive ar = {};
ar.arena = temp.Arena;
ar.loading = true;
ar.base = parsed;
String title = {};
Vector4 position = {};
bool read_title = SERIALIZE_SIMPLE(ar, title);
bool read_pos = SERIALIZE_SIMPLE(ar, position);
Assert(read_title);
Assert(StringCompare(title, WrapString("Sword of Destiny")) == 0);
Assert(read_pos);
Assert(position.x == 10.0f);
Assert(position.y == 20.0f);
Assert(position.z == 30.0f);
Assert(position.w == 1.0f);
scratch_end(temp);
LogMessage(LogCategory::Core, "test_string_and_vector4 passed.");
}
void SerializationUnitTest()
{
LogMessage(LogCategory::Core, "=== Running Serialization & .jasset Unit Tests (Juliet) ===");
test_parser_tokenization();
test_default_value_retention();
test_deprecation_migration();
test_class_inheritance();
test_string_and_vector4();
LogMessage(LogCategory::Core, "=== All Serialization Unit Tests Passed Successfully ===");
}
} // namespace UnitTest
#endif