cleanup
This commit is contained in:
@@ -5,6 +5,6 @@ resolver = "2"
|
|||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Your Name <your.email@example.com>"]
|
authors = ["connordemeyer@gmail.com"]
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = "https://github.com/yourusername/unity-parser-rust"
|
repository = "https://github.com/yourusername/unity-parser-rust"
|
||||||
|
|||||||
446
README.md
446
README.md
@@ -1,30 +1,33 @@
|
|||||||
# Unity Parser
|
# Cursebreaker Unity Parser
|
||||||
|
|
||||||
A high-performance Rust library for parsing Unity project files (.unity scenes, .prefab prefabs, and .asset ScriptableObjects) with automatic MonoBehaviour component discovery and ECS integration.
|
A high-performance Rust library for parsing Unity project files (.unity scenes, .prefab prefabs, and .asset files) with automatic MonoBehaviour component discovery and ECS integration.
|
||||||
|
|
||||||
[](https://www.rust-lang.org/)
|
[](https://www.rust-lang.org/)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
|
||||||
|
**⚠️ Work in Progress**: This library is under active development. APIs may change.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### 🚀 Core Parsing
|
### Core Parsing
|
||||||
- **Multi-format support**: Parse `.unity` scenes, `.prefab` prefabs, and `.asset` files
|
- **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
|
- **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.)
|
- **Type-safe**: Strong typing for Unity primitives (Vector3, Quaternion, Color, etc.)
|
||||||
- **Fast**: Efficient parsing with minimal allocations
|
- **Fast**: Efficient YAML parsing with minimal allocations
|
||||||
|
|
||||||
### 🎯 Custom Component System
|
### Component System
|
||||||
- **Derive macro**: `#[derive(UnityComponent)]` for automatic component parsing
|
- **Derive macro**: `#[derive(UnityComponent)]` for automatic component parsing
|
||||||
- **Auto-registration**: Components register themselves via [inventory](https://github.com/dtolnay/inventory)
|
- **Auto-registration**: Components register themselves via [inventory](https://github.com/dtolnay/inventory)
|
||||||
- **GUID resolution**: Automatically resolves MonoBehaviour GUIDs to class names
|
- **GUID resolution**: Automatically resolves MonoBehaviour script GUIDs to class names
|
||||||
|
- **Prefab resolution**: Resolves prefab GUIDs for nested prefab references
|
||||||
- **Type filtering**: Selectively parse only the components you need
|
- **Type filtering**: Selectively parse only the components you need
|
||||||
|
|
||||||
### 🔍 Advanced Features
|
### Advanced Features
|
||||||
- **Prefab instantiation**: Clone and modify prefab instances
|
- **Prefab instantiation**: Clone and modify prefab instances (basic support)
|
||||||
- **Reference resolution**: Automatic FileID → Entity mapping
|
- **Reference resolution**: FileID → Entity mapping
|
||||||
- **Regex filtering**: Parse only scenes matching specific patterns
|
- **Regex filtering**: Parse only files matching specific patterns
|
||||||
- **Transform hierarchies**: Parent-child relationships preserved
|
- **Transform hierarchies**: Parent-child relationships preserved
|
||||||
- **Memory efficient**: 128-bit GUIDs with 3.5x memory reduction vs strings
|
- **Memory efficient**: 128-bit GUIDs stored as u128 (16 bytes vs ~56 bytes for String)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -32,7 +35,7 @@ Add to your `Cargo.toml`:
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
unity-parser = "0.1"
|
unity_parser = { path = "unity-parser" }
|
||||||
sparsey = "0.13" # For ECS queries
|
sparsey = "0.13" # For ECS queries
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -70,66 +73,111 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Define Custom Components
|
### Parse a Prefab
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use unity_parser::UnityComponent;
|
use unity_parser::UnityFile;
|
||||||
|
|
||||||
#[derive(Debug, Clone, UnityComponent)]
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
#[unity_class("PlaySFX")]
|
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 struct PlaySFX {
|
||||||
#[unity_field("volume")]
|
|
||||||
pub volume: f64,
|
pub volume: f64,
|
||||||
|
|
||||||
#[unity_field("startTime")]
|
|
||||||
pub start_time: f64,
|
pub start_time: f64,
|
||||||
|
|
||||||
#[unity_field("endTime")]
|
|
||||||
pub end_time: f64,
|
pub end_time: f64,
|
||||||
|
|
||||||
#[unity_field("isLoop")]
|
|
||||||
pub is_loop: bool,
|
pub is_loop: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now automatically parsed from Unity scenes!
|
// Manual implementation (recommended until macro is fixed)
|
||||||
fn find_audio_components(scene: &UnityScene) {
|
impl UnityComponent for PlaySFX {
|
||||||
let playsfx_view = scene.world.borrow::<PlaySFX>();
|
fn parse(yaml: &Mapping, _ctx: &ComponentContext) -> Option<Self> {
|
||||||
|
Some(Self {
|
||||||
for entity in scene.entity_map.values() {
|
volume: unity_parser::yaml_helpers::get_f64(yaml, "volume").unwrap_or(1.0),
|
||||||
if let Some(sfx) = playsfx_view.get(*entity) {
|
start_time: unity_parser::yaml_helpers::get_f64(yaml, "startTime").unwrap_or(0.0),
|
||||||
println!("Found audio: volume={}, loop={}", sfx.volume, sfx.is_loop);
|
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
|
## GUID Resolution
|
||||||
|
|
||||||
The parser automatically resolves Unity MonoBehaviour GUIDs to class names, enabling seamless custom component discovery:
|
The parser automatically resolves Unity MonoBehaviour GUIDs to class names:
|
||||||
|
|
||||||
```
|
```
|
||||||
Unity Scene File Rust Code
|
Unity Scene File Rust Code
|
||||||
───────────────── ─────────
|
───────────────── ─────────
|
||||||
MonoBehaviour: #[derive(UnityComponent)]
|
MonoBehaviour: Custom component
|
||||||
m_Script: #[unity_class("PlaySFX")]
|
m_Script: in ECS World
|
||||||
guid: 091c537... ──────────> pub struct PlaySFX { ... }
|
guid: 091c537... ──────────>
|
||||||
volume: 1.0
|
volume: 1.0
|
||||||
isLoop: 0
|
isLoop: 0
|
||||||
```
|
```
|
||||||
|
|
||||||
### How It Works
|
### How It Works
|
||||||
|
|
||||||
1. **Scan**: Parser scans `Assets/` for `*.cs.meta` files
|
1. **Scan**: Parser scans Unity project's `Assets/` for `*.cs.meta` files
|
||||||
2. **Build Map**: Extracts GUIDs from `.meta` files
|
2. **Build Map**: Extracts GUIDs from `.meta` files
|
||||||
3. **Extract Class**: Parses `.cs` files to get `class Name : MonoBehaviour`
|
3. **Extract Class**: Parses `.cs` files to get `class Name : MonoBehaviour`
|
||||||
4. **Resolve**: Maps GUID → Class Name → Registered Component
|
4. **Resolve**: Maps GUID → Class Name → Registered Component
|
||||||
5. **Parse**: Automatically parses MonoBehaviour YAML into your Rust struct
|
5. **Parse**: Automatically parses MonoBehaviour YAML into components
|
||||||
|
|
||||||
**Performance**: GUID resolver caches mappings per project for fast lookups.
|
The GUID resolver builds automatically when parsing scenes if a Unity project root is detected.
|
||||||
|
|
||||||
## Regex Filtering
|
## Type Filtering
|
||||||
|
|
||||||
Parse only scenes matching specific patterns:
|
Parse only specific Unity types and MonoBehaviours for better performance:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use unity_parser::{TypeFilter, parse_unity_file_filtered};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
// Parse only Transforms and GameObjects
|
||||||
|
let mut types = HashSet::new();
|
||||||
|
types.insert("Transform".to_string());
|
||||||
|
types.insert("GameObject".to_string());
|
||||||
|
let filter = TypeFilter::with_unity_types(types);
|
||||||
|
|
||||||
|
let file = parse_unity_file_filtered(
|
||||||
|
Path::new("Scene.unity"),
|
||||||
|
None, // No regex filter
|
||||||
|
Some(&filter)
|
||||||
|
)?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Regex Path Filtering
|
||||||
|
|
||||||
|
Parse only files matching specific patterns:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
@@ -137,38 +185,31 @@ use unity_parser::parse_unity_file_filtered;
|
|||||||
|
|
||||||
// Only parse production scenes
|
// Only parse production scenes
|
||||||
let filter = Regex::new(r"Assets/Scenes/Production/")?;
|
let filter = Regex::new(r"Assets/Scenes/Production/")?;
|
||||||
let scene = parse_unity_file_filtered("Assets/Scenes/Production/Level1.unity", Some(&filter))?;
|
let scene = parse_unity_file_filtered(
|
||||||
|
Path::new("Assets/Scenes/Production/Level1.unity"),
|
||||||
// Parse everything (default)
|
Some(&filter),
|
||||||
let scene = parse_unity_file_filtered("Scene.unity", None)?;
|
None
|
||||||
|
)?;
|
||||||
```
|
```
|
||||||
|
|
||||||
Common patterns:
|
## Supported Unity Types
|
||||||
|
|
||||||
```rust
|
### Built-in Components
|
||||||
// Test scenes only
|
- ✅ GameObject
|
||||||
let filter = Regex::new(r"(?i)test")?;
|
- ✅ Transform
|
||||||
|
- ✅ RectTransform
|
||||||
|
- ✅ PrefabInstance
|
||||||
|
|
||||||
// Exclude debug/temp scenes
|
### Value Types
|
||||||
let filter = Regex::new(r"^(?!.*(debug|tmp|test))")?;
|
- ✅ Vector2, Vector3
|
||||||
|
- ✅ Quaternion
|
||||||
|
- ✅ Color
|
||||||
|
- ✅ FileID, GUID
|
||||||
|
- ✅ ExternalRef, FileRef
|
||||||
|
|
||||||
// Specific level range
|
### Custom Components
|
||||||
let filter = Regex::new(r"Level[1-5]\.unity$")?;
|
- ✅ Any `MonoBehaviour` via manual `UnityComponent` implementation
|
||||||
```
|
- ⚠️ `#[derive(UnityComponent)]` macro (has namespace bugs, not recommended)
|
||||||
|
|
||||||
## Type Filtering
|
|
||||||
|
|
||||||
Selectively parse components for better performance:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use unity_parser::{TypeFilter, parse_with_types};
|
|
||||||
|
|
||||||
// Parse only transforms and custom components
|
|
||||||
let filter = TypeFilter::parse_with_types(&["Transform", "PlaySFX"]);
|
|
||||||
|
|
||||||
// Or use the macro for convenience
|
|
||||||
let filter = parse_with_types!(Transform, PlaySFX);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -181,234 +222,169 @@ Raw YAML Documents
|
|||||||
↓
|
↓
|
||||||
┌─────────────────────┐
|
┌─────────────────────┐
|
||||||
│ GUID Resolution │ ← .meta files + .cs files
|
│ GUID Resolution │ ← .meta files + .cs files
|
||||||
│ (MonoBehaviour) │
|
│ (MonoBehaviour) │ (Script GUID → Class Name)
|
||||||
└─────────────────────┘
|
└─────────────────────┘
|
||||||
↓
|
↓
|
||||||
┌─────────────────────┐
|
┌─────────────────────┐
|
||||||
│ Component Registry │ ← #[derive(UnityComponent)]
|
│ Prefab GUID Res. │ ← .prefab.meta files
|
||||||
|
│ (Nested Prefabs) │ (Prefab GUID → Prefab Path)
|
||||||
|
└─────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ Component Registry │ ← UnityComponent trait impls
|
||||||
│ (inventory) │
|
│ (inventory) │
|
||||||
└─────────────────────┘
|
└─────────────────────┘
|
||||||
↓
|
↓
|
||||||
┌─────────────────────┐
|
┌─────────────────────┐
|
||||||
│ ECS World │ ← Sparsey entities
|
│ ECS World │ ← Sparsey entities & components
|
||||||
│ (Transforms, etc) │
|
│ (Transform, etc) │
|
||||||
└─────────────────────┘
|
└─────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### Memory Efficiency
|
### Memory Efficiency
|
||||||
|
|
||||||
**GUID Storage**:
|
**GUID Storage**:
|
||||||
- Old: `String` (56 bytes heap allocated)
|
- Old approach: `String` (24 bytes stack + 32 bytes heap = 56 bytes)
|
||||||
- New: `Guid` (16 bytes on stack)
|
- Current: `u128` (16 bytes on stack)
|
||||||
- **3.5x memory reduction** for GUID maps
|
- **3.5x memory reduction** for GUID storage
|
||||||
|
|
||||||
**GUID Comparison**:
|
**GUID Comparison**:
|
||||||
- Old: O(n) string comparison (32 characters)
|
- Old: O(n) string comparison (32 characters)
|
||||||
- New: O(1) integer comparison
|
- New: O(1) integer comparison
|
||||||
- **⚡ Significant speedup** for lookups
|
- **Significant speedup** for HashMap lookups
|
||||||
|
|
||||||
## Supported Unity Types
|
## Project Structure
|
||||||
|
|
||||||
### Built-in Components
|
```
|
||||||
- ✅ GameObject
|
cursebreaker-parser-rust/
|
||||||
- ✅ Transform
|
├── unity-parser/ # Main library crate
|
||||||
- ✅ RectTransform
|
│ ├── src/
|
||||||
- ✅ PrefabInstance
|
│ │ ├── ecs/ # ECS world building
|
||||||
|
│ │ │ └── builder.rs
|
||||||
### Value Types
|
│ │ ├── model/ # UnityFile, Scene, Prefab, Asset
|
||||||
- ✅ Vector2, Vector3, Vector4
|
│ │ │ └── mod.rs
|
||||||
- ✅ Quaternion
|
│ │ ├── parser/ # YAML parsing & GUID resolution
|
||||||
- ✅ Color, Color32
|
│ │ │ ├── guid_resolver.rs # Script GUID → Class Name
|
||||||
- ✅ FileID, GUID
|
│ │ │ ├── prefab_guid_resolver.rs # Prefab GUID → Path
|
||||||
- ✅ ExternalRef, FileRef
|
│ │ │ ├── meta.rs # .meta file parsing
|
||||||
|
│ │ │ ├── yaml.rs # YAML document splitting
|
||||||
### Custom Components
|
│ │ │ └── mod.rs
|
||||||
- ✅ Any `MonoBehaviour` via `#[derive(UnityComponent)]`
|
│ │ ├── project/ # ⚠️ OUTDATED - needs refactoring
|
||||||
- ✅ Automatic field mapping with `#[unity_field("fieldName")]`
|
│ │ │ └── mod.rs
|
||||||
- ✅ Support for all Unity primitive types
|
│ │ ├── types/ # Unity types & components
|
||||||
|
│ │ │ ├── unity_types/
|
||||||
## Examples
|
│ │ │ │ ├── game_object.rs
|
||||||
|
│ │ │ │ ├── transform.rs
|
||||||
### Find All Components of a Type
|
│ │ │ │ ├── prefab_instance.rs
|
||||||
|
│ │ │ │ └── mod.rs
|
||||||
```rust
|
│ │ │ ├── component.rs # UnityComponent trait & helpers
|
||||||
use unity_parser::UnityFile;
|
│ │ │ ├── guid.rs # 128-bit GUID type
|
||||||
|
│ │ │ ├── ids.rs # FileID, LocalID
|
||||||
let scene = UnityFile::from_path("Scene.unity")?;
|
│ │ │ ├── reference.rs # UnityReference enum
|
||||||
|
│ │ │ ├── type_filter.rs # TypeFilter for selective parsing
|
||||||
if let UnityFile::Scene(scene) = scene {
|
│ │ │ ├── values.rs # Vector3, Quaternion, Color, etc.
|
||||||
let transforms = scene.world.borrow::<Transform>();
|
│ │ │ └── mod.rs
|
||||||
let gameobjects = scene.world.borrow::<GameObject>();
|
│ │ ├── error.rs # Error types
|
||||||
|
│ │ ├── macros.rs
|
||||||
for (file_id, entity) in &scene.entity_map {
|
│ │ ├── property/
|
||||||
let name = gameobjects.get(*entity)
|
│ │ └── lib.rs
|
||||||
.and_then(|go| go.name())
|
│ ├── examples/
|
||||||
.unwrap_or("(unnamed)");
|
│ │ ├── basic_parsing.rs
|
||||||
|
│ │ ├── custom_component.rs
|
||||||
if let Some(transform) = transforms.get(*entity) {
|
│ │ ├── ecs_integration.rs
|
||||||
println!("{}: {:?}", name, transform.local_position());
|
│ │ ├── find_playsfx.rs
|
||||||
}
|
│ │ ├── parse_resources.rs
|
||||||
}
|
│ │ └── parse_resource_prefabs.rs
|
||||||
}
|
│ ├── tests/
|
||||||
|
│ └── Cargo.toml
|
||||||
|
├── unity-parser-macros/ # Proc macro crate (⚠️ has bugs)
|
||||||
|
│ ├── src/
|
||||||
|
│ │ └── lib.rs
|
||||||
|
│ └── Cargo.toml
|
||||||
|
├── Cargo.toml # Workspace config
|
||||||
|
└── README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
### Prefab Instantiation
|
## Known Issues
|
||||||
|
|
||||||
```rust
|
### Critical Issues
|
||||||
let prefab_file = UnityFile::from_path("Player.prefab")?;
|
1. **`unity-parser/src/project/mod.rs` is OUTDATED**
|
||||||
|
- Built for old architecture before `UnityFile` enum refactor
|
||||||
|
- References non-existent `UnityDocument` type (should be `RawDocument`)
|
||||||
|
- Module is disabled in lib.rs until refactored
|
||||||
|
|
||||||
if let UnityFile::Prefab(prefab) = prefab_file {
|
2. **Derive macro namespace mismatch**
|
||||||
// Create instance
|
- `unity-parser-macros` uses `unity_parser` namespace
|
||||||
let mut instance = prefab.instantiate();
|
- Actual crate name is `unity_parser::` (underscore, not hyphen)
|
||||||
|
- Manual `UnityComponent` implementation recommended
|
||||||
|
|
||||||
// Override values
|
3. **Placeholder values in Cargo.toml**
|
||||||
instance.override_value(
|
- Author and repository fields need updating
|
||||||
file_id,
|
|
||||||
"m_Name",
|
|
||||||
serde_yaml::Value::String("Player_Clone".to_string())
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Access remapped FileIDs
|
### Minor Issues
|
||||||
let new_file_ids = instance.file_id_map();
|
1. Disabled example/test files may reference outdated APIs
|
||||||
}
|
2. Some examples may have incorrect YAML access patterns
|
||||||
```
|
|
||||||
|
|
||||||
### Batch Processing
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use walkdir::WalkDir;
|
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
let filter = Regex::new(r"Level\d+\.unity")?;
|
|
||||||
|
|
||||||
for entry in WalkDir::new("Assets/Scenes") {
|
|
||||||
let path = entry?.path();
|
|
||||||
|
|
||||||
if path.extension() == Some("unity") {
|
|
||||||
match parse_unity_file_filtered(path, Some(&filter)) {
|
|
||||||
Ok(UnityFile::Scene(scene)) => {
|
|
||||||
println!("Processed: {} ({} entities)",
|
|
||||||
path.display(),
|
|
||||||
scene.entity_map.len()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(e) if e.to_string().contains("does not match filter") => {
|
|
||||||
// Filtered out
|
|
||||||
}
|
|
||||||
Err(e) => eprintln!("Error: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running Examples
|
## Running Examples
|
||||||
|
|
||||||
The repository includes several examples:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Parse and display basic scene info
|
# Parse basic Unity file
|
||||||
cargo run --example basic_parsing
|
cargo run --example basic_parsing
|
||||||
|
|
||||||
# Demonstrate custom component parsing
|
# Custom component parsing (requires Unity project)
|
||||||
cargo run --example custom_component
|
cargo run --example custom_component
|
||||||
|
|
||||||
# ECS integration showcase
|
# ECS integration showcase
|
||||||
cargo run --example ecs_integration
|
cargo run --example ecs_integration
|
||||||
|
|
||||||
# Find all PlaySFX components in VR Horror project
|
# Find PlaySFX components (requires CBAssets project)
|
||||||
cargo run --example find_playsfx
|
cargo run --example find_playsfx
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Run the test suite:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Unit tests
|
# Unit tests
|
||||||
cargo test --lib
|
cargo test --lib
|
||||||
|
|
||||||
# Integration tests (requires git for downloading test projects)
|
# Integration tests
|
||||||
cargo test --test integration_tests
|
cargo test --test integration_tests
|
||||||
|
|
||||||
# GUID resolution tests
|
|
||||||
cargo test test_guid_resolution -- --nocapture
|
|
||||||
|
|
||||||
# All tests
|
# All tests
|
||||||
cargo test
|
cargo test
|
||||||
```
|
```
|
||||||
|
|
||||||
## Performance
|
|
||||||
|
|
||||||
Benchmarks on VR Horror project (21 scenes, 77 C# scripts):
|
|
||||||
|
|
||||||
| Operation | Time | Throughput |
|
|
||||||
|-----------|------|------------|
|
|
||||||
| GUID Resolver Build | ~800ms | 77 scripts |
|
|
||||||
| Scene Parse | ~100-500ms | per scene |
|
|
||||||
| GUID Lookup | <1μs | O(1) HashMap |
|
|
||||||
|
|
||||||
**Memory**: ~16 bytes per GUID (vs ~56 bytes for String-based approach)
|
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
### Phase 1: GUID Resolution ✅ COMPLETE
|
### Completed
|
||||||
- [x] Scan `.cs.meta` files
|
- ✅ Phase 1: GUID Resolution (Script GUID → Class Name)
|
||||||
- [x] Extract class names from C# scripts
|
- ✅ Phase 2: MonoBehaviour Parser
|
||||||
- [x] Build GUID → Class Name mapping
|
- ✅ Phase 3: Prefab GUID Resolution
|
||||||
- [x] 128-bit `Guid` type with 3.5x memory reduction
|
- ✅ Type filtering for selective parsing
|
||||||
|
- ✅ Regex path filtering
|
||||||
|
- ✅ Basic prefab instantiation
|
||||||
|
|
||||||
### Phase 2: MonoBehaviour Parser ✅ COMPLETE
|
### In Progress / Needs Work
|
||||||
- [x] Extract `m_Script` GUID from components
|
- 🔧 Refactor `project` module for new architecture
|
||||||
- [x] Resolve GUID to class name
|
- 🔧 Update disabled example/test files
|
||||||
- [x] Match with registered components
|
- 🔧 Fix example code YAML access patterns
|
||||||
- [x] Automatic parsing via `#[derive(UnityComponent)]`
|
|
||||||
|
|
||||||
### Phase 3: Advanced Features ✅ COMPLETE
|
|
||||||
- [x] Regex filtering for selective parsing
|
|
||||||
- [x] Type filtering for performance
|
|
||||||
- [x] Prefab instantiation and overrides
|
|
||||||
|
|
||||||
### Future Enhancements
|
### Future Enhancements
|
||||||
- [x] Prefab GUID resolution (nested prefabs)
|
- [ ] Full prefab modification system
|
||||||
- [ ] Full AssetDatabase resolution (materials, textures)
|
|
||||||
- [ ] Persistent GUID cache for instant loading
|
- [ ] Persistent GUID cache for instant loading
|
||||||
- [ ] Watch mode for live Unity project monitoring
|
- [ ] Watch mode for live Unity project monitoring
|
||||||
- [ ] Cross-platform path handling
|
- [ ] More built-in Unity component types
|
||||||
|
- [ ] Better error messages with line numbers
|
||||||
## Project Structure
|
- [ ] Parallel processing support
|
||||||
|
- [ ] Cross-platform path handling improvements
|
||||||
```
|
|
||||||
unity-parser/
|
|
||||||
├── src/
|
|
||||||
│ ├── ecs/ # ECS world building
|
|
||||||
│ ├── model/ # UnityFile, Scene, Prefab models
|
|
||||||
│ ├── parser/ # YAML parsing, GUID resolution
|
|
||||||
│ │ ├── guid_resolver.rs # GUID → Class Name mapping
|
|
||||||
│ │ ├── meta.rs # .meta file parsing
|
|
||||||
│ │ └── yaml.rs # YAML document splitting
|
|
||||||
│ ├── types/ # Unity types and components
|
|
||||||
│ │ ├── unity_types/ # Unity-specific types
|
|
||||||
│ │ │ ├── game_object.rs
|
|
||||||
│ │ │ ├── prefab_instance.rs
|
|
||||||
│ │ │ └── transform.rs
|
|
||||||
│ │ ├── guid.rs # 128-bit GUID type
|
|
||||||
│ │ ├── component.rs # Component trait system
|
|
||||||
│ │ └── ...
|
|
||||||
│ └── lib.rs
|
|
||||||
├── unity-parser-macros/ # Derive macro crate
|
|
||||||
├── examples/ # Usage examples
|
|
||||||
├── tests/ # Integration tests
|
|
||||||
└── test_data/ # Test Unity projects
|
|
||||||
```
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions are welcome! Areas for improvement:
|
Contributions welcome! Areas needing help:
|
||||||
|
|
||||||
|
- **Documentation**: API docs, more examples, tutorials
|
||||||
|
- **Testing**: Integration tests with real Unity projects
|
||||||
- **Performance**: Optimize YAML parsing, parallel processing
|
- **Performance**: Optimize YAML parsing, parallel processing
|
||||||
- **Features**: Additional Unity component types, better error messages
|
|
||||||
- **Testing**: More integration tests with real Unity projects
|
|
||||||
- **Documentation**: API docs, tutorials, cookbook examples
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
@@ -421,11 +397,7 @@ at your option.
|
|||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
- **Unity**: For the YAML-based file format
|
- **Unity Technologies**: For the YAML-based file format
|
||||||
- **Sparsey**: ECS library for component storage
|
- **Sparsey**: ECS library for component storage
|
||||||
- **serde_yaml**: YAML parsing foundation
|
- **serde_yaml**: YAML parsing foundation
|
||||||
- **inventory**: Compile-time component registration
|
- **inventory**: Compile-time component registration
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Built with ❤️ in Rust**
|
|
||||||
|
|||||||
@@ -38,13 +38,10 @@ fn main() {
|
|||||||
let game_objects = prefab.get_documents_by_class("GameObject");
|
let game_objects = prefab.get_documents_by_class("GameObject");
|
||||||
println!("Found {} GameObjects:", game_objects.len());
|
println!("Found {} GameObjects:", game_objects.len());
|
||||||
for go in game_objects {
|
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(mapping) = go.as_mapping() {
|
||||||
if let Some(go_obj) = mapping.get("GameObject") {
|
if let Some(name) = mapping.get("m_Name").and_then(|v| v.as_str()) {
|
||||||
if let Some(props) = go_obj.as_mapping() {
|
println!(" - {}", name);
|
||||||
if let Some(name) = props.get("m_Name").and_then(|v| v.as_str()) {
|
|
||||||
println!(" - {}", name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(())
|
|
||||||
}
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
//! # Example
|
//! # Example
|
||||||
//!
|
//!
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! use cursebreaker_parser::parser::GuidResolver;
|
//! use unity_parser::parser::GuidResolver;
|
||||||
//! use std::path::Path;
|
//! use std::path::Path;
|
||||||
//!
|
//!
|
||||||
//! // Build resolver from Unity project directory
|
//! // Build resolver from Unity project directory
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
//! if let Some(class_name) = resolver.resolve_class_name(guid) {
|
//! if let Some(class_name) = resolver.resolve_class_name(guid) {
|
||||||
//! println!("GUID {} → {}", guid, class_name);
|
//! println!("GUID {} → {}", guid, class_name);
|
||||||
//! }
|
//! }
|
||||||
//! # Ok::<(), cursebreaker_parser::Error>(())
|
//! # Ok::<(), unity_parser::Error>(())
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use crate::parser::meta::MetaFile;
|
use crate::parser::meta::MetaFile;
|
||||||
@@ -71,11 +71,11 @@ impl GuidResolver {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// use cursebreaker_parser::parser::GuidResolver;
|
/// use unity_parser::parser::GuidResolver;
|
||||||
/// use std::path::Path;
|
/// use std::path::Path;
|
||||||
///
|
///
|
||||||
/// let resolver = GuidResolver::from_project(Path::new("MyUnityProject"))?;
|
/// let resolver = GuidResolver::from_project(Path::new("MyUnityProject"))?;
|
||||||
/// # Ok::<(), cursebreaker_parser::Error>(())
|
/// # Ok::<(), unity_parser::Error>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub fn from_project(project_path: impl AsRef<Path>) -> Result<Self> {
|
pub fn from_project(project_path: impl AsRef<Path>) -> Result<Self> {
|
||||||
let project_path = project_path.as_ref();
|
let project_path = project_path.as_ref();
|
||||||
@@ -176,7 +176,7 @@ impl GuidResolver {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// # use cursebreaker_parser::{GuidResolver, Guid};
|
/// # use unity_parser::{GuidResolver, Guid};
|
||||||
/// # use std::path::Path;
|
/// # use std::path::Path;
|
||||||
/// # let resolver = GuidResolver::from_project(Path::new("."))?;
|
/// # let resolver = GuidResolver::from_project(Path::new("."))?;
|
||||||
/// // Resolve by string
|
/// // Resolve by string
|
||||||
@@ -189,7 +189,7 @@ impl GuidResolver {
|
|||||||
/// if let Some(class_name) = resolver.resolve_class_name(&guid) {
|
/// if let Some(class_name) = resolver.resolve_class_name(&guid) {
|
||||||
/// println!("Found class: {}", class_name);
|
/// println!("Found class: {}", class_name);
|
||||||
/// }
|
/// }
|
||||||
/// # Ok::<(), cursebreaker_parser::Error>(())
|
/// # Ok::<(), unity_parser::Error>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub fn resolve_class_name<G: AsGuid>(&self, guid: G) -> Option<&str> {
|
pub fn resolve_class_name<G: AsGuid>(&self, guid: G) -> Option<&str> {
|
||||||
guid.as_guid()
|
guid.as_guid()
|
||||||
@@ -214,7 +214,7 @@ impl GuidResolver {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cursebreaker_parser::{GuidResolver, Guid};
|
/// use unity_parser::{GuidResolver, Guid};
|
||||||
///
|
///
|
||||||
/// let mut resolver = GuidResolver::new();
|
/// let mut resolver = GuidResolver::new();
|
||||||
/// let guid = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
/// let guid = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||||
@@ -347,13 +347,13 @@ fn extract_class_name(cs_path: &Path) -> Result<Option<String>> {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// use cursebreaker_parser::parser::find_project_root;
|
/// use unity_parser::parser::find_project_root;
|
||||||
/// use std::path::Path;
|
/// use std::path::Path;
|
||||||
///
|
///
|
||||||
/// let scene_path = Path::new("MyProject/Assets/Scenes/Main.unity");
|
/// let scene_path = Path::new("MyProject/Assets/Scenes/Main.unity");
|
||||||
/// let project_root = find_project_root(scene_path)?;
|
/// let project_root = find_project_root(scene_path)?;
|
||||||
/// assert_eq!(project_root.file_name().unwrap(), "MyProject");
|
/// assert_eq!(project_root.file_name().unwrap(), "MyProject");
|
||||||
/// # Ok::<(), cursebreaker_parser::Error>(())
|
/// # Ok::<(), unity_parser::Error>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub fn find_project_root(path: impl AsRef<Path>) -> Result<PathBuf> {
|
pub fn find_project_root(path: impl AsRef<Path>) -> Result<PathBuf> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ impl MetaFile {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// use cursebreaker_parser::parser::meta::MetaFile;
|
/// use unity_parser::parser::meta::MetaFile;
|
||||||
///
|
///
|
||||||
/// let meta = MetaFile::from_path("Assets/Prefabs/Player.prefab.meta")?;
|
/// let meta = MetaFile::from_path("Assets/Prefabs/Player.prefab.meta")?;
|
||||||
/// println!("GUID: {}", meta.guid);
|
/// println!("GUID: {}", meta.guid);
|
||||||
/// # Ok::<(), cursebreaker_parser::Error>(())
|
/// # Ok::<(), unity_parser::Error>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
|
pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
@@ -90,7 +90,7 @@ impl MetaFile {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cursebreaker_parser::parser::meta::get_meta_path;
|
/// use unity_parser::parser::meta::get_meta_path;
|
||||||
/// use std::path::PathBuf;
|
/// use std::path::PathBuf;
|
||||||
///
|
///
|
||||||
/// let asset = PathBuf::from("Assets/Scenes/MainMenu.unity");
|
/// let asset = PathBuf::from("Assets/Scenes/MainMenu.unity");
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ use std::path::Path;
|
|||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// use cursebreaker_parser::parser::parse_unity_file;
|
/// use unity_parser::parser::parse_unity_file;
|
||||||
/// use cursebreaker_parser::UnityFile;
|
/// use unity_parser::UnityFile;
|
||||||
/// use std::path::Path;
|
/// use std::path::Path;
|
||||||
///
|
///
|
||||||
/// let file = parse_unity_file(Path::new("Scene.unity"))?;
|
/// let file = parse_unity_file(Path::new("Scene.unity"))?;
|
||||||
@@ -40,7 +40,7 @@ use std::path::Path;
|
|||||||
/// UnityFile::Prefab(prefab) => println!("Prefab with {} documents", prefab.documents.len()),
|
/// UnityFile::Prefab(prefab) => println!("Prefab with {} documents", prefab.documents.len()),
|
||||||
/// UnityFile::Asset(asset) => println!("Asset with {} documents", asset.documents.len()),
|
/// UnityFile::Asset(asset) => println!("Asset with {} documents", asset.documents.len()),
|
||||||
/// }
|
/// }
|
||||||
/// # Ok::<(), cursebreaker_parser::Error>(())
|
/// # Ok::<(), unity_parser::Error>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub fn parse_unity_file(path: &Path) -> Result<UnityFile> {
|
pub fn parse_unity_file(path: &Path) -> Result<UnityFile> {
|
||||||
parse_unity_file_filtered(path, None, None)
|
parse_unity_file_filtered(path, None, None)
|
||||||
@@ -60,8 +60,8 @@ pub fn parse_unity_file(path: &Path) -> Result<UnityFile> {
|
|||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// use cursebreaker_parser::parser::{parse_unity_file_filtered};
|
/// use unity_parser::parser::{parse_unity_file_filtered};
|
||||||
/// use cursebreaker_parser::TypeFilter;
|
/// use unity_parser::TypeFilter;
|
||||||
/// use regex::Regex;
|
/// use regex::Regex;
|
||||||
/// use std::path::Path;
|
/// use std::path::Path;
|
||||||
/// use std::collections::HashSet;
|
/// use std::collections::HashSet;
|
||||||
@@ -76,7 +76,7 @@ pub fn parse_unity_file(path: &Path) -> Result<UnityFile> {
|
|||||||
/// types.insert("GameObject".to_string());
|
/// types.insert("GameObject".to_string());
|
||||||
/// let type_filter = TypeFilter::with_unity_types(types);
|
/// let type_filter = TypeFilter::with_unity_types(types);
|
||||||
/// let file2 = parse_unity_file_filtered(Path::new("Scene.unity"), None, Some(&type_filter))?;
|
/// let file2 = parse_unity_file_filtered(Path::new("Scene.unity"), None, Some(&type_filter))?;
|
||||||
/// # Ok::<(), cursebreaker_parser::Error>(())
|
/// # Ok::<(), unity_parser::Error>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub fn parse_unity_file_filtered(
|
pub fn parse_unity_file_filtered(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
//! # Example
|
//! # Example
|
||||||
//!
|
//!
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! use cursebreaker_parser::parser::PrefabGuidResolver;
|
//! use unity_parser::parser::PrefabGuidResolver;
|
||||||
//! use std::path::Path;
|
//! use std::path::Path;
|
||||||
//!
|
//!
|
||||||
//! // Build resolver from Unity project directory
|
//! // Build resolver from Unity project directory
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
//! if let Some(path) = resolver.resolve_path(guid) {
|
//! if let Some(path) = resolver.resolve_path(guid) {
|
||||||
//! println!("GUID {} → {}", guid, path.display());
|
//! println!("GUID {} → {}", guid, path.display());
|
||||||
//! }
|
//! }
|
||||||
//! # Ok::<(), cursebreaker_parser::Error>(())
|
//! # Ok::<(), unity_parser::Error>(())
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use crate::parser::meta::MetaFile;
|
use crate::parser::meta::MetaFile;
|
||||||
@@ -67,11 +67,11 @@ impl PrefabGuidResolver {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// use cursebreaker_parser::parser::PrefabGuidResolver;
|
/// use unity_parser::parser::PrefabGuidResolver;
|
||||||
/// use std::path::Path;
|
/// use std::path::Path;
|
||||||
///
|
///
|
||||||
/// let resolver = PrefabGuidResolver::from_project(Path::new("MyUnityProject"))?;
|
/// let resolver = PrefabGuidResolver::from_project(Path::new("MyUnityProject"))?;
|
||||||
/// # Ok::<(), cursebreaker_parser::Error>(())
|
/// # Ok::<(), unity_parser::Error>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub fn from_project(project_path: impl AsRef<Path>) -> Result<Self> {
|
pub fn from_project(project_path: impl AsRef<Path>) -> Result<Self> {
|
||||||
let project_path = project_path.as_ref();
|
let project_path = project_path.as_ref();
|
||||||
@@ -155,7 +155,7 @@ impl PrefabGuidResolver {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// # use cursebreaker_parser::{PrefabGuidResolver, Guid};
|
/// # use unity_parser::{PrefabGuidResolver, Guid};
|
||||||
/// # use std::path::Path;
|
/// # use std::path::Path;
|
||||||
/// # let resolver = PrefabGuidResolver::from_project(Path::new("."))?;
|
/// # let resolver = PrefabGuidResolver::from_project(Path::new("."))?;
|
||||||
/// // Resolve by string
|
/// // Resolve by string
|
||||||
@@ -168,7 +168,7 @@ impl PrefabGuidResolver {
|
|||||||
/// if let Some(path) = resolver.resolve_path(&guid) {
|
/// if let Some(path) = resolver.resolve_path(&guid) {
|
||||||
/// println!("Found prefab: {}", path.display());
|
/// println!("Found prefab: {}", path.display());
|
||||||
/// }
|
/// }
|
||||||
/// # Ok::<(), cursebreaker_parser::Error>(())
|
/// # Ok::<(), unity_parser::Error>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub fn resolve_path<G: AsGuid>(&self, guid: G) -> Option<&Path> {
|
pub fn resolve_path<G: AsGuid>(&self, guid: G) -> Option<&Path> {
|
||||||
guid.as_guid()
|
guid.as_guid()
|
||||||
@@ -193,7 +193,7 @@ impl PrefabGuidResolver {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cursebreaker_parser::{PrefabGuidResolver, Guid};
|
/// use unity_parser::{PrefabGuidResolver, Guid};
|
||||||
/// use std::path::PathBuf;
|
/// use std::path::PathBuf;
|
||||||
///
|
///
|
||||||
/// let mut resolver = PrefabGuidResolver::new();
|
/// let mut resolver = PrefabGuidResolver::new();
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ fn unity_tag_regex() -> &'static Regex {
|
|||||||
/// # Example
|
/// # 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 doc = "--- !u!1 &12345\nGameObject:\n m_Name: Test";
|
||||||
/// let tag = parse_unity_tag(doc).unwrap();
|
/// let tag = parse_unity_tag(doc).unwrap();
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use crate::{Error, Result};
|
|||||||
/// # Example
|
/// # 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 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();
|
/// let docs = split_yaml_documents(content).unwrap();
|
||||||
|
|||||||
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,7 @@ impl UnityProject {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// use cursebreaker_parser::UnityProject;
|
/// use unity_parser::UnityProject;
|
||||||
///
|
///
|
||||||
/// let project = UnityProject::new(100);
|
/// let project = UnityProject::new(100);
|
||||||
/// let game_objects = project.find_all_by_type(1); // GameObject = type 1
|
/// let game_objects = project.find_all_by_type(1); // GameObject = type 1
|
||||||
@@ -37,7 +37,7 @@ impl UnityProject {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// use cursebreaker_parser::UnityProject;
|
/// use unity_parser::UnityProject;
|
||||||
///
|
///
|
||||||
/// let project = UnityProject::new(100);
|
/// let project = UnityProject::new(100);
|
||||||
/// let transforms = project.find_all_by_class("Transform");
|
/// let transforms = project.find_all_by_class("Transform");
|
||||||
@@ -61,7 +61,7 @@ impl UnityProject {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// use cursebreaker_parser::UnityProject;
|
/// use unity_parser::UnityProject;
|
||||||
///
|
///
|
||||||
/// let project = UnityProject::new(100);
|
/// let project = UnityProject::new(100);
|
||||||
/// let players = project.find_by_name("Player");
|
/// let players = project.find_by_name("Player");
|
||||||
@@ -97,7 +97,7 @@ impl UnityProject {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// use cursebreaker_parser::UnityProject;
|
/// use unity_parser::UnityProject;
|
||||||
///
|
///
|
||||||
/// let project = UnityProject::new(100);
|
/// let project = UnityProject::new(100);
|
||||||
/// # let game_object = &project.find_all_by_type(1)[0];
|
/// # let game_object = &project.find_all_by_type(1)[0];
|
||||||
@@ -148,7 +148,7 @@ impl UnityProject {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// use cursebreaker_parser::UnityProject;
|
/// use unity_parser::UnityProject;
|
||||||
///
|
///
|
||||||
/// let project = UnityProject::new(100);
|
/// let project = UnityProject::new(100);
|
||||||
/// # let game_object = &project.find_all_by_type(1)[0];
|
/// # let game_object = &project.find_all_by_type(1)[0];
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use std::str::FromStr;
|
|||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cursebreaker_parser::Guid;
|
/// use unity_parser::Guid;
|
||||||
/// use std::str::FromStr;
|
/// use std::str::FromStr;
|
||||||
///
|
///
|
||||||
/// // Parse from string
|
/// // Parse from string
|
||||||
@@ -36,7 +36,7 @@ impl Guid {
|
|||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cursebreaker_parser::Guid;
|
/// use unity_parser::Guid;
|
||||||
///
|
///
|
||||||
/// let guid = Guid::from_u128(0x091c537484687e9419460cdcd7038234);
|
/// let guid = Guid::from_u128(0x091c537484687e9419460cdcd7038234);
|
||||||
/// ```
|
/// ```
|
||||||
@@ -49,7 +49,7 @@ impl Guid {
|
|||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cursebreaker_parser::Guid;
|
/// use unity_parser::Guid;
|
||||||
///
|
///
|
||||||
/// let guid = Guid::from_u128(42);
|
/// let guid = Guid::from_u128(42);
|
||||||
/// assert_eq!(guid.as_u128(), 42);
|
/// assert_eq!(guid.as_u128(), 42);
|
||||||
@@ -69,7 +69,7 @@ impl Guid {
|
|||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cursebreaker_parser::Guid;
|
/// use unity_parser::Guid;
|
||||||
///
|
///
|
||||||
/// let guid = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
/// let guid = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||||
/// ```
|
/// ```
|
||||||
@@ -97,7 +97,7 @@ impl Guid {
|
|||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cursebreaker_parser::Guid;
|
/// use unity_parser::Guid;
|
||||||
/// use std::str::FromStr;
|
/// use std::str::FromStr;
|
||||||
///
|
///
|
||||||
/// let guid = Guid::from_str("091c537484687e9419460cdcd7038234").unwrap();
|
/// let guid = Guid::from_str("091c537484687e9419460cdcd7038234").unwrap();
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::fmt;
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cursebreaker_parser::FileID;
|
/// use unity_parser::FileID;
|
||||||
///
|
///
|
||||||
/// let file_id = FileID::from_i64(1866116814460599870);
|
/// let file_id = FileID::from_i64(1866116814460599870);
|
||||||
/// assert_eq!(file_id.as_i64(), 1866116814460599870);
|
/// assert_eq!(file_id.as_i64(), 1866116814460599870);
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ impl UnityReference {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cursebreaker_parser::types::{UnityReference, FileRef, FileID};
|
/// use unity_parser::types::{UnityReference, FileRef, FileID};
|
||||||
///
|
///
|
||||||
/// // Local reference
|
/// // Local reference
|
||||||
/// let file_ref = FileRef::new(FileID::from_i64(12345));
|
/// let file_ref = FileRef::new(FileID::from_i64(12345));
|
||||||
@@ -63,7 +63,7 @@ impl UnityReference {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cursebreaker_parser::types::{UnityReference, ExternalRef};
|
/// use unity_parser::types::{UnityReference, ExternalRef};
|
||||||
///
|
///
|
||||||
/// let ext_ref = ExternalRef::new("abc123".to_string(), 2);
|
/// let ext_ref = ExternalRef::new("abc123".to_string(), 2);
|
||||||
/// let reference = UnityReference::from_external_ref(&ext_ref);
|
/// let reference = UnityReference::from_external_ref(&ext_ref);
|
||||||
@@ -82,7 +82,7 @@ impl UnityReference {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cursebreaker_parser::types::UnityReference;
|
/// use unity_parser::types::UnityReference;
|
||||||
///
|
///
|
||||||
/// let reference = UnityReference::Null;
|
/// let reference = UnityReference::Null;
|
||||||
/// assert!(reference.is_null());
|
/// assert!(reference.is_null());
|
||||||
@@ -96,7 +96,7 @@ impl UnityReference {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cursebreaker_parser::types::{UnityReference, FileID};
|
/// use unity_parser::types::{UnityReference, FileID};
|
||||||
///
|
///
|
||||||
/// let reference = UnityReference::Local(FileID::from_i64(12345));
|
/// let reference = UnityReference::Local(FileID::from_i64(12345));
|
||||||
/// assert!(reference.is_local());
|
/// assert!(reference.is_local());
|
||||||
@@ -110,7 +110,7 @@ impl UnityReference {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cursebreaker_parser::types::UnityReference;
|
/// use unity_parser::types::UnityReference;
|
||||||
///
|
///
|
||||||
/// let reference = UnityReference::External {
|
/// let reference = UnityReference::External {
|
||||||
/// guid: "abc123".to_string(),
|
/// guid: "abc123".to_string(),
|
||||||
@@ -130,7 +130,7 @@ impl UnityReference {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cursebreaker_parser::types::{UnityReference, FileID};
|
/// use unity_parser::types::{UnityReference, FileID};
|
||||||
///
|
///
|
||||||
/// let reference = UnityReference::Local(FileID::from_i64(12345));
|
/// let reference = UnityReference::Local(FileID::from_i64(12345));
|
||||||
/// assert_eq!(reference.as_file_id(), Some(FileID::from_i64(12345)));
|
/// assert_eq!(reference.as_file_id(), Some(FileID::from_i64(12345)));
|
||||||
@@ -152,7 +152,7 @@ impl UnityReference {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cursebreaker_parser::types::UnityReference;
|
/// use unity_parser::types::UnityReference;
|
||||||
///
|
///
|
||||||
/// let reference = UnityReference::External {
|
/// let reference = UnityReference::External {
|
||||||
/// guid: "abc123".to_string(),
|
/// guid: "abc123".to_string(),
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ impl TypeFilter {
|
|||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```
|
/// ```
|
||||||
/// use cursebreaker_parser::TypeFilter;
|
/// use unity_parser::TypeFilter;
|
||||||
///
|
///
|
||||||
/// let filter = TypeFilter::new(
|
/// let filter = TypeFilter::new(
|
||||||
/// vec!["Transform", "Camera", "Light"],
|
/// vec!["Transform", "Camera", "Light"],
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ pub static UNITY_TYPE_REGISTRY: Lazy<UnityTypeRegistry> = Lazy::new(|| {
|
|||||||
/// # Examples
|
/// # 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(1), Some("GameObject"));
|
||||||
/// assert_eq!(get_class_name(4), Some("Transform"));
|
/// assert_eq!(get_class_name(4), Some("Transform"));
|
||||||
@@ -279,7 +279,7 @@ pub fn get_class_name(type_id: u32) -> Option<&'static str> {
|
|||||||
/// # Examples
|
/// # 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("GameObject"), Some(1));
|
||||||
/// assert_eq!(get_type_id("Transform"), Some(4));
|
/// assert_eq!(get_type_id("Transform"), Some(4));
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ impl crate::types::EcsInsertable for Transform {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// use cursebreaker_parser::{UnityFile, types::RectTransform};
|
/// use unity_parser::{UnityFile, types::RectTransform};
|
||||||
///
|
///
|
||||||
/// let file = UnityFile::from_path("Canvas.prefab")?;
|
/// let file = UnityFile::from_path("Canvas.prefab")?;
|
||||||
/// match file {
|
/// match file {
|
||||||
@@ -152,7 +152,7 @@ impl crate::types::EcsInsertable for Transform {
|
|||||||
/// }
|
/// }
|
||||||
/// _ => {}
|
/// _ => {}
|
||||||
/// }
|
/// }
|
||||||
/// # Ok::<(), cursebreaker_parser::Error>(())
|
/// # Ok::<(), unity_parser::Error>(())
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RectTransform {
|
pub struct RectTransform {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ pub use glam::{Quat as Quaternion, Vec2 as Vector2, Vec3 as Vector3, Vec4 as Col
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cursebreaker_parser::{FileRef, FileID};
|
/// use unity_parser::{FileRef, FileID};
|
||||||
///
|
///
|
||||||
/// let file_ref = FileRef::new(FileID::from_i64(12345));
|
/// let file_ref = FileRef::new(FileID::from_i64(12345));
|
||||||
/// assert_eq!(file_ref.file_id.as_i64(), 12345);
|
/// assert_eq!(file_ref.file_id.as_i64(), 12345);
|
||||||
@@ -32,7 +32,7 @@ impl FileRef {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cursebreaker_parser::ExternalRef;
|
/// use unity_parser::ExternalRef;
|
||||||
///
|
///
|
||||||
/// let ext_ref = ExternalRef::new("abc123".to_string(), 2);
|
/// let ext_ref = ExternalRef::new("abc123".to_string(), 2);
|
||||||
/// assert_eq!(ext_ref.guid, "abc123");
|
/// assert_eq!(ext_ref.guid, "abc123");
|
||||||
|
|||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user