# 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::()` - Full integration with existing Transform, GameObject, etc. ### What's Missing **MonoBehaviour GUID Resolution**: Unity scene/prefab files store custom scripts like this: ```yaml --- !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**: ```yaml fileFormatVersion: 2 guid: 091c537484687e9419460cdcd7038234 # 👈 This is the GUID MonoImporter: serializedVersion: 2 defaultReferences: [] executionOrder: 0 ``` 3. **MonoBehaviour references the GUID**: ```yaml m_Script: {fileID: 11500000, guid: 091c537484687e9419460cdcd7038234, type: 3} ``` 4. **We need to map**: `GUID → Script Path → Class Name` - `091c537484687e9419460cdcd7038234` → `Assets/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` ```rust /// Resolves Unity GUIDs to asset paths pub struct GuidResolver { /// Map from GUID string to asset file path guid_to_path: HashMap, } impl GuidResolver { /// Scan a Unity project directory for .meta files pub fn from_project(project_path: &Path) -> Result { // 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) ```rust impl GuidResolver { /// Extract class name from a C# script file fn extract_class_name(cs_path: &Path) -> Result { 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 { 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): ```rust _ => { // Check if this is a registered custom component for reg in inventory::iter:: { if reg.class_name == doc.class_name.as_str() { // ⚠️ Won't match "MonoBehaviour" // ... } } } ``` **Enhanced MonoBehaviour Parsing**: ```rust "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:: { 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) ```rust pub fn build_world_from_documents( documents: Vec, guid_resolver: Option<&GuidResolver>, // 👈 New parameter ) -> Result<(World, HashMap)> ``` **Option B**: Store in ComponentContext (preferred) ```rust pub struct ComponentContext<'a> { pub type_id: u32, pub file_id: FileID, pub class_name: &'a str, pub entity: Option, pub linking_ctx: Option<&'a RefCell>, 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`): ```rust pub fn from_path(path: &Path) -> Result { 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**: ```rust pub fn from_path(path: &Path) -> Result { // 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) ```rust // Thread-local cache for performance thread_local! { static GUID_CACHE: RefCell>> = 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**: ```rust #[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::(); 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**: ```csharp 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 ## Related Documentation - Unity YAML Format: [UnityYAMLParser](https://github.com/HearthSim/UnityYAMLParser) - Unity .meta Files: [Unity Manual - Meta Files](https://docs.unity3d.com/Manual/class-Meta.html) - GUID Format: RFC 4122 compliant UUIDs without hyphens --- **Status**: 📋 Planning Complete - Ready for Implementation **Last Updated**: 2026-01-02