Files
cursebreaker-parser-rust/ROADMAP.md
2026-01-02 15:11:14 +00:00

18 KiB

ROADMAP: GUID Resolution for MonoBehaviour Components

Executive Summary

The #[derive(UnityComponent)] macro system is fully functional for parsing custom Unity components. However, Unity stores custom MonoBehaviour scripts in scene/prefab files using a generic MonoBehaviour class name with a GUID reference to the actual script. To enable automatic discovery and parsing of custom components like PlaySFX from real Unity projects, we need to implement GUID → Class Name resolution.

Current Status

What's Working

  1. Procedural Macro System

    • #[derive(UnityComponent)] generates parsing code automatically
    • Supports all Unity primitive types (f64, String, bool, Vector3, etc.)
    • Generates UnityComponent::parse() implementation
    • Generates EcsInsertable::insert_into_world() implementation
  2. Auto-Registration via Inventory

    • Components automatically register themselves at compile time
    • build_world_from_documents() discovers and registers all custom components
    • No manual modification of builder.rs needed
  3. Type Filtering System

    • TypeFilter allows selective parsing for performance
    • parse_with_types! macro for ergonomic type selection
    • Works with both Unity types and custom types
  4. ECS Integration

    • Custom components insert into Sparsey ECS world
    • Components can be queried via world.borrow::<PlaySFX>()
    • Full integration with existing Transform, GameObject, etc.

What's Missing

MonoBehaviour GUID Resolution: Unity scene/prefab files store custom scripts like this:

--- !u!114 &1234567
MonoBehaviour:              # ⚠️ Generic class name, not "PlaySFX"
  m_GameObject: {fileID: 890}
  m_Script: {fileID: 11500000, guid: 091c537484687e9419460cdcd7038234, type: 3}  # 👈 Actual type
  volume: 1.0
  startTime: 0.0
  endTime: 5.0
  isLoop: 0

Problem: When parsing, we see class_name = "MonoBehaviour", so we can't match it to the PlaySFX component registration.

Solution: Resolve the m_Script GUID to discover the actual class name "PlaySFX".

The GUID Resolution Feature

How Unity GUID Resolution Works

  1. Every asset has a .meta file alongside it with a unique GUID:

    Assets/Scripts/PlaySFX.cs       # The actual C# script
    Assets/Scripts/PlaySFX.cs.meta  # Contains GUID
    
  2. The .meta file contains the GUID:

    fileFormatVersion: 2
    guid: 091c537484687e9419460cdcd7038234  # 👈 This is the GUID
    MonoImporter:
      serializedVersion: 2
      defaultReferences: []
      executionOrder: 0
    
  3. MonoBehaviour references the GUID:

    m_Script: {fileID: 11500000, guid: 091c537484687e9419460cdcd7038234, type: 3}
    
  4. We need to map: GUID → Script Path → Class Name

    • 091c537484687e9419460cdcd7038234Assets/Scripts/PlaySFX.cs"PlaySFX"

Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│                    Unity Project Parsing                     │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  1. Scan for .meta files (*.cs.meta)                         │
│     Build GUID → Asset Path mapping                          │
│     Parse class name from .cs files                          │
│     Result: GuidResolver { guid → class_name }               │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  2. Parse Unity files (.unity, .prefab)                      │
│     When encountering MonoBehaviour:                         │
│       - Extract m_Script.guid                                │
│       - Lookup guid in GuidResolver                          │
│       - Get actual class_name (e.g., "PlaySFX")              │
│       - Match with component registry                        │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  3. Component Registration (via inventory)                   │
│     Check if class_name matches registered component         │
│     If match found:                                          │
│       - Call component.parse_and_insert()                    │
│       - Insert into ECS world                                │
└─────────────────────────────────────────────────────────────┘

Implementation Plan

Phase 1: GUID → Asset Path Resolution

Goal: Build a mapping from GUID to file path by scanning .meta files

New Module: src/parser/guid_resolver.rs

