selective parsing
This commit is contained in:
192
DESIGN.md
192
DESIGN.md
@@ -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<UnityDocument>,
|
||||
}
|
||||
|
||||
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<PathBuf, UnityFile>,
|
||||
// Reference resolution cache
|
||||
}
|
||||
|
||||
// Property values (simplified)
|
||||
pub enum PropertyValue {
|
||||
Integer(i64),
|
||||
Float(f64),
|
||||
String(String),
|
||||
Boolean(bool),
|
||||
FileRef { file_id: i64, guid: Option<String> },
|
||||
Vector3 { x: f64, y: f64, z: f64 },
|
||||
Color { r: f64, g: f64, b: f64, a: f64 },
|
||||
Array(Vec<PropertyValue>),
|
||||
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
|
||||
@@ -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<dyn std::any::Any>)
|
||||
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
|
||||
};
|
||||
|
||||
|
||||
146
cursebreaker-parser/examples/ecs_integration.rs
Normal file
146
cursebreaker-parser/examples/ecs_integration.rs
Normal file
@@ -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::<PlaySFX>().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::<PlaySFX>();
|
||||
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));
|
||||
}
|
||||
@@ -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<LinkingContext>,
|
||||
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::<crate::types::ComponentRegistration> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
111
cursebreaker-parser/src/macros.rs
Normal file
111
cursebreaker-parser/src/macros.rs
Normal file
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -84,6 +84,29 @@ pub trait UnityComponent: Sized {
|
||||
fn parse(yaml: &Mapping, ctx: &ComponentContext) -> Option<Self>;
|
||||
}
|
||||
|
||||
/// 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<Box<dyn std::any::Any>>,
|
||||
/// 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
|
||||
|
||||
@@ -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,));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,));
|
||||
}
|
||||
}
|
||||
|
||||
161
cursebreaker-parser/src/types/type_filter.rs
Normal file
161
cursebreaker-parser/src/types/type_filter.rs
Normal file
@@ -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<HashSet<String>>,
|
||||
|
||||
/// Set of custom component names to parse (e.g., "PlaySFX")
|
||||
/// If None, all custom types are parsed
|
||||
custom_types: Option<HashSet<String>>,
|
||||
|
||||
/// 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<S1, S2>(unity_types: Vec<S1>, custom_types: Vec<S2>) -> Self
|
||||
where
|
||||
S1: Into<String>,
|
||||
S2: Into<String>,
|
||||
{
|
||||
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<S: Into<String>>(types: Vec<S>) -> 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<S: Into<String>>(types: Vec<S>) -> 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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user