diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 924182e..83ed828 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -10,7 +10,8 @@ "Bash(findstr:*)", "Bash(cargo check:*)", "Bash(ls:*)", - "Bash(find:*)" + "Bash(find:*)", + "Bash(grep:*)" ] } } diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..320a87f --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,491 @@ +# 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 diff --git a/cursebreaker-parser-macros/src/lib.rs b/cursebreaker-parser-macros/src/lib.rs index 8c301e7..e3fc602 100644 --- a/cursebreaker-parser-macros/src/lib.rs +++ b/cursebreaker-parser-macros/src/lib.rs @@ -101,7 +101,8 @@ pub fn derive_unity_component(input: TokenStream) -> TokenStream { <#struct_name as cursebreaker_parser::EcsInsertable>::parse_and_insert( yaml, ctx, world, entity ) - } + }, + register: |builder| builder.register::<#struct_name>() } } }; diff --git a/cursebreaker-parser/examples/find_playsfx.rs b/cursebreaker-parser/examples/find_playsfx.rs new file mode 100644 index 0000000..ee48951 --- /dev/null +++ b/cursebreaker-parser/examples/find_playsfx.rs @@ -0,0 +1,194 @@ +//! Demo: Find all PlaySFX components and their locations in VR_Horror_YouCantRun +//! +//! This example demonstrates: +//! 1. Parsing a real Unity project +//! 2. Finding custom MonoBehaviour components (PlaySFX) +//! 3. Querying the ECS world for components +//! 4. Accessing Transform data for component locations + +use cursebreaker_parser::{UnityComponent, UnityFile}; +use std::path::Path; + +/// PlaySFX component from VR_Horror_YouCantRun +/// +/// C# definition: +/// ```csharp +/// public class PlaySFX : MonoBehaviour +/// { +/// [SerializeField] float volume; +/// [SerializeField] float startTime; +/// [SerializeField] float endTime; +/// [SerializeField] bool isLoop; +/// } +/// ``` +#[derive(Debug, Clone, UnityComponent)] +#[unity_class("PlaySFX")] +pub struct PlaySFX { + #[unity_field("volume")] + pub volume: f64, + + #[unity_field("startTime")] + pub start_time: f64, + + #[unity_field("endTime")] + pub end_time: f64, + + #[unity_field("isLoop")] + pub is_loop: bool, +} + +fn main() -> Result<(), Box> { + println!("🎮 VR Horror - PlaySFX Component Finder"); + println!("{}", "=".repeat(70)); + println!(); + + let project_path = Path::new("test_data/VR_Horror_YouCantRun"); + + // Check if project exists + if !project_path.exists() { + eprintln!("❌ Error: VR_Horror_YouCantRun project not found at {}", project_path.display()); + eprintln!(" Run the integration tests first to download it:"); + eprintln!(" cargo test test_vr_horror_project"); + return Ok(()); + } + + println!("📁 Scanning project: {}", project_path.display()); + println!(); + + // Find all Unity scene files + let scene_files = find_unity_files(project_path, "unity"); + + println!("📄 Found {} scene file(s)", scene_files.len()); + println!(); + + let mut total_playsfx = 0; + + // Parse each scene + for scene_path in scene_files { + println!("🔍 Parsing: {}", scene_path.file_name().unwrap().to_string_lossy()); + + match UnityFile::from_path(&scene_path) { + Ok(UnityFile::Scene(scene)) => { + // Get views for all component types we need + let playsfx_view = scene.world.borrow::(); + let transform_view = scene.world.borrow::(); + let rect_transform_view = scene.world.borrow::(); + let gameobject_view = scene.world.borrow::(); + + // Find all entities that have PlaySFX + let mut found_count = 0; + let mut found_entities = Vec::new(); + + // Iterate through all entities in the entity_map + for entity in scene.entity_map.values() { + if let Some(playsfx) = playsfx_view.get(*entity) { + found_entities.push((*entity, playsfx.clone())); + found_count += 1; + } + } + + if found_count > 0 { + println!(" ✅ Found {} PlaySFX component(s)", found_count); + total_playsfx += found_count; + + // Process each found PlaySFX component + for (entity, playsfx) in found_entities { + let transform = transform_view.get(entity); + let rect_transform = rect_transform_view.get(entity); + let game_object = gameobject_view.get(entity); + + let name = game_object + .and_then(|go| go.name()) + .unwrap_or("(unnamed)"); + + println!(); + println!(" 🔊 PlaySFX on GameObject: \"{}\"", name); + println!(" Entity: {:?}", entity); + println!(" Properties:"); + println!(" • volume: {}", playsfx.volume); + println!(" • startTime: {}", playsfx.start_time); + println!(" • endTime: {}", playsfx.end_time); + println!(" • isLoop: {}", playsfx.is_loop); + + // Print position if available + if let Some(transform) = transform { + if let Some(pos) = transform.local_position() { + println!(" Transform:"); + println!(" • Position: ({:.2}, {:.2}, {:.2})", + pos.x, pos.y, pos.z); + } + if let Some(rot) = transform.local_rotation() { + println!(" • Rotation: ({:.2}, {:.2}, {:.2}, {:.2})", + rot.x, rot.y, rot.z, rot.w); + } + if let Some(scale) = transform.local_scale() { + println!(" • Scale: ({:.2}, {:.2}, {:.2})", + scale.x, scale.y, scale.z); + } + } else if let Some(rect_transform) = rect_transform { + let transform = rect_transform.transform(); + if let Some(pos) = transform.local_position() { + println!(" RectTransform (UI):"); + println!(" • Position: ({:.2}, {:.2}, {:.2})", + pos.x, pos.y, pos.z); + } + } else { + println!(" ⚠️ No Transform found"); + } + } + } else { + println!(" ⊘ No PlaySFX components found"); + } + + println!(); + } + Ok(_) => { + println!(" ⊘ Skipped (not a scene file)"); + println!(); + } + Err(e) => { + println!(" ❌ Parse error: {}", e); + println!(); + } + } + } + + println!("{}", "=".repeat(70)); + println!("📊 Summary:"); + println!(" Total PlaySFX components found: {}", total_playsfx); + println!("{}", "=".repeat(70)); + + Ok(()) +} + +/// Find all Unity files with a specific extension in a directory +fn find_unity_files(dir: &Path, extension: &str) -> Vec { + let mut files = Vec::new(); + + fn visit_dir(dir: &Path, extension: &str, files: &mut Vec) { + if let Ok(entries) = std::fs::read_dir(dir) { + for entry in entries.flatten() { + let path = entry.path(); + + // Skip Library, Temp, Builds, and .git directories + if let Some(name) = path.file_name().and_then(|n| n.to_str()) { + if name == "Library" || name == "Temp" || name == "Builds" || name == ".git" { + continue; + } + } + + if path.is_dir() { + visit_dir(&path, extension, files); + } else if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + if ext == extension { + files.push(path); + } + } + } + } + } + + visit_dir(dir, extension, &mut files); + files.sort(); + files +} diff --git a/cursebreaker-parser/src/ecs/builder.rs b/cursebreaker-parser/src/ecs/builder.rs index f47a268..1c2896a 100644 --- a/cursebreaker-parser/src/ecs/builder.rs +++ b/cursebreaker-parser/src/ecs/builder.rs @@ -25,13 +25,20 @@ use std::collections::HashMap; pub fn build_world_from_documents( documents: Vec, ) -> Result<(World, HashMap)> { - // Create World with registered component types - let mut world = World::builder() + // Create World builder with registered component types + let mut builder = World::builder(); + builder .register::() .register::() .register::() - .register::() - .build(); + .register::(); + + // Register all custom components from inventory + for reg in inventory::iter:: { + (reg.register)(&mut builder); + } + + let mut world = builder.build(); let linking_ctx = RefCell::new(LinkingContext::new()); diff --git a/cursebreaker-parser/src/types/component.rs b/cursebreaker-parser/src/types/component.rs index 8a59784..40746a7 100644 --- a/cursebreaker-parser/src/types/component.rs +++ b/cursebreaker-parser/src/types/component.rs @@ -2,6 +2,7 @@ use crate::types::*; use serde_yaml::{Mapping, Value}; +use sparsey::world::WorldBuilder; use sparsey::Entity; use std::cell::RefCell; use std::collections::HashMap; @@ -118,6 +119,8 @@ pub struct ComponentRegistration { pub class_name: &'static str, /// Parser function that parses and inserts the component into the ECS world pub parse_and_insert: fn(&Mapping, &ComponentContext, &mut sparsey::World, Entity) -> bool, + /// Function to register this component type with a WorldBuilder + pub register: for<'a> fn(&'a mut WorldBuilder) -> &'a mut WorldBuilder, } // Collect all component registrations submitted via the macro