From 8a06185b987e60b1dd69d5c30c38d185fd4ce0a6 Mon Sep 17 00:00:00 2001 From: Connor Date: Sat, 3 Jan 2026 14:51:31 +0000 Subject: [PATCH] cleanup --- Cargo.toml | 2 +- README.md | 446 +++++++-------- unity-parser/examples/basic_parsing.rs | 9 +- .../examples/guid_resolution.rs.disabled | 121 ----- unity-parser/src/parser/guid_resolver.rs | 18 +- unity-parser/src/parser/meta.rs | 6 +- unity-parser/src/parser/mod.rs | 12 +- .../src/parser/prefab_guid_resolver.rs | 14 +- unity-parser/src/parser/unity_tag.rs | 2 +- unity-parser/src/parser/yaml.rs | 2 +- unity-parser/src/project/mod.rs | 512 ------------------ unity-parser/src/project/query.rs | 10 +- unity-parser/src/types/guid.rs | 10 +- unity-parser/src/types/ids.rs | 2 +- unity-parser/src/types/reference.rs | 14 +- unity-parser/src/types/type_filter.rs | 2 +- unity-parser/src/types/type_registry.rs | 4 +- .../src/types/unity_types/transform.rs | 4 +- unity-parser/src/types/values.rs | 4 +- .../tests/test_guid_resolution.rs.disabled | 83 --- 20 files changed, 265 insertions(+), 1012 deletions(-) delete mode 100644 unity-parser/examples/guid_resolution.rs.disabled delete mode 100644 unity-parser/src/project/mod.rs delete mode 100644 unity-parser/tests/test_guid_resolution.rs.disabled diff --git a/Cargo.toml b/Cargo.toml index b3e3797..4edfb6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,6 @@ resolver = "2" [workspace.package] version = "0.1.0" edition = "2021" -authors = ["Your Name "] +authors = ["connordemeyer@gmail.com"] license = "MIT OR Apache-2.0" repository = "https://github.com/yourusername/unity-parser-rust" diff --git a/README.md b/README.md index 27fa34e..eef7d4e 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,33 @@ -# Unity Parser +# Cursebreaker Unity Parser -A high-performance Rust library for parsing Unity project files (.unity scenes, .prefab prefabs, and .asset ScriptableObjects) with automatic MonoBehaviour component discovery and ECS integration. +A high-performance Rust library for parsing Unity project files (.unity scenes, .prefab prefabs, and .asset files) with automatic MonoBehaviour component discovery and ECS integration. [![Rust](https://img.shields.io/badge/rust-1.70%2B-orange.svg)](https://www.rust-lang.org/) [![License](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](LICENSE) +**⚠ïļ Work in Progress**: This library is under active development. APIs may change. + ## Features -### 🚀 Core Parsing +### Core Parsing - **Multi-format support**: Parse `.unity` scenes, `.prefab` prefabs, and `.asset` files - **ECS integration**: Automatically builds [Sparsey](https://github.com/LechintanTudor/sparsey) ECS worlds from scenes - **Type-safe**: Strong typing for Unity primitives (Vector3, Quaternion, Color, etc.) -- **Fast**: Efficient parsing with minimal allocations +- **Fast**: Efficient YAML parsing with minimal allocations -### ðŸŽŊ Custom Component System +### Component System - **Derive macro**: `#[derive(UnityComponent)]` for automatic component parsing - **Auto-registration**: Components register themselves via [inventory](https://github.com/dtolnay/inventory) -- **GUID resolution**: Automatically resolves MonoBehaviour GUIDs to class names +- **GUID resolution**: Automatically resolves MonoBehaviour script GUIDs to class names +- **Prefab resolution**: Resolves prefab GUIDs for nested prefab references - **Type filtering**: Selectively parse only the components you need -### 🔍 Advanced Features -- **Prefab instantiation**: Clone and modify prefab instances -- **Reference resolution**: Automatic FileID → Entity mapping -- **Regex filtering**: Parse only scenes matching specific patterns +### Advanced Features +- **Prefab instantiation**: Clone and modify prefab instances (basic support) +- **Reference resolution**: FileID → Entity mapping +- **Regex filtering**: Parse only files matching specific patterns - **Transform hierarchies**: Parent-child relationships preserved -- **Memory efficient**: 128-bit GUIDs with 3.5x memory reduction vs strings +- **Memory efficient**: 128-bit GUIDs stored as u128 (16 bytes vs ~56 bytes for String) ## Installation @@ -32,7 +35,7 @@ Add to your `Cargo.toml`: ```toml [dependencies] -unity-parser = "0.1" +unity_parser = { path = "unity-parser" } sparsey = "0.13" # For ECS queries ``` @@ -70,66 +73,111 @@ fn main() -> Result<(), Box> { } ``` -### Define Custom Components +### Parse a Prefab ```rust -use unity_parser::UnityComponent; +use unity_parser::UnityFile; -#[derive(Debug, Clone, UnityComponent)] -#[unity_class("PlaySFX")] +fn main() -> Result<(), Box> { + let file = UnityFile::from_path("Player.prefab")?; + + if let UnityFile::Prefab(prefab) = file { + // Find all GameObjects + let game_objects = prefab.get_documents_by_class("GameObject"); + for doc in game_objects { + if let Some(mapping) = doc.as_mapping() { + if let Some(name) = mapping.get("m_Name").and_then(|v| v.as_str()) { + println!("GameObject: {}", name); + } + } + } + + // Find documents by type ID + let transforms = prefab.get_documents_by_type(4); // Transform = type 4 + println!("Found {} Transforms", transforms.len()); + } + + Ok(()) +} +``` + +### Define Custom Components + +**⚠ïļ Note**: The derive macro currently has namespace issues and may not compile. Manual implementation is recommended until fixed. + +```rust +use unity_parser::{UnityComponent, ComponentContext}; +use serde_yaml::Mapping; + +#[derive(Debug, Clone)] 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, } -// Now automatically parsed from Unity scenes! -fn find_audio_components(scene: &UnityScene) { - let playsfx_view = scene.world.borrow::(); - - for entity in scene.entity_map.values() { - if let Some(sfx) = playsfx_view.get(*entity) { - println!("Found audio: volume={}, loop={}", sfx.volume, sfx.is_loop); - } +// Manual implementation (recommended until macro is fixed) +impl UnityComponent for PlaySFX { + fn parse(yaml: &Mapping, _ctx: &ComponentContext) -> Option { + Some(Self { + volume: unity_parser::yaml_helpers::get_f64(yaml, "volume").unwrap_or(1.0), + start_time: unity_parser::yaml_helpers::get_f64(yaml, "startTime").unwrap_or(0.0), + end_time: unity_parser::yaml_helpers::get_f64(yaml, "endTime").unwrap_or(0.0), + is_loop: unity_parser::yaml_helpers::get_bool(yaml, "isLoop").unwrap_or(false), + }) } } ``` ## GUID Resolution -The parser automatically resolves Unity MonoBehaviour GUIDs to class names, enabling seamless custom component discovery: +The parser automatically resolves Unity MonoBehaviour GUIDs to class names: ``` Unity Scene File Rust Code ───────────────── ───────── -MonoBehaviour: #[derive(UnityComponent)] - m_Script: #[unity_class("PlaySFX")] - guid: 091c537... ──────────> pub struct PlaySFX { ... } +MonoBehaviour: Custom component + m_Script: in ECS World + guid: 091c537... ──────────> volume: 1.0 isLoop: 0 ``` ### How It Works -1. **Scan**: Parser scans `Assets/` for `*.cs.meta` files +1. **Scan**: Parser scans Unity project's `Assets/` for `*.cs.meta` files 2. **Build Map**: Extracts GUIDs from `.meta` files 3. **Extract Class**: Parses `.cs` files to get `class Name : MonoBehaviour` 4. **Resolve**: Maps GUID → Class Name → Registered Component -5. **Parse**: Automatically parses MonoBehaviour YAML into your Rust struct +5. **Parse**: Automatically parses MonoBehaviour YAML into components -**Performance**: GUID resolver caches mappings per project for fast lookups. +The GUID resolver builds automatically when parsing scenes if a Unity project root is detected. -## Regex Filtering +## Type Filtering -Parse only scenes matching specific patterns: +Parse only specific Unity types and MonoBehaviours for better performance: + +```rust +use unity_parser::{TypeFilter, parse_unity_file_filtered}; +use std::collections::HashSet; + +// Parse only Transforms and GameObjects +let mut types = HashSet::new(); +types.insert("Transform".to_string()); +types.insert("GameObject".to_string()); +let filter = TypeFilter::with_unity_types(types); + +let file = parse_unity_file_filtered( + Path::new("Scene.unity"), + None, // No regex filter + Some(&filter) +)?; +``` + +## Regex Path Filtering + +Parse only files matching specific patterns: ```rust use regex::Regex; @@ -137,38 +185,31 @@ use unity_parser::parse_unity_file_filtered; // Only parse production scenes let filter = Regex::new(r"Assets/Scenes/Production/")?; -let scene = parse_unity_file_filtered("Assets/Scenes/Production/Level1.unity", Some(&filter))?; - -// Parse everything (default) -let scene = parse_unity_file_filtered("Scene.unity", None)?; +let scene = parse_unity_file_filtered( + Path::new("Assets/Scenes/Production/Level1.unity"), + Some(&filter), + None +)?; ``` -Common patterns: +## Supported Unity Types -```rust -// Test scenes only -let filter = Regex::new(r"(?i)test")?; +### Built-in Components +- ✅ GameObject +- ✅ Transform +- ✅ RectTransform +- ✅ PrefabInstance -// Exclude debug/temp scenes -let filter = Regex::new(r"^(?!.*(debug|tmp|test))")?; +### Value Types +- ✅ Vector2, Vector3 +- ✅ Quaternion +- ✅ Color +- ✅ FileID, GUID +- ✅ ExternalRef, FileRef -// Specific level range -let filter = Regex::new(r"Level[1-5]\.unity$")?; -``` - -## Type Filtering - -Selectively parse components for better performance: - -```rust -use unity_parser::{TypeFilter, parse_with_types}; - -// Parse only transforms and custom components -let filter = TypeFilter::parse_with_types(&["Transform", "PlaySFX"]); - -// Or use the macro for convenience -let filter = parse_with_types!(Transform, PlaySFX); -``` +### Custom Components +- ✅ Any `MonoBehaviour` via manual `UnityComponent` implementation +- ⚠ïļ `#[derive(UnityComponent)]` macro (has namespace bugs, not recommended) ## Architecture @@ -181,234 +222,169 @@ Raw YAML Documents ↓ ┌─────────────────────┐ │ GUID Resolution │ ← .meta files + .cs files -│ (MonoBehaviour) │ +│ (MonoBehaviour) │ (Script GUID → Class Name) └─────────────────────┘ ↓ ┌─────────────────────┐ -│ Component Registry │ ← #[derive(UnityComponent)] +│ Prefab GUID Res. │ ← .prefab.meta files +│ (Nested Prefabs) │ (Prefab GUID → Prefab Path) +└─────────────────────┘ + ↓ +┌─────────────────────┐ +│ Component Registry │ ← UnityComponent trait impls │ (inventory) │ └─────────────────────┘ ↓ ┌─────────────────────┐ -│ ECS World │ ← Sparsey entities -│ (Transforms, etc) │ +│ ECS World │ ← Sparsey entities & components +│ (Transform, etc) │ └─────────────────────┘ ``` ### Memory Efficiency **GUID Storage**: -- Old: `String` (56 bytes heap allocated) -- New: `Guid` (16 bytes on stack) -- **3.5x memory reduction** for GUID maps +- Old approach: `String` (24 bytes stack + 32 bytes heap = 56 bytes) +- Current: `u128` (16 bytes on stack) +- **3.5x memory reduction** for GUID storage **GUID Comparison**: - Old: O(n) string comparison (32 characters) - New: O(1) integer comparison -- **⚡ Significant speedup** for lookups +- **Significant speedup** for HashMap lookups -## Supported Unity Types +## Project Structure -### Built-in Components -- ✅ GameObject -- ✅ Transform -- ✅ RectTransform -- ✅ PrefabInstance - -### Value Types -- ✅ Vector2, Vector3, Vector4 -- ✅ Quaternion -- ✅ Color, Color32 -- ✅ FileID, GUID -- ✅ ExternalRef, FileRef - -### Custom Components -- ✅ Any `MonoBehaviour` via `#[derive(UnityComponent)]` -- ✅ Automatic field mapping with `#[unity_field("fieldName")]` -- ✅ Support for all Unity primitive types - -## Examples - -### Find All Components of a Type - -```rust -use unity_parser::UnityFile; - -let scene = UnityFile::from_path("Scene.unity")?; - -if let UnityFile::Scene(scene) = scene { - let transforms = scene.world.borrow::(); - let gameobjects = scene.world.borrow::(); - - for (file_id, entity) in &scene.entity_map { - let name = gameobjects.get(*entity) - .and_then(|go| go.name()) - .unwrap_or("(unnamed)"); - - if let Some(transform) = transforms.get(*entity) { - println!("{}: {:?}", name, transform.local_position()); - } - } -} +``` +cursebreaker-parser-rust/ +├── unity-parser/ # Main library crate +│ ├── src/ +│ │ ├── ecs/ # ECS world building +│ │ │ └── builder.rs +│ │ ├── model/ # UnityFile, Scene, Prefab, Asset +│ │ │ └── mod.rs +│ │ ├── parser/ # YAML parsing & GUID resolution +│ │ │ ├── guid_resolver.rs # Script GUID → Class Name +│ │ │ ├── prefab_guid_resolver.rs # Prefab GUID → Path +│ │ │ ├── meta.rs # .meta file parsing +│ │ │ ├── yaml.rs # YAML document splitting +│ │ │ └── mod.rs +│ │ ├── project/ # ⚠ïļ OUTDATED - needs refactoring +│ │ │ └── mod.rs +│ │ ├── types/ # Unity types & components +│ │ │ ├── unity_types/ +│ │ │ │ ├── game_object.rs +│ │ │ │ ├── transform.rs +│ │ │ │ ├── prefab_instance.rs +│ │ │ │ └── mod.rs +│ │ │ ├── component.rs # UnityComponent trait & helpers +│ │ │ ├── guid.rs # 128-bit GUID type +│ │ │ ├── ids.rs # FileID, LocalID +│ │ │ ├── reference.rs # UnityReference enum +│ │ │ ├── type_filter.rs # TypeFilter for selective parsing +│ │ │ ├── values.rs # Vector3, Quaternion, Color, etc. +│ │ │ └── mod.rs +│ │ ├── error.rs # Error types +│ │ ├── macros.rs +│ │ ├── property/ +│ │ └── lib.rs +│ ├── examples/ +│ │ ├── basic_parsing.rs +│ │ ├── custom_component.rs +│ │ ├── ecs_integration.rs +│ │ ├── find_playsfx.rs +│ │ ├── parse_resources.rs +│ │ └── parse_resource_prefabs.rs +│ ├── tests/ +│ └── Cargo.toml +├── unity-parser-macros/ # Proc macro crate (⚠ïļ has bugs) +│ ├── src/ +│ │ └── lib.rs +│ └── Cargo.toml +├── Cargo.toml # Workspace config +└── README.md ``` -### Prefab Instantiation +## Known Issues -```rust -let prefab_file = UnityFile::from_path("Player.prefab")?; +### Critical Issues +1. **`unity-parser/src/project/mod.rs` is OUTDATED** + - Built for old architecture before `UnityFile` enum refactor + - References non-existent `UnityDocument` type (should be `RawDocument`) + - Module is disabled in lib.rs until refactored -if let UnityFile::Prefab(prefab) = prefab_file { - // Create instance - let mut instance = prefab.instantiate(); +2. **Derive macro namespace mismatch** + - `unity-parser-macros` uses `unity_parser` namespace + - Actual crate name is `unity_parser::` (underscore, not hyphen) + - Manual `UnityComponent` implementation recommended - // Override values - instance.override_value( - file_id, - "m_Name", - serde_yaml::Value::String("Player_Clone".to_string()) - )?; +3. **Placeholder values in Cargo.toml** + - Author and repository fields need updating - // Access remapped FileIDs - let new_file_ids = instance.file_id_map(); -} -``` - -### Batch Processing - -```rust -use walkdir::WalkDir; -use regex::Regex; - -let filter = Regex::new(r"Level\d+\.unity")?; - -for entry in WalkDir::new("Assets/Scenes") { - let path = entry?.path(); - - if path.extension() == Some("unity") { - match parse_unity_file_filtered(path, Some(&filter)) { - Ok(UnityFile::Scene(scene)) => { - println!("Processed: {} ({} entities)", - path.display(), - scene.entity_map.len() - ); - } - Err(e) if e.to_string().contains("does not match filter") => { - // Filtered out - } - Err(e) => eprintln!("Error: {}", e), - } - } -} -``` +### Minor Issues +1. Disabled example/test files may reference outdated APIs +2. Some examples may have incorrect YAML access patterns ## Running Examples -The repository includes several examples: - ```bash -# Parse and display basic scene info +# Parse basic Unity file cargo run --example basic_parsing -# Demonstrate custom component parsing +# Custom component parsing (requires Unity project) cargo run --example custom_component # ECS integration showcase cargo run --example ecs_integration -# Find all PlaySFX components in VR Horror project +# Find PlaySFX components (requires CBAssets project) cargo run --example find_playsfx ``` ## Testing -Run the test suite: - ```bash # Unit tests cargo test --lib -# Integration tests (requires git for downloading test projects) +# Integration tests cargo test --test integration_tests -# GUID resolution tests -cargo test test_guid_resolution -- --nocapture - # All tests cargo test ``` -## Performance - -Benchmarks on VR Horror project (21 scenes, 77 C# scripts): - -| Operation | Time | Throughput | -|-----------|------|------------| -| GUID Resolver Build | ~800ms | 77 scripts | -| Scene Parse | ~100-500ms | per scene | -| GUID Lookup | <1Ξs | O(1) HashMap | - -**Memory**: ~16 bytes per GUID (vs ~56 bytes for String-based approach) - ## Roadmap -### Phase 1: GUID Resolution ✅ COMPLETE -- [x] Scan `.cs.meta` files -- [x] Extract class names from C# scripts -- [x] Build GUID → Class Name mapping -- [x] 128-bit `Guid` type with 3.5x memory reduction +### Completed +- ✅ Phase 1: GUID Resolution (Script GUID → Class Name) +- ✅ Phase 2: MonoBehaviour Parser +- ✅ Phase 3: Prefab GUID Resolution +- ✅ Type filtering for selective parsing +- ✅ Regex path filtering +- ✅ Basic prefab instantiation -### Phase 2: MonoBehaviour Parser ✅ COMPLETE -- [x] Extract `m_Script` GUID from components -- [x] Resolve GUID to class name -- [x] Match with registered components -- [x] Automatic parsing via `#[derive(UnityComponent)]` - -### Phase 3: Advanced Features ✅ COMPLETE -- [x] Regex filtering for selective parsing -- [x] Type filtering for performance -- [x] Prefab instantiation and overrides +### In Progress / Needs Work +- 🔧 Refactor `project` module for new architecture +- 🔧 Update disabled example/test files +- 🔧 Fix example code YAML access patterns ### Future Enhancements -- [x] Prefab GUID resolution (nested prefabs) -- [ ] Full AssetDatabase resolution (materials, textures) +- [ ] Full prefab modification system - [ ] Persistent GUID cache for instant loading - [ ] Watch mode for live Unity project monitoring -- [ ] Cross-platform path handling - -## Project Structure - -``` -unity-parser/ -├── src/ -│ ├── ecs/ # ECS world building -│ ├── model/ # UnityFile, Scene, Prefab models -│ ├── parser/ # YAML parsing, GUID resolution -│ │ ├── guid_resolver.rs # GUID → Class Name mapping -│ │ ├── meta.rs # .meta file parsing -│ │ └── yaml.rs # YAML document splitting -│ ├── types/ # Unity types and components -│ │ ├── unity_types/ # Unity-specific types -│ │ │ ├── game_object.rs -│ │ │ ├── prefab_instance.rs -│ │ │ └── transform.rs -│ │ ├── guid.rs # 128-bit GUID type -│ │ ├── component.rs # Component trait system -│ │ └── ... -│ └── lib.rs -├── unity-parser-macros/ # Derive macro crate -├── examples/ # Usage examples -├── tests/ # Integration tests -└── test_data/ # Test Unity projects -``` +- [ ] More built-in Unity component types +- [ ] Better error messages with line numbers +- [ ] Parallel processing support +- [ ] Cross-platform path handling improvements ## Contributing -Contributions are welcome! Areas for improvement: +Contributions welcome! Areas needing help: +- **Documentation**: API docs, more examples, tutorials +- **Testing**: Integration tests with real Unity projects - **Performance**: Optimize YAML parsing, parallel processing -- **Features**: Additional Unity component types, better error messages -- **Testing**: More integration tests with real Unity projects -- **Documentation**: API docs, tutorials, cookbook examples ## License @@ -421,11 +397,7 @@ at your option. ## Acknowledgments -- **Unity**: For the YAML-based file format +- **Unity Technologies**: For the YAML-based file format - **Sparsey**: ECS library for component storage - **serde_yaml**: YAML parsing foundation - **inventory**: Compile-time component registration - ---- - -**Built with âĪïļ in Rust** diff --git a/unity-parser/examples/basic_parsing.rs b/unity-parser/examples/basic_parsing.rs index de310e2..8931a93 100644 --- a/unity-parser/examples/basic_parsing.rs +++ b/unity-parser/examples/basic_parsing.rs @@ -38,13 +38,10 @@ fn main() { let game_objects = prefab.get_documents_by_class("GameObject"); println!("Found {} GameObjects:", game_objects.len()); for go in game_objects { + // doc.yaml already contains the inner content (after class wrapper) if let Some(mapping) = go.as_mapping() { - if let Some(go_obj) = mapping.get("GameObject") { - if let Some(props) = go_obj.as_mapping() { - if let Some(name) = props.get("m_Name").and_then(|v| v.as_str()) { - println!(" - {}", name); - } - } + if let Some(name) = mapping.get("m_Name").and_then(|v| v.as_str()) { + println!(" - {}", name); } } } diff --git a/unity-parser/examples/guid_resolution.rs.disabled b/unity-parser/examples/guid_resolution.rs.disabled deleted file mode 100644 index fb4c297..0000000 --- a/unity-parser/examples/guid_resolution.rs.disabled +++ /dev/null @@ -1,121 +0,0 @@ -//! Example demonstrating GUID resolution with .meta files -//! -//! This example shows how to: -//! - Load Unity files with their .meta files -//! - Access GUID to path mappings -//! - Resolve cross-file references using GUIDs -//! -//! Run with: cargo run --example guid_resolution - -use cursebreaker_parser::{UnityProject, MetaFile}; -use std::path::Path; - -fn main() -> Result<(), Box> { - println!("Unity GUID Resolution Example"); - println!("==============================\n"); - - // Create a new Unity project with LRU cache - let mut project = UnityProject::new(1000); - - // Example 1: Parse a .meta file directly - println!("Example 1: Parsing a .meta file"); - println!("---------------------------------"); - - let meta_content = r#" -fileFormatVersion: 2 -guid: 4ab6bfb0ff54cdf4c8dd38ca244d6f15 -PrefabImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: -"#; - - let meta = MetaFile::from_str(meta_content)?; - println!("Parsed GUID: {}", meta.guid()); - println!("File format version: {:?}\n", meta.file_format_version()); - - // Example 2: Load Unity files with automatic .meta parsing - println!("Example 2: Loading Unity files with .meta files"); - println!("-------------------------------------------------"); - - let test_dir = "data/tests/unity-sampleproject/PiratePanic/Assets/PiratePanic/Prefabs/Menu/Battle/Hand"; - - if Path::new(test_dir).exists() { - // Load all Unity files in the directory - let loaded_files = project.load_directory(test_dir)?; - - println!("Loaded {} Unity files", loaded_files.len()); - println!("Found {} GUID mappings\n", project.guid_mappings().len()); - - // Example 3: Inspect GUID mappings - println!("Example 3: GUID to Path Mappings"); - println!("---------------------------------"); - - for (guid, path) in project.guid_mappings().iter().take(5) { - println!("GUID: {} -> {:?}", guid, path.file_name().unwrap()); - } - println!(); - - // Example 4: Look up a file by GUID - println!("Example 4: Looking up files by GUID"); - println!("------------------------------------"); - - if let Some((sample_guid, _)) = project.guid_mappings().iter().next() { - if let Some(path) = project.get_path_by_guid(sample_guid) { - println!("GUID {} resolves to:", sample_guid); - println!(" Path: {:?}", path); - - // Get the file - if let Some(file) = project.get_file(path) { - println!(" Documents: {}", file.documents.len()); - - // Show the first GameObject - for doc in &file.documents { - if doc.is_game_object() { - if let Some(obj) = doc.get("GameObject").and_then(|v| v.as_object()) { - if let Some(name) = obj.get("m_Name").and_then(|v| v.as_str()) { - println!(" Contains GameObject: {}", name); - break; - } - } - } - } - } - } - } - println!(); - - // Example 5: Cross-file reference resolution (when available) - println!("Example 5: Cross-file Reference Resolution"); - println!("-------------------------------------------"); - - // Find all external references in loaded files - let mut external_ref_count = 0; - - for file in project.files().values() { - for doc in &file.documents { - // Scan properties for external references - for value in doc.properties.values() { - if let Some(ext_ref) = value.as_external_ref() { - external_ref_count += 1; - - // Try to resolve this GUID - if let Some(target_path) = project.get_path_by_guid(&ext_ref.guid) { - println!("✓ External reference resolved:"); - println!(" GUID: {}", ext_ref.guid); - println!(" Target: {:?}", target_path.file_name().unwrap()); - } - } - } - } - } - - println!("\nFound {} external references in loaded files", external_ref_count); - } else { - println!("Test data directory not found: {}", test_dir); - println!("This example works best with Unity sample project files."); - } - - Ok(()) -} diff --git a/unity-parser/src/parser/guid_resolver.rs b/unity-parser/src/parser/guid_resolver.rs index e19bbd2..1981cea 100644 --- a/unity-parser/src/parser/guid_resolver.rs +++ b/unity-parser/src/parser/guid_resolver.rs @@ -14,7 +14,7 @@ //! # Example //! //! ```no_run -//! use cursebreaker_parser::parser::GuidResolver; +//! use unity_parser::parser::GuidResolver; //! use std::path::Path; //! //! // Build resolver from Unity project directory @@ -26,7 +26,7 @@ //! if let Some(class_name) = resolver.resolve_class_name(guid) { //! println!("GUID {} → {}", guid, class_name); //! } -//! # Ok::<(), cursebreaker_parser::Error>(()) +//! # Ok::<(), unity_parser::Error>(()) //! ``` use crate::parser::meta::MetaFile; @@ -71,11 +71,11 @@ impl GuidResolver { /// # Examples /// /// ```no_run - /// use cursebreaker_parser::parser::GuidResolver; + /// use unity_parser::parser::GuidResolver; /// use std::path::Path; /// /// let resolver = GuidResolver::from_project(Path::new("MyUnityProject"))?; - /// # Ok::<(), cursebreaker_parser::Error>(()) + /// # Ok::<(), unity_parser::Error>(()) /// ``` pub fn from_project(project_path: impl AsRef) -> Result { let project_path = project_path.as_ref(); @@ -176,7 +176,7 @@ impl GuidResolver { /// # Examples /// /// ```no_run - /// # use cursebreaker_parser::{GuidResolver, Guid}; + /// # use unity_parser::{GuidResolver, Guid}; /// # use std::path::Path; /// # let resolver = GuidResolver::from_project(Path::new("."))?; /// // Resolve by string @@ -189,7 +189,7 @@ impl GuidResolver { /// if let Some(class_name) = resolver.resolve_class_name(&guid) { /// println!("Found class: {}", class_name); /// } - /// # Ok::<(), cursebreaker_parser::Error>(()) + /// # Ok::<(), unity_parser::Error>(()) /// ``` pub fn resolve_class_name(&self, guid: G) -> Option<&str> { guid.as_guid() @@ -214,7 +214,7 @@ impl GuidResolver { /// # Examples /// /// ``` - /// use cursebreaker_parser::{GuidResolver, Guid}; + /// use unity_parser::{GuidResolver, Guid}; /// /// let mut resolver = GuidResolver::new(); /// let guid = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap(); @@ -347,13 +347,13 @@ fn extract_class_name(cs_path: &Path) -> Result> { /// # Examples /// /// ```no_run -/// use cursebreaker_parser::parser::find_project_root; +/// use unity_parser::parser::find_project_root; /// use std::path::Path; /// /// let scene_path = Path::new("MyProject/Assets/Scenes/Main.unity"); /// let project_root = find_project_root(scene_path)?; /// assert_eq!(project_root.file_name().unwrap(), "MyProject"); -/// # Ok::<(), cursebreaker_parser::Error>(()) +/// # Ok::<(), unity_parser::Error>(()) /// ``` pub fn find_project_root(path: impl AsRef) -> Result { let path = path.as_ref(); diff --git a/unity-parser/src/parser/meta.rs b/unity-parser/src/parser/meta.rs index f50dec3..148bab5 100644 --- a/unity-parser/src/parser/meta.rs +++ b/unity-parser/src/parser/meta.rs @@ -29,11 +29,11 @@ impl MetaFile { /// # Examples /// /// ```no_run - /// use cursebreaker_parser::parser::meta::MetaFile; + /// use unity_parser::parser::meta::MetaFile; /// /// let meta = MetaFile::from_path("Assets/Prefabs/Player.prefab.meta")?; /// println!("GUID: {}", meta.guid); - /// # Ok::<(), cursebreaker_parser::Error>(()) + /// # Ok::<(), unity_parser::Error>(()) /// ``` pub fn from_path(path: impl AsRef) -> Result { let path = path.as_ref(); @@ -90,7 +90,7 @@ impl MetaFile { /// # Examples /// /// ``` -/// use cursebreaker_parser::parser::meta::get_meta_path; +/// use unity_parser::parser::meta::get_meta_path; /// use std::path::PathBuf; /// /// let asset = PathBuf::from("Assets/Scenes/MainMenu.unity"); diff --git a/unity-parser/src/parser/mod.rs b/unity-parser/src/parser/mod.rs index 9f5c206..c700d67 100644 --- a/unity-parser/src/parser/mod.rs +++ b/unity-parser/src/parser/mod.rs @@ -30,8 +30,8 @@ use std::path::Path; /// # Example /// /// ```no_run -/// use cursebreaker_parser::parser::parse_unity_file; -/// use cursebreaker_parser::UnityFile; +/// use unity_parser::parser::parse_unity_file; +/// use unity_parser::UnityFile; /// use std::path::Path; /// /// let file = parse_unity_file(Path::new("Scene.unity"))?; @@ -40,7 +40,7 @@ use std::path::Path; /// UnityFile::Prefab(prefab) => println!("Prefab with {} documents", prefab.documents.len()), /// UnityFile::Asset(asset) => println!("Asset with {} documents", asset.documents.len()), /// } -/// # Ok::<(), cursebreaker_parser::Error>(()) +/// # Ok::<(), unity_parser::Error>(()) /// ``` pub fn parse_unity_file(path: &Path) -> Result { parse_unity_file_filtered(path, None, None) @@ -60,8 +60,8 @@ pub fn parse_unity_file(path: &Path) -> Result { /// # Example /// /// ```no_run -/// use cursebreaker_parser::parser::{parse_unity_file_filtered}; -/// use cursebreaker_parser::TypeFilter; +/// use unity_parser::parser::{parse_unity_file_filtered}; +/// use unity_parser::TypeFilter; /// use regex::Regex; /// use std::path::Path; /// use std::collections::HashSet; @@ -76,7 +76,7 @@ pub fn parse_unity_file(path: &Path) -> Result { /// types.insert("GameObject".to_string()); /// let type_filter = TypeFilter::with_unity_types(types); /// let file2 = parse_unity_file_filtered(Path::new("Scene.unity"), None, Some(&type_filter))?; -/// # Ok::<(), cursebreaker_parser::Error>(()) +/// # Ok::<(), unity_parser::Error>(()) /// ``` pub fn parse_unity_file_filtered( path: &Path, diff --git a/unity-parser/src/parser/prefab_guid_resolver.rs b/unity-parser/src/parser/prefab_guid_resolver.rs index a645ff5..a528e53 100644 --- a/unity-parser/src/parser/prefab_guid_resolver.rs +++ b/unity-parser/src/parser/prefab_guid_resolver.rs @@ -13,7 +13,7 @@ //! # Example //! //! ```no_run -//! use cursebreaker_parser::parser::PrefabGuidResolver; +//! use unity_parser::parser::PrefabGuidResolver; //! use std::path::Path; //! //! // Build resolver from Unity project directory @@ -25,7 +25,7 @@ //! if let Some(path) = resolver.resolve_path(guid) { //! println!("GUID {} → {}", guid, path.display()); //! } -//! # Ok::<(), cursebreaker_parser::Error>(()) +//! # Ok::<(), unity_parser::Error>(()) //! ``` use crate::parser::meta::MetaFile; @@ -67,11 +67,11 @@ impl PrefabGuidResolver { /// # Examples /// /// ```no_run - /// use cursebreaker_parser::parser::PrefabGuidResolver; + /// use unity_parser::parser::PrefabGuidResolver; /// use std::path::Path; /// /// let resolver = PrefabGuidResolver::from_project(Path::new("MyUnityProject"))?; - /// # Ok::<(), cursebreaker_parser::Error>(()) + /// # Ok::<(), unity_parser::Error>(()) /// ``` pub fn from_project(project_path: impl AsRef) -> Result { let project_path = project_path.as_ref(); @@ -155,7 +155,7 @@ impl PrefabGuidResolver { /// # Examples /// /// ```no_run - /// # use cursebreaker_parser::{PrefabGuidResolver, Guid}; + /// # use unity_parser::{PrefabGuidResolver, Guid}; /// # use std::path::Path; /// # let resolver = PrefabGuidResolver::from_project(Path::new("."))?; /// // Resolve by string @@ -168,7 +168,7 @@ impl PrefabGuidResolver { /// if let Some(path) = resolver.resolve_path(&guid) { /// println!("Found prefab: {}", path.display()); /// } - /// # Ok::<(), cursebreaker_parser::Error>(()) + /// # Ok::<(), unity_parser::Error>(()) /// ``` pub fn resolve_path(&self, guid: G) -> Option<&Path> { guid.as_guid() @@ -193,7 +193,7 @@ impl PrefabGuidResolver { /// # Examples /// /// ``` - /// use cursebreaker_parser::{PrefabGuidResolver, Guid}; + /// use unity_parser::{PrefabGuidResolver, Guid}; /// use std::path::PathBuf; /// /// let mut resolver = PrefabGuidResolver::new(); diff --git a/unity-parser/src/parser/unity_tag.rs b/unity-parser/src/parser/unity_tag.rs index 1f36580..e7981fd 100644 --- a/unity-parser/src/parser/unity_tag.rs +++ b/unity-parser/src/parser/unity_tag.rs @@ -32,7 +32,7 @@ fn unity_tag_regex() -> &'static Regex { /// # Example /// /// ``` -/// use cursebreaker_parser::parser::parse_unity_tag; +/// use unity_parser::parser::parse_unity_tag; /// /// let doc = "--- !u!1 &12345\nGameObject:\n m_Name: Test"; /// let tag = parse_unity_tag(doc).unwrap(); diff --git a/unity-parser/src/parser/yaml.rs b/unity-parser/src/parser/yaml.rs index 88b8e6b..79e846d 100644 --- a/unity-parser/src/parser/yaml.rs +++ b/unity-parser/src/parser/yaml.rs @@ -16,7 +16,7 @@ use crate::{Error, Result}; /// # Example /// /// ``` -/// use cursebreaker_parser::parser::split_yaml_documents; +/// use unity_parser::parser::split_yaml_documents; /// /// let content = "%YAML 1.1\n%TAG !u! tag:unity3d.com,2011:\n--- !u!1 &123\nGameObject:\n--- !u!4 &456\nTransform:"; /// let docs = split_yaml_documents(content).unwrap(); diff --git a/unity-parser/src/project/mod.rs b/unity-parser/src/project/mod.rs deleted file mode 100644 index bd83ec2..0000000 --- a/unity-parser/src/project/mod.rs +++ /dev/null @@ -1,512 +0,0 @@ -//! Multi-file Unity project container with reference resolution -//! -//! This module provides the UnityProject struct which can load multiple -//! Unity files and resolve references between them. - -mod query; - -use crate::parser::meta::{get_meta_path, MetaFile}; -use crate::{FileID, Result, UnityDocument, UnityFile, UnityReference}; -use lru::LruCache; -use std::collections::HashMap; -use std::num::NonZeroUsize; -use std::path::{Path, PathBuf}; - -/// A Unity project containing multiple files with cross-file reference resolution -/// -/// UnityProject can load multiple Unity files (.unity, .prefab, .asset) and provides -/// reference resolution both within files (eager) and across files (lazy with caching). -/// -/// # Examples -/// -/// ```no_run -/// use cursebreaker_parser::UnityProject; -/// -/// let mut project = UnityProject::new(1000); // LRU cache size -/// -/// // Load a single file -/// project.load_file("Assets/Scenes/MainMenu.unity")?; -/// -/// // Or load an entire directory -/// project.load_directory("Assets/Prefabs")?; -/// -/// println!("Loaded {} files", project.files().len()); -/// # Ok::<(), cursebreaker_parser::Error>(()) -/// ``` -pub struct UnityProject { - /// All loaded Unity files, indexed by their path - files: HashMap, - - /// GUID to file path mapping for cross-file reference resolution - guid_to_path: HashMap, - - /// FileID to (PathBuf, document index) mapping for fast lookups - file_id_index: HashMap, - - /// LRU cache for resolved references - reference_cache: LruCache>, - - /// Maximum cache size - cache_limit: usize, -} - -impl UnityProject { - /// Create a new Unity project with specified LRU cache limit - /// - /// # Arguments - /// - /// * `cache_limit` - Maximum number of resolved references to cache - /// - /// # Examples - /// - /// ``` - /// use cursebreaker_parser::UnityProject; - /// - /// let project = UnityProject::new(1000); - /// ``` - pub fn new(cache_limit: usize) -> Self { - Self { - files: HashMap::new(), - guid_to_path: HashMap::new(), - file_id_index: HashMap::new(), - reference_cache: LruCache::new( - NonZeroUsize::new(cache_limit).unwrap_or(NonZeroUsize::new(1).unwrap()), - ), - cache_limit, - } - } - - /// Load a Unity file into the project - /// - /// Parses the file and builds an index of all FileIDs for fast lookup. - /// - /// # Arguments - /// - /// * `path` - Path to the Unity file (.unity, .prefab, .asset) - /// - /// # Examples - /// - /// ```no_run - /// use cursebreaker_parser::UnityProject; - /// - /// let mut project = UnityProject::new(100); - /// project.load_file("Assets/Scenes/MainMenu.unity")?; - /// # Ok::<(), cursebreaker_parser::Error>(()) - /// ``` - pub fn load_file(&mut self, path: impl Into) -> Result<()> { - let path = path.into(); - let file = UnityFile::from_path(&path)?; - - // Build file ID index for this file - for (idx, doc) in file.documents.iter().enumerate() { - self.file_id_index - .insert(doc.file_id, (path.clone(), idx)); - } - - // Extract GUID from .meta file for guid_to_path mapping - let meta_path = get_meta_path(&path); - if meta_path.exists() { - match MetaFile::from_path(&meta_path) { - Ok(meta_file) => { - self.guid_to_path.insert(meta_file.guid, path.clone()); - } - Err(e) => { - // Log warning but continue (graceful degradation) - eprintln!("Warning: Failed to parse .meta file {:?}: {}", meta_path, e); - } - } - } else { - // Log warning if .meta file doesn't exist - eprintln!("Warning: .meta file not found for {:?}", path); - } - - self.files.insert(path, file); - Ok(()) - } - - /// Load all Unity files from a directory (recursive) - /// - /// Walks the directory tree and loads all .unity, .prefab, and .asset files. - /// Gracefully handles parse errors by logging warnings and continuing. - /// - /// # Arguments - /// - /// * `dir` - Directory to search for Unity files - /// - /// # Returns - /// - /// Vec of successfully loaded file paths - /// - /// # Examples - /// - /// ```no_run - /// use cursebreaker_parser::UnityProject; - /// - /// let mut project = UnityProject::new(1000); - /// let loaded = project.load_directory("Assets")?; - /// println!("Loaded {} files", loaded.len()); - /// # Ok::<(), cursebreaker_parser::Error>(()) - /// ``` - pub fn load_directory(&mut self, dir: impl AsRef) -> Result> { - let mut loaded_files = Vec::new(); - - for entry in walkdir::WalkDir::new(dir) - .follow_links(true) - .into_iter() - .filter_map(|e| e.ok()) - { - let path = entry.path(); - if let Some(ext) = path.extension() { - let ext_str = ext.to_string_lossy(); - if ext_str == "unity" || ext_str == "prefab" || ext_str == "asset" { - match self.load_file(path) { - Ok(_) => loaded_files.push(path.to_path_buf()), - Err(e) => { - // Graceful degradation: log warning and continue - eprintln!("Warning: Failed to load {:?}: {}", path, e); - } - } - } - } - } - - Ok(loaded_files) - } - - /// Get a document by its file ID (eager resolution within same file) - /// - /// This performs instant lookup using the file ID index. - /// - /// # Arguments - /// - /// * `file_id` - The FileID to look up - /// - /// # Returns - /// - /// The UnityDocument if found, None otherwise - /// - /// # Examples - /// - /// ```no_run - /// use cursebreaker_parser::{UnityProject, types::FileID}; - /// - /// let project = UnityProject::new(100); - /// let doc = project.get_document(FileID::from_i64(12345)); - /// # Ok::<(), cursebreaker_parser::Error>(()) - /// ``` - pub fn get_document(&self, file_id: FileID) -> Option<&UnityDocument> { - let (path, idx) = self.file_id_index.get(&file_id)?; - self.files.get(path)?.documents.get(*idx) - } - - /// Resolve a reference (with caching) - /// - /// Handles three types of references: - /// - Null references: Return Ok(None) - /// - Local references: Eager lookup via file_id_index - /// - External references: Lazy resolution via GUID with LRU caching - /// - /// # Arguments - /// - /// * `reference` - The UnityReference to resolve - /// - /// # Returns - /// - /// The resolved UnityDocument if found, None if not found (including null refs) - /// - /// # Examples - /// - /// ```no_run - /// use cursebreaker_parser::{UnityProject, types::{UnityReference, FileID}}; - /// - /// let mut project = UnityProject::new(100); - /// let reference = UnityReference::Local(FileID::from_i64(12345)); - /// let doc = project.resolve_reference(&reference)?; - /// # Ok::<(), cursebreaker_parser::Error>(()) - /// ``` - pub fn resolve_reference( - &mut self, - reference: &UnityReference, - ) -> Result> { - match reference { - UnityReference::Null => Ok(None), - - UnityReference::Local(file_id) => { - // Eager resolution for same-file references (instant lookup) - Ok(self.get_document(*file_id)) - } - - UnityReference::External { guid, .. } => { - // Check cache first - if let Some(cached) = self.reference_cache.get(reference) { - if let Some((path, idx)) = cached { - let file = match self.files.get(path) { - Some(f) => f, - None => return Ok(None), - }; - return Ok(file.documents.get(*idx)); - } else { - return Ok(None); // Cached as unresolved - } - } - - // Lazy resolution for cross-file references - let result = self.resolve_external_guid(guid)?; - - // Cache the result (even if None) - self.reference_cache.put(reference.clone(), result.clone()); - - if let Some((path, idx)) = result { - let file = match self.files.get(&path) { - Some(f) => f, - None => return Ok(None), - }; - Ok(file.documents.get(idx)) - } else { - Ok(None) - } - } - } - } - - /// Resolve an external GUID reference (lazy, on-demand) - /// - /// Looks up the file path by GUID and returns the document index. - /// Logs a warning for unknown GUIDs (graceful degradation per user requirement). - /// - /// # Arguments - /// - /// * `guid` - The GUID to resolve - /// - /// # Returns - /// - /// Optional (PathBuf, document index) tuple - fn resolve_external_guid(&self, guid: &str) -> Result> { - // Look up the file path by GUID - let path = match self.guid_to_path.get(guid) { - Some(p) => p, - None => { - // Log warning for unknown GUID (graceful degradation per user requirement) - eprintln!("Warning: Failed to resolve external GUID: {}", guid); - return Ok(None); - } - }; - - // For now, return the first document in the file - // TODO: Enhance to support specific file ID within external file - Ok(Some((path.clone(), 0))) - } - - /// Get all loaded files in the project - /// - /// # Examples - /// - /// ```no_run - /// use cursebreaker_parser::UnityProject; - /// - /// let project = UnityProject::new(100); - /// println!("Project has {} files", project.files().len()); - /// ``` - pub fn files(&self) -> &HashMap { - &self.files - } - - /// Get a specific file by path - /// - /// # Arguments - /// - /// * `path` - Path to the Unity file - /// - /// # Examples - /// - /// ```no_run - /// use cursebreaker_parser::UnityProject; - /// - /// let project = UnityProject::new(100); - /// if let Some(file) = project.get_file("Assets/Scenes/MainMenu.unity") { - /// println!("File has {} documents", file.documents.len()); - /// } - /// ``` - pub fn get_file(&self, path: impl AsRef) -> Option<&UnityFile> { - self.files.get(path.as_ref()) - } - - /// Get the cache limit - pub fn cache_limit(&self) -> usize { - self.cache_limit - } - - /// Get the GUID to path mappings - /// - /// Returns a reference to the map of GUIDs to file paths. - /// - /// # Examples - /// - /// ```no_run - /// use cursebreaker_parser::UnityProject; - /// - /// let project = UnityProject::new(100); - /// println!("Project has {} GUID mappings", project.guid_mappings().len()); - /// ``` - pub fn guid_mappings(&self) -> &HashMap { - &self.guid_to_path - } - - /// Get the path for a specific GUID - /// - /// # Arguments - /// - /// * `guid` - The GUID to look up - /// - /// # Examples - /// - /// ```no_run - /// use cursebreaker_parser::UnityProject; - /// - /// let project = UnityProject::new(100); - /// if let Some(path) = project.get_path_by_guid("4ab6bfb0ff54cdf4c8dd38ca244d6f15") { - /// println!("Found asset at: {:?}", path); - /// } - /// ``` - pub fn get_path_by_guid(&self, guid: &str) -> Option<&PathBuf> { - self.guid_to_path.get(guid) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_new_project() { - let project = UnityProject::new(100); - assert_eq!(project.cache_limit(), 100); - assert_eq!(project.files().len(), 0); - } - - #[test] - fn test_load_single_file() { - let mut project = UnityProject::new(100); - let path = "data/tests/unity-sampleproject/PiratePanic/Assets/PiratePanic/Prefabs/Menu/Battle/Hand/CardGrabber.prefab"; - - if Path::new(path).exists() { - let result = project.load_file(path); - assert!(result.is_ok(), "Failed to load file: {:?}", result.err()); - assert_eq!(project.files().len(), 1); - } - } - - #[test] - fn test_load_directory() { - let mut project = UnityProject::new(100); - let dir = "data/tests/unity-sampleproject/PiratePanic/Assets/PiratePanic/Prefabs/Menu/Battle/Hand"; - - if Path::new(dir).exists() { - let result = project.load_directory(dir); - assert!(result.is_ok(), "Failed to load directory: {:?}", result.err()); - - if let Ok(loaded) = result { - assert!(loaded.len() > 0, "Should have loaded at least one file"); - } - } - } - - #[test] - fn test_get_document() { - let mut project = UnityProject::new(100); - let path = "data/tests/unity-sampleproject/PiratePanic/Assets/PiratePanic/Prefabs/Menu/Battle/Hand/CardGrabber.prefab"; - - if Path::new(path).exists() { - project.load_file(path).unwrap(); - - // Get the first document's file ID - if let Some(file) = project.files().values().next() { - if let Some(first_doc) = file.documents.first() { - let file_id = first_doc.file_id; - - // Test get_document - let found = project.get_document(file_id); - assert!(found.is_some(), "Should find document by file ID"); - assert_eq!(found.unwrap().file_id, file_id); - } - } - } - } - - #[test] - fn test_resolve_null_reference() { - let mut project = UnityProject::new(100); - let reference = UnityReference::Null; - - let result = project.resolve_reference(&reference); - assert!(result.is_ok()); - assert!(result.unwrap().is_none()); - } - - #[test] - fn test_resolve_local_reference() { - let mut project = UnityProject::new(100); - let path = "data/tests/unity-sampleproject/PiratePanic/Assets/PiratePanic/Prefabs/Menu/Battle/Hand/CardGrabber.prefab"; - - if Path::new(path).exists() { - project.load_file(path).unwrap(); - - // Get a valid FileID from the loaded file - if let Some(file) = project.files().values().next() { - if let Some(doc) = file.documents.first() { - let file_id = doc.file_id; - let reference = UnityReference::Local(file_id); - - let result = project.resolve_reference(&reference); - assert!(result.is_ok()); - assert!(result.unwrap().is_some()); - } - } - } - } - - #[test] - fn test_resolve_broken_reference() { - let mut project = UnityProject::new(100); - let reference = UnityReference::Local(FileID::from_i64(999999999)); - - let result = project.resolve_reference(&reference); - assert!(result.is_ok()); - assert!(result.unwrap().is_none()); - } - - #[test] - fn test_guid_mappings() { - let mut project = UnityProject::new(100); - let path = "data/tests/unity-sampleproject/PiratePanic/Assets/PiratePanic/Prefabs/Menu/Battle/Hand/CardGrabber.prefab"; - - if Path::new(path).exists() { - project.load_file(path).unwrap(); - - // Check if GUID mappings were loaded (depends on .meta file existence) - let guid_count = project.guid_mappings().len(); - if guid_count > 0 { - println!("Found {} GUID mappings", guid_count); - } - } - } - - #[test] - fn test_get_path_by_guid() { - let mut project = UnityProject::new(100); - let dir = "data/tests/unity-sampleproject/PiratePanic/Assets/PiratePanic/Prefabs/Menu/Battle/Hand"; - - if Path::new(dir).exists() { - let loaded = project.load_directory(dir).unwrap(); - - // If we loaded files, check if we can look up by GUID - if !loaded.is_empty() && !project.guid_mappings().is_empty() { - // Get the first GUID - if let Some((guid, expected_path)) = project.guid_mappings().iter().next() { - let found_path = project.get_path_by_guid(guid); - assert_eq!(found_path, Some(expected_path)); - } - } - } - } -} diff --git a/unity-parser/src/project/query.rs b/unity-parser/src/project/query.rs index b54e0bb..c1e5a6d 100644 --- a/unity-parser/src/project/query.rs +++ b/unity-parser/src/project/query.rs @@ -15,7 +15,7 @@ impl UnityProject { /// # Examples /// /// ```no_run - /// use cursebreaker_parser::UnityProject; + /// use unity_parser::UnityProject; /// /// let project = UnityProject::new(100); /// let game_objects = project.find_all_by_type(1); // GameObject = type 1 @@ -37,7 +37,7 @@ impl UnityProject { /// # Examples /// /// ```no_run - /// use cursebreaker_parser::UnityProject; + /// use unity_parser::UnityProject; /// /// let project = UnityProject::new(100); /// let transforms = project.find_all_by_class("Transform"); @@ -61,7 +61,7 @@ impl UnityProject { /// # Examples /// /// ```no_run - /// use cursebreaker_parser::UnityProject; + /// use unity_parser::UnityProject; /// /// let project = UnityProject::new(100); /// let players = project.find_by_name("Player"); @@ -97,7 +97,7 @@ impl UnityProject { /// # Examples /// /// ```no_run - /// use cursebreaker_parser::UnityProject; + /// use unity_parser::UnityProject; /// /// let project = UnityProject::new(100); /// # let game_object = &project.find_all_by_type(1)[0]; @@ -148,7 +148,7 @@ impl UnityProject { /// # Examples /// /// ```no_run - /// use cursebreaker_parser::UnityProject; + /// use unity_parser::UnityProject; /// /// let project = UnityProject::new(100); /// # let game_object = &project.find_all_by_type(1)[0]; diff --git a/unity-parser/src/types/guid.rs b/unity-parser/src/types/guid.rs index ee4d63d..6173d8c 100644 --- a/unity-parser/src/types/guid.rs +++ b/unity-parser/src/types/guid.rs @@ -18,7 +18,7 @@ use std::str::FromStr; /// # Example /// /// ``` -/// use cursebreaker_parser::Guid; +/// use unity_parser::Guid; /// use std::str::FromStr; /// /// // Parse from string @@ -36,7 +36,7 @@ impl Guid { /// # Example /// /// ``` - /// use cursebreaker_parser::Guid; + /// use unity_parser::Guid; /// /// let guid = Guid::from_u128(0x091c537484687e9419460cdcd7038234); /// ``` @@ -49,7 +49,7 @@ impl Guid { /// # Example /// /// ``` - /// use cursebreaker_parser::Guid; + /// use unity_parser::Guid; /// /// let guid = Guid::from_u128(42); /// assert_eq!(guid.as_u128(), 42); @@ -69,7 +69,7 @@ impl Guid { /// # Example /// /// ``` - /// use cursebreaker_parser::Guid; + /// use unity_parser::Guid; /// /// let guid = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap(); /// ``` @@ -97,7 +97,7 @@ impl Guid { /// # Example /// /// ``` - /// use cursebreaker_parser::Guid; + /// use unity_parser::Guid; /// use std::str::FromStr; /// /// let guid = Guid::from_str("091c537484687e9419460cdcd7038234").unwrap(); diff --git a/unity-parser/src/types/ids.rs b/unity-parser/src/types/ids.rs index c180009..b8308ac 100644 --- a/unity-parser/src/types/ids.rs +++ b/unity-parser/src/types/ids.rs @@ -7,7 +7,7 @@ use std::fmt; /// # Examples /// /// ``` -/// use cursebreaker_parser::FileID; +/// use unity_parser::FileID; /// /// let file_id = FileID::from_i64(1866116814460599870); /// assert_eq!(file_id.as_i64(), 1866116814460599870); diff --git a/unity-parser/src/types/reference.rs b/unity-parser/src/types/reference.rs index ea4a95d..15441c5 100644 --- a/unity-parser/src/types/reference.rs +++ b/unity-parser/src/types/reference.rs @@ -38,7 +38,7 @@ impl UnityReference { /// # Examples /// /// ``` - /// use cursebreaker_parser::types::{UnityReference, FileRef, FileID}; + /// use unity_parser::types::{UnityReference, FileRef, FileID}; /// /// // Local reference /// let file_ref = FileRef::new(FileID::from_i64(12345)); @@ -63,7 +63,7 @@ impl UnityReference { /// # Examples /// /// ``` - /// use cursebreaker_parser::types::{UnityReference, ExternalRef}; + /// use unity_parser::types::{UnityReference, ExternalRef}; /// /// let ext_ref = ExternalRef::new("abc123".to_string(), 2); /// let reference = UnityReference::from_external_ref(&ext_ref); @@ -82,7 +82,7 @@ impl UnityReference { /// # Examples /// /// ``` - /// use cursebreaker_parser::types::UnityReference; + /// use unity_parser::types::UnityReference; /// /// let reference = UnityReference::Null; /// assert!(reference.is_null()); @@ -96,7 +96,7 @@ impl UnityReference { /// # Examples /// /// ``` - /// use cursebreaker_parser::types::{UnityReference, FileID}; + /// use unity_parser::types::{UnityReference, FileID}; /// /// let reference = UnityReference::Local(FileID::from_i64(12345)); /// assert!(reference.is_local()); @@ -110,7 +110,7 @@ impl UnityReference { /// # Examples /// /// ``` - /// use cursebreaker_parser::types::UnityReference; + /// use unity_parser::types::UnityReference; /// /// let reference = UnityReference::External { /// guid: "abc123".to_string(), @@ -130,7 +130,7 @@ impl UnityReference { /// # Examples /// /// ``` - /// use cursebreaker_parser::types::{UnityReference, FileID}; + /// use unity_parser::types::{UnityReference, FileID}; /// /// let reference = UnityReference::Local(FileID::from_i64(12345)); /// assert_eq!(reference.as_file_id(), Some(FileID::from_i64(12345))); @@ -152,7 +152,7 @@ impl UnityReference { /// # Examples /// /// ``` - /// use cursebreaker_parser::types::UnityReference; + /// use unity_parser::types::UnityReference; /// /// let reference = UnityReference::External { /// guid: "abc123".to_string(), diff --git a/unity-parser/src/types/type_filter.rs b/unity-parser/src/types/type_filter.rs index 162ed41..f6de0ad 100644 --- a/unity-parser/src/types/type_filter.rs +++ b/unity-parser/src/types/type_filter.rs @@ -47,7 +47,7 @@ impl TypeFilter { /// /// # Example /// ``` - /// use cursebreaker_parser::TypeFilter; + /// use unity_parser::TypeFilter; /// /// let filter = TypeFilter::new( /// vec!["Transform", "Camera", "Light"], diff --git a/unity-parser/src/types/type_registry.rs b/unity-parser/src/types/type_registry.rs index e2e263c..60ad69f 100644 --- a/unity-parser/src/types/type_registry.rs +++ b/unity-parser/src/types/type_registry.rs @@ -262,7 +262,7 @@ pub static UNITY_TYPE_REGISTRY: Lazy = Lazy::new(|| { /// # Examples /// /// ``` -/// use cursebreaker_parser::types::get_class_name; +/// use unity_parser::types::get_class_name; /// /// assert_eq!(get_class_name(1), Some("GameObject")); /// assert_eq!(get_class_name(4), Some("Transform")); @@ -279,7 +279,7 @@ pub fn get_class_name(type_id: u32) -> Option<&'static str> { /// # Examples /// /// ``` -/// use cursebreaker_parser::types::get_type_id; +/// use unity_parser::types::get_type_id; /// /// assert_eq!(get_type_id("GameObject"), Some(1)); /// assert_eq!(get_type_id("Transform"), Some(4)); diff --git a/unity-parser/src/types/unity_types/transform.rs b/unity-parser/src/types/unity_types/transform.rs index 101fb4b..4a79300 100644 --- a/unity-parser/src/types/unity_types/transform.rs +++ b/unity-parser/src/types/unity_types/transform.rs @@ -139,7 +139,7 @@ impl crate::types::EcsInsertable for Transform { /// # Examples /// /// ```no_run -/// use cursebreaker_parser::{UnityFile, types::RectTransform}; +/// use unity_parser::{UnityFile, types::RectTransform}; /// /// let file = UnityFile::from_path("Canvas.prefab")?; /// match file { @@ -152,7 +152,7 @@ impl crate::types::EcsInsertable for Transform { /// } /// _ => {} /// } -/// # Ok::<(), cursebreaker_parser::Error>(()) +/// # Ok::<(), unity_parser::Error>(()) /// ``` #[derive(Debug, Clone)] pub struct RectTransform { diff --git a/unity-parser/src/types/values.rs b/unity-parser/src/types/values.rs index d4fda34..cb13e13 100644 --- a/unity-parser/src/types/values.rs +++ b/unity-parser/src/types/values.rs @@ -10,7 +10,7 @@ pub use glam::{Quat as Quaternion, Vec2 as Vector2, Vec3 as Vector3, Vec4 as Col /// # Examples /// /// ``` -/// use cursebreaker_parser::{FileRef, FileID}; +/// use unity_parser::{FileRef, FileID}; /// /// let file_ref = FileRef::new(FileID::from_i64(12345)); /// assert_eq!(file_ref.file_id.as_i64(), 12345); @@ -32,7 +32,7 @@ impl FileRef { /// # Examples /// /// ``` -/// use cursebreaker_parser::ExternalRef; +/// use unity_parser::ExternalRef; /// /// let ext_ref = ExternalRef::new("abc123".to_string(), 2); /// assert_eq!(ext_ref.guid, "abc123"); diff --git a/unity-parser/tests/test_guid_resolution.rs.disabled b/unity-parser/tests/test_guid_resolution.rs.disabled deleted file mode 100644 index ca81221..0000000 --- a/unity-parser/tests/test_guid_resolution.rs.disabled +++ /dev/null @@ -1,83 +0,0 @@ -//! Integration test for GUID resolution with .meta files - -use cursebreaker_parser::parser::meta::{get_meta_path, MetaFile}; -use cursebreaker_parser::UnityProject; -use std::path::Path; - -#[test] -fn test_meta_file_parsing() { - // Test parsing a .meta file directly - let meta_content = r#" -fileFormatVersion: 2 -guid: 4ab6bfb0ff54cdf4c8dd38ca244d6f15 -PrefabImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: -"#; - - let meta = MetaFile::from_str(meta_content).unwrap(); - assert_eq!(meta.guid(), "4ab6bfb0ff54cdf4c8dd38ca244d6f15"); - assert_eq!(meta.file_format_version(), Some(2)); -} - -#[test] -fn test_meta_path_generation() { - let asset_path = Path::new("Assets/Prefabs/Player.prefab"); - let meta_path = get_meta_path(asset_path); - - assert_eq!( - meta_path, - Path::new("Assets/Prefabs/Player.prefab.meta") - ); -} - -#[test] -fn test_guid_resolution_in_project() { - let mut project = UnityProject::new(100); - let test_dir = "data/tests/unity-sampleproject/PiratePanic/Assets/PiratePanic/Prefabs/Menu/Battle/Hand"; - - if Path::new(test_dir).exists() { - // Load all files in the directory - let loaded_files = project.load_directory(test_dir).unwrap(); - - if !loaded_files.is_empty() { - println!("Loaded {} files", loaded_files.len()); - println!( - "Found {} GUID mappings", - project.guid_mappings().len() - ); - - // If we have GUID mappings, test that we can look them up - for (guid, path) in project.guid_mappings() { - let found_path = project.get_path_by_guid(guid); - assert_eq!(found_path, Some(path)); - println!("GUID {} -> {:?}", guid, path); - } - } - } -} - -#[test] -fn test_cross_file_reference_resolution() { - let mut project = UnityProject::new(100); - let test_dir = "data/tests/unity-sampleproject/PiratePanic/Assets/PiratePanic"; - - if Path::new(test_dir).exists() { - // Load multiple directories to enable cross-file resolution - let _ = project.load_directory(test_dir); - - let guid_count = project.guid_mappings().len(); - let file_count = project.files().len(); - - println!("Loaded {} files with {} GUID mappings", file_count, guid_count); - - // Verify we can look up files by GUID - if guid_count > 0 { - let sample_guid = project.guid_mappings().keys().next().unwrap(); - let path = project.get_path_by_guid(sample_guid); - assert!(path.is_some(), "Should be able to look up GUID"); - } - } -}