/// Resolves Unity GUIDs to asset paths
pub struct GuidResolver {
    /// Map from GUID string to asset file path
    guid_to_path: HashMap<String, PathBuf>,
}

impl GuidResolver {
    /// Scan a Unity project directory for .meta files
    pub fn from_project(project_path: &Path) -> Result<Self> {
        // 1. Find all *.meta files (recursively)
        // 2. Parse each .meta file to extract GUID
        // 3. Derive asset path from .meta path (remove .meta extension)
        // 4. Build HashMap
    }

    /// Look up a GUID to get the asset path
    pub fn resolve(&self, guid: &str) -> Option<&Path> {
        self.guid_to_path.get(guid).map(|p| p.as_path())
    }
}

Files to Create/Modify:

  • 📄 src/parser/guid_resolver.rs - New file
  • 📄 src/parser/mod.rs - Export GuidResolver
  • 📄 src/lib.rs - Re-export GuidResolver

Implementation Details:

  1. Recursively walk project directory
  2. Filter for *.cs.meta files (MonoBehaviour scripts)
  3. Parse YAML to extract guid: field
  4. Store guid → "Assets/Scripts/PlaySFX.cs" mapping

Phase 2: Class Name Extraction from C# Files

Goal: Parse .cs files to extract the class name

Approach: Simple regex/string parsing (no full C# parser needed)

impl GuidResolver {
    /// Extract class name from a C# script file
    fn extract_class_name(cs_path: &Path) -> Result<String> {
        let content = std::fs::read_to_string(cs_path)?;

        // Look for: "public class PlaySFX" or "class PlaySFX"
        // Regex: r"(?:public\s+)?class\s+(\w+)"
        // Return the captured class name
    }

    /// Resolve GUID to class name
    pub fn resolve_class_name(&self, guid: &str) -> Option<String> {
        let path = self.resolve(guid)?;
        Self::extract_class_name(path).ok()
    }
}

Files to Modify:

  • 📄 src/parser/guid_resolver.rs - Add class name extraction
  • 📄 Cargo.toml - Add regex dependency (optional, can use manual parsing)

Phase 3: MonoBehaviour Parser Enhancement

Goal: Extract m_Script GUID when parsing MonoBehaviour components

Current MonoBehaviour Parsing (builder.rs line 206):

_ => {
    // Check if this is a registered custom component
    for reg in inventory::iter::<crate::types::ComponentRegistration> {
        if reg.class_name == doc.class_name.as_str() {  // ⚠️ Won't match "MonoBehaviour"
            // ...
        }
    }
}

Enhanced MonoBehaviour Parsing:

"MonoBehaviour" => {
    // Extract m_Script GUID
    if let Some(script_ref) = yaml_helpers::get_external_ref(yaml, "m_Script") {
        let guid = script_ref.guid();

        // Resolve GUID to class name (requires access to GuidResolver)
        if let Some(class_name) = guid_resolver.resolve_class_name(guid) {
            // Now match against registered components
            for reg in inventory::iter::<crate::types::ComponentRegistration> {
                if reg.class_name == class_name.as_str() {
                    if (reg.parse_and_insert)(yaml, &ctx, world, entity) {
                        linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
                    }
                    return Ok(());
                }
            }
        }
    }

    // Fall back to generic MonoBehaviour warning
    eprintln!("Warning: Skipping unknown MonoBehaviour");
}

Files to Modify:

  • 📄 src/ecs/builder.rs - Add MonoBehaviour case, pass GuidResolver
  • 📄 src/types/references.rs - Ensure ExternalRef has guid() method
  • 📄 src/types/component.rs - Add get_external_ref to yaml_helpers (if not exists)

Challenge: How to pass GuidResolver to build_world_from_documents()?

Option A: Add parameter (breaking change)

pub fn build_world_from_documents(
    documents: Vec<RawDocument>,
    guid_resolver: Option<&GuidResolver>,  // 👈 New parameter
) -> Result<(World, HashMap<FileID, Entity>)>

Option B: Store in ComponentContext (preferred)

pub struct ComponentContext<'a> {
    pub type_id: u32,
    pub file_id: FileID,
    pub class_name: &'a str,
    pub entity: Option<Entity>,
    pub linking_ctx: Option<&'a RefCell<LinkingContext>>,
    pub yaml: &'a Mapping,
    pub guid_resolver: Option<&'a GuidResolver>,  // 👈 New field
}

