Files
Juliet/Game/Plans/04_Entity_Templates_And_Inheritance.md
T

41 KiB

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/<WorldName>/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 (<Key>: <Value>).
# ==============================================================================
# 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:

# ==============================================================================
# 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.
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:

#pragma once

#include <Core/Common/CoreTypes.h>
#include <Core/Common/CoreUtils.h>
#include <Core/Common/NonNullPtr.h>
#include <Core/Common/String.h>
#include <Core/Container/Vector.h>
#include <Core/Memory/MemoryArena.h>
#include <Engine/Class.h>
#include <Entity/Entity.h>

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<CachedTemplate, kMaxCachedTemplates> Templates;
};

// Subsystem API
[[nodiscard]] EntityTemplateCache* InitEntityTemplateCache(NonNullPtr<Arena> parentArena);
void                               ShutdownEntityTemplateCache(NonNullPtr<EntityTemplateCache> cache);
[[nodiscard]] CachedTemplate*      GetOrLoadTemplate(NonNullPtr<EntityTemplateCache> cache, String relativePath);
[[nodiscard]] CachedTemplate*      FindCachedTemplate(NonNullPtr<EntityTemplateCache> cache, uint32 templateCrc);
void                               InvalidateTemplateCache(NonNullPtr<EntityTemplateCache> 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: <TypeName>. 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:
    void* archetypeMem = ArenaPushSize(cache->CacheArena,
                                       entityClass->size_of,
                                       entityClass->alignment,
                                       true JULIET_DEBUG_PARAM("CachedTemplateDerived"));
    
  6. Default Base Setup: Initialize a local Entity defaultBase:
    Entity defaultBase = {};
    defaultBase.derived_kind = entityClass;
    defaultBase.derived      = archetypeMem;
    
  7. Back-Pointer Linking: Set entity_template::base in the archetype memory:
    auto* archetypeTemplate = reinterpret_cast<entity_template*>(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:
    struct Entity final
    {
        DECLARE_CLASS()
    
        EntityID    ID           = 0;
        Class*      derived_kind = nullptr;
        DerivedType derived      = nullptr; // Points to the specialized struct
        Vector4     position     = {};
        bool        is_dirty     = false;
    };
    
  2. Derived (Specialized Type): e.g., Inert, Collectible, Player. The first member is always DECLARE_ENTITY(), which expands to:
    Entity* base;       // Back-pointer to the base Entity
    DECLARE_CLASS()     // static Class* kind;
    

Because DerivedType stores a back-pointer (base) to Entity, a shallow memory copy of an archetype invalidates this pointer! The loading pipeline must explicitly restore this invariant.

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

[[nodiscard]] Entity* InstantiateEntityFromTemplate(EntityManager& manager,
                                                    NonNullPtr<const CachedTemplate> templateArchetype,
                                                    NonNullPtr<Arena> 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<entity_template*>(
        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<Inert*>(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:

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 || StringCompare(pair.Key, ConstString("position")) == 0)
        {
            Vector3 pos = ParseVector3(pair.Value);
            base->position.x = pos.x;
            base->position.y = pos.y;
            base->position.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<uint8*>(derived) + prop->Offset;
            switch (prop->Type)
            {
                case PropertyType::Float:
                {
                    *reinterpret_cast<float*>(targetField) = ParseFloat(pair.Value);
                    break;
                }
                case PropertyType::Int32:
                {
                    *reinterpret_cast<int32*>(targetField) = ParseInt32(pair.Value);
                    break;
                }
                case PropertyType::Bool:
                {
                    *reinterpret_cast<bool*>(targetField) = ParseBool(pair.Value);
                    break;
                }
                case PropertyType::MeshAsset:
                {
                    String meshPath = TrimWhitespace(pair.Value);
                    MeshAssetID meshId = LoadMesh(meshPath);
                    *reinterpret_cast<MeshAssetID*>(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: <KindName>
      • 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!
#if JULIET_EDITOR
bool CreateTemplateFromEntity(World& world,
                              size_t entityIndex,
                              String templateName,
                              NonNullPtr<Arena> scratchArena)
{
    auto& manager = *world.EntityManager;
    Assert(entityIndex < manager.Entities.Size());

    Entity* sourceEntity = &manager.Entities[entityIndex];
    Class* entityKind = sourceEntity->derived_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:
    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].
#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->derived_kind;
    for (size_t i = 0; i < cls->PropertyCount; ++i)
    {
        const PropertyDescriptor& prop = cls->Properties[i];
        void* instanceField = static_cast<uint8*>(entity->derived) + prop.Offset;
        void* templateField = isTemplated ? (static_cast<uint8*>(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<int>(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:

void RevertEntityToTemplate(NonNullPtr<Entity> instance, NonNullPtr<const CachedTemplate> archetype)
{
    Assert(instance->derived_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<entity_template*>(derivedMem);
    templateDerived->base = basePtr;

    // 3. Mark visual / physics state as updated
    if (instance->derived_kind->kind == ENTITY(Inert))
    {
        auto* inert = reinterpret_cast<Inert*>(derivedMem);
        if (inert->MeshInstance != indexMax)
        {
            SetMeshInstanceTransform(inert->MeshInstance,
                                     MatrixTranslation(basePtr->position.x, basePtr->position.y, basePtr->position.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)

#pragma once

#include <Juliet.h>

#if JULIET_DEBUG

namespace UnitTest
{
    void RunTemplateAndInheritanceUnitTests();
}

#endif

7.3 Complete Unit Test Suite Implementation (Game/UnitTest/TemplateUnitTest.cpp)

#include <UnitTest/TemplateUnitTest.h>

#if JULIET_DEBUG

#include <Core/Common/CoreUtils.h>
#include <Core/Logging/LogManager.h>
#include <Core/Memory/MemoryArena.h>
#include <Core/Thread/ThreadContext.h>
#include <Data/World.h>
#include <Entity/Entity.h>
#include <Entity/EntityManager.h>
#include <Entity/TemplateCache.h>

namespace UnitTest
{
    // =========================================================================
    // Test 1: Key-Value Parser Verification
    // =========================================================================
    static void TestKvParser(NonNullPtr<Arena> 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<Arena> 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<Arena> 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->position.x == 100.0f);
        Assert(instance->position.y == 200.0f);
        Assert(instance->position.z == 300.0f);

        auto* inertDerived = DownCast<Inert>(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<Arena> 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<Inert>(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->position.x == 5.0f);
        Assert(instance->position.y == 5.0f);
        Assert(instance->position.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