Compare commits
8 Commits
17ea08caac
...
8a06185b98
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a06185b98 | |||
| bfd451aca9 | |||
| ff3c092d9e | |||
| 0552b4dff0 | |||
| cd35339151 | |||
| 6f40cb9177 | |||
| 673fa114fa | |||
| 7e3d338373 |
@@ -10,7 +10,12 @@
|
||||
"Bash(findstr:*)",
|
||||
"Bash(cargo check:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(find:*)"
|
||||
"Bash(find:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(wc:*)"
|
||||
],
|
||||
"additionalDirectories": [
|
||||
"/home/connor/repos/CBAssets/"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
56
Cargo.lock
generated
56
Cargo.lock
generated
@@ -23,34 +23,6 @@ version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c"
|
||||
|
||||
[[package]]
|
||||
name = "cursebreaker-parser"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cursebreaker-parser-macros",
|
||||
"glam",
|
||||
"indexmap",
|
||||
"inventory",
|
||||
"lru",
|
||||
"once_cell",
|
||||
"pretty_assertions",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_yaml",
|
||||
"sparsey",
|
||||
"thiserror",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cursebreaker-parser-macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
@@ -318,6 +290,34 @@ version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "unity-parser"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"glam",
|
||||
"indexmap",
|
||||
"inventory",
|
||||
"lru",
|
||||
"once_cell",
|
||||
"pretty_assertions",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_yaml",
|
||||
"sparsey",
|
||||
"thiserror",
|
||||
"unity-parser-macros",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unity-parser-macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[workspace]
|
||||
members = ["cursebreaker-parser", "cursebreaker-parser-macros"]
|
||||
members = ["unity-parser", "unity-parser-macros"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Your Name <your.email@example.com>"]
|
||||
authors = ["connordemeyer@gmail.com"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/yourusername/cursebreaker-parser-rust"
|
||||
repository = "https://github.com/yourusername/unity-parser-rust"
|
||||
|
||||
403
README.md
Normal file
403
README.md
Normal file
@@ -0,0 +1,403 @@
|
||||
# Cursebreaker Unity Parser
|
||||
|
||||
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.
|
||||
|
||||
[](https://www.rust-lang.org/)
|
||||
[](LICENSE)
|
||||
|
||||
**⚠️ Work in Progress**: This library is under active development. APIs may change.
|
||||
|
||||
## Features
|
||||
|
||||
### 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 YAML parsing with minimal allocations
|
||||
|
||||
### 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 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 (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 stored as u128 (16 bytes vs ~56 bytes for String)
|
||||
|
||||
## Installation
|
||||
|
||||
Add to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
unity_parser = { path = "unity-parser" }
|
||||
sparsey = "0.13" # For ECS queries
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Parse a Unity Scene
|
||||
|
||||
```rust
|
||||
use unity_parser::UnityFile;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let file = UnityFile::from_path("Scene.unity")?;
|
||||
|
||||
match file {
|
||||
UnityFile::Scene(scene) => {
|
||||
println!("Scene with {} entities", scene.entity_map.len());
|
||||
|
||||
// Query transforms
|
||||
let transforms = scene.world.borrow::<unity_parser::Transform>();
|
||||
for (file_id, entity) in &scene.entity_map {
|
||||
if let Some(transform) = transforms.get(*entity) {
|
||||
println!("Entity {} at {:?}", file_id, transform.local_position());
|
||||
}
|
||||
}
|
||||
}
|
||||
UnityFile::Prefab(prefab) => {
|
||||
println!("Prefab with {} documents", prefab.documents.len());
|
||||
}
|
||||
UnityFile::Asset(asset) => {
|
||||
println!("Asset with {} documents", asset.documents.len());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Parse a Prefab
|
||||
|
||||
```rust
|
||||
use unity_parser::UnityFile;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
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 {
|
||||
pub volume: f64,
|
||||
pub start_time: f64,
|
||||
pub end_time: f64,
|
||||
pub is_loop: bool,
|
||||
}
|
||||
|
||||
// Manual implementation (recommended until macro is fixed)
|
||||
impl UnityComponent for PlaySFX {
|
||||
fn parse(yaml: &Mapping, _ctx: &ComponentContext) -> Option<Self> {
|
||||
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:
|
||||
|
||||
```
|
||||
Unity Scene File Rust Code
|
||||
───────────────── ─────────
|
||||
MonoBehaviour: Custom component
|
||||
m_Script: in ECS World
|
||||
guid: 091c537... ──────────>
|
||||
volume: 1.0
|
||||
isLoop: 0
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
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 components
|
||||
|
||||
The GUID resolver builds automatically when parsing scenes if a Unity project root is detected.
|
||||
|
||||
## Type Filtering
|
||||
|
||||
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;
|
||||
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(
|
||||
Path::new("Assets/Scenes/Production/Level1.unity"),
|
||||
Some(&filter),
|
||||
None
|
||||
)?;
|
||||
```
|
||||
|
||||
## Supported Unity Types
|
||||
|
||||
### Built-in Components
|
||||
- ✅ GameObject
|
||||
- ✅ Transform
|
||||
- ✅ RectTransform
|
||||
- ✅ PrefabInstance
|
||||
|
||||
### Value Types
|
||||
- ✅ Vector2, Vector3
|
||||
- ✅ Quaternion
|
||||
- ✅ Color
|
||||
- ✅ FileID, GUID
|
||||
- ✅ ExternalRef, FileRef
|
||||
|
||||
### Custom Components
|
||||
- ✅ Any `MonoBehaviour` via manual `UnityComponent` implementation
|
||||
- ⚠️ `#[derive(UnityComponent)]` macro (has namespace bugs, not recommended)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Component Flow
|
||||
|
||||
```
|
||||
Unity Scene File
|
||||
↓
|
||||
Raw YAML Documents
|
||||
↓
|
||||
┌─────────────────────┐
|
||||
│ GUID Resolution │ ← .meta files + .cs files
|
||||
│ (MonoBehaviour) │ (Script GUID → Class Name)
|
||||
└─────────────────────┘
|
||||
↓
|
||||
┌─────────────────────┐
|
||||
│ Prefab GUID Res. │ ← .prefab.meta files
|
||||
│ (Nested Prefabs) │ (Prefab GUID → Prefab Path)
|
||||
└─────────────────────┘
|
||||
↓
|
||||
┌─────────────────────┐
|
||||
│ Component Registry │ ← UnityComponent trait impls
|
||||
│ (inventory) │
|
||||
└─────────────────────┘
|
||||
↓
|
||||
┌─────────────────────┐
|
||||
│ ECS World │ ← Sparsey entities & components
|
||||
│ (Transform, etc) │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
### Memory Efficiency
|
||||
|
||||
**GUID Storage**:
|
||||
- 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 HashMap lookups
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
## Known Issues
|
||||
|
||||
### 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
|
||||
|
||||
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
|
||||
|
||||
3. **Placeholder values in Cargo.toml**
|
||||
- Author and repository fields need updating
|
||||
|
||||
### Minor Issues
|
||||
1. Disabled example/test files may reference outdated APIs
|
||||
2. Some examples may have incorrect YAML access patterns
|
||||
|
||||
## Running Examples
|
||||
|
||||
```bash
|
||||
# Parse basic Unity file
|
||||
cargo run --example basic_parsing
|
||||
|
||||
# Custom component parsing (requires Unity project)
|
||||
cargo run --example custom_component
|
||||
|
||||
# ECS integration showcase
|
||||
cargo run --example ecs_integration
|
||||
|
||||
# Find PlaySFX components (requires CBAssets project)
|
||||
cargo run --example find_playsfx
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Unit tests
|
||||
cargo test --lib
|
||||
|
||||
# Integration tests
|
||||
cargo test --test integration_tests
|
||||
|
||||
# All tests
|
||||
cargo test
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
### 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
|
||||
|
||||
### In Progress / Needs Work
|
||||
- 🔧 Refactor `project` module for new architecture
|
||||
- 🔧 Update disabled example/test files
|
||||
- 🔧 Fix example code YAML access patterns
|
||||
|
||||
### Future Enhancements
|
||||
- [ ] Full prefab modification system
|
||||
- [ ] Persistent GUID cache for instant loading
|
||||
- [ ] Watch mode for live Unity project monitoring
|
||||
- [ ] More built-in Unity component types
|
||||
- [ ] Better error messages with line numbers
|
||||
- [ ] Parallel processing support
|
||||
- [ ] Cross-platform path handling improvements
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions welcome! Areas needing help:
|
||||
|
||||
- **Documentation**: API docs, more examples, tutorials
|
||||
- **Testing**: Integration tests with real Unity projects
|
||||
- **Performance**: Optimize YAML parsing, parallel processing
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of:
|
||||
|
||||
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
|
||||
- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
||||
|
||||
at your option.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- **Unity Technologies**: For the YAML-based file format
|
||||
- **Sparsey**: ECS library for component storage
|
||||
- **serde_yaml**: YAML parsing foundation
|
||||
- **inventory**: Compile-time component registration
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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(())
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
//! ECS world building from Unity documents
|
||||
|
||||
use crate::model::RawDocument;
|
||||
use crate::types::{
|
||||
yaml_helpers, ComponentContext, FileID, GameObject, LinkingContext, PrefabInstanceComponent,
|
||||
RectTransform, Transform, TypeFilter, UnityComponent,
|
||||
};
|
||||
use crate::{Error, Result};
|
||||
use sparsey::{Entity, World};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Build a Sparsey ECS World from raw Unity documents
|
||||
///
|
||||
/// This uses a 3-pass approach:
|
||||
/// 1. Create entities for all GameObjects
|
||||
/// 2. Attach components (Transform, RectTransform, etc.) to entities
|
||||
/// 3. Resolve Transform hierarchy (parent/children Entity references)
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `documents`: Parsed Unity documents to build the world from
|
||||
///
|
||||
/// # Returns
|
||||
/// A tuple of (World, FileID → Entity mapping)
|
||||
pub fn build_world_from_documents(
|
||||
documents: Vec<RawDocument>,
|
||||
) -> Result<(World, HashMap<FileID, Entity>)> {
|
||||
// Create World with registered component types
|
||||
let mut world = World::builder()
|
||||
.register::<GameObject>()
|
||||
.register::<Transform>()
|
||||
.register::<RectTransform>()
|
||||
.register::<PrefabInstanceComponent>()
|
||||
.build();
|
||||
|
||||
let linking_ctx = RefCell::new(LinkingContext::new());
|
||||
|
||||
// PASS 1: Create entities for all GameObjects
|
||||
for doc in documents.iter().filter(|d| d.type_id == 1 || d.class_name == "GameObject") {
|
||||
let entity = spawn_game_object(&mut world, doc)?;
|
||||
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
|
||||
}
|
||||
|
||||
// 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, &type_filter)?;
|
||||
}
|
||||
|
||||
// PASS 3: Execute all deferred linking callbacks
|
||||
let entity_map = linking_ctx.into_inner().execute_callbacks(&mut world);
|
||||
|
||||
Ok((world, entity_map))
|
||||
}
|
||||
|
||||
/// Build entities from raw Unity documents into an existing world
|
||||
///
|
||||
/// This is similar to `build_world_from_documents` but spawns into an existing
|
||||
/// world instead of creating a new one. This is used for prefab instantiation.
|
||||
///
|
||||
/// Uses the same 3-pass approach:
|
||||
/// 1. Create entities for all GameObjects
|
||||
/// 2. Attach components (Transform, RectTransform, etc.) to entities
|
||||
/// 3. Resolve Transform hierarchy (parent/children Entity references)
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `documents`: Parsed Unity documents to build entities from
|
||||
/// - `world`: Existing Sparsey ECS world to spawn entities into
|
||||
/// - `entity_map`: Existing entity map to merge new mappings into
|
||||
///
|
||||
/// # Returns
|
||||
/// Vec of newly spawned entities
|
||||
pub fn build_world_from_documents_into(
|
||||
documents: Vec<RawDocument>,
|
||||
world: &mut World,
|
||||
entity_map: &mut HashMap<FileID, Entity>,
|
||||
) -> Result<Vec<Entity>> {
|
||||
let linking_ctx = RefCell::new(LinkingContext::new());
|
||||
|
||||
// Initialize linking context with existing entity_map
|
||||
// This allows cross-references between prefab instances and scene entities
|
||||
*linking_ctx.borrow_mut().entity_map_mut() = entity_map.clone();
|
||||
|
||||
let mut spawned_entities = Vec::new();
|
||||
|
||||
// PASS 1: Create entities for all GameObjects
|
||||
for doc in documents.iter().filter(|d| d.type_id == 1 || d.class_name == "GameObject") {
|
||||
let entity = spawn_game_object(world, doc)?;
|
||||
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
|
||||
spawned_entities.push(entity);
|
||||
}
|
||||
|
||||
// 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, &type_filter)?;
|
||||
}
|
||||
|
||||
// PASS 3: Execute all deferred linking callbacks
|
||||
let final_entity_map = linking_ctx.into_inner().execute_callbacks(world);
|
||||
|
||||
// Update caller's entity_map with new mappings
|
||||
entity_map.extend(final_entity_map);
|
||||
|
||||
Ok(spawned_entities)
|
||||
}
|
||||
|
||||
/// Spawn a GameObject entity
|
||||
fn spawn_game_object(world: &mut World, doc: &RawDocument) -> Result<Entity> {
|
||||
let yaml = doc
|
||||
.as_mapping()
|
||||
.ok_or_else(|| Error::invalid_format("GameObject YAML must be mapping"))?;
|
||||
|
||||
let ctx = ComponentContext {
|
||||
type_id: doc.type_id,
|
||||
file_id: doc.file_id,
|
||||
class_name: &doc.class_name,
|
||||
entity: None,
|
||||
linking_ctx: None,
|
||||
yaml,
|
||||
};
|
||||
|
||||
let go = GameObject::parse(yaml, &ctx)
|
||||
.ok_or_else(|| Error::invalid_format("Failed to parse GameObject"))?;
|
||||
|
||||
// Create entity with GameObject component
|
||||
let entity = world.create((go,));
|
||||
|
||||
Ok(entity)
|
||||
}
|
||||
|
||||
/// Attach a component to its GameObject entity
|
||||
fn attach_component(
|
||||
world: &mut World,
|
||||
doc: &RawDocument,
|
||||
linking_ctx: &RefCell<LinkingContext>,
|
||||
type_filter: &TypeFilter,
|
||||
) -> Result<()> {
|
||||
let yaml = doc
|
||||
.as_mapping()
|
||||
.ok_or_else(|| Error::invalid_format("Component YAML must be mapping"))?;
|
||||
|
||||
// Get m_GameObject reference to find which entity owns this component
|
||||
let go_ref = yaml_helpers::get_file_ref(yaml, "m_GameObject");
|
||||
|
||||
let entity = match go_ref {
|
||||
Some(r) => linking_ctx
|
||||
.borrow()
|
||||
.entity_map()
|
||||
.get(&r.file_id)
|
||||
.copied()
|
||||
.ok_or_else(|| {
|
||||
Error::reference_error(format!("Unknown GameObject: {}", r.file_id))
|
||||
})?,
|
||||
None => {
|
||||
// Some components might not have m_GameObject (e.g., standalone assets)
|
||||
eprintln!(
|
||||
"Warning: Component {} has no m_GameObject reference",
|
||||
doc.class_name
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let ctx = ComponentContext {
|
||||
type_id: doc.type_id,
|
||||
file_id: doc.file_id,
|
||||
class_name: &doc.class_name,
|
||||
entity: Some(entity),
|
||||
linking_ctx: Some(linking_ctx),
|
||||
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" => {
|
||||
if let Some(transform) = Transform::parse(yaml, &ctx) {
|
||||
world.insert(entity, (transform,));
|
||||
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
|
||||
}
|
||||
}
|
||||
"RectTransform" => {
|
||||
if let Some(rect) = RectTransform::parse(yaml, &ctx) {
|
||||
world.insert(entity, (rect,));
|
||||
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
|
||||
}
|
||||
}
|
||||
"PrefabInstance" => {
|
||||
// Parse and store nested prefab reference
|
||||
if let Some(prefab_comp) = PrefabInstanceComponent::parse(yaml, &ctx) {
|
||||
world.insert(entity, (prefab_comp,));
|
||||
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Check if this is a registered custom component
|
||||
let mut found_custom = false;
|
||||
for reg in inventory::iter::<crate::types::ComponentRegistration> {
|
||||
if reg.class_name == doc.class_name.as_str() {
|
||||
found_custom = true;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
if !found_custom {
|
||||
// Unknown component type - skip with warning
|
||||
eprintln!(
|
||||
"Warning: Skipping unknown component type: {}",
|
||||
doc.class_name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
//! Unity YAML parsing module
|
||||
|
||||
pub mod meta;
|
||||
mod unity_tag;
|
||||
mod yaml;
|
||||
|
||||
pub use meta::{get_meta_path, MetaFile};
|
||||
pub use unity_tag::{parse_unity_tag, UnityTag};
|
||||
pub use yaml::split_yaml_documents;
|
||||
|
||||
use crate::model::{RawDocument, UnityAsset, UnityFile, UnityPrefab, UnityScene};
|
||||
use crate::types::FileID;
|
||||
use crate::{Error, Result};
|
||||
use std::path::Path;
|
||||
|
||||
/// Parse a Unity file from the given path
|
||||
///
|
||||
/// Automatically detects file type based on extension:
|
||||
/// - .unity → UnityFile::Scene with ECS World
|
||||
/// - .prefab → UnityFile::Prefab with raw YAML
|
||||
/// - .asset → UnityFile::Asset with raw YAML
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use cursebreaker_parser::parser::parse_unity_file;
|
||||
/// use cursebreaker_parser::UnityFile;
|
||||
/// use std::path::Path;
|
||||
///
|
||||
/// let file = parse_unity_file(Path::new("Scene.unity"))?;
|
||||
/// match file {
|
||||
/// UnityFile::Scene(scene) => println!("Scene with {} entities", scene.entity_map.len()),
|
||||
/// UnityFile::Prefab(prefab) => println!("Prefab with {} documents", prefab.documents.len()),
|
||||
/// UnityFile::Asset(asset) => println!("Asset with {} documents", asset.documents.len()),
|
||||
/// }
|
||||
/// # Ok::<(), cursebreaker_parser::Error>(())
|
||||
/// ```
|
||||
pub fn parse_unity_file(path: &Path) -> Result<UnityFile> {
|
||||
// Read the file
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
|
||||
// Validate Unity header
|
||||
validate_unity_header(&content, path)?;
|
||||
|
||||
// Detect file type by extension
|
||||
let file_type = detect_file_type(path);
|
||||
|
||||
// Parse based on file type
|
||||
match file_type {
|
||||
FileType::Scene => parse_scene(path, &content),
|
||||
FileType::Prefab => parse_prefab(path, &content),
|
||||
FileType::Asset => parse_asset(path, &content),
|
||||
FileType::Unknown => Err(Error::invalid_format(format!(
|
||||
"Unknown file extension: {}",
|
||||
path.display()
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// File type enumeration
|
||||
enum FileType {
|
||||
Scene,
|
||||
Prefab,
|
||||
Asset,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Detect file type based on extension
|
||||
fn detect_file_type(path: &Path) -> FileType {
|
||||
match path.extension().and_then(|s| s.to_str()) {
|
||||
Some("unity") => FileType::Scene,
|
||||
Some("prefab") => FileType::Prefab,
|
||||
Some("asset") => FileType::Asset,
|
||||
_ => FileType::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a scene file into an ECS World
|
||||
fn parse_scene(path: &Path, content: &str) -> Result<UnityFile> {
|
||||
let raw_documents = parse_raw_documents(content)?;
|
||||
|
||||
// Build ECS world from documents
|
||||
let (world, entity_map) = crate::ecs::build_world_from_documents(raw_documents)?;
|
||||
|
||||
Ok(UnityFile::Scene(UnityScene::new(
|
||||
path.to_path_buf(),
|
||||
world,
|
||||
entity_map,
|
||||
)))
|
||||
}
|
||||
|
||||
/// Parse a prefab file into raw YAML documents
|
||||
fn parse_prefab(path: &Path, content: &str) -> Result<UnityFile> {
|
||||
let raw_documents = parse_raw_documents(content)?;
|
||||
|
||||
Ok(UnityFile::Prefab(UnityPrefab::new(
|
||||
path.to_path_buf(),
|
||||
raw_documents,
|
||||
)))
|
||||
}
|
||||
|
||||
/// Parse an asset file into raw YAML documents
|
||||
fn parse_asset(path: &Path, content: &str) -> Result<UnityFile> {
|
||||
let raw_documents = parse_raw_documents(content)?;
|
||||
|
||||
Ok(UnityFile::Asset(UnityAsset::new(
|
||||
path.to_path_buf(),
|
||||
raw_documents,
|
||||
)))
|
||||
}
|
||||
|
||||
/// Parse raw YAML documents from file content
|
||||
fn parse_raw_documents(content: &str) -> Result<Vec<RawDocument>> {
|
||||
// Split into individual YAML documents
|
||||
let raw_docs = split_yaml_documents(content)?;
|
||||
|
||||
// Parse each document
|
||||
raw_docs
|
||||
.iter()
|
||||
.filter_map(|raw| parse_raw_document(raw).transpose())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Parse a single raw YAML document into a RawDocument
|
||||
fn parse_raw_document(raw_doc: &str) -> Result<Option<RawDocument>> {
|
||||
// Parse the Unity tag line (e.g., "--- !u!1 &12345")
|
||||
let tag = match parse_unity_tag(raw_doc) {
|
||||
Some(tag) => tag,
|
||||
None => return Ok(None), // Skip documents without Unity tags
|
||||
};
|
||||
|
||||
// Extract the YAML content (everything after the tag line)
|
||||
let yaml_content = extract_yaml_content(raw_doc);
|
||||
|
||||
if yaml_content.trim().is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Parse YAML but don't convert to PropertyValue yet
|
||||
let yaml_value: serde_yaml::Value = serde_yaml::from_str(yaml_content)?;
|
||||
|
||||
// Unity documents have format "GameObject: { ... }"
|
||||
// Extract class name and inner YAML
|
||||
let (class_name, inner_yaml) = match &yaml_value {
|
||||
serde_yaml::Value::Mapping(map) if map.len() == 1 => {
|
||||
// Single-key mapping - this is the standard Unity format
|
||||
let (key, value) = map.iter().next().unwrap();
|
||||
let class_name = key
|
||||
.as_str()
|
||||
.ok_or_else(|| Error::invalid_format("Class name must be string"))?
|
||||
.to_string();
|
||||
(class_name, value.clone())
|
||||
}
|
||||
_ => {
|
||||
// Fallback for malformed documents
|
||||
let class_name = format!("UnityType{}", tag.type_id);
|
||||
(class_name, yaml_value)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Some(RawDocument::new(
|
||||
tag.type_id,
|
||||
FileID::from_i64(tag.file_id),
|
||||
class_name,
|
||||
inner_yaml,
|
||||
)))
|
||||
}
|
||||
|
||||
/// Validate that the file has a proper Unity YAML header
|
||||
fn validate_unity_header(content: &str, path: &Path) -> Result<()> {
|
||||
let has_yaml_header = content.starts_with("%YAML");
|
||||
let has_unity_tag = content.contains("%TAG !u! tag:unity3d.com");
|
||||
|
||||
if !has_yaml_header || !has_unity_tag {
|
||||
return Err(Error::MissingHeader(path.to_path_buf()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract the YAML content from a raw document (skip the Unity tag line)
|
||||
fn extract_yaml_content(raw_doc: &str) -> &str {
|
||||
// Find the first newline after the "--- !u!" tag
|
||||
if let Some(first_line_end) = raw_doc.find('\n') {
|
||||
&raw_doc[first_line_end + 1..]
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_validate_unity_header() {
|
||||
let valid_content = "%YAML 1.1\n%TAG !u! tag:unity3d.com,2011:\n";
|
||||
assert!(validate_unity_header(valid_content, Path::new("test.unity")).is_ok());
|
||||
|
||||
let invalid_content = "Not a Unity file";
|
||||
assert!(validate_unity_header(invalid_content, Path::new("test.unity")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_yaml_content() {
|
||||
let raw_doc = "--- !u!1 &12345\nGameObject:\n m_Name: Test";
|
||||
let content = extract_yaml_content(raw_doc);
|
||||
assert_eq!(content, "GameObject:\n m_Name: Test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_file_type() {
|
||||
assert!(matches!(
|
||||
detect_file_type(Path::new("test.unity")),
|
||||
FileType::Scene
|
||||
));
|
||||
assert!(matches!(
|
||||
detect_file_type(Path::new("test.prefab")),
|
||||
FileType::Prefab
|
||||
));
|
||||
assert!(matches!(
|
||||
detect_file_type(Path::new("test.asset")),
|
||||
FileType::Asset
|
||||
));
|
||||
assert!(matches!(
|
||||
detect_file_type(Path::new("test.txt")),
|
||||
FileType::Unknown
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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<PathBuf, UnityFile>,
|
||||
|
||||
/// GUID to file path mapping for cross-file reference resolution
|
||||
guid_to_path: HashMap<String, PathBuf>,
|
||||
|
||||
/// FileID to (PathBuf, document index) mapping for fast lookups
|
||||
file_id_index: HashMap<FileID, (PathBuf, usize)>,
|
||||
|
||||
/// LRU cache for resolved references
|
||||
reference_cache: LruCache<UnityReference, Option<(PathBuf, usize)>>,
|
||||
|
||||
/// 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<PathBuf>) -> 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<Path>) -> Result<Vec<PathBuf>> {
|
||||
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<Option<&UnityDocument>> {
|
||||
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<Option<(PathBuf, usize)>> {
|
||||
// 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<PathBuf, UnityFile> {
|
||||
&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<Path>) -> 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<String, PathBuf> {
|
||||
&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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
//! 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"));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
693
resource_prefabs_output.txt
Normal file
693
resource_prefabs_output.txt
Normal file
@@ -0,0 +1,693 @@
|
||||
Cursebreaker Resource Prefabs
|
||||
======================================================================
|
||||
|
||||
Total resources found: 171
|
||||
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Prefab: HarvestableSpawner_73Medicinal Herbs
|
||||
TypeID: 73
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_65Radishes
|
||||
TypeID: 65
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_22Sandstone Grouper
|
||||
TypeID: 22
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_14Mageflower
|
||||
TypeID: 14
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_108mountain Ore3
|
||||
TypeID: 108
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_120Swamp Brambles
|
||||
TypeID: 120
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_20Caveshroom
|
||||
TypeID: 20
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_30HeartbellVine
|
||||
TypeID: 30
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_76Speed herb
|
||||
TypeID: 76
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_22SandstoneGrouper
|
||||
TypeID: 22
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: Prefab_Resource_64Carrot_Field
|
||||
TypeID: 64
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_26Betta Iotachi
|
||||
TypeID: 26
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_85Magic res ampoule crafting mat
|
||||
TypeID: 85
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_15Blueberries
|
||||
TypeID: 15
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_58Radiant Sunflower
|
||||
TypeID: 58
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_76Waspheart Iris
|
||||
TypeID: 76
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_30Heartbell Vine
|
||||
TypeID: 30
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_61Eggplants
|
||||
TypeID: 61
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_8Deadwood
|
||||
TypeID: 0
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_70Cabbage
|
||||
TypeID: 70
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_19Coal
|
||||
TypeID: 19
|
||||
MaxHealth: 20
|
||||
|
||||
Prefab: Prefab_Resource_12PebbleSturgeon
|
||||
TypeID: 12
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: Prefab_Resource_6Oak
|
||||
TypeID: 6
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_2Copper Ore
|
||||
TypeID: 2
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_111Deep Earth Moss
|
||||
TypeID: 111
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_49Dreamylion
|
||||
TypeID: 49
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_71Nest
|
||||
TypeID: 71
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_19Coal
|
||||
TypeID: 19
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_21CharcoalSnapper
|
||||
TypeID: 21
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_12Pebble Sturgeon
|
||||
TypeID: 12
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_25ObsidianWalleye
|
||||
TypeID: 25
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: Prefab_Resource_65Radish_Field
|
||||
TypeID: 65
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_83Phys res ampoule crafting mat
|
||||
TypeID: 83
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_78Firestick flower
|
||||
TypeID: 78
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_13ArcaneSeedTree
|
||||
TypeID: 13
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_116Tree
|
||||
TypeID: 116
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_6Oak
|
||||
TypeID: 6
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_37Ancients' Tears
|
||||
TypeID: 37
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_63Corn
|
||||
TypeID: 63
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_44Voidlight Tendril
|
||||
TypeID: 44
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_5TitaniumOre
|
||||
TypeID: 5
|
||||
MaxHealth: 20
|
||||
|
||||
Prefab: HarvestableSpawner_55Poppy of the Fallen
|
||||
TypeID: 55
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_102mountain fish3
|
||||
TypeID: 102
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_15Blueberries
|
||||
TypeID: 15
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_41Falls Hydrangea
|
||||
TypeID: 41
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_26BettaIotachi
|
||||
TypeID: 26
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: Prefab_Resource_36Noblerose
|
||||
TypeID: 36
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: Prefab_Resource_58Sunflower
|
||||
TypeID: 58
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_45Autumnleaf Lily
|
||||
TypeID: 45
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_32Lakesberry
|
||||
TypeID: 32
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_106Imberite Ore
|
||||
TypeID: 106
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_100mountain fish1
|
||||
TypeID: 100
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_27Rock Lobster
|
||||
TypeID: 27
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_9Wild Goopy
|
||||
TypeID: 9
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_68Epic ore
|
||||
TypeID: 68
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_113Soulgrowth Mushrooms
|
||||
TypeID: 113
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_28Horsetail Reed Bass
|
||||
TypeID: 28
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_8Deadwood tree
|
||||
TypeID: 8
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_77Glowing Willow
|
||||
TypeID: 77
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_33Silvermirror
|
||||
TypeID: 33
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_42Wight's Pearls
|
||||
TypeID: 42
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_16Spellbound Oak
|
||||
TypeID: 16
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_25Obsidian Walleye
|
||||
TypeID: 25
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_50Haniflower
|
||||
TypeID: 50
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: Prefab_Resource_2CopperOre
|
||||
TypeID: 2
|
||||
MaxHealth: 20
|
||||
|
||||
Prefab: HarvestableSpawner_64Carrots
|
||||
TypeID: 64
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_32Lakesberry
|
||||
TypeID: 32
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_115Malicious Sporecap
|
||||
TypeID: 115
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_88Saltpeter Mine
|
||||
TypeID: 88
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_23FuchsiaPerch
|
||||
TypeID: 23
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: Prefab_Resource_54GoldenSunflower
|
||||
TypeID: 54
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_58Sunflower
|
||||
TypeID: 58
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_117Ore
|
||||
TypeID: 117
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_1Spruce
|
||||
TypeID: 1
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_79Minigame tree
|
||||
TypeID: 79
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_7Evark tree
|
||||
TypeID: 7
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_81Puppet Flower
|
||||
TypeID: 81
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_6Oak tree
|
||||
TypeID: 6
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_14Mageflower
|
||||
TypeID: 14
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: Prefab_Resource_7Evark
|
||||
TypeID: 0
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_86Generic mining
|
||||
TypeID: 86
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_85Darkmire Damp Root
|
||||
TypeID: 85
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_110Crystalvein Rose
|
||||
TypeID: 110
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_4Imberite ore
|
||||
TypeID: 4
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_16SpellboundOak
|
||||
TypeID: 16
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_59Evening's Cup
|
||||
TypeID: 59
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_38Dandelions
|
||||
TypeID: 38
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_101mountain fish2
|
||||
TypeID: 101
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_115MaliciousSporecap
|
||||
TypeID: 115
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_43Skyfold Flower
|
||||
TypeID: 43
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_67Cotton
|
||||
TypeID: 67
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_84Ranged res ampoule crafting mat
|
||||
TypeID: 84
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_23Fuchsia Perch
|
||||
TypeID: 23
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_84Marshland Feather Reed
|
||||
TypeID: 84
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_24GoblinShad
|
||||
TypeID: 24
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_109mountain herb1
|
||||
TypeID: 109
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_35Red Nymph
|
||||
TypeID: 35
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_80Sunburst Marlin
|
||||
TypeID: 80
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_74Soothing Mallow
|
||||
TypeID: 74
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_60Golden Tulip
|
||||
TypeID: 60
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_114ColdwaterLily
|
||||
TypeID: 114
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_24Goblin Shad
|
||||
TypeID: 24
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_46Cloud Yarrow
|
||||
TypeID: 46
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_113SoulgrowthMushroom
|
||||
TypeID: 113
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_104mountain tree2
|
||||
TypeID: 104
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_62Chili_Pepper_Field
|
||||
TypeID: 62
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: Prefab_Resource_67Cotton
|
||||
TypeID: 67
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_111mountain herb3
|
||||
TypeID: 111
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_18Dust
|
||||
TypeID: 18
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_18Dust
|
||||
TypeID: 18
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_1Spruce
|
||||
TypeID: 1
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_73Sweetgrass
|
||||
TypeID: 73
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_35RedNymph
|
||||
TypeID: 35
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_72MinigameOre
|
||||
TypeID: 72
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_67Northwind Cotton
|
||||
TypeID: 67
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_57Queen's Lily
|
||||
TypeID: 57
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_103mountain tree1
|
||||
TypeID: 103
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_3IronOre
|
||||
TypeID: 3
|
||||
MaxHealth: 20
|
||||
|
||||
Prefab: HarvestableSpawner_87Generic alchemy
|
||||
TypeID: 87
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_31Bloodpetal
|
||||
TypeID: 31
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_1Spruce tree
|
||||
TypeID: 1
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_40Night's Bloom
|
||||
TypeID: 40
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_28HorsetailReedBass
|
||||
TypeID: 28
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_11Redberries
|
||||
TypeID: 11
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_20Cavern Puffball
|
||||
TypeID: 20
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_37Ancient's Tears
|
||||
TypeID: 37
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_107mountain Ore2
|
||||
TypeID: 107
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_27RockLobster
|
||||
TypeID: 27
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: Prefab_Resource_11RedberryBush
|
||||
TypeID: 11
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_62Chili Peppers
|
||||
TypeID: 62
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_106mountain Ore1
|
||||
TypeID: 106
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_56Deepforest Daisy
|
||||
TypeID: 56
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_105mountain tree3
|
||||
TypeID: 105
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_53Drakefire Root
|
||||
TypeID: 53
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_66Pumpkins
|
||||
TypeID: 66
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_34SorcerersWeed
|
||||
TypeID: 34
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_112Rye
|
||||
TypeID: 112
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_3Iron ore
|
||||
TypeID: 3
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_66Pumpkin_Patch
|
||||
TypeID: 66
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_73MedicinalHerbs
|
||||
TypeID: 73
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_33Silvermirror Iris
|
||||
TypeID: 33
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_69Cabbage
|
||||
TypeID: 69
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_13Arcane Everbloom
|
||||
TypeID: 13
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_61Eggplant_Field
|
||||
TypeID: 61
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_48Sorrowleaf
|
||||
TypeID: 48
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_63Corn_Field
|
||||
TypeID: 63
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_39Dawnflame Ivy
|
||||
TypeID: 39
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_54Golden Sunflower
|
||||
TypeID: 54
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_37AncientsTears
|
||||
TypeID: 37
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_21Charcoal Snapper
|
||||
TypeID: 21
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_82Spider Egg
|
||||
TypeID: 82
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_34Sorcerer's Weed
|
||||
TypeID: 34
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_75Aurora Trout
|
||||
TypeID: 75
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_47Spellspore Cap
|
||||
TypeID: 47
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_109Skyfall Orchid
|
||||
TypeID: 109
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_52Shyflower Orchid
|
||||
TypeID: 52
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_110mountain herb2
|
||||
TypeID: 110
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_38Dandelions
|
||||
TypeID: 38
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_10Barley
|
||||
TypeID: 10
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_51Royal Daisy
|
||||
TypeID: 51
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_20CaveShroom
|
||||
TypeID: 20
|
||||
MaxHealth: 20
|
||||
|
||||
Prefab: HarvestableSpawner_74Sven Herb 2
|
||||
TypeID: 74
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_69Potatoes
|
||||
TypeID: 69
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_114Coldwater Lily
|
||||
TypeID: 114
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: HarvestableSpawner_36Noblerose
|
||||
TypeID: 36
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_10Barley_Field
|
||||
TypeID: 10
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_31Bloodpetal Rose
|
||||
TypeID: 31
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_9WildGoopy
|
||||
TypeID: 9
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_83Gravelstem Whiteflower
|
||||
TypeID: 83
|
||||
MaxHealth: 0
|
||||
|
||||
Prefab: Prefab_Resource_41FallsHydrangea
|
||||
TypeID: 41
|
||||
MaxHealth: 5
|
||||
|
||||
Prefab: HarvestableSpawner_50Haniflower
|
||||
TypeID: 50
|
||||
MaxHealth: 0
|
||||
|
||||
======================================================================
|
||||
End of resource data
|
||||
19
resources_output.txt
Normal file
19
resources_output.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
Cursebreaker Resources - 10_3.unity Scene
|
||||
======================================================================
|
||||
|
||||
Total resources found: 2
|
||||
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Resource: HarvestableSpawner_2Copper Ore
|
||||
TypeID: 2
|
||||
MaxHealth: 0
|
||||
Position: (1788.727173, 40.725288, 172.017670)
|
||||
|
||||
Resource: HarvestableSpawner_38Dandelions
|
||||
TypeID: 38
|
||||
MaxHealth: 0
|
||||
Position: (1746.709717, 44.599632, 299.696503)
|
||||
|
||||
======================================================================
|
||||
End of resource data
|
||||
@@ -1,11 +1,11 @@
|
||||
[package]
|
||||
name = "cursebreaker-parser-macros"
|
||||
name = "unity-parser-macros"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Your Name <your.email@example.com>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Procedural macros for cursebreaker-parser"
|
||||
repository = "https://github.com/yourusername/cursebreaker-parser-rust"
|
||||
description = "Procedural macros for unity-parser"
|
||||
repository = "https://github.com/yourusername/unity-parser-rust"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Procedural macros for cursebreaker-parser
|
||||
//! Procedural macros for unity-parser
|
||||
//!
|
||||
//! This crate provides the `#[derive(UnityComponent)]` macro for automatically
|
||||
//! generating Unity component parsing code.
|
||||
@@ -101,7 +101,8 @@ pub fn derive_unity_component(input: TokenStream) -> TokenStream {
|
||||
<#struct_name as cursebreaker_parser::EcsInsertable>::parse_and_insert(
|
||||
yaml, ctx, world, entity
|
||||
)
|
||||
}
|
||||
},
|
||||
register: |builder| builder.register::<#struct_name>()
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,17 +1,17 @@
|
||||
[package]
|
||||
name = "cursebreaker-parser"
|
||||
name = "unity-parser"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Your Name <your.email@example.com>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "A high-performance Rust library for parsing Unity project files (.unity, .prefab, .asset)"
|
||||
repository = "https://github.com/yourusername/cursebreaker-parser-rust"
|
||||
repository = "https://github.com/yourusername/unity-parser-rust"
|
||||
keywords = ["unity", "parser", "yaml", "gamedev"]
|
||||
categories = ["parser-implementations", "game-development"]
|
||||
rust-version = "1.70"
|
||||
|
||||
[lib]
|
||||
name = "cursebreaker_parser"
|
||||
name = "unity_parser"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
@@ -47,7 +47,7 @@ once_cell = "1.19"
|
||||
inventory = "0.3"
|
||||
|
||||
# Procedural macro for derive(UnityComponent)
|
||||
cursebreaker-parser-macros = { path = "../cursebreaker-parser-macros" }
|
||||
unity-parser-macros = { path = "../unity-parser-macros" }
|
||||
|
||||
[dev-dependencies]
|
||||
# Testing utilities
|
||||
@@ -1,4 +1,4 @@
|
||||
use cursebreaker_parser::UnityFile;
|
||||
use unity_parser::UnityFile;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
@@ -38,16 +38,13 @@ 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()) {
|
||||
if let Some(name) = mapping.get("m_Name").and_then(|v| v.as_str()) {
|
||||
println!(" - {}", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Example demonstrating how to define custom Unity MonoBehaviour components
|
||||
//! using the #[derive(UnityComponent)] macro.
|
||||
|
||||
use cursebreaker_parser::{yaml_helpers, ComponentContext, UnityComponent};
|
||||
use unity_parser::{yaml_helpers, ComponentContext, UnityComponent};
|
||||
|
||||
/// Custom Unity MonoBehaviour component for playing sound effects
|
||||
///
|
||||
@@ -71,7 +71,7 @@ isLoop: 1
|
||||
let mapping = yaml.as_mapping().unwrap();
|
||||
|
||||
// Create a dummy context
|
||||
use cursebreaker_parser::{ComponentContext, FileID};
|
||||
use unity_parser::{ComponentContext, FileID};
|
||||
let ctx = ComponentContext {
|
||||
type_id: 114,
|
||||
file_id: FileID::from_i64(12345),
|
||||
@@ -5,7 +5,7 @@
|
||||
//! 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};
|
||||
use unity_parser::{parse_with_types, ComponentContext, EcsInsertable, FileID, TypeFilter, UnityComponent};
|
||||
|
||||
/// Custom Unity MonoBehaviour component
|
||||
#[derive(Debug, Clone, UnityComponent)]
|
||||
194
unity-parser/examples/find_playsfx.rs
Normal file
194
unity-parser/examples/find_playsfx.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
//! Demo: Find all PlaySFX components and their locations in VR_Horror_YouCantRun
|
||||
//!
|
||||
//! This example demonstrates:
|
||||
//! 1. Parsing a real Unity project
|
||||
//! 2. Finding custom MonoBehaviour components (PlaySFX)
|
||||
//! 3. Querying the ECS world for components
|
||||
//! 4. Accessing Transform data for component locations
|
||||
|
||||
use unity_parser::{UnityComponent, UnityFile};
|
||||
use std::path::Path;
|
||||
|
||||
/// PlaySFX component from VR_Horror_YouCantRun
|
||||
///
|
||||
/// C# definition:
|
||||
/// ```csharp
|
||||
/// public class PlaySFX : MonoBehaviour
|
||||
/// {
|
||||
/// [SerializeField] float volume;
|
||||
/// [SerializeField] float startTime;
|
||||
/// [SerializeField] float endTime;
|
||||
/// [SerializeField] bool isLoop;
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Debug, Clone, UnityComponent)]
|
||||
#[unity_class("PlaySFX")]
|
||||
pub struct PlaySFX {
|
||||
#[unity_field("volume")]
|
||||
pub volume: f64,
|
||||
|
||||
#[unity_field("startTime")]
|
||||
pub start_time: f64,
|
||||
|
||||
#[unity_field("endTime")]
|
||||
pub end_time: f64,
|
||||
|
||||
#[unity_field("isLoop")]
|
||||
pub is_loop: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("🎮 VR Horror - PlaySFX Component Finder");
|
||||
println!("{}", "=".repeat(70));
|
||||
println!();
|
||||
|
||||
let project_path = Path::new("test_data/VR_Horror_YouCantRun");
|
||||
|
||||
// Check if project exists
|
||||
if !project_path.exists() {
|
||||
eprintln!("❌ Error: VR_Horror_YouCantRun project not found at {}", project_path.display());
|
||||
eprintln!(" Run the integration tests first to download it:");
|
||||
eprintln!(" cargo test test_vr_horror_project");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("📁 Scanning project: {}", project_path.display());
|
||||
println!();
|
||||
|
||||
// Find all Unity scene files
|
||||
let scene_files = find_unity_files(project_path, "unity");
|
||||
|
||||
println!("📄 Found {} scene file(s)", scene_files.len());
|
||||
println!();
|
||||
|
||||
let mut total_playsfx = 0;
|
||||
|
||||
// Parse each scene
|
||||
for scene_path in scene_files {
|
||||
println!("🔍 Parsing: {}", scene_path.file_name().unwrap().to_string_lossy());
|
||||
|
||||
match UnityFile::from_path(&scene_path) {
|
||||
Ok(UnityFile::Scene(scene)) => {
|
||||
// Get views for all component types we need
|
||||
let playsfx_view = scene.world.borrow::<PlaySFX>();
|
||||
let transform_view = scene.world.borrow::<unity_parser::Transform>();
|
||||
let rect_transform_view = scene.world.borrow::<unity_parser::RectTransform>();
|
||||
let gameobject_view = scene.world.borrow::<unity_parser::GameObject>();
|
||||
|
||||
// Find all entities that have PlaySFX
|
||||
let mut found_count = 0;
|
||||
let mut found_entities = Vec::new();
|
||||
|
||||
// Iterate through all entities in the entity_map
|
||||
for entity in scene.entity_map.values() {
|
||||
if let Some(playsfx) = playsfx_view.get(*entity) {
|
||||
found_entities.push((*entity, playsfx.clone()));
|
||||
found_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if found_count > 0 {
|
||||
println!(" ✅ Found {} PlaySFX component(s)", found_count);
|
||||
total_playsfx += found_count;
|
||||
|
||||
// Process each found PlaySFX component
|
||||
for (entity, playsfx) in found_entities {
|
||||
let transform = transform_view.get(entity);
|
||||
let rect_transform = rect_transform_view.get(entity);
|
||||
let game_object = gameobject_view.get(entity);
|
||||
|
||||
let name = game_object
|
||||
.and_then(|go| go.name())
|
||||
.unwrap_or("(unnamed)");
|
||||
|
||||
println!();
|
||||
println!(" 🔊 PlaySFX on GameObject: \"{}\"", name);
|
||||
println!(" Entity: {:?}", entity);
|
||||
println!(" Properties:");
|
||||
println!(" • volume: {}", playsfx.volume);
|
||||
println!(" • startTime: {}", playsfx.start_time);
|
||||
println!(" • endTime: {}", playsfx.end_time);
|
||||
println!(" • isLoop: {}", playsfx.is_loop);
|
||||
|
||||
// Print position if available
|
||||
if let Some(transform) = transform {
|
||||
if let Some(pos) = transform.local_position() {
|
||||
println!(" Transform:");
|
||||
println!(" • Position: ({:.2}, {:.2}, {:.2})",
|
||||
pos.x, pos.y, pos.z);
|
||||
}
|
||||
if let Some(rot) = transform.local_rotation() {
|
||||
println!(" • Rotation: ({:.2}, {:.2}, {:.2}, {:.2})",
|
||||
rot.x, rot.y, rot.z, rot.w);
|
||||
}
|
||||
if let Some(scale) = transform.local_scale() {
|
||||
println!(" • Scale: ({:.2}, {:.2}, {:.2})",
|
||||
scale.x, scale.y, scale.z);
|
||||
}
|
||||
} else if let Some(rect_transform) = rect_transform {
|
||||
let transform = rect_transform.transform();
|
||||
if let Some(pos) = transform.local_position() {
|
||||
println!(" RectTransform (UI):");
|
||||
println!(" • Position: ({:.2}, {:.2}, {:.2})",
|
||||
pos.x, pos.y, pos.z);
|
||||
}
|
||||
} else {
|
||||
println!(" ⚠️ No Transform found");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!(" ⊘ No PlaySFX components found");
|
||||
}
|
||||
|
||||
println!();
|
||||
}
|
||||
Ok(_) => {
|
||||
println!(" ⊘ Skipped (not a scene file)");
|
||||
println!();
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" ❌ Parse error: {}", e);
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("{}", "=".repeat(70));
|
||||
println!("📊 Summary:");
|
||||
println!(" Total PlaySFX components found: {}", total_playsfx);
|
||||
println!("{}", "=".repeat(70));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Find all Unity files with a specific extension in a directory
|
||||
fn find_unity_files(dir: &Path, extension: &str) -> Vec<std::path::PathBuf> {
|
||||
let mut files = Vec::new();
|
||||
|
||||
fn visit_dir(dir: &Path, extension: &str, files: &mut Vec<std::path::PathBuf>) {
|
||||
if let Ok(entries) = std::fs::read_dir(dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
|
||||
// Skip Library, Temp, Builds, and .git directories
|
||||
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if name == "Library" || name == "Temp" || name == "Builds" || name == ".git" {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if path.is_dir() {
|
||||
visit_dir(&path, extension, files);
|
||||
} else if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
||||
if ext == extension {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visit_dir(dir, extension, &mut files);
|
||||
files.sort();
|
||||
files
|
||||
}
|
||||
218
unity-parser/examples/parse_resource_prefabs.rs
Normal file
218
unity-parser/examples/parse_resource_prefabs.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
//! Parse Cursebreaker Resource Prefabs
|
||||
//!
|
||||
//! This example demonstrates:
|
||||
//! 1. Parsing Cursebreaker prefab files directly
|
||||
//! 2. Finding Interactable_Resource components in prefabs
|
||||
//! 3. Extracting typeId and maxHealth data
|
||||
//! 4. Writing resource data to an output file
|
||||
//!
|
||||
//! Note: The 10_3.unity scene uses prefab instances, and the current parser
|
||||
//! doesn't yet support resolving components from nested prefabs. This example
|
||||
//! parses the prefab files directly instead.
|
||||
|
||||
use unity_parser::{GuidResolver, UnityComponent, UnityFile};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// Interactable_Resource component from Cursebreaker
|
||||
///
|
||||
/// C# definition from Interactable_Resource.cs:
|
||||
/// ```csharp
|
||||
/// public class Interactable_Resource : Interactable
|
||||
/// {
|
||||
/// public int health;
|
||||
/// public int maxHealth;
|
||||
/// public int typeId;
|
||||
/// // ... other fields
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Debug, Clone, UnityComponent)]
|
||||
#[unity_class("Interactable_Resource")]
|
||||
pub struct InteractableResource {
|
||||
#[unity_field("maxHealth")]
|
||||
pub max_health: i64,
|
||||
|
||||
#[unity_field("typeId")]
|
||||
pub type_id: i64,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("🎮 Cursebreaker - Resource Prefab Parser");
|
||||
println!("{}", "=".repeat(70));
|
||||
println!();
|
||||
|
||||
// Build GUID resolver for the project
|
||||
let project_path = Path::new("/home/connor/repos/CBAssets");
|
||||
println!("📦 Building GUID resolver for project: {}", project_path.display());
|
||||
|
||||
let resolver = match GuidResolver::from_project(project_path) {
|
||||
Ok(r) => {
|
||||
println!(" ✅ GUID resolver built successfully ({} mappings)", r.len());
|
||||
|
||||
// Debug: Check if we have Interactable_Resource
|
||||
if let Some(class) = r.resolve_class_name("d39ddbf1c2c3d1a4baa070e5e76548bd") {
|
||||
println!(" ✅ Found Interactable_Resource in resolver: {}", class);
|
||||
} else {
|
||||
println!(" ⚠️ Interactable_Resource NOT found in resolver");
|
||||
// Try to find what we did find related to "Interactable"
|
||||
println!(" Searching for similar class names...");
|
||||
}
|
||||
|
||||
Some(r)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(" ❌ Failed to build GUID resolver: {}", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
println!();
|
||||
|
||||
let harvestables_dir = Path::new("/home/connor/repos/CBAssets/_GameAssets/Prefabs/Harvestables");
|
||||
|
||||
if !harvestables_dir.exists() {
|
||||
eprintln!("❌ Error: Harvestables directory not found at {}", harvestables_dir.display());
|
||||
return Err("Harvestables directory not found".into());
|
||||
}
|
||||
|
||||
println!("📁 Scanning for harvestable prefabs in:");
|
||||
println!(" {}", harvestables_dir.display());
|
||||
println!();
|
||||
|
||||
// Find all prefab files
|
||||
let mut prefab_files = Vec::new();
|
||||
for entry in WalkDir::new(harvestables_dir)
|
||||
.follow_links(false)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
{
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|s| s.to_str()) == Some("prefab") {
|
||||
prefab_files.push(path.to_path_buf());
|
||||
}
|
||||
}
|
||||
|
||||
println!("📄 Found {} prefab file(s)", prefab_files.len());
|
||||
println!();
|
||||
|
||||
let mut all_resources = Vec::new();
|
||||
|
||||
// Parse each prefab using the GUID resolver we built
|
||||
for prefab_path in &prefab_files {
|
||||
println!("🔍 Parsing: {}", prefab_path.file_name().unwrap().to_string_lossy());
|
||||
|
||||
// For prefabs, we need to manually parse and check documents
|
||||
// since prefabs don't have an ECS world like scenes do
|
||||
match UnityFile::from_path(prefab_path) {
|
||||
Ok(UnityFile::Prefab(prefab)) => {
|
||||
// Search through YAML documents for Interactable_Resource components
|
||||
let mut found_in_prefab = false;
|
||||
|
||||
for doc in &prefab.documents {
|
||||
// Check if this document is a MonoBehaviour
|
||||
if doc.class_name == "MonoBehaviour" {
|
||||
// Try to extract the m_Script GUID
|
||||
if let Some(m_script) = doc.yaml.get("m_Script").and_then(|v| v.as_mapping()) {
|
||||
if let Some(guid_val) = m_script.get("guid").and_then(|v| v.as_str()) {
|
||||
// Resolve GUID to class name
|
||||
if let Some(ref res) = resolver {
|
||||
if let Some(class_name) = res.resolve_class_name(guid_val) {
|
||||
// Debug: print what we found
|
||||
if prefab_path.file_name().unwrap().to_string_lossy().contains("Copper Ore") {
|
||||
eprintln!("DEBUG: Found class '{}' in Copper Ore prefab", class_name);
|
||||
}
|
||||
|
||||
if class_name == "Interactable_Resource" {
|
||||
// Extract fields
|
||||
let type_id = doc.yaml.get("typeId")
|
||||
.and_then(|v| v.as_i64())
|
||||
.unwrap_or(0);
|
||||
let max_health = doc.yaml.get("maxHealth")
|
||||
.and_then(|v| v.as_i64())
|
||||
.unwrap_or(0);
|
||||
|
||||
let prefab_name = prefab_path
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("unknown");
|
||||
|
||||
all_resources.push((
|
||||
prefab_name.to_string(),
|
||||
type_id,
|
||||
max_health,
|
||||
));
|
||||
|
||||
found_in_prefab = true;
|
||||
}
|
||||
} else if prefab_path.file_name().unwrap().to_string_lossy().contains("Copper Ore") {
|
||||
eprintln!("DEBUG: Could not resolve GUID '{}' in Copper Ore prefab", guid_val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found_in_prefab {
|
||||
println!(" ✅ Found Interactable_Resource");
|
||||
} else {
|
||||
println!(" ⊘ No Interactable_Resource found");
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
println!(" ⊘ Not a prefab file");
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" ❌ Parse error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "=".repeat(70));
|
||||
println!("📊 Summary: Found {} resource(s)", all_resources.len());
|
||||
println!("{}", "=".repeat(70));
|
||||
println!();
|
||||
|
||||
if !all_resources.is_empty() {
|
||||
// Display resources
|
||||
for (name, type_id, max_health) in &all_resources {
|
||||
println!(" 📦 Prefab: \"{}\"", name);
|
||||
println!(" • typeId: {}", type_id);
|
||||
println!(" • maxHealth: {}", max_health);
|
||||
println!();
|
||||
}
|
||||
|
||||
// Write to output file
|
||||
let output_path = "resource_prefabs_output.txt";
|
||||
let mut output_file = File::create(output_path)?;
|
||||
|
||||
writeln!(output_file, "Cursebreaker Resource Prefabs")?;
|
||||
writeln!(output_file, "{}", "=".repeat(70))?;
|
||||
writeln!(output_file)?;
|
||||
writeln!(output_file, "Total resources found: {}", all_resources.len())?;
|
||||
writeln!(output_file)?;
|
||||
writeln!(output_file, "{}", "-".repeat(70))?;
|
||||
writeln!(output_file)?;
|
||||
|
||||
for (name, type_id, max_health) in &all_resources {
|
||||
writeln!(output_file, "Prefab: {}", name)?;
|
||||
writeln!(output_file, " TypeID: {}", type_id)?;
|
||||
writeln!(output_file, " MaxHealth: {}", max_health)?;
|
||||
writeln!(output_file)?;
|
||||
}
|
||||
|
||||
writeln!(output_file, "{}", "=".repeat(70))?;
|
||||
writeln!(output_file, "End of resource data")?;
|
||||
|
||||
println!("📝 Resource data written to: {}", output_path);
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "=".repeat(70));
|
||||
println!("✅ Parsing complete!");
|
||||
println!("{}", "=".repeat(70));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
147
unity-parser/examples/parse_resources.rs
Normal file
147
unity-parser/examples/parse_resources.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
//! Parse Cursebreaker Resources from 10_3.unity Scene
|
||||
//!
|
||||
//! This example demonstrates:
|
||||
//! 1. Parsing the Cursebreaker Unity project
|
||||
//! 2. Finding Interactable_Resource components
|
||||
//! 3. Extracting typeId and transform positions
|
||||
//! 4. Writing resource data to an output file
|
||||
|
||||
use unity_parser::{UnityComponent, UnityFile};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
/// Interactable_Resource component from Cursebreaker
|
||||
///
|
||||
/// C# definition from Interactable_Resource.cs:
|
||||
/// ```csharp
|
||||
/// public class Interactable_Resource : Interactable
|
||||
/// {
|
||||
/// public int health;
|
||||
/// public int maxHealth;
|
||||
/// public int typeId;
|
||||
/// // ... other fields
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Debug, Clone, UnityComponent)]
|
||||
#[unity_class("Interactable_Resource")]
|
||||
pub struct InteractableResource {
|
||||
#[unity_field("maxHealth")]
|
||||
pub max_health: i64,
|
||||
|
||||
#[unity_field("typeId")]
|
||||
pub type_id: i64,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("🎮 Cursebreaker - Resource Parser");
|
||||
println!("{}", "=".repeat(70));
|
||||
println!();
|
||||
|
||||
let scene_path = Path::new("/home/connor/repos/CBAssets/_GameAssets/Scenes/Tiles/10_3.unity");
|
||||
|
||||
// Check if scene exists
|
||||
if !scene_path.exists() {
|
||||
eprintln!("❌ Error: Scene not found at {}", scene_path.display());
|
||||
return Err("Scene file not found".into());
|
||||
}
|
||||
|
||||
println!("📁 Parsing scene: {}", scene_path.display());
|
||||
println!();
|
||||
|
||||
// Parse the scene
|
||||
match UnityFile::from_path(&scene_path) {
|
||||
Ok(UnityFile::Scene(scene)) => {
|
||||
println!("✅ Scene parsed successfully!");
|
||||
println!(" Total entities: {}", scene.entity_map.len());
|
||||
println!();
|
||||
|
||||
// Get views for component types we need
|
||||
let resource_view = scene.world.borrow::<InteractableResource>();
|
||||
let transform_view = scene.world.borrow::<unity_parser::Transform>();
|
||||
let gameobject_view = scene.world.borrow::<unity_parser::GameObject>();
|
||||
|
||||
// Find all entities that have Interactable_Resource
|
||||
let mut found_resources = Vec::new();
|
||||
|
||||
for entity in scene.entity_map.values() {
|
||||
if let Some(resource) = resource_view.get(*entity) {
|
||||
let transform = transform_view.get(*entity);
|
||||
let game_object = gameobject_view.get(*entity);
|
||||
|
||||
let name = game_object
|
||||
.and_then(|go| go.name())
|
||||
.unwrap_or("(unnamed)");
|
||||
|
||||
let position = transform
|
||||
.and_then(|t| t.local_position())
|
||||
.map(|p| (p.x, p.y, p.z));
|
||||
|
||||
found_resources.push((name.to_string(), resource.clone(), position));
|
||||
}
|
||||
}
|
||||
|
||||
println!("🔍 Found {} Interactable_Resource component(s)", found_resources.len());
|
||||
println!();
|
||||
|
||||
if !found_resources.is_empty() {
|
||||
// Display resources in console
|
||||
for (name, resource, position) in &found_resources {
|
||||
println!(" 📦 Resource: \"{}\"", name);
|
||||
println!(" • typeId: {}", resource.type_id);
|
||||
println!(" • maxHealth: {}", resource.max_health);
|
||||
if let Some((x, y, z)) = position {
|
||||
println!(" • Position: ({:.2}, {:.2}, {:.2})", x, y, z);
|
||||
} else {
|
||||
println!(" • Position: (no transform)");
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
// Write to output file
|
||||
let output_path = "resources_output.txt";
|
||||
let mut output_file = File::create(output_path)?;
|
||||
|
||||
writeln!(output_file, "Cursebreaker Resources - 10_3.unity Scene")?;
|
||||
writeln!(output_file, "{}", "=".repeat(70))?;
|
||||
writeln!(output_file)?;
|
||||
writeln!(output_file, "Total resources found: {}", found_resources.len())?;
|
||||
writeln!(output_file)?;
|
||||
writeln!(output_file, "{}", "-".repeat(70))?;
|
||||
writeln!(output_file)?;
|
||||
|
||||
for (name, resource, position) in &found_resources {
|
||||
writeln!(output_file, "Resource: {}", name)?;
|
||||
writeln!(output_file, " TypeID: {}", resource.type_id)?;
|
||||
writeln!(output_file, " MaxHealth: {}", resource.max_health)?;
|
||||
if let Some((x, y, z)) = position {
|
||||
writeln!(output_file, " Position: ({:.6}, {:.6}, {:.6})", x, y, z)?;
|
||||
} else {
|
||||
writeln!(output_file, " Position: N/A")?;
|
||||
}
|
||||
writeln!(output_file)?;
|
||||
}
|
||||
|
||||
writeln!(output_file, "{}", "=".repeat(70))?;
|
||||
writeln!(output_file, "End of resource data")?;
|
||||
|
||||
println!("📝 Resource data written to: {}", output_path);
|
||||
println!();
|
||||
}
|
||||
|
||||
println!("{}", "=".repeat(70));
|
||||
println!("✅ Parsing complete!");
|
||||
println!("{}", "=".repeat(70));
|
||||
}
|
||||
Ok(_) => {
|
||||
eprintln!("❌ Error: File is not a scene");
|
||||
return Err("Not a Unity scene file".into());
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ Parse error: {}", e);
|
||||
return Err(Box::new(e));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
19
unity-parser/resources_output.txt
Normal file
19
unity-parser/resources_output.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
Cursebreaker Resources - 10_3.unity Scene
|
||||
======================================================================
|
||||
|
||||
Total resources found: 2
|
||||
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Resource: HarvestableSpawner_11Redberries
|
||||
TypeID: 11
|
||||
MaxHealth: 0
|
||||
Position: (1769.135864, 32.664658, 150.395081)
|
||||
|
||||
Resource: HarvestableSpawner_38Dandelions
|
||||
TypeID: 38
|
||||
MaxHealth: 0
|
||||
Position: (1746.709717, 44.599632, 299.696503)
|
||||
|
||||
======================================================================
|
||||
End of resource data
|
||||
434
unity-parser/src/ecs/builder.rs
Normal file
434
unity-parser/src/ecs/builder.rs
Normal file
@@ -0,0 +1,434 @@
|
||||
//! ECS world building from Unity documents
|
||||
|
||||
use crate::model::RawDocument;
|
||||
use crate::parser::{GuidResolver, PrefabGuidResolver};
|
||||
use crate::types::{
|
||||
yaml_helpers, ComponentContext, FileID, GameObject, LinkingContext, PrefabInstanceComponent,
|
||||
PrefabResolver, RectTransform, Transform, TypeFilter, UnityComponent,
|
||||
};
|
||||
use crate::{Error, Result};
|
||||
use sparsey::{Entity, World};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Build a Sparsey ECS World from raw Unity documents
|
||||
///
|
||||
/// This uses a 4-pass approach:
|
||||
/// 1. Create entities for all GameObjects
|
||||
/// 2. Attach components (Transform, RectTransform, etc.) to entities
|
||||
/// 3. Resolve and instantiate prefab instances
|
||||
/// 4. Resolve Callbacks generated when parsing components
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `documents`: Parsed Unity documents to build the world from
|
||||
/// - `guid_resolver`: Optional GUID resolver for resolving MonoBehaviour scripts to class names
|
||||
/// - `prefab_guid_resolver`: Optional prefab GUID resolver for automatic prefab instantiation
|
||||
///
|
||||
/// # Returns
|
||||
/// A tuple of (World, FileID → Entity mapping)
|
||||
pub fn build_world_from_documents(
|
||||
documents: Vec<RawDocument>,
|
||||
guid_resolver: Option<&GuidResolver>,
|
||||
prefab_guid_resolver: Option<&PrefabGuidResolver>,
|
||||
) -> Result<(World, HashMap<FileID, Entity>)> {
|
||||
// Create World builder with registered component types
|
||||
let mut builder = World::builder();
|
||||
builder
|
||||
.register::<GameObject>()
|
||||
.register::<Transform>()
|
||||
.register::<RectTransform>()
|
||||
.register::<PrefabInstanceComponent>();
|
||||
|
||||
// Register all custom components from inventory
|
||||
for reg in inventory::iter::<crate::types::ComponentRegistration> {
|
||||
(reg.register)(&mut builder);
|
||||
}
|
||||
|
||||
let mut world = builder.build();
|
||||
|
||||
let linking_ctx = RefCell::new(LinkingContext::new());
|
||||
|
||||
// PASS 1: Create entities for all GameObjects and PrefabInstances
|
||||
for doc in documents.iter().filter(|d| d.type_id == 1 || d.class_name == "GameObject") {
|
||||
let entity = spawn_game_object(&mut world, doc)?;
|
||||
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
|
||||
}
|
||||
|
||||
// Also create entities for PrefabInstances (type 1001)
|
||||
for doc in documents.iter().filter(|d| d.type_id == 1001 || d.class_name == "PrefabInstance") {
|
||||
// Create an entity to represent this prefab instance
|
||||
let entity = world.create(());
|
||||
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
|
||||
|
||||
// Parse and attach the PrefabInstanceComponent
|
||||
if let Some(yaml) = doc.as_mapping() {
|
||||
let ctx = ComponentContext {
|
||||
type_id: doc.type_id,
|
||||
file_id: doc.file_id,
|
||||
class_name: &doc.class_name,
|
||||
entity: Some(entity),
|
||||
linking_ctx: Some(&linking_ctx),
|
||||
yaml,
|
||||
guid_resolver,
|
||||
};
|
||||
|
||||
if let Some(prefab_comp) = PrefabInstanceComponent::parse(yaml, &ctx) {
|
||||
world.insert(entity, (prefab_comp,));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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" &&
|
||||
d.type_id != 1001 && d.class_name != "PrefabInstance"
|
||||
}) {
|
||||
attach_component(&mut world, doc, &linking_ctx, &type_filter, guid_resolver)?;
|
||||
}
|
||||
|
||||
// PASS 2.5: Resolve and instantiate prefab instances (NEW)
|
||||
if let Some(prefab_resolver_ref) = prefab_guid_resolver {
|
||||
let mut prefab_resolver = PrefabResolver::from_resolvers(guid_resolver, prefab_resolver_ref);
|
||||
|
||||
// Query for entities with PrefabInstanceComponent
|
||||
// We need to collect first to avoid borrowing conflicts
|
||||
let prefab_view = world.borrow::<PrefabInstanceComponent>();
|
||||
let prefab_entities: Vec<_> = linking_ctx
|
||||
.borrow()
|
||||
.entity_map()
|
||||
.values()
|
||||
.filter_map(|&entity| {
|
||||
prefab_view.get(entity).map(|component| (entity, component.clone()))
|
||||
})
|
||||
.collect();
|
||||
drop(prefab_view); // Release the borrow
|
||||
|
||||
eprintln!("Pass 2.5: Found {} prefab instance(s) to resolve", prefab_entities.len());
|
||||
|
||||
for (entity, component) in prefab_entities {
|
||||
match prefab_resolver.instantiate_from_component(
|
||||
&component,
|
||||
Some(entity),
|
||||
&mut world,
|
||||
linking_ctx.borrow_mut().entity_map_mut(),
|
||||
) {
|
||||
Ok(spawned) => {
|
||||
eprintln!(" ✅ Spawned {} entities from prefab GUID: {}",
|
||||
spawned.len(), component.prefab_ref.guid);
|
||||
}
|
||||
Err(e) => {
|
||||
// Soft failure - warn but continue
|
||||
eprintln!(" ⚠️ Warning: Failed to instantiate prefab: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove PrefabInstanceComponent after resolution
|
||||
// This prevents it from being processed again
|
||||
let _ = world.remove::<(PrefabInstanceComponent,)>(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// PASS 3: Execute all deferred linking callbacks
|
||||
let entity_map = linking_ctx.into_inner().execute_callbacks(&mut world);
|
||||
|
||||
Ok((world, entity_map))
|
||||
}
|
||||
|
||||
/// Build entities from raw Unity documents into an existing world
|
||||
///
|
||||
/// This is similar to `build_world_from_documents` but spawns into an existing
|
||||
/// world instead of creating a new one. This is used for prefab instantiation.
|
||||
///
|
||||
/// Uses the same 3.5-pass approach:
|
||||
/// 1. Create entities for all GameObjects
|
||||
/// 2. Attach components (Transform, RectTransform, etc.) to entities
|
||||
/// 2.5. Resolve and instantiate prefab instances (if resolver provided)
|
||||
/// 3. Resolve Transform hierarchy (parent/children Entity references)
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `documents`: Parsed Unity documents to build entities from
|
||||
/// - `world`: Existing Sparsey ECS world to spawn entities into
|
||||
/// - `entity_map`: Existing entity map to merge new mappings into
|
||||
/// - `guid_resolver`: Optional script GUID resolver for MonoBehaviour components
|
||||
/// - `prefab_guid_resolver`: Optional prefab GUID resolver for nested prefab instantiation
|
||||
///
|
||||
/// # Returns
|
||||
/// Vec of newly spawned entities
|
||||
pub fn build_world_from_documents_into(
|
||||
documents: Vec<RawDocument>,
|
||||
world: &mut World,
|
||||
entity_map: &mut HashMap<FileID, Entity>,
|
||||
guid_resolver: Option<&GuidResolver>,
|
||||
prefab_guid_resolver: Option<&PrefabGuidResolver>,
|
||||
) -> Result<Vec<Entity>> {
|
||||
let linking_ctx = RefCell::new(LinkingContext::new());
|
||||
|
||||
// Initialize linking context with existing entity_map
|
||||
// This allows cross-references between prefab instances and scene entities
|
||||
*linking_ctx.borrow_mut().entity_map_mut() = entity_map.clone();
|
||||
|
||||
let mut spawned_entities = Vec::new();
|
||||
|
||||
// PASS 1: Create entities for all GameObjects and PrefabInstances
|
||||
for doc in documents.iter().filter(|d| d.type_id == 1 || d.class_name == "GameObject") {
|
||||
let entity = spawn_game_object(world, doc)?;
|
||||
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
|
||||
spawned_entities.push(entity);
|
||||
}
|
||||
|
||||
// Also create entities for PrefabInstances (type 1001)
|
||||
for doc in documents.iter().filter(|d| d.type_id == 1001 || d.class_name == "PrefabInstance") {
|
||||
// Create an entity to represent this prefab instance
|
||||
let entity = world.create(());
|
||||
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
|
||||
spawned_entities.push(entity);
|
||||
|
||||
// Parse and attach the PrefabInstanceComponent
|
||||
if let Some(yaml) = doc.as_mapping() {
|
||||
let ctx = ComponentContext {
|
||||
type_id: doc.type_id,
|
||||
file_id: doc.file_id,
|
||||
class_name: &doc.class_name,
|
||||
entity: Some(entity),
|
||||
linking_ctx: Some(&linking_ctx),
|
||||
yaml,
|
||||
guid_resolver: None, // Nested prefabs use None
|
||||
};
|
||||
|
||||
if let Some(prefab_comp) = PrefabInstanceComponent::parse(yaml, &ctx) {
|
||||
world.insert(entity, (prefab_comp,));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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" &&
|
||||
d.type_id != 1001 && d.class_name != "PrefabInstance"
|
||||
}) {
|
||||
// Use GUID resolver to resolve MonoBehaviour components in prefabs
|
||||
attach_component(world, doc, &linking_ctx, &type_filter, guid_resolver)?;
|
||||
}
|
||||
|
||||
// PASS 2.5: Resolve and instantiate nested prefab instances
|
||||
if let Some(prefab_resolver_ref) = prefab_guid_resolver {
|
||||
let mut prefab_resolver = PrefabResolver::from_resolvers(guid_resolver, prefab_resolver_ref);
|
||||
|
||||
// Query for entities with PrefabInstanceComponent
|
||||
let prefab_view = world.borrow::<PrefabInstanceComponent>();
|
||||
let prefab_entities: Vec<_> = linking_ctx
|
||||
.borrow()
|
||||
.entity_map()
|
||||
.values()
|
||||
.filter_map(|&entity| {
|
||||
prefab_view.get(entity).map(|component| (entity, component.clone()))
|
||||
})
|
||||
.collect();
|
||||
drop(prefab_view); // Release the borrow
|
||||
|
||||
if !prefab_entities.is_empty() {
|
||||
eprintln!("Pass 2.5 (nested): Found {} prefab instance(s) to resolve", prefab_entities.len());
|
||||
}
|
||||
|
||||
for (entity, component) in prefab_entities {
|
||||
match prefab_resolver.instantiate_from_component(
|
||||
&component,
|
||||
Some(entity),
|
||||
world,
|
||||
linking_ctx.borrow_mut().entity_map_mut(),
|
||||
) {
|
||||
Ok(spawned) => {
|
||||
eprintln!(" ✅ Spawned {} entities from nested prefab", spawned.len());
|
||||
spawned_entities.extend(spawned);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(" ⚠️ Warning: Failed to instantiate nested prefab: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove PrefabInstanceComponent after resolution
|
||||
let _ = world.remove::<(PrefabInstanceComponent,)>(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// PASS 3: Execute all deferred linking callbacks
|
||||
let final_entity_map = linking_ctx.into_inner().execute_callbacks(world);
|
||||
|
||||
// Update caller's entity_map with new mappings
|
||||
entity_map.extend(final_entity_map);
|
||||
|
||||
Ok(spawned_entities)
|
||||
}
|
||||
|
||||
/// Spawn a GameObject entity
|
||||
fn spawn_game_object(world: &mut World, doc: &RawDocument) -> Result<Entity> {
|
||||
let yaml = doc
|
||||
.as_mapping()
|
||||
.ok_or_else(|| Error::invalid_format("GameObject YAML must be mapping"))?;
|
||||
|
||||
let ctx = ComponentContext {
|
||||
type_id: doc.type_id,
|
||||
file_id: doc.file_id,
|
||||
class_name: &doc.class_name,
|
||||
entity: None,
|
||||
linking_ctx: None,
|
||||
yaml,
|
||||
guid_resolver: None,
|
||||
};
|
||||
|
||||
let go = GameObject::parse(yaml, &ctx)
|
||||
.ok_or_else(|| Error::invalid_format("Failed to parse GameObject"))?;
|
||||
|
||||
// Create entity with GameObject component
|
||||
let entity = world.create((go,));
|
||||
|
||||
Ok(entity)
|
||||
}
|
||||
|
||||
/// Attach a component to its GameObject entity
|
||||
fn attach_component(
|
||||
world: &mut World,
|
||||
doc: &RawDocument,
|
||||
linking_ctx: &RefCell<LinkingContext>,
|
||||
type_filter: &TypeFilter,
|
||||
guid_resolver: Option<&GuidResolver>,
|
||||
) -> Result<()> {
|
||||
let yaml = doc
|
||||
.as_mapping()
|
||||
.ok_or_else(|| Error::invalid_format("Component YAML must be mapping"))?;
|
||||
|
||||
// Get m_GameObject reference to find which entity owns this component
|
||||
let go_ref = yaml_helpers::get_file_ref(yaml, "m_GameObject");
|
||||
|
||||
let entity = match go_ref {
|
||||
Some(r) => linking_ctx
|
||||
.borrow()
|
||||
.entity_map()
|
||||
.get(&r.file_id)
|
||||
.copied()
|
||||
.ok_or_else(|| {
|
||||
Error::reference_error(format!("Unknown GameObject: {}", r.file_id))
|
||||
})?,
|
||||
None => {
|
||||
// Some components might not have m_GameObject (e.g., standalone assets)
|
||||
eprintln!(
|
||||
"Warning: Component {} has no m_GameObject reference",
|
||||
doc.class_name
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let ctx = ComponentContext {
|
||||
type_id: doc.type_id,
|
||||
file_id: doc.file_id,
|
||||
class_name: &doc.class_name,
|
||||
entity: Some(entity),
|
||||
linking_ctx: Some(linking_ctx),
|
||||
yaml,
|
||||
guid_resolver,
|
||||
};
|
||||
|
||||
// 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" => {
|
||||
if let Some(transform) = Transform::parse(yaml, &ctx) {
|
||||
world.insert(entity, (transform,));
|
||||
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
|
||||
}
|
||||
}
|
||||
"RectTransform" => {
|
||||
if let Some(rect) = RectTransform::parse(yaml, &ctx) {
|
||||
world.insert(entity, (rect,));
|
||||
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
|
||||
}
|
||||
}
|
||||
"PrefabInstance" => {
|
||||
// Parse and store nested prefab reference
|
||||
if let Some(prefab_comp) = PrefabInstanceComponent::parse(yaml, &ctx) {
|
||||
world.insert(entity, (prefab_comp,));
|
||||
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
|
||||
}
|
||||
}
|
||||
"MonoBehaviour" => {
|
||||
// Extract m_Script GUID to resolve the actual class name
|
||||
if let Some(resolver) = guid_resolver {
|
||||
if let Some(script_ref) = yaml_helpers::get_external_ref(yaml, "m_Script") {
|
||||
// Resolve GUID to class name
|
||||
if let Some(class_name) = resolver.resolve_class_name(script_ref.guid.as_str()) {
|
||||
// Try to find a registered custom component with this class name
|
||||
let mut found_custom = false;
|
||||
for reg in inventory::iter::<crate::types::ComponentRegistration> {
|
||||
if reg.class_name == class_name {
|
||||
found_custom = true;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
if !found_custom {
|
||||
// GUID resolved but no registered component found
|
||||
eprintln!(
|
||||
"Warning: Skipping MonoBehaviour '{}' (no registered parser)",
|
||||
class_name
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// GUID not found in resolver
|
||||
eprintln!(
|
||||
"Warning: Could not resolve MonoBehaviour GUID: {}",
|
||||
script_ref.guid
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// No m_Script reference found
|
||||
eprintln!("Warning: MonoBehaviour missing m_Script reference");
|
||||
}
|
||||
} else {
|
||||
// No GUID resolver available
|
||||
eprintln!("Warning: Skipping MonoBehaviour (no GUID resolver available)");
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Check if this is a registered custom component
|
||||
let mut found_custom = false;
|
||||
for reg in inventory::iter::<crate::types::ComponentRegistration> {
|
||||
if reg.class_name == doc.class_name.as_str() {
|
||||
found_custom = true;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
if !found_custom {
|
||||
// Unknown component type - skip with warning
|
||||
eprintln!(
|
||||
"Warning: Skipping unknown component type: {}",
|
||||
doc.class_name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
//! # Example
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use cursebreaker_parser::UnityFile;
|
||||
//! use unity_parser::UnityFile;
|
||||
//!
|
||||
//! let file = UnityFile::from_path("Scene.unity")?;
|
||||
//! match file {
|
||||
@@ -21,7 +21,7 @@
|
||||
//! println!("Asset with {} documents", asset.documents.len());
|
||||
//! }
|
||||
//! }
|
||||
//! # Ok::<(), cursebreaker_parser::Error>(())
|
||||
//! # Ok::<(), unity_parser::Error>(())
|
||||
//! ```
|
||||
|
||||
// Public modules
|
||||
@@ -38,16 +38,19 @@ pub mod types;
|
||||
// Re-exports
|
||||
pub use error::{Error, Result};
|
||||
pub use model::{RawDocument, UnityAsset, UnityFile, UnityPrefab, UnityScene};
|
||||
pub use parser::{meta::MetaFile, parse_unity_file};
|
||||
pub use parser::{
|
||||
find_project_root, meta::MetaFile, parse_unity_file, parse_unity_file_filtered,
|
||||
GuidResolver, PrefabGuidResolver,
|
||||
};
|
||||
// TODO: Re-enable once project module is updated
|
||||
// pub use project::UnityProject;
|
||||
pub use property::PropertyValue;
|
||||
pub use types::{
|
||||
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,
|
||||
ExternalRef, FileID, FileRef, GameObject, Guid, LocalID, PrefabInstance,
|
||||
PrefabInstanceComponent, PrefabModification, PrefabResolver, Quaternion, RectTransform,
|
||||
Transform, TypeFilter, UnityComponent, UnityReference, Vector2, Vector3, yaml_helpers,
|
||||
};
|
||||
|
||||
// Re-export the derive macro from the macro crate
|
||||
pub use cursebreaker_parser_macros::UnityComponent;
|
||||
pub use unity_parser_macros::UnityComponent;
|
||||
501
unity-parser/src/parser/guid_resolver.rs
Normal file
501
unity-parser/src/parser/guid_resolver.rs
Normal file
@@ -0,0 +1,501 @@
|
||||
//! Unity GUID resolution for MonoBehaviour scripts
|
||||
//!
|
||||
//! This module resolves Unity GUIDs to their corresponding MonoBehaviour class names
|
||||
//! by scanning .meta files and parsing C# scripts.
|
||||
//!
|
||||
//! # How Unity GUID Resolution Works
|
||||
//!
|
||||
//! 1. Every asset has a `.meta` file with a unique GUID
|
||||
//! 2. MonoBehaviour components reference scripts via GUID in `m_Script`
|
||||
//! 3. We scan `.cs.meta` files to build a GUID → Script Path mapping
|
||||
//! 4. We parse the `.cs` files to extract the class name
|
||||
//! 5. Result: GUID → Class Name mapping for component registration
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use unity_parser::parser::GuidResolver;
|
||||
//! use std::path::Path;
|
||||
//!
|
||||
//! // Build resolver from Unity project directory
|
||||
//! let project_path = Path::new("path/to/UnityProject");
|
||||
//! let resolver = GuidResolver::from_project(project_path)?;
|
||||
//!
|
||||
//! // Resolve a GUID to class name
|
||||
//! let guid = "091c537484687e9419460cdcd7038234";
|
||||
//! if let Some(class_name) = resolver.resolve_class_name(guid) {
|
||||
//! println!("GUID {} → {}", guid, class_name);
|
||||
//! }
|
||||
//! # Ok::<(), unity_parser::Error>(())
|
||||
//! ```
|
||||
|
||||
use crate::parser::meta::MetaFile;
|
||||
use crate::types::Guid;
|
||||
use crate::{Error, Result};
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// Resolves Unity GUIDs to MonoBehaviour class names
|
||||
///
|
||||
/// This struct builds a mapping from GUID to class name by scanning
|
||||
/// a Unity project's `.cs.meta` files and parsing the corresponding
|
||||
/// C# scripts to extract class names.
|
||||
///
|
||||
/// Uses a 128-bit `Guid` type for efficient storage and fast comparisons.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GuidResolver {
|
||||
/// Map from GUID to MonoBehaviour class name
|
||||
guid_to_class: HashMap<Guid, String>,
|
||||
}
|
||||
|
||||
impl GuidResolver {
|
||||
/// Create a new empty GuidResolver
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
guid_to_class: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a GuidResolver by scanning a Unity project directory
|
||||
///
|
||||
/// This scans for all `.cs.meta` files (MonoBehaviour scripts),
|
||||
/// parses them to extract GUIDs, then parses the corresponding
|
||||
/// C# files to extract class names.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `project_path` - Path to the Unity project root (containing Assets/ folder)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use unity_parser::parser::GuidResolver;
|
||||
/// use std::path::Path;
|
||||
///
|
||||
/// let resolver = GuidResolver::from_project(Path::new("MyUnityProject"))?;
|
||||
/// # Ok::<(), unity_parser::Error>(())
|
||||
/// ```
|
||||
pub fn from_project(project_path: impl AsRef<Path>) -> Result<Self> {
|
||||
let project_path = project_path.as_ref();
|
||||
|
||||
// Verify this looks like a Unity project - check for Assets/ or _GameAssets/
|
||||
let assets_dir = if project_path.join("Assets").exists() {
|
||||
project_path.join("Assets")
|
||||
} else if project_path.join("_GameAssets").exists() {
|
||||
project_path.join("_GameAssets")
|
||||
} else {
|
||||
return Err(Error::invalid_format(format!(
|
||||
"Not a Unity project: missing Assets/ or _GameAssets/ directory at {}",
|
||||
project_path.display()
|
||||
)));
|
||||
};
|
||||
|
||||
let mut resolver = Self::new();
|
||||
|
||||
// Walk the Assets directory looking for .cs.meta files
|
||||
for entry in WalkDir::new(&assets_dir)
|
||||
.follow_links(false)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
{
|
||||
let path = entry.path();
|
||||
|
||||
// Only process .cs.meta files (MonoBehaviour scripts)
|
||||
if !is_cs_meta_file(path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse the .meta file to get the GUID
|
||||
let meta = match MetaFile::from_path(path) {
|
||||
Ok(meta) => meta,
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Failed to parse {}: {}", path.display(), e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Get the corresponding .cs file path
|
||||
let cs_path = path.with_file_name(
|
||||
path.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.and_then(|s| s.strip_suffix(".meta"))
|
||||
.unwrap_or(""),
|
||||
);
|
||||
|
||||
// Extract the class name from the .cs file
|
||||
let class_name = match extract_class_name(&cs_path) {
|
||||
Ok(Some(name)) => name,
|
||||
Ok(None) => {
|
||||
// No MonoBehaviour class found in this file
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Warning: Failed to extract class name from {}: {}",
|
||||
cs_path.display(),
|
||||
e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Parse the GUID string to Guid type
|
||||
let guid = match Guid::from_hex(meta.guid()) {
|
||||
Ok(g) => g,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Warning: Invalid GUID in {}: {}",
|
||||
path.display(),
|
||||
e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Store the mapping
|
||||
resolver.guid_to_class.insert(guid, class_name);
|
||||
}
|
||||
|
||||
Ok(resolver)
|
||||
}
|
||||
|
||||
/// Resolve a GUID to its class name
|
||||
///
|
||||
/// Accepts either a `&Guid` or a string that can be parsed as a GUID.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `guid` - The Unity GUID (e.g., "091c537484687e9419460cdcd7038234" or a `Guid`)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The class name if the GUID is found, otherwise None
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use unity_parser::{GuidResolver, Guid};
|
||||
/// # use std::path::Path;
|
||||
/// # let resolver = GuidResolver::from_project(Path::new("."))?;
|
||||
/// // Resolve by string
|
||||
/// if let Some(class_name) = resolver.resolve_class_name("091c537484687e9419460cdcd7038234") {
|
||||
/// println!("Found class: {}", class_name);
|
||||
/// }
|
||||
///
|
||||
/// // Resolve by Guid
|
||||
/// let guid = Guid::from_hex("091c537484687e9419460cdcd7038234")?;
|
||||
/// if let Some(class_name) = resolver.resolve_class_name(&guid) {
|
||||
/// println!("Found class: {}", class_name);
|
||||
/// }
|
||||
/// # Ok::<(), unity_parser::Error>(())
|
||||
/// ```
|
||||
pub fn resolve_class_name<G: AsGuid>(&self, guid: G) -> Option<&str> {
|
||||
guid.as_guid()
|
||||
.and_then(|g| self.guid_to_class.get(&g))
|
||||
.map(|s| s.as_str())
|
||||
}
|
||||
|
||||
/// Get the number of GUID mappings
|
||||
pub fn len(&self) -> usize {
|
||||
self.guid_to_class.len()
|
||||
}
|
||||
|
||||
/// Check if the resolver is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.guid_to_class.is_empty()
|
||||
}
|
||||
|
||||
/// Insert a GUID → class name mapping manually
|
||||
///
|
||||
/// Useful for testing or adding custom mappings
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use unity_parser::{GuidResolver, Guid};
|
||||
///
|
||||
/// let mut resolver = GuidResolver::new();
|
||||
/// let guid = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||
/// resolver.insert(guid, "PlaySFX".to_string());
|
||||
/// ```
|
||||
pub fn insert(&mut self, guid: Guid, class_name: String) {
|
||||
self.guid_to_class.insert(guid, class_name);
|
||||
}
|
||||
|
||||
/// Get all resolved GUIDs
|
||||
///
|
||||
/// Returns an iterator over all GUIDs in the resolver.
|
||||
pub fn guids(&self) -> impl Iterator<Item = Guid> + '_ {
|
||||
self.guid_to_class.keys().copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GuidResolver {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for types that can be converted to a GUID for lookup
|
||||
///
|
||||
/// This allows the `resolve_class_name` method to accept both `&Guid` and `&str`.
|
||||
pub trait AsGuid {
|
||||
/// Convert to a GUID, returning None if conversion fails
|
||||
fn as_guid(&self) -> Option<Guid>;
|
||||
}
|
||||
|
||||
impl AsGuid for Guid {
|
||||
fn as_guid(&self) -> Option<Guid> {
|
||||
Some(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsGuid for &Guid {
|
||||
fn as_guid(&self) -> Option<Guid> {
|
||||
Some(**self)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsGuid for str {
|
||||
fn as_guid(&self) -> Option<Guid> {
|
||||
Guid::from_hex(self).ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsGuid for &str {
|
||||
fn as_guid(&self) -> Option<Guid> {
|
||||
Guid::from_hex(self).ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsGuid for String {
|
||||
fn as_guid(&self) -> Option<Guid> {
|
||||
Guid::from_hex(self).ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a path points to a C# .meta file
|
||||
fn is_cs_meta_file(path: &Path) -> bool {
|
||||
path.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.map(|ext| ext == "meta")
|
||||
.unwrap_or(false)
|
||||
&& path
|
||||
.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.map(|name| name.ends_with(".cs.meta"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Extract the MonoBehaviour class name from a C# script file
|
||||
///
|
||||
/// This looks for patterns like:
|
||||
/// - `public class ClassName : MonoBehaviour`
|
||||
/// - `class ClassName : MonoBehaviour`
|
||||
/// - `public class ClassName:MonoBehaviour`
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cs_path` - Path to the .cs file
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Ok(Some(class_name)) if a MonoBehaviour class was found
|
||||
/// Ok(None) if no MonoBehaviour class was found
|
||||
/// Err if the file couldn't be read
|
||||
fn extract_class_name(cs_path: &Path) -> Result<Option<String>> {
|
||||
let content = std::fs::read_to_string(cs_path)?;
|
||||
|
||||
// Regex to match MonoBehaviour class declarations (direct or indirect inheritance)
|
||||
// Matches: public class ClassName : MonoBehaviour
|
||||
// Also matches: public class ClassName : SomeBaseClass (which may inherit from MonoBehaviour)
|
||||
// Captures the class name in group 1
|
||||
//
|
||||
// We match any class with inheritance (: BaseClass) because in Unity,
|
||||
// scripts can inherit from MonoBehaviour indirectly through base classes.
|
||||
// The component registration system will filter for actual MonoBehaviours.
|
||||
let class_regex = Regex::new(
|
||||
r"(?:public\s+)?class\s+(\w+)\s*:\s*\w+"
|
||||
).unwrap();
|
||||
|
||||
// Find the first class with inheritance in the file
|
||||
// Unity typically has one main class per script file
|
||||
if let Some(captures) = class_regex.captures(&content) {
|
||||
if let Some(class_name) = captures.get(1) {
|
||||
return Ok(Some(class_name.as_str().to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Find the Unity project root from any path within the project
|
||||
///
|
||||
/// Searches upward from the given path until it finds a directory
|
||||
/// containing an "Assets" folder.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Any path within the Unity project (file or directory)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The project root path if found, otherwise an error
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// 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::<(), unity_parser::Error>(())
|
||||
/// ```
|
||||
pub fn find_project_root(path: impl AsRef<Path>) -> Result<PathBuf> {
|
||||
let path = path.as_ref();
|
||||
|
||||
// Start from the file's directory (or the directory itself)
|
||||
let mut current = if path.is_file() {
|
||||
path.parent().ok_or_else(|| {
|
||||
Error::invalid_format("Cannot get parent directory")
|
||||
})?
|
||||
} else {
|
||||
path
|
||||
};
|
||||
|
||||
// Search upward for Assets/ or _GameAssets/ directory
|
||||
loop {
|
||||
let assets_dir = current.join("Assets");
|
||||
let game_assets_dir = current.join("_GameAssets");
|
||||
if (assets_dir.exists() && assets_dir.is_dir()) || (game_assets_dir.exists() && game_assets_dir.is_dir()) {
|
||||
return Ok(current.to_path_buf());
|
||||
}
|
||||
|
||||
// Move up one directory
|
||||
current = current.parent().ok_or_else(|| {
|
||||
Error::invalid_format(format!(
|
||||
"Could not find Unity project root from {}",
|
||||
path.display()
|
||||
))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_cs_meta_file() {
|
||||
assert!(is_cs_meta_file(Path::new("PlaySFX.cs.meta")));
|
||||
assert!(is_cs_meta_file(Path::new("Assets/Scripts/PlaySFX.cs.meta")));
|
||||
assert!(!is_cs_meta_file(Path::new("PlaySFX.cs")));
|
||||
assert!(!is_cs_meta_file(Path::new("Scene.unity.meta")));
|
||||
assert!(!is_cs_meta_file(Path::new("texture.png.meta")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_class_name() {
|
||||
// Test standard MonoBehaviour class
|
||||
let content = r#"
|
||||
using UnityEngine;
|
||||
|
||||
public class PlaySFX : MonoBehaviour
|
||||
{
|
||||
public float volume = 1.0f;
|
||||
}
|
||||
"#;
|
||||
let temp_file = std::env::temp_dir().join("test_playsfx.cs");
|
||||
std::fs::write(&temp_file, content).unwrap();
|
||||
let result = extract_class_name(&temp_file).unwrap();
|
||||
assert_eq!(result, Some("PlaySFX".to_string()));
|
||||
std::fs::remove_file(&temp_file).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_class_name_no_public() {
|
||||
// Test without public modifier
|
||||
let content = r#"
|
||||
using UnityEngine;
|
||||
|
||||
class MyBehaviour : MonoBehaviour
|
||||
{
|
||||
void Start() {}
|
||||
}
|
||||
"#;
|
||||
let temp_file = std::env::temp_dir().join("test_mybehaviour.cs");
|
||||
std::fs::write(&temp_file, content).unwrap();
|
||||
let result = extract_class_name(&temp_file).unwrap();
|
||||
assert_eq!(result, Some("MyBehaviour".to_string()));
|
||||
std::fs::remove_file(&temp_file).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_class_name_no_space() {
|
||||
// Test with no space after colon
|
||||
let content = r#"
|
||||
using UnityEngine;
|
||||
|
||||
public class TestScript:MonoBehaviour
|
||||
{
|
||||
void Update() {}
|
||||
}
|
||||
"#;
|
||||
let temp_file = std::env::temp_dir().join("test_testscript.cs");
|
||||
std::fs::write(&temp_file, content).unwrap();
|
||||
let result = extract_class_name(&temp_file).unwrap();
|
||||
assert_eq!(result, Some("TestScript".to_string()));
|
||||
std::fs::remove_file(&temp_file).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_class_name_not_monobehaviour() {
|
||||
// Test class that doesn't inherit from MonoBehaviour
|
||||
let content = r#"
|
||||
using UnityEngine;
|
||||
|
||||
public class HelperClass
|
||||
{
|
||||
public int value;
|
||||
}
|
||||
"#;
|
||||
let temp_file = std::env::temp_dir().join("test_helper.cs");
|
||||
std::fs::write(&temp_file, content).unwrap();
|
||||
let result = extract_class_name(&temp_file).unwrap();
|
||||
assert_eq!(result, None);
|
||||
std::fs::remove_file(&temp_file).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_guid_resolver_manual() {
|
||||
let mut resolver = GuidResolver::new();
|
||||
assert!(resolver.is_empty());
|
||||
|
||||
let guid = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||
resolver.insert(guid, "PlaySFX".to_string());
|
||||
|
||||
assert_eq!(resolver.len(), 1);
|
||||
|
||||
// Test resolving by string
|
||||
assert_eq!(
|
||||
resolver.resolve_class_name("091c537484687e9419460cdcd7038234"),
|
||||
Some("PlaySFX")
|
||||
);
|
||||
|
||||
// Test resolving by Guid
|
||||
assert_eq!(
|
||||
resolver.resolve_class_name(&guid),
|
||||
Some("PlaySFX")
|
||||
);
|
||||
|
||||
// Test nonexistent GUID
|
||||
assert_eq!(
|
||||
resolver.resolve_class_name("00000000000000000000000000000000"),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<Path>) -> Result<Self> {
|
||||
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");
|
||||
638
unity-parser/src/parser/mod.rs
Normal file
638
unity-parser/src/parser/mod.rs
Normal file
@@ -0,0 +1,638 @@
|
||||
//! Unity YAML parsing module
|
||||
|
||||
pub mod guid_resolver;
|
||||
pub mod meta;
|
||||
pub mod prefab_guid_resolver;
|
||||
mod unity_tag;
|
||||
mod yaml;
|
||||
|
||||
pub use guid_resolver::{find_project_root, GuidResolver};
|
||||
pub use meta::{get_meta_path, MetaFile};
|
||||
pub use prefab_guid_resolver::PrefabGuidResolver;
|
||||
pub use unity_tag::{parse_unity_tag, UnityTag};
|
||||
pub use yaml::split_yaml_documents;
|
||||
|
||||
use crate::model::{RawDocument, UnityAsset, UnityFile, UnityPrefab, UnityScene};
|
||||
use crate::types::{FileID, Guid, TypeFilter};
|
||||
use crate::{Error, Result};
|
||||
use regex::Regex;
|
||||
use std::path::Path;
|
||||
|
||||
/// Parse a Unity file from the given path
|
||||
///
|
||||
/// Automatically detects file type based on extension:
|
||||
/// - .unity → UnityFile::Scene with ECS World
|
||||
/// - .prefab → UnityFile::Prefab with raw YAML
|
||||
/// - .asset → UnityFile::Asset with raw YAML
|
||||
///
|
||||
/// By default, parses all files. Use `parse_unity_file_filtered` for regex filtering.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use unity_parser::parser::parse_unity_file;
|
||||
/// use unity_parser::UnityFile;
|
||||
/// use std::path::Path;
|
||||
///
|
||||
/// let file = parse_unity_file(Path::new("Scene.unity"))?;
|
||||
/// match file {
|
||||
/// UnityFile::Scene(scene) => println!("Scene with {} entities", scene.entity_map.len()),
|
||||
/// UnityFile::Prefab(prefab) => println!("Prefab with {} documents", prefab.documents.len()),
|
||||
/// UnityFile::Asset(asset) => println!("Asset with {} documents", asset.documents.len()),
|
||||
/// }
|
||||
/// # Ok::<(), unity_parser::Error>(())
|
||||
/// ```
|
||||
pub fn parse_unity_file(path: &Path) -> Result<UnityFile> {
|
||||
parse_unity_file_filtered(path, None, None)
|
||||
}
|
||||
|
||||
/// Parse a Unity file with optional regex filtering and type filtering
|
||||
///
|
||||
/// Same as `parse_unity_file`, but allows filtering files by path pattern and Unity types.
|
||||
/// If the path doesn't match the regex, returns an error.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Path to the Unity file to parse
|
||||
/// * `filter` - Optional regex to match against the file path. If None, parses all files (default behavior).
|
||||
/// * `type_filter` - Optional filter for Unity types and MonoBehaviour GUIDs. If None, parses all types (default behavior).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use unity_parser::parser::{parse_unity_file_filtered};
|
||||
/// use unity_parser::TypeFilter;
|
||||
/// use regex::Regex;
|
||||
/// use std::path::Path;
|
||||
/// use std::collections::HashSet;
|
||||
///
|
||||
/// // Only parse files with "Test" in the name
|
||||
/// let filter = Regex::new(r"Test").unwrap();
|
||||
/// let file = parse_unity_file_filtered(Path::new("TestScene.unity"), Some(&filter), None)?;
|
||||
///
|
||||
/// // Only parse Transform and GameObject types
|
||||
/// let mut types = HashSet::new();
|
||||
/// types.insert("Transform".to_string());
|
||||
/// 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::<(), unity_parser::Error>(())
|
||||
/// ```
|
||||
pub fn parse_unity_file_filtered(
|
||||
path: &Path,
|
||||
filter: Option<&Regex>,
|
||||
type_filter: Option<&TypeFilter>,
|
||||
) -> Result<UnityFile> {
|
||||
// Apply filter if provided
|
||||
if let Some(regex) = filter {
|
||||
let path_str = path.to_str().ok_or_else(|| {
|
||||
Error::invalid_format("Path contains invalid UTF-8")
|
||||
})?;
|
||||
|
||||
if !regex.is_match(path_str) {
|
||||
return Err(Error::invalid_format(format!(
|
||||
"Path '{}' does not match filter pattern",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
parse_unity_file_impl(path, type_filter)
|
||||
}
|
||||
|
||||
/// Internal implementation of Unity file parsing
|
||||
fn parse_unity_file_impl(path: &Path, type_filter: Option<&TypeFilter>) -> Result<UnityFile> {
|
||||
// Read the file
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
|
||||
// Validate Unity header
|
||||
validate_unity_header(&content, path)?;
|
||||
|
||||
// Detect file type by extension
|
||||
let file_type = detect_file_type(path);
|
||||
|
||||
// Parse based on file type
|
||||
match file_type {
|
||||
FileType::Scene => parse_scene(path, &content, type_filter),
|
||||
FileType::Prefab => parse_prefab(path, &content, type_filter),
|
||||
FileType::Asset => parse_asset(path, &content, type_filter),
|
||||
FileType::Unknown => Err(Error::invalid_format(format!(
|
||||
"Unknown file extension: {}",
|
||||
path.display()
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// File type enumeration
|
||||
enum FileType {
|
||||
Scene,
|
||||
Prefab,
|
||||
Asset,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Detect file type based on extension
|
||||
fn detect_file_type(path: &Path) -> FileType {
|
||||
match path.extension().and_then(|s| s.to_str()) {
|
||||
Some("unity") => FileType::Scene,
|
||||
Some("prefab") => FileType::Prefab,
|
||||
Some("asset") => FileType::Asset,
|
||||
_ => FileType::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a scene file into an ECS World
|
||||
fn parse_scene(path: &Path, content: &str, type_filter: Option<&TypeFilter>) -> Result<UnityFile> {
|
||||
let raw_documents = parse_raw_documents(content, type_filter)?;
|
||||
|
||||
// Try to find Unity project root and build both GUID resolvers
|
||||
let (guid_resolver, prefab_guid_resolver) = match find_project_root(path) {
|
||||
Ok(project_root) => {
|
||||
eprintln!("📦 Found Unity project root: {}", project_root.display());
|
||||
|
||||
// Build script GUID resolver
|
||||
let guid_res = match GuidResolver::from_project(&project_root) {
|
||||
Ok(resolver) => {
|
||||
eprintln!(" ✅ Script GUID resolver built ({} mappings)", resolver.len());
|
||||
Some(resolver)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(" ⚠️ Warning: Failed to build script GUID resolver: {}", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// Build prefab GUID resolver
|
||||
let prefab_res = match PrefabGuidResolver::from_project(&project_root) {
|
||||
Ok(resolver) => {
|
||||
eprintln!(" ✅ Prefab GUID resolver built ({} mappings)", resolver.len());
|
||||
Some(resolver)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(" ⚠️ Warning: Failed to build prefab GUID resolver: {}", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
(guid_res, prefab_res)
|
||||
}
|
||||
Err(_) => {
|
||||
// Not part of a Unity project, or project root not found
|
||||
(None, None)
|
||||
}
|
||||
};
|
||||
|
||||
// Build ECS world from documents with both resolvers
|
||||
let (world, entity_map) = crate::ecs::build_world_from_documents(
|
||||
raw_documents,
|
||||
guid_resolver.as_ref(),
|
||||
prefab_guid_resolver.as_ref(),
|
||||
)?;
|
||||
|
||||
Ok(UnityFile::Scene(UnityScene::new(
|
||||
path.to_path_buf(),
|
||||
world,
|
||||
entity_map,
|
||||
)))
|
||||
}
|
||||
|
||||
/// Parse a prefab file into raw YAML documents
|
||||
fn parse_prefab(path: &Path, content: &str, type_filter: Option<&TypeFilter>) -> Result<UnityFile> {
|
||||
let raw_documents = parse_raw_documents(content, type_filter)?;
|
||||
|
||||
Ok(UnityFile::Prefab(UnityPrefab::new(
|
||||
path.to_path_buf(),
|
||||
raw_documents,
|
||||
)))
|
||||
}
|
||||
|
||||
/// Parse an asset file into raw YAML documents
|
||||
fn parse_asset(path: &Path, content: &str, type_filter: Option<&TypeFilter>) -> Result<UnityFile> {
|
||||
let raw_documents = parse_raw_documents(content, type_filter)?;
|
||||
|
||||
Ok(UnityFile::Asset(UnityAsset::new(
|
||||
path.to_path_buf(),
|
||||
raw_documents,
|
||||
)))
|
||||
}
|
||||
|
||||
/// Parse raw YAML documents from file content with optional type filtering
|
||||
fn parse_raw_documents(content: &str, type_filter: Option<&TypeFilter>) -> Result<Vec<RawDocument>> {
|
||||
// Split into individual YAML documents
|
||||
let raw_docs = split_yaml_documents(content)?;
|
||||
|
||||
// Parse each document
|
||||
raw_docs
|
||||
.iter()
|
||||
.filter_map(|raw| parse_raw_document(raw, type_filter).transpose())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Parse a single raw YAML document into a RawDocument with optional type filtering
|
||||
fn parse_raw_document(raw_doc: &str, type_filter: Option<&TypeFilter>) -> Result<Option<RawDocument>> {
|
||||
// Parse the Unity tag line (e.g., "--- !u!1 &12345")
|
||||
let tag = match parse_unity_tag(raw_doc) {
|
||||
Some(tag) => tag,
|
||||
None => return Ok(None), // Skip documents without Unity tags
|
||||
};
|
||||
|
||||
// Extract the YAML content (everything after the tag line)
|
||||
let yaml_content = extract_yaml_content(raw_doc);
|
||||
|
||||
if yaml_content.trim().is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Early filtering: Extract class name without full YAML parsing
|
||||
if let Some(filter) = type_filter {
|
||||
if filter.is_filtering() {
|
||||
// Extract the class name efficiently
|
||||
let class_name = match extract_class_name(yaml_content) {
|
||||
Some(name) => name,
|
||||
None => return Ok(None), // Can't extract class name, skip
|
||||
};
|
||||
|
||||
// Check if this is a MonoBehaviour
|
||||
if class_name == "MonoBehaviour" {
|
||||
// For MonoBehaviour, we need to check the m_Script GUID
|
||||
match extract_monobehaviour_guid(yaml_content) {
|
||||
Some(guid) => {
|
||||
if !filter.should_parse_guid(&guid) {
|
||||
// GUID not in whitelist, skip this document
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Can't extract GUID, skip this MonoBehaviour
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For non-MonoBehaviour, check the Unity type whitelist
|
||||
if !filter.should_parse_type(class_name) {
|
||||
// Type not in whitelist, skip this document
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach here, the document passed the filter (or no filter was applied)
|
||||
// Now do the full YAML parsing
|
||||
let yaml_value: serde_yaml::Value = serde_yaml::from_str(yaml_content)?;
|
||||
|
||||
// Unity documents have format "GameObject: { ... }"
|
||||
// Extract class name and inner YAML
|
||||
let (class_name, inner_yaml) = match &yaml_value {
|
||||
serde_yaml::Value::Mapping(map) if map.len() == 1 => {
|
||||
// Single-key mapping - this is the standard Unity format
|
||||
let (key, value) = map.iter().next().unwrap();
|
||||
let class_name = key
|
||||
.as_str()
|
||||
.ok_or_else(|| Error::invalid_format("Class name must be string"))?
|
||||
.to_string();
|
||||
(class_name, value.clone())
|
||||
}
|
||||
_ => {
|
||||
// Fallback for malformed documents
|
||||
let class_name = format!("UnityType{}", tag.type_id);
|
||||
(class_name, yaml_value)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Some(RawDocument::new(
|
||||
tag.type_id,
|
||||
FileID::from_i64(tag.file_id),
|
||||
class_name,
|
||||
inner_yaml,
|
||||
)))
|
||||
}
|
||||
|
||||
/// Validate that the file has a proper Unity YAML header
|
||||
fn validate_unity_header(content: &str, path: &Path) -> Result<()> {
|
||||
let has_yaml_header = content.starts_with("%YAML");
|
||||
let has_unity_tag = content.contains("%TAG !u! tag:unity3d.com");
|
||||
|
||||
if !has_yaml_header || !has_unity_tag {
|
||||
return Err(Error::MissingHeader(path.to_path_buf()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract the YAML content from a raw document (skip the Unity tag line)
|
||||
fn extract_yaml_content(raw_doc: &str) -> &str {
|
||||
// Find the first newline after the "--- !u!" tag
|
||||
if let Some(first_line_end) = raw_doc.find('\n') {
|
||||
&raw_doc[first_line_end + 1..]
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the Unity class name from YAML content without full parsing
|
||||
///
|
||||
/// Unity documents have the format:
|
||||
/// ```yaml
|
||||
/// ClassName:
|
||||
/// field1: value1
|
||||
/// field2: value2
|
||||
/// ```
|
||||
///
|
||||
/// This function extracts "ClassName" efficiently without parsing the entire YAML.
|
||||
fn extract_class_name(yaml_content: &str) -> Option<&str> {
|
||||
// Find the first line that's not empty
|
||||
let first_line = yaml_content.lines().find(|line| !line.trim().is_empty())?;
|
||||
|
||||
// Class name is the first non-whitespace text before ':'
|
||||
let class_name = first_line.trim().strip_suffix(':')?;
|
||||
|
||||
Some(class_name)
|
||||
}
|
||||
|
||||
/// Extract the m_Script GUID from a MonoBehaviour YAML document without full parsing
|
||||
///
|
||||
/// MonoBehaviour documents have the format:
|
||||
/// ```yaml
|
||||
/// MonoBehaviour:
|
||||
/// m_Script: {fileID: 11500000, guid: d39ddbf1c2c3d1a4baa070e5e76548bd, type: 3}
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Or multi-line format:
|
||||
/// ```yaml
|
||||
/// MonoBehaviour:
|
||||
/// m_Script:
|
||||
/// fileID: 11500000
|
||||
/// guid: d39ddbf1c2c3d1a4baa070e5e76548bd
|
||||
/// type: 3
|
||||
/// ```
|
||||
///
|
||||
/// This function extracts the GUID value efficiently.
|
||||
fn extract_monobehaviour_guid(yaml_content: &str) -> Option<Guid> {
|
||||
// Look for any line with "guid: <32 hex chars>"
|
||||
// This works for both inline and multi-line formats
|
||||
for line in yaml_content.lines() {
|
||||
if line.contains("guid:") {
|
||||
// Find "guid: " and extract the 32-character hex string after it
|
||||
if let Some(guid_start) = line.find("guid:") {
|
||||
let after_guid = &line[guid_start + 5..].trim();
|
||||
|
||||
// Extract the hex string (32 characters)
|
||||
let guid_str: String = after_guid
|
||||
.chars()
|
||||
.take_while(|c| c.is_ascii_hexdigit())
|
||||
.collect();
|
||||
|
||||
if guid_str.len() == 32 {
|
||||
return Guid::from_hex(&guid_str).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_validate_unity_header() {
|
||||
let valid_content = "%YAML 1.1\n%TAG !u! tag:unity3d.com,2011:\n";
|
||||
assert!(validate_unity_header(valid_content, Path::new("test.unity")).is_ok());
|
||||
|
||||
let invalid_content = "Not a Unity file";
|
||||
assert!(validate_unity_header(invalid_content, Path::new("test.unity")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_yaml_content() {
|
||||
let raw_doc = "--- !u!1 &12345\nGameObject:\n m_Name: Test";
|
||||
let content = extract_yaml_content(raw_doc);
|
||||
assert_eq!(content, "GameObject:\n m_Name: Test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_file_type() {
|
||||
assert!(matches!(
|
||||
detect_file_type(Path::new("test.unity")),
|
||||
FileType::Scene
|
||||
));
|
||||
assert!(matches!(
|
||||
detect_file_type(Path::new("test.prefab")),
|
||||
FileType::Prefab
|
||||
));
|
||||
assert!(matches!(
|
||||
detect_file_type(Path::new("test.asset")),
|
||||
FileType::Asset
|
||||
));
|
||||
assert!(matches!(
|
||||
detect_file_type(Path::new("test.txt")),
|
||||
FileType::Unknown
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_unity_file_filtered_accepts_matching_path() {
|
||||
use regex::Regex;
|
||||
|
||||
let filter = Regex::new(r"Test").unwrap();
|
||||
let path = Path::new("TestScene.unity");
|
||||
|
||||
// Should match and attempt to parse (will fail because file doesn't exist)
|
||||
let result = parse_unity_file_filtered(path, Some(&filter), None);
|
||||
assert!(result.is_err());
|
||||
|
||||
// Error should be IO error (file not found), not filter error
|
||||
match result {
|
||||
Err(e) => {
|
||||
let err_msg = e.to_string();
|
||||
assert!(
|
||||
!err_msg.contains("does not match filter"),
|
||||
"Should not be a filter error, got: {}",
|
||||
err_msg
|
||||
);
|
||||
}
|
||||
Ok(_) => panic!("Expected error for non-existent file"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_unity_file_filtered_rejects_non_matching_path() {
|
||||
use regex::Regex;
|
||||
|
||||
let filter = Regex::new(r"Test").unwrap();
|
||||
let path = Path::new("MainScene.unity");
|
||||
|
||||
// Should reject due to filter
|
||||
let result = parse_unity_file_filtered(path, Some(&filter), None);
|
||||
assert!(result.is_err());
|
||||
|
||||
// Error should be filter error
|
||||
match result {
|
||||
Err(e) => {
|
||||
let err_msg = e.to_string();
|
||||
assert!(
|
||||
err_msg.contains("does not match filter"),
|
||||
"Expected filter error, got: {}",
|
||||
err_msg
|
||||
);
|
||||
}
|
||||
Ok(_) => panic!("Expected filter error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_unity_file_filtered_none_accepts_all() {
|
||||
let path = Path::new("AnyScene.unity");
|
||||
|
||||
// No filter should accept any path (will fail with IO error)
|
||||
let result = parse_unity_file_filtered(path, None, None);
|
||||
assert!(result.is_err());
|
||||
|
||||
// Should be IO error, not filter error
|
||||
match result {
|
||||
Err(e) => {
|
||||
let err_msg = e.to_string();
|
||||
assert!(
|
||||
!err_msg.contains("does not match filter"),
|
||||
"Should not be a filter error with None filter, got: {}",
|
||||
err_msg
|
||||
);
|
||||
}
|
||||
Ok(_) => panic!("Expected IO error for non-existent file"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_unity_file_uses_default_filter() {
|
||||
let path = Path::new("AnyScene.unity");
|
||||
|
||||
// parse_unity_file should work the same as filtered with None
|
||||
let result1 = parse_unity_file(path);
|
||||
let result2 = parse_unity_file_filtered(path, None, None);
|
||||
|
||||
// Both should have the same error (IO error for missing file)
|
||||
assert!(result1.is_err());
|
||||
assert!(result2.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_class_name() {
|
||||
let yaml = "GameObject:\n m_Name: Test";
|
||||
assert_eq!(extract_class_name(yaml), Some("GameObject"));
|
||||
|
||||
let yaml2 = "Transform:\n m_LocalPosition: {x: 1, y: 2, z: 3}";
|
||||
assert_eq!(extract_class_name(yaml2), Some("Transform"));
|
||||
|
||||
let yaml3 = "MonoBehaviour:\n m_Script: {fileID: 11500000}";
|
||||
assert_eq!(extract_class_name(yaml3), Some("MonoBehaviour"));
|
||||
|
||||
let empty = "";
|
||||
assert_eq!(extract_class_name(empty), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_monobehaviour_guid() {
|
||||
let yaml = "MonoBehaviour:\n m_Script: {fileID: 11500000, guid: d39ddbf1c2c3d1a4baa070e5e76548bd, type: 3}";
|
||||
let guid = extract_monobehaviour_guid(yaml);
|
||||
assert!(guid.is_some());
|
||||
assert_eq!(
|
||||
guid.unwrap().to_hex(),
|
||||
"d39ddbf1c2c3d1a4baa070e5e76548bd"
|
||||
);
|
||||
|
||||
// Multi-line format
|
||||
let yaml2 = "MonoBehaviour:\n m_Script:\n fileID: 11500000\n guid: abc123def456789012345678901234ab\n type: 3";
|
||||
let guid2 = extract_monobehaviour_guid(yaml2);
|
||||
assert!(guid2.is_some());
|
||||
assert_eq!(
|
||||
guid2.unwrap().to_hex(),
|
||||
"abc123def456789012345678901234ab"
|
||||
);
|
||||
|
||||
let no_guid = "MonoBehaviour:\n m_Name: Test";
|
||||
assert_eq!(extract_monobehaviour_guid(no_guid), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_type_filter_document_parse_all() {
|
||||
let filter = TypeFilter::parse_all();
|
||||
assert!(filter.should_parse_type("Transform"));
|
||||
assert!(filter.should_parse_type("GameObject"));
|
||||
assert!(filter.should_parse_type("AnyType"));
|
||||
assert!(!filter.is_filtering());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_type_filter_document_with_unity_types() {
|
||||
use std::collections::HashSet;
|
||||
|
||||
let mut types = HashSet::new();
|
||||
types.insert("Transform".to_string());
|
||||
types.insert("GameObject".to_string());
|
||||
|
||||
let filter = TypeFilter::with_unity_types(types);
|
||||
|
||||
assert!(filter.should_parse_type("Transform"));
|
||||
assert!(filter.should_parse_type("GameObject"));
|
||||
assert!(!filter.should_parse_type("RectTransform"));
|
||||
assert!(filter.is_filtering());
|
||||
|
||||
// Should still accept any MonoBehaviour GUID since we didn't set a GUID filter
|
||||
let guid = Guid::from_hex("d39ddbf1c2c3d1a4baa070e5e76548bd").unwrap();
|
||||
assert!(filter.should_parse_guid(&guid));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_type_filter_document_with_monobehaviour_guids() {
|
||||
use std::collections::HashSet;
|
||||
|
||||
let mut guids = HashSet::new();
|
||||
let guid1 = Guid::from_hex("d39ddbf1c2c3d1a4baa070e5e76548bd").unwrap();
|
||||
let guid2 = Guid::from_hex("abc123def456789012345678901234ab").unwrap();
|
||||
guids.insert(guid1);
|
||||
guids.insert(guid2);
|
||||
|
||||
let filter = TypeFilter::with_monobehaviour_guids(guids);
|
||||
|
||||
assert!(filter.should_parse_guid(&guid1));
|
||||
assert!(filter.should_parse_guid(&guid2));
|
||||
|
||||
let guid3 = Guid::from_hex("00000000000000000000000000000000").unwrap();
|
||||
assert!(!filter.should_parse_guid(&guid3));
|
||||
|
||||
assert!(filter.is_filtering());
|
||||
|
||||
// Should still accept any Unity type since we didn't set a type filter
|
||||
assert!(filter.should_parse_type("Transform"));
|
||||
assert!(filter.should_parse_type("AnyType"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_type_filter_document_with_both() {
|
||||
use std::collections::HashSet;
|
||||
|
||||
let mut types = HashSet::new();
|
||||
types.insert("Transform".to_string());
|
||||
|
||||
let mut guids = HashSet::new();
|
||||
let guid1 = Guid::from_hex("d39ddbf1c2c3d1a4baa070e5e76548bd").unwrap();
|
||||
guids.insert(guid1);
|
||||
|
||||
let filter = TypeFilter::with_both(types, guids);
|
||||
|
||||
// Only Transform should pass
|
||||
assert!(filter.should_parse_type("Transform"));
|
||||
assert!(!filter.should_parse_type("GameObject"));
|
||||
|
||||
// Only guid1 should pass
|
||||
assert!(filter.should_parse_guid(&guid1));
|
||||
let guid2 = Guid::from_hex("abc123def456789012345678901234ab").unwrap();
|
||||
assert!(!filter.should_parse_guid(&guid2));
|
||||
|
||||
assert!(filter.is_filtering());
|
||||
}
|
||||
}
|
||||
314
unity-parser/src/parser/prefab_guid_resolver.rs
Normal file
314
unity-parser/src/parser/prefab_guid_resolver.rs
Normal file
@@ -0,0 +1,314 @@
|
||||
//! Unity GUID resolution for Prefab files
|
||||
//!
|
||||
//! This module resolves Unity GUIDs to their corresponding Prefab file paths
|
||||
//! by scanning .prefab.meta files.
|
||||
//!
|
||||
//! # How Unity Prefab GUID Resolution Works
|
||||
//!
|
||||
//! 1. Every prefab has a `.prefab.meta` file with a unique GUID
|
||||
//! 2. PrefabInstance components reference prefabs via GUID in `m_CorrespondingSourceObject`
|
||||
//! 3. We scan `.prefab.meta` files to build a GUID → Prefab Path mapping
|
||||
//! 4. Result: GUID → File Path mapping for automatic prefab instantiation
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use unity_parser::parser::PrefabGuidResolver;
|
||||
//! use std::path::Path;
|
||||
//!
|
||||
//! // Build resolver from Unity project directory
|
||||
//! let project_path = Path::new("path/to/UnityProject");
|
||||
//! let resolver = PrefabGuidResolver::from_project(project_path)?;
|
||||
//!
|
||||
//! // Resolve a GUID to prefab path
|
||||
//! let guid = "091c537484687e9419460cdcd7038234";
|
||||
//! if let Some(path) = resolver.resolve_path(guid) {
|
||||
//! println!("GUID {} → {}", guid, path.display());
|
||||
//! }
|
||||
//! # Ok::<(), unity_parser::Error>(())
|
||||
//! ```
|
||||
|
||||
use crate::parser::meta::MetaFile;
|
||||
use crate::types::Guid;
|
||||
use crate::{Error, Result};
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// Resolves Unity GUIDs to Prefab file paths
|
||||
///
|
||||
/// This struct builds a mapping from GUID to prefab file path by scanning
|
||||
/// a Unity project's `.prefab.meta` files.
|
||||
///
|
||||
/// Uses a 128-bit `Guid` type for efficient storage and fast comparisons.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PrefabGuidResolver {
|
||||
/// Map from GUID to Prefab file path
|
||||
guid_to_path: HashMap<Guid, PathBuf>,
|
||||
}
|
||||
|
||||
impl PrefabGuidResolver {
|
||||
/// Create a new empty PrefabGuidResolver
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
guid_to_path: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a PrefabGuidResolver by scanning a Unity project directory
|
||||
///
|
||||
/// This scans for all `.prefab.meta` files and extracts their GUIDs
|
||||
/// to build a GUID → Prefab Path mapping.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `project_path` - Path to the Unity project root (containing Assets/ folder)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use unity_parser::parser::PrefabGuidResolver;
|
||||
/// use std::path::Path;
|
||||
///
|
||||
/// let resolver = PrefabGuidResolver::from_project(Path::new("MyUnityProject"))?;
|
||||
/// # Ok::<(), unity_parser::Error>(())
|
||||
/// ```
|
||||
pub fn from_project(project_path: impl AsRef<Path>) -> Result<Self> {
|
||||
let project_path = project_path.as_ref();
|
||||
|
||||
// Verify this looks like a Unity project - check for Assets/ or _GameAssets/
|
||||
let assets_dir = if project_path.join("Assets").exists() {
|
||||
project_path.join("Assets")
|
||||
} else if project_path.join("_GameAssets").exists() {
|
||||
project_path.join("_GameAssets")
|
||||
} else {
|
||||
return Err(Error::invalid_format(format!(
|
||||
"Not a Unity project: missing Assets/ or _GameAssets/ directory at {}",
|
||||
project_path.display()
|
||||
)));
|
||||
};
|
||||
|
||||
let mut resolver = Self::new();
|
||||
|
||||
// Walk the Assets directory looking for .prefab.meta files
|
||||
for entry in WalkDir::new(&assets_dir)
|
||||
.follow_links(false)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
{
|
||||
let path = entry.path();
|
||||
|
||||
// Only process .prefab.meta files
|
||||
if !is_prefab_meta_file(path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse the .meta file to get the GUID
|
||||
let meta = match MetaFile::from_path(path) {
|
||||
Ok(meta) => meta,
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Failed to parse {}: {}", path.display(), e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Get the corresponding .prefab file path
|
||||
let prefab_path = path.with_file_name(
|
||||
path.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.and_then(|s| s.strip_suffix(".meta"))
|
||||
.unwrap_or(""),
|
||||
);
|
||||
|
||||
// Parse the GUID string to Guid type
|
||||
let guid = match Guid::from_hex(meta.guid()) {
|
||||
Ok(g) => g,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Warning: Invalid GUID in {}: {}",
|
||||
path.display(),
|
||||
e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Store the mapping
|
||||
resolver.guid_to_path.insert(guid, prefab_path);
|
||||
}
|
||||
|
||||
Ok(resolver)
|
||||
}
|
||||
|
||||
/// Resolve a GUID to its prefab file path
|
||||
///
|
||||
/// Accepts either a `&Guid` or a string that can be parsed as a GUID.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `guid` - The Unity GUID (e.g., "091c537484687e9419460cdcd7038234" or a `Guid`)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The prefab file path if the GUID is found, otherwise None
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use unity_parser::{PrefabGuidResolver, Guid};
|
||||
/// # use std::path::Path;
|
||||
/// # let resolver = PrefabGuidResolver::from_project(Path::new("."))?;
|
||||
/// // Resolve by string
|
||||
/// if let Some(path) = resolver.resolve_path("091c537484687e9419460cdcd7038234") {
|
||||
/// println!("Found prefab: {}", path.display());
|
||||
/// }
|
||||
///
|
||||
/// // Resolve by Guid
|
||||
/// let guid = Guid::from_hex("091c537484687e9419460cdcd7038234")?;
|
||||
/// if let Some(path) = resolver.resolve_path(&guid) {
|
||||
/// println!("Found prefab: {}", path.display());
|
||||
/// }
|
||||
/// # Ok::<(), unity_parser::Error>(())
|
||||
/// ```
|
||||
pub fn resolve_path<G: AsGuid>(&self, guid: G) -> Option<&Path> {
|
||||
guid.as_guid()
|
||||
.and_then(|g| self.guid_to_path.get(&g))
|
||||
.map(|p| p.as_path())
|
||||
}
|
||||
|
||||
/// Get the number of GUID mappings
|
||||
pub fn len(&self) -> usize {
|
||||
self.guid_to_path.len()
|
||||
}
|
||||
|
||||
/// Check if the resolver is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.guid_to_path.is_empty()
|
||||
}
|
||||
|
||||
/// Insert a GUID → prefab path mapping manually
|
||||
///
|
||||
/// Useful for testing or adding custom mappings
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use unity_parser::{PrefabGuidResolver, Guid};
|
||||
/// use std::path::PathBuf;
|
||||
///
|
||||
/// let mut resolver = PrefabGuidResolver::new();
|
||||
/// let guid = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||
/// resolver.insert(guid, PathBuf::from("Assets/Prefabs/Player.prefab"));
|
||||
/// ```
|
||||
pub fn insert(&mut self, guid: Guid, path: PathBuf) {
|
||||
self.guid_to_path.insert(guid, path);
|
||||
}
|
||||
|
||||
/// Get all resolved GUIDs
|
||||
///
|
||||
/// Returns an iterator over all GUIDs in the resolver.
|
||||
pub fn guids(&self) -> impl Iterator<Item = Guid> + '_ {
|
||||
self.guid_to_path.keys().copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PrefabGuidResolver {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for types that can be converted to a GUID for lookup
|
||||
///
|
||||
/// This allows the `resolve_path` method to accept both `&Guid` and `&str`.
|
||||
pub trait AsGuid {
|
||||
/// Convert to a GUID, returning None if conversion fails
|
||||
fn as_guid(&self) -> Option<Guid>;
|
||||
}
|
||||
|
||||
impl AsGuid for Guid {
|
||||
fn as_guid(&self) -> Option<Guid> {
|
||||
Some(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsGuid for &Guid {
|
||||
fn as_guid(&self) -> Option<Guid> {
|
||||
Some(**self)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsGuid for str {
|
||||
fn as_guid(&self) -> Option<Guid> {
|
||||
Guid::from_hex(self).ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsGuid for &str {
|
||||
fn as_guid(&self) -> Option<Guid> {
|
||||
Guid::from_hex(self).ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsGuid for String {
|
||||
fn as_guid(&self) -> Option<Guid> {
|
||||
Guid::from_hex(self).ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a path points to a Prefab .meta file
|
||||
fn is_prefab_meta_file(path: &Path) -> bool {
|
||||
path.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.map(|ext| ext == "meta")
|
||||
.unwrap_or(false)
|
||||
&& path
|
||||
.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.map(|name| name.ends_with(".prefab.meta"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_prefab_meta_file() {
|
||||
assert!(is_prefab_meta_file(Path::new("Player.prefab.meta")));
|
||||
assert!(is_prefab_meta_file(Path::new("Assets/Prefabs/Player.prefab.meta")));
|
||||
assert!(!is_prefab_meta_file(Path::new("Player.prefab")));
|
||||
assert!(!is_prefab_meta_file(Path::new("PlaySFX.cs.meta")));
|
||||
assert!(!is_prefab_meta_file(Path::new("Scene.unity.meta")));
|
||||
assert!(!is_prefab_meta_file(Path::new("texture.png.meta")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prefab_guid_resolver_manual() {
|
||||
let mut resolver = PrefabGuidResolver::new();
|
||||
assert!(resolver.is_empty());
|
||||
|
||||
let guid = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||
resolver.insert(guid, PathBuf::from("Assets/Prefabs/Player.prefab"));
|
||||
|
||||
assert_eq!(resolver.len(), 1);
|
||||
|
||||
// Test resolving by string
|
||||
assert_eq!(
|
||||
resolver.resolve_path("091c537484687e9419460cdcd7038234").map(|p| p.to_str().unwrap()),
|
||||
Some("Assets/Prefabs/Player.prefab")
|
||||
);
|
||||
|
||||
// Test resolving by Guid
|
||||
assert_eq!(
|
||||
resolver.resolve_path(&guid).map(|p| p.to_str().unwrap()),
|
||||
Some("Assets/Prefabs/Player.prefab")
|
||||
);
|
||||
|
||||
// Test nonexistent GUID
|
||||
assert_eq!(
|
||||
resolver.resolve_path("00000000000000000000000000000000"),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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];
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use crate::types::*;
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use sparsey::world::WorldBuilder;
|
||||
use sparsey::Entity;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
@@ -69,6 +70,8 @@ pub struct ComponentContext<'a> {
|
||||
pub linking_ctx: Option<&'a RefCell<LinkingContext>>,
|
||||
/// The raw YAML mapping for this component (for extracting FileRefs)
|
||||
pub yaml: &'a Mapping,
|
||||
/// GUID resolver for resolving MonoBehaviour script GUIDs to class names
|
||||
pub guid_resolver: Option<&'a crate::parser::GuidResolver>,
|
||||
}
|
||||
|
||||
/// Trait for Unity components that can be parsed from YAML
|
||||
@@ -118,6 +121,8 @@ pub struct ComponentRegistration {
|
||||
pub class_name: &'static str,
|
||||
/// Parser function that parses and inserts the component into the ECS world
|
||||
pub parse_and_insert: fn(&Mapping, &ComponentContext, &mut sparsey::World, Entity) -> bool,
|
||||
/// Function to register this component type with a WorldBuilder
|
||||
pub register: for<'a> fn(&'a mut WorldBuilder) -> &'a mut WorldBuilder,
|
||||
}
|
||||
|
||||
// Collect all component registrations submitted via the macro
|
||||
255
unity-parser/src/types/guid.rs
Normal file
255
unity-parser/src/types/guid.rs
Normal file
@@ -0,0 +1,255 @@
|
||||
//! Unity GUID type
|
||||
//!
|
||||
//! Unity uses 128-bit GUIDs to uniquely identify assets across a project.
|
||||
//! GUIDs are stored as 32 hexadecimal characters (e.g., "091c537484687e9419460cdcd7038234").
|
||||
//!
|
||||
//! This module provides a type-safe, efficient representation of Unity GUIDs
|
||||
//! using a 128-bit integer for fast comparisons and minimal memory footprint.
|
||||
|
||||
use crate::{Error, Result};
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// A Unity GUID represented as a 128-bit integer
|
||||
///
|
||||
/// Unity GUIDs are 32 hexadecimal characters representing a 128-bit value.
|
||||
/// This type stores the GUID as a `u128` for efficient storage and comparison.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use unity_parser::Guid;
|
||||
/// use std::str::FromStr;
|
||||
///
|
||||
/// // Parse from string
|
||||
/// let guid = Guid::from_str("091c537484687e9419460cdcd7038234").unwrap();
|
||||
///
|
||||
/// // Convert back to string
|
||||
/// assert_eq!(guid.to_string(), "091c537484687e9419460cdcd7038234");
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct Guid(u128);
|
||||
|
||||
impl Guid {
|
||||
/// Create a new GUID from a 128-bit integer
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use unity_parser::Guid;
|
||||
///
|
||||
/// let guid = Guid::from_u128(0x091c537484687e9419460cdcd7038234);
|
||||
/// ```
|
||||
pub const fn from_u128(value: u128) -> Self {
|
||||
Guid(value)
|
||||
}
|
||||
|
||||
/// Get the underlying 128-bit integer value
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use unity_parser::Guid;
|
||||
///
|
||||
/// let guid = Guid::from_u128(42);
|
||||
/// assert_eq!(guid.as_u128(), 42);
|
||||
/// ```
|
||||
pub const fn as_u128(&self) -> u128 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Create a GUID from a hex string
|
||||
///
|
||||
/// The string must be exactly 32 hexadecimal characters.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the string is not valid hex or not 32 characters.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use unity_parser::Guid;
|
||||
///
|
||||
/// let guid = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||
/// ```
|
||||
pub fn from_hex(s: &str) -> Result<Self> {
|
||||
// Unity GUIDs are always 32 hex characters (128 bits)
|
||||
if s.len() != 32 {
|
||||
return Err(Error::invalid_format(format!(
|
||||
"GUID must be 32 hex characters, got {}",
|
||||
s.len()
|
||||
)));
|
||||
}
|
||||
|
||||
// Parse hex string to u128
|
||||
let value = u128::from_str_radix(s, 16).map_err(|e| {
|
||||
Error::invalid_format(format!("Invalid hex in GUID '{}': {}", s, e))
|
||||
})?;
|
||||
|
||||
Ok(Guid(value))
|
||||
}
|
||||
|
||||
/// Convert the GUID to a lowercase hex string
|
||||
///
|
||||
/// Returns a 32-character hex string representing the GUID.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use unity_parser::Guid;
|
||||
/// use std::str::FromStr;
|
||||
///
|
||||
/// let guid = Guid::from_str("091c537484687e9419460cdcd7038234").unwrap();
|
||||
/// assert_eq!(guid.to_hex(), "091c537484687e9419460cdcd7038234");
|
||||
/// ```
|
||||
pub fn to_hex(&self) -> String {
|
||||
format!("{:032x}", self.0)
|
||||
}
|
||||
|
||||
/// A zero GUID (all bits set to 0)
|
||||
pub const ZERO: Guid = Guid(0);
|
||||
}
|
||||
|
||||
impl FromStr for Guid {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
Self::from_hex(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Guid {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:032x}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u128> for Guid {
|
||||
fn from(value: u128) -> Self {
|
||||
Guid(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Guid> for u128 {
|
||||
fn from(guid: Guid) -> Self {
|
||||
guid.0
|
||||
}
|
||||
}
|
||||
|
||||
// Implement serde serialization if serde feature is enabled
|
||||
#[cfg(feature = "serde")]
|
||||
impl serde::Serialize for Guid {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_hex())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de> serde::Deserialize<'de> for Guid {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Guid::from_hex(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_guid_from_hex() {
|
||||
let guid = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||
assert_eq!(guid.to_hex(), "091c537484687e9419460cdcd7038234");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_guid_from_str() {
|
||||
let guid: Guid = "091c537484687e9419460cdcd7038234".parse().unwrap();
|
||||
assert_eq!(guid.to_string(), "091c537484687e9419460cdcd7038234");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_guid_invalid_length() {
|
||||
assert!(Guid::from_hex("123").is_err());
|
||||
assert!(Guid::from_hex("091c537484687e9419460cdcd70382341234").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_guid_invalid_hex() {
|
||||
assert!(Guid::from_hex("091c537484687e9419460cdcd703823g").is_err());
|
||||
assert!(Guid::from_hex("ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_guid_zero() {
|
||||
let zero = Guid::ZERO;
|
||||
assert_eq!(zero.as_u128(), 0);
|
||||
assert_eq!(zero.to_hex(), "00000000000000000000000000000000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_guid_from_u128() {
|
||||
let value: u128 = 0x091c537484687e9419460cdcd7038234;
|
||||
let guid = Guid::from_u128(value);
|
||||
assert_eq!(guid.as_u128(), value);
|
||||
assert_eq!(guid.to_hex(), "091c537484687e9419460cdcd7038234");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_guid_equality() {
|
||||
let guid1 = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||
let guid2 = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||
let guid3 = Guid::from_hex("091c537484687e9419460cdcd7038235").unwrap();
|
||||
|
||||
assert_eq!(guid1, guid2);
|
||||
assert_ne!(guid1, guid3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_guid_hash() {
|
||||
use std::collections::HashSet;
|
||||
|
||||
let mut set = HashSet::new();
|
||||
let guid1 = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||
let guid2 = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||
let guid3 = Guid::from_hex("091c537484687e9419460cdcd7038235").unwrap();
|
||||
|
||||
set.insert(guid1);
|
||||
set.insert(guid2); // Duplicate, shouldn't increase size
|
||||
set.insert(guid3);
|
||||
|
||||
assert_eq!(set.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_guid_ordering() {
|
||||
let guid1 = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||
let guid2 = Guid::from_hex("091c537484687e9419460cdcd7038235").unwrap();
|
||||
|
||||
assert!(guid1 < guid2);
|
||||
assert!(guid2 > guid1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_guid_uppercase_hex() {
|
||||
// Unity GUIDs are lowercase, but we should handle uppercase too
|
||||
let guid = Guid::from_hex("091C537484687E9419460CDCD7038234").unwrap();
|
||||
// Output is always lowercase
|
||||
assert_eq!(guid.to_hex(), "091c537484687e9419460cdcd7038234");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_guid_conversion() {
|
||||
let value: u128 = 12345678901234567890;
|
||||
let guid: Guid = value.into();
|
||||
let back: u128 = guid.into();
|
||||
assert_eq!(value, back);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -5,26 +5,25 @@
|
||||
//! wrappers for GameObjects and Components.
|
||||
|
||||
mod component;
|
||||
mod game_object;
|
||||
mod guid;
|
||||
mod ids;
|
||||
mod prefab_instance;
|
||||
mod reference;
|
||||
mod transform;
|
||||
mod type_filter;
|
||||
mod type_registry;
|
||||
mod unity_types;
|
||||
mod values;
|
||||
|
||||
pub use component::{
|
||||
yaml_helpers, ComponentContext, ComponentRegistration, EcsInsertable, LinkCallback,
|
||||
LinkingContext, UnityComponent,
|
||||
};
|
||||
pub use game_object::GameObject;
|
||||
pub use guid::Guid;
|
||||
pub use ids::{FileID, LocalID};
|
||||
pub use prefab_instance::{
|
||||
PrefabInstance, PrefabInstanceComponent, PrefabModification, PrefabResolver,
|
||||
};
|
||||
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 unity_types::{
|
||||
GameObject, PrefabInstance, PrefabInstanceComponent, PrefabModification, PrefabResolver,
|
||||
RectTransform, Transform,
|
||||
};
|
||||
pub use values::{Color, ExternalRef, FileRef, Quaternion, Vector2, Vector3};
|
||||
@@ -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(),
|
||||
326
unity-parser/src/types/type_filter.rs
Normal file
326
unity-parser/src/types/type_filter.rs
Normal file
@@ -0,0 +1,326 @@
|
||||
//! Type filtering for selective parsing
|
||||
//!
|
||||
//! This module provides functionality to selectively parse only specific Unity
|
||||
//! component types, improving performance and reducing memory usage.
|
||||
|
||||
use crate::types::Guid;
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Filter for controlling which Unity types and MonoBehaviour scripts get parsed
|
||||
///
|
||||
/// This filter operates at two levels:
|
||||
/// 1. **Document-level (YAML parsing)**: Skips parsing YAML for unwanted Unity types and MonoBehaviour GUIDs
|
||||
/// 2. **ECS-level (component insertion)**: Controls which components get inserted into the ECS world
|
||||
///
|
||||
/// 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>>,
|
||||
|
||||
/// Set of MonoBehaviour script GUIDs to parse
|
||||
/// If None, all MonoBehaviour scripts are parsed
|
||||
monobehaviour_guids: Option<HashSet<Guid>>,
|
||||
|
||||
/// 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,
|
||||
monobehaviour_guids: None,
|
||||
parse_all: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new filter with specific Unity and custom types
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use unity_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()),
|
||||
monobehaviour_guids: None,
|
||||
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()),
|
||||
monobehaviour_guids: None,
|
||||
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()),
|
||||
monobehaviour_guids: None,
|
||||
parse_all: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a filter with specific Unity types and MonoBehaviour GUIDs
|
||||
pub fn with_unity_types(types: HashSet<String>) -> Self {
|
||||
Self {
|
||||
unity_types: Some(types),
|
||||
custom_types: None,
|
||||
monobehaviour_guids: None,
|
||||
parse_all: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a filter with specific MonoBehaviour GUIDs
|
||||
pub fn with_monobehaviour_guids(guids: HashSet<Guid>) -> Self {
|
||||
Self {
|
||||
unity_types: None,
|
||||
custom_types: None,
|
||||
monobehaviour_guids: Some(guids),
|
||||
parse_all: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a filter with both Unity types and MonoBehaviour GUIDs
|
||||
pub fn with_both(types: HashSet<String>, guids: HashSet<Guid>) -> Self {
|
||||
Self {
|
||||
unity_types: Some(types),
|
||||
custom_types: None,
|
||||
monobehaviour_guids: Some(guids),
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a Unity class name should be parsed at the document level
|
||||
///
|
||||
/// This is used during YAML parsing to skip unwanted documents.
|
||||
/// Returns true if there's no filter, or if the class is in the whitelist.
|
||||
pub fn should_parse_type(&self, class_name: &str) -> bool {
|
||||
if self.parse_all {
|
||||
return true;
|
||||
}
|
||||
|
||||
match &self.unity_types {
|
||||
None => true, // No filter = parse all
|
||||
Some(whitelist) => whitelist.contains(class_name),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a MonoBehaviour GUID should be parsed
|
||||
///
|
||||
/// This is used during YAML parsing to skip unwanted MonoBehaviour scripts.
|
||||
/// Returns true if there's no filter, or if the GUID is in the whitelist.
|
||||
pub fn should_parse_guid(&self, guid: &Guid) -> bool {
|
||||
if self.parse_all {
|
||||
return true;
|
||||
}
|
||||
|
||||
match &self.monobehaviour_guids {
|
||||
None => true, // No filter = parse all
|
||||
Some(whitelist) => whitelist.contains(guid),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if we're filtering anything at all
|
||||
pub fn is_filtering(&self) -> bool {
|
||||
!self.parse_all
|
||||
&& (self.unity_types.is_some()
|
||||
|| self.custom_types.is_some()
|
||||
|| self.monobehaviour_guids.is_some())
|
||||
}
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_unity_types() {
|
||||
use std::collections::HashSet;
|
||||
|
||||
let mut types = HashSet::new();
|
||||
types.insert("Transform".to_string());
|
||||
types.insert("GameObject".to_string());
|
||||
|
||||
let filter = TypeFilter::with_unity_types(types);
|
||||
|
||||
assert!(filter.should_parse_type("Transform"));
|
||||
assert!(filter.should_parse_type("GameObject"));
|
||||
assert!(!filter.should_parse_type("RectTransform"));
|
||||
assert!(filter.is_filtering());
|
||||
|
||||
// Should still accept any MonoBehaviour GUID since we didn't set a GUID filter
|
||||
let guid = Guid::from_hex("d39ddbf1c2c3d1a4baa070e5e76548bd").unwrap();
|
||||
assert!(filter.should_parse_guid(&guid));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_monobehaviour_guids() {
|
||||
use std::collections::HashSet;
|
||||
|
||||
let mut guids = HashSet::new();
|
||||
let guid1 = Guid::from_hex("d39ddbf1c2c3d1a4baa070e5e76548bd").unwrap();
|
||||
let guid2 = Guid::from_hex("abc123def456789012345678901234ab").unwrap();
|
||||
guids.insert(guid1);
|
||||
guids.insert(guid2);
|
||||
|
||||
let filter = TypeFilter::with_monobehaviour_guids(guids);
|
||||
|
||||
assert!(filter.should_parse_guid(&guid1));
|
||||
assert!(filter.should_parse_guid(&guid2));
|
||||
|
||||
let guid3 = Guid::from_hex("00000000000000000000000000000000").unwrap();
|
||||
assert!(!filter.should_parse_guid(&guid3));
|
||||
|
||||
assert!(filter.is_filtering());
|
||||
|
||||
// Should still accept any Unity type since we didn't set a type filter
|
||||
assert!(filter.should_parse_type("Transform"));
|
||||
assert!(filter.should_parse_type("AnyType"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_both() {
|
||||
use std::collections::HashSet;
|
||||
|
||||
let mut types = HashSet::new();
|
||||
types.insert("Transform".to_string());
|
||||
|
||||
let mut guids = HashSet::new();
|
||||
let guid1 = Guid::from_hex("d39ddbf1c2c3d1a4baa070e5e76548bd").unwrap();
|
||||
guids.insert(guid1);
|
||||
|
||||
let filter = TypeFilter::with_both(types, guids);
|
||||
|
||||
// Only Transform should pass
|
||||
assert!(filter.should_parse_type("Transform"));
|
||||
assert!(!filter.should_parse_type("GameObject"));
|
||||
|
||||
// Only guid1 should pass
|
||||
assert!(filter.should_parse_guid(&guid1));
|
||||
let guid2 = Guid::from_hex("abc123def456789012345678901234ab").unwrap();
|
||||
assert!(!filter.should_parse_guid(&guid2));
|
||||
|
||||
assert!(filter.is_filtering());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_filtering() {
|
||||
let filter_all = TypeFilter::parse_all();
|
||||
assert!(!filter_all.is_filtering());
|
||||
|
||||
let filter_unity = TypeFilter::unity_only(vec!["Transform"]);
|
||||
assert!(filter_unity.is_filtering());
|
||||
|
||||
let mut guids = std::collections::HashSet::new();
|
||||
guids.insert(Guid::from_hex("d39ddbf1c2c3d1a4baa070e5e76548bd").unwrap());
|
||||
let filter_guids = TypeFilter::with_monobehaviour_guids(guids);
|
||||
assert!(filter_guids.is_filtering());
|
||||
}
|
||||
}
|
||||
@@ -262,7 +262,7 @@ pub static UNITY_TYPE_REGISTRY: Lazy<UnityTypeRegistry> = 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));
|
||||
11
unity-parser/src/types/unity_types/mod.rs
Normal file
11
unity-parser/src/types/unity_types/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
//! Unity-specific types (GameObjects, Transforms, PrefabInstances)
|
||||
|
||||
pub mod game_object;
|
||||
pub mod prefab_instance;
|
||||
pub mod transform;
|
||||
|
||||
pub use game_object::GameObject;
|
||||
pub use prefab_instance::{
|
||||
PrefabInstance, PrefabInstanceComponent, PrefabModification, PrefabResolver,
|
||||
};
|
||||
pub use transform::{RectTransform, Transform};
|
||||
@@ -277,25 +277,35 @@ impl PrefabInstance {
|
||||
/// # Arguments
|
||||
/// * `world` - The Sparsey ECS world to spawn entities into
|
||||
/// * `entity_map` - HashMap to track FileID → Entity mappings
|
||||
/// * `guid_resolver` - Optional GUID resolver for MonoBehaviour scripts
|
||||
/// * `prefab_guid_resolver` - Optional prefab GUID resolver for nested prefabs
|
||||
///
|
||||
/// # Returns
|
||||
/// Vec of newly created entities
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let entities = instance.spawn_into(&mut scene.world, &mut scene.entity_map)?;
|
||||
/// let entities = instance.spawn_into(&mut scene.world, &mut scene.entity_map, Some(&guid_resolver), None)?;
|
||||
/// println!("Spawned {} entities", entities.len());
|
||||
/// ```
|
||||
pub fn spawn_into(
|
||||
mut self,
|
||||
world: &mut World,
|
||||
entity_map: &mut HashMap<FileID, Entity>,
|
||||
guid_resolver: Option<&crate::parser::GuidResolver>,
|
||||
prefab_guid_resolver: Option<&crate::parser::PrefabGuidResolver>,
|
||||
) -> Result<Vec<Entity>> {
|
||||
// Apply overrides before spawning
|
||||
self.apply_overrides()?;
|
||||
|
||||
// Spawn into existing world using the builder
|
||||
crate::ecs::build_world_from_documents_into(self.documents, world, entity_map)
|
||||
crate::ecs::build_world_from_documents_into(
|
||||
self.documents,
|
||||
world,
|
||||
entity_map,
|
||||
guid_resolver,
|
||||
prefab_guid_resolver,
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the source prefab path (for debugging)
|
||||
@@ -322,6 +332,18 @@ pub struct PrefabInstanceComponent {
|
||||
pub modifications: Vec<PrefabModification>,
|
||||
}
|
||||
|
||||
impl Default for PrefabInstanceComponent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
prefab_ref: ExternalRef {
|
||||
guid: String::new(),
|
||||
type_id: 0,
|
||||
},
|
||||
modifications: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UnityComponent for PrefabInstanceComponent {
|
||||
fn parse(yaml: &Mapping, _ctx: &ComponentContext) -> Option<Self> {
|
||||
// Extract m_SourcePrefab (external GUID reference)
|
||||
@@ -412,7 +434,7 @@ fn parse_single_modification(yaml: &Mapping) -> Option<PrefabModification> {
|
||||
/// - Caching loaded prefabs
|
||||
/// - Detecting circular prefab references
|
||||
/// - Recursively instantiating nested prefabs
|
||||
pub struct PrefabResolver {
|
||||
pub struct PrefabResolver<'a> {
|
||||
/// Cache of loaded prefabs (GUID → Prefab)
|
||||
prefab_cache: HashMap<String, Arc<UnityPrefab>>,
|
||||
|
||||
@@ -421,9 +443,15 @@ pub struct PrefabResolver {
|
||||
|
||||
/// Stack of GUIDs currently being instantiated (for cycle detection)
|
||||
instantiation_stack: Vec<String>,
|
||||
|
||||
/// GUID resolver for MonoBehaviour scripts
|
||||
guid_resolver: Option<&'a crate::parser::GuidResolver>,
|
||||
|
||||
/// Prefab GUID resolver for nested prefabs
|
||||
prefab_guid_resolver: Option<&'a crate::parser::PrefabGuidResolver>,
|
||||
}
|
||||
|
||||
impl PrefabResolver {
|
||||
impl<'a> PrefabResolver<'a> {
|
||||
/// Create a new PrefabResolver
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -433,9 +461,116 @@ impl PrefabResolver {
|
||||
prefab_cache: HashMap::new(),
|
||||
guid_to_path,
|
||||
instantiation_stack: Vec::new(),
|
||||
guid_resolver: None,
|
||||
prefab_guid_resolver: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a PrefabResolver from resolvers
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `guid_resolver` - Script GUID resolver for MonoBehaviour components
|
||||
/// * `prefab_guid_resolver` - Prefab GUID resolver for prefab file paths
|
||||
pub fn from_resolvers(
|
||||
guid_resolver: Option<&'a crate::parser::GuidResolver>,
|
||||
prefab_guid_resolver: &'a crate::parser::PrefabGuidResolver,
|
||||
) -> Self {
|
||||
// Convert Guid → PathBuf mapping to String → PathBuf mapping
|
||||
let guid_to_path = prefab_guid_resolver
|
||||
.guids()
|
||||
.filter_map(|guid| {
|
||||
let path = prefab_guid_resolver.resolve_path(&guid)?;
|
||||
Some((guid.to_hex(), path.to_path_buf()))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
prefab_cache: HashMap::new(),
|
||||
guid_to_path,
|
||||
instantiation_stack: Vec::new(),
|
||||
guid_resolver,
|
||||
prefab_guid_resolver: Some(prefab_guid_resolver),
|
||||
}
|
||||
}
|
||||
|
||||
/// Instantiate a prefab from a PrefabInstanceComponent
|
||||
///
|
||||
/// This is the main entry point for automatic prefab instantiation during
|
||||
/// scene parsing. It:
|
||||
/// 1. Loads the prefab by GUID
|
||||
/// 2. Creates a PrefabInstance
|
||||
/// 3. Applies modifications from the component
|
||||
/// 4. Recursively instantiates nested prefabs
|
||||
/// 5. Links the prefab root to the parent entity
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `component` - The PrefabInstanceComponent from the scene
|
||||
/// * `parent_entity` - The GameObject entity that contains this PrefabInstance component
|
||||
/// * `world` - The ECS world to spawn entities into
|
||||
/// * `entity_map` - Entity mapping to update
|
||||
///
|
||||
/// # Returns
|
||||
/// Vec of spawned entities (including the root and all children)
|
||||
pub fn instantiate_from_component(
|
||||
&mut self,
|
||||
component: &PrefabInstanceComponent,
|
||||
parent_entity: Option<Entity>,
|
||||
world: &mut World,
|
||||
entity_map: &mut HashMap<FileID, Entity>,
|
||||
) -> Result<Vec<Entity>> {
|
||||
// 1. Extract GUID from component.prefab_ref
|
||||
let guid = &component.prefab_ref.guid;
|
||||
|
||||
// 2. Load prefab via load_prefab()
|
||||
let prefab = self.load_prefab(guid)?;
|
||||
|
||||
// 3. Create PrefabInstance
|
||||
let mut instance = prefab.instantiate();
|
||||
|
||||
// 4. Apply component.modifications using override_value()
|
||||
for modification in &component.modifications {
|
||||
instance.override_value(
|
||||
modification.target_file_id,
|
||||
&modification.property_path,
|
||||
modification.value.clone(),
|
||||
)?;
|
||||
}
|
||||
|
||||
// 5. Spawn the instance
|
||||
// Note: We pass None for prefab_guid_resolver to prevent infinite recursion
|
||||
// Nested prefabs should be handled explicitly if needed
|
||||
let spawned = instance.spawn_into(world, entity_map, self.guid_resolver, None)?;
|
||||
|
||||
// 6. Link spawned root to parent_entity (if provided)
|
||||
if let Some(parent) = parent_entity {
|
||||
if let Some(&root_entity) = spawned.first() {
|
||||
// Find the Transform component on the root and link to parent
|
||||
// Note: This is a simplified version. Full implementation would:
|
||||
// - Find the actual root GameObject Transform
|
||||
// - Set m_Father to parent entity
|
||||
// - Update parent's m_Children array
|
||||
//
|
||||
// For now, we'll let the deferred linking system handle this
|
||||
// via the normal Transform hierarchy resolution in Pass 3
|
||||
|
||||
// Try to get Transform components for both parent and child
|
||||
let mut transforms = world.borrow_mut::<crate::types::Transform>();
|
||||
if let Some(_child_transform) = transforms.get_mut(root_entity) {
|
||||
// Store the parent FileID for deferred linking
|
||||
// This will be picked up by Pass 3's hierarchy resolution
|
||||
if let Some(_parent_transform) = transforms.get(parent) {
|
||||
// The parent linking will be handled by the deferred linking system
|
||||
// We just need to ensure the entities are in the world
|
||||
drop(transforms); // Release the borrow
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Return spawned entities
|
||||
Ok(spawned)
|
||||
}
|
||||
|
||||
/// Recursively instantiate a prefab and its nested prefabs
|
||||
///
|
||||
/// This handles:
|
||||
@@ -498,7 +633,8 @@ impl PrefabResolver {
|
||||
}
|
||||
|
||||
// Spawn this prefab's entities
|
||||
let spawned = instance.spawn_into(world, entity_map)?;
|
||||
// Note: Pass None for prefab_guid_resolver since we handle nesting manually
|
||||
let spawned = instance.spawn_into(world, entity_map, self.guid_resolver, None)?;
|
||||
|
||||
// Pop from stack
|
||||
self.instantiation_stack.pop();
|
||||
@@ -524,6 +660,7 @@ impl PrefabResolver {
|
||||
entity: None,
|
||||
linking_ctx: None,
|
||||
yaml: mapping,
|
||||
guid_resolver: None,
|
||||
};
|
||||
|
||||
if let Some(component) = PrefabInstanceComponent::parse(mapping, &ctx) {
|
||||
@@ -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 {
|
||||
@@ -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");
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Integration tests for parsing real Unity projects
|
||||
|
||||
use cursebreaker_parser::UnityFile;
|
||||
use unity_parser::{GuidResolver, UnityFile};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::time::Instant;
|
||||
@@ -400,3 +400,155 @@ fn benchmark_parsing() {
|
||||
|
||||
stats.print_summary();
|
||||
}
|
||||
|
||||
/// Test GUID resolution for MonoBehaviour scripts
|
||||
#[test]
|
||||
fn test_guid_resolution() {
|
||||
let project_path = match clone_test_project(&TestProject::VR_HORROR) {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to clone project: {}", e);
|
||||
eprintln!("Skipping GUID resolution test (git may not be available)");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
println!("\n{}", "=".repeat(60));
|
||||
println!("Testing GUID Resolution");
|
||||
println!("{}", "=".repeat(60));
|
||||
|
||||
// Build GUID resolver from project
|
||||
println!("\nBuilding GuidResolver from project...");
|
||||
let resolver = GuidResolver::from_project(&project_path)
|
||||
.expect("Should successfully build GuidResolver");
|
||||
|
||||
println!(" ✓ Found {} GUID mappings", resolver.len());
|
||||
assert!(
|
||||
!resolver.is_empty(),
|
||||
"Should find at least some C# scripts with GUIDs"
|
||||
);
|
||||
|
||||
// Test the known PlaySFX GUID from the roadmap
|
||||
let playsfx_guid_str = "091c537484687e9419460cdcd7038234";
|
||||
println!("\nTesting known GUID resolution:");
|
||||
println!(" GUID: {}", playsfx_guid_str);
|
||||
|
||||
// Test resolution by string
|
||||
match resolver.resolve_class_name(playsfx_guid_str) {
|
||||
Some(class_name) => {
|
||||
println!(" ✓ Resolved by string to: {}", class_name);
|
||||
assert_eq!(
|
||||
class_name, "PlaySFX",
|
||||
"PlaySFX GUID should resolve to 'PlaySFX' class name"
|
||||
);
|
||||
}
|
||||
None => {
|
||||
panic!("Failed to resolve PlaySFX GUID. Available GUIDs: {:?}",
|
||||
resolver.guids().take(5).collect::<Vec<_>>());
|
||||
}
|
||||
}
|
||||
|
||||
// Test resolution by Guid type
|
||||
use unity_parser::Guid;
|
||||
let playsfx_guid = Guid::from_hex(playsfx_guid_str).unwrap();
|
||||
match resolver.resolve_class_name(&playsfx_guid) {
|
||||
Some(class_name) => {
|
||||
println!(" ✓ Resolved by Guid type to: {}", class_name);
|
||||
assert_eq!(class_name, "PlaySFX");
|
||||
}
|
||||
None => panic!("Failed to resolve PlaySFX GUID by Guid type"),
|
||||
}
|
||||
|
||||
// Show sample of resolved GUIDs
|
||||
println!("\nSample of resolved GUIDs:");
|
||||
for guid in resolver.guids().take(5) {
|
||||
if let Some(class_name) = resolver.resolve_class_name(&guid) {
|
||||
println!(" {} → {}", guid, class_name);
|
||||
}
|
||||
}
|
||||
|
||||
// Demonstrate memory efficiency
|
||||
println!("\nMemory efficiency:");
|
||||
println!(" Each Guid: {} bytes (u128)", std::mem::size_of::<Guid>());
|
||||
println!(" Each String GUID: ~{} bytes (heap allocated)", 32 + std::mem::size_of::<String>());
|
||||
|
||||
println!("\n{}", "=".repeat(60));
|
||||
}
|
||||
|
||||
/// Test parsing PlaySFX components from actual scene file
|
||||
#[test]
|
||||
fn test_playsfx_parsing() {
|
||||
use unity_parser::UnityComponent;
|
||||
|
||||
/// PlaySFX component from VR_Horror_YouCantRun
|
||||
#[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,
|
||||
}
|
||||
|
||||
let project_path = match clone_test_project(&TestProject::VR_HORROR) {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to clone project: {}", e);
|
||||
eprintln!("Skipping PlaySFX parsing test (git may not be available)");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
println!("\n{}", "=".repeat(60));
|
||||
println!("Testing PlaySFX Component Parsing");
|
||||
println!("{}", "=".repeat(60));
|
||||
|
||||
// Parse the 1F.unity scene that contains PlaySFX components
|
||||
let scene_path = project_path.join("Assets/Scenes/TEST/Final_1F/1F.unity");
|
||||
|
||||
if !scene_path.exists() {
|
||||
eprintln!("Scene file not found: {}", scene_path.display());
|
||||
return;
|
||||
}
|
||||
|
||||
println!("\n Parsing scene: {}", scene_path.display());
|
||||
|
||||
match unity_parser::UnityFile::from_path(&scene_path) {
|
||||
Ok(unity_parser::UnityFile::Scene(scene)) => {
|
||||
println!(" ✓ Scene parsed successfully");
|
||||
println!(" - Total entities: {}", scene.entity_map.len());
|
||||
|
||||
// Try to get PlaySFX components
|
||||
let playsfx_view = scene.world.borrow::<PlaySFX>();
|
||||
let mut found_count = 0;
|
||||
|
||||
for entity in scene.entity_map.values() {
|
||||
if playsfx_view.get(*entity).is_some() {
|
||||
found_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
println!(" ✓ Found {} PlaySFX component(s)", found_count);
|
||||
|
||||
assert!(
|
||||
found_count > 0,
|
||||
"Should find at least one PlaySFX component in 1F.unity"
|
||||
);
|
||||
}
|
||||
Ok(_) => {
|
||||
panic!("File was not parsed as a scene");
|
||||
}
|
||||
Err(e) => {
|
||||
panic!("Failed to parse scene: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n{}", "=".repeat(60));
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Tests for the #[derive(UnityComponent)] macro
|
||||
|
||||
use cursebreaker_parser::{ComponentContext, FileID, UnityComponent};
|
||||
use unity_parser::{ComponentContext, FileID, UnityComponent};
|
||||
|
||||
/// Test component matching the PlaySFX script from VR_Horror_YouCantRun
|
||||
#[derive(Debug, Clone, UnityComponent)]
|
||||
@@ -133,7 +133,7 @@ fn test_component_registration() {
|
||||
let mut found_play_sfx = false;
|
||||
let mut found_test_component = false;
|
||||
|
||||
for reg in inventory::iter::<cursebreaker_parser::ComponentRegistration> {
|
||||
for reg in inventory::iter::<unity_parser::ComponentRegistration> {
|
||||
if reg.class_name == "PlaySFX" {
|
||||
found_play_sfx = true;
|
||||
assert_eq!(reg.type_id, 114);
|
||||
@@ -177,7 +177,7 @@ isLoop: 0
|
||||
};
|
||||
|
||||
// Find the PlaySFX registration and call its parser
|
||||
for reg in inventory::iter::<cursebreaker_parser::ComponentRegistration> {
|
||||
for reg in inventory::iter::<unity_parser::ComponentRegistration> {
|
||||
if reg.class_name == "PlaySFX" {
|
||||
let result = (reg.parser)(mapping, &ctx);
|
||||
assert!(result.is_some(), "Registered parser failed to parse");
|
||||
Reference in New Issue
Block a user