Phase 4: Integration with UnityFile Parsing

Goal: Build GuidResolver when loading a Unity project

Current Flow (UnityFile::from_path):

pub fn from_path(path: &Path) -> Result<UnityFile> {
    let content = std::fs::read_to_string(path)?;
    let documents = parse_unity_file(&content)?;

    match file_type {
        Scene => {
            let (world, entity_map) = build_world_from_documents(documents)?;
            // ...
        }
    }
}

Enhanced Flow:

pub fn from_path(path: &Path) -> Result<UnityFile> {
    // 1. Detect if this is part of a Unity project
    let project_root = find_project_root(path)?;

    // 2. Build GUID resolver for the project
    let guid_resolver = GuidResolver::from_project(&project_root)?;

    // 3. Parse file with GUID resolution
    let content = std::fs::read_to_string(path)?;
    let documents = parse_unity_file(&content)?;

    match file_type {
        Scene => {
            let (world, entity_map) = build_world_from_documents(
                documents,
                Some(&guid_resolver)  // 👈 Pass resolver
            )?;
            // ...
        }
    }
}

Files to Modify:

  • 📄 src/model.rs - Update UnityFile::from_path()
  • 📄 src/parser/guid_resolver.rs - Add find_project_root() helper

Optimization: Cache GuidResolver per project (expensive to rebuild)

// Thread-local cache for performance
thread_local! {
    static GUID_CACHE: RefCell<HashMap<PathBuf, Arc<GuidResolver>>> = RefCell::new(HashMap::new());
}

Phase 5: Update Examples and Tests

Examples to Update:

  • 📄 examples/find_playsfx.rs - Should now find PlaySFX components!
  • 📄 examples/ecs_integration.rs - Add example with GUID resolution

Tests to Add:

  • 📄 tests/guid_resolution_tests.rs - New test file
    • Test GuidResolver can scan project
    • Test GUID → path resolution
    • Test class name extraction
    • Integration test with VR_Horror project

Integration Test:

#[test]
fn test_guid_resolution_vr_horror() {
    let project_path = Path::new("test_data/VR_Horror_YouCantRun");

    // Build resolver
    let resolver = GuidResolver::from_project(project_path).unwrap();

    // Known PlaySFX GUID
    let playsfx_guid = "091c537484687e9419460cdcd7038234";

    // Should resolve to class name
    assert_eq!(
        resolver.resolve_class_name(playsfx_guid),
        Some("PlaySFX".to_string())
    );

    // Now parse a scene with PlaySFX
    let scene_path = project_path.join("Assets/Scenes/TEST/Final_1F/1F.unity");
    let file = UnityFile::from_path(&scene_path).unwrap();

    if let UnityFile::Scene(scene) = file {
        // Should find PlaySFX components
        let playsfx_view = scene.world.borrow::<PlaySFX>();
        let mut count = 0;
        for entity in scene.entity_map.values() {
            if playsfx_view.get(*entity).is_some() {
                count += 1;
            }
        }
        assert!(count > 0, "Should find at least one PlaySFX component");
    }
}

File Checklist

New Files to Create

  • src/parser/guid_resolver.rs - GUID resolution logic
  • tests/guid_resolution_tests.rs - Unit and integration tests

Existing Files to Modify

  • src/parser/mod.rs - Export GuidResolver
  • src/lib.rs - Re-export GuidResolver
  • src/ecs/builder.rs - Add MonoBehaviour GUID resolution
  • src/model.rs - Update UnityFile::from_path()
  • src/types/component.rs - Add guid_resolver to ComponentContext
  • src/types/references.rs - Ensure ExternalRef has guid() method
  • Cargo.toml - Add regex dependency (optional)
  • examples/find_playsfx.rs - Update with notes/verification
  • examples/ecs_integration.rs - Add GUID resolution example

Technical Challenges & Solutions

Challenge 1: Performance - Scanning Large Projects

Problem: Scanning thousands of .meta files on every parse is slow

Solutions:

  1. Lazy Loading: Only scan when needed (first MonoBehaviour encountered)
  2. Caching: Store GuidResolver per project path in thread-local cache
  3. Incremental: Only scan Assets/ directory, skip Library/Temp
  4. Parallel: Use rayon to scan .meta files in parallel

Recommendation: Start with simple implementation, optimize if needed

Challenge 2: Class Name Extraction Complexity

Problem: C# parsing can be complex (namespaces, partial classes, etc.)

Solutions:

  1. Simple Regex: r"(?:public\s+)?class\s+(\w+)(?:\s*:\s*MonoBehaviour)?"
    • Good enough for 95% of cases
    • Fast and simple
  2. Full C# Parser: Use tree-sitter or similar
    • Overkill for this use case
    • Adds heavy dependency

Recommendation: Start with regex, handle edge cases as discovered

Challenge 3: Breaking API Changes

Problem: Adding GuidResolver parameter changes public API

Solutions:

  1. Option A: Make it optional with Option<&GuidResolver>
    • Backward compatible
    • GUIDs won't resolve if not provided
  2. Option B: Add separate method from_path_with_resolver()
    • Keep original API unchanged
    • Explicit opt-in to GUID resolution
  3. Option C: Detect and auto-create GuidResolver
    • Best user experience
    • Always attempt GUID resolution
    • Fall back gracefully if project not detected

Recommendation: Option C - Auto-detect and resolve

Challenge 4: Multiple Scripts Per File

Problem: C# files can have multiple classes

Example:

public class PlaySFX : MonoBehaviour { }
public class SFXHelper { }  // Same file!

Solution: The .meta file is associated with the file, not the class. Unity creates separate .meta files for each public MonoBehaviour. Non-MonoBehaviour helper classes in the same file aren't referenced by GUID.

Recommendation: Extract first public class that inherits from MonoBehaviour

Success Criteria

When this feature is complete:

Functionality:

  • GuidResolver can scan a Unity project and build GUID mappings
  • MonoBehaviour components resolve to their actual class names
  • Custom components like PlaySFX are discovered and parsed automatically
  • examples/find_playsfx.rs finds PlaySFX components in VR_Horror project

Performance:

  • GUID resolution adds < 500ms overhead for typical projects
  • Caching prevents redundant scanning

Testing:

  • Unit tests for GUID resolution
  • Integration test finds PlaySFX in VR_Horror
  • Edge cases handled (missing .meta, invalid GUIDs, etc.)

Documentation:

  • GUID resolution documented in README
  • Examples show usage with real Unity projects
  • API docs explain GuidResolver usage

Timeline Estimate

  • Phase 1 (GUID → Path): 2-4 hours
  • Phase 2 (Class Name Extraction): 1-2 hours
  • Phase 3 (MonoBehaviour Parser): 2-3 hours
  • Phase 4 (Integration): 2-3 hours
  • Phase 5 (Tests & Examples): 2-3 hours

Total: 9-15 hours of development time

Future Enhancements (Out of Scope)

These can be added later if needed:

  1. Prefab GUID Resolution: Resolve nested prefab references
  2. AssetDatabase: Full asset path resolution (materials, textures, etc.)
  3. GUID Cache File: Persist GUID mappings to disk for instant loading
  4. Watch Mode: Auto-update GUID mappings when .meta files change
  5. Cross-Platform Paths: Handle Windows/Mac/Linux path differences

Status: 📋 Planning Complete - Ready for Implementation

Last Updated: 2026-01-02