From 17ea08caac752d5e9c36b30686da85c9fc8ba599 Mon Sep 17 00:00:00 2001 From: Connor Date: Fri, 2 Jan 2026 14:53:29 +0000 Subject: [PATCH] selective parsing --- DESIGN.md | 192 ------------------ cursebreaker-parser-macros/src/lib.rs | 17 +- .../examples/ecs_integration.rs | 146 +++++++++++++ cursebreaker-parser/src/ecs/builder.rs | 43 ++-- cursebreaker-parser/src/lib.rs | 9 +- cursebreaker-parser/src/macros.rs | 111 ++++++++++ cursebreaker-parser/src/types/component.rs | 27 ++- cursebreaker-parser/src/types/game_object.rs | 7 + cursebreaker-parser/src/types/mod.rs | 6 +- .../src/types/prefab_instance.rs | 6 + cursebreaker-parser/src/types/transform.rs | 12 ++ cursebreaker-parser/src/types/type_filter.rs | 161 +++++++++++++++ 12 files changed, 511 insertions(+), 226 deletions(-) delete mode 100644 DESIGN.md create mode 100644 cursebreaker-parser/examples/ecs_integration.rs create mode 100644 cursebreaker-parser/src/macros.rs create mode 100644 cursebreaker-parser/src/types/type_filter.rs diff --git a/DESIGN.md b/DESIGN.md deleted file mode 100644 index 100042a..0000000 --- a/DESIGN.md +++ /dev/null @@ -1,192 +0,0 @@ -# Cursebreaker Unity Parser - Design Document - -## Project Overview - -A high-performance Rust library for parsing and querying Unity project files (.unity scenes, .prefab prefabs, and .asset ScriptableObjects). - -## Goals - -1. **Parse Unity YAML Format**: Handle Unity's YAML 1.1 format with custom tags (`!u!`) and file ID references -2. **Extract Structure**: Parse GameObjects, Components, and their properties into queryable data structures -3. **High Performance**: Optimized for large Unity projects with minimal memory footprint -4. **Type Safety**: Strong typing for Unity's component system -5. **Library-First**: Designed as a reusable SDK for other Rust tools - -## Target File Formats - -- `.unity` - Unity scene files -- `.prefab` - Unity prefab files -- `.asset` - Unity ScriptableObject and other asset files - -All three formats share the same underlying YAML structure with Unity-specific extensions. - -## Unity File Format Structure - -Unity files use YAML 1.1 with special conventions: - -```yaml -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!1 &1866116814460599870 -GameObject: - m_ObjectHideFlags: 0 - m_Component: - - component: {fileID: 8151827567463220614} - - component: {fileID: 8755205353704683373} - m_Name: CardGrabber ---- !u!224 &8151827567463220614 -RectTransform: - m_GameObject: {fileID: 1866116814460599870} - m_LocalPosition: {x: 0, y: 0, z: 0} -``` - -### Key Concepts - -1. **Documents**: Each `---` starts a new YAML document representing a Unity object -2. **Type Tags**: `!u!N` indicates Unity type (e.g., `!u!1` = GameObject, `!u!224` = RectTransform) -3. **Anchors**: `&ID` defines a local file ID for the object -4. **File References**: `{fileID: N}` references objects by their ID (local or external) -5. **GUID References**: `{guid: ...}` references external assets -6. **Properties**: All Unity objects have serialized fields (usually prefixed with `m_`) - -## Architecture - -### Core Components - -``` -cursebreaker-parser/ -├── src/ -│ ├── lib.rs # Public API exports -│ ├── parser/ # YAML parsing layer -│ │ ├── mod.rs -│ │ ├── yaml.rs # YAML document parser -│ │ ├── unity_tag.rs # Unity type tag handler (!u!) -│ │ └── reference.rs # FileID/GUID reference parser -│ ├── model/ # Data model -│ │ ├── mod.rs -│ │ ├── document.rs # UnityDocument struct -│ │ ├── object.rs # UnityObject base -│ │ ├── gameobject.rs # GameObject type -│ │ ├── component.rs # Component types -│ │ └── property.rs # Property value types -│ ├── types/ # Unity type system -│ │ ├── mod.rs -│ │ ├── type_id.rs # Unity type ID -> name mapping -│ │ └── component_types.rs -│ ├── query/ # Query API -│ │ ├── mod.rs -│ │ ├── project.rs # UnityProject (multi-file) -│ │ ├── find.rs # Find objects/components -│ │ └── filter.rs # Filter/search utilities -│ └── error.rs # Error types -``` - -### Data Model - -```rust -// Core types -pub struct UnityFile { - pub path: PathBuf, - pub documents: Vec, -} - -pub struct UnityDocument { - pub type_id: u32, // From !u!N - pub file_id: i64, // From &ID - pub class_name: String, // E.g., "GameObject" - pub properties: PropertyMap, -} - -pub struct UnityProject { - pub files: HashMap, - // Reference resolution cache -} - -// Property values (simplified) -pub enum PropertyValue { - Integer(i64), - Float(f64), - String(String), - Boolean(bool), - FileRef { file_id: i64, guid: Option }, - Vector3 { x: f64, y: f64, z: f64 }, - Color { r: f64, g: f64, b: f64, a: f64 }, - Array(Vec), - Object(PropertyMap), -} -``` - -## Performance Considerations - -1. **Streaming Parser**: Parse YAML incrementally rather than loading entire file into memory -2. **Lazy Loading**: Only parse files when accessed -3. **Reference Caching**: Cache resolved references to avoid repeated lookups -4. **Zero-Copy Where Possible**: Use string slices and borrowed data where feasible -5. **Parallel Parsing**: Support parsing multiple files concurrently - -## Dependencies - -- `yaml-rust2` or `serde_yaml` - YAML parsing (evaluate both) -- `serde` - Serialization/deserialization -- `rayon` - Parallel processing (optional, for multi-file parsing) -- `thiserror` - Error handling -- `indexmap` - Ordered maps for properties - -## Testing Strategy - -1. **Unit Tests**: Each parser component tested independently -2. **Integration Tests**: Full file parsing with real Unity files -3. **Sample Data**: Use PiratePanic project as test corpus -4. **Benchmarks**: Performance tests on large Unity projects -5. **Fuzzing**: Fuzz testing for parser robustness (future) - -## API Design Goals - -### Simple File Parsing -```rust -let file = UnityFile::from_path("Scene.unity")?; -for doc in &file.documents { - println!("{}: {}", doc.class_name, doc.file_id); -} -``` - -### Query API -```rust -let project = UnityProject::from_directory("Assets/")?; - -// Find all GameObjects -let objects = project.find_all_by_type("GameObject"); - -// Find by name -let player = project.find_by_name("Player")?; - -// Get components -let transform = player.get_component("Transform")?; -let position = transform.get_vector3("m_LocalPosition")?; -``` - -### Reference Resolution -```rust -// Follow references automatically -let gameobject = project.get_object(file_id)?; -let transform_ref = gameobject.get_file_ref("m_Component[0].component")?; -let transform = project.resolve_reference(transform_ref)?; -``` - -## Future Enhancements (Out of Scope for v1) - -- Unity YAML serialization (writing files) -- C# script parsing -- Asset dependency graphs -- Unity version detection and compatibility -- Binary .unity format support (older Unity versions) -- Meta file parsing (.meta files) - -## Success Criteria - -1. Successfully parse all files in PiratePanic sample project -2. Extract all GameObjects and Components with properties -3. Resolve all internal file references correctly -4. Parse large scene files (>10MB) in <100ms -5. Memory usage scales linearly with file size -6. Clean, documented public API diff --git a/cursebreaker-parser-macros/src/lib.rs b/cursebreaker-parser-macros/src/lib.rs index 1a7d097..8c301e7 100644 --- a/cursebreaker-parser-macros/src/lib.rs +++ b/cursebreaker-parser-macros/src/lib.rs @@ -82,15 +82,25 @@ pub fn derive_unity_component(input: TokenStream) -> TokenStream { } }; + // Generate EcsInsertable implementation + let ecs_insertable_impl = quote! { + impl cursebreaker_parser::EcsInsertable for #struct_name { + fn insert_into_world(self, world: &mut sparsey::World, entity: sparsey::Entity) { + world.insert(entity, (self,)); + } + } + }; + // Generate inventory registration let registration = quote! { inventory::submit! { cursebreaker_parser::ComponentRegistration { type_id: #type_id, class_name: #class_name, - parser: |yaml, ctx| { - #struct_name::parse(yaml, ctx) - .map(|c| Box::new(c) as Box) + parse_and_insert: |yaml, ctx, world, entity| { + <#struct_name as cursebreaker_parser::EcsInsertable>::parse_and_insert( + yaml, ctx, world, entity + ) } } } @@ -99,6 +109,7 @@ pub fn derive_unity_component(input: TokenStream) -> TokenStream { // Combine everything let expanded = quote! { #parse_impl + #ecs_insertable_impl #registration }; diff --git a/cursebreaker-parser/examples/ecs_integration.rs b/cursebreaker-parser/examples/ecs_integration.rs new file mode 100644 index 0000000..6bc7d5c --- /dev/null +++ b/cursebreaker-parser/examples/ecs_integration.rs @@ -0,0 +1,146 @@ +//! Example demonstrating ECS integration and selective type parsing +//! +//! This example shows: +//! 1. Custom components being automatically inserted into the ECS world +//! 2. Using the parse_with_types! macro for selective parsing +//! 3. Querying the ECS world for components + +use cursebreaker_parser::{parse_with_types, ComponentContext, EcsInsertable, FileID, TypeFilter, UnityComponent}; + +/// Custom Unity MonoBehaviour component +#[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, +} + +/// Another custom component +#[derive(Debug, Clone, UnityComponent)] +#[unity_class("Interactable")] +pub struct Interactable { + #[unity_field("interactionRadius")] + pub interaction_radius: f32, + + #[unity_field("interactionText")] + pub interaction_text: String, + + #[unity_field("canInteract")] + pub can_interact: bool, +} + +fn main() { + println!("ECS Integration & Selective Parsing Example"); + println!("{}", "=".repeat(60)); + + // Example 1: Using parse_with_types! macro + println!("\n1. Creating type filters:"); + println!("{}", "-".repeat(60)); + + let _filter_all = TypeFilter::parse_all(); + println!("✓ Filter that parses ALL types"); + + let filter_selective = parse_with_types! { + unity_types(Transform, Camera), + custom_types(PlaySFX) + }; + println!("✓ Filter for Transform, Camera, and PlaySFX only"); + + let filter_custom_only = parse_with_types! { + custom_types(PlaySFX, Interactable) + }; + println!("✓ Filter for PlaySFX and Interactable only (no Unity types)"); + + // Example 2: Demonstrating ECS insertion + println!("\n2. ECS Integration:"); + println!("{}", "-".repeat(60)); + + // Simulate parsing a PlaySFX component + let yaml_str = r#" +volume: 0.8 +startTime: 0.0 +endTime: 5.0 +isLoop: 0 +"#; + + let yaml: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap(); + let mapping = yaml.as_mapping().unwrap(); + + let ctx = ComponentContext { + type_id: 114, + file_id: FileID::from_i64(12345), + class_name: "PlaySFX", + entity: None, + linking_ctx: None, + yaml: mapping, + }; + + // Parse the component + if let Some(play_sfx) = PlaySFX::parse(mapping, &ctx) { + println!("✓ Parsed PlaySFX component:"); + println!(" - volume: {}", play_sfx.volume); + println!(" - start_time: {}", play_sfx.start_time); + println!(" - end_time: {}", play_sfx.end_time); + println!(" - is_loop: {}", play_sfx.is_loop); + + // Create a minimal ECS world to demonstrate insertion + use sparsey::World; + let mut world = World::builder().register::().build(); + let entity = world.create(()); + + println!("\n✓ Created ECS entity: {:?}", entity); + + // Insert the component into the world + play_sfx.clone().insert_into_world(&mut world, entity); + println!("✓ Inserted PlaySFX component into ECS world"); + + // Query it back + { + let view = world.borrow::(); + if let Some(component) = view.get(entity) { + println!("✓ Successfully queried component from ECS:"); + println!(" - volume: {}", component.volume); + } + } + } + + // Example 3: Type filter usage + println!("\n3. Type Filter Behavior:"); + println!("{}", "-".repeat(60)); + + println!("Filter checks:"); + println!(" Transform in selective filter: {}", filter_selective.should_parse_unity("Transform")); + println!(" Camera in selective filter: {}", filter_selective.should_parse_unity("Camera")); + println!(" Light in selective filter: {}", filter_selective.should_parse_unity("Light")); + println!(" PlaySFX in selective filter: {}", filter_selective.should_parse_custom("PlaySFX")); + println!(" Interactable in selective filter: {}", filter_selective.should_parse_custom("Interactable")); + + println!("\n PlaySFX in custom-only filter: {}", filter_custom_only.should_parse_custom("PlaySFX")); + println!(" Transform in custom-only filter: {}", filter_custom_only.should_parse_unity("Transform")); + + // Example 4: Benefits of selective parsing + println!("\n4. Benefits of Selective Parsing:"); + println!("{}", "-".repeat(60)); + println!("When parsing a large Unity project:"); + println!(" • Parse ALL types: Parse everything (default)"); + println!(" • Parse specific types: Faster parsing & less memory"); + println!(" • Parse only what you need for your tool/analysis"); + println!("\nExample use cases:"); + println!(" • Animation tool: Only parse Animator, AnimationClip"); + println!(" • Audio tool: Only parse AudioSource, PlaySFX"); + println!(" • Transform analyzer: Only parse Transform, RectTransform"); + + println!(); + println!("{}", "=".repeat(60)); + println!("Complete! Custom components now work with ECS!"); + println!("{}", "=".repeat(60)); +} diff --git a/cursebreaker-parser/src/ecs/builder.rs b/cursebreaker-parser/src/ecs/builder.rs index d5523c2..f47a268 100644 --- a/cursebreaker-parser/src/ecs/builder.rs +++ b/cursebreaker-parser/src/ecs/builder.rs @@ -3,7 +3,7 @@ use crate::model::RawDocument; use crate::types::{ yaml_helpers, ComponentContext, FileID, GameObject, LinkingContext, PrefabInstanceComponent, - RectTransform, Transform, UnityComponent, + RectTransform, Transform, TypeFilter, UnityComponent, }; use crate::{Error, Result}; use sparsey::{Entity, World}; @@ -42,8 +42,9 @@ pub fn build_world_from_documents( } // PASS 2: Attach components to entities + let type_filter = TypeFilter::parse_all(); for doc in documents.iter().filter(|d| d.type_id != 1 && d.class_name != "GameObject") { - attach_component(&mut world, doc, &linking_ctx)?; + attach_component(&mut world, doc, &linking_ctx, &type_filter)?; } // PASS 3: Execute all deferred linking callbacks @@ -90,8 +91,9 @@ pub fn build_world_from_documents_into( } // PASS 2: Attach components to entities + let type_filter = TypeFilter::parse_all(); for doc in documents.iter().filter(|d| d.type_id != 1 && d.class_name != "GameObject") { - attach_component(world, doc, &linking_ctx)?; + attach_component(world, doc, &linking_ctx, &type_filter)?; } // PASS 3: Execute all deferred linking callbacks @@ -132,6 +134,7 @@ fn attach_component( world: &mut World, doc: &RawDocument, linking_ctx: &RefCell, + type_filter: &TypeFilter, ) -> Result<()> { let yaml = doc .as_mapping() @@ -168,6 +171,16 @@ fn attach_component( yaml, }; + // Check type filter to see if we should parse this component + let is_custom = doc.class_name.as_str() != "Transform" + && doc.class_name.as_str() != "RectTransform" + && doc.class_name.as_str() != "PrefabInstance"; + + if !type_filter.should_parse(&doc.class_name, is_custom) { + // Skip this component type based on filter + return Ok(()); + } + // Dispatch to appropriate component parser match doc.class_name.as_str() { "Transform" => { @@ -195,26 +208,10 @@ fn attach_component( for reg in inventory::iter:: { if reg.class_name == doc.class_name.as_str() { found_custom = true; - // Try to parse the component - if let Some(_boxed_component) = (reg.parser)(yaml, &ctx) { - eprintln!( - "Info: Custom component '{}' parsed successfully via #[derive(UnityComponent)]", - doc.class_name - ); - eprintln!( - "Note: ECS integration for custom components is not yet fully implemented." - ); - eprintln!( - " Component data was parsed but not inserted into the ECS world." - ); - eprintln!( - " To use this data, access it directly from the parsed documents." - ); - // TODO: Future enhancement - add dynamic component insertion support - // This would require either: - // 1. A type-erased component enum wrapper - // 2. Component trait objects in the ECS - // 3. User-defined registration with downcasting logic + // Parse and insert the component into the ECS world + if (reg.parse_and_insert)(yaml, &ctx, world, entity) { + // Successfully parsed and inserted + linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity); } break; } diff --git a/cursebreaker-parser/src/lib.rs b/cursebreaker-parser/src/lib.rs index 3a6fa44..1e2ff46 100644 --- a/cursebreaker-parser/src/lib.rs +++ b/cursebreaker-parser/src/lib.rs @@ -27,6 +27,7 @@ // Public modules pub mod ecs; pub mod error; +pub mod macros; pub mod model; pub mod parser; // TODO: Update project module to work with new UnityFile enum architecture @@ -42,10 +43,10 @@ pub use parser::{meta::MetaFile, parse_unity_file}; // pub use project::UnityProject; pub use property::PropertyValue; pub use types::{ - get_class_name, get_type_id, Color, ComponentContext, ComponentRegistration, ExternalRef, - FileID, FileRef, GameObject, LocalID, PrefabInstance, PrefabInstanceComponent, - PrefabModification, PrefabResolver, Quaternion, RectTransform, Transform, UnityComponent, - UnityReference, Vector2, Vector3, yaml_helpers, + get_class_name, get_type_id, Color, ComponentContext, ComponentRegistration, EcsInsertable, + ExternalRef, FileID, FileRef, GameObject, LocalID, PrefabInstance, PrefabInstanceComponent, + PrefabModification, PrefabResolver, Quaternion, RectTransform, Transform, TypeFilter, + UnityComponent, UnityReference, Vector2, Vector3, yaml_helpers, }; // Re-export the derive macro from the macro crate diff --git a/cursebreaker-parser/src/macros.rs b/cursebreaker-parser/src/macros.rs new file mode 100644 index 0000000..f84918e --- /dev/null +++ b/cursebreaker-parser/src/macros.rs @@ -0,0 +1,111 @@ +//! Declarative macros for convenient API usage + +/// Create a TypeFilter with specific Unity and custom types +/// +/// # Syntax +/// ```ignore +/// parse_with_types! { +/// unity_types(Transform, Camera, Light), +/// custom_types(PlaySFX, Interact) +/// } +/// ``` +/// +/// You can omit either section: +/// ```ignore +/// // Only Unity types +/// parse_with_types! { +/// unity_types(Transform, Camera) +/// } +/// +/// // Only custom types +/// parse_with_types! { +/// custom_types(PlaySFX) +/// } +/// ``` +#[macro_export] +macro_rules! parse_with_types { + // Full syntax with both Unity and custom types + (unity_types($($unity:ident),+ $(,)?), custom_types($($custom:ident),+ $(,)?)) => { + $crate::TypeFilter::new( + vec![$(stringify!($unity)),+], + vec![$(stringify!($custom)),+] + ) + }; + + // Only Unity types + (unity_types($($unity:ident),+ $(,)?)) => { + $crate::TypeFilter::unity_only( + vec![$(stringify!($unity)),+] + ) + }; + + // Only custom types + (custom_types($($custom:ident),+ $(,)?)) => { + $crate::TypeFilter::custom_only( + vec![$(stringify!($custom)),+] + ) + }; + + // Alternative order: custom_types first + (custom_types($($custom:ident),+ $(,)?), unity_types($($unity:ident),+ $(,)?)) => { + $crate::TypeFilter::new( + vec![$(stringify!($unity)),+], + vec![$(stringify!($custom)),+] + ) + }; +} + +#[cfg(test)] +mod tests { + use crate::TypeFilter; + + #[test] + fn test_parse_with_types_macro() { + let filter = parse_with_types! { + unity_types(Transform, Camera, Light), + custom_types(PlaySFX, Interact) + }; + + assert!(filter.should_parse_unity("Transform")); + assert!(filter.should_parse_unity("Camera")); + assert!(filter.should_parse_unity("Light")); + assert!(!filter.should_parse_unity("AudioSource")); + + assert!(filter.should_parse_custom("PlaySFX")); + assert!(filter.should_parse_custom("Interact")); + assert!(!filter.should_parse_custom("OtherComponent")); + } + + #[test] + fn test_parse_with_types_unity_only() { + let filter = parse_with_types! { + unity_types(Transform, Camera) + }; + + assert!(filter.should_parse_unity("Transform")); + assert!(!filter.should_parse_unity("Light")); + assert!(!filter.should_parse_custom("PlaySFX")); + } + + #[test] + fn test_parse_with_types_custom_only() { + let filter = parse_with_types! { + custom_types(PlaySFX, Interact) + }; + + assert!(!filter.should_parse_unity("Transform")); + assert!(filter.should_parse_custom("PlaySFX")); + assert!(filter.should_parse_custom("Interact")); + } + + #[test] + fn test_parse_with_types_reversed_order() { + let filter = parse_with_types! { + custom_types(PlaySFX), + unity_types(Transform) + }; + + assert!(filter.should_parse_unity("Transform")); + assert!(filter.should_parse_custom("PlaySFX")); + } +} diff --git a/cursebreaker-parser/src/types/component.rs b/cursebreaker-parser/src/types/component.rs index dd742d7..8a59784 100644 --- a/cursebreaker-parser/src/types/component.rs +++ b/cursebreaker-parser/src/types/component.rs @@ -84,6 +84,29 @@ pub trait UnityComponent: Sized { fn parse(yaml: &Mapping, ctx: &ComponentContext) -> Option; } +/// Trait for components that can be inserted into the ECS world +/// +/// This enables dynamic component insertion for both built-in and custom components. +pub trait EcsInsertable: UnityComponent { + /// Insert this component into the ECS world + fn insert_into_world(self, world: &mut sparsey::World, entity: Entity); + + /// Parse and insert in one step + fn parse_and_insert( + yaml: &Mapping, + ctx: &ComponentContext, + world: &mut sparsey::World, + entity: Entity, + ) -> bool { + if let Some(component) = Self::parse(yaml, ctx) { + component.insert_into_world(world, entity); + true + } else { + false + } + } +} + /// Registration entry for custom Unity components /// /// This is submitted via the `inventory` crate by the `#[derive(UnityComponent)]` macro @@ -93,8 +116,8 @@ pub struct ComponentRegistration { pub type_id: u32, /// Unity class name (e.g., "PlaySFX") pub class_name: &'static str, - /// Parser function that creates a boxed component from YAML - pub parser: fn(&Mapping, &ComponentContext) -> Option>, + /// Parser function that parses and inserts the component into the ECS world + pub parse_and_insert: fn(&Mapping, &ComponentContext, &mut sparsey::World, Entity) -> bool, } // Collect all component registrations submitted via the macro diff --git a/cursebreaker-parser/src/types/game_object.rs b/cursebreaker-parser/src/types/game_object.rs index c48f76c..f244026 100644 --- a/cursebreaker-parser/src/types/game_object.rs +++ b/cursebreaker-parser/src/types/game_object.rs @@ -1,6 +1,7 @@ //! GameObject component use crate::types::{yaml_helpers, ComponentContext, UnityComponent}; +use sparsey::Entity; /// A GameObject component /// @@ -56,3 +57,9 @@ impl UnityComponent for GameObject { }) } } + +impl crate::types::EcsInsertable for GameObject { + fn insert_into_world(self, world: &mut sparsey::World, entity: Entity) { + world.insert(entity, (self,)); + } +} diff --git a/cursebreaker-parser/src/types/mod.rs b/cursebreaker-parser/src/types/mod.rs index dd99a0f..eb2a03b 100644 --- a/cursebreaker-parser/src/types/mod.rs +++ b/cursebreaker-parser/src/types/mod.rs @@ -10,12 +10,13 @@ mod ids; mod prefab_instance; mod reference; mod transform; +mod type_filter; mod type_registry; mod values; pub use component::{ - yaml_helpers, ComponentContext, ComponentRegistration, LinkCallback, LinkingContext, - UnityComponent, + yaml_helpers, ComponentContext, ComponentRegistration, EcsInsertable, LinkCallback, + LinkingContext, UnityComponent, }; pub use game_object::GameObject; pub use ids::{FileID, LocalID}; @@ -24,5 +25,6 @@ pub use prefab_instance::{ }; pub use reference::UnityReference; pub use transform::{RectTransform, Transform}; +pub use type_filter::TypeFilter; pub use type_registry::{get_class_name, get_type_id}; pub use values::{Color, ExternalRef, FileRef, Quaternion, Vector2, Vector3}; diff --git a/cursebreaker-parser/src/types/prefab_instance.rs b/cursebreaker-parser/src/types/prefab_instance.rs index 4ac66d3..ccc73c7 100644 --- a/cursebreaker-parser/src/types/prefab_instance.rs +++ b/cursebreaker-parser/src/types/prefab_instance.rs @@ -337,6 +337,12 @@ impl UnityComponent for PrefabInstanceComponent { } } +impl crate::types::EcsInsertable for PrefabInstanceComponent { + fn insert_into_world(self, world: &mut sparsey::World, entity: Entity) { + world.insert(entity, (self,)); + } +} + /// A modification applied to a nested prefab /// /// Unity stores modifications as changes to specific properties of objects diff --git a/cursebreaker-parser/src/types/transform.rs b/cursebreaker-parser/src/types/transform.rs index e1a446a..101fb4b 100644 --- a/cursebreaker-parser/src/types/transform.rs +++ b/cursebreaker-parser/src/types/transform.rs @@ -126,6 +126,12 @@ impl UnityComponent for Transform { } } +impl crate::types::EcsInsertable for Transform { + fn insert_into_world(self, world: &mut sparsey::World, entity: Entity) { + world.insert(entity, (self,)); + } +} + /// A RectTransform component /// /// RectTransform is used for UI elements and extends Transform with additional properties. @@ -288,3 +294,9 @@ impl UnityComponent for RectTransform { }) } } + +impl crate::types::EcsInsertable for RectTransform { + fn insert_into_world(self, world: &mut sparsey::World, entity: Entity) { + world.insert(entity, (self,)); + } +} diff --git a/cursebreaker-parser/src/types/type_filter.rs b/cursebreaker-parser/src/types/type_filter.rs new file mode 100644 index 0000000..4d337cc --- /dev/null +++ b/cursebreaker-parser/src/types/type_filter.rs @@ -0,0 +1,161 @@ +//! Type filtering for selective parsing +//! +//! This module provides functionality to selectively parse only specific Unity +//! component types, improving performance and reducing memory usage. + +use std::collections::HashSet; + +/// Filter for controlling which Unity types get parsed +/// +/// By default, all types are parsed. Use `TypeFilter::new()` to create +/// a filter that only parses specific types. +#[derive(Debug, Clone)] +pub struct TypeFilter { + /// Set of Unity type names to parse (e.g., "Transform", "Camera") + /// If None, all types are parsed + unity_types: Option>, + + /// Set of custom component names to parse (e.g., "PlaySFX") + /// If None, all custom types are parsed + custom_types: Option>, + + /// Whether to parse all types (default) + parse_all: bool, +} + +impl TypeFilter { + /// Create a new filter that parses ALL types (default behavior) + pub fn parse_all() -> Self { + Self { + unity_types: None, + custom_types: None, + parse_all: true, + } + } + + /// Create a new filter with specific Unity and custom types + /// + /// # Example + /// ``` + /// use cursebreaker_parser::TypeFilter; + /// + /// let filter = TypeFilter::new( + /// vec!["Transform", "Camera", "Light"], + /// vec!["PlaySFX", "Interact"] + /// ); + /// ``` + pub fn new(unity_types: Vec, custom_types: Vec) -> Self + where + S1: Into, + S2: Into, + { + Self { + unity_types: Some(unity_types.into_iter().map(|s| s.into()).collect()), + custom_types: Some(custom_types.into_iter().map(|s| s.into()).collect()), + parse_all: false, + } + } + + /// Create a filter that only parses specific Unity types (no custom types) + pub fn unity_only>(types: Vec) -> Self { + Self { + unity_types: Some(types.into_iter().map(|s| s.into()).collect()), + custom_types: Some(HashSet::new()), + parse_all: false, + } + } + + /// Create a filter that only parses specific custom types (no Unity types) + pub fn custom_only>(types: Vec) -> Self { + Self { + unity_types: Some(HashSet::new()), + custom_types: Some(types.into_iter().map(|s| s.into()).collect()), + parse_all: false, + } + } + + /// Check if a Unity type should be parsed + pub fn should_parse_unity(&self, type_name: &str) -> bool { + if self.parse_all { + return true; + } + + match &self.unity_types { + Some(types) => types.contains(type_name), + None => true, // If not specified, parse all + } + } + + /// Check if a custom type should be parsed + pub fn should_parse_custom(&self, type_name: &str) -> bool { + if self.parse_all { + return true; + } + + match &self.custom_types { + Some(types) => types.contains(type_name), + None => true, // If not specified, parse all + } + } + + /// Check if any type should be parsed + pub fn should_parse(&self, type_name: &str, is_custom: bool) -> bool { + if is_custom { + self.should_parse_custom(type_name) + } else { + self.should_parse_unity(type_name) + } + } +} + +impl Default for TypeFilter { + fn default() -> Self { + Self::parse_all() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_all() { + let filter = TypeFilter::parse_all(); + assert!(filter.should_parse_unity("Transform")); + assert!(filter.should_parse_unity("Camera")); + assert!(filter.should_parse_custom("PlaySFX")); + } + + #[test] + fn test_specific_types() { + let filter = TypeFilter::new( + vec!["Transform", "Camera"], + vec!["PlaySFX"] + ); + + assert!(filter.should_parse_unity("Transform")); + assert!(filter.should_parse_unity("Camera")); + assert!(!filter.should_parse_unity("Light")); + + assert!(filter.should_parse_custom("PlaySFX")); + assert!(!filter.should_parse_custom("Interact")); + } + + #[test] + fn test_unity_only() { + let filter = TypeFilter::unity_only(vec!["Transform"]); + + assert!(filter.should_parse_unity("Transform")); + assert!(!filter.should_parse_unity("Camera")); + assert!(!filter.should_parse_custom("PlaySFX")); + } + + #[test] + fn test_custom_only() { + let filter = TypeFilter::custom_only(vec!["PlaySFX"]); + + assert!(!filter.should_parse_unity("Transform")); + assert!(filter.should_parse_custom("PlaySFX")); + assert!(!filter.should_parse_custom("Interact")); + } +}