# Juliet Game Engine: Technical Specification & Implementation Plan # 04: Entity Templates ("Blueprints / Prefabs") & Delta Inheritance - **Document ID**: ENG-PLAN-04 - **Component**: Game Architecture / Entity Component System / Asset Pipeline / Editor - **Author**: Senior Engine Architect - **Status**: Draft / Approved for Implementation - **Target Engine**: Juliet Engine (Milestone: 3D Platformer / World System) - **Target File**: `w:\Classified\Juliet\Game\Plans\04_Entity_Templates_And_Inheritance.md` --- ## 1. Executive Summary & Architecture Goals ### 1.1 Motivation & Context As the Juliet engine evolves toward supporting complex interactive worlds, populating scenes by manually constructing raw entity memory from hardcoded values or monolithic binary streams (`world.bin`) creates severe scalability bottlenecks. Designers and environment artists require reusable entity archetypes—such as foliage, dynamic destructibles, hazards, enemies, and interactive props—that can be authored once and instantiated thousands of times with custom overrides. In modern commercial engines, this pattern is foundational: - **Unreal Engine**: Blueprint classes (`.uasset`) serve as class archetypes, instanced as Actors with per-instance component property overrides. - **Unity**: Prefabs (`.prefab`) serve as asset archetypes, instanced in scenes with explicit serialized property modification lists. In the **Juliet Game Engine**, this feature is formalized as **Entity Templates**. ### 1.2 Architectural Goals 1. **Unified File Format (`.jasset`)**: Templates and world entity instances must share the exact same human-readable, version-control-friendly Key-Value (KV) file format. A template is simply an entity `.jasset` file located within `Assets/Templates/`, while a world entity is a `.jasset` file located within `Assets/Worlds//Entities/` that references a parent template via metadata. 2. **Zero Format Duplication**: There must be no separate "prefab file format" versus "entity instance file format". The same serialization/deserialization code paths parse both archetypes and instances, completely eliminating format divergence, schema version desynchronization, and redundant parsing logic. 3. **Delta Property Overrides (Sparse Inheritance)**: Instances only serialize the fields that intentionally deviate from their template archetype. Any property omitted in the instance `.jasset` retains the exact bitwise value defined by the template archetype. 4. **Zero Runtime Inheritance Overhead**: No virtual table lookups, no runtime inheritance trees, and no pointer chasing at tick time. At instantiation time, the template archetype's memory footprint is blitted into contiguous engine arrays (`EntityManager::by_type`), and instance delta overrides are parsed directly into that memory block. Once instantiated, an entity created from a template executes with identical CPU cache locality and zero performance penalty compared to a hardcoded entity. 5. **Arena-Centric Memory Model**: All template assets are loaded into a dedicated, isolated `TemplateArena`. World entity instances allocate their runtime state out of `World::WorldArena` and `EntityManager::by_type[kind].arena`. Scratch computations during parsing utilize thread-local scratch arenas (`scratch_begin` / `scratch_end`). No heap allocation (`malloc`, `calloc`, `new`) is permitted. 6. **Strict Engine Conformance**: - Zero exceptions (`noexcept` by design). - Strict warning compliance (`-Wall -Wextra -Werror` / `/W4 /WX`). - Strict explicit casting (`static_cast`, `reinterpret_cast`; C-style casts strictly prohibited). - Universal `[[nodiscard]]` on all value-returning queries and allocators. - Consistent naming (`CamelCase` for types, functions, and member variables). - Comprehensive precondition assertions via `Assert`. - Mandatory curly braces `{}` across all control flow statements. --- ## 2. The Template Linking Mechanism ### 2.1 File Format Specification (`.jasset`) All templates and entities in Juliet use the `.jasset` text format. The format consists of: - **Comment Lines**: Lines starting with `#` or `//` are treated as comments and ignored. - **Metadata Directives**: Lines starting with `; ` represent engine-level structural metadata (e.g., entity type, template references, versioning). - **Key-Value Pairs**: Key name followed by a colon and a space (`: `). ```ini # ============================================================================== # Assets/Templates/RockLarge.jasset # Entity Archetype: Large Mossy Rock # ============================================================================== ; entity_type: Inert Position: 0.0, 0.0, 0.0 MeshAsset: Assets/Meshes/Rock_01.obj Scale: 1.0, 1.0, 1.0 Mass: 250.0 IsDestructible: true Health: 100.0 ``` ### 2.2 The `; template` Directive When an entity instance is authored for a world, it specifies its archetype via the `; template` directive: ```ini # ============================================================================== # Assets/Worlds/Level01/Entities/RockLarge_042.jasset # Instance: Rock Large #042 in Level 01 # ============================================================================== ; template: Assets/Templates/RockLarge.jasset Position: 142.5, 12.0, -84.2 Scale: 1.4, 1.4, 1.4 Health: 50.0 ``` Notice the power of sparse delta overrides in this format: - `Position` is set to the instance's unique world coordinates. - `Scale` is enlarged to $1.4\times$. - `Health` is damaged down to $50.0$. - Properties omitted—`MeshAsset`, `Mass`, and `IsDestructible`—are not duplicated in the file. They automatically inherit the authoritative values from `RockLarge.jasset`. ### 2.3 Relative Path vs CRC Identifier To balance human-readability in source control with blazing runtime lookup speeds: - **Asset Authoring & Storage**: Files store canonical workspace-relative paths (e.g., `Assets/Templates/RockLarge.jasset`). - **Runtime Representation**: The engine computes a 32-bit CRC (`crc32`) of the normalized relative path string. - Runtime lookup into `EntityTemplateCache` operates via `uint32 TemplateCrc`. - In `JULIET_DEBUG` builds, the original `String TemplatePath` is retained within the cached struct for diagnostic logging, error messages, and inspector UI display. ```cpp constexpr uint32 kInvalidTemplateCrc = 0; [[nodiscard]] inline uint32 HashTemplatePath(String path) { Assert(IsValid(path)); return crc32(path.Str, path.Size); } ``` ### 2.4 Standalone Entities vs Templated Instances The engine architecture seamlessly unifies two entity categories: | Entity Category | `; template` Directive Present? | Memory Initialization Source | Primary Use Case | | :--- | :--- | :--- | :--- | | **Standalone Entity** | **No** | Zero-initialized memory block (`ArenaPushStruct` / `MemSet`). All properties must be specified in the instance `.jasset`. | Unique, one-off actors (e.g., Level Script Trigger, Primary Player Spawn Marker, Boss Controller). | | **Templated Instance** | **Yes** | Blitted directly from `CachedTemplate::DefaultDerivedMemory`. Instance `.jasset` only specifies delta property overrides. | Reusable archetypes (e.g., environmental props, foliage, pickups, enemy minions, projectiles). | --- ## 3. In-Engine Template Caching ### 3.1 Memory Layout & `TemplateArena` Isolation Reading and parsing text files from storage is orders of magnitude slower than memory copying. In a world containing 10,000 instances of `GrassClump` and 2,000 instances of `RockLarge`, disk I/O and text tokenization must occur **exactly once per archetype**. To achieve zero memory fragmentation and eliminate dynamic heap allocations: 1. The engine instantiates an isolated `TemplateArena` at subsystem startup. 2. When a template is requested, the system checks the `EntityTemplateCache`. 3. If not cached, the template `.jasset` file is loaded from disk into temporary scratch memory, parsed, and baked into a contiguous binary archetype memory snapshot inside `TemplateArena`. 4. The parsed archetype snapshot remains resident in `TemplateArena` for the lifetime of the application or world session. ``` +---------------------------------------------------------------------------------+ | TemplateArena | +---------------------------------------------------------------------------------+ | [CachedTemplate #0] | | - TemplateCrc: 0x9A4B12F0 ("Assets/Templates/RockLarge.jasset") | | - EntityKind: Pointer to Inert::Kind | | - DefaultBase: { ID=0, Kind=Inert::Kind, Derived=nullptr, X=0, Y=0, Z=0 } | | - DefaultDerivedMemory: [ sizeof(Inert) binary snapshot: MeshInstance=... ] | +---------------------------------------------------------------------------------+ | [CachedTemplate #1] | | - TemplateCrc: 0x4D2E88C1 ("Assets/Templates/CoinPickup.jasset") | | - EntityKind: Pointer to Collectible::Kind | | - DefaultBase: { ID=0, Kind=Collectible::Kind, ... } | | - DefaultDerivedMemory: [ sizeof(Collectible) binary snapshot: Value=100 ] | +---------------------------------------------------------------------------------+ | ... Free Arena Capacity for Additional Archetypes ... | +---------------------------------------------------------------------------------+ ``` ### 3.2 Data Structures The template caching infrastructure is defined with explicit, strictly-typed C++ structures adhering to Juliet guidelines: ```cpp #pragma once #include #include #include #include #include #include #include #include struct CachedTemplate { uint32 TemplateCrc = 0; Class* EntityKind = nullptr; Entity DefaultBase = {}; void* DefaultDerivedMemory = nullptr; size_t DerivedMemorySize = 0; #if JULIET_DEBUG String SourcePath = {}; #endif }; constexpr size_t kMaxCachedTemplates = 1024; struct EntityTemplateCache { Arena* CacheArena = nullptr; VectorArena Templates; }; // Subsystem API [[nodiscard]] EntityTemplateCache* InitEntityTemplateCache(NonNullPtr parentArena); void ShutdownEntityTemplateCache(NonNullPtr cache); [[nodiscard]] CachedTemplate* GetOrLoadTemplate(NonNullPtr cache, String relativePath); [[nodiscard]] CachedTemplate* FindCachedTemplate(NonNullPtr cache, uint32 templateCrc); void InvalidateTemplateCache(NonNullPtr cache); ``` ### 3.3 Loading Pipeline: Disk to Archetype Memory Snapshot When `GetOrLoadTemplate` is invoked with a relative path: 1. **Hash & Probe**: Compute `crc32` of `relativePath`. Search `cache->Templates` for an existing entry. If found, immediately return the cached pointer ($O(1)$ amortized). 2. **Scratch Allocation**: Open a scratch arena frame (`TempArena scratch = scratch_begin(nullptr, 0);`). 3. **Disk I/O**: Resolve the path via `GetAssetPath(scratch.Arena, relativePath)` and load the entire file into a raw byte buffer via `LoadFile(scratch.Arena, fullPath)`. - Precondition check: `Assert(fileBuffer.Data != nullptr);` 4. **Header Parse**: Scan the file buffer for `; entity_type: `. Look up the corresponding `Class*` via the global entity registry (`kEntity_type_class_ptr`). 5. **Archetype Memory Allocation**: Allocate `DefaultDerivedMemory` directly out of `cache->CacheArena`: ```cpp void* archetypeMem = ArenaPushSize(cache->CacheArena, entityClass->size_of, entityClass->alignment, true JULIET_DEBUG_PARAM("CachedTemplateDerived")); ``` 6. **Default Base Setup**: Initialize a local `Entity defaultBase`: ```cpp Entity defaultBase = {}; defaultBase.Kind = entityClass; defaultBase.Derived = archetypeMem; ``` 7. **Back-Pointer Linking**: Set `entity_template::Base` in the archetype memory: ```cpp auto* archetypeTemplate = reinterpret_cast(archetypeMem); archetypeTemplate->Base = &cachedEntry->DefaultBase; ``` 8. **KV Property Parsing**: Parse all key-value pairs in the template `.jasset` file and write their deserialized values directly into `archetypeMem` and `defaultBase`. 9. **Cache Insertion**: Store the fully baked `CachedTemplate` record in `cache->Templates`. 10. **Scratch Release**: Release the temporary file buffer (`scratch_end(scratch);`). --- ## 4. Instantiation & Delta Property Overrides ### 4.1 Memory Architecture: Base vs. Derived Entities In Juliet, an entity is split into two tightly coupled structures: 1. **`Entity` (Base)**: Contains universal spatial and lifecycle fields: ```cpp struct Entity final { EntityID ID = 0; Class* Kind = nullptr; DerivedType Derived = nullptr; // Points to the specialized struct float X = 0.0f; float Y = 0.0f; float Z = 0.0f; }; ``` 2. **`Derived` (Specialized Type)**: e.g., `Inert`, `Collectible`, `Player`. The first member is always `DECLARE_ENTITY()`, which expands to: ```cpp Entity* Base; // Back-pointer to the base Entity static Class* Kind; ``` Because `DerivedType` stores a back-pointer (`Base`) to `Entity`, **a shallow memory copy of an archetype invalidates this pointer**! The loading pipeline must explicitly restore this invariant. ### 4.2 The 5-Step Instantiation Pipeline When an entity instance is spawned or deserialized from a `.jasset` file, the engine executes this strict 5-step sequence: ``` [ Step 1: Pre-allocate ] - manager.Entities.PushBack(Entity{}) - ArenaPushSize(manager.by_type[Kind].arena) | v [ Step 2: Copy Archetype Memory ] - MemCopy(instanceDerivedMem, template->DefaultDerivedMemory, size_of) - instanceBase->X/Y/Z = template->DefaultBase.X/Y/Z | v [ Step 3: Fixup Base Back-Pointer ] <--- CRITICAL ARCHITECTURAL STEP - instanceDerived->Base = instanceBase - instanceBase->Derived = instanceDerived | v [ Step 4: Parse Instance KV Delta ] - Scan instance .jasset tokens - Match each key to struct property offset - Overwrite only specified properties | v [ Step 5: Finalize & Post-Init ] - Assign Unique EntityID - Register with Render/Physics systems ``` #### Step-by-Step Code Execution ```cpp [[nodiscard]] Entity* InstantiateEntityFromTemplate(EntityManager& manager, NonNullPtr templateArchetype, NonNullPtr scratchArena, String instanceKvContent) { Assert(templateArchetype->EntityKind != nullptr); Assert(templateArchetype->DefaultDerivedMemory != nullptr); Class* entityKind = templateArchetype->EntityKind; const size_t derivedSize = entityKind->size_of; const size_t derivedAlign = entityKind->alignment; // ------------------------------------------------------------------------- // STEP 1: Pre-allocate instance in EntityManager // ------------------------------------------------------------------------- Entity baseEntity = {}; baseEntity.ID = EntityManager::ID++; baseEntity.Kind = entityKind; // Push into flat base vector manager.Entities.PushBack(baseEntity); Entity* instanceBase = manager.Entities.Back(); Assert(instanceBase != nullptr); // Allocate memory in the typed array arena auto* instanceDerived = reinterpret_cast( ArenaPushSize(manager.by_type[entityKind->kind].arena, derivedSize, derivedAlign, false JULIET_DEBUG_PARAM(kEntity_type_names[entityKind->kind])) ); Assert(instanceDerived != nullptr); // Track array head if first element if (manager.by_type[entityKind->kind].array == nullptr) { manager.by_type[entityKind->kind].array = instanceDerived; } manager.by_type[entityKind->kind].count += 1; // ------------------------------------------------------------------------- // STEP 2: Copy cached template defaults into instance derived memory // ------------------------------------------------------------------------- MemCopy(instanceDerived, templateArchetype->DefaultDerivedMemory, derivedSize); // Inherit base spatial defaults instanceBase->X = templateArchetype->DefaultBase.X; instanceBase->Y = templateArchetype->DefaultBase.Y; instanceBase->Z = templateArchetype->DefaultBase.Z; // ------------------------------------------------------------------------- // STEP 3: Ensure derived back-pointer points to THIS instance's base // ------------------------------------------------------------------------- instanceDerived->Base = instanceBase; instanceBase->Derived = instanceDerived; // ------------------------------------------------------------------------- // STEP 4: Parse instance .jasset KV nodes over the memory // ------------------------------------------------------------------------- if (IsValid(instanceKvContent)) { ApplyKvDeltaOverrides(instanceBase, instanceDerived, entityKind, instanceKvContent); } // ------------------------------------------------------------------------- // STEP 5: Post-Instantiation Initialization // ------------------------------------------------------------------------- // If the entity is an Inert mesh, update its graphics transform if (entityKind->kind == ENTITY(Inert)) { auto* inert = reinterpret_cast(instanceDerived); if (inert->MeshInstance != indexMax) { SetMeshInstanceTransform(inert->MeshInstance, MatrixTranslation(instanceBase->X, instanceBase->Y, instanceBase->Z)); } } return instanceBase; } ``` ### 4.3 Key-Value Parsing & Reflection Binding To apply delta overrides, the engine maps parsed string keys to memory offsets. Juliet utilizes lightweight property reflection metadata registered on each `Class`: ```cpp enum class PropertyType : uint8 { Float, Int32, Bool, Vector3, MeshAsset, String }; struct PropertyDescriptor { String Name; size_t Offset; PropertyType Type; bool IsBaseProperty; // True if located on Entity, false if on Derived }; void ApplyKvDeltaOverrides(Entity* base, void* derived, Class* cls, String kvContent) { Assert(base != nullptr); Assert(derived != nullptr); Assert(cls != nullptr); TempArena scratch = scratch_begin(nullptr, 0); KvParser parser = InitKvParser(kvContent); KvPair pair = {}; while (NextKvPair(&parser, &pair)) { // Directives like '; template' or '; entity_type' are skipped if (pair.Key.Size > 0 && pair.Key.Str[0] == ';') { continue; } // Check base properties first if (StringCompare(pair.Key, ConstString("Position")) == 0) { Vector3 pos = ParseVector3(pair.Value); base->X = pos.X; base->Y = pos.Y; base->Z = pos.Z; continue; } // Look up property in Class reflection table const PropertyDescriptor* prop = FindPropertyDescriptor(cls, pair.Key); if (prop != nullptr) { void* targetField = static_cast(derived) + prop->Offset; switch (prop->Type) { case PropertyType::Float: { *reinterpret_cast(targetField) = ParseFloat(pair.Value); break; } case PropertyType::Int32: { *reinterpret_cast(targetField) = ParseInt32(pair.Value); break; } case PropertyType::Bool: { *reinterpret_cast(targetField) = ParseBool(pair.Value); break; } case PropertyType::MeshAsset: { String meshPath = TrimWhitespace(pair.Value); MeshAssetID meshId = LoadMesh(meshPath); *reinterpret_cast(targetField) = meshId; break; } default: { break; } } } } scratch_end(scratch); } ``` --- ## 5. Editor Workflow & Operations (Romeo / ImGui) ### 5.1 "Create Template from Entity" Workflow An artist or designer often crafts an intricate entity in the active level (configuring mesh, collider, and scale) and decides it should become a reusable archetype. #### Sequence Diagram / Workflow: 1. **User Action**: Right-click an entity in the Romeo World Editor Outliner $\rightarrow$ Select *"Convert to Template..."*. 2. **Modal Dialog**: The editor prompts for the template asset name (e.g., `SpikeTrap_Large`). 3. **Sanitize World Transform**: - The world position (`X, Y, Z`) is sanitized to origin (`0.0, 0.0, 0.0`) for the template asset. - Rotations and local scale are preserved. 4. **Serialize Archetype**: - Write `Assets/Templates/SpikeTrap_Large.jasset` containing: - `; entity_type: ` - All authored property values. 5. **Convert Live Instance**: - The selected world entity is transformed into an instance of the newly created template. - The entity is assigned the template CRC: `entity->TemplateCrc = crc32("Assets/Templates/SpikeTrap_Large.jasset")`. - When the world is saved, this entity serializes as a sparse delta referencing the template! ```cpp #if JULIET_EDITOR bool CreateTemplateFromEntity(World& world, size_t entityIndex, String templateName, NonNullPtr scratchArena) { auto& manager = *world.EntityManager; Assert(entityIndex < manager.Entities.Size()); Entity* sourceEntity = &manager.Entities[entityIndex]; Class* entityKind = sourceEntity->Kind; void* derivedMem = sourceEntity->Derived; // Format destination template path String templatePath = Format(scratchArena, "Assets/Templates/{}.jasset", CStr(templateName)); String fullDiskPath = GetAssetPath(scratchArena, templatePath); // Open IOStream for write IOStream* fileStream = IOFromFile(scratchArena, fullDiskPath, ConstString("wb")); if (fileStream == nullptr) { LogError(LogCategory::Game, "Failed to open file for template creation: %s", CStr(fullDiskPath)); return false; } // Write Header Directive IOPrintf(fileStream, "; entity_type: %s\n\n", kEntity_type_names[entityKind->kind]); // Write Origin Position IOPrintf(fileStream, "Position: 0.0, 0.0, 0.0\n"); // Write Derived Properties via Reflection Table SerializeDerivedPropertiesToKv(fileStream, entityKind, derivedMem); IOClose(fileStream); // Register with in-engine cache immediately auto* templateCache = GetGameState()->TemplateCache; if (templateCache != nullptr) { (void)GetOrLoadTemplate(templateCache, templatePath); } LogMessage(LogCategory::Game, "Successfully created template: %s", CStr(templatePath)); return true; } #endif ``` ### 5.2 "Spawn Instance from Template" Workflow 1. In the Romeo Content Browser, browse `Assets/Templates/`. 2. Drag a `.jasset` template into the 3D viewport, or click *"Spawn Template"* in the World Editor toolbar. 3. The viewport raycasts against the collision mesh/floor to compute `hitPosition`. 4. The editor calls: ```cpp CachedTemplate* archetype = GetOrLoadTemplate(templateCache, templatePath); Entity* newInstance = InstantiateEntityFromTemplate(manager, archetype, scratchArena, {}); newInstance->X = hitPosition.X; newInstance->Y = hitPosition.Y; newInstance->Z = hitPosition.Z; ``` 5. The entity is immediately live, selectable, and rendered. ### 5.3 Inspector Delta Highlighting & Property Diffing To make template inheritance intuitive, the inspector visually flags overridden properties: - **Default Property**: Rendered in standard gray text. - **Overridden Property**: Rendered in **bold bright cyan** with an undo/revert icon button `[R]`. ```cpp #if JULIET_EDITOR void RenderEntityPropertyInspector(Entity* entity, CachedTemplate* archetype) { Assert(entity != nullptr); const bool isTemplated = (archetype != nullptr); ImGui::Text("Entity ID: %llu", entity->ID); if (isTemplated) { ImGui::TextColored(ImVec2(0.4f, 0.8f, 1.0f, 1.0f), "Template: %s", archetype->SourcePath.Str); ImGui::SameLine(); if (ImGui::SmallButton("Revert All to Template")) { RevertEntityToTemplate(entity, archetype); } ImGui::Separator(); } // Iterate through properties Class* cls = entity->Kind; for (size_t i = 0; i < cls->PropertyCount; ++i) { const PropertyDescriptor& prop = cls->Properties[i]; void* instanceField = static_cast(entity->Derived) + prop.Offset; void* templateField = isTemplated ? (static_cast(archetype->DefaultDerivedMemory) + prop.Offset) : nullptr; const bool isOverridden = isTemplated && (MemCompare(instanceField, templateField, GetPropertySize(prop.Type)) != 0); if (isOverridden) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec2(0.2f, 1.0f, 1.0f, 1.0f)); } // Render editor widget (DragFloat, InputText, etc.) RenderPropertyWidget(prop, instanceField); if (isOverridden) { ImGui::PopStyleColor(); ImGui::SameLine(); ImGui::PushID(static_cast(i)); if (ImGui::SmallButton("R")) { // Revert this single property MemCopy(instanceField, templateField, GetPropertySize(prop.Type)); } ImGui::PopID(); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Revert property to template default"); } } } } #endif ``` ### 5.4 "Revert to Template" Mechanics Reverting an entire entity to its template archetype restores all properties while strictly preserving world positioning and the instance's unique `EntityID`: ```cpp void RevertEntityToTemplate(NonNullPtr instance, NonNullPtr archetype) { Assert(instance->Kind == archetype->EntityKind); Assert(archetype->DefaultDerivedMemory != nullptr); void* derivedMem = instance->Derived; const size_t derivedSize = archetype->EntityKind->size_of; // Preserve the current Base pointer Entity* basePtr = instance.Get(); // 1. Re-copy the archetype defaults MemCopy(derivedMem, archetype->DefaultDerivedMemory, derivedSize); // 2. Re-establish the Base back-pointer auto* templateDerived = reinterpret_cast(derivedMem); templateDerived->Base = basePtr; // 3. Mark visual / physics state as updated if (instance->Kind->kind == ENTITY(Inert)) { auto* inert = reinterpret_cast(derivedMem); if (inert->MeshInstance != indexMax) { SetMeshInstanceTransform(inert->MeshInstance, MatrixTranslation(basePtr->X, basePtr->Y, basePtr->Z)); } } } ``` --- ## 6. Step-by-Step Implementation Roadmap ``` +-----------------------------------------------------------------------------+ | Phase 1: Core KV Parser & File Serialization Architecture | | - Fast, zero-allocation Key-Value streaming parser | | - Property reflection table integration on Class struct | +-----------------------------------------------------------------------------+ | v +-----------------------------------------------------------------------------+ | Phase 2: Template Cache Subsystem (`EntityTemplateCache`) | | - Dedicated TemplateArena initialization | | - Archetype loading, CRC hashing, and resident memory snapshot baking | +-----------------------------------------------------------------------------+ | v +-----------------------------------------------------------------------------+ | Phase 3: Instantiation & Delta Overrides in `EntityManager` | | - Implement 5-Step Instantiation Pipeline | | - Derived back-pointer fixup validation | | - Sparse delta serialization for world saving | +-----------------------------------------------------------------------------+ | v +-----------------------------------------------------------------------------+ | Phase 4: Romeo Editor UI & Inspector Workflow | | - Template browser window in ImGui | | - "Create Template from Entity" context menu | | - Inspector delta highlighting and per-property "Revert" action | +-----------------------------------------------------------------------------+ | v +-----------------------------------------------------------------------------+ | Phase 5: Production Verification & Non-Invasive Unit Testing Suite | | - Comprehensive test cases for cache, delta inheritance, and memory safety | +-----------------------------------------------------------------------------+ ``` ### Detailed Milestone Tasks | Phase | Target Files | Objective / Deliverable | Success Criteria | | :--- | :--- | :--- | :--- | | **Phase 1** | `Juliet/include/Engine/KvParser.h`
`Juliet/src/Engine/KvParser.cpp`
`Juliet/include/Engine/Class.h` | Build a zero-allocation streaming Key-Value parser operating entirely over `String` slices and `Arena*`. Add property descriptor tables to `Class`. | Parses full `.jasset` buffer in $<50\mu\text{s}$ without dynamic allocations. Zero exceptions. | | **Phase 2** | `Game/Entity/TemplateCache.h`
`Game/Entity/TemplateCache.cpp` | Implement `EntityTemplateCache`, `CachedTemplate`, and on-demand disk loader backed by `TemplateArena`. | Repeated loads of same template return cached pointer in $O(1)$ time without disk reads. | | **Phase 3** | `Game/Entity/EntityManager.h`
`Game/Entity/EntityManager.cpp`
`Game/Data/World.cpp` | Implement `InstantiateEntityFromTemplate` using the 5-step pipeline. Update world save/load to serialize sparse deltas when `; template` is present. | Instantiated entities correctly retain archetype defaults while applying deltas. `Base` pointer is guaranteed valid. | | **Phase 4** | `Game/Data/World.cpp`
`Game/Debug/WorldEditorUI.cpp` | Add ImGui widgets for template authoring, template instantiation drag-and-drop, delta highlighting, and "Revert" actions. | Designer can convert an entity to a template and revert modified properties with instant visual update. | | **Phase 5** | `Game/UnitTest/TemplateUnitTest.h`
`Game/UnitTest/TemplateUnitTest.cpp` | Author non-invasive unit tests validating KV parsing, caching, delta override correctness, back-pointer fixup, and revert operations. | 100% test pass rate with zero memory leaks and all assertions satisfied under `/WX`. | --- ## 7. Production-Grade Unit Testing Plan ### 7.1 Testing Philosophy & Non-Invasive Framework Rules In accordance with Juliet engine guidelines: - **No framework pollution**: Unit tests must not introduce test-only `#ifdef` branches or dummy parameters into production engine systems. - **Isolated test arenas**: All tests create their own temporary scratch arena or sub-arena and release it upon completion. - **Deterministic verification**: Every assumption (data alignment, pointer fixups, delta override values) is verified through explicit `Assert` statements. ### 7.2 Header Specification (`Game/UnitTest/TemplateUnitTest.h`) ```cpp #pragma once #include #if JULIET_DEBUG namespace UnitTest { void RunTemplateAndInheritanceUnitTests(); } #endif ``` ### 7.3 Complete Unit Test Suite Implementation (`Game/UnitTest/TemplateUnitTest.cpp`) ```cpp #include #if JULIET_DEBUG #include #include #include #include #include #include #include #include namespace UnitTest { // ========================================================================= // Test 1: Key-Value Parser Verification // ========================================================================= static void TestKvParser(NonNullPtr testArena) { LogMessage(LogCategory::Game, "[UnitTest] Starting TestKvParser..."); const char* sampleKv = "; entity_type: Inert\n" "# This is a comment\n" "Position: 10.5, -20.0, 30.25\n" "Scale: 2.0, 2.0, 2.0\n" "MeshAsset: Assets/Meshes/Cube.obj\n" "IsActive: true\n"; KvParser parser = InitKvParser(WrapString(sampleKv)); KvPair pair = {}; // 1. Directive Assert(NextKvPair(&parser, &pair)); Assert(StringCompare(pair.Key, ConstString("; entity_type")) == 0); Assert(StringCompare(pair.Value, ConstString("Inert")) == 0); // 2. Position Assert(NextKvPair(&parser, &pair)); Assert(StringCompare(pair.Key, ConstString("Position")) == 0); Vector3 pos = ParseVector3(pair.Value); Assert(pos.X == 10.5f); Assert(pos.Y == -20.0f); Assert(pos.Z == 30.25f); // 3. Scale Assert(NextKvPair(&parser, &pair)); Assert(StringCompare(pair.Key, ConstString("Scale")) == 0); // 4. MeshAsset Assert(NextKvPair(&parser, &pair)); Assert(StringCompare(pair.Key, ConstString("MeshAsset")) == 0); Assert(StringCompare(pair.Value, ConstString("Assets/Meshes/Cube.obj")) == 0); // 5. IsActive Assert(NextKvPair(&parser, &pair)); Assert(StringCompare(pair.Key, ConstString("IsActive")) == 0); Assert(ParseBool(pair.Value) == true); // End of stream Assert(!NextKvPair(&parser, &pair)); LogMessage(LogCategory::Game, "[UnitTest] TestKvParser PASSED."); } // ========================================================================= // Test 2: Template Cache & Single-Load Invariant // ========================================================================= static void TestTemplateCache(NonNullPtr testArena) { LogMessage(LogCategory::Game, "[UnitTest] Starting TestTemplateCache..."); EntityTemplateCache* cache = InitEntityTemplateCache(testArena); Assert(cache != nullptr); String templatePath = ConstString("Assets/Templates/TestRock.jasset"); // First load: loads and caches CachedTemplate* firstLoad = GetOrLoadTemplate(cache, templatePath); Assert(firstLoad != nullptr); Assert(firstLoad->TemplateCrc == HashTemplatePath(templatePath)); Assert(firstLoad->EntityKind == Inert::Kind); // Second load: must return identical cached pointer without re-allocating CachedTemplate* secondLoad = GetOrLoadTemplate(cache, templatePath); Assert(secondLoad == firstLoad); // Verify lookup by CRC CachedTemplate* crcLookup = FindCachedTemplate(cache, firstLoad->TemplateCrc); Assert(crcLookup == firstLoad); ShutdownEntityTemplateCache(cache); LogMessage(LogCategory::Game, "[UnitTest] TestTemplateCache PASSED."); } // ========================================================================= // Test 3: 5-Step Instantiation Pipeline & Derived Back-Pointer Fixup // ========================================================================= static void TestInstantiationPipeline(NonNullPtr testArena) { LogMessage(LogCategory::Game, "[UnitTest] Starting TestInstantiationPipeline..."); // Setup mock World and EntityManager World world = {}; InitWorld(&world, testArena); InitEntityManager(&world); auto& manager = *world.EntityManager; // Construct a synthetic template archetype CachedTemplate mockTemplate = {}; mockTemplate.TemplateCrc = 0xABCD1234; mockTemplate.EntityKind = Inert::Kind; mockTemplate.DefaultBase.X = 1.0f; mockTemplate.DefaultBase.Y = 2.0f; mockTemplate.DefaultBase.Z = 3.0f; Inert defaultInert = {}; defaultInert.MeshInstance = 42; // Template default mockTemplate.DefaultDerivedMemory = &defaultInert; mockTemplate.DerivedMemorySize = sizeof(Inert); // Delta content overrides Position and leaves MeshInstance unspecified String instanceKv = ConstString("Position: 100.0, 200.0, 300.0\n"); Entity* instance = InstantiateEntityFromTemplate(manager, &mockTemplate, testArena, instanceKv); Assert(instance != nullptr); // Verify Step 1 & 2: Base spatial delta applied, non-overridden derived property retained Assert(instance->X == 100.0f); Assert(instance->Y == 200.0f); Assert(instance->Z == 300.0f); auto* inertDerived = DownCast(instance); Assert(inertDerived != nullptr); Assert(inertDerived->MeshInstance == 42); // Retained from template! // Verify Step 3: CRITICAL back-pointer fixup check Assert(inertDerived->Base == instance); Assert(instance->Derived == inertDerived); ShutdownEntityManager(); ShutdownWorld(&world); LogMessage(LogCategory::Game, "[UnitTest] TestInstantiationPipeline PASSED."); } // ========================================================================= // Test 4: Delta Override & Revert Functionality // ========================================================================= static void TestDeltaOverrideAndRevert(NonNullPtr testArena) { LogMessage(LogCategory::Game, "[UnitTest] Starting TestDeltaOverrideAndRevert..."); World world = {}; InitWorld(&world, testArena); InitEntityManager(&world); auto& manager = *world.EntityManager; CachedTemplate mockTemplate = {}; mockTemplate.TemplateCrc = 0x11223344; mockTemplate.EntityKind = Inert::Kind; mockTemplate.DefaultBase.X = 0.0f; mockTemplate.DefaultBase.Y = 0.0f; mockTemplate.DefaultBase.Z = 0.0f; Inert defaultInert = {}; defaultInert.MeshInstance = 100; mockTemplate.DefaultDerivedMemory = &defaultInert; mockTemplate.DerivedMemorySize = sizeof(Inert); // Instantiate with delta String instanceKv = ConstString("MeshInstance: 999\nPosition: 5.0, 5.0, 5.0\n"); Entity* instance = InstantiateEntityFromTemplate(manager, &mockTemplate, testArena, instanceKv); auto* inertDerived = DownCast(instance); Assert(inertDerived->MeshInstance == 999); // Delta applied // Execute Revert to Template RevertEntityToTemplate(instance, &mockTemplate); // Verify property restored to template default Assert(inertDerived->MeshInstance == 100); // Verify world position is preserved across revert Assert(instance->X == 5.0f); Assert(instance->Y == 5.0f); Assert(instance->Z == 5.0f); // Verify back-pointer invariant preserved after revert Assert(inertDerived->Base == instance); ShutdownEntityManager(); ShutdownWorld(&world); LogMessage(LogCategory::Game, "[UnitTest] TestDeltaOverrideAndRevert PASSED."); } // ========================================================================= // Master Runner // ========================================================================= void RunTemplateAndInheritanceUnitTests() { TempArena scratch = scratch_begin(nullptr, 0); TestKvParser(scratch.Arena); TestTemplateCache(scratch.Arena); TestInstantiationPipeline(scratch.Arena); TestDeltaOverrideAndRevert(scratch.Arena); scratch_end(scratch); LogMessage(LogCategory::Game, "[UnitTest] All Entity Template & Inheritance tests PASSED successfully."); } } #